diff options
104 files changed, 1310 insertions, 768 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md new file mode 100644 index 00000000000..b7db5a33faf --- /dev/null +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -0,0 +1,43 @@ +<!-- Title suggestion: [Feature flag] Enable description of feature --> + +## What + +Remove the `:feature_name` feature flag ... + +## Owners + +- Team: NAME_OF_TEAM +- Most appropriate slack channel to reach out to: `#g_TEAM_NAME` +- Best individual to reach out to: NAME + +## Expectations + +### What are we expecting to happen? + +### What might happen if this goes wrong? + +### What can we monitor to detect problems with this? + +<!-- Which dashboards from https://dashboards.gitlab.net are most relevant? --> + +## Beta groups/projects + +If applicable, any groups/projects that are happy to have this feature turned on early. Some organizations may wish to test big changes they are interested in with a small subset of users ahead of time for example. + +- `gitlab-org/gitlab-ce`/`gitlab-org/gitlab-ee` projects +- `gitlab-org`/`gitlab-com` groups +- ... + +## Roll Out Steps + +- [ ] Enable on staging +- [ ] Test on staging +- [ ] Ensure that documentation has been updated +- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour +- [ ] Announce on the issue an estimated time this will be enabled on GitLab.com +- [ ] Enable on GitLab.com by running chatops command in `#production` +- [ ] Cross post chatops slack command to `#support_gitlab-com` and in your team channel +- [ ] Announce on the issue that the flag has been enabled +- [ ] Remove feature flag and add changelog entry + +/label ~"feature flag" diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 17de7b2cf1e..a8516f178fc 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import eventHub from '../eventhub'; import IssueDueDate from './issue_due_date.vue'; import IssueTimeEstimate from './issue_time_estimate.vue'; import boardsStore from '../stores/boards_store'; @@ -136,23 +135,7 @@ export default { const labelTitle = encodeURIComponent(label.title); const filter = `label_name[]=${labelTitle}`; - this.applyFilter(filter); - }, - applyFilter(filter) { - const filterPath = boardsStore.filter.path.split('&'); - const filterIndex = filterPath.indexOf(filter); - - if (filterIndex === -1) { - filterPath.push(filter); - } else { - filterPath.splice(filterIndex, 1); - } - - boardsStore.filter.path = filterPath.join('&'); - - boardsStore.updateFiltersUrl(); - - eventHub.$emit('updateTokens'); + boardsStore.toggleFilter(filter); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 838a4ed63ca..d6718b96f2c 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,7 @@ import Cookies from 'js-cookie'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import eventHub from '../eventhub'; const boardsStore = { disabled: false, @@ -188,6 +189,24 @@ const boardsStore = { findListByLabelId(id) { return this.state.lists.find(list => list.type === 'label' && list.label.id === id); }, + + toggleFilter(filter) { + const filterPath = this.filter.path.split('&'); + const filterIndex = filterPath.indexOf(filter); + + if (filterIndex === -1) { + filterPath.push(filter); + } else { + filterPath.splice(filterIndex, 1); + } + + this.filter.path = filterPath.join('&'); + + this.updateFiltersUrl(); + + eventHub.$emit('updateTokens'); + }, + updateFiltersUrl() { window.history.pushState(null, null, `?${this.filter.path}`); }, diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js index e39452353f5..ce315963723 100644 --- a/app/assets/javascripts/error_tracking_settings/index.js +++ b/app/assets/javascripts/error_tracking_settings/index.js @@ -1,10 +1,8 @@ import Vue from 'vue'; import ErrorTrackingSettings from './components/app.vue'; import createStore from './store'; -import initSettingsPanels from '~/settings_panels'; export default () => { - initSettingsPanels(); const formContainerEl = document.querySelector('.js-error-tracking-form'); const { dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint }, diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 74c4ae64712..3fd9e07fa8b 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -8,8 +8,5 @@ export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE'; export const SET_TIME_WINDOW = 'SET_TIME_WINDOW'; -export const SET_METRICS_ENDPOINT = 'SET_METRICS_ENDPOINT'; -export const SET_ENVIRONMENTS_ENDPOINT = 'SET_ENVIRONMENTS_ENDPOINT'; -export const SET_DEPLOYMENTS_ENDPOINT = 'SET_DEPLOYMENTS_ENDPOINT'; export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue index 0a87d193b72..59251f70337 100644 --- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue +++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue @@ -23,11 +23,12 @@ export default { </script> <template> - <section class="settings expanded"> + <section class="settings no-animate"> <div class="settings-header"> <h4 class="js-section-header"> {{ s__('ExternalMetrics|External Dashboard') }} </h4> + <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button> <p class="js-section-sub-header"> {{ s__( diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js index 5270a7924ec..98e19705976 100644 --- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js @@ -1,7 +1,9 @@ import mountErrorTrackingForm from '~/error_tracking_settings'; import mountOperationSettings from '~/operation_settings'; +import initSettingsPanels from '~/settings_panels'; document.addEventListener('DOMContentLoaded', () => { mountErrorTrackingForm(); mountOperationSettings(); + initSettingsPanels(); }); diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue index f9b4e789563..94341050b86 100644 --- a/app/assets/javascripts/serverless/components/functions.vue +++ b/app/assets/javascripts/serverless/components/functions.vue @@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import FunctionRow from './function_row.vue'; import EnvironmentRow from './environment_row.vue'; import EmptyState from './empty_state.vue'; +import { CHECKING_INSTALLED } from '../constants'; export default { components: { @@ -13,10 +14,6 @@ export default { GlLoadingIcon, }, props: { - installed: { - type: Boolean, - required: true, - }, clustersPath: { type: String, required: true, @@ -31,8 +28,15 @@ export default { }, }, computed: { - ...mapState(['isLoading', 'hasFunctionData']), + ...mapState(['installed', 'isLoading', 'hasFunctionData']), ...mapGetters(['getFunctions']), + + checkingInstalled() { + return this.installed === CHECKING_INSTALLED; + }, + isInstalled() { + return this.installed === true; + }, }, created() { this.fetchFunctions({ @@ -47,15 +51,16 @@ export default { <template> <section id="serverless-functions"> - <div v-if="installed"> + <gl-loading-icon + v-if="checkingInstalled" + :size="2" + class="prepend-top-default append-bottom-default" + /> + + <div v-else-if="isInstalled"> <div v-if="hasFunctionData"> - <gl-loading-icon - v-if="isLoading" - :size="2" - class="prepend-top-default append-bottom-default" - /> - <template v-else> - <div class="groups-list-tree-container"> + <template> + <div class="groups-list-tree-container js-functions-wrapper"> <ul class="content-list group-list-tree"> <environment-row v-for="(env, index) in getFunctions" @@ -66,6 +71,11 @@ export default { </ul> </div> </template> + <gl-loading-icon + v-if="isLoading" + :size="2" + class="prepend-top-default append-bottom-default js-functions-loader" + /> </div> <div v-else class="empty-state js-empty-state"> <div class="text-content"> diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js index 35f77205f2c..2fa15e56ccb 100644 --- a/app/assets/javascripts/serverless/constants.js +++ b/app/assets/javascripts/serverless/constants.js @@ -1,3 +1,7 @@ export const MAX_REQUESTS = 3; // max number of times to retry export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis + +export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed + +export const TIMEOUT = 'timeout'; diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js index 2d3f086ffee..ed3b633d766 100644 --- a/app/assets/javascripts/serverless/serverless_bundle.js +++ b/app/assets/javascripts/serverless/serverless_bundle.js @@ -45,7 +45,7 @@ export default class Serverless { }, }); } else { - const { statusPath, clustersPath, helpPath, installed } = document.querySelector( + const { statusPath, clustersPath, helpPath } = document.querySelector( '.js-serverless-functions-page', ).dataset; @@ -56,7 +56,6 @@ export default class Serverless { render(createElement) { return createElement(Functions, { props: { - installed: installed !== undefined, clustersPath, helpPath, statusPath, diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js index 826501c9022..a0a9fdf7ace 100644 --- a/app/assets/javascripts/serverless/store/actions.js +++ b/app/assets/javascripts/serverless/store/actions.js @@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; import { backOff } from '~/lib/utils/common_utils'; import createFlash from '~/flash'; -import { MAX_REQUESTS } from '../constants'; +import { __ } from '~/locale'; +import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants'; export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING); export const receiveFunctionsSuccess = ({ commit }, data) => commit(types.RECEIVE_FUNCTIONS_SUCCESS, data); -export const receiveFunctionsNoDataSuccess = ({ commit }) => - commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS); +export const receiveFunctionsPartial = ({ commit }, data) => + commit(types.RECEIVE_FUNCTIONS_PARTIAL, data); +export const receiveFunctionsTimeout = ({ commit }, data) => + commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data); +export const receiveFunctionsNoDataSuccess = ({ commit }, data) => + commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data); export const receiveFunctionsError = ({ commit }, error) => commit(types.RECEIVE_FUNCTIONS_ERROR, error); @@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) => export const fetchFunctions = ({ dispatch }, { functionsPath }) => { let retryCount = 0; + const functionsPartiallyFetched = data => { + if (data.functions !== null && data.functions.length) { + dispatch('receiveFunctionsPartial', data); + } + }; + dispatch('requestFunctionsLoading'); backOff((next, stop) => { axios .get(functionsPath) .then(response => { - if (response.status === statusCodes.NO_CONTENT) { + if (response.data.knative_installed === CHECKING_INSTALLED) { retryCount += 1; if (retryCount < MAX_REQUESTS) { + functionsPartiallyFetched(response.data); next(); } else { - stop(null); + stop(TIMEOUT); } } else { stop(response.data); @@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { .catch(stop); }) .then(data => { - if (data !== null) { + if (data === TIMEOUT) { + dispatch('receiveFunctionsTimeout'); + createFlash(__('Loading functions timed out. Please reload the page to try again.')); + } else if (data.functions !== null && data.functions.length) { dispatch('receiveFunctionsSuccess', data); } else { - dispatch('receiveFunctionsNoDataSuccess'); + dispatch('receiveFunctionsNoDataSuccess', data); } }) .catch(error => { diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js index 25b2f7ac38a..b8fa9ea1a01 100644 --- a/app/assets/javascripts/serverless/store/mutation_types.js +++ b/app/assets/javascripts/serverless/store/mutation_types.js @@ -1,5 +1,7 @@ export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING'; export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS'; +export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL'; +export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT'; export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS'; export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR'; diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js index 991f32a275d..2685a5b11ff 100644 --- a/app/assets/javascripts/serverless/store/mutations.js +++ b/app/assets/javascripts/serverless/store/mutations.js @@ -5,12 +5,23 @@ export default { state.isLoading = true; }, [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) { - state.functions = data; + state.functions = data.functions; + state.installed = data.knative_installed; state.isLoading = false; state.hasFunctionData = true; }, - [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) { + [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) { + state.functions = data.functions; + state.installed = true; + state.isLoading = true; + state.hasFunctionData = true; + }, + [types.RECEIVE_FUNCTIONS_TIMEOUT](state) { + state.isLoading = false; + }, + [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) { state.isLoading = false; + state.installed = data.knative_installed; state.hasFunctionData = false; }, [types.RECEIVE_FUNCTIONS_ERROR](state, error) { diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js index afc3f37d7ba..fdd29299749 100644 --- a/app/assets/javascripts/serverless/store/state.js +++ b/app/assets/javascripts/serverless/store/state.js @@ -1,5 +1,6 @@ export default () => ({ error: null, + installed: 'checking', isLoading: true, // functions diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue index 040315b3c66..19a222462b3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue @@ -37,7 +37,7 @@ export default { </script> <template> - <div class="m-3 ml-5" :class="messageClass"> + <div class="m-3 ml-7" :class="messageClass"> <slot></slot> <gl-link v-if="helpPath" :href="helpPath" target="_blank"> <icon :size="16" name="question-o" class="align-middle" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index a3a44dd8e99..83e7d6db9fa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -35,9 +35,7 @@ export default { <status-icon status="warning" /> <div class="media-body space-children"> <span class="bold"> - <template v-if="mr.mergeError" - >{{ mr.mergeError }}.</template - > + <template v-if="mr.mergeError">{{ mr.mergeError }}</template> {{ s__('mrWidget|This merge request failed to be merged automatically') }} </span> <button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 5aa1f0799fc..615d59a7b8e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -330,6 +330,7 @@ export default { :commits-count="mr.commitsCount" :target-branch="mr.targetBranch" :is-fast-forward-enabled="mr.ffOnlyEnabled" + :class="{ 'border-bottom': mr.mergeError }" > <ul class="border-top content-list commits-list flex-list"> <commit-edit diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index b1f5655a15a..accb9d9fef1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -29,8 +29,8 @@ export default { </script> <template> - <div class="accept-control inline"> - <label class="merge-param-checkbox"> + <div class="inline"> + <label> <input :checked="value" :disabled="isDisabled" diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index eef2667e141..d02bb2f341d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,6 +1,6 @@ <script> import _ from 'underscore'; -import { __ } from '~/locale'; +import { sprintf, s__, __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store'; @@ -125,6 +125,11 @@ export default { this.mr.pipeline.target_sha !== this.mr.targetBranchSha, ); }, + mergeError() { + return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), { + mergeError: this.mr.mergeError, + }); + }, }, watch: { state(newVal, oldVal) { @@ -370,6 +375,10 @@ export default { }} </mr-widget-alert-message> + <mr-widget-alert-message v-if="mr.mergeError" type="danger"> + {{ mergeError }} + </mr-widget-alert-message> + <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" /> </div> </div> diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 93377b8dd91..7f6384f4eea 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -22,7 +22,9 @@ body, .form-control, .search form { // Override default font size used in non-csslab UI - font-size: 14px; + // Use rem to keep default font-size at 14px on body so 1rem still + // fits 8px grid, but also allow users to change browser font size + font-size: .875rem; } legend { diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss index 658e0ff638e..8c32b6c8985 100644 --- a/app/assets/stylesheets/errors.scss +++ b/app/assets/stylesheets/errors.scss @@ -17,7 +17,7 @@ body { text-align: center; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; margin: auto; - font-size: 14px; + font-size: .875rem; } h1 { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ab5a9e170f0..77b40fe2d30 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -185,46 +185,6 @@ } } } - - .accept-control { - display: inline-block; - float: left; - margin: 0; - margin-left: 20px; - padding: 5px; - padding-top: 8px; - line-height: 20px; - - &.right { - float: right; - padding-right: 0; - } - - .modify-merge-commit-link { - padding: 0; - background-color: transparent; - border: 0; - color: $gl-text-color; - - &:hover, - &:focus { - text-decoration: underline; - } - } - - .merge-param-checkbox { - margin: 0; - } - - a .fa-question-circle { - color: $gl-text-color-secondary; - - &:hover, - &:focus { - color: $link-hover-color; - } - } - } } .ci-widget { @@ -407,12 +367,6 @@ width: 100%; text-align: center; } - - .accept-control { - width: 100%; - text-align: center; - margin: 0; - } } .commit-message-editor { diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb index 79030da64d3..4b0d001fca6 100644 --- a/app/controllers/projects/serverless/functions_controller.rb +++ b/app/controllers/projects/serverless/functions_controller.rb @@ -10,15 +10,13 @@ module Projects format.json do functions = finder.execute - if functions.any? - render json: serialize_function(functions) - else - head :no_content - end + render json: { + knative_installed: finder.knative_installed, + functions: serialize_function(functions) + }.to_json end format.html do - @installed = finder.installed? render end end diff --git a/app/finders/clusters/knative_services_finder.rb b/app/finders/clusters/knative_services_finder.rb new file mode 100644 index 00000000000..7d3b53ef663 --- /dev/null +++ b/app/finders/clusters/knative_services_finder.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true +module Clusters + class KnativeServicesFinder + include ReactiveCaching + include Gitlab::Utils::StrongMemoize + + KNATIVE_STATES = { + 'checking' => 'checking', + 'installed' => 'installed', + 'not_found' => 'not_found' + }.freeze + + self.reactive_cache_key = ->(finder) { finder.model_name } + self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) } + + attr_reader :cluster, :project + + def initialize(cluster, project) + @cluster = cluster + @project = project + end + + def with_reactive_cache_memoized(*cache_args, &block) + strong_memoize(:reactive_cache) do + with_reactive_cache(*cache_args, &block) + end + end + + def clear_cache! + clear_reactive_cache!(*cache_args) + end + + def self.from_cache(cluster_id, project_id) + cluster = Clusters::Cluster.find(cluster_id) + project = ::Project.find(project_id) + + new(cluster, project) + end + + def calculate_reactive_cache(*) + # read_services calls knative_client.discover implicitily. If we stop + # detecting services but still want to detect knative, we'll need to + # explicitily call: knative_client.discover + # + # We didn't create it separately to avoid 2 cluster requests. + ksvc = read_services + pods = knative_client.discovered ? read_pods : [] + { services: ksvc, pods: pods, knative_detected: knative_client.discovered } + end + + def services + return [] unless search_namespace + + cached_data = with_reactive_cache_memoized(*cache_args) { |data| data } + cached_data.to_h.fetch(:services, []) + end + + def cache_args + [cluster.id, project.id] + end + + def service_pod_details(service) + cached_data = with_reactive_cache_memoized(*cache_args) { |data| data } + cached_data.to_h.fetch(:pods, []).select do |pod| + filter_pods(pod, service) + end + end + + def knative_detected + cached_data = with_reactive_cache_memoized(*cache_args) { |data| data } + + knative_state = cached_data.to_h[:knative_detected] + + return KNATIVE_STATES['checking'] if knative_state.nil? + return KNATIVE_STATES['installed'] if knative_state + + KNATIVE_STATES['uninstalled'] + end + + def model_name + self.class.name.underscore.tr('/', '_') + end + + private + + def search_namespace + @search_namespace ||= cluster.kubernetes_namespace_for(project) + end + + def knative_client + cluster.kubeclient.knative_client + end + + def filter_pods(pod, service) + pod["metadata"]["labels"]["serving.knative.dev/service"] == service + end + + def read_services + knative_client.get_services(namespace: search_namespace).as_json + rescue Kubeclient::ResourceNotFoundError + [] + end + + def read_pods + cluster.kubeclient.core_client.get_pods(namespace: search_namespace).as_json + end + + def id + nil + end + end +end diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb index e5bffccabfe..ebe50806ca1 100644 --- a/app/finders/projects/serverless/functions_finder.rb +++ b/app/finders/projects/serverless/functions_finder.rb @@ -14,8 +14,16 @@ module Projects knative_services.flatten.compact end - def installed? - clusters_with_knative_installed.exists? + # Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE + def knative_installed + states = @clusters.map do |cluster| + cluster.application_knative + cluster.knative_services_finder(project).knative_detected.tap do |state| + return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks + end + end + + states.any? { |state| state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['installed'] } end def service(environment_scope, name) @@ -25,7 +33,7 @@ module Projects def invocation_metrics(environment_scope, name) return unless prometheus_adapter&.can_query? - cluster = clusters_with_knative_installed.preload_knative.find do |c| + cluster = @clusters.find do |c| environment_scope == c.environment_scope end @@ -34,7 +42,7 @@ module Projects end def has_prometheus?(environment_scope) - clusters_with_knative_installed.preload_knative.to_a.any? do |cluster| + @clusters.any? do |cluster| environment_scope == cluster.environment_scope && cluster.application_prometheus_available? end end @@ -42,10 +50,12 @@ module Projects private def knative_service(environment_scope, name) - clusters_with_knative_installed.preload_knative.map do |cluster| + @clusters.map do |cluster| next if environment_scope != cluster.environment_scope - services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project)) + services = cluster + .knative_services_finder(project) + .services .select { |svc| svc["metadata"]["name"] == name } add_metadata(cluster, services).first unless services.nil? @@ -53,8 +63,11 @@ module Projects end def knative_services - clusters_with_knative_installed.preload_knative.map do |cluster| - services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project)) + @clusters.map do |cluster| + services = cluster + .knative_services_finder(project) + .services + add_metadata(cluster, services) unless services.nil? end end @@ -65,17 +78,14 @@ module Projects s["cluster_id"] = cluster.id if services.length == 1 - s["podcount"] = cluster.application_knative.service_pod_details( - cluster.kubernetes_namespace_for(project), - s["metadata"]["name"]).length + s["podcount"] = cluster + .knative_services_finder(project) + .service_pod_details(s["metadata"]["name"]) + .length end end end - def clusters_with_knative_installed - @clusters.with_knative_installed - end - # rubocop: disable CodeReuse/ServiceClass def prometheus_adapter @prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb index 35ae23c21fc..62537361918 100644 --- a/app/graphql/types/project_statistics_type.rb +++ b/app/graphql/types/project_statistics_type.rb @@ -11,5 +11,6 @@ module Types field :lfs_objects_size, GraphQL::INT_TYPE, null: false field :build_artifacts_size, GraphQL::INT_TYPE, null: false field :packages_size, GraphQL::INT_TYPE, null: false + field :wiki_size, GraphQL::INT_TYPE, null: true end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index dc0e5511fcf..2beb081ab77 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -98,16 +98,17 @@ module EmailsHelper case format when :html - " via merge request #{link_to(merge_request.to_reference, merge_request.web_url)}" + merge_request_link = link_to(merge_request.to_reference, merge_request.web_url) + _("via merge request %{link}").html_safe % { link: merge_request_link } else # If it's not HTML nor text then assume it's text to be safe - " via merge request #{merge_request.to_reference} (#{merge_request.web_url})" + _("via merge request %{link}") % { link: "#{merge_request.to_reference} (#{merge_request.web_url})" } end when String # Technically speaking this should be Commit but per # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339 # we can't deserialize Commit without custom serializer for ActiveJob - " via #{closed_via}" + _("via %{closed_via}") % { closed_via: closed_via } else "" end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 80401ca0a1e..3727a9861aa 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -166,6 +166,16 @@ module Ci end end + after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| + pipeline.run_after_commit do + pipeline.all_merge_requests.each do |merge_request| + next unless merge_request.auto_merge_enabled? + + AutoMergeProcessWorker.perform_async(merge_request.id) + end + end + end + after_transition any => [:success, :failed] do |pipeline| pipeline.run_after_commit do PipelineNotificationWorker.perform_async(pipeline.id) diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index c0a0ca9acf6..c40ad39be61 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -27,9 +27,13 @@ module Ci scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } + scope :runnable_schedules, -> { active.where("next_run_at < ?", Time.now) } + scope :preloaded, -> { preload(:owner, :project) } accepts_nested_attributes_for :variables, allow_destroy: true + alias_attribute :real_next_run, :next_run_at + def owned_by?(current_user) owner == current_user end @@ -46,8 +50,14 @@ module Ci update_attribute(:active, false) end + ## + # The `next_run_at` column is set to the actual execution date of `PipelineScheduleWorker`. + # This way, a schedule like `*/1 * * * *` won't be triggered in a short interval + # when PipelineScheduleWorker runs irregularly by Sidekiq Memory Killer. def set_next_run_at - self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) + self.next_run_at = Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], + Time.zone.name) + .next_time_from(ideal_next_run_at) end def schedule_next_run! @@ -56,15 +66,14 @@ module Ci update_attribute(:next_run_at, nil) # update without validation end - def real_next_run( - worker_cron: Settings.cron_jobs['pipeline_schedule_worker']['cron'], - worker_time_zone: Time.zone.name) - Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone) - .next_time_from(next_run_at) - end - def job_variables variables&.map(&:to_runner_variable) || [] end + + private + + def ideal_next_run_at + Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) + end end end diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 9fbf5d8af04..d5a3bd62e3d 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -15,9 +15,6 @@ module Clusters include ::Clusters::Concerns::ApplicationVersion include ::Clusters::Concerns::ApplicationData include AfterCommitQueue - include ReactiveCaching - - self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] } def set_initial_status return unless not_installable? @@ -41,8 +38,6 @@ module Clusters scope :for_cluster, -> (cluster) { where(cluster: cluster) } - after_save :clear_reactive_cache! - def chart 'knative/knative' end @@ -77,55 +72,12 @@ module Clusters ClusterWaitForIngressIpAddressWorker.perform_async(name, id) end - def client - cluster.kubeclient.knative_client - end - - def services - with_reactive_cache do |data| - data[:services] - end - end - - def calculate_reactive_cache - { services: read_services, pods: read_pods } - end - def ingress_service cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system') end - def services_for(ns: namespace) - return [] unless services - return [] unless ns - - services.select do |service| - service.dig('metadata', 'namespace') == ns - end - end - - def service_pod_details(ns, service) - with_reactive_cache do |data| - data[:pods].select { |pod| filter_pods(pod, ns, service) } - end - end - private - def read_pods - cluster.kubeclient.core_client.get_pods.as_json - end - - def filter_pods(pod, namespace, service) - pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service - end - - def read_services - client.get_services.as_json - rescue Kubeclient::ResourceNotFoundError - [] - end - def install_knative_metrics ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available? end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 57a1e461b2d..e1d6b2a802b 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -223,6 +223,10 @@ module Clusters end end + def knative_services_finder(project) + @knative_services_finder ||= KnativeServicesFinder.new(self, project) + end + private def instance_domain diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index bfd0c36942b..4b428b0af83 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -3,14 +3,16 @@ module Noteable extend ActiveSupport::Concern - # `Noteable` class names that support resolvable notes. - RESOLVABLE_TYPES = %w(MergeRequest).freeze - class_methods do # `Noteable` class names that support replying to individual notes. def replyable_types %w(Issue MergeRequest) end + + # `Noteable` class names that support resolvable notes. + def resolvable_types + %w(MergeRequest) + end end # The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via @@ -36,7 +38,7 @@ module Noteable end def supports_resolvable_notes? - RESOLVABLE_TYPES.include?(base_class_name) + self.class.resolvable_types.include?(base_class_name) end def supports_discussions? @@ -131,3 +133,5 @@ module Noteable ) end end + +Noteable.extend(Noteable::ClassMethods) diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 16ea330701d..2d2d5fb7168 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -12,7 +12,7 @@ module ResolvableNote validates :resolved_by, presence: true, if: :resolved? # Keep this scope in sync with `#potentially_resolvable?` - scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable::RESOLVABLE_TYPES) } + scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable.resolvable_types) } # Keep this scope in sync with `#resolvable?` scope :resolvable, -> { potentially_resolvable.user } diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index feabea9b8ba..1a87fc47c56 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -15,7 +15,9 @@ class DiffNote < Note validates :original_position, presence: true validates :position, presence: true validates :line_code, presence: true, line_code: true, if: :on_text? - validates :noteable_type, inclusion: { in: noteable_types } + # We need to evaluate the `noteable` types when running the validation since + # EE might have added a type when the module was prepended + validates :noteable_type, inclusion: { in: -> (_note) { noteable_types } } validate :positions_complete validate :verify_supported validate :diff_refs_match_commit, if: :for_commit? @@ -44,7 +46,7 @@ class DiffNote < Note # Returns the diff file from `position` def latest_diff_file strong_memoize(:latest_diff_file) do - position.diff_file(project.repository) + position.diff_file(repository) end end @@ -111,7 +113,7 @@ class DiffNote < Note if note_diff_file diff = Gitlab::Git::Diff.new(note_diff_file.to_hash) Gitlab::Diff::File.new(diff, - repository: project.repository, + repository: repository, diff_refs: original_position.diff_refs) elsif created_at_diff?(noteable.diff_refs) # We're able to use the already persisted diffs (Postgres) if we're @@ -122,7 +124,7 @@ class DiffNote < Note # `Diff::FileCollection::MergeRequestDiff`. noteable.diffs(original_position.diff_options).diff_files.first else - original_position.diff_file(self.project.repository) + original_position.diff_file(repository) end # Since persisted diff files already have its content "unfolded" @@ -137,7 +139,7 @@ class DiffNote < Note end def set_line_code - self.line_code = self.position.line_code(self.project.repository) + self.line_code = self.position.line_code(repository) end def verify_supported @@ -171,6 +173,10 @@ class DiffNote < Note shas << self.position.head_sha end - project.repository.keep_around(*shas) + repository.keep_around(*shas) + end + + def repository + noteable.respond_to?(:repository) ? noteable.repository : project.repository end end diff --git a/app/models/key.rb b/app/models/key.rb index b097be8cc89..8aa25924c28 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -59,6 +59,11 @@ class Key < ApplicationRecord "key-#{id}" end + # EE overrides this + def can_delete? + true + end + # rubocop: disable CodeReuse/ServiceClass def update_last_used_at Keys::LastUsedService.new(self).execute diff --git a/app/services/ci/pipeline_schedule_service.rb b/app/services/ci/pipeline_schedule_service.rb new file mode 100644 index 00000000000..387d0351490 --- /dev/null +++ b/app/services/ci/pipeline_schedule_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ci + class PipelineScheduleService < BaseService + def execute(schedule) + # Ensure `next_run_at` is set properly before creating a pipeline. + # Otherwise, multiple pipelines could be created in a short interval. + schedule.schedule_next_run! + + RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner.id) + end + end +end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index 2a19e57a94f..805721212ba 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -29,7 +29,7 @@ module Issues event_service.close_issue(issue, current_user) create_note(issue, closed_via) if system_note - closed_via = "commit #{closed_via.id}" if closed_via.is_a?(Commit) + closed_via = _("commit %{commit_id}") % { commit_id: closed_via.id } if closed_via.is_a?(Commit) notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications todo_service.close_issue(issue, current_user) diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index e77051bb1c9..b0f6166ea1c 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -18,6 +18,7 @@ module MergeRequests invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches cleanup_environments(merge_request) + cancel_auto_merge(merge_request) end merge_request @@ -33,5 +34,9 @@ module MergeRequests merge_request_metrics_service(merge_request).close(close_event) end end + + def cancel_auto_merge(merge_request) + AutoMergeService.new(project, current_user).cancel(merge_request) + end end end diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml index f21cf1ad34b..d3733ab3a09 100644 --- a/app/views/notify/closed_issue_email.html.haml +++ b/app/views/notify/closed_issue_email.html.haml @@ -1,2 +1,2 @@ %p - Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}. + = _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) } diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml index 5567adc9165..ff2548a4b42 100644 --- a/app/views/notify/closed_issue_email.text.haml +++ b/app/views/notify/closed_issue_email.text.haml @@ -1,3 +1,3 @@ -Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}. += _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) } Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)} diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 47494fc3f06..b9d73d89334 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -18,6 +18,7 @@ .float-right %span.key-created-at = s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)} - = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do - %span.sr-only= _('Remove') - = icon('trash') + - if key.can_delete? + = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do + %span.sr-only= _('Remove') + = icon('trash') diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index dcdb7fc63b1..0ef01dec493 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -24,4 +24,5 @@ = @key.key .col-md-12 .float-right - = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button" + - if @key.can_delete? + = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button" diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 2c46efdc124..3a5adb34ad1 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -158,7 +158,7 @@ %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } = _('Move issue') - .dropdown-menu.dropdown-menu-selectable + .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height = dropdown_title(_('Move issue')) = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search') = dropdown_content diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index e4e85de93da..fd0cc5fb24e 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1,6 +1,8 @@ --- - auto_devops:auto_devops_disable +- auto_merge:auto_merge_process + - cronjob:admin_email - cronjob:expire_build_artifacts - cronjob:gitlab_usage_ping diff --git a/app/workers/auto_merge_process_worker.rb b/app/workers/auto_merge_process_worker.rb new file mode 100644 index 00000000000..cd81cdbc60c --- /dev/null +++ b/app/workers/auto_merge_process_worker.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AutoMergeProcessWorker + include ApplicationWorker + + queue_namespace :auto_merge + + def perform(merge_request_id) + MergeRequest.find_by_id(merge_request_id).try do |merge_request| + AutoMergeService.new(merge_request.project, merge_request.merge_user) + .process(merge_request) + end + end +end diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index 8a9ee7808e4..9410fd1a786 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -3,47 +3,12 @@ class PipelineScheduleWorker include ApplicationWorker include CronjobQueue - include ::Gitlab::ExclusiveLeaseHelpers - EXCLUSIVE_LOCK_KEY = 'pipeline_schedules:run:lock' - LOCK_TIMEOUT = 50.minutes - - # rubocop: disable CodeReuse/ActiveRecord def perform - in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do - Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) - .preload(:owner, :project).find_each do |schedule| - - schedule.schedule_next_run! - - Ci::CreatePipelineService.new(schedule.project, - schedule.owner, - ref: schedule.ref) - .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule) - rescue => e - error(schedule, e) + Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules| + schedules.each do |schedule| + Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule) end end end - # rubocop: enable CodeReuse/ActiveRecord - - private - - def error(schedule, error) - failed_creation_counter.increment - - Rails.logger.error "Failed to create a scheduled pipeline. " \ - "schedule_id: #{schedule.id} message: #{error.message}" - - Gitlab::Sentry - .track_exception(error, - issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', - extra: { schedule_id: schedule.id }) - end - - def failed_creation_counter - @failed_creation_counter ||= - Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total, - "Counter of failed attempts of pipeline schedule creation") - end end diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index ce6c88c85c1..666331e6cd4 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -6,14 +6,7 @@ class PipelineSuccessWorker queue_namespace :pipeline_processing - # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) - Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| - pipeline.all_merge_requests.preload(:merge_user).each do |merge_request| - AutoMergeService.new(pipeline.project, merge_request.merge_user) - .process(merge_request) - end - end + # no-op end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb index f72331c003a..43e0b9db22f 100644 --- a/app/workers/run_pipeline_schedule_worker.rb +++ b/app/workers/run_pipeline_schedule_worker.rb @@ -21,6 +21,30 @@ class RunPipelineScheduleWorker Ci::CreatePipelineService.new(schedule.project, user, ref: schedule.ref) - .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule) + .execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule) + rescue Ci::CreatePipelineService::CreateError + # no-op. This is a user operation error such as corrupted .gitlab-ci.yml. + rescue => e + error(schedule, e) + end + + private + + def error(schedule, error) + failed_creation_counter.increment + + Rails.logger.error "Failed to create a scheduled pipeline. " \ + "schedule_id: #{schedule.id} message: #{error.message}" + + Gitlab::Sentry + .track_exception(error, + issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', + extra: { schedule_id: schedule.id }) + end + + def failed_creation_counter + @failed_creation_counter ||= + Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total, + "Counter of failed attempts of pipeline schedule creation") end end diff --git a/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml b/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml new file mode 100644 index 00000000000..a2de6cd6d45 --- /dev/null +++ b/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Fix Merge Request merge checkbox alignment on mobile view +merge_request: 28845 +author: +type: fixed diff --git a/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml new file mode 100644 index 00000000000..53be008816d --- /dev/null +++ b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml @@ -0,0 +1,5 @@ +--- +title: Enable function features for external Knative installations +merge_request: 27173 +author: +type: changed diff --git a/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml b/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml new file mode 100644 index 00000000000..50b3efba0a5 --- /dev/null +++ b/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml @@ -0,0 +1,5 @@ +--- +title: I18n for issue closure reason in emails +merge_request: 28489 +author: Michał Zając +type: changed diff --git a/changelogs/unreleased/ac-graphql-wikisize.yml b/changelogs/unreleased/ac-graphql-wikisize.yml new file mode 100644 index 00000000000..be9c347ec21 --- /dev/null +++ b/changelogs/unreleased/ac-graphql-wikisize.yml @@ -0,0 +1,5 @@ +--- +title: Expose wiki_size on GraphQL API +merge_request: 29123 +author: +type: added diff --git a/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml b/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml new file mode 100644 index 00000000000..d38046ebcbf --- /dev/null +++ b/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml @@ -0,0 +1,5 @@ +--- +title: Cancel auto merge when merge request is closed +merge_request: 28782 +author: +type: fixed diff --git a/changelogs/unreleased/increase-move-issue-dropdown-height.yml b/changelogs/unreleased/increase-move-issue-dropdown-height.yml new file mode 100644 index 00000000000..bb67e9341b2 --- /dev/null +++ b/changelogs/unreleased/increase-move-issue-dropdown-height.yml @@ -0,0 +1,5 @@ +--- +title: Increase height of move issue dropdown +merge_request: +author: +type: other diff --git a/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml b/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml new file mode 100644 index 00000000000..04eb035b157 --- /dev/null +++ b/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml @@ -0,0 +1,5 @@ +--- +title: Make pipeline schedule worker resilient +merge_request: 28407 +author: +type: performance diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0615da2d241..fd9ce4d3374 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -30,6 +30,7 @@ - [pipeline_default, 3] - [pipeline_cache, 3] - [deployment, 3] + - [auto_merge, 3] - [pipeline_hooks, 2] - [gitlab_shell, 2] - [email_receiver, 2] diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index f1cedb85455..dcf8d8715ca 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -53,6 +53,10 @@ But since 11.8 the indexer uses Gitaly for data access as well. NFS can still be leveraged for redudancy on block level of the Git data. But only has to be mounted on the Gitaly server. +NOTE: **Note:** While Gitaly can be used as a replacement for NFS, we do not recommend +using EFS as it may impact GitLab's performance. Please review the [relevant documentation](../high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) +for more details. + ### Network architecture - gitlab-rails shards repositories into "repository storages" @@ -73,18 +77,29 @@ be mounted on the Gitaly server. - Gitaly servers must not be exposed to the public internet Gitaly network traffic is unencrypted by default, but supports -[TLS](#tls-support). Authentication is done through a static token. For -security in depth, its recommended to use a firewall to restrict access -to your Gitaly server. +[TLS](#tls-support). Authentication is done through a static token. + +NOTE: **Note:** Gitaly network traffic is unencrypted so we recommend a firewall to +restrict access to your Gitaly server. Below we describe how to configure a Gitaly server at address `gitaly.internal:8075` with secret token `abc123secret`. We assume your GitLab installation has two repository storages, `default` and `storage1`. +### Installation + +First install Gitaly using either Omnibus or from source. + +Omnibus: [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab +package you want using **steps 1 and 2** from the GitLab downloads page but +**_do not_** provide the `EXTERNAL_URL=` value. + +Source: [Install Gitaly](../../install/installation.md#install-gitaly) + ### Client side token configuration -Start by configuring a token on the client side. +Configure a token on the client side. Omnibus installations: @@ -110,7 +125,7 @@ changes to be picked up. Next, on the Gitaly server, we need to configure storage paths, enable the network listener and configure the token. -Note: if you want to reduce the risk of downtime when you enable +NOTE: **Note:** if you want to reduce the risk of downtime when you enable authentication you can temporarily disable enforcement, see [the documentation on configuring Gitaly authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication) @@ -122,12 +137,17 @@ the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gi from an existing GitLab server to the Gitaly server. Without this shared secret, Git operations in GitLab will result in an API error. -> **NOTE:** In most or all cases the storage paths below end in `/repositories` which is +NOTE: **Note:** In most or all cases the storage paths below end in `/repositories` which is different than `path` in `git_data_dirs` of Omnibus installations. Check the directory layout on your Gitaly server to be sure. Omnibus installations: +<!-- +updates to following example must also be made at +https://gitlab.com/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab +--> + ```ruby # /etc/gitlab/gitlab.rb @@ -147,6 +167,7 @@ gitlab_rails['auto_migrate'] = false # Configure the gitlab-shell API callback URL. Without this, `git push` will # fail. This can be your 'front door' GitLab URL or an internal load # balancer. +# Don't forget to copy `/etc/gitlab/gitlab-secrets.json` from web server to Gitaly server. gitlab_rails['internal_api_url'] = 'https://gitlab.example.com' # Make Gitaly accept connections on all network interfaces. You must use diff --git a/doc/administration/high_availability/gitaly.md b/doc/administration/high_availability/gitaly.md index d44744f2af8..40f85f28cb8 100644 --- a/doc/administration/high_availability/gitaly.md +++ b/doc/administration/high_availability/gitaly.md @@ -12,77 +12,8 @@ environments and [High Availability Architecture](./README.md#high-availability- ## Running Gitaly on its own server -Starting with GitLab 11.4, Gitaly is a replacement for NFS except -when the [Elastic Search indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer) -is used. - -NOTE: **Note:** While Gitaly can be used as a replacement for NFS, we do not recommend using EFS as it may impact GitLab's performance. Please review the [relevant documentation](nfs.md#avoid-using-awss-elastic-file-system-efs) for more details. - -NOTE: **Note:** Gitaly network traffic is unencrypted so we recommend a firewall to -restrict access to your Gitaly server. - -The steps below are the minimum necessary to configure a Gitaly server with -Omnibus: - -1. SSH into the Gitaly server. -1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab - package you want using **steps 1 and 2** from the GitLab downloads page. - - Do not complete any other steps on the download page. - -1. Edit `/etc/gitlab/gitlab.rb` and add the contents: - - Gitaly must trigger some callbacks to GitLab via GitLab Shell. As a result, - the GitLab Shell secret must be the same between the other GitLab servers and - the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gitlab-secrets.json` - from an existing GitLab server to the Gitaly server. Without this shared secret, - Git operations in GitLab will result in an API error. - - > **NOTE:** In most or all cases the storage paths below end in `repositories` which is - different than `path` in `git_data_dirs` of Omnibus installations. Check the - directory layout on your Gitaly server to be sure. - - ```ruby - # Enable Gitaly - gitaly['enable'] = true - - ## Disable all other services - sidekiq['enable'] = false - gitlab_workhorse['enable'] = false - unicorn['enable'] = false - postgresql['enable'] = false - nginx['enable'] = false - prometheus['enable'] = false - alertmanager['enable'] = false - pgbouncer_exporter['enable'] = false - redis_exporter['enable'] = false - gitlab_monitor['enable'] = false - - # Prevent database connections during 'gitlab-ctl reconfigure' - gitlab_rails['rake_cache_clear'] = false - gitlab_rails['auto_migrate'] = false - - # Configure the gitlab-shell API callback URL. Without this, `git push` will - # fail. This can be your 'front door' GitLab URL or an internal load - # balancer. - gitlab_rails['internal_api_url'] = 'https://gitlab.example.com' - - # Make Gitaly accept connections on all network interfaces. You must use - # firewalls to restrict access to this address/port. - gitaly['listen_addr'] = "0.0.0.0:8075" - gitaly['auth_token'] = 'abc123secret' - - gitaly['storage'] = [ - { 'name' => 'default', 'path' => '/mnt/gitlab/default/repositories' }, - { 'name' => 'storage1', 'path' => '/mnt/gitlab/storage1/repositories' }, - ] - - # To use tls for gitaly you need to add - gitaly['tls_listen_addr'] = "0.0.0.0:9999" - gitaly['certificate_path'] = "path/to/cert.pem" - gitaly['key_path'] = "path/to/key.pem" - ``` - -Again, reconfigure (Omnibus) or restart (source). +See [Running Gitaly on its own server](../gitaly/index.md#running-gitaly-on-its-own-server) +in Gitaly documentation. Continue configuration of other components by going back to: diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 2596e3fe68b..c34858cd0db 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -43,6 +43,11 @@ detail below. ## Enabling and disabling terminal support +NOTE: **Note:** AWS Elastic Load Balancers (ELBs) do not support web sockets. +AWS Application Load Balancers (ALBs) must be used if you want web terminals +to work. See [AWS Elastic Load Balancing Product Comparison](https://aws.amazon.com/elasticloadbalancing/features/#compare) +for more information. + As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers through to the next one in the chain. If you installed GitLab using Omnibus, or diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 02370bead00..11bcfd5dc2c 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -4,10 +4,15 @@ type: reference # Getting started with GitLab CI/CD ->**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI) +NOTE: **Note:** +Starting from version 8.0, GitLab [Continuous Integration][ci] (CI) is fully integrated into GitLab itself and is [enabled] by default on all projects. +NOTE: **Note:** +Please keep in mind that only project Maintainers and Admin users have +the permissions to access a project's settings. + GitLab offers a [continuous integration][ci] service. If you [add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository, and configure your GitLab project to use a [Runner], then each commit or @@ -44,6 +49,7 @@ This guide assumes that you have: - A working GitLab instance of version 8.0+r or are using [GitLab.com](https://gitlab.com). - A project in GitLab that you would like to use CI for. +- Maintainer or owner access to the project Let's break it down to pieces and work on solving the GitLab CI puzzle. @@ -77,6 +83,8 @@ You need to create a file named `.gitlab-ci.yml` in the root directory of your repository. Below is an example for a Ruby on Rails project. ```yaml +image: "ruby:2.5" + before_script: - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs - ruby -v diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md index da36a3218e5..c926f0b4888 100644 --- a/doc/university/training/topics/unstage.md +++ b/doc/university/training/topics/unstage.md @@ -8,13 +8,13 @@ comments: false ## Unstage -- To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch. +- To remove files from stage use reset HEAD where HEAD is the last commit of the current branch. This will unstage the file but maintain the modifications. ```bash git reset HEAD <file> ``` -- This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use: +- To revert the file back to the state it was in before the changes we can use: ```bash git checkout -- <file> diff --git a/doc/user/admin_area/settings/third_party_offers.md b/doc/user/admin_area/settings/third_party_offers.md index 23311801790..d3c9cf7d8ff 100644 --- a/doc/user/admin_area/settings/third_party_offers.md +++ b/doc/user/admin_area/settings/third_party_offers.md @@ -1,9 +1,26 @@ +--- +type: reference +--- + # Third party offers > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20379) > in [GitLab Core](https://about.gitlab.com/pricing/) 11.1 -Within GitLab, we inform users of available third-party offers they might find valuable in order to enhance the development of their projects. -An example is the Google Cloud Platform free credit for using [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/). +Within GitLab, we inform users of available third-party offers they might find valuable in order +to enhance the development of their projects. An example is the Google Cloud Platform free credit +for using [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/). + +The display of third-party offers can be toggled in the **Admin Area > Settings** page. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. -The display of third-party offers can be toggled in the Admin area on the Settings page. +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/user/project/import/phabricator.md b/doc/user/project/import/phabricator.md index 4d1d99fd35b..5c624e3aff6 100644 --- a/doc/user/project/import/phabricator.md +++ b/doc/user/project/import/phabricator.md @@ -15,18 +15,15 @@ Currently, only the following basic fields are imported: - Created at - Closed at - ## Enabling this feature While this feature is incomplete, a feature flag is required to enable it so that we can gain early feedback before releasing it for everyone. To enable it: -1. Enable Phabricator as an [import source](../../admin_area/settings/visibility_and_access_controls.md#import-sources) in the Admin area. +1. Run the following command in a Rails console: - ``` {.ruby} - Feature.enable(:phabricator_import) - ``` + ```ruby + Feature.enable(:phabricator_import) + ``` -The [import -source](../../admin_area/settings/visibility_and_access_controls.md#import-sources) -also needs to be activated by an admin in the admin interface. +1. Enable Phabricator as an [import source](../../admin_area/settings/visibility_and_access_controls.md#import-sources) in the Admin area. diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 52b6b56af84..2e9db949890 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -3,7 +3,7 @@ > Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only). NOTE: **Note:** -If you are running a self-managed instance, the new interface shown on +Prior to 12.0, if you are running a self-managed instance, the new interface shown on this page will not be available unless the feature flag `approval_rules` is enabled, which can be done from the Rails console by instance administrators. diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 5928ee1657b..693172b7d08 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -206,7 +206,7 @@ module API delete_note(noteable, params[:note_id]) end - if Noteable::RESOLVABLE_TYPES.include?(noteable_type.to_s) + if Noteable.resolvable_types.include?(noteable_type.to_s) desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do success Entities::Discussion end diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml index c9838c7a7ff..08dc74e041a 100644 --- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml @@ -1,18 +1,14 @@ ---- +# This file is a template, and might need editing before it works on your project. + # Build JAVA applications using Apache Maven (http://maven.apache.org) # For docker image tags see https://hub.docker.com/_/maven/ # # For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html -# -# This template will build and test your projects as well as create the documentation. -# + +# This template will build and test your projects # * Caches downloaded dependencies and plugins between invocation. # * Verify but don't deploy merge requests. # * Deploy built artifacts from master branch only. -# * Shows how to use multiple jobs in test stage for verifying functionality -# with multiple JDKs. -# * Uses site:stage to collect the documentation for multi-module projects. -# * Publishes the documentation for `master` branch. variables: # This will suppress any download for dependencies and plugins or upload messages which would clutter the console log. @@ -23,78 +19,38 @@ variables: # `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins. MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true" +# This template uses jdk8 for verifying and deploying images +image: maven:3.3.9-jdk-8 + # Cache downloaded dependencies and plugins between builds. # To keep cache across branches add 'key: "$CI_JOB_NAME"' cache: paths: - .m2/repository -# This will only validate and compile stuff and run e.g. maven-enforcer-plugin. -# Because some enforcer rules might check dependency convergence and class duplications -# we use `test-compile` here instead of `validate`, so the correct classpath is picked up. -.validate: &validate - stage: build - script: - - 'mvn $MAVEN_CLI_OPTS test-compile' - # For merge requests do not `deploy` but only run `verify`. # See https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html .verify: &verify stage: test script: - - 'mvn $MAVEN_CLI_OPTS verify site site:stage' + - 'mvn $MAVEN_CLI_OPTS verify' except: - master -# Validate merge requests using JDK7 -validate:jdk7: - <<: *validate - image: maven:3.3.9-jdk-7 - -# Validate merge requests using JDK8 -validate:jdk8: - <<: *validate - image: maven:3.3.9-jdk-8 - -# Verify merge requests using JDK7 -verify:jdk7: - <<: *verify - image: maven:3.3.9-jdk-7 - # Verify merge requests using JDK8 verify:jdk8: <<: *verify - image: maven:3.3.9-jdk-8 +# To deploy packages from CI, create a ci_settings.xml file +# For deploying packages to GitLab's Maven Repository: See https://gitlab.com/help/user/project/packages/maven_repository.md#creating-maven-packages-with-gitlab-cicd for more details. +# Please note: The GitLab Maven Repository is currently only available in GitLab Premium / Ultimate. # For `master` branch run `mvn deploy` automatically. -# Here you need to decide whether you want to use JDK7 or 8. -# To get this working you need to define a volume while configuring your gitlab-ci-multi-runner. -# Mount your `settings.xml` as `/root/.m2/settings.xml` which holds your secrets. -# See https://maven.apache.org/settings.html deploy:jdk8: - # Use stage test here, so the pages job may later pickup the created site. - stage: test - script: - - 'mvn $MAVEN_CLI_OPTS deploy site site:stage' - only: - - master - # Archive up the built documentation site. - artifacts: - paths: - - target/staging - image: maven:3.3.9-jdk-8 - -pages: - image: busybox:latest stage: deploy script: - # Because Maven appends the artifactId automatically to the staging path if you did define a parent pom, - # you might need to use `mv target/staging/YOUR_ARTIFACT_ID public` instead. - - mv target/staging public - dependencies: - - deploy:jdk8 - artifacts: - paths: - - public + - if [ ! -f ci_settings.xml ]; + then echo "CI settings missing\! If deploying to GitLab Maven Repository, please see https://gitlab.com/help/user/project/packages/maven_repository.md#creating-maven-packages-with-gitlab-cicd for instructions."; + fi + - 'mvn $MAVEN_CLI_OPTS deploy -s ci_settings.xml' only: - master diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb index 87669b253bc..25e40c70230 100644 --- a/lib/gitlab/metrics/samplers/puma_sampler.rb +++ b/lib/gitlab/metrics/samplers/puma_sampler.rb @@ -51,10 +51,11 @@ module Gitlab set_master_metrics(stats) stats['worker_status'].each do |worker| + last_status = worker['last_status'] labels = { worker: "worker_#{worker['index']}" } metrics[:puma_phase].set(labels, worker['phase']) - set_worker_metrics(worker['last_status'], labels) + set_worker_metrics(last_status, labels) if last_status.present? end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index afce09cd621..294c938d87d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5444,6 +5444,9 @@ msgstr "" msgid "Issue update failed" msgstr "" +msgid "Issue was closed by %{name} %{reason}" +msgstr "" + msgid "IssueBoards|Board" msgstr "" @@ -5859,6 +5862,9 @@ msgstr "" msgid "Live preview" msgstr "" +msgid "Loading functions timed out. Please reload the page to try again." +msgstr "" + msgid "Loading the GitLab IDE..." msgstr "" @@ -11988,6 +11994,9 @@ msgstr "" msgid "commented on %{link_to_project}" msgstr "" +msgid "commit %{commit_id}" +msgstr "" + msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue." msgstr "" @@ -12249,6 +12258,9 @@ msgstr "" msgid "mrWidget|Merge failed." msgstr "" +msgid "mrWidget|Merge failed: %{mergeError}. Please try again." +msgstr "" + msgid "mrWidget|Merge locally" msgstr "" @@ -12513,6 +12525,12 @@ msgstr "" msgid "verify ownership" msgstr "" +msgid "via %{closed_via}" +msgstr "" + +msgid "via merge request %{link}" +msgstr "" + msgid "view it on GitLab" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb index f97b0e56ca2..530fc684437 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/53 - context 'Plan', :quarantine do + context 'Plan' do describe 'issue suggestions' do let(:issue_title) { 'Issue Lists are awesome' } @@ -10,12 +9,12 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - project = Resource::Project.fabricate! do |resource| + project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'project-for-issue-suggestions' resource.description = 'project for issue suggestions' end - Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate_via_browser_ui! do |issue| issue.title = issue_title issue.project = project end diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index 782f5f272d9..18c594acae0 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -8,9 +8,8 @@ describe Projects::Serverless::FunctionsController do let(:user) { create(:user) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } let(:service) { cluster.platform_kubernetes } - let(:project) { cluster.project} + let(:project) { cluster.project } let(:namespace) do create(:cluster_kubernetes_namespace, @@ -30,17 +29,69 @@ describe Projects::Serverless::FunctionsController do end describe 'GET #index' do - context 'empty cache' do - it 'has no data' do + let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } } + + context 'when cache is being read' do + let(:knative_state) { 'checking' } + let(:functions) { [] } + + before do get :index, params: params({ format: :json }) + end - expect(response).to have_gitlab_http_status(204) + it 'returns checking' do + expect(json_response).to eq expected_json end - it 'renders an html page' do - get :index, params: params + it { expect(response).to have_gitlab_http_status(200) } + end + + context 'when cache is ready' do + let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) } + let(:knative_state) { true } - expect(response).to have_gitlab_http_status(200) + before do + allow_any_instance_of(Clusters::Cluster) + .to receive(:knative_services_finder) + .and_return(knative_services_finder) + synchronous_reactive_cache(knative_services_finder) + stub_kubeclient_service_pods( + kube_response({ "kind" => "PodList", "items" => [] }), + namespace: namespace.namespace + ) + end + + context 'when no functions were found' do + let(:functions) { [] } + + before do + stub_kubeclient_knative_services( + namespace: namespace.namespace, + response: kube_response({ "kind" => "ServiceList", "items" => [] }) + ) + get :index, params: params({ format: :json }) + end + + it 'returns checking' do + expect(json_response).to eq expected_json + end + + it { expect(response).to have_gitlab_http_status(200) } + end + + context 'when functions were found' do + let(:functions) { ["asdf"] } + + before do + stub_kubeclient_knative_services(namespace: namespace.namespace) + get :index, params: params({ format: :json }) + end + + it 'returns functions' do + expect(json_response["functions"]).not_to be_empty + end + + it { expect(response).to have_gitlab_http_status(200) } end end end @@ -56,11 +107,12 @@ describe Projects::Serverless::FunctionsController do context 'valid data', :use_clean_rails_memory_store_caching do before do stub_kubeclient_service_pods - stub_reactive_cache(knative, + stub_reactive_cache(cluster.knative_services_finder(project), { services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }) + }, + *cluster.knative_services_finder(project).cache_args) end it 'has a valid function name' do @@ -88,11 +140,12 @@ describe Projects::Serverless::FunctionsController do describe 'GET #index with data', :use_clean_rails_memory_store_caching do before do stub_kubeclient_service_pods - stub_reactive_cache(knative, + stub_reactive_cache(cluster.knative_services_finder(project), { services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }) + }, + *cluster.knative_services_finder(project).cache_args) end it 'has data' do @@ -100,11 +153,16 @@ describe Projects::Serverless::FunctionsController do expect(response).to have_gitlab_http_status(200) - expect(json_response).to contain_exactly( - a_hash_including( - "name" => project.name, - "url" => "http://#{project.name}.#{namespace.namespace}.example.com" - ) + expect(json_response).to match( + { + "knative_installed" => "checking", + "functions" => [ + a_hash_including( + "name" => project.name, + "url" => "http://#{project.name}.#{namespace.namespace}.example.com" + ) + ] + } ) end diff --git a/spec/factories/ci/pipeline_schedule.rb b/spec/factories/ci/pipeline_schedule.rb index b2b79807429..4b83ba2ac1b 100644 --- a/spec/factories/ci/pipeline_schedule.rb +++ b/spec/factories/ci/pipeline_schedule.rb @@ -7,6 +7,16 @@ FactoryBot.define do description "pipeline schedule" project + trait :every_minute do + cron '*/1 * * * *' + cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE + end + + trait :hourly do + cron '* */1 * * *' + cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE + end + trait :nightly do cron '0 1 * * *' cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index bcc11217389..d05ef2a8f12 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -8,8 +8,6 @@ describe "User creates a merge request", :js do let(:user) { create(:user) } before do - stub_feature_flags(approval_rules: false) - project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index e7b92dc5535..586b3ba170d 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -159,8 +159,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - page.within('.mr-widget-body') do - expect(page).to have_content('Something went wrong') + page.within('.mr-section-container') do + expect(page).to have_content('Merge failed: Something went wrong') end end end @@ -178,8 +178,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - page.within('.mr-widget-body') do - expect(page).to have_content('Something went wrong') + page.within('.mr-section-container') do + expect(page).to have_content('Merge failed: Something went wrong') end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 1477307ed7b..0066e985fbb 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -327,8 +327,8 @@ describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - page.within('.mr-widget-body') do - expect(page).to have_content('Something went wrong') + page.within('.mr-section-container') do + expect(page).to have_content('Merge failed: Something went wrong') end end end @@ -348,8 +348,8 @@ describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - page.within('.mr-widget-body') do - expect(page).to have_content('Something went wrong') + page.within('.mr-section-container') do + expect(page).to have_content('Merge failed: Something went wrong') end end end diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index b1a705f09ce..24041a51383 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -225,7 +225,7 @@ describe 'Pipeline Schedules', :js do context 'when active is true and next_run_at is NULL' do before do create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule| - pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil + pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil end end diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb index e14934b1672..9865dbbfb3c 100644 --- a/spec/features/projects/serverless/functions_spec.rb +++ b/spec/features/projects/serverless/functions_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe 'Functions', :js do include KubernetesHelpers + include ReactiveCachingHelpers let(:project) { create(:project) } let(:user) { create(:user) } @@ -13,44 +14,70 @@ describe 'Functions', :js do gitlab_sign_in(user) end - context 'when user does not have a cluster and visits the serverless page' do + shared_examples "it's missing knative installation" do before do visit project_serverless_functions_path(project) end - it 'sees an empty state' do + it 'sees an empty state require Knative installation' do expect(page).to have_link('Install Knative') expect(page).to have_selector('.empty-state') end end + context 'when user does not have a cluster and visits the serverless page' do + it_behaves_like "it's missing knative installation" + end + context 'when the user does have a cluster and visits the serverless page' do let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - before do - visit project_serverless_functions_path(project) - end - - it 'sees an empty state' do - expect(page).to have_link('Install Knative') - expect(page).to have_selector('.empty-state') - end + it_behaves_like "it's missing knative installation" end context 'when the user has a cluster and knative installed and visits the serverless page' do let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:service) { cluster.platform_kubernetes } - let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } - let(:project) { knative.cluster.project } + let(:project) { cluster.project } + let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) } + let(:namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: cluster.cluster_project.project) + end before do - stub_kubeclient_knative_services - stub_kubeclient_service_pods + allow_any_instance_of(Clusters::Cluster) + .to receive(:knative_services_finder) + .and_return(knative_services_finder) + synchronous_reactive_cache(knative_services_finder) + stub_kubeclient_knative_services(stub_get_services_options) + stub_kubeclient_service_pods(nil, namespace: namespace.namespace) visit project_serverless_functions_path(project) end - it 'sees an empty listing of serverless functions' do - expect(page).to have_selector('.empty-state') + context 'when there are no functions' do + let(:stub_get_services_options) do + { + namespace: namespace.namespace, + response: kube_response({ "kind" => "ServiceList", "items" => [] }) + } + end + + it 'sees an empty listing of serverless functions' do + expect(page).to have_selector('.empty-state') + expect(page).not_to have_selector('.content-list') + end + end + + context 'when there are functions' do + let(:stub_get_services_options) { { namespace: namespace.namespace } } + + it 'does not see an empty listing of serverless functions' do + expect(page).not_to have_selector('.empty-state') + expect(page).to have_selector('.content-list') + end end end end diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb index dc0278370aa..df33d215602 100644 --- a/spec/features/projects/settings/forked_project_settings_spec.rb +++ b/spec/features/projects/settings/forked_project_settings_spec.rb @@ -7,7 +7,6 @@ describe 'Projects > Settings > For a forked project', :js do let(:forked_project) { fork_project(original_project, user) } before do - stub_feature_flags(approval_rules: false) original_project.add_maintainer(user) forked_project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 4fe45311b2d..27f6ed56283 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -6,7 +6,6 @@ describe 'Project' do before do stub_feature_flags(vue_file_list: false) - stub_feature_flags(approval_rules: false) end describe 'creating from template' do diff --git a/spec/finders/clusters/knative_services_finder_spec.rb b/spec/finders/clusters/knative_services_finder_spec.rb new file mode 100644 index 00000000000..b731c2bd6bf --- /dev/null +++ b/spec/finders/clusters/knative_services_finder_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::KnativeServicesFinder do + include KubernetesHelpers + include ReactiveCachingHelpers + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:service) { cluster.platform_kubernetes } + let(:project) { cluster.cluster_project.project } + let(:namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: project) + end + + before do + stub_kubeclient_knative_services(namespace: namespace.namespace) + stub_kubeclient_service_pods( + kube_response( + kube_knative_pods_body( + project.name, namespace.namespace + ) + ), + namespace: namespace.namespace + ) + end + + shared_examples 'a cached data' do + it 'has an unintialized cache' do + is_expected.to be_blank + end + + context 'when using synchronous reactive cache' do + before do + synchronous_reactive_cache(cluster.knative_services_finder(project)) + end + + context 'when there are functions for cluster namespace' do + it { is_expected.not_to be_blank } + end + + context 'when there are no functions for cluster namespace' do + before do + stub_kubeclient_knative_services( + namespace: namespace.namespace, + response: kube_response({ "kind" => "ServiceList", "items" => [] }) + ) + stub_kubeclient_service_pods( + kube_response({ "kind" => "PodList", "items" => [] }), + namespace: namespace.namespace + ) + end + + it { is_expected.to be_blank } + end + end + end + + describe '#service_pod_details' do + subject { cluster.knative_services_finder(project).service_pod_details(project.name) } + + it_behaves_like 'a cached data' + end + + describe '#services' do + subject { cluster.knative_services_finder(project).services } + + it_behaves_like 'a cached data' + end + + describe '#knative_detected' do + subject { cluster.knative_services_finder(project).knative_detected } + before do + synchronous_reactive_cache(cluster.knative_services_finder(project)) + end + + context 'when knative is installed' do + before do + stub_kubeclient_discover(service.api_url) + end + + it { is_expected.to be_truthy } + it "discovers knative installation" do + expect { subject } + .to change { cluster.kubeclient.knative_client.discovered } + .from(false) + .to(true) + end + end + + context 'when knative is not installed' do + before do + stub_kubeclient_discover_knative_not_found(service.api_url) + end + + it { is_expected.to be_falsy } + it "does not discover knative installation" do + expect { subject }.not_to change { cluster.kubeclient.knative_client.discovered } + end + end + end +end diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb index 3ad38207da4..8aea45b457c 100644 --- a/spec/finders/projects/serverless/functions_finder_spec.rb +++ b/spec/finders/projects/serverless/functions_finder_spec.rb @@ -10,7 +10,7 @@ describe Projects::Serverless::FunctionsFinder do let(:user) { create(:user) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:service) { cluster.platform_kubernetes } - let(:project) { cluster.project} + let(:project) { cluster.project } let(:namespace) do create(:cluster_kubernetes_namespace, @@ -23,9 +23,45 @@ describe Projects::Serverless::FunctionsFinder do project.add_maintainer(user) end + describe '#installed' do + it 'when reactive_caching is still fetching data' do + expect(described_class.new(project).knative_installed).to eq 'checking' + end + + context 'when reactive_caching has finished' do + let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) } + + before do + allow_any_instance_of(Clusters::Cluster) + .to receive(:knative_services_finder) + .and_return(knative_services_finder) + synchronous_reactive_cache(knative_services_finder) + end + + context 'when knative is not installed' do + it 'returns false' do + stub_kubeclient_discover_knative_not_found(service.api_url) + + expect(described_class.new(project).knative_installed).to eq false + end + end + + context 'reactive_caching is finished and knative is installed' do + let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) } + + it 'returns true' do + stub_kubeclient_knative_services(namespace: namespace.namespace) + stub_kubeclient_service_pods(nil, namespace: namespace.namespace) + + expect(described_class.new(project).knative_installed).to be true + end + end + end + end + describe 'retrieve data from knative' do - it 'does not have knative installed' do - expect(described_class.new(project).execute).to be_empty + context 'does not have knative installed' do + it { expect(described_class.new(project).execute).to be_empty } end context 'has knative installed' do @@ -38,22 +74,24 @@ describe Projects::Serverless::FunctionsFinder do it 'there are functions', :use_clean_rails_memory_store_caching do stub_kubeclient_service_pods - stub_reactive_cache(knative, + stub_reactive_cache(cluster.knative_services_finder(project), { services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }) + }, + *cluster.knative_services_finder(project).cache_args) expect(finder.execute).not_to be_empty end it 'has a function', :use_clean_rails_memory_store_caching do stub_kubeclient_service_pods - stub_reactive_cache(knative, + stub_reactive_cache(cluster.knative_services_finder(project), { services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }) + }, + *cluster.knative_services_finder(project).cache_args) result = finder.service(cluster.environment_scope, cluster.project.name) expect(result).not_to be_empty @@ -84,20 +122,4 @@ describe Projects::Serverless::FunctionsFinder do end end end - - describe 'verify if knative is installed' do - context 'knative is not installed' do - it 'does not have knative installed' do - expect(described_class.new(project).installed?).to be false - end - end - - context 'knative is installed' do - let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } - - it 'does have knative installed' do - expect(described_class.new(project).installed?).to be true - end - end - end end diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js index de1dd219fe0..23dc3c3db8c 100644 --- a/spec/frontend/operation_settings/components/external_dashboard_spec.js +++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js @@ -21,6 +21,14 @@ describe('operation settings external dashboard component', () => { expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard'); }); + describe('expand/collapse button', () => { + it('renders as an expand button by default', () => { + const button = wrapper.find(GlButton); + + expect(button.text()).toBe('Expand'); + }); + }); + describe('sub-header', () => { let subHeader; diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js index 161a637dd75..0ad85e218dc 100644 --- a/spec/frontend/serverless/components/environment_row_spec.js +++ b/spec/frontend/serverless/components/environment_row_spec.js @@ -14,7 +14,7 @@ describe('environment row component', () => { beforeEach(() => { localVue = createLocalVue(); - vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*'); + vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*'); }); afterEach(() => vm.$destroy()); @@ -48,7 +48,11 @@ describe('environment row component', () => { beforeEach(() => { localVue = createLocalVue(); - vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test'); + vm = createComponent( + localVue, + translate(mockServerlessFunctionsDiffEnv.functions).test, + 'test', + ); }); afterEach(() => vm.$destroy()); diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js index 6924fb9e91f..d8a80f8031e 100644 --- a/spec/frontend/serverless/components/functions_spec.js +++ b/spec/frontend/serverless/components/functions_spec.js @@ -34,11 +34,11 @@ describe('functionsComponent', () => { }); it('should render empty state when Knative is not installed', () => { + store.dispatch('receiveFunctionsSuccess', { knative_installed: false }); component = shallowMount(functionsComponent, { localVue, store, propsData: { - installed: false, clustersPath: '', helpPath: '', statusPath: '', @@ -55,7 +55,6 @@ describe('functionsComponent', () => { localVue, store, propsData: { - installed: true, clustersPath: '', helpPath: '', statusPath: '', @@ -67,12 +66,11 @@ describe('functionsComponent', () => { }); it('should render empty state when there is no function data', () => { - store.dispatch('receiveFunctionsNoDataSuccess'); + store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true }); component = shallowMount(functionsComponent, { localVue, store, propsData: { - installed: true, clustersPath: '', helpPath: '', statusPath: '', @@ -91,12 +89,31 @@ describe('functionsComponent', () => { ); }); + it('should render functions and a loader when functions are partially fetched', () => { + store.dispatch('receiveFunctionsPartial', { + ...mockServerlessFunctions, + knative_installed: 'checking', + }); + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); + + expect(component.find('.js-functions-wrapper').exists()).toBe(true); + expect(component.find('.js-functions-loader').exists()).toBe(true); + }); + it('should render the functions list', () => { component = shallowMount(functionsComponent, { localVue, store, propsData: { - installed: true, clustersPath: 'clustersPath', helpPath: 'helpPath', statusPath, diff --git a/spec/frontend/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js index a2c18616324..ef616ceb37f 100644 --- a/spec/frontend/serverless/mock_data.js +++ b/spec/frontend/serverless/mock_data.js @@ -1,56 +1,62 @@ -export const mockServerlessFunctions = [ - { - name: 'testfunc1', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc1.tm-example.apps.example.com', - description: 'A test service', - image: 'knative-test-container-buildtemplate', - }, - { - name: 'testfunc2', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc2.tm-example.apps.example.com', - description: 'A second test service\nThis one with additional descriptions', - image: 'knative-test-echo-buildtemplate', - }, -]; +export const mockServerlessFunctions = { + knative_installed: true, + functions: [ + { + name: 'testfunc1', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc1.tm-example.apps.example.com', + description: 'A test service', + image: 'knative-test-container-buildtemplate', + }, + { + name: 'testfunc2', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc2.tm-example.apps.example.com', + description: 'A second test service\nThis one with additional descriptions', + image: 'knative-test-echo-buildtemplate', + }, + ], +}; -export const mockServerlessFunctionsDiffEnv = [ - { - name: 'testfunc1', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc1.tm-example.apps.example.com', - description: 'A test service', - image: 'knative-test-container-buildtemplate', - }, - { - name: 'testfunc2', - namespace: 'tm-example', - environment_scope: 'test', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc2.tm-example.apps.example.com', - description: 'A second test service\nThis one with additional descriptions', - image: 'knative-test-echo-buildtemplate', - }, -]; +export const mockServerlessFunctionsDiffEnv = { + knative_installed: true, + functions: [ + { + name: 'testfunc1', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc1.tm-example.apps.example.com', + description: 'A test service', + image: 'knative-test-container-buildtemplate', + }, + { + name: 'testfunc2', + namespace: 'tm-example', + environment_scope: 'test', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc2.tm-example.apps.example.com', + description: 'A second test service\nThis one with additional descriptions', + image: 'knative-test-echo-buildtemplate', + }, + ], +}; export const mockServerlessFunction = { name: 'testfunc1', diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js index fb549c8f153..92853fda37c 100644 --- a/spec/frontend/serverless/store/getters_spec.js +++ b/spec/frontend/serverless/store/getters_spec.js @@ -32,7 +32,7 @@ describe('Serverless Store Getters', () => { describe('getFunctions', () => { it('should translate the raw function array to group the functions per environment scope', () => { - state.functions = mockServerlessFunctions; + state.functions = mockServerlessFunctions.functions; const funcs = getters.getFunctions(state); diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js index ca3053e5c38..e2771c7e5fd 100644 --- a/spec/frontend/serverless/store/mutations_spec.js +++ b/spec/frontend/serverless/store/mutations_spec.js @@ -19,13 +19,13 @@ describe('ServerlessMutations', () => { expect(state.isLoading).toEqual(false); expect(state.hasFunctionData).toEqual(true); - expect(state.functions).toEqual(mockServerlessFunctions); + expect(state.functions).toEqual(mockServerlessFunctions.functions); }); it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => { const state = {}; - mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state); + mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, { knative_installed: true }); expect(state.isLoading).toEqual(false); expect(state.hasFunctionData).toEqual(false); diff --git a/spec/graphql/types/project_statistics_type_spec.rb b/spec/graphql/types/project_statistics_type_spec.rb index 485e194edb1..e9feac57a36 100644 --- a/spec/graphql/types/project_statistics_type_spec.rb +++ b/spec/graphql/types/project_statistics_type_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' describe GitlabSchema.types['ProjectStatistics'] do it "has all the required fields" do is_expected.to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size, - :build_artifacts_size, :packages_size, :commit_count) + :build_artifacts_size, :packages_size, :commit_count, + :wiki_size) end end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 0434af25866..e6aacb5b92b 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -8,19 +8,19 @@ describe EmailsHelper do context "and format is text" do it "returns plain text" do - expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") end end context "and format is HTML" do it "returns HTML" do - expect(closure_reason_text(merge_request, format: :html)).to eq(" via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}") + expect(closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}") end end context "and format is unknown" do it "returns plain text" do - expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") end end end @@ -29,7 +29,7 @@ describe EmailsHelper do let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" } it "returns plain text" do - expect(closure_reason_text(closed_via)).to eq(" via #{closed_via}") + expect(closure_reason_text(closed_via)).to eq("via #{closed_via}") end end diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 9b125593869..e77768e3597 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -12,6 +12,7 @@ import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/services/board_service'; import boardsStore from '~/boards/stores/boards_store'; +import eventHub from '~/boards/eventhub'; import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; describe('Store', () => { @@ -53,6 +54,39 @@ describe('Store', () => { }); }); + describe('toggleFilter', () => { + const dummyFilter = 'x=42'; + let updateTokensSpy; + + beforeEach(() => { + updateTokensSpy = jasmine.createSpy('updateTokens'); + eventHub.$once('updateTokens', updateTokensSpy); + + // prevent using window.history + spyOn(boardsStore, 'updateFiltersUrl').and.callFake(() => {}); + }); + + it('adds the filter if it is not present', () => { + boardsStore.filter.path = 'something'; + + boardsStore.toggleFilter(dummyFilter); + + expect(boardsStore.filter.path).toEqual(`something&${dummyFilter}`); + expect(updateTokensSpy).toHaveBeenCalled(); + expect(boardsStore.updateFiltersUrl).toHaveBeenCalled(); + }); + + it('removes the filter if it is present', () => { + boardsStore.filter.path = `something&${dummyFilter}`; + + boardsStore.toggleFilter(dummyFilter); + + expect(boardsStore.filter.path).toEqual('something'); + expect(updateTokensSpy).toHaveBeenCalled(); + expect(boardsStore.updateFiltersUrl).toHaveBeenCalled(); + }); + }); + describe('lists', () => { it('creates new list without persisting to DB', () => { boardsStore.addList(listObj); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 58bcd916739..6e16ab64be2 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -176,10 +176,6 @@ describe('Dashboard', () => { store, }); - component.$store.commit( - `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`, - '/environments', - ); component.$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, []); component.$store.commit( `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, @@ -211,10 +207,6 @@ describe('Dashboard', () => { }); component.$store.commit( - `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`, - '/environments', - ); - component.$store.commit( `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData, ); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 7653c10b94b..918717c4547 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -21,7 +21,6 @@ describe('mrWidgetOptions', () => { const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch'; beforeEach(() => { - gon.features = { approvalRules: false }; // Prevent component mounting delete mrWidgetOptions.el; @@ -32,7 +31,6 @@ describe('mrWidgetOptions', () => { }); afterEach(() => { - gon.features = null; vm.$destroy(); }); diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb index c471c30a194..f4a6e1fc7d9 100644 --- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb @@ -61,6 +61,33 @@ describe Gitlab::Metrics::Samplers::PumaSampler do end end + context 'with empty worker stats' do + let(:puma_stats) do + <<~EOS + { + "workers": 2, + "phase": 2, + "booted_workers": 2, + "old_workers": 0, + "worker_status": [{ + "pid": 32534, + "index": 0, + "phase": 1, + "booted": true, + "last_checkin": "2019-05-15T07:57:55Z", + "last_status": {} + }] + } + EOS + end + + it 'does not log worker stats' do + expect(subject).not_to receive(:set_worker_metrics) + + subject.sample + end + end + context 'in single mode' do let(:puma_stats) do <<~EOS diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 42d4769a921..6382be73ea7 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -48,32 +48,116 @@ describe Ci::PipelineSchedule do end end + describe '.runnable_schedules' do + subject { described_class.runnable_schedules } + + let!(:pipeline_schedule) do + Timecop.freeze(1.day.ago) do + create(:ci_pipeline_schedule, :hourly) + end + end + + it 'returns the runnable schedule' do + is_expected.to eq([pipeline_schedule]) + end + + context 'when there are no runnable schedules' do + let!(:pipeline_schedule) { } + + it 'returns an empty array' do + is_expected.to be_empty + end + end + end + + describe '.preloaded' do + subject { described_class.preloaded } + + before do + create_list(:ci_pipeline_schedule, 3) + end + + it 'preloads the associations' do + subject + + query = ActiveRecord::QueryRecorder.new { subject.each(&:project) } + + expect(query.count).to eq(2) + end + end + describe '#set_next_run_at' do - let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) } + let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) } + let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) } + + let(:expected_next_run_at) do + Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name) + .next_time_from(ideal_next_run_at) + end + + let(:cron_worker_next_run_at) do + Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name) + .next_time_from(Time.now) + end context 'when creates new pipeline schedule' do - let(:expected_next_run_at) do - Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone) - .next_time_from(Time.now) + it 'updates next_run_at automatically' do + expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at) end + end - it 'updates next_run_at automatically' do - expect(described_class.last.next_run_at).to eq(expected_next_run_at) + context 'when PipelineScheduleWorker runs at a specific interval' do + before do + allow(Settings).to receive(:cron_jobs) do + { + 'pipeline_schedule_worker' => { + 'cron' => '0 1 2 3 *' + } + } + end + end + + it "updates next_run_at to the sidekiq worker's execution time" do + expect(pipeline_schedule.next_run_at.min).to eq(0) + expect(pipeline_schedule.next_run_at.hour).to eq(1) + expect(pipeline_schedule.next_run_at.day).to eq(2) + expect(pipeline_schedule.next_run_at.month).to eq(3) end end - context 'when updates cron of exsisted pipeline schedule' do - let(:new_cron) { '0 0 1 1 *' } + context 'when pipeline schedule runs every minute' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) } - let(:expected_next_run_at) do - Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone) - .next_time_from(Time.now) + it "updates next_run_at to the sidekiq worker's execution time" do + expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at) + end + end + + context 'when there are two different pipeline schedules in different time zones' do + let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') } + let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } + + it 'sets different next_run_at' do + expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at) + end + end + + context 'when there are two different pipeline schedules in the same time zones' do + let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } + let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } + + it 'sets the sames next_run_at' do + expect(pipeline_schedule_1.next_run_at).to eq(pipeline_schedule_2.next_run_at) end + end + + context 'when updates cron of exsisted pipeline schedule' do + let(:new_cron) { '0 0 1 1 *' } it 'updates next_run_at automatically' do pipeline_schedule.update!(cron: new_cron) - expect(described_class.last.next_run_at).to eq(expected_next_run_at) + expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at) end end end @@ -83,10 +167,11 @@ describe Ci::PipelineSchedule do context 'when reschedules after 10 days from now' do let(:future_time) { 10.days.from_now } + let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) } let(:expected_next_run_at) do - Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone) - .next_time_from(future_time) + Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name) + .next_time_from(ideal_next_run_at) end it 'points to proper next_run_at' do @@ -99,38 +184,6 @@ describe Ci::PipelineSchedule do end end - describe '#real_next_run' do - subject do - described_class.last.real_next_run(worker_cron: worker_cron, - worker_time_zone: worker_time_zone) - end - - context 'when GitLab time_zone is UTC' do - before do - allow(Time).to receive(:zone) - .and_return(ActiveSupport::TimeZone[worker_time_zone]) - end - - let(:worker_time_zone) { 'UTC' } - - context 'when cron_timezone is Eastern Time (US & Canada)' do - before do - create(:ci_pipeline_schedule, :nightly, - cron_timezone: 'Eastern Time (US & Canada)') - end - - let(:worker_cron) { '0 1 2 3 *' } - - it 'returns the next time worker executes' do - expect(subject.min).to eq(0) - expect(subject.hour).to eq(1) - expect(subject.day).to eq(2) - expect(subject.month).to eq(3) - end - end - end - end - describe '#job_variables' do let!(:pipeline_schedule) { create(:ci_pipeline_schedule) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index a0319b3eb0a..a8701f0efa4 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1381,6 +1381,40 @@ describe Ci::Pipeline, :mailer do end end + describe 'auto merge' do + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + + let(:pipeline) do + create(:ci_pipeline, :running, project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end + + before do + merge_request.update_head_pipeline + end + + %w[succeed! drop! cancel! skip!].each do |action| + context "when the pipeline recieved #{action} event" do + it 'performs AutoMergeProcessWorker' do + expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id) + + pipeline.public_send(action) + end + end + end + + context 'when auto merge is not enabled in the merge request' do + let(:merge_request) { create(:merge_request) } + + it 'performs AutoMergeProcessWorker' do + expect(AutoMergeProcessWorker).not_to receive(:perform_async) + + pipeline.succeed! + end + end + end + def create_build(name, *traits, queued_at: current, started_from: 0, **opts) create(:ci_build, *traits, name: name, diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index d5974f47190..b38cf96de7e 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -3,9 +3,6 @@ require 'rails_helper' describe Clusters::Applications::Knative do - include KubernetesHelpers - include ReactiveCachingHelpers - let(:knative) { create(:clusters_applications_knative) } include_examples 'cluster application core specs', :clusters_applications_knative @@ -146,77 +143,4 @@ describe Clusters::Applications::Knative do describe 'validations' do it { is_expected.to validate_presence_of(:hostname) } end - - describe '#service_pod_details' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:service) { cluster.platform_kubernetes } - let(:knative) { create(:clusters_applications_knative, cluster: cluster) } - - let(:namespace) do - create(:cluster_kubernetes_namespace, - cluster: cluster, - cluster_project: cluster.cluster_project, - project: cluster.cluster_project.project) - end - - before do - stub_kubeclient_discover(service.api_url) - stub_kubeclient_knative_services - stub_kubeclient_service_pods - stub_reactive_cache(knative, - { - services: kube_response(kube_knative_services_body), - pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace)) - }) - synchronous_reactive_cache(knative) - end - - it 'is able k8s core for pod details' do - expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil - end - end - - describe '#services' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:service) { cluster.platform_kubernetes } - let(:knative) { create(:clusters_applications_knative, cluster: cluster) } - - let(:namespace) do - create(:cluster_kubernetes_namespace, - cluster: cluster, - cluster_project: cluster.cluster_project, - project: cluster.cluster_project.project) - end - - subject { knative.services } - - before do - stub_kubeclient_discover(service.api_url) - stub_kubeclient_knative_services - stub_kubeclient_service_pods - end - - it 'has an unintialized cache' do - is_expected.to be_nil - end - - context 'when using synchronous reactive cache' do - before do - stub_reactive_cache(knative, - { - services: kube_response(kube_knative_services_body), - pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace)) - }) - synchronous_reactive_cache(knative) - end - - it 'has cached services' do - is_expected.not_to be_nil - end - - it 'matches our namespace' do - expect(knative.services_for(ns: namespace)).not_to be_nil - end - end - end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 4739e62289a..f206bb41f45 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -38,6 +38,11 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do it { is_expected.to respond_to :project } + it do + expect(subject.knative_services_finder(subject.project)) + .to be_instance_of(Clusters::KnativeServicesFinder) + end + describe '.enabled' do subject { described_class.enabled } diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb index ee613b199ad..e17b98536fa 100644 --- a/spec/models/concerns/noteable_spec.rb +++ b/spec/models/concerns/noteable_spec.rb @@ -260,4 +260,16 @@ describe Noteable do end end end + + describe '.replyable_types' do + it 'exposes the replyable types' do + expect(described_class.replyable_types).to include('Issue', 'MergeRequest') + end + end + + describe '.resolvable_types' do + it 'exposes the replyable types' do + expect(described_class.resolvable_types).to include('MergeRequest') + end + end end diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb new file mode 100644 index 00000000000..f2ac53cb25a --- /dev/null +++ b/spec/services/ci/pipeline_schedule_service_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::PipelineScheduleService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + subject { service.execute(schedule) } + + let(:schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + + it 'schedules next run' do + expect(schedule).to receive(:schedule_next_run!) + + subject + end + + it 'runs RunPipelineScheduleWorker' do + expect(RunPipelineScheduleWorker) + .to receive(:perform_async).with(schedule.id, schedule.owner.id) + + subject + end + end +end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index ffa612cf315..29b7e0f17e2 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -52,6 +52,14 @@ describe MergeRequests::CloseService do it 'marks todos as done' do expect(todo.reload).to be_done end + + context 'when auto merge is enabled' do + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + + it 'cancels the auto merge' do + expect(@merge_request).not_to be_auto_merge_enabled + end + end end it 'updates metrics' do diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 78b7ae9c00c..011c4df0fe5 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -17,17 +17,38 @@ module KubernetesHelpers kube_response(kube_deployments_body) end - def stub_kubeclient_discover(api_url) + def stub_kubeclient_discover_base(api_url) WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) - WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body)) - WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body)) - WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body)) + WebMock + .stub_request(:get, api_url + '/apis/extensions/v1beta1') + .to_return(kube_response(kube_v1beta1_discovery_body)) + WebMock + .stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1') + .to_return(kube_response(kube_v1_rbac_authorization_discovery_body)) + end + + def stub_kubeclient_discover(api_url) + stub_kubeclient_discover_base(api_url) + + WebMock + .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1') + .to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body)) + end + + def stub_kubeclient_discover_knative_not_found(api_url) + stub_kubeclient_discover_base(api_url) + + WebMock + .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1') + .to_return(status: [404, "Resource Not Found"]) end - def stub_kubeclient_service_pods(status: nil) + def stub_kubeclient_service_pods(response = nil, options = {}) stub_kubeclient_discover(service.api_url) - pods_url = service.api_url + "/api/v1/pods" - response = { status: status } if status + + namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : "" + + pods_url = service.api_url + "/api/v1/#{namespace_path}pods" WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) end @@ -56,15 +77,18 @@ module KubernetesHelpers WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response) end - def stub_kubeclient_knative_services(**options) + def stub_kubeclient_knative_services(options = {}) + namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : "" + options[:name] ||= "kubetest" - options[:namespace] ||= "default" options[:domain] ||= "example.com" + options[:response] ||= kube_response(kube_knative_services_body(options)) stub_kubeclient_discover(service.api_url) - knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services" - WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options))) + knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/#{namespace_path}services" + + WebMock.stub_request(:get, knative_url).to_return(options[:response]) end def stub_kubeclient_get_secret(api_url, **options) diff --git a/spec/workers/auto_merge_process_worker_spec.rb b/spec/workers/auto_merge_process_worker_spec.rb new file mode 100644 index 00000000000..616727ce5ca --- /dev/null +++ b/spec/workers/auto_merge_process_worker_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AutoMergeProcessWorker do + describe '#perform' do + subject { described_class.new.perform(merge_request&.id) } + + context 'when merge request is found' do + let(:merge_request) { create(:merge_request) } + + it 'executes AutoMergeService' do + expect_next_instance_of(AutoMergeService) do |auto_merge| + expect(auto_merge).to receive(:process) + end + + subject + end + end + + context 'when merge request is not found' do + let(:merge_request) { nil } + + it 'does not execute AutoMergeService' do + expect(AutoMergeService).not_to receive(:new) + + subject + end + end + end +end diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb index 8c604b13297..9326db34209 100644 --- a/spec/workers/pipeline_schedule_worker_spec.rb +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -41,16 +41,6 @@ describe PipelineScheduleWorker do it_behaves_like 'successful scheduling' - context 'when exclusive lease has already been taken by the other instance' do - before do - stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT) - end - - it 'raises an error and does not start creating pipelines' do - expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) - end - end - context 'when the latest commit contains [ci skip]' do before do allow_any_instance_of(Ci::Pipeline) @@ -77,47 +67,19 @@ describe PipelineScheduleWorker do stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } )) end - it 'creates a failed pipeline with the reason' do - expect { subject }.to change { project.ci_pipelines.count }.by(1) - expect(Ci::Pipeline.last).to be_config_error - expect(Ci::Pipeline.last.yaml_errors).not_to be_nil + it 'does not creates a new pipeline' do + expect { subject }.not_to change { project.ci_pipelines.count } end end end context 'when the schedule is not runnable by the user' do - before do - expect(Gitlab::Sentry) - .to receive(:track_exception) - .with(Ci::CreatePipelineService::CreateError, - issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', - extra: { schedule_id: pipeline_schedule.id } ).once - end - it 'does not deactivate the schedule' do subject expect(pipeline_schedule.reload.active).to be_truthy end - it 'increments Prometheus counter' do - expect(Gitlab::Metrics) - .to receive(:counter) - .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation") - .and_call_original - - subject - end - - it 'logging a pipeline error' do - expect(Rails.logger) - .to receive(:error) - .with(a_string_matching("Insufficient permissions to create a new pipeline")) - .and_call_original - - subject - end - it 'does not create a pipeline' do expect { subject }.not_to change { project.ci_pipelines.count } end @@ -131,21 +93,6 @@ describe PipelineScheduleWorker do before do stub_ci_pipeline_yaml_file(nil) project.add_maintainer(user) - - expect(Gitlab::Sentry) - .to receive(:track_exception) - .with(Ci::CreatePipelineService::CreateError, - issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', - extra: { schedule_id: pipeline_schedule.id } ).once - end - - it 'logging a pipeline error' do - expect(Rails.logger) - .to receive(:error) - .with(a_string_matching("Missing .gitlab-ci.yml file")) - .and_call_original - - subject end it 'does not create a pipeline' do diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb deleted file mode 100644 index b511edfa620..00000000000 --- a/spec/workers/pipeline_success_worker_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe PipelineSuccessWorker do - describe '#perform' do - context 'when pipeline exists' do - let(:pipeline) { create(:ci_pipeline, status: 'success', ref: merge_request.source_branch, project: merge_request.source_project) } - let(:merge_request) { create(:merge_request) } - - it 'performs "merge when pipeline succeeds"' do - expect_next_instance_of(AutoMergeService) do |auto_merge| - expect(auto_merge).to receive(:process) - end - - described_class.new.perform(pipeline.id) - end - end - - context 'when pipeline does not exist' do - it 'does not raise exception' do - expect { described_class.new.perform(123) } - .not_to raise_error - end - end - end -end diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb index 690af22f4dc..7414470f8e7 100644 --- a/spec/workers/run_pipeline_schedule_worker_spec.rb +++ b/spec/workers/run_pipeline_schedule_worker_spec.rb @@ -32,7 +32,37 @@ describe RunPipelineScheduleWorker do it 'calls the Service' do expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service) - expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule) + expect(create_pipeline_service).to receive(:execute!).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule) + + worker.perform(pipeline_schedule.id, user.id) + end + end + + context 'when database statement timeout happens' do + before do + allow(Ci::CreatePipelineService).to receive(:new) { raise ActiveRecord::StatementInvalid } + + expect(Gitlab::Sentry) + .to receive(:track_exception) + .with(ActiveRecord::StatementInvalid, + issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', + extra: { schedule_id: pipeline_schedule.id } ).once + end + + it 'increments Prometheus counter' do + expect(Gitlab::Metrics) + .to receive(:counter) + .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation") + .and_call_original + + worker.perform(pipeline_schedule.id, user.id) + end + + it 'logging a pipeline error' do + expect(Rails.logger) + .to receive(:error) + .with(a_string_matching('ActiveRecord::StatementInvalid')) + .and_call_original worker.perform(pipeline_schedule.id, user.id) end |