diff options
49 files changed, 749 insertions, 196 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 76f3020c5c2..4dc4ce543e9 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -24,6 +24,7 @@ const Api = { projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', projectRunnersPath: '/api/:version/projects/:id/runners', + projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches', mergeRequestsPath: '/api/:version/merge_requests', groupLabelsPath: '/groups/:namespace_path/-/labels', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', @@ -220,6 +221,22 @@ const Api = { return axios.get(url, config); }, + projectProtectedBranches(id, query = '') { + const url = Api.buildUrl(Api.projectProtectedBranchesPath).replace( + ':id', + encodeURIComponent(id), + ); + + return axios + .get(url, { + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + }, + }) + .then(({ data }) => data); + }, + mergeRequests(params = {}) { const url = Api.buildUrl(Api.mergeRequestsPath); diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js index 0bba2a2e160..da33e092086 100644 --- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import axios from '../lib/utils/axios_utils'; import { s__ } from '../locale'; import Flash from '../flash'; @@ -10,7 +10,7 @@ function generateErrorBoxContent(errors) { const errorList = [].concat(errors).map( errorString => ` <li> - ${_.escape(errorString)} + ${esc(errorString)} </li> `, ); diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js index 2f268419bff..25640f71af2 100644 --- a/app/assets/javascripts/commons/jquery.js +++ b/app/assets/javascripts/commons/jquery.js @@ -4,6 +4,6 @@ import 'jquery'; import 'jquery-ujs'; import 'vendor/jquery.endless-scroll'; import 'jquery.caret'; // must be imported before at.js -import 'at.js'; +import '@gitlab/at.js'; import 'vendor/jquery.scrollTo'; import 'jquery.waitforimages'; diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue index caad2a835fa..8dbf0a68c43 100644 --- a/app/assets/javascripts/contributors/components/contributors.vue +++ b/app/assets/javascripts/contributors/components/contributors.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { debounce, uniq } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import { GlAreaChart } from '@gitlab/ui/dist/charts'; @@ -120,7 +120,7 @@ export default { return this.xAxisRange[this.xAxisRange.length - 1]; }, charts() { - return _.uniq(this.individualCharts); + return uniq(this.individualCharts); }, }, mounted() { @@ -171,7 +171,7 @@ export default { }); }) .catch(() => {}); - this.masterChart.on('datazoom', _.debounce(this.setIndividualChartsZoom, 200)); + this.masterChart.on('datazoom', debounce(this.setIndividualChartsZoom, 200)); }, onIndividualChartCreated(chart) { this.individualCharts.push(chart); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index c762e298eb8..6f486e1a539 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import 'at.js'; +import '@gitlab/at.js'; import _ from 'underscore'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import glRegexp from './lib/utils/regexp'; diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue index 163849d3c40..797aa15a2b9 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -12,6 +12,11 @@ export default { type: Object, required: true, }, + deploymentCluster: { + type: Object, + required: false, + default: null, + }, iconStatus: { type: Object, required: true, @@ -61,14 +66,14 @@ export default { : ''; }, hasCluster() { - return this.hasLastDeployment && this.lastDeployment.cluster; + return Boolean(this.deploymentCluster) && Boolean(this.deploymentCluster.name); }, clusterNameOrLink() { if (!this.hasCluster) { return ''; } - const { name, path } = this.lastDeployment.cluster; + const { name, path } = this.deploymentCluster; const escapedName = _.escape(name); const escapedPath = _.escape(path); @@ -86,6 +91,9 @@ export default { false, ); }, + kubernetesNamespace() { + return this.hasCluster ? this.deploymentCluster.kubernetes_namespace : null; + }, }, methods: { deploymentLink(name) { @@ -109,75 +117,153 @@ export default { ); }, lastEnvironmentMessage() { - const { environmentLink, clusterNameOrLink, hasCluster } = this; - - const message = hasCluster - ? __('This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}.') - : __('This job is deployed to %{environmentLink}.'); - - return sprintf(message, { environmentLink, clusterNameOrLink }, false); + const { environmentLink, clusterNameOrLink, hasCluster, kubernetesNamespace } = this; + if (hasCluster) { + if (kubernetesNamespace) { + return sprintf( + __( + 'This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.', + ), + { environmentLink, clusterNameOrLink, kubernetesNamespace }, + false, + ); + } + // we know the cluster but not the namespace + return sprintf( + __('This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}.'), + { environmentLink, clusterNameOrLink }, + false, + ); + } + // not a cluster deployment + return sprintf(__('This job is deployed to %{environmentLink}.'), { environmentLink }, false); }, outOfDateEnvironmentMessage() { - const { hasLastDeployment, hasCluster, environmentLink, clusterNameOrLink } = this; + const { + hasLastDeployment, + hasCluster, + environmentLink, + clusterNameOrLink, + kubernetesNamespace, + } = this; if (hasLastDeployment) { - const message = hasCluster - ? __( - 'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}.', - ) - : __( - 'This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}.', + const deploymentLink = this.deploymentLink(__('most recent deployment')); + if (hasCluster) { + if (kubernetesNamespace) { + return sprintf( + __( + 'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. View the %{deploymentLink}.', + ), + { environmentLink, clusterNameOrLink, kubernetesNamespace, deploymentLink }, + false, ); - + } + // we know the cluster but not the namespace + return sprintf( + __( + 'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}.', + ), + { environmentLink, clusterNameOrLink, deploymentLink }, + false, + ); + } + // not a cluster deployment return sprintf( - message, - { - environmentLink, - clusterNameOrLink, - deploymentLink: this.deploymentLink(__('most recent deployment')), - }, + __( + 'This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}.', + ), + { environmentLink, deploymentLink }, false, ); } - - const message = hasCluster - ? __( + // no last deployment, i.e. this is the first deployment + if (hasCluster) { + if (kubernetesNamespace) { + return sprintf( + __( + 'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.', + ), + { environmentLink, clusterNameOrLink, kubernetesNamespace }, + false, + ); + } + // we know the cluster but not the namespace + return sprintf( + __( 'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}.', - ) - : __('This job is an out-of-date deployment to %{environmentLink}.'); - + ), + { environmentLink, clusterNameOrLink }, + false, + ); + } + // not a cluster deployment return sprintf( - message, - { - environmentLink, - clusterNameOrLink, - }, + __('This job is an out-of-date deployment to %{environmentLink}.'), + { environmentLink }, false, ); }, creatingEnvironmentMessage() { - const { hasLastDeployment, hasCluster, environmentLink, clusterNameOrLink } = this; + const { + hasLastDeployment, + hasCluster, + environmentLink, + clusterNameOrLink, + kubernetesNamespace, + } = this; if (hasLastDeployment) { - const message = hasCluster - ? __( - 'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}.', - ) - : __( - 'This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}.', + const deploymentLink = this.deploymentLink(__('latest deployment')); + if (hasCluster) { + if (kubernetesNamespace) { + return sprintf( + __( + 'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. This will overwrite the %{deploymentLink}.', + ), + { environmentLink, clusterNameOrLink, kubernetesNamespace, deploymentLink }, + false, ); - + } + // we know the cluster but not the namespace + return sprintf( + __( + 'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}.', + ), + { environmentLink, clusterNameOrLink, deploymentLink }, + false, + ); + } + // not a cluster deployment return sprintf( - message, - { - environmentLink, - clusterNameOrLink, - deploymentLink: this.deploymentLink(__('latest deployment')), - }, + __( + 'This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}.', + ), + { environmentLink, deploymentLink }, false, ); } - + // no last deployment, i.e. this is the first deployment + if (hasCluster) { + if (kubernetesNamespace) { + return sprintf( + __( + 'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.', + ), + { environmentLink, clusterNameOrLink, kubernetesNamespace }, + false, + ); + } + // we know the cluster but not the namespace + return sprintf( + __( + 'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}.', + ), + { environmentLink, clusterNameOrLink }, + false, + ); + } + // not a cluster deployment return sprintf( __('This job is creating a deployment to %{environmentLink}.'), { environmentLink }, diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 0ca13e897f3..bc310f77a58 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -256,6 +256,7 @@ export default { v-if="hasEnvironment" class="js-job-environment" :deployment-status="job.deployment_status" + :deployment-cluster="job.deployment_cluster" :icon-status="job.status" /> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index c01024fc2cd..b3b189c1114 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -15,7 +15,7 @@ import { escape, uniqueId } from 'lodash'; import Cookies from 'js-cookie'; import Autosize from 'autosize'; import 'jquery.caret'; // required by at.js -import 'at.js'; +import '@gitlab/at.js'; import Vue from 'vue'; import { GlSkeletonLoading } from '@gitlab/ui'; import AjaxCache from '~/lib/utils/ajax_cache'; diff --git a/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js index 9a1bc46bf4a..95f4ba28b42 100644 --- a/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js +++ b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js @@ -26,18 +26,18 @@ export default class UsagePingPayload { requestPayload() { if (this.isInserted) return this.showPayload(); - this.spinner.classList.add('d-inline'); + this.spinner.classList.add('d-inline-flex'); return axios .get(this.container.dataset.endpoint, { responseType: 'text', }) .then(({ data }) => { - this.spinner.classList.remove('d-inline'); + this.spinner.classList.remove('d-inline-flex'); this.insertPayload(data); }) .catch(() => { - this.spinner.classList.remove('d-inline'); + this.spinner.classList.remove('d-inline-flex'); flash(__('Error fetching usage ping data.')); }); } diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e98030f1511..657e52674db 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,7 +11,7 @@ // like a table or typography then make changes in the framework/ directory. // If you need to add unique style that should affect only one page - use pages/ // directory. -@import "at.js/dist/css/jquery.atwho"; +@import "@gitlab/at.js/dist/css/jquery.atwho"; @import "dropzone/dist/basic"; @import "select2/select2"; diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index bd0134a82d3..a8244219b10 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -63,7 +63,8 @@ display: block; } - .select2-choices { + .select2-choices, + .select2-choice { border-color: $red-500; } } diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss index 91fe75075dc..5e05311041c 100644 --- a/app/assets/stylesheets/framework/spinner.scss +++ b/app/assets/stylesheets/framework/spinner.scss @@ -49,3 +49,9 @@ @include spinner-color($white); } } + +.btn { + .spinner { + vertical-align: text-bottom; + } +} diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 5450357fe1b..68f50b13a07 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -30,6 +30,7 @@ class Deployment < ApplicationRecord validate :valid_ref, on: :create delegate :name, to: :environment, prefix: true + delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true scope :for_environment, -> (environment) { where(environment_id: environment) } scope :for_environment_name, -> (name) do diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 0ef71ff8af5..fe6afa4ff6f 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -22,6 +22,12 @@ class BuildDetailsEntity < JobEntity end end + expose :deployment_cluster, if: -> (build) { build&.deployment&.cluster } do |build, options| + # Until data is copied over from deployments.cluster_id, this entity must represent Deployment instead of DeploymentCluster + # https://gitlab.com/gitlab-org/gitlab/issues/202628 + DeploymentClusterEntity.represent(build.deployment, options) + end + expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do expose :download_path, if: -> (*) { build.artifacts? } do |build| download_project_job_artifacts_path(project, build) diff --git a/app/serializers/cluster_basic_entity.rb b/app/serializers/cluster_basic_entity.rb deleted file mode 100644 index d104f2c8bbd..00000000000 --- a/app/serializers/cluster_basic_entity.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class ClusterBasicEntity < Grape::Entity - include RequestAwareEntity - - expose :name - expose :path, if: -> (cluster) { can?(request.current_user, :read_cluster, cluster) } do |cluster| - cluster.present(current_user: request.current_user).show_path - end -end diff --git a/app/serializers/deployment_cluster_entity.rb b/app/serializers/deployment_cluster_entity.rb new file mode 100644 index 00000000000..98736472b62 --- /dev/null +++ b/app/serializers/deployment_cluster_entity.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class DeploymentClusterEntity < Grape::Entity + include RequestAwareEntity + + # Until data is copied over from deployments.cluster_id, this entity must represent Deployment instead of DeploymentCluster + # https://gitlab.com/gitlab-org/gitlab/issues/202628 + + expose :name do |deployment| + deployment.cluster.name + end + + expose :path, if: -> (deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment| + deployment.cluster.present(current_user: request.current_user).show_path + end + + expose :kubernetes_namespace, if: -> (deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment| + deployment.kubernetes_namespace + end +end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 94773eeebd0..dc7c4654208 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -41,7 +41,11 @@ class DeploymentEntity < Grape::Entity JobEntity.represent(deployment.playable_build, options.merge(only: [:play_path, :retry_path])) end - expose :cluster, using: ClusterBasicEntity + expose :cluster do |deployment, options| + # Until data is copied over from deployments.cluster_id, this entity must represent Deployment instead of DeploymentCluster + # https://gitlab.com/gitlab-org/gitlab/issues/202628 + DeploymentClusterEntity.represent(deployment, options) unless deployment.cluster.nil? + end private diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index b4fe1bbf028..9421585b70c 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -26,7 +26,7 @@ %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } %button.btn.js-usage-ping-payload-trigger{ type: 'button' } - .js-spinner.d-none= icon('spinner spin') + .spinner.js-spinner.d-none .js-text.d-inline= _('Preview payload') %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } - else diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml index e236c24b088..e00a10398d3 100644 --- a/app/views/shared/milestones/_delete_button.html.haml +++ b/app/views/shared/milestones/_delete_button.html.haml @@ -9,6 +9,6 @@ milestone_merge_request_count: @milestone.merge_requests.count }, disabled: true } = _('Delete') - = icon('spin spinner', class: 'js-loading-icon hidden' ) + .spinner.js-loading-icon.hidden #delete-milestone-modal diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index fbbcc4f3e68..a6fb8e6d4fc 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -98,10 +98,6 @@ human_time_estimate: @milestone.human_total_issue_time_estimate, human_time_spent: @milestone.human_total_issue_time_spent, limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s } } - // Fallback while content is loading - .title.hide-collapsed - = _('Time tracking') - = icon('spinner spin') = render_if_exists 'shared/milestones/weight', milestone: milestone diff --git a/app/views/shared/milestones/_tab_loading.html.haml b/app/views/shared/milestones/_tab_loading.html.haml index 68458c2d0aa..dfca6a184be 100644 --- a/app/views/shared/milestones/_tab_loading.html.haml +++ b/app/views/shared/milestones/_tab_loading.html.haml @@ -1,2 +1,2 @@ .text-center.prepend-top-default - = icon('spin spinner 2x', 'aria-hidden': 'true', 'aria-label': 'Loading tab content') + .spinner.spinner-md diff --git a/changelogs/unreleased/196678-replace-_-with-lodash.yml b/changelogs/unreleased/196678-replace-_-with-lodash.yml new file mode 100644 index 00000000000..fec7b254e9d --- /dev/null +++ b/changelogs/unreleased/196678-replace-_-with-lodash.yml @@ -0,0 +1,5 @@ +--- +title: Replaced underscore with lodash for spec/javascripts/badges +merge_request: 25135 +author: Shubham Pandey +type: other diff --git a/changelogs/unreleased/an-sidekiq-query.yml b/changelogs/unreleased/an-sidekiq-query.yml new file mode 100644 index 00000000000..b0e5239984a --- /dev/null +++ b/changelogs/unreleased/an-sidekiq-query.yml @@ -0,0 +1,5 @@ +--- +title: Add experimental --queue-selector option to sidekiq-cluster +merge_request: 18877 +author: +type: changed diff --git a/changelogs/unreleased/leipert-fix-user-label-bug.yml b/changelogs/unreleased/leipert-fix-user-label-bug.yml new file mode 100644 index 00000000000..0e1e23bbf0c --- /dev/null +++ b/changelogs/unreleased/leipert-fix-user-label-bug.yml @@ -0,0 +1,5 @@ +--- +title: Fix autocomplete limitation bug +merge_request: 25167 +author: +type: fixed diff --git a/changelogs/unreleased/refactoring-entities-file-31.yml b/changelogs/unreleased/refactoring-entities-file-31.yml new file mode 100644 index 00000000000..83d0eff2758 --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-31.yml @@ -0,0 +1,5 @@ +--- +title: Separate provider, platform and post receive entities into own class files +merge_request: 25119 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/show-kubernetes-namespace-on-job-show-page.yml b/changelogs/unreleased/show-kubernetes-namespace-on-job-show-page.yml new file mode 100644 index 00000000000..10fbb7b7d13 --- /dev/null +++ b/changelogs/unreleased/show-kubernetes-namespace-on-job-show-page.yml @@ -0,0 +1,5 @@ +--- +title: Show Kubernetes namespace on job show page +merge_request: 20983 +author: +type: added diff --git a/config/webpack.vendor.config.js b/config/webpack.vendor.config.js index bddbf067d7c..8da4938191c 100644 --- a/config/webpack.vendor.config.js +++ b/config/webpack.vendor.config.js @@ -29,7 +29,7 @@ module.exports = { 'vuex', 'pikaday', 'vue/dist/vue.esm.js', - 'at.js', + '@gitlab/at.js', 'jed', 'mermaid', 'katex', diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md index 5cdd33ba507..d70e9d1baa5 100644 --- a/doc/administration/operations/extra_sidekiq_processes.md +++ b/doc/administration/operations/extra_sidekiq_processes.md @@ -82,6 +82,93 @@ you list: sudo gitlab-ctl reconfigure ``` +## Queue selector (experimental) + +> [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8. + +CAUTION: **Caution:** +As this is marked as **experimental**, it is subject to change at any +time, including **breaking backwards compatibility**. This is so that we +can react to changes we need for our GitLab.com deployment. We have a +tracking issue open to [remove the experimental +designation](https://gitlab.com/gitlab-com/gl-infra/scalability/issues/147) +from this feature; please comment there if you are interested in using +this in your own deployment. + +In addition to selecting queues by name, as above, the +`experimental_queue_selector` option allows queue groups to be selected +in a more general way using the following components: + +- Attributes that can be selected. +- Operators used to construct a query. + +### Available attributes + +From the [list of all available +attributes](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/workers/all_queues.yml), +`experimental_queue_selector` allows selecting of queues by the +following attributes: + +- `feature_category` - the [GitLab feature + category](https://about.gitlab.com/direction/maturity/#category-maturity) the + queue belongs to. For example, the `merge` queue belongs to the + `source_code_management` category. +- `has_external_dependencies` - whether or not the queue connects to external + services. For example, all importers have this set to `true`. +- `latency_sensitive` - whether or not the queue is particularly sensitive to + latency, which also means that its jobs should run quickly. For example, the + `authorized_projects` queue is used to refresh user permissions, and is + latency sensitive. +- `name` - the queue name. The other attributes are typically more useful as + they are more general, but this is available in case a particular queue needs + to be selected. +- `resource_boundary` - if the worker is bound by `cpu`, `memory`, or + `unknown`. For example, the `project_export` queue is memory bound as it has + to load data in memory before saving it for export. + +Both `has_external_dependencies` and `latency_sensitive` are boolean attributes: +only the exact string `true` is considered true, and everything else is +considered false. + +### Available operators + +`experimental_queue_selector` supports the following operators, listed +from highest to lowest precedence: + +- `|` - the logical OR operator. For example, `query_a|query_b` (where `query_a` + and `query_b` are queries made up of the other operators here) will include + queues that match either query. +- `&` - the logical AND operator. For example, `query_a&query_b` (where + `query_a` and `query_b` are queries made up of the other operators here) will + only include queues that match both queries. +- `!=` - the NOT IN operator. For example, `feature_category!=issue_tracking` + excludes all queues from the `issue_tracking` feature category. +- `=` - the IN operator. For example, `resource_boundary=cpu` includes all + queues that are CPU bound. +- `,` - the concatenate set operator. For example, + `feature_category=continuous_integration,pages` includes all queues from + either the `continuous_integration` category or the `pages` category. This + example is also possible using the OR operator, but allows greater brevity, as + well as being lower precedence. + +The operator precedence for this syntax is fixed: it's not possible to make AND +have higher precedence than OR. + +### Example queries + +In `/etc/gitlab/gitlab.rb`: + +```ruby +sidekiq_cluster['enable'] = true +sidekiq_cluster['experimental_queue_selector'] = true +sidekiq_cluster['queue_groups'] = [ + # Run all non-CPU-bound queues that are latency sensitive + 'resource_boundary!=cpu&latency_sensitive=true', + # Run all continuous integration and pages queues that are not latency sensitive + 'feature_category=continuous_integration,pages&latency_sensitive=false' +] +``` + ## Ignore all GitHub import queues When [importing from GitHub](../../user/project/import/github.md), Sidekiq might diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 03e4fbb3b3f..7d02346af67 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -198,11 +198,11 @@ The Windows Shared Runners are currently in [beta](https://about.gitlab.com/handbook/product/#beta) and should not be used for production workloads. -During the beta period for groups and private projects the use of -Windows Shared Runners will count towards the [shared runner pipeline -quota](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#shared-runners-pipeline-minutes-quota-starter-only) -as if they are Linux Runners, we do have plans to change this in -[#30835](https://gitlab.com/gitlab-org/gitlab/issues/30834). +During the beta period, the +[shared runner pipeline quota](../admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota-starter-only) +will apply for groups and projects in the same way as Linux Runners. +This may change when the beta period ends, as discussed in this +[related issue](https://gitlab.com/gitlab-org/gitlab/issues/30834). Windows Shared Runners on GitLab.com automatically autoscale by launching virtual machines on the Google Cloud Platform. This solution uses @@ -321,18 +321,17 @@ test: - All the limitations mentioned in our [beta definition](https://about.gitlab.com/handbook/product/#beta). - The average provisioning time for a new Windows VM is 5 minutes. - This means that for the beta you will notice slower build start times - on the Windows Shared Runner fleet compared to Linux. In a future - release we will add the ability to the autoscaler which will enable - the pre-warming of virtual machines. This will significantly reduce + This means that you may notice slower build start times + on the Windows Shared Runner fleet during the beta. In a future + release we will update the autoscaler to enable + the pre-provisioning of virtual machines. This will significantly reduce the time it takes to provision a VM on the Windows fleet. You can - follow along in this - [issue](https://gitlab.com/gitlab-org/ci-cd/custom-executor-drivers/autoscaler/issues/32). + follow along in the [related issue](https://gitlab.com/gitlab-org/ci-cd/custom-executor-drivers/autoscaler/issues/32). - The Windows Shared Runner fleet may be unavailable occasionally for maintenance or updates. - The Windows Shared Runner virtual machine instances do not use the - GitLab Docker executor. This means that unlike the Linux Shared - Runners, you will not be able to specify `image` and `services` in + GitLab Docker executor. This means that you will not be able to specify + [`image`](../../ci/yaml/README.md#image) or [`services`](../../ci/yaml/README.md#services) in your pipeline configuration. - For the beta release, we have included a set of software packages in the base VM image. If your CI job requires additional software that's diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index 80075efe4ac..627aeac54d6 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -130,7 +130,7 @@ Once selected, click the **Delete selected** button to confirm the deletion: ![Delete multiple designs](img/delete_multiple_designs_v12_4.png) -NOTE: **Note:** +**Note:** Only the latest version of the designs can be deleted. Deleted designs are not permanently lost; they can be viewed by browsing previous versions. @@ -144,6 +144,9 @@ which you can start a new discussion: ![Starting a new discussion on design](img/adding_note_to_design_1.png) +From GitLab 12.8 on, when you are starting a new discussion, you can adjust the badge's position by +dragging it around the image. + Different discussions have different badge numbers: ![Discussions on design annotations](img/adding_note_to_design_2.png) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b9805973c54..df7c7b15aeb 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -243,39 +243,6 @@ module API expose :startline expose :project_id end - - module Platform - class Kubernetes < Grape::Entity - expose :api_url - expose :namespace - expose :authorization_type - expose :ca_cert - end - end - - module Provider - class Gcp < Grape::Entity - expose :cluster_id - expose :status_name - expose :gcp_project_id - expose :zone - expose :machine_type - expose :num_nodes - expose :endpoint - end - end - - module InternalPostReceive - class Message < Grape::Entity - expose :message - expose :type - end - - class Response < Grape::Entity - expose :messages, using: Message - expose :reference_counter_decreased - end - end end end diff --git a/lib/api/entities/internal_post_receive/message.rb b/lib/api/entities/internal_post_receive/message.rb new file mode 100644 index 00000000000..3cfefa84d9b --- /dev/null +++ b/lib/api/entities/internal_post_receive/message.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module InternalPostReceive + class Message < Grape::Entity + expose :message + expose :type + end + end + end +end diff --git a/lib/api/entities/internal_post_receive/response.rb b/lib/api/entities/internal_post_receive/response.rb new file mode 100644 index 00000000000..c33418ed658 --- /dev/null +++ b/lib/api/entities/internal_post_receive/response.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module InternalPostReceive + class Response < Grape::Entity + expose :messages, using: Entities::InternalPostReceive::Message + expose :reference_counter_decreased + end + end + end +end diff --git a/lib/api/entities/platform/kubernetes.rb b/lib/api/entities/platform/kubernetes.rb new file mode 100644 index 00000000000..eeb6d57bb8f --- /dev/null +++ b/lib/api/entities/platform/kubernetes.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Platform + class Kubernetes < Grape::Entity + expose :api_url + expose :namespace + expose :authorization_type + expose :ca_cert + end + end + end +end diff --git a/lib/api/entities/provider/gcp.rb b/lib/api/entities/provider/gcp.rb new file mode 100644 index 00000000000..85f56a9ac1e --- /dev/null +++ b/lib/api/entities/provider/gcp.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + module Provider + class Gcp < Grape::Entity + expose :cluster_id + expose :status_name + expose :gcp_project_id + expose :zone + expose :machine_type + expose :num_nodes + expose :endpoint + end + end + end +end diff --git a/lib/gitlab/sidekiq_config/cli_methods.rb b/lib/gitlab/sidekiq_config/cli_methods.rb index 0676e9df9c5..8f19b557d24 100644 --- a/lib/gitlab/sidekiq_config/cli_methods.rb +++ b/lib/gitlab/sidekiq_config/cli_methods.rb @@ -18,17 +18,39 @@ module Gitlab result end.freeze - def worker_queues(rails_path = Rails.root.to_s) + QUERY_OR_OPERATOR = '|' + QUERY_AND_OPERATOR = '&' + QUERY_CONCATENATE_OPERATOR = ',' + QUERY_TERM_REGEX = %r{^(\w+)(!?=)([\w#{QUERY_CONCATENATE_OPERATOR}]+)}.freeze + + QUERY_PREDICATES = { + feature_category: :to_sym, + has_external_dependencies: lambda { |value| value == 'true' }, + latency_sensitive: lambda { |value| value == 'true' }, + name: :to_s, + resource_boundary: :to_sym + }.freeze + + QueryError = Class.new(StandardError) + InvalidTerm = Class.new(QueryError) + UnknownOperator = Class.new(QueryError) + UnknownPredicate = Class.new(QueryError) + + def all_queues(rails_path = Rails.root.to_s) @worker_queues ||= {} @worker_queues[rails_path] ||= QUEUE_CONFIG_PATHS.flat_map do |path| full_path = File.join(rails_path, path) - queues = File.exist?(full_path) ? YAML.load_file(full_path) : [] - # https://gitlab.com/gitlab-org/gitlab/issues/199230 - queues.map { |queue| queue.is_a?(Hash) ? queue[:name] : queue } + File.exist?(full_path) ? YAML.load_file(full_path) : [] end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def worker_queues(rails_path = Rails.root.to_s) + # https://gitlab.com/gitlab-org/gitlab/issues/199230 + worker_names(all_queues(rails_path)) + end def expand_queues(queues, all_queues = self.worker_queues) return [] if queues.empty? @@ -40,12 +62,64 @@ module Gitlab end end + def query_workers(query_string, queues) + worker_names(queues.select(&query_string_to_lambda(query_string))) + end + def clear_memoization! if instance_variable_defined?('@worker_queues') remove_instance_variable('@worker_queues') end end - # rubocop:enable Gitlab/ModuleWithInstanceVariables + + private + + def worker_names(workers) + workers.map { |queue| queue.is_a?(Hash) ? queue[:name] : queue } + end + + def query_string_to_lambda(query_string) + or_clauses = query_string.split(QUERY_OR_OPERATOR).map do |and_clauses_string| + and_clauses_predicates = and_clauses_string.split(QUERY_AND_OPERATOR).map do |term| + predicate_for_term(term) + end + + lambda { |worker| and_clauses_predicates.all? { |predicate| predicate.call(worker) } } + end + + lambda { |worker| or_clauses.any? { |predicate| predicate.call(worker) } } + end + + def predicate_for_term(term) + match = term.match(QUERY_TERM_REGEX) + + raise InvalidTerm.new("Invalid term: #{term}") unless match + + _, lhs, op, rhs = *match + + predicate_for_op(op, predicate_factory(lhs, rhs.split(QUERY_CONCATENATE_OPERATOR))) + end + + def predicate_for_op(op, predicate) + case op + when '=' + predicate + when '!=' + lambda { |worker| !predicate.call(worker) } + else + # This is unreachable because InvalidTerm will be raised instead, but + # keeping it allows to guard against that changing in future. + raise UnknownOperator.new("Unknown operator: #{op}") + end + end + + def predicate_factory(lhs, values) + values_block = QUERY_PREDICATES[lhs.to_sym] + + raise UnknownPredicate.new("Unknown predicate: #{lhs}") unless values_block + + lambda { |queue| values.map(&values_block).include?(queue[lhs.to_sym]) } + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8b695e1b127..cfb1e7d8ca2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1958,6 +1958,9 @@ msgstr "" msgid "Any Milestone" msgstr "" +msgid "Any branch" +msgstr "" + msgid "Any eligible user" msgstr "" @@ -2033,6 +2036,9 @@ msgstr "" msgid "Apply template" msgstr "" +msgid "Apply this approval rule to any branch or a specific protected branch." +msgstr "" + msgid "Applying a template will replace the existing issue description. Any changes you have made will be lost." msgstr "" @@ -2086,6 +2092,9 @@ msgstr "" msgid "ApprovalRule|Rule name" msgstr "" +msgid "ApprovalRule|Target branch" +msgstr "" + msgid "ApprovalRule|e.g. QA, Security, etc." msgstr "" @@ -13999,6 +14008,9 @@ msgstr "" msgid "Please select a group." msgstr "" +msgid "Please select a valid target branch" +msgstr "" + msgid "Please select and add a member" msgstr "" @@ -19587,6 +19599,12 @@ msgstr "" msgid "This job has not started yet" msgstr "" +msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}." +msgstr "" + +msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. View the %{deploymentLink}." +msgstr "" + msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}." msgstr "" @@ -19602,6 +19620,15 @@ msgstr "" msgid "This job is archived. Only the complete pipeline can be retried." msgstr "" +msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}." +msgstr "" + +msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. This will overwrite the %{deploymentLink}." +msgstr "" + +msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}." +msgstr "" + msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}." msgstr "" @@ -19611,6 +19638,9 @@ msgstr "" msgid "This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}." msgstr "" +msgid "This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}." +msgstr "" + msgid "This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}." msgstr "" diff --git a/package.json b/package.json index 3a5f4edb4ef..4a0d2cde754 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,9 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.6.2", + "@gitlab/at.js": "^1.5.5", "@gitlab/svgs": "^1.96.0", - "@gitlab/ui": "^9.6.0", + "@gitlab/ui": "^9.8.0", "@gitlab/visual-review-tools": "1.5.1", "@sentry/browser": "^5.10.2", "@sourcegraph/code-host-integration": "0.0.30", @@ -49,7 +50,6 @@ "apollo-link": "^1.2.11", "apollo-link-batch-http": "^1.2.11", "apollo-upload-client": "^10.0.0", - "at.js": "^1.5.4", "autosize": "^4.0.0", "aws-sdk": "^2.526.0", "axios": "^0.19.0", diff --git a/spec/factories/deployment_clusters.rb b/spec/factories/deployment_clusters.rb new file mode 100644 index 00000000000..1bdfff79aaf --- /dev/null +++ b/spec/factories/deployment_clusters.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :deployment_cluster, class: 'DeploymentCluster' do + cluster + deployment + kubernetes_namespace { 'the-namespace' } + end +end diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json index 0cfeadfe548..ac37dd084d3 100644 --- a/spec/fixtures/api/schemas/deployment.json +++ b/spec/fixtures/api/schemas/deployment.json @@ -50,7 +50,7 @@ "cluster": { "oneOf": [ { "type": "null" }, - { "$ref": "cluster_basic.json" } + { "$ref": "deployment_cluster.json" } ] }, "manual_actions": { diff --git a/spec/fixtures/api/schemas/cluster_basic.json b/spec/fixtures/api/schemas/deployment_cluster.json index 6f0e77997f0..86497f98dcb 100644 --- a/spec/fixtures/api/schemas/cluster_basic.json +++ b/spec/fixtures/api/schemas/deployment_cluster.json @@ -10,6 +10,12 @@ { "type": "null" }, { "type": "string" } ] + }, + "kubernetes_namespace": { + "oneOf": [ + { "type": "null" }, + { "type": "string" } + ] } }, "additionalProperties": false diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index cdf7b049ab6..ae05ecea9ef 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -15,6 +15,12 @@ "terminal_path": { "type": "string" }, "trigger": { "$ref": "trigger.json" }, "deployment_status": { "$ref": "deployment_status.json" }, + "deployment_cluster": { + "oneOf": [ + { "$ref": "../deployment_cluster.json" }, + { "type": "null" } + ] + }, "runner": { "$ref": "runner.json" }, "runners": { "$ref": "runners.json" }, "has_trace": { "type": "boolean" }, diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 99869c46f3f..f58615000ee 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -4,7 +4,7 @@ import $ from 'jquery'; import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete'; import 'jquery.caret'; -import 'at.js'; +import '@gitlab/at.js'; import { TEST_HOST } from 'helpers/test_constants'; import { getJSONFixture } from 'helpers/fixtures'; diff --git a/spec/javascripts/badges/dummy_badge.js b/spec/javascripts/badges/dummy_badge.js index e8a460cdc76..a0dee89736e 100644 --- a/spec/javascripts/badges/dummy_badge.js +++ b/spec/javascripts/badges/dummy_badge.js @@ -1,9 +1,9 @@ -import _ from 'underscore'; +import { uniqueId } from 'lodash'; import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants'; import { PROJECT_BADGE } from '~/badges/constants'; export const createDummyBadge = () => { - const id = _.uniqueId(); + const id = uniqueId(); return { id, name: 'TestBadge', diff --git a/spec/javascripts/jobs/components/environments_block_spec.js b/spec/javascripts/jobs/components/environments_block_spec.js index 64a59d659a7..4f2359e83b6 100644 --- a/spec/javascripts/jobs/components/environments_block_spec.js +++ b/spec/javascripts/jobs/components/environments_block_spec.js @@ -4,6 +4,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper'; const TEST_CLUSTER_NAME = 'test_cluster'; const TEST_CLUSTER_PATH = 'path/to/test_cluster'; +const TEST_KUBERNETES_NAMESPACE = 'this-is-a-kubernetes-namespace'; describe('Environments block', () => { const Component = Vue.extend(component); @@ -28,17 +29,18 @@ describe('Environments block', () => { last_deployment: { ...lastDeployment }, }); - const createEnvironmentWithCluster = () => ({ - ...environment, - last_deployment: { - ...lastDeployment, - cluster: { name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH }, - }, + const createDeploymentWithCluster = () => ({ name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH }); + + const createDeploymentWithClusterAndKubernetesNamespace = () => ({ + name: TEST_CLUSTER_NAME, + path: TEST_CLUSTER_PATH, + kubernetes_namespace: TEST_KUBERNETES_NAMESPACE, }); - const createComponent = (deploymentStatus = {}) => { + const createComponent = (deploymentStatus = {}, deploymentCluster = {}) => { vm = mountComponent(Component, { deploymentStatus, + deploymentCluster, iconStatus: status, }); }; @@ -62,15 +64,36 @@ describe('Environments block', () => { expect(findText()).toEqual('This job is deployed to environment.'); }); - it('renders info with cluster', () => { - createComponent({ - status: 'last', - environment: createEnvironmentWithCluster(), + describe('when there is a cluster', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toEqual( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); }); - expect(findText()).toEqual( - `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, - ); + describe('when there is a kubernetes namespace', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithClusterAndKubernetesNamespace(), + ); + + expect(findText()).toEqual( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}.`, + ); + }); + }); }); }); @@ -89,15 +112,36 @@ describe('Environments block', () => { expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar'); }); - it('renders info with cluster', () => { - createComponent({ - status: 'out_of_date', - environment: createEnvironmentWithCluster(), + describe('when there is a cluster', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toEqual( + `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`, + ); }); - expect(findText()).toEqual( - `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`, - ); + describe('when there is a kubernetes namespace', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithClusterAndKubernetesNamespace(), + ); + + expect(findText()).toEqual( + `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}. View the most recent deployment.`, + ); + }); + }); }); }); @@ -143,7 +187,7 @@ describe('Environments block', () => { }); describe('without last deployment', () => { - it('renders info about failed deployment', () => { + it('renders info about deployment being created', () => { createComponent({ status: 'creating', environment, @@ -151,6 +195,22 @@ describe('Environments block', () => { expect(findText()).toEqual('This job is creating a deployment to environment.'); }); + + describe('when there is a cluster', () => { + it('inclues information about the cluster', () => { + createComponent( + { + status: 'creating', + environment, + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toEqual( + `This job is creating a deployment to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); + }); + }); }); describe('without environment', () => { @@ -167,10 +227,13 @@ describe('Environments block', () => { describe('with a cluster', () => { it('renders the cluster link', () => { - createComponent({ - status: 'last', - environment: createEnvironmentWithCluster(), - }); + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); expect(findText()).toEqual( `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, @@ -181,18 +244,13 @@ describe('Environments block', () => { describe('when the cluster is missing the path', () => { it('renders the name without a link', () => { - const cluster = { - name: 'the-cluster', - }; - createComponent({ - status: 'last', - environment: Object.assign({}, environment, { - last_deployment: { - ...lastDeployment, - cluster, - }, - }), - }); + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + { name: 'the-cluster' }, + ); expect(findText()).toContain('using cluster the-cluster.'); diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb index 60d946db744..e6d0055df64 100644 --- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb +++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'rspec-parameterized' describe Gitlab::SidekiqConfig::CliMethods do let(:dummy_root) { '/tmp/' } @@ -82,7 +83,7 @@ describe Gitlab::SidekiqConfig::CliMethods do end describe '.expand_queues' do - let(:all_queues) do + let(:worker_queues) do ['cronjob:stuck_import_jobs', 'cronjob:stuck_merge_jobs', 'post_receive'] end @@ -92,25 +93,125 @@ describe Gitlab::SidekiqConfig::CliMethods do expect(described_class.expand_queues(['cronjob'])) .to contain_exactly('cronjob') - allow(described_class).to receive(:worker_queues).and_return(all_queues) + allow(described_class).to receive(:worker_queues).and_return(worker_queues) expect(described_class.expand_queues(['cronjob'])) .to contain_exactly('cronjob', 'cronjob:stuck_import_jobs', 'cronjob:stuck_merge_jobs') end it 'expands queue namespaces to concrete queue names' do - expect(described_class.expand_queues(['cronjob'], all_queues)) + expect(described_class.expand_queues(['cronjob'], worker_queues)) .to contain_exactly('cronjob', 'cronjob:stuck_import_jobs', 'cronjob:stuck_merge_jobs') end it 'lets concrete queue names pass through' do - expect(described_class.expand_queues(['post_receive'], all_queues)) + expect(described_class.expand_queues(['post_receive'], worker_queues)) .to contain_exactly('post_receive') end it 'lets unknown queues pass through' do - expect(described_class.expand_queues(['unknown'], all_queues)) + expect(described_class.expand_queues(['unknown'], worker_queues)) .to contain_exactly('unknown') end end + + describe '.query_workers' do + using RSpec::Parameterized::TableSyntax + + let(:queues) do + [ + { + name: 'a', + feature_category: :category_a, + has_external_dependencies: false, + latency_sensitive: false, + resource_boundary: :cpu + }, + { + name: 'a_2', + feature_category: :category_a, + has_external_dependencies: false, + latency_sensitive: true, + resource_boundary: :none + }, + { + name: 'b', + feature_category: :category_b, + has_external_dependencies: true, + latency_sensitive: true, + resource_boundary: :memory + }, + { + name: 'c', + feature_category: :category_c, + has_external_dependencies: false, + latency_sensitive: false, + resource_boundary: :memory + } + ] + end + + context 'with valid input' do + where(:query, :selected_queues) do + # feature_category + 'feature_category=category_a' | %w(a a_2) + 'feature_category=category_a,category_c' | %w(a a_2 c) + 'feature_category=category_a|feature_category=category_c' | %w(a a_2 c) + 'feature_category!=category_a' | %w(b c) + + # has_external_dependencies + 'has_external_dependencies=true' | %w(b) + 'has_external_dependencies=false' | %w(a a_2 c) + 'has_external_dependencies=true,false' | %w(a a_2 b c) + 'has_external_dependencies=true|has_external_dependencies=false' | %w(a a_2 b c) + 'has_external_dependencies!=true' | %w(a a_2 c) + + # latency_sensitive + 'latency_sensitive=true' | %w(a_2 b) + 'latency_sensitive=false' | %w(a c) + 'latency_sensitive=true,false' | %w(a a_2 b c) + 'latency_sensitive=true|latency_sensitive=false' | %w(a a_2 b c) + 'latency_sensitive!=true' | %w(a c) + + # name + 'name=a' | %w(a) + 'name=a,b' | %w(a b) + 'name=a,a_2|name=b' | %w(a a_2 b) + 'name!=a,a_2' | %w(b c) + + # resource_boundary + 'resource_boundary=memory' | %w(b c) + 'resource_boundary=memory,cpu' | %w(a b c) + 'resource_boundary=memory|resource_boundary=cpu' | %w(a b c) + 'resource_boundary!=memory,cpu' | %w(a_2) + + # combinations + 'feature_category=category_a&latency_sensitive=true' | %w(a_2) + 'feature_category=category_a&latency_sensitive=true|feature_category=category_c' | %w(a_2 c) + end + + with_them do + it do + expect(described_class.query_workers(query, queues)) + .to match_array(selected_queues) + end + end + end + + context 'with invalid input' do + where(:query, :error) do + 'feature_category="category_a"' | described_class::InvalidTerm + 'feature_category=' | described_class::InvalidTerm + 'feature_category~category_a' | described_class::InvalidTerm + 'worker_name=a' | described_class::UnknownPredicate + end + + with_them do + it do + expect { described_class.query_workers(query, queues) } + .to raise_error(error) + end + end + end + end end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 89fb4eb3ff2..257f699a459 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -18,6 +18,7 @@ describe Deployment do it { is_expected.to delegate_method(:commit).to(:project) } it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) } it { is_expected.to delegate_method(:manual_actions).to(:deployable).as(:try) } + it { is_expected.to delegate_method(:kubernetes_namespace).to(:deployment_cluster).as(:kubernetes_namespace) } it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } diff --git a/spec/serializers/cluster_basic_entity_spec.rb b/spec/serializers/deployment_cluster_entity_spec.rb index 8c3307a1837..b22a93fcec7 100644 --- a/spec/serializers/cluster_basic_entity_spec.rb +++ b/spec/serializers/deployment_cluster_entity_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -describe ClusterBasicEntity do +describe DeploymentClusterEntity do describe '#as_json' do - subject { described_class.new(cluster, request: request).as_json } + subject { described_class.new(deployment, request: request).as_json } let(:maintainer) { create(:user) } let(:developer) { create(:user) } @@ -12,26 +12,30 @@ describe ClusterBasicEntity do let(:request) { double(:request, current_user: current_user) } let(:project) { create(:project) } let(:cluster) { create(:cluster, name: 'the-cluster', projects: [project]) } + let(:deployment) { create(:deployment, cluster: cluster) } + let!(:deployment_cluster) { create(:deployment_cluster, cluster: cluster, deployment: deployment) } before do project.add_maintainer(maintainer) project.add_developer(developer) end - it 'matches cluster_basic entity schema' do - expect(subject.as_json).to match_schema('cluster_basic') + it 'matches deployment_cluster entity schema' do + expect(subject.as_json).to match_schema('deployment_cluster') end it 'exposes the cluster details' do expect(subject[:name]).to eq('the-cluster') expect(subject[:path]).to eq("/#{project.full_path}/-/clusters/#{cluster.id}") + expect(subject[:kubernetes_namespace]).to eq(deployment_cluster.kubernetes_namespace) end context 'when the user does not have permission to view the cluster' do let(:current_user) { developer } - it 'does not include the path' do + it 'does not include the path nor the namespace' do expect(subject[:path]).to be_nil + expect(subject[:kubernetes_namespace]).to be_nil end end end diff --git a/yarn.lock b/yarn.lock index ce6a0e94fc1..c667eac957b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -705,6 +705,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@gitlab/at.js@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.5.tgz#5f6bfe6baaef360daa9b038fa78798d7a6a916b4" + integrity sha512-282Dn3SPVsUHVDhMsXgfnv+Rzog0uxecjttxGRQvxh25es1+xvkGQFsvJfkSKJ3X1kHVkSjKf+Tt5Rra+Jhp9g== + "@gitlab/eslint-config@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-2.1.2.tgz#9f4011d3bf15f3e2668a1faa754f0b9804f23f8f" @@ -740,10 +745,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.96.0.tgz#1d32730389e94358dc245e8336912523446d1269" integrity sha512-mhg6kndxDhwjWChKhs5utO6PowlOyFdaCXUrkkxxe2H3cd8DYa40QOEcJeUrSIhkmgIMVesUawesx5tt4Bnnnw== -"@gitlab/ui@^9.6.0": - version "9.6.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.6.0.tgz#13119a56a34be34fd07e761cab0af3c00462159d" - integrity sha512-R0pUa30l/JX/+1K/rZGAjDvCLLoQuodwCxBNzQ5U1ylnnfGclVrM2rBlZT3UlWnMkb9BRhTPn6uoC/HBOAo37g== +"@gitlab/ui@^9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.8.0.tgz#b1a0b5f1f6ac9fdb19b64d74f0f729e3ec182495" + integrity sha512-0VjSTjCCtevdoeByxf5o/OimzV3zt1MMH5DlZSqakML38uoOM0WpgXI/4xAipzfYwiKUW+IWbuyZGJ3ucaJnhQ== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" @@ -1774,11 +1779,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -at.js@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/at.js/-/at.js-1.5.4.tgz#8fc60cc80eadbe4874449b166a818e7ae1d784c1" - integrity sha512-G8mgUb/PqShPoH8AyjuxsTGvIr1o716BtQUKDM44C8qN2W615y7KGJ68MlTGamd0J0D/m28emUkzagaHTdrGZw== - atob@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" |