diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-15 15:08:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-15 15:08:44 +0300 |
commit | 67441623767b3084d594288408bb078b2eb9f83e (patch) | |
tree | 0593430be110f1cbf67531081b0b8e5784efa6e4 | |
parent | b109901317f810e6708d4165e52b334b70297beb (diff) |
Add latest changes from gitlab-org/gitlab@master
75 files changed, 1359 insertions, 519 deletions
diff --git a/app/assets/javascripts/ide/lib/languages/README.md b/app/assets/javascripts/ide/lib/languages/README.md new file mode 100644 index 00000000000..e4d1a4c7818 --- /dev/null +++ b/app/assets/javascripts/ide/lib/languages/README.md @@ -0,0 +1,21 @@ +# Web IDE Languages + +The Web IDE uses the [Monaco editor](https://microsoft.github.io/monaco-editor/) which uses the [Monarch library](https://microsoft.github.io/monaco-editor/monarch.html) for syntax highlighting. +The Web IDE currently supports all langauges defined in the [monaco-languages](https://github.com/microsoft/monaco-languages/tree/master/src) repository. + +## Adding New Languages + +While Monaco supports a wide variety of languages, there's always the chance that it's missing something. +You'll find a list of [unsupported languages in this epic](https://gitlab.com/groups/gitlab-org/-/epics/1474), which is the right place to add more if needed. + +Should you be willing to help us and add support to GitLab for any missing languages, here are the steps to do so: + +1. Create a new issue and add it to [this epic](https://gitlab.com/groups/gitlab-org/-/epics/1474), if it doesn't already exist. +2. Create a new file in this folder called `{languageName}.js`, where `{languageName}` is the name of the language you want to add support for. +3. Follow the [Monarch documentation](https://microsoft.github.io/monaco-editor/monarch.html) to add a configuration for the new language. + - Example: The [`vue.js`](./vue.js) file in the current directory adds support for Vue.js Syntax Highlighting. +4. Add tests for the new langauge implementation in `spec/frontend/ide/lib/languages/{langaugeName}.js`. + - Example: See [`vue_spec.js`](spec/frontend/ide/lib/languages/vue_spec.js). +5. Create a [Merge Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) with your newly added language. + +Thank you! diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index ab32fe18972..7181332a1d6 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -46,7 +46,11 @@ export default { allowedVisibilityOptions: { type: Array, required: false, - default: () => [0, 10, 20], + default: () => [ + visibilityOptions.PRIVATE, + visibilityOptions.INTERNAL, + visibilityOptions.PUBLIC, + ], }, lfsAvailable: { type: Boolean, @@ -118,16 +122,14 @@ export default { const defaults = { visibilityOptions, visibilityLevel: visibilityOptions.PUBLIC, - // TODO: Change all of these to use the visibilityOptions constants - // https://gitlab.com/gitlab-org/gitlab/-/issues/214667 - issuesAccessLevel: 20, - repositoryAccessLevel: 20, - forkingAccessLevel: 20, - mergeRequestsAccessLevel: 20, - buildsAccessLevel: 20, - wikiAccessLevel: 20, - snippetsAccessLevel: 20, - pagesAccessLevel: 20, + issuesAccessLevel: featureAccessLevel.EVERYONE, + repositoryAccessLevel: featureAccessLevel.EVERYONE, + forkingAccessLevel: featureAccessLevel.EVERYONE, + mergeRequestsAccessLevel: featureAccessLevel.EVERYONE, + buildsAccessLevel: featureAccessLevel.EVERYONE, + wikiAccessLevel: featureAccessLevel.EVERYONE, + snippetsAccessLevel: featureAccessLevel.EVERYONE, + pagesAccessLevel: featureAccessLevel.EVERYONE, metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS, containerRegistryEnabled: true, lfsEnabled: true, @@ -180,7 +182,7 @@ export default { }, repositoryEnabled() { - return this.repositoryAccessLevel > 0; + return this.repositoryAccessLevel > featureAccessLevel.NOT_ENABLED; }, visibilityLevelDescription() { @@ -206,40 +208,70 @@ export default { visibilityLevel(value, oldValue) { if (value === visibilityOptions.PRIVATE) { // when private, features are restricted to "only team members" - this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); - this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); - this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); - this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); - this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); - this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); - this.metricsDashboardAccessLevel = Math.min(10, this.metricsDashboardAccessLevel); - if (this.pagesAccessLevel === 20) { + this.issuesAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.issuesAccessLevel, + ); + this.repositoryAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.repositoryAccessLevel, + ); + this.mergeRequestsAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.mergeRequestsAccessLevel, + ); + this.buildsAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.buildsAccessLevel, + ); + this.wikiAccessLevel = Math.min(featureAccessLevel.PROJECT_MEMBERS, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.snippetsAccessLevel, + ); + this.metricsDashboardAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.metricsDashboardAccessLevel, + ); + if (this.pagesAccessLevel === featureAccessLevel.EVERYONE) { // When from Internal->Private narrow access for only members - this.pagesAccessLevel = 10; + this.pagesAccessLevel = featureAccessLevel.PROJECT_MEMBERS; } this.highlightChanges(); } else if (oldValue === visibilityOptions.PRIVATE) { // if changing away from private, make enabled features more permissive - if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; - if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; - if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; - if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; - if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; - if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; - if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20; - if (this.metricsDashboardAccessLevel === 10) this.metricsDashboardAccessLevel = 20; + if (this.issuesAccessLevel > featureAccessLevel.NOT_ENABLED) + this.issuesAccessLevel = featureAccessLevel.EVERYONE; + if (this.repositoryAccessLevel > featureAccessLevel.NOT_ENABLED) + this.repositoryAccessLevel = featureAccessLevel.EVERYONE; + if (this.mergeRequestsAccessLevel > featureAccessLevel.NOT_ENABLED) + this.mergeRequestsAccessLevel = featureAccessLevel.EVERYONE; + if (this.buildsAccessLevel > featureAccessLevel.NOT_ENABLED) + this.buildsAccessLevel = featureAccessLevel.EVERYONE; + if (this.wikiAccessLevel > featureAccessLevel.NOT_ENABLED) + this.wikiAccessLevel = featureAccessLevel.EVERYONE; + if (this.snippetsAccessLevel > featureAccessLevel.NOT_ENABLED) + this.snippetsAccessLevel = featureAccessLevel.EVERYONE; + if (this.pagesAccessLevel === featureAccessLevel.PROJECT_MEMBERS) + this.pagesAccessLevel = featureAccessLevel.EVERYONE; + if (this.metricsDashboardAccessLevel === featureAccessLevel.PROJECT_MEMBERS) + this.metricsDashboardAccessLevel = featureAccessLevel.EVERYONE; this.highlightChanges(); } }, issuesAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + if (value === featureAccessLevel.NOT_ENABLED) + toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === featureAccessLevel.NOT_ENABLED) + toggleHiddenClassBySelector('.issues-feature', false); }, mergeRequestsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + if (value === featureAccessLevel.NOT_ENABLED) + toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === featureAccessLevel.NOT_ENABLED) + toggleHiddenClassBySelector('.merge-requests-feature', false); }, }, diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 0253cc12606..a6651515e47 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -137,18 +137,34 @@ export default { this.onExistingSnippetFetched(); } }, + getAttachedFiles() { + const fileInputs = Array.from(this.$el.querySelectorAll('[name="files[]"]')); + return fileInputs.map(node => node.value); + }, + createMutation() { + return { + mutation: CreateSnippetMutation, + variables: { + input: { + ...this.apiData, + uploadedFiles: this.getAttachedFiles(), + projectPath: this.projectPath, + }, + }, + }; + }, + updateMutation() { + return { + mutation: UpdateSnippetMutation, + variables: { + input: this.apiData, + }, + }; + }, handleFormSubmit() { this.isUpdating = true; this.$apollo - .mutate({ - mutation: this.newSnippet ? CreateSnippetMutation : UpdateSnippetMutation, - variables: { - input: { - ...this.apiData, - projectPath: this.newSnippet ? this.projectPath : undefined, - }, - }, - }) + .mutate(this.newSnippet ? this.createMutation() : this.updateMutation()) .then(({ data }) => { const baseObj = this.newSnippet ? data?.createSnippet : data?.updateSnippet; @@ -176,6 +192,8 @@ export default { <form class="snippet-form js-requires-input js-quick-submit common-note-form" :data-snippet-type="isProjectSnippet ? 'project' : 'personal'" + data-testid="snippet-edit-form" + @submit.prevent="handleFormSubmit" > <gl-loading-icon v-if="isLoading" @@ -211,17 +229,17 @@ export default { <form-footer-actions> <template #prepend> <gl-button - type="submit" category="primary" + type="submit" variant="success" :disabled="updatePrevented" data-qa-selector="submit_button" - @click.prevent="handleFormSubmit" + data-testid="snippet-submit-btn" >{{ saveButtonLabel }}</gl-button > </template> <template #append> - <gl-button data-testid="snippet-cancel-btn" :href="cancelButtonHref">{{ + <gl-button type="cancel" data-testid="snippet-cancel-btn" :href="cancelButtonHref">{{ __('Cancel') }}</gl-button> </template> diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue index 0fe539a5de7..737845d09b8 100644 --- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue @@ -30,7 +30,7 @@ export default { </script> <template> <div class="form-group js-description-input"> - <label>{{ s__('Snippets|Description (optional)') }}</label> + <label for="snippet-description">{{ s__('Snippets|Description (optional)') }}</label> <div class="js-collapsible-input"> <div class="js-collapsed" :class="{ 'd-none': value }"> <gl-form-input @@ -46,22 +46,26 @@ export default { <markdown-field class="js-expanded" :class="{ 'd-none': !value }" + :add-spacing-classes="false" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" > - <textarea - slot="textarea" - class="note-textarea js-gfm-input js-autosize markdown-area" - dir="auto" - data-qa-selector="snippet_description_field" - data-supports-quick-actions="false" - :value="value" - :aria-label="__('Description')" - :placeholder="__('Write a comment or drag your files here…')" - v-bind="$attrs" - @input="$emit('input', $event.target.value)" - > - </textarea> + <template #textarea> + <textarea + id="snippet-description" + ref="textarea" + :value="value" + class="note-textarea js-gfm-input js-autosize markdown-area" + dir="auto" + data-qa-selector="snippet_description_field" + data-supports-quick-actions="false" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" + v-bind="$attrs" + @input="$emit('input', $event.target.value)" + > + </textarea> + </template> </markdown-field> </div> </div> diff --git a/app/controllers/jwks_controller.rb b/app/controllers/jwks_controller.rb new file mode 100644 index 00000000000..ea5da62fb7a --- /dev/null +++ b/app/controllers/jwks_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController + def index + render json: { keys: keys } + end + + private + + def keys + [ + # We keep openid_connect_signing_key so that we can seamlessly + # replace it with ci_jwt_signing_key and remove it on the next release. + # TODO: Remove openid_connect_signing_key in 13.2 + # https://gitlab.com/gitlab-org/gitlab/-/issues/221031 + Rails.application.secrets.openid_connect_signing_key, + Rails.application.secrets.ci_jwt_signing_key + ].compact.map do |key_data| + OpenSSL::PKey::RSA.new(key_data) + .public_key + .to_jwk + .slice(:kty, :kid, :e, :n) + .merge(use: 'sig', alg: 'RS256') + end + end +end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index dfa51229262..0b6c0db211e 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -12,8 +12,8 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action do push_frontend_feature_flag(:junit_pipeline_view, project) - push_frontend_feature_flag(:filter_pipelines_search, default_enabled: true) - push_frontend_feature_flag(:dag_pipeline_tab, default_enabled: true) + push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true) + push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: false) push_frontend_feature_flag(:pipelines_security_report_summary, project) end before_action :ensure_pipeline, only: [:show] diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 4b2081f2977..779c6c0396f 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -24,7 +24,7 @@ module Ci def status strong_memoize(:status) do - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) Gitlab::Ci::Status::Composite .new(@jobs) .status diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 6b0f11dd7ae..5a2279133f0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -391,7 +391,7 @@ module Ci end def ordered_stages - if Feature.enabled?(:ci_atomic_processing, project, default_enabled: false) + if ::Gitlab::Ci::Features.atomic_processing?(project) # The `Ci::Stage` contains all up-to date data # as atomic processing updates all data in-bulk stages @@ -439,7 +439,7 @@ module Ci end def legacy_stages - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) legacy_stages_using_composite_status else legacy_stages_using_sql diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b136b6f2c61..475f82f23ca 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -99,7 +99,7 @@ class CommitStatus < ApplicationRecord # will not be refreshed to pick the change self.processed_will_change! - if Feature.disabled?(:ci_atomic_processing, project) + if !::Gitlab::Ci::Features.atomic_processing?(project) self.processed = nil elsif latest? self.processed = false # force refresh of all dependent ones @@ -287,7 +287,7 @@ class CommitStatus < ApplicationRecord end def schedule_stage_and_pipeline_update - if Feature.enabled?(:ci_atomic_processing, project) + if ::Gitlab::Ci::Features.atomic_processing?(project) # Atomic Processing requires only single Worker PipelineProcessWorker.perform_async(pipeline_id, [id]) else diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 904de3878e2..c885dea862f 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -66,7 +66,7 @@ module HasStatus # 1. By plucking all related objects, # 2. Or executes expensive SQL query def slow_composite_status(project:) - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) Gitlab::Ci::Status::Composite .new(all, with_allow_failure: columns_hash.key?('allow_failure')) .status diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb new file mode 100644 index 00000000000..d1f1aa544cd --- /dev/null +++ b/app/models/group_deploy_key.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class GroupDeployKey < Key + self.table_name = 'group_deploy_keys' + + validates :user, presence: true + + def type + 'DeployKey' + end +end diff --git a/app/models/snippet_input_action.rb b/app/models/snippet_input_action.rb index cfc33852c51..0cc2a3a6be3 100644 --- a/app/models/snippet_input_action.rb +++ b/app/models/snippet_input_action.rb @@ -3,7 +3,7 @@ class SnippetInputAction include ActiveModel::Validations - ACTIONS = %w[create update delete move].freeze + ACTIONS = %i[create update delete move].freeze ACTIONS.each do |action_const| define_method "#{action_const}_action?" do @@ -20,7 +20,7 @@ class SnippetInputAction validate :ensure_same_file_path_and_previous_path, if: :update_action? def initialize(action: nil, previous_path: nil, file_path: nil, content: nil) - @action = action + @action = action&.to_sym @previous_path = previous_path @file_path = file_path @content = content @@ -28,7 +28,7 @@ class SnippetInputAction def to_commit_action { - action: action&.to_sym, + action: action, previous_path: build_previous_path, file_path: file_path, content: content @@ -44,6 +44,7 @@ class SnippetInputAction end def ensure_same_file_path_and_previous_path + return if previous_path.blank? || file_path.blank? return if previous_path == file_path errors.add(:file_path, "can't be different from the previous_path attribute") diff --git a/app/services/ci/create_cross_project_pipeline_service.rb b/app/services/ci/create_cross_project_pipeline_service.rb index a73a2e2b471..1700312b941 100644 --- a/app/services/ci/create_cross_project_pipeline_service.rb +++ b/app/services/ci/create_cross_project_pipeline_service.rb @@ -47,6 +47,7 @@ module Ci # and update the status when the downstream pipeline completes. subject.success! unless subject.dependent? else + subject.options[:downstream_errors] = pipeline.errors.full_messages subject.drop!(:downstream_pipeline_creation_failed) end end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 3f23e81dcdd..80ebe5f5eb6 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -11,7 +11,7 @@ module Ci def execute(trigger_build_ids = nil, initial_process: false) update_retried - if Feature.enabled?(:ci_atomic_processing, pipeline.project) + if ::Gitlab::Ci::Features.atomic_processing?(pipeline.project) Ci::PipelineProcessing::AtomicProcessingService .new(pipeline) .execute diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb index 32c4ab645df..c256de7b35d 100644 --- a/app/services/keys/create_service.rb +++ b/app/services/keys/create_service.rb @@ -2,6 +2,14 @@ module Keys class CreateService < ::Keys::BaseService + attr_accessor :current_user + + def initialize(current_user, params = {}) + @current_user, @params = current_user, params + @ip_address = @params.delete(:ip_address) + @user = params.delete(:user) || current_user + end + def execute key = user.keys.create(params) notification_service.new_key(key) if key.persisted? diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 3049f5a3639..92edde034a6 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,5 +1,5 @@ - test_reports_enabled = Feature.enabled?(:junit_pipeline_view, @project) -- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab) +- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab, @project, default_enabled: false) .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index 4372763fcf7..e7a509abc8b 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -12,6 +12,8 @@ .gl-alert-actions = link_to _('Go to Webhooks'), project_hooks_path(@project), class: 'btn gl-alert-action btn-info new-gl-button' -%h4= s_('Integrations') -%p= s_('Integrations allow you to integrate GitLab with other applications') +%h4= _('Integrations') +- integrations_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('user/project/integrations/overview') } +- webhooks_link_start = '<a href="%{url}">'.html_safe % { url: project_hooks_path(@project) } +%p= _("%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, webhooks_link_start: webhooks_link_start, link_end: '</a>'.html_safe } = render 'shared/integrations/index', integrations: @services diff --git a/app/views/shared/web_hooks/_title_and_docs.html.haml b/app/views/shared/web_hooks/_title_and_docs.html.haml index d74372bcb0f..f00f3473efa 100644 --- a/app/views/shared/web_hooks/_title_and_docs.html.haml +++ b/app/views/shared/web_hooks/_title_and_docs.html.haml @@ -1,5 +1,10 @@ +- webhooks_link_start = '<a href="%{url}">'.html_safe % { url: help_page_path(hook.help_path) } + %h4.gl-mt-0 = page_title -%p - - link = link_to(hook.pluralized_name, help_page_path(hook.help_path)) - = _('%{link} can be used for binding events when something is happening within the project.').html_safe % { link: link } + +- if @project + - integrations_link_start = '<a href="%{url}">'.html_safe % { url: scoped_integrations_path } + %p= _("%{webhooks_link_start}%{webhook_type}%{link_end} enable you to send notifications to web applications in response to events in a group or project. We recommend using an %{integrations_link_start}integration%{link_end} in preference to a webhook.").html_safe % { webhooks_link_start: webhooks_link_start, webhook_type: hook.pluralized_name, integrations_link_start: integrations_link_start, link_end: '</a>'.html_safe } +- else + %p= _("%{webhooks_link_start}%{webhook_type}%{link_end} enable you to send notifications to web applications in response to events in a group or project.").html_safe % { webhooks_link_start: webhooks_link_start, webhook_type: hook.pluralized_name, link_end: '</a>'.html_safe } diff --git a/changelogs/unreleased/200016-display-downstream-pipeline-errors.yml b/changelogs/unreleased/200016-display-downstream-pipeline-errors.yml new file mode 100644 index 00000000000..4457947639f --- /dev/null +++ b/changelogs/unreleased/200016-display-downstream-pipeline-errors.yml @@ -0,0 +1,5 @@ +--- +title: Implement displaying downstream pipeline error details +merge_request: 32844 +author: +type: fixed diff --git a/changelogs/unreleased/210482-update-descriptions-on-the-integrations-and-webhooks-pages-at-the-.yml b/changelogs/unreleased/210482-update-descriptions-on-the-integrations-and-webhooks-pages-at-the-.yml new file mode 100644 index 00000000000..4e8aa0d7ca5 --- /dev/null +++ b/changelogs/unreleased/210482-update-descriptions-on-the-integrations-and-webhooks-pages-at-the-.yml @@ -0,0 +1,5 @@ +--- +title: Change copy of webhooks / integration help text +merge_request: 34301 +author: +type: changed diff --git a/changelogs/unreleased/214607-ci-jwt-signing-key-jwks.yml b/changelogs/unreleased/214607-ci-jwt-signing-key-jwks.yml new file mode 100644 index 00000000000..66052c4a39c --- /dev/null +++ b/changelogs/unreleased/214607-ci-jwt-signing-key-jwks.yml @@ -0,0 +1,5 @@ +--- +title: Use dedicated RSA key to sign CI_JOB_JWT +merge_request: 34249 +author: +type: added diff --git a/changelogs/unreleased/217034-auto-creation-of-issues-for-alerts-off-by-default.yml b/changelogs/unreleased/217034-auto-creation-of-issues-for-alerts-off-by-default.yml new file mode 100644 index 00000000000..c109a644871 --- /dev/null +++ b/changelogs/unreleased/217034-auto-creation-of-issues-for-alerts-off-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Turn off alert issue creation by default +merge_request: 34107 +author: +type: added diff --git a/changelogs/unreleased/217934-snippet-description-files.yml b/changelogs/unreleased/217934-snippet-description-files.yml new file mode 100644 index 00000000000..193188beeb8 --- /dev/null +++ b/changelogs/unreleased/217934-snippet-description-files.yml @@ -0,0 +1,5 @@ +--- +title: Send information about attached files to the GraphQL mutation +merge_request: 34221 +author: +type: fixed diff --git a/changelogs/unreleased/enable-atomic-processing-by-default.yml b/changelogs/unreleased/enable-atomic-processing-by-default.yml new file mode 100644 index 00000000000..691728b4ae4 --- /dev/null +++ b/changelogs/unreleased/enable-atomic-processing-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Enable CI Atomic Processing by default +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/services-usage-1.yml b/changelogs/unreleased/services-usage-1.yml new file mode 100644 index 00000000000..d202273ca8c --- /dev/null +++ b/changelogs/unreleased/services-usage-1.yml @@ -0,0 +1,5 @@ +--- +title: Record audit event when an admin creates a new SSH Key for a user via the API +merge_request: 33859 +author: Rajendra Kadam +type: fixed diff --git a/config/initializers/01_secret_token.rb b/config/initializers/01_secret_token.rb index 8b96727a2a1..3240621164c 100644 --- a/config/initializers/01_secret_token.rb +++ b/config/initializers/01_secret_token.rb @@ -39,7 +39,8 @@ def create_tokens secret_key_base: file_secret_key || generate_new_secure_token, otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token, db_key_base: generate_new_secure_token, - openid_connect_signing_key: generate_new_rsa_private_key + openid_connect_signing_key: generate_new_rsa_private_key, + ci_jwt_signing_key: generate_new_rsa_private_key } missing_secrets = set_missing_keys(defaults) diff --git a/config/routes.rb b/config/routes.rb index 598a52cddb3..3fcc9941250 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -171,9 +171,8 @@ Rails.application.routes.draw do resources :abuse_reports, only: [:new, :create] # JWKS (JSON Web Key Set) endpoint - # Used by third parties to verify CI_JOB_JWT, placeholder route - # in case we decide to move away from doorkeeper-openid_connect - get 'jwks' => 'doorkeeper/openid_connect/discovery#keys' + # Used by third parties to verify CI_JOB_JWT + get 'jwks' => 'jwks#index' end # End of the /-/ scope. diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index d73f6bf4f10..54559af4066 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -36,36 +36,22 @@ Please consider creating a merge request to for them. MARKDOWN -NO_REVIEWER = 'No reviewer available'.freeze -NO_MAINTAINER = 'No maintainer available'.freeze +OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze +NOT_AVAILABLE_TEMPLATE = 'No %{role} available'.freeze -Spin = Struct.new(:reviewer, :maintainer) - -def spin_role_for_category(team, role, project, category) - team.select do |member| - member.public_send("#{role}?", project, category, gitlab.mr_labels) # rubocop:disable GitlabSecurity/PublicSend +def note_for_category_role(spin, role) + if spin.optional_role == role + return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) } end -end - -def spin_for_category(team, project, category, branch_name) - reviewers, traintainers, maintainers = - %i[reviewer traintainer maintainer].map do |role| - spin_role_for_category(team, role, project, category) - end - # TODO: take CODEOWNERS into account? - # https://gitlab.com/gitlab-org/gitlab/issues/26723 - - # Make traintainers have triple the chance to be picked as a reviewer - random = roulette.new_random(branch_name) - reviewer = roulette.spin_for_person(reviewers + traintainers + traintainers, random: random) - maintainer = roulette.spin_for_person(maintainers, random: random) - - Spin.new(reviewer, maintainer) + spin.public_send(role)&.markdown_name || NOT_AVAILABLE_TEMPLATE % { role: role } # rubocop:disable GitlabSecurity/PublicSend end -def markdown_row_for_category(category, reviewer, maintainer) - "| #{helper.label_for_category(category)} | #{reviewer&.markdown_name || NO_REVIEWER} | #{maintainer&.markdown_name || NO_MAINTAINER} |" +def markdown_row_for_spin(spin) + reviewer_note = note_for_category_role(spin, :reviewer) + maintainer_note = note_for_category_role(spin, :maintainer) + + "| #{helper.label_for_category(spin.category)} | #{reviewer_note} | #{maintainer_note} |" end changes = helper.changes_by_category @@ -78,43 +64,13 @@ categories = changes.keys - [:unknown] categories << :database if gitlab.mr_labels.include?('database') && !categories.include?(:database) if changes.any? - # Strip leading and trailing CE/EE markers - canonical_branch_name = - roulette.canonical_branch_name(gitlab.mr_json['source_branch']) - - team = - begin - roulette.project_team(helper.project_name) - rescue => err - warn("Reviewer roulette failed to load team data: #{err.message}") - [] - end - project = helper.project_name - unknown = changes.fetch(:unknown, []) - spin_per_category = categories.each_with_object({}) do |category, memo| - memo[category] = spin_for_category(team, project, category, canonical_branch_name) - end + branch_name = gitlab.mr_json['source_branch'] - rows = spin_per_category.map do |category, spin| - reviewer = spin.reviewer - maintainer = spin.maintainer - - case category - when :test - if reviewer.nil? - # Fetch an already picked backend reviewer, or pick one otherwise - reviewer = spin_per_category[:backend]&.reviewer || spin_for_category(team, project, :backend, canonical_branch_name).reviewer - end - when :engineering_productivity - if maintainer.nil? - # Fetch an already picked backend maintainer, or pick one otherwise - maintainer = spin_per_category[:backend]&.maintainer || spin_for_category(team, project, :backend, canonical_branch_name).maintainer - end - end - - markdown_row_for_category(category, reviewer, maintainer) - end + roulette_spins = roulette.spin(project, categories, branch_name) + rows = roulette_spins.map { |spin| markdown_row_for_spin(spin) } + + unknown = changes.fetch(:unknown, []) markdown(MESSAGE) markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty? diff --git a/db/migrate/20200430174637_create_group_deploy_keys.rb b/db/migrate/20200430174637_create_group_deploy_keys.rb new file mode 100644 index 00000000000..9771ae013ea --- /dev/null +++ b/db/migrate/20200430174637_create_group_deploy_keys.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class CreateGroupDeployKeys < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless table_exists?(:group_deploy_keys) + with_lock_retries do + create_table :group_deploy_keys do |t| + t.references :user, foreign_key: { on_delete: :restrict }, index: true + t.timestamps_with_timezone + t.datetime_with_timezone :last_used_at + t.datetime_with_timezone :expires_at + t.text :key, null: false, unique: true + t.text :title + t.text :fingerprint, null: false, unique: true + t.binary :fingerprint_sha256 + + t.index :fingerprint, unique: true + t.index :fingerprint_sha256 + end + end + end + + add_text_limit(:group_deploy_keys, :key, 4096) + add_text_limit(:group_deploy_keys, :title, 255) + add_text_limit(:group_deploy_keys, :fingerprint, 255) + end + + def down + drop_table :group_deploy_keys + end +end diff --git a/db/migrate/20200608214008_change_column_default_project_incident_management_settings.rb b/db/migrate/20200608214008_change_column_default_project_incident_management_settings.rb new file mode 100644 index 00000000000..1bfafb410c0 --- /dev/null +++ b/db/migrate/20200608214008_change_column_default_project_incident_management_settings.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ChangeColumnDefaultProjectIncidentManagementSettings < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def up + change_column_default(:project_incident_management_settings, :create_issue, from: true, to: false) + end + + def down + change_column_default(:project_incident_management_settings, :create_issue, from: false, to: true) + end +end diff --git a/db/migrate/20200609212701_add_incident_settings_to_all_existing_projects.rb b/db/migrate/20200609212701_add_incident_settings_to_all_existing_projects.rb new file mode 100644 index 00000000000..60286e0dca6 --- /dev/null +++ b/db/migrate/20200609212701_add_incident_settings_to_all_existing_projects.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class AddIncidentSettingsToAllExistingProjects < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def up + # Add records to projects project_incident_management_settings + # to preserve behavior for existing projects that + # are using the create issue functionality with the default setting of true + query = <<-SQL + WITH project_ids AS ( + SELECT DISTINCT issues.project_id AS id + FROM issues + LEFT OUTER JOIN project_incident_management_settings + ON project_incident_management_settings.project_id = issues.project_id + INNER JOIN label_links + ON label_links.target_type = 'Issue' + AND label_links.target_id = issues.id + INNER JOIN labels + ON labels.id = label_links.label_id + WHERE ( project_incident_management_settings.project_id IS NULL ) + -- Use incident labels even though they could be manually added by users who + -- are not using alert funtionality. + AND labels.title = 'incident' + AND labels.color = '#CC0033' + AND labels.description = 'Denotes a disruption to IT services and the associated issues require immediate attention' + ) + INSERT INTO project_incident_management_settings (project_id, create_issue, send_email, issue_template_key) + SELECT project_ids.id, TRUE, FALSE, NULL + FROM project_ids + ON CONFLICT (project_id) DO NOTHING; + SQL + + execute(query) + end + + def down + # no-op + end +end diff --git a/db/structure.sql b/db/structure.sql index 98d397c801c..87f6a280791 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -3181,6 +3181,31 @@ CREATE TABLE public.group_deletion_schedules ( marked_for_deletion_on date NOT NULL ); +CREATE TABLE public.group_deploy_keys ( + id bigint NOT NULL, + user_id bigint, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + last_used_at timestamp with time zone, + expires_at timestamp with time zone, + key text NOT NULL, + title text, + fingerprint text NOT NULL, + fingerprint_sha256 bytea, + CONSTRAINT check_cc0365908d CHECK ((char_length(title) <= 255)), + CONSTRAINT check_e4526dcf91 CHECK ((char_length(fingerprint) <= 255)), + CONSTRAINT check_f58fa0a0f7 CHECK ((char_length(key) <= 4096)) +); + +CREATE SEQUENCE public.group_deploy_keys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.group_deploy_keys_id_seq OWNED BY public.group_deploy_keys.id; + CREATE TABLE public.group_deploy_tokens ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -5241,7 +5266,7 @@ ALTER SEQUENCE public.project_import_data_id_seq OWNED BY public.project_import_ CREATE TABLE public.project_incident_management_settings ( project_id integer NOT NULL, - create_issue boolean DEFAULT true NOT NULL, + create_issue boolean DEFAULT false NOT NULL, send_email boolean DEFAULT false NOT NULL, issue_template_key text ); @@ -7689,6 +7714,8 @@ ALTER TABLE ONLY public.grafana_integrations ALTER COLUMN id SET DEFAULT nextval ALTER TABLE ONLY public.group_custom_attributes ALTER COLUMN id SET DEFAULT nextval('public.group_custom_attributes_id_seq'::regclass); +ALTER TABLE ONLY public.group_deploy_keys ALTER COLUMN id SET DEFAULT nextval('public.group_deploy_keys_id_seq'::regclass); + ALTER TABLE ONLY public.group_deploy_tokens ALTER COLUMN id SET DEFAULT nextval('public.group_deploy_tokens_id_seq'::regclass); ALTER TABLE ONLY public.group_group_links ALTER COLUMN id SET DEFAULT nextval('public.group_group_links_id_seq'::regclass); @@ -8497,6 +8524,9 @@ ALTER TABLE ONLY public.group_custom_attributes ALTER TABLE ONLY public.group_deletion_schedules ADD CONSTRAINT group_deletion_schedules_pkey PRIMARY KEY (group_id); +ALTER TABLE ONLY public.group_deploy_keys + ADD CONSTRAINT group_deploy_keys_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.group_deploy_tokens ADD CONSTRAINT group_deploy_tokens_pkey PRIMARY KEY (id); @@ -9953,6 +9983,12 @@ CREATE INDEX index_group_deletion_schedules_on_marked_for_deletion_on ON public. CREATE INDEX index_group_deletion_schedules_on_user_id ON public.group_deletion_schedules USING btree (user_id); +CREATE UNIQUE INDEX index_group_deploy_keys_on_fingerprint ON public.group_deploy_keys USING btree (fingerprint); + +CREATE INDEX index_group_deploy_keys_on_fingerprint_sha256 ON public.group_deploy_keys USING btree (fingerprint_sha256); + +CREATE INDEX index_group_deploy_keys_on_user_id ON public.group_deploy_keys USING btree (user_id); + CREATE INDEX index_group_deploy_tokens_on_deploy_token_id ON public.group_deploy_tokens USING btree (deploy_token_id); CREATE UNIQUE INDEX index_group_deploy_tokens_on_group_and_deploy_token_ids ON public.group_deploy_tokens USING btree (group_id, deploy_token_id); @@ -12177,6 +12213,9 @@ ALTER TABLE ONLY public.clusters_applications_knative ALTER TABLE ONLY public.terraform_states ADD CONSTRAINT fk_rails_558901b030 FOREIGN KEY (locked_by_user_id) REFERENCES public.users(id); +ALTER TABLE ONLY public.group_deploy_keys + ADD CONSTRAINT fk_rails_5682fc07f8 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE RESTRICT; + ALTER TABLE ONLY public.issue_user_mentions ADD CONSTRAINT fk_rails_57581fda73 FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE; @@ -13831,6 +13870,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200429182245 20200430103158 20200430130048 +20200430174637 20200505164958 20200505171834 20200505172405 @@ -13925,6 +13965,8 @@ COPY "schema_migrations" (version) FROM STDIN; 20200605003204 20200608072931 20200608075553 +20200608214008 20200609002841 +20200609212701 \. diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md index ae0f46408dc..0a3ade1acf1 100644 --- a/doc/administration/reference_architectures/2k_users.md +++ b/doc/administration/reference_architectures/2k_users.md @@ -10,72 +10,73 @@ For a full list of reference architectures, see > - **Supported users (approximate):** 2,000 > - **High Availability:** False -> - **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS +> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS | Service | Nodes | Configuration | GCP | AWS | Azure | |--------------------------------------------------------------|-----------|---------------------------------|---------------|-----------------------|----------------| -| Load balancer | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 | -| Object Storage | n/a | n/a | n/a | n/a | n/a | -| NFS Server (optional, not recommended) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 | -| PostgreSQL | 1 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 | -| Redis | 1 | 1 vCPU, 3.75GB Memory | n1-standard-1 | m5.large | D2s v3 | -| Gitaly | 1 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 | -| GitLab Rails | 2 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | F8s v2 | -| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 | - -The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms) -CPU platform on GCP. On different hardware you may find that adjustments, either lower -or higher, are required for your CPU or Node counts accordingly. For more information, a -[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found -[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks). - -AWS-equivalent and Azure-equivalent configurations are rough suggestions -and may change in the future. They have not been tested and validated. - -For data objects such as LFS, Uploads, Artifacts, etc, an [object storage service](#configure-the-object-storage) -is recommended over NFS where possible, due to better performance and availability. -Since this doesn't require a node to be set up, it's marked as not applicable (n/a) -in the table above. +| Load balancer | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 | +| Object storage | n/a | n/a | n/a | n/a | n/a | +| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 | +| PostgreSQL | 1 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 | +| Redis | 1 | 1 vCPU, 3.75GB memory | n1-standard-1 | m5.large | D2s v3 | +| Gitaly | 1 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 | +| GitLab Rails | 2 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 | +| Monitoring node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 | + +The Google Cloud Platform (GCP) architectures were built and tested using the +[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms) +CPU platform. On different hardware you may find that adjustments, either lower +or higher, are required for your CPU or node counts. For more information, see +our [Sysbench](https://github.com/akopytov/sysbench)-based +[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks). + +AWS-equivalent and Azure-equivalent configurations are rough suggestions that +may change in the future, and haven't been tested or validated. + +Due to better performance and availability, for data objects (such as LFS, +uploads, or artifacts), using an [object storage service](#configure-the-object-storage) +is recommended instead of using NFS. Using an object storage service also +doesn't require you to provision and maintain a node. ## Setup components To set up GitLab and its components to accommodate up to 2,000 users: 1. [Configure the external load balancing node](#configure-the-load-balancer) - that will handle the load balancing of the two GitLab application services nodes. -1. [Configure the Object Storage](#configure-the-object-storage) - used for shared data objects. -1. [Configure NFS (Optional)](#configure-nfs-optional) - to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although - not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using - that feature. -1. [Configure PostgreSQL](#configure-postgresql), - the database for GitLab. + to handle the load balancing of the two GitLab application services nodes. +1. [Configure the object storage](#configure-the-object-storage) used for + shared data objects. +1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended) + to have shared disk storage service as an alternative to Gitaly or object + storage. You can skip this step if you're not using GitLab Pages (which + requires NFS). +1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab. 1. [Configure Redis](#configure-redis). -1. [Configure Gitaly](#configure-gitaly), - which provides access to the Git repositories. +1. [Configure Gitaly](#configure-gitaly), which provides access to the Git + repositories. 1. [Configure the main GitLab Rails application](#configure-gitlab-rails) - to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend requests (UI, API, Git - over HTTP/SSH). -1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment. + to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend + requests (which include UI, API, and Git over HTTP/SSH). +1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab + environment. ## Configure the load balancer NOTE: **Note:** -This architecture has been tested and validated with [HAProxy](https://www.haproxy.org/) -as the load balancer. Although other load balancers with similar feature sets -could also be used, those load balancers have not been validated. +This architecture has been tested and validated with [HAProxy](https://www.haproxy.org/). +Although you can use a load balancer with a similar set of features, GitLab +hasn't validated other load balancers. -In an active/active GitLab configuration, you will need a load balancer to route -traffic to the application servers. The specifics on which load balancer to use -or the exact configuration is beyond the scope of GitLab documentation. We hope -that if you're managing multi-node systems like GitLab you have a load balancer of -choice already. Some examples including HAProxy (open-source), F5 Big-IP LTM, -and Citrix Net Scaler. This documentation will outline what ports and protocols -you need to use with GitLab. +In an active/active GitLab configuration, you'll need a load balancer to route +traffic to the application servers. The specifics for which load balancer to +use or its exact configuration is out of scope for the GitLab documentation. +If you're managing multi-node systems (including GitLab) you'll probably +already have a load balancer of choice. Some examples including HAProxy +(open-source), F5 Big-IP LTM, and Citrix Net Scaler. This documentation +includes the ports and protocols for use with GitLab. -The next question is how you will handle SSL in your environment. -There are several different options: +The next question is how you will handle SSL in your environment. There are +several different options: - [The application node terminates SSL](#application-node-terminates-ssl). - [The load balancer terminates SSL without backend SSL](#load-balancer-terminates-ssl-without-backend-ssl) @@ -85,83 +86,82 @@ There are several different options: ### Application node terminates SSL -Configure your load balancer to pass connections on port 443 as `TCP` rather -than `HTTP(S)` protocol. This will pass the connection to the application node's -NGINX service untouched. NGINX will have the SSL certificate and listen on port 443. +Configure your load balancer to pass connections on port 443 as `TCP` instead +of `HTTP(S)`. This will pass the connection unaltered to the application node's +NGINX service, which has the SSL certificate and listens to port 443. -See the [NGINX HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) -for details on managing SSL certificates and configuring NGINX. +For details about managing SSL certificates and configuring NGINX, see the +[NGINX HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https). ### Load balancer terminates SSL without backend SSL -Configure your load balancer to use the `HTTP(S)` protocol rather than `TCP`. -The load balancer will then be responsible for managing SSL certificates and +Configure your load balancer to use the `HTTP(S)` protocol instead of `TCP`. +The load balancer will be responsible for both managing SSL certificates and terminating SSL. -Since communication between the load balancer and GitLab will not be secure, -there is some additional configuration needed. See the -[NGINX proxied SSL documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#supporting-proxied-ssl) -for details. +Due to communication between the load balancer and GitLab not being secure, +you'll need to complete some additional configuration. For details, see the +[NGINX proxied SSL documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#supporting-proxied-ssl). ### Load balancer terminates SSL with backend SSL -Configure your load balancer(s) to use the 'HTTP(S)' protocol rather than 'TCP'. -The load balancer(s) will be responsible for managing SSL certificates that -end users will see. +Configure your load balancers (or single balancer, if you have only one) to use +the `HTTP(S)` protocol rather than `TCP`. The load balancers will be +responsible for the managing SSL certificates for end users. -Traffic will also be secure between the load balancer(s) and NGINX in this -scenario. There is no need to add configuration for proxied SSL since the -connection will be secure all the way. However, configuration will need to be -added to GitLab to configure SSL certificates. See -[NGINX HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) -for details on managing SSL certificates and configuring NGINX. +Traffic will be secure between the load balancers and NGINX in this scenario, +and there's no need to add a configuration for proxied SSL. However, you'll +need to add a configuration to GitLab to configure SSL certificates. For +details about managing SSL certificates and configuring NGINX, see the +[NGINX HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https). ### Ports -The basic ports to be used are shown in the table below. +The basic load balancer ports you should use are described in the following +table: -| LB Port | Backend Port | Protocol | +| Port | Backend Port | Protocol | | ------- | ------------ | ------------------------ | | 80 | 80 | HTTP (*1*) | | 443 | 443 | TCP or HTTPS (*1*) (*2*) | | 22 | 22 | TCP | -- (*1*): [Web terminal](../../ci/environments/index.md#web-terminals) support requires - your load balancer to correctly handle WebSocket connections. When using - HTTP or HTTPS proxying, this means your load balancer must be configured - to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the - [web terminal](../integration/terminal.md) integration guide for - more details. -- (*2*): When using HTTPS protocol for port 443, you will need to add an SSL - certificate to the load balancers. If you wish to terminate SSL at the - GitLab application server instead, use TCP protocol. +- (*1*): [Web terminal](../../ci/environments/index.md#web-terminals) support + requires your load balancer to correctly handle WebSocket connections. + When using HTTP or HTTPS proxying, your load balancer must be configured + to pass through the `Connection` and `Upgrade` hop-by-hop headers. For + details, see the [web terminal](../integration/terminal.md) integration guide. +- (*2*): When using the HTTPS protocol for port 443, you'll need to add an SSL + certificate to the load balancers. If you need to terminate SSL at the + GitLab application server, use the TCP protocol. If you're using GitLab Pages with custom domain support you will need some -additional port configurations. -GitLab Pages requires a separate virtual IP address. Configure DNS to point the -`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the -[GitLab Pages documentation](../pages/index.md) for more information. +additional port configurations. GitLab Pages requires a separate virtual IP +address. Configure DNS to point the `pages_external_url` from +`/etc/gitlab/gitlab.rb` to the new virtual IP address. For more information, +see the [GitLab Pages documentation](../pages/index.md). -| LB Port | Backend Port | Protocol | +| Port | Backend Port | Protocol | | ------- | ------------- | --------- | | 80 | Varies (*1*) | HTTP | | 443 | Varies (*1*) | TCP (*2*) | - (*1*): The backend port for GitLab Pages depends on the `gitlab_pages['external_http']` and `gitlab_pages['external_https']` - setting. See [GitLab Pages documentation](../pages/index.md) for more details. -- (*2*): Port 443 for GitLab Pages should always use the TCP protocol. Users can - configure custom domains with custom SSL, which would not be possible - if SSL was terminated at the load balancer. + settings. For details, see the [GitLab Pages documentation](../pages/index.md). +- (*2*): Port 443 for GitLab Pages must use the TCP protocol. Users can + configure custom domains with custom SSL, which wouldn't be possible if SSL + was terminated at the load balancer. #### Alternate SSH Port Some organizations have policies against opening SSH port 22. In this case, -it may be helpful to configure an alternate SSH hostname that allows users -to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address -compared to the other GitLab HTTP configuration above. +it may be helpful to configure an alternate SSH hostname that instead allows +users to use SSH over port 443. An alternate SSH hostname requires a new +virtual IP address compared to the previously described GitLab HTTP +configuration. -Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. +Configure DNS for an alternate SSH hostname, such as `altssh.gitlab.example.com`: | LB Port | Backend Port | Protocol | | ------- | ------------ | -------- | @@ -175,49 +175,49 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. ## Configure the object storage -GitLab supports using an object storage service for holding numerous types of data. -It's recommended over [NFS](#configure-nfs-optional) and in general it's better -in larger setups as object storage is typically much more performant, reliable, -and scalable. +GitLab supports using an object storage service for holding several types of +data, and is recommended over [NFS](#configure-nfs-optional). In general, +object storage services are better for larger environments, as object storage +is typically much more performant, reliable, and scalable. -Object storage options that GitLab has tested, or is aware of customers using include: +Object storage options that GitLab has either tested or is aware of customers +using, includes: -- SaaS/Cloud solutions such as [Amazon S3](https://aws.amazon.com/s3/), [Google cloud storage](https://cloud.google.com/storage). -- On-premises hardware and appliances from various storage vendors. -- MinIO. There is [a guide to deploying this](https://docs.gitlab.com/charts/advanced/external-object-storage/minio.html) within our Helm Chart documentation. +- SaaS/Cloud solutions (such as [Amazon S3](https://aws.amazon.com/s3/) or + [Google Cloud Storage](https://cloud.google.com/storage)). +- On-premises hardware and appliances, from various storage vendors. +- MinIO ([Deployment guide](https://docs.gitlab.com/charts/advanced/external-object-storage/minio.html)). -For configuring GitLab to use Object Storage refer to the following guides -based on what features you intend to use: +To configure GitLab to use object storage, refer to the following guides based +on the features you intend to use: -1. Configure [object storage for backups](../../raketasks/backup_restore.md#uploading-backups-to-a-remote-cloud-storage). -1. Configure [object storage for job artifacts](../job_artifacts.md#using-object-storage) +1. [Object storage for backups](../../raketasks/backup_restore.md#uploading-backups-to-a-remote-cloud-storage). +1. [Object storage for job artifacts](../job_artifacts.md#using-object-storage) including [incremental logging](../job_logs.md#new-incremental-logging-architecture). -1. Configure [object storage for LFS objects](../lfs/index.md#storing-lfs-objects-in-remote-object-storage). -1. Configure [object storage for uploads](../uploads.md#using-object-storage-core-only). -1. Configure [object storage for merge request diffs](../merge_request_diffs.md#using-object-storage). -1. Configure [object storage for Container Registry](../packages/container_registry.md#container-registry-storage-driver) (optional feature). -1. Configure [object storage for Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage) (optional feature). -1. Configure [object storage for packages](../packages/index.md#using-object-storage) (optional feature). **(PREMIUM ONLY)** -1. Configure [object storage for Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature). **(PREMIUM ONLY)** -1. Configure [object storage for Pseudonymizer](../pseudonymizer.md#configuration) (optional feature). **(ULTIMATE ONLY)** -1. Configure [object storage for autoscale Runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional - for improved performance). -1. Configure [object storage for Terraform state files](../terraform_state.md#using-object-storage-core-only). +1. [Object storage for LFS objects](../lfs/index.md#storing-lfs-objects-in-remote-object-storage). +1. [Object storage for uploads](../uploads.md#using-object-storage-core-only). +1. [Object storage for merge request diffs](../merge_request_diffs.md#using-object-storage). +1. [Object storage for Container Registry](../packages/container_registry.md#container-registry-storage-driver) (optional feature). +1. [Object storage for Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage) (optional feature). +1. [Object storage for packages](../packages/index.md#using-object-storage) (optional feature). **(PREMIUM ONLY)** +1. [Object storage for Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature). **(PREMIUM ONLY)** +1. [Object storage for Pseudonymizer](../pseudonymizer.md#configuration) (optional feature). **(ULTIMATE ONLY)** +1. [Object storage for autoscale Runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional, for improved performance). +1. [Object storage for Terraform state files](../terraform_state.md#using-object-storage-core-only). Using separate buckets for each data type is the recommended approach for GitLab. -A limitation of our configuration is that each use of object storage is separately configured. -[We have an issue for improving this](https://gitlab.com/gitlab-org/gitlab/-/issues/23345) -and easily using one bucket with separate folders is one improvement that this might bring. +A limitation of our configuration is that each use of object storage is +separately configured. We have an [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/23345) +for improving this, which would allow for one bucket with separate folders. -There is at least one specific issue with using the same bucket: -when GitLab is deployed with the Helm chart restore from backup -[will not properly function](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer) -unless separate buckets are used. - -One risk of using a single bucket would be if your organization decided to -migrate GitLab to the Helm deployment in the future. GitLab would run, but the situation with -backups might not be realized until the organization had a critical requirement for the backups to -work. +Using a single bucket when GitLab is deployed with the Helm chart causes +restoring from a backup to +[not function properly](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer). +Although you may not be using a Helm deployment right now, if you migrate +GitLab to a Helm deployment later, GitLab would still work, but you may not +realize backups aren't working correctly until a critical requirement for +functioning backups is encountered. <div align="right"> <a type="button" class="btn btn-default" href="#setup-components"> @@ -227,11 +227,12 @@ work. ## Configure NFS (optional) -[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly) -are recommended over NFS wherever possible for improved performance. If you intend -to use GitLab Pages, this currently [requires NFS](troubleshooting.md#gitlab-pages-requires-nfs). +For improved performance, [object storage](#configure-the-object-storage), +along with [Gitaly](#configure-gitaly), are recommended over using NFS whenever +possible. However, if you intend to use GitLab Pages, +[you must use NFS](troubleshooting.md#gitlab-pages-requires-nfs). -See how to [configure NFS](../high_availability/nfs.md). +For information about configuring NFS, see the [NFS documentation page](../high_availability/nfs.md). <div align="right"> <a type="button" class="btn btn-default" href="#setup-components"> @@ -247,14 +248,14 @@ to be used with GitLab. ### Provide your own PostgreSQL instance If you're hosting GitLab on a cloud provider, you can optionally use a -managed service for PostgreSQL. For example, AWS offers a managed Relational -Database Service (RDS) that runs PostgreSQL. +managed service for PostgreSQL. For example, AWS offers a managed relational +database service (RDS) that runs PostgreSQL. If you use a cloud-managed service, or provide your own PostgreSQL: 1. Set up PostgreSQL according to the [database requirements document](../../install/requirements.md#database). -1. Set up a `gitlab` username with a password of your choice. The `gitlab` user +1. Create a `gitlab` username with a password of your choice. The `gitlab` user needs privileges to create the `gitlabhq_production` database. 1. Configure the GitLab application servers with the appropriate details. This step is covered in [Configuring the GitLab Rails application](#configure-gitlab-rails). diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md index 7dbf1812920..fff0fda0ab4 100644 --- a/doc/ci/directed_acyclic_graph/index.md +++ b/doc/ci/directed_acyclic_graph/index.md @@ -82,11 +82,10 @@ are certain use cases that you may need to work around. For more information: ## DAG Visualization > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215517) in GitLab 13.1 as a [Beta feature](https://about.gitlab.com/handbook/product/#beta). -> - It's deployed behind a feature flag, enabled by default. +> - It's deployed behind a feature flag, disabled by default. > - It's enabled on GitLab.com. -> - It's not able to be enabled or disabled per-project > - It's not recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-dag-visualization-core-only) +> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-dag-visualization-core-only) The DAG visualization makes it easier to visualize the relationships between dependent jobs in a DAG. This graph will display all the jobs in a pipeline that need or are needed by other jobs. Jobs with no relationships are not displayed in this view. @@ -100,18 +99,13 @@ Clicking a node will highlight all the job paths it depends on. DAG Visualization is under development and requires more testing, but is being made available as a beta features so users can check its limitations and uses. -It is deployed behind a feature flag that is **enabled by default**. +It is deployed behind a feature flag that is **disabled by default**. [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) -can opt to disable it for your instance. It cannot be enabled or disabled per-project. - -To disable it: - -```ruby -Feature.disable(:dag_pipeline_tab) -``` - -To enable it: +can opt to enable it for your instance: ```ruby +# Instance-wide Feature.enable(:dag_pipeline_tab) +# or by project +Feature.enable(:dag_pipeline_tab, Project.find(<project id>)) ``` diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md index 3b8be54ae59..489c7425797 100644 --- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md +++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md @@ -50,7 +50,7 @@ The JWT's payload looks like this: } ``` -The JWT is encoded by using RS256 and signed with your GitLab instance's OpenID Connect private key. The expire time for the token will be set to job's timeout, if specifed, or 5 minutes if it is not. The key used to sign this token may change without any notice. In such case retrying the job will generate new JWT using the current signing key. +The JWT is encoded by using RS256 and signed with a dedicated RSA private key. The expire time for the token will be set to job's timeout, if specifed, or 5 minutes if it is not. The key used to sign this token may change without any notice. In such case retrying the job will generate new JWT using the current signing key. You can use this JWT and your instance's JWKS endpoint (`https://gitlab.example.com/-/jwks`) to authenticate with a Vault server that is configured to allow the JWT Authentication method for authentication. diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 8d1cfdc6d04..5bdae998ec9 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -44,6 +44,8 @@ later (once a day). If you need to garbage collect them immediately, run ## Remove unreferenced LFS files +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36628) in GitLab 12.10. + Unreferenced LFS files are removed on a daily basis but you can remove them immediately if you need to. For example: @@ -64,6 +66,8 @@ I, [2020-01-08T20:51:17.148765 #43765] INFO -- : Removed unreferenced LFS files ## Remove garbage from filesystem +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20863) in GitLab 11.2. + Clean up local project upload files if they don't exist in GitLab database. The task attempts to fix the file if it can find its project, otherwise it moves the file to a lost and found directory. @@ -96,6 +100,10 @@ I, [2018-07-27T12:08:33.755624 #89817] INFO -- : Did fix /opt/gitlab/embedded/s I, [2018-07-27T12:08:33.760257 #89817] INFO -- : Did move to lost and found /opt/gitlab/embedded/service/gitlab-rails/public/uploads/foo/bar/1dd6f0f7eefd2acc4c2233f89a0f7b0b/image.png -> /opt/gitlab/embedded/service/gitlab-rails/public/uploads/-/project-lost-found/foo/bar/1dd6f0f7eefd2acc4c2233f89a0f7b0b/image.png ``` +## Remove garbage from object storage + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20918) in GitLab 11.2. + Remove object store upload files if they don't exist in GitLab database. ```shell diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_export_csv_v13_1.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_export_csv_v13_1.png Binary files differnew file mode 100644 index 00000000000..0dfe7b637cd --- /dev/null +++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_export_csv_v13_1.png diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.png Binary files differindex c788e2165ad..4c7b5cc724f 100644 --- a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.png +++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.png diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png Binary files differindex bb88b7f371c..878bb83c2a2 100644 --- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png +++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 52a3d2ec7fe..60798b9c921 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -120,6 +120,21 @@ vulnerabilities are not included either. Read more on how to [interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities). +### Export vulnerabilities + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213013) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.1. + +You can export all your vulnerabilities as CSV by clicking the **{upload}** **Export** button +located at the top right of the **Group Security Dashboard**. After the report builds, the CSV +report downloads to your local machine. The report contains all vulnerabilities for the projects +defined in the **Group Security Dashboard**, as filters don't apply to the export function. + +NOTE: **Note:** +It may take several minutes for the download to start if your project contains thousands of +vulnerabilities. Don't close the page until the download finishes. + +![CSV Export Button](img/group_security_dashboard_export_csv_v13_1.png) + ## Instance Security Dashboard > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6953) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.8. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 4ace36e8eb9..f68e6e913df 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -998,7 +998,7 @@ In GitLab versions 13.1 and greater, you can configure your manually configured >- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4925) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11. >- [From GitLab Ultimate 12.5](https://gitlab.com/gitlab-org/gitlab/-/issues/13401), when GitLab receives a recovery alert, it will automatically close the associated issue. -Alerts can be used to trigger actions, like opening an issue automatically (enabled by default since `12.1`). To configure the actions: +Alerts can be used to trigger actions, like opening an issue automatically (disabled by default since `13.1`). To configure the actions: 1. Navigate to your project's **Settings > Operations > Incidents**. 1. Enable the option to create issues. diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 22b36155004..a6d2f413d3e 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -57,6 +57,25 @@ which applies to the entire Web IDE screen. |---------------------------------------------------------------|-----------------------------------------| | ![Solarized Light Theme](img/solarized_light_theme_v13.0.png) | ![Dark Theme](img/dark_theme_v13.0.png) | +## Configure the Web IDE + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23352) in [GitLab Core](https://about.gitlab.com/pricing/) 13.1. + +The Web IDE supports configuration of certain editor settings by using +[`.editorconfig` files](https://editorconfig.org/). When opening a file, the +Web IDE looks for a file named `.editorconfig` in the current directory +and all parent directories. If a configuration file is found and has settings +that match the file's path, these settings will be enforced on the opened file. + +The Web IDE currently supports the following `.editorconfig` settings: + +- `indent_style` +- `indent_size` +- `end_of_line` +- `trim_trailing_whitespace` +- `tab_width` +- `insert_final_newline` + ## Commit changes > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4539) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.4. diff --git a/lib/api/users.rb b/lib/api/users.rb index ed9dac8b494..831e6efe203 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -264,9 +264,9 @@ module API user = User.find_by(id: params.delete(:id)) not_found!('User') unless user - key = user.keys.new(declared_params(include_missing: false)) + key = ::Keys::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute - if key.save + if key.persisted? present key, with: Entities::SSHKey else render_validation_error!(key) diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 18d1f2183f4..8cc6a5952aa 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -29,6 +29,14 @@ module Gitlab def self.instance_variables_ui_enabled? ::Feature.enabled?(:ci_instance_variables_ui, default_enabled: true) end + + def self.composite_status?(project) + ::Feature.enabled?(:ci_composite_status, project, default_enabled: true) + end + + def self.atomic_processing?(project) + ::Feature.enabled?(:ci_atomic_processing, project, default_enabled: true) + end end end end diff --git a/lib/gitlab/ci/jwt.rb b/lib/gitlab/ci/jwt.rb index 491facd0a43..5555f74c05c 100644 --- a/lib/gitlab/ci/jwt.rb +++ b/lib/gitlab/ci/jwt.rb @@ -60,7 +60,7 @@ module Gitlab end def key - @key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) + @key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.ci_jwt_signing_key) end def public_key diff --git a/lib/gitlab/ci/status/bridge/failed.rb b/lib/gitlab/ci/status/bridge/failed.rb index de7446c238c..b0ab0992594 100644 --- a/lib/gitlab/ci/status/bridge/failed.rb +++ b/lib/gitlab/ci/status/bridge/failed.rb @@ -5,6 +5,14 @@ module Gitlab module Status module Bridge class Failed < Status::Build::Failed + private + + def failure_reason_message + [ + self.class.reasons.fetch(subject.failure_reason.to_sym), + subject.options[:downstream_errors] + ].flatten.compact.join(', ') + end end end end diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb index dbf42912882..9f7980dc20a 100644 --- a/lib/gitlab/danger/roulette.rb +++ b/lib/gitlab/danger/roulette.rb @@ -6,6 +6,46 @@ module Gitlab module Danger module Roulette ROULETTE_DATA_URL = 'https://about.gitlab.com/roulette.json' + OPTIONAL_CATEGORIES = [:qa, :test].freeze + + Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role) + + # Assigns GitLab team members to be reviewer and maintainer + # for each change category that a Merge Request contains. + # + # @return [Array<Spin>] + def spin(project, categories, branch_name) + team = + begin + project_team(project) + rescue => err + warn("Reviewer roulette failed to load team data: #{err.message}") + [] + end + + canonical_branch_name = canonical_branch_name(branch_name) + + spin_per_category = categories.each_with_object({}) do |category, memo| + memo[category] = spin_for_category(team, project, category, canonical_branch_name) + end + + spin_per_category.map do |category, spin| + case category + when :test + if spin.reviewer.nil? + # Fetch an already picked backend reviewer, or pick one otherwise + spin.reviewer = spin_per_category[:backend]&.reviewer || spin_for_category(team, project, :backend, canonical_branch_name).reviewer + end + when :engineering_productivity + if spin.maintainer.nil? + # Fetch an already picked backend maintainer, or pick one otherwise + spin.maintainer = spin_per_category[:backend]&.maintainer || spin_for_category(team, project, :backend, canonical_branch_name).maintainer + end + end + + spin + end + end # Looks up the current list of GitLab team members and parses it into a # useful form @@ -58,6 +98,33 @@ module Gitlab def mr_author?(person) person.username == gitlab.mr_author end + + def spin_role_for_category(team, role, project, category) + team.select do |member| + member.public_send("#{role}?", project, category, gitlab.mr_labels) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def spin_for_category(team, project, category, branch_name) + reviewers, traintainers, maintainers = + %i[reviewer traintainer maintainer].map do |role| + spin_role_for_category(team, role, project, category) + end + + # TODO: take CODEOWNERS into account? + # https://gitlab.com/gitlab-org/gitlab/issues/26723 + + # Make traintainers have triple the chance to be picked as a reviewer + random = new_random(branch_name) + reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random) + maintainer = spin_for_person(maintainers, random: random) + + Spin.new(category, reviewer, maintainer).tap do |spin| + if OPTIONAL_CATEGORIES.include?(category) + spin.optional_role = :maintainer + end + end + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9fa8cc70ffd..f9b9543ec7b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -374,6 +374,9 @@ msgstr "" msgid "%{icon}You are about to add %{usersTag} people to the discussion. Proceed with caution." msgstr "" +msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}." +msgstr "" + msgid "%{issuableType} will be removed! Are you sure?" msgstr "" @@ -428,9 +431,6 @@ msgstr "" msgid "%{link_start}Read more%{link_end} about role permissions" msgstr "" -msgid "%{link} can be used for binding events when something is happening within the project." -msgstr "" - msgid "%{listToShow}, and %{awardsListLength} more." msgstr "" @@ -658,6 +658,12 @@ msgstr "" msgid "%{verb} %{time_spent_value} spent time." msgstr "" +msgid "%{webhooks_link_start}%{webhook_type}%{link_end} enable you to send notifications to web applications in response to events in a group or project." +msgstr "" + +msgid "%{webhooks_link_start}%{webhook_type}%{link_end} enable you to send notifications to web applications in response to events in a group or project. We recommend using an %{integrations_link_start}integration%{link_end} in preference to a webhook." +msgstr "" + msgid "'%{level}' is not a valid visibility level" msgstr "" @@ -12098,9 +12104,6 @@ msgstr "" msgid "Integrations" msgstr "" -msgid "Integrations allow you to integrate GitLab with other applications" -msgstr "" - msgid "Integrations|All details" msgstr "" @@ -19647,6 +19650,9 @@ msgstr "" msgid "SecurityReports|False positive" msgstr "" +msgid "SecurityReports|Group Security Dashboard" +msgstr "" + msgid "SecurityReports|Hide dismissed" msgstr "" diff --git a/package.json b/package.json index 14817dde83d..e06f54fec97 100644 --- a/package.json +++ b/package.json @@ -210,6 +210,7 @@ "timezone-mock": "^1.0.8", "vue-jest": "^4.0.0-beta.2", "webpack-dev-server": "^3.10.3", + "xhr-mock": "^2.5.1", "yarn-check-webpack-plugin": "^1.2.0", "yarn-deduplicate": "^1.1.1" }, diff --git a/spec/controllers/jwks_controller_spec.rb b/spec/controllers/jwks_controller_spec.rb new file mode 100644 index 00000000000..5e4e81421aa --- /dev/null +++ b/spec/controllers/jwks_controller_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JwksController do + describe 'GET #index' do + let(:oidc_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key).to_jwk } + let(:ci_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.ci_jwt_signing_key).to_jwk } + + it 'returns signing keys used to sign CI_JOB_JWT' do + get :index + + expect(response).to have_gitlab_http_status(:ok) + + ids = json_response['keys'].map { |jwk| jwk['kid'] } + expect(ids).to contain_exactly(ci_jwk['kid'], oidc_jwk['kid']) + end + + it 'does not leak private key data' do + get :index + + aggregate_failures do + json_response['keys'].each do |jwk| + expect(jwk.keys).to contain_exactly('kty', 'kid', 'e', 'n', 'use', 'alg') + expect(jwk['use']).to eq('sig') + expect(jwk['alg']).to eq('RS256') + end + end + end + end +end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index 087d2521836..cf52e772ae0 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -13,6 +13,10 @@ FactoryBot.define do factory :deploy_key, class: 'DeployKey' + factory :group_deploy_key, class: 'GroupDeployKey' do + user + end + factory :personal_key do user end diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb index 5c7c83aea6d..4c5033bb1ed 100644 --- a/spec/features/groups/members/leave_group_spec.rb +++ b/spec/features/groups/members/leave_group_spec.rb @@ -31,6 +31,7 @@ describe 'Groups > Members > Leave group' do page.accept_confirm + wait_for_all_requests expect(current_path).to eq(dashboard_groups_path) expect(group.users).not_to include(user) end diff --git a/spec/javascripts/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js index 6f6f20ccca2..688b9164e5f 100644 --- a/spec/javascripts/dropzone_input_spec.js +++ b/spec/frontend/dropzone_input_spec.js @@ -1,7 +1,9 @@ import $ from 'jquery'; +import mock from 'xhr-mock'; import { TEST_HOST } from 'spec/test_constants'; import dropzoneInput from '~/dropzone_input'; import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table'; +import waitForPromises from 'helpers/wait_for_promises'; const TEST_FILE = new File([], 'somefile.jpg'); TEST_FILE.upload = {}; @@ -38,14 +40,16 @@ describe('dropzone_input', () => { it('pastes Markdown tables', () => { const event = $.Event('paste'); const origEvent = new Event('paste'); - const pasteData = new DataTransfer(); - pasteData.setData('text/plain', 'Hello World'); - pasteData.setData('text/html', '<table><tr><td>Hello World</td></tr></table>'); - origEvent.clipboardData = pasteData; + + origEvent.clipboardData = { + types: ['text/plain', 'text/html'], + getData: () => '<table><tr><td>Hello World</td></tr></table>', + items: [], + }; event.originalEvent = origEvent; - spyOn(PasteMarkdownTable.prototype, 'isTable').and.callThrough(); - spyOn(PasteMarkdownTable.prototype, 'convertToTableMarkdown').and.callThrough(); + jest.spyOn(PasteMarkdownTable.prototype, 'isTable'); + jest.spyOn(PasteMarkdownTable.prototype, 'convertToTableMarkdown'); $('.js-gfm-input').trigger(event); @@ -57,53 +61,37 @@ describe('dropzone_input', () => { describe('shows error message', () => { let form; let dropzone; - let xhr; - let oldXMLHttpRequest; beforeEach(() => { + mock.setup(); + form = $(TEMPLATE); dropzone = dropzoneInput(form); - - xhr = jasmine.createSpyObj(Object.keys(XMLHttpRequest.prototype)); - oldXMLHttpRequest = window.XMLHttpRequest; - window.XMLHttpRequest = () => xhr; }); afterEach(() => { - window.XMLHttpRequest = oldXMLHttpRequest; + mock.teardown(); }); - it('when AJAX fails with json', () => { - xhr = { - ...xhr, - statusCode: 400, - readyState: 4, - responseText: JSON.stringify({ message: TEST_ERROR_MESSAGE }), - getResponseHeader: () => 'application/json', - }; - - dropzone.processFile(TEST_FILE); - - xhr.onload(); - - expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE); - }); + beforeEach(() => {}); - it('when AJAX fails with text', () => { - xhr = { - ...xhr, - statusCode: 400, - readyState: 4, - responseText: TEST_ERROR_MESSAGE, - getResponseHeader: () => 'text/plain', - }; + it.each` + responseType | responseBody + ${'application/json'} | ${JSON.stringify({ message: TEST_ERROR_MESSAGE })} + ${'text/plain'} | ${TEST_ERROR_MESSAGE} + `('when AJAX fails with json', ({ responseType, responseBody }) => { + mock.post(TEST_UPLOAD_PATH, { + status: 400, + body: responseBody, + headers: { 'Content-Type': responseType }, + }); dropzone.processFile(TEST_FILE); - xhr.onload(); - - expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE); + return waitForPromises().then(() => { + expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE); + }); }); }); }); diff --git a/spec/javascripts/pdf/index_spec.js b/spec/frontend/pdf/index_spec.js index 39cd4dacd70..0d8caa28fd1 100644 --- a/spec/javascripts/pdf/index_spec.js +++ b/spec/frontend/pdf/index_spec.js @@ -3,6 +3,10 @@ import Vue from 'vue'; import { FIXTURES_PATH } from 'spec/test_constants'; import PDFLab from '~/pdf/index.vue'; +jest.mock('pdfjs-dist/webpack', () => { + return { default: jest.requireActual('pdfjs-dist/build/pdf') }; +}); + const pdf = `${FIXTURES_PATH}/blob/pdf/test.pdf`; const Component = Vue.extend(PDFLab); diff --git a/spec/javascripts/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js index cc2cc204ee3..4e24b0696ec 100644 --- a/spec/javascripts/pdf/page_spec.js +++ b/spec/frontend/pdf/page_spec.js @@ -1,27 +1,14 @@ import Vue from 'vue'; -import pdfjsLib from 'pdfjs-dist/webpack'; - -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { FIXTURES_PATH } from 'spec/test_constants'; import PageComponent from '~/pdf/page/index.vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; -const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`; +jest.mock('pdfjs-dist/webpack', () => { + return { default: jest.requireActual('pdfjs-dist/build/pdf') }; +}); describe('Page component', () => { const Component = Vue.extend(PageComponent); let vm; - let testPage; - - beforeEach(done => { - pdfjsLib - .getDocument(testPDF) - .promise.then(pdf => pdf.getPage(1)) - .then(page => { - testPage = page; - }) - .then(done) - .catch(done.fail); - }); afterEach(() => { vm.$destroy(); @@ -29,7 +16,10 @@ describe('Page component', () => { it('renders the page when mounting', done => { const promise = Promise.resolve(); - spyOn(testPage, 'render').and.returnValue({ promise }); + const testPage = { + render: jest.fn().mockReturnValue({ promise: Promise.resolve() }), + getViewport: jest.fn().mockReturnValue({}), + }; vm = mountComponent(Component, { page: testPage, diff --git a/spec/javascripts/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js index 3957edce9e0..621c7d87a7e 100644 --- a/spec/javascripts/performance_bar/index_spec.js +++ b/spec/frontend/performance_bar/index_spec.js @@ -9,6 +9,11 @@ describe('performance bar wrapper', () => { let vm; beforeEach(() => { + URL.createObjectURL = jest.fn(); + performance.getEntriesByType = jest.fn().mockReturnValue([]); + + // clear html so that elements from previous tests don't mess with this test + document.body.innerHTML = ''; const peekWrapper = document.createElement('div'); peekWrapper.setAttribute('id', 'js-peek'); @@ -49,11 +54,11 @@ describe('performance bar wrapper', () => { describe('loadRequestDetails', () => { beforeEach(() => { - spyOn(vm.store, 'addRequest').and.callThrough(); + jest.spyOn(vm.store, 'addRequest'); }); it('does nothing if the request cannot be tracked', () => { - spyOn(vm.store, 'canTrackRequest').and.callFake(() => false); + jest.spyOn(vm.store, 'canTrackRequest').mockImplementation(() => false); vm.loadRequestDetails('123', 'https://gitlab.com/'); @@ -67,7 +72,7 @@ describe('performance bar wrapper', () => { }); it('makes an HTTP request for the request details', () => { - spyOn(PerformanceBarService, 'fetchRequestDetails').and.callThrough(); + jest.spyOn(PerformanceBarService, 'fetchRequestDetails'); vm.loadRequestDetails('456', 'https://gitlab.com/'); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap index 9fd4cba5b87..297ad16b681 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap @@ -4,7 +4,9 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = <div class="form-group js-description-input" > - <label> + <label + for="snippet-description" + > Description (optional) </label> @@ -21,27 +23,67 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = /> </div> - <markdown-field-stub - addspacingclasses="true" - canattachfile="true" - class="js-expanded" - enableautocomplete="true" - helppagepath="" - markdowndocspath="help/" - markdownpreviewpath="foo/" - note="[object Object]" - quickactionsdocspath="" - textareavalue="" + <div + class="js-vue-markdown-field md-area position-relative js-expanded gfm-form" > - <textarea - aria-label="Description" - class="note-textarea js-gfm-input js-autosize markdown-area" - data-qa-selector="snippet_description_field" - data-supports-quick-actions="false" - dir="auto" - placeholder="Write a comment or drag your files here…" + <markdown-header-stub + linecontent="" /> - </markdown-field-stub> + + <div + class="md-write-holder" + > + <div + class="zen-backdrop div-dropzone-wrapper" + > + <div + class="div-dropzone js-invalid-dropzone" + > + <textarea + aria-label="Description" + class="note-textarea js-gfm-input js-autosize markdown-area" + data-qa-selector="snippet_description_field" + data-supports-quick-actions="false" + dir="auto" + id="snippet-description" + placeholder="Write a comment or drag your files here…" + style="overflow-x: hidden; word-wrap: break-word; overflow-y: hidden;" + /> + <div + class="div-dropzone-hover" + > + <i + class="fa fa-paperclip div-dropzone-icon" + /> + </div> + </div> + + <a + aria-label="Leave zen mode" + class="zen-control zen-control-leave js-zen-leave gl-text-gray-700" + href="#" + > + <icon-stub + name="screen-normal" + size="16" + /> + </a> + + <markdown-toolbar-stub + canattachfile="true" + markdowndocspath="help/" + quickactionsdocspath="" + /> + </div> + </div> + + <div + class="js-vue-md-preview md md-preview-holder" + style="display: none;" + /> + + <!----> + </div> </div> </div> `; diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index c21aba5e251..83f46dd347f 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -40,6 +40,9 @@ const newlyEditedSnippetUrl = 'http://foo.bar'; const apiError = { message: 'Ufff' }; const mutationError = 'Bummer'; +const attachedFilePath1 = 'foo/bar'; +const attachedFilePath2 = 'alpha/beta'; + const defaultProps = { snippetGid: 'gid://gitlab/PersonalSnippet/42', markdownPreviewPath: 'http://preview.foo.bar', @@ -120,8 +123,9 @@ describe('Snippet Edit app', () => { wrapper.destroy(); }); - const findSubmitButton = () => wrapper.find('[type=submit]'); + const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]'); const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]'); + const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit'); describe('rendering', () => { it('renders loader while the query is in flight', () => { @@ -289,13 +293,15 @@ describe('Snippet Edit app', () => { }, }; - wrapper.vm.handleFormSubmit(); + clickSubmitBtn(); + expect(resolveMutate).toHaveBeenCalledWith(mutationPayload); }); it('redirects to snippet view on successful mutation', () => { createComponent(); - wrapper.vm.handleFormSubmit(); + clickSubmitBtn(); + return waitForPromises().then(() => { expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl); }); @@ -322,7 +328,8 @@ describe('Snippet Edit app', () => { mutationRes: mutationTypes.RESOLVE_WITH_ERRORS, }); - wrapper.vm.handleFormSubmit(); + clickSubmitBtn(); + return waitForPromises().then(() => { expect(redirectTo).not.toHaveBeenCalled(); expect(flashSpy).toHaveBeenCalledWith(mutationError); @@ -334,7 +341,9 @@ describe('Snippet Edit app', () => { createComponent({ mutationRes: mutationTypes.REJECT, }); - wrapper.vm.handleFormSubmit(); + + clickSubmitBtn(); + return waitForPromises().then(() => { expect(redirectTo).not.toHaveBeenCalled(); expect(flashSpy).toHaveBeenCalledWith(apiError); @@ -354,12 +363,61 @@ describe('Snippet Edit app', () => { }, mutationRes: mutationTypes.REJECT, }); - wrapper.vm.handleFormSubmit(); + + clickSubmitBtn(); + return waitForPromises().then(() => { expect(Flash).toHaveBeenCalledWith(expect.stringContaining(expectation)); }); }, ); }); + + describe('correctly includes attached files into the mutation', () => { + const createMutationPayload = expectation => { + return expect.objectContaining({ + variables: { + input: expect.objectContaining({ uploadedFiles: expectation }), + }, + }); + }; + + const updateMutationPayload = () => { + return expect.objectContaining({ + variables: { + input: expect.not.objectContaining({ uploadedFiles: expect.anything() }), + }, + }); + }; + + it.each` + paths | expectation + ${[attachedFilePath1]} | ${[attachedFilePath1]} + ${[attachedFilePath1, attachedFilePath2]} | ${[attachedFilePath1, attachedFilePath2]} + ${[]} | ${[]} + `(`correctly sends paths for $paths.length files`, ({ paths, expectation }) => { + createComponent({ + data: { + newSnippet: true, + }, + }); + + const fixtures = paths.map(path => { + return path ? `<input name="files[]" value="${path}">` : undefined; + }); + wrapper.vm.$el.innerHTML += fixtures.join(''); + + clickSubmitBtn(); + + expect(resolveMutate).toHaveBeenCalledWith(createMutationPayload(expectation)); + }); + + it(`neither fails nor sends 'uploadedFiles' to update mutation`, () => { + createComponent(); + + clickSubmitBtn(); + expect(resolveMutate).toHaveBeenCalledWith(updateMutationPayload()); + }); + }); }); }); diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js index c5e667747c6..816ab4e48de 100644 --- a/spec/frontend/snippets/components/snippet_description_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js @@ -1,4 +1,5 @@ import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { shallowMount } from '@vue/test-utils'; describe('Snippet Description Edit component', () => { @@ -15,6 +16,9 @@ describe('Snippet Description Edit component', () => { markdownPreviewPath, markdownDocsPath, }, + stubs: { + MarkdownField, + }, }); } diff --git a/spec/frontend/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js index 867bf88ff50..639b4828a09 100644 --- a/spec/frontend/vue_shared/components/pikaday_spec.js +++ b/spec/frontend/vue_shared/components/pikaday_spec.js @@ -1,30 +1,42 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; import datePicker from '~/vue_shared/components/pikaday.vue'; describe('datePicker', () => { - let vm; + let wrapper; beforeEach(() => { - const DatePicker = Vue.extend(datePicker); - vm = mountComponent(DatePicker, { - label: 'label', + wrapper = shallowMount(datePicker, { + propsData: { + label: 'label', + }, + attachToDocument: true, }); }); + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + it('should render label text', () => { - expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label'); + expect( + wrapper + .find('.dropdown-toggle-text') + .text() + .trim(), + ).toEqual('label'); }); it('should show calendar', () => { - expect(vm.$el.querySelector('.pika-single')).toBeDefined(); + expect(wrapper.find('.pika-single').element).toBeDefined(); }); - it('should toggle when dropdown is clicked', () => { - const hidePicker = jest.fn(); - vm.$on('hidePicker', hidePicker); + it('should emit hidePicker event when dropdown is clicked', () => { + // Removing the bootstrap data-toggle property, + // because it interfers with our click event + delete wrapper.find('.dropdown-menu-toggle').element.dataset.toggle; - vm.$el.querySelector('.dropdown-menu-toggle').click(); + wrapper.find('.dropdown-menu-toggle').trigger('click'); - expect(hidePicker).toHaveBeenCalled(); + expect(wrapper.emitted('hidePicker')).toEqual([[]]); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js index 198af09c9f5..47edfbe3115 100644 --- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js @@ -1,121 +1,149 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; - -describe('sidebarDatePicker', () => { - let vm; - beforeEach(() => { - const SidebarDatePicker = Vue.extend(sidebarDatePicker); - vm = mountComponent(SidebarDatePicker, { - label: 'label', - isLoading: true, +import { mount } from '@vue/test-utils'; +import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; +import DatePicker from '~/vue_shared/components/pikaday.vue'; + +describe('SidebarDatePicker', () => { + let wrapper; + + const mountComponent = (propsData = {}, data = {}) => { + if (wrapper) { + throw new Error('tried to call mountComponent without d'); + } + wrapper = mount(SidebarDatePicker, { + stubs: { + DatePicker: true, + }, + propsData, + data: () => data, }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; }); it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => { - const toggleCollapse = jest.fn(); - vm.$on('toggleCollapse', toggleCollapse); + mountComponent(); - vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click(); + wrapper.find('.issuable-sidebar-header .gutter-toggle').element.click(); - expect(toggleCollapse).toHaveBeenCalled(); + expect(wrapper.emitted('toggleCollapse')).toEqual([[]]); }); it('should render collapsed-calendar-icon', () => { - expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined(); + mountComponent(); + + expect(wrapper.find('.sidebar-collapsed-icon').element).toBeDefined(); }); - it('should render label', () => { - expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label'); + it('should render value when not editing', () => { + mountComponent(); + + expect(wrapper.find('.value-content').element).toBeDefined(); }); - it('should render loading-icon when isLoading', () => { - expect(vm.$el.querySelector('.fa-spin')).toBeDefined(); + it('should render None if there is no selectedDate', () => { + mountComponent(); + + expect( + wrapper + .find('.value-content span') + .text() + .trim(), + ).toEqual('None'); }); - it('should render value when not editing', () => { - expect(vm.$el.querySelector('.value-content')).toBeDefined(); + it('should render date-picker when editing', () => { + mountComponent({}, { editing: true }); + + expect(wrapper.find(DatePicker).element).toBeDefined(); }); - it('should render None if there is no selectedDate', () => { - expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None'); + it('should render label', () => { + const label = 'label'; + mountComponent({ label }); + expect( + wrapper + .find('.title') + .text() + .trim(), + ).toEqual(label); }); - it('should render date-picker when editing', done => { - vm.editing = true; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.pika-label')).toBeDefined(); - done(); - }); + it('should render loading-icon when isLoading', () => { + mountComponent({ isLoading: true }); + expect(wrapper.find('.gl-spinner').element).toBeDefined(); }); describe('editable', () => { - beforeEach(done => { - vm.editable = true; - Vue.nextTick(done); + beforeEach(() => { + mountComponent({ editable: true }); }); it('should render edit button', () => { - expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit'); + expect( + wrapper + .find('.title .btn-blank') + .text() + .trim(), + ).toEqual('Edit'); }); - it('should enable editing when edit button is clicked', done => { - vm.isLoading = false; - Vue.nextTick(() => { - vm.$el.querySelector('.title .btn-blank').click(); + it('should enable editing when edit button is clicked', async () => { + wrapper.find('.title .btn-blank').element.click(); + + await wrapper.vm.$nextTick(); - expect(vm.editing).toEqual(true); - done(); - }); + expect(wrapper.vm.editing).toEqual(true); }); }); - it('should render date if selectedDate', done => { - vm.selectedDate = new Date('07/07/2017'); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017'); - done(); - }); + it('should render date if selectedDate', () => { + mountComponent({ selectedDate: new Date('07/07/2017') }); + + expect( + wrapper + .find('.value-content strong') + .text() + .trim(), + ).toEqual('Jul 7, 2017'); }); describe('selectedDate and editable', () => { - beforeEach(done => { - vm.selectedDate = new Date('07/07/2017'); - vm.editable = true; - Vue.nextTick(done); + beforeEach(() => { + mountComponent({ selectedDate: new Date('07/07/2017'), editable: true }); }); it('should render remove button if selectedDate and editable', () => { - expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove'); + expect( + wrapper + .find('.value-content .btn-blank') + .text() + .trim(), + ).toEqual('remove'); }); - it('should emit saveDate when remove button is clicked', () => { - const saveDate = jest.fn(); - vm.$on('saveDate', saveDate); + it('should emit saveDate with null when remove button is clicked', () => { + wrapper.find('.value-content .btn-blank').element.click(); - vm.$el.querySelector('.value-content .btn-blank').click(); - - expect(saveDate).toHaveBeenCalled(); + expect(wrapper.emitted('saveDate')).toEqual([[null]]); }); }); describe('showToggleSidebar', () => { - beforeEach(done => { - vm.showToggleSidebar = true; - Vue.nextTick(done); + beforeEach(() => { + mountComponent({ showToggleSidebar: true }); }); it('should render toggle-sidebar when showToggleSidebar', () => { - expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined(); + expect(wrapper.find('.title .gutter-toggle').element).toBeDefined(); }); it('should emit toggleCollapse when toggle sidebar is clicked', () => { - const toggleCollapse = jest.fn(); - vm.$on('toggleCollapse', toggleCollapse); - - vm.$el.querySelector('.title .gutter-toggle').click(); + wrapper.find('.title .gutter-toggle').element.click(); - expect(toggleCollapse).toHaveBeenCalled(); + expect(wrapper.emitted('toggleCollapse')).toEqual([[]]); }); }); }); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 319e90709f3..4e072f02ae0 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -32,8 +32,8 @@ describe ProjectsHelper do setting = helper.project_incident_management_setting expect(setting).not_to be_persisted + expect(setting.create_issue).to be_falsey expect(setting.send_email).to be_falsey - expect(setting.create_issue).to be_truthy expect(setting.issue_template_key).to be_nil end end diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index b7979144c72..b3f7f19cc29 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -37,10 +37,10 @@ describe 'create_tokens' do expect(keys).to all(match(hex_key)) end - it 'generates an RSA key for openid_connect_signing_key' do + it 'generates an RSA key for openid_connect_signing_key and ci_jwt_signing_key' do create_tokens - keys = secrets.values_at(:openid_connect_signing_key) + keys = secrets.values_at(:openid_connect_signing_key, :ci_jwt_signing_key) expect(keys.uniq).to eq(keys) expect(keys).to all(match(rsa_key)) @@ -51,6 +51,7 @@ describe 'create_tokens' do expect(self).to receive(:warn_missing_secret).with('otp_key_base') expect(self).to receive(:warn_missing_secret).with('db_key_base') expect(self).to receive(:warn_missing_secret).with('openid_connect_signing_key') + expect(self).to receive(:warn_missing_secret).with('ci_jwt_signing_key') create_tokens end @@ -63,6 +64,7 @@ describe 'create_tokens' do expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base) expect(new_secrets['db_key_base']).to eq(secrets.db_key_base) expect(new_secrets['openid_connect_signing_key']).to eq(secrets.openid_connect_signing_key) + expect(new_secrets['ci_jwt_signing_key']).to eq(secrets.ci_jwt_signing_key) end create_tokens @@ -79,6 +81,7 @@ describe 'create_tokens' do before do secrets.db_key_base = 'db_key_base' secrets.openid_connect_signing_key = 'openid_connect_signing_key' + secrets.ci_jwt_signing_key = 'ci_jwt_signing_key' allow(File).to receive(:exist?).with('.secret').and_return(true) allow(File).to receive(:read).with('.secret').and_return('file_key') @@ -90,6 +93,7 @@ describe 'create_tokens' do secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' secrets.openid_connect_signing_key = 'openid_connect_signing_key' + secrets.ci_jwt_signing_key = 'ci_jwt_signing_key' end it 'does not issue a warning' do @@ -116,6 +120,7 @@ describe 'create_tokens' do secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' secrets.openid_connect_signing_key = 'openid_connect_signing_key' + secrets.ci_jwt_signing_key = 'ci_jwt_signing_key' end it 'does not write any files' do @@ -131,6 +136,7 @@ describe 'create_tokens' do expect(secrets.otp_key_base).to eq('otp_key_base') expect(secrets.db_key_base).to eq('db_key_base') expect(secrets.openid_connect_signing_key).to eq('openid_connect_signing_key') + expect(secrets.ci_jwt_signing_key).to eq('ci_jwt_signing_key') end it 'deletes the .secret file' do @@ -155,6 +161,7 @@ describe 'create_tokens' do expect(new_secrets['otp_key_base']).to eq('file_key') expect(new_secrets['db_key_base']).to eq('db_key_base') expect(new_secrets['openid_connect_signing_key']).to eq('openid_connect_signing_key') + expect(new_secrets['ci_jwt_signing_key']).to eq('ci_jwt_signing_key') end create_tokens diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb index f2897708b08..f3de7f0f5b8 100644 --- a/spec/lib/gitlab/ci/jwt_spec.rb +++ b/spec/lib/gitlab/ci/jwt_spec.rb @@ -93,7 +93,7 @@ describe Gitlab::Ci::Jwt do end describe '.for_build' do - let(:rsa_key) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) } + let(:rsa_key) { OpenSSL::PKey::RSA.new(Rails.application.secrets.ci_jwt_signing_key) } subject(:jwt) { described_class.for_build(build) } diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb index 1f417781988..6c67864855d 100644 --- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb @@ -59,12 +59,13 @@ describe Gitlab::Ci::Status::Bridge::Factory do context 'failed with downstream_pipeline_creation_failed' do before do + bridge.options = { downstream_errors: ['No stages / jobs for this pipeline.', 'other error'] } bridge.failure_reason = 'downstream_pipeline_creation_failed' end it 'fabricates correct status_tooltip' do expect(status.status_tooltip).to eq( - "#{s_('CiStatusText|failed')} - (downstream pipeline can not be created)" + "#{s_('CiStatusText|failed')} - (downstream pipeline can not be created, No stages / jobs for this pipeline., other error)" ) end end diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb index 4d41e2c45aa..b6148cd1407 100644 --- a/spec/lib/gitlab/danger/roulette_spec.rb +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -6,40 +6,149 @@ require 'webmock/rspec' require 'gitlab/danger/roulette' describe Gitlab::Danger::Roulette do + let(:backend_maintainer) do + { + username: 'backend-maintainer', + name: 'Backend maintainer', + role: 'Backend engineer', + projects: { 'gitlab' => 'maintainer backend' } + } + end + let(:frontend_reviewer) do + { + username: 'frontend-reviewer', + name: 'Frontend reviewer', + role: 'Frontend engineer', + projects: { 'gitlab' => 'reviewer frontend' } + } + end + let(:frontend_maintainer) do + { + username: 'frontend-maintainer', + name: 'Frontend maintainer', + role: 'Frontend engineer', + projects: { 'gitlab' => "maintainer frontend" } + } + end + let(:software_engineer_in_test) do + { + username: 'software-engineer-in-test', + name: 'Software Engineer in Test', + role: 'Software Engineer in Test, Create:Source Code', + projects: { + 'gitlab' => 'reviewer qa', + 'gitlab-qa' => 'maintainer' + } + } + end + let(:engineering_productivity_reviewer) do + { + username: 'eng-prod-reviewer', + name: 'EP engineer', + role: 'Engineering Productivity', + projects: { 'gitlab' => 'reviewer backend' } + } + end + let(:teammate_json) do - <<~JSON [ - { - "username": "in-gitlab-ce", - "name": "CE maintainer", - "projects":{ "gitlab-ce": "maintainer backend" } - }, - { - "username": "in-gitlab-ee", - "name": "EE reviewer", - "projects":{ "gitlab-ee": "reviewer frontend" } - } - ] - JSON + backend_maintainer, + frontend_maintainer, + frontend_reviewer, + software_engineer_in_test, + engineering_productivity_reviewer + ].to_json end - let(:ce_teammate_matcher) do + subject(:roulette) { Object.new.extend(described_class) } + + def matching_teammate(person) satisfy do |teammate| - teammate.username == 'in-gitlab-ce' && - teammate.name == 'CE maintainer' && - teammate.projects == { 'gitlab-ce' => 'maintainer backend' } + teammate.username == person[:username] && + teammate.name == person[:name] && + teammate.role == person[:role] && + teammate.projects == person[:projects] end end - let(:ee_teammate_matcher) do - satisfy do |teammate| - teammate.username == 'in-gitlab-ee' && - teammate.name == 'EE reviewer' && - teammate.projects == { 'gitlab-ee' => 'reviewer frontend' } + def matching_spin(category, reviewer: { username: nil }, maintainer: { username: nil }, optional: nil) + satisfy do |spin| + spin.category == category && + spin.reviewer&.username == reviewer[:username] && + spin.maintainer&.username == maintainer[:username] && + spin.optional_role == optional end end - subject(:roulette) { Object.new.extend(described_class) } + describe '#spin' do + let!(:project) { 'gitlab' } + let!(:branch_name) { 'a-branch' } + let!(:mr_labels) { ['backend', 'devops::create'] } + let!(:author) { Gitlab::Danger::Teammate.new('username' => 'filipa') } + + before do + [ + backend_maintainer, + frontend_reviewer, + frontend_maintainer, + software_engineer_in_test, + engineering_productivity_reviewer + ].each do |person| + stub_person_status(instance_double(Gitlab::Danger::Teammate, username: person[:username]), message: 'making GitLab magic') + end + + WebMock + .stub_request(:get, described_class::ROULETTE_DATA_URL) + .to_return(body: teammate_json) + allow(subject).to receive_message_chain(:gitlab, :mr_author).and_return(author.username) + allow(subject).to receive_message_chain(:gitlab, :mr_labels).and_return(mr_labels) + end + + context 'when change contains backend category' do + it 'assigns backend reviewer and maintainer' do + categories = [:backend] + spins = subject.spin(project, categories, branch_name) + + expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer)) + end + end + + context 'when change contains frontend category' do + it 'assigns frontend reviewer and maintainer' do + categories = [:frontend] + spins = subject.spin(project, categories, branch_name) + + expect(spins).to contain_exactly(matching_spin(:frontend, reviewer: frontend_reviewer, maintainer: frontend_maintainer)) + end + end + + context 'when change contains QA category' do + it 'assigns QA reviewer and sets optional QA maintainer' do + categories = [:qa] + spins = subject.spin(project, categories, branch_name) + + expect(spins).to contain_exactly(matching_spin(:qa, reviewer: software_engineer_in_test, optional: :maintainer)) + end + end + + context 'when change contains Engineering Productivity category' do + it 'assigns Engineering Productivity reviewer and fallback to backend maintainer' do + categories = [:engineering_productivity] + spins = subject.spin(project, categories, branch_name) + + expect(spins).to contain_exactly(matching_spin(:engineering_productivity, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer)) + end + end + + context 'when change contains test category' do + it 'assigns corresponding SET and sets optional test maintainer' do + categories = [:test] + spins = subject.spin(project, categories, branch_name) + + expect(spins).to contain_exactly(matching_spin(:test, reviewer: software_engineer_in_test, optional: :maintainer)) + end + end + end describe '#team' do subject(:team) { roulette.team } @@ -76,7 +185,15 @@ describe Gitlab::Danger::Roulette do end it 'returns an array of teammates' do - is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher) + expected_teammates = [ + matching_teammate(backend_maintainer), + matching_teammate(frontend_reviewer), + matching_teammate(frontend_maintainer), + matching_teammate(software_engineer_in_test), + matching_teammate(engineering_productivity_reviewer) + ] + + is_expected.to contain_exactly(*expected_teammates) end it 'memoizes the result' do @@ -86,7 +203,7 @@ describe Gitlab::Danger::Roulette do end describe '#project_team' do - subject { roulette.project_team('gitlab-ce') } + subject { roulette.project_team('gitlab-qa') } before do WebMock @@ -95,7 +212,7 @@ describe Gitlab::Danger::Roulette do end it 'filters team by project_name' do - is_expected.to contain_exactly(ce_teammate_matcher) + is_expected.to contain_exactly(matching_teammate(software_engineer_in_test)) end end @@ -136,15 +253,15 @@ describe Gitlab::Danger::Roulette do it 'excludes person with no capacity' do expect(subject.spin_for_person([no_capacity], random: Random.new)).to be_nil end + end - private + private - def stub_person_status(person, message: 'dummy message', emoji: 'unicorn') - body = { message: message, emoji: emoji }.to_json + def stub_person_status(person, message: 'dummy message', emoji: 'unicorn') + body = { message: message, emoji: emoji }.to_json - WebMock - .stub_request(:get, "https://gitlab.com/api/v4/users/#{person.username}/status") - .to_return(body: body) - end + WebMock + .stub_request(:get, "https://gitlab.com/api/v4/users/#{person.username}/status") + .to_return(body: body) end end diff --git a/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb b/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb new file mode 100644 index 00000000000..507b1a8d580 --- /dev/null +++ b/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20200609212701_add_incident_settings_to_all_existing_projects.rb') + +describe AddIncidentSettingsToAllExistingProjects, :migration do + let(:project_incident_management_settings) { table(:project_incident_management_settings) } + let(:labels) { table(:labels) } + let(:label_links) { table(:label_links) } + let(:issues) { table(:issues) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + + RSpec.shared_examples 'setting not added' do + it 'does not add settings' do + migrate! + + expect { migrate! }.not_to change { IncidentManagement::ProjectIncidentManagementSetting.count } + end + end + + RSpec.shared_examples 'project has no incident settings' do + it 'has no settings' do + migrate! + + expect(settings).to eq(nil) + end + end + + RSpec.shared_examples 'no change to incident settings' do + it 'does not change existing settings' do + migrate! + + expect(settings.create_issue).to eq(existing_create_issue) + end + end + + RSpec.shared_context 'with incident settings' do + let(:existing_create_issue) { false } + before do + project_incident_management_settings.create( + project_id: project.id, + create_issue: existing_create_issue + ) + end + end + + describe 'migrate!' do + let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') } + let!(:project) { projects.create!(namespace_id: namespace.id) } + let(:settings) { project_incident_management_settings.find_by(project_id: project.id) } + + context 'when project does not have incident label' do + context 'does not have incident settings' do + include_examples 'setting not added' + include_examples 'project has no incident settings' + end + + context 'and has incident settings' do + include_context 'with incident settings' + + include_examples 'setting not added' + include_examples 'no change to incident settings' + end + end + + context 'when project has incident labels' do + before do + issue = issues.create!(project_id: project.id) + incident_label_attrs = IncidentManagement::CreateIssueService::INCIDENT_LABEL + incident_label = labels.create!(project_id: project.id, **incident_label_attrs) + label_links.create!(target_id: issue.id, label_id: incident_label.id, target_type: 'Issue') + end + + context 'when project has incident settings' do + include_context 'with incident settings' + + include_examples 'setting not added' + include_examples 'no change to incident settings' + end + + context 'does not have incident settings' do + it 'adds incident settings with old defaults' do + migrate! + + expect(settings.create_issue).to eq(true) + expect(settings.send_email).to eq(false) + expect(settings.issue_template_key).to eq(nil) + end + end + end + end +end diff --git a/spec/models/group_deploy_key_spec.rb b/spec/models/group_deploy_key_spec.rb new file mode 100644 index 00000000000..3ba56c7e504 --- /dev/null +++ b/spec/models/group_deploy_key_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GroupDeployKey do + it { is_expected.to validate_presence_of(:user) } + + it 'is of type DeployKey' do + expect(build(:group_deploy_key).type).to eq('DeployKey') + end +end diff --git a/spec/models/snippet_input_action_spec.rb b/spec/models/snippet_input_action_spec.rb index 584463efc3e..d1c66216cea 100644 --- a/spec/models/snippet_input_action_spec.rb +++ b/spec/models/snippet_input_action_spec.rb @@ -7,6 +7,11 @@ describe SnippetInputAction do using RSpec::Parameterized::TableSyntax where(:action, :file_path, :content, :previous_path, :is_valid, :invalid_field) do + :create | 'foobar' | 'foobar' | 'foobar' | true | nil + :move | 'foobar' | 'foobar' | 'foobar' | true | nil + :delete | 'foobar' | 'foobar' | 'foobar' | true | nil + :update | 'foobar' | 'foobar' | 'foobar' | true | nil + :foo | 'foobar' | 'foobar' | 'foobar' | false | :action 'create' | 'foobar' | 'foobar' | 'foobar' | true | nil 'move' | 'foobar' | 'foobar' | 'foobar' | true | nil 'delete' | 'foobar' | 'foobar' | 'foobar' | true | nil @@ -14,14 +19,17 @@ describe SnippetInputAction do 'foo' | 'foobar' | 'foobar' | 'foobar' | false | :action nil | 'foobar' | 'foobar' | 'foobar' | false | :action '' | 'foobar' | 'foobar' | 'foobar' | false | :action - 'move' | 'foobar' | 'foobar' | nil | false | :previous_path - 'move' | 'foobar' | 'foobar' | '' | false | :previous_path - 'create' | 'foobar' | nil | 'foobar' | false | :content - 'create' | 'foobar' | '' | 'foobar' | false | :content - 'create' | nil | 'foobar' | 'foobar' | false | :file_path - 'create' | '' | 'foobar' | 'foobar' | false | :file_path - 'update' | 'foobar' | nil | 'foobar' | false | :content - 'update' | 'other' | 'foobar' | 'foobar' | false | :file_path + :move | 'foobar' | 'foobar' | nil | false | :previous_path + :move | 'foobar' | 'foobar' | '' | false | :previous_path + :create | 'foobar' | nil | 'foobar' | false | :content + :create | 'foobar' | '' | 'foobar' | false | :content + :create | nil | 'foobar' | 'foobar' | false | :file_path + :create | '' | 'foobar' | 'foobar' | false | :file_path + :update | 'foobar' | nil | 'foobar' | false | :content + :update | 'foobar' | '' | 'foobar' | false | :content + :update | 'other' | 'foobar' | 'foobar' | false | :file_path + :update | 'foobar' | 'foobar' | nil | true | nil + :update | 'foobar' | 'foobar' | '' | true | nil end with_them do diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb index fc170f8986c..70470032930 100644 --- a/spec/routing/openid_connect_spec.rb +++ b/spec/routing/openid_connect_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' # oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys -# jwks GET /-/jwks(.:format) doorkeeper/openid_connect/discovery#keys # oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider # oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do @@ -18,10 +17,6 @@ describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do it "to #keys" do expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys') end - - it "/-/jwks" do - expect(get('/-/jwks')).to route_to('doorkeeper/openid_connect/discovery#keys') - end end # oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 9c3d17f7d8f..370a5ec0f99 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -368,3 +368,10 @@ describe AutocompleteController, 'routing' do expect(get("/autocomplete/award_emojis")).to route_to('autocomplete#award_emojis') end end + +# jwks GET /-/jwks(.:format) jwks#index +describe JwksController, "routing" do + it "to #index" do + expect(get('/-/jwks')).to route_to('jwks#index') + end +end diff --git a/spec/services/ci/create_cross_project_pipeline_service_spec.rb b/spec/services/ci/create_cross_project_pipeline_service_spec.rb index 5c59aaa4ce9..9e2497854bc 100644 --- a/spec/services/ci/create_cross_project_pipeline_service_spec.rb +++ b/spec/services/ci/create_cross_project_pipeline_service_spec.rb @@ -487,10 +487,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do end it 'does not create a pipeline and drops the bridge' do - service.execute(bridge) + expect { service.execute(bridge) }.not_to change(downstream_project.ci_pipelines, :count) expect(bridge.reload).to be_failed expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed') + expect(bridge.options[:downstream_errors]).to eq(['Reference not found']) end end @@ -509,10 +510,35 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do end it 'does not create a pipeline and drops the bridge' do - service.execute(bridge) + expect { service.execute(bridge) }.not_to change(downstream_project.ci_pipelines, :count) + + expect(bridge.reload).to be_failed + expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed') + expect(bridge.options[:downstream_errors]).to eq(['No stages / jobs for this pipeline.']) + end + end + + context 'when downstream pipeline has invalid YAML' do + before do + stub_ci_pipeline_yaml_file(config) + end + + let(:config) do + <<-EOY + test: + stage: testx + script: echo 1 + EOY + end + + it 'creates the pipeline but drops the bridge' do + expect { service.execute(bridge) }.to change(downstream_project.ci_pipelines, :count).by(1) expect(bridge.reload).to be_failed expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed') + expect(bridge.options[:downstream_errors]).to eq( + ['test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post'] + ) end end end diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb index 81e55083629..0470552d933 100644 --- a/spec/workers/incident_management/process_alert_worker_spec.rb +++ b/spec/workers/incident_management/process_alert_worker_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe IncidentManagement::ProcessAlertWorker do let_it_be(:project) { create(:project) } + let_it_be(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) } describe '#perform' do let(:alert_management_alert_id) { nil } diff --git a/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb b/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb index d80365cfba4..c9ea96df5c2 100644 --- a/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb +++ b/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb @@ -8,6 +8,7 @@ describe IncidentManagement::ProcessPrometheusAlertWorker do let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) } let(:payload_key) { Gitlab::Alerting::Alert.new(project: project, payload: alert_params).gitlab_fingerprint } let!(:prometheus_alert_event) { create(:prometheus_alert_event, prometheus_alert: prometheus_alert, payload_key: payload_key) } + let!(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) } let(:alert_params) do { diff --git a/yarn.lock b/yarn.lock index 63d6bbba414..76e6de4b6e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4013,6 +4013,11 @@ dom-serializer@0, dom-serializer@^0.2.1: domelementtype "^2.0.1" entities "^2.0.0" +dom-walk@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" + integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== + domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" @@ -5354,6 +5359,14 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" +global@^4.3.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" + integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== + dependencies: + min-document "^2.19.0" + process "^0.11.10" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -7905,6 +7918,13 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +min-document@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= + dependencies: + dom-walk "^0.1.0" + minify@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/minify/-/minify-4.1.2.tgz#88755f4faa5f7ab6d0c64fdd659aa34ea658f180" @@ -12302,6 +12322,14 @@ xdg-basedir@^4.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xhr-mock@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-2.5.1.tgz#c591498a8269cc1ce5fefac20d590357affd348b" + integrity sha512-UKOjItqjFgPUwQGPmRAzNBn8eTfIhcGjBVGvKYAWxUQPQsXNGD6KEckGTiHwyaAUp9C9igQlnN1Mp79KWCg7CQ== + dependencies: + global "^4.3.0" + url "^0.11.0" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" |