diff options
64 files changed, 1021 insertions, 377 deletions
@@ -419,7 +419,7 @@ end gem 'octokit', '~> 4.15' # https://gitlab.com/gitlab-org/gitlab/issues/207207 -gem 'gitlab-mail_room', '~> 0.0.3', require: 'mail_room' +gem 'gitlab-mail_room', '~> 0.0.4', require: 'mail_room' gem 'email_reply_trimmer', '~> 0.1' gem 'html2text' diff --git a/Gemfile.lock b/Gemfile.lock index e68b0d9e819..8a65cdc8cc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -391,7 +391,7 @@ GEM opentracing (~> 0.4) redis (> 3.0.0, < 5.0.0) gitlab-license (1.0.0) - gitlab-mail_room (0.0.3) + gitlab-mail_room (0.0.4) gitlab-markup (1.7.0) gitlab-net-dns (0.9.1) gitlab-puma (4.3.3.gitlab.2) @@ -1244,7 +1244,7 @@ DEPENDENCIES gitlab-chronic (~> 0.10.5) gitlab-labkit (= 0.12.0) gitlab-license (~> 1.0) - gitlab-mail_room (~> 0.0.3) + gitlab-mail_room (~> 0.0.4) gitlab-markup (~> 1.7.0) gitlab-net-dns (~> 0.9.1) gitlab-puma (~> 4.3.3.gitlab.2) diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue index 504391ffdc7..975d54c7a4e 100644 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -79,7 +79,7 @@ export default { <icon name="chevron-left" /> {{ __('View jobs') }} </button> </header> - <div class="top-bar d-flex border-left-0"> + <div class="top-bar d-flex border-left-0 mr-3"> <job-description :job="detailJob" /> <div class="controllers ml-auto"> <a @@ -97,7 +97,7 @@ export default { <scroll-button :disabled="isScrolledToBottom" direction="down" @click="scrollDown" /> </div> </div> - <pre ref="buildTrace" class="build-trace mb-0 h-100" @scroll="scrollBuildLog"> + <pre ref="buildTrace" class="build-trace mb-0 h-100 mr-3" @scroll="scrollBuildLog"> <code v-show="!detailJob.isLoading" class="bash" diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js index 9269aa074f8..2bba3ee4ff9 100644 --- a/app/assets/javascripts/registry/explorer/index.js +++ b/app/assets/javascripts/registry/explorer/index.js @@ -19,7 +19,7 @@ export default () => { const { endpoint } = el.dataset; const store = createStore(); - const router = createRouter(endpoint, store); + const router = createRouter(endpoint); store.dispatch('setInitialState', el.dataset); const attachMainComponent = () => diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index 1d4dcd9a7a2..df29ee44419 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -157,6 +157,9 @@ export default { return config; }, }, + mounted() { + this.requestTagsList({ params: this.$route.params.id }); + }, methods: { ...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']), setModalDescription(itemIndex = -1) { diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index 8923c305b2d..e932544feb8 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -103,8 +103,16 @@ export default { : DELETE_IMAGE_ERROR_MESSAGE; }, }, + mounted() { + this.loadImageList(this.$route.name); + }, methods: { ...mapActions(['requestImagesList', 'requestDeleteImage']), + loadImageList(fromName) { + if (!fromName || !this.images?.length) { + this.requestImagesList(); + } + }, deleteImage(item) { this.track('click_button'); this.itemToDelete = item; diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js index 28df3177df4..478eaca1a68 100644 --- a/app/assets/javascripts/registry/explorer/router.js +++ b/app/assets/javascripts/registry/explorer/router.js @@ -7,7 +7,7 @@ import { decodeAndParse } from './utils'; Vue.use(VueRouter); -export default function createRouter(base, store) { +export default function createRouter(base) { const router = new VueRouter({ base, mode: 'history', @@ -20,12 +20,6 @@ export default function createRouter(base, store) { nameGenerator: () => s__('ContainerRegistry|Container Registry'), root: true, }, - beforeEnter: (to, from, next) => { - if (!from.name || !store.state.images?.length) { - store.dispatch('requestImagesList'); - } - next(); - }, }, { name: 'details', @@ -34,10 +28,6 @@ export default function createRouter(base, store) { meta: { nameGenerator: route => decodeAndParse(route.params.id).name, }, - beforeEnter: (to, from, next) => { - store.dispatch('requestTagsList', { params: to.params.id }); - next(); - }, }, ], }); diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js index b3ff2e6e002..153032e37d3 100644 --- a/app/assets/javascripts/registry/explorer/stores/index.js +++ b/app/assets/javascripts/registry/explorer/stores/index.js @@ -15,4 +15,5 @@ export const createStore = () => mutations, }); +// Deprecated and to be removed export default createStore(); diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index 615983ed3cf..46ab04c9b19 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -10,6 +10,7 @@ import { GlDropdown, GlDropdownItem, GlButton, + GlTooltipDirective, } from '@gitlab/ui'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -30,6 +31,9 @@ export default { TimeAgoTooltip, GlButton, }, + directives: { + GlTooltip: GlTooltipDirective, + }, apollo: { canCreateSnippet: { query() { @@ -67,6 +71,10 @@ export default { condition: this.snippet.userPermissions.updateSnippet, text: __('Edit'), href: this.editLink, + disabled: this.snippet.blob.binary, + title: this.snippet.blob.binary + ? __('Snippets with non-text files can only be edited via Git.') + : undefined, }, { condition: this.snippet.userPermissions.adminSnippet, @@ -186,18 +194,24 @@ export default { <div class="detail-page-header-actions"> <div class="d-none d-sm-flex"> <template v-for="(action, index) in personalSnippetActions"> - <gl-button + <div v-if="action.condition" :key="index" - :disabled="action.disabled" - :variant="action.variant" - :category="action.category" - :class="action.cssClass" - :href="action.href" - @click="action.click ? action.click() : undefined" + v-gl-tooltip + :title="action.title" + class="d-inline-block" > - {{ action.text }} - </gl-button> + <gl-button + :disabled="action.disabled" + :variant="action.variant" + :category="action.category" + :class="action.cssClass" + :href="action.href" + @click="action.click ? action.click() : undefined" + > + {{ action.text }} + </gl-button> + </div> </template> </div> <div class="d-block d-sm-none dropdown"> @@ -205,6 +219,8 @@ export default { <gl-dropdown-item v-for="(action, index) in personalSnippetActions" :key="index" + :disabled="action.disabled" + :title="action.title" :href="action.href" @click="action.click ? action.click() : undefined" >{{ action.text }}</gl-dropdown-item diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss index 9465dd5bed6..48b8a7230b1 100644 --- a/app/assets/stylesheets/page_bundles/_ide_mixins.scss +++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss @@ -9,7 +9,6 @@ top: 0; font-size: 12px; border-top-right-radius: $border-radius-default; - margin-left: -$gl-padding; .controllers { @include build-controllers(15px, center, false, 0, inline, 0); diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index c5869880af9..d0660422f7e 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -890,11 +890,15 @@ $ide-commit-header-height: 48px; .multi-file-commit-panel-inner { width: 350px; - padding: $grid-size $gl-padding; + padding: $grid-size 0; background-color: $white; border-left: 1px solid $white-dark; } + .ide-right-sidebar-jobs-detail { + padding-bottom: 0; + } + .ide-right-sidebar-clientside { padding: 0; } @@ -915,15 +919,12 @@ $ide-commit-header-height: 48px; margin: 0; } } - - .build-trace { - margin-left: -$gl-padding; - } } .ide-pipeline-list { flex: 1; overflow: auto; + padding: 0 $gl-padding; } .ide-pipeline-header { @@ -966,6 +967,7 @@ $ide-commit-header-height: 48px; .ide-job-header { min-height: 60px; + padding: 0 $gl-padding; } .ide-nav-form { diff --git a/app/assets/stylesheets/page_bundles/themes/_dark.scss b/app/assets/stylesheets/page_bundles/themes/_dark.scss index 66b4b745532..634f18ee1bd 100644 --- a/app/assets/stylesheets/page_bundles/themes/_dark.scss +++ b/app/assets/stylesheets/page_bundles/themes/_dark.scss @@ -33,7 +33,7 @@ $diff-insert: rgba(155, 185, 85, 0.2); $diff-remove: rgba(255, 0, 0, 0.2); - a { + a:not(.btn) { color: $link-color; } @@ -57,27 +57,46 @@ textarea, .md-area.is-focused, .ide-entry-dropdown-toggle, - .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover, .dropdown-menu li button, .ide-merge-request-project-path, .dropdown-menu-selectable li a.is-active, .dropdown-menu-inner-title, - .dropdown-menu-inner-content { + .dropdown-menu-inner-content, + .nav-links:not(.quick-links) li:not(.md-header-toolbar) a, + .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover, + .nav-links:not(.quick-links) li:not(.md-header-toolbar) a.active .badge.badge-pill, + .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover .badge.badge-pill, + .badge.badge-pill, + .ide-navigator-button, + .bs-callout, + .ide-navigator-btn, + .ide-pipeline .top-bar, + .ide-pipeline .top-bar .controllers .controllers-buttons { color: $text-color; } + .drag-handle:hover, + .card-header .badge.badge-pill { + background-color: $dropdown-hover-background; + } + .modal-body { color: $gl-text-color; } .dropdown-menu-toggle svg, .dropdown-menu-toggle svg:hover, - .ide-tree-header svg, + .ide-tree-header:not(.ide-pipeline-header) svg, .file-row .file-row-icon svg, - .file-row:hover .file-row-icon svg { + .file-row:hover .file-row-icon svg, + .controllers-buttons svg { fill: $text-color; } + .ide-pipeline svg { + --svg-status-bg: transparent; + } + .multi-file-tab-close:hover { background-color: $input-border; } @@ -118,7 +137,12 @@ .ide-commit-editor-header, .ide-file-templates, .ide-entry-dropdown-toggle, - .ide-staged-action-btn { + .ide-staged-action-btn, + .badge.badge-pill, + .card-header, + .bs-callout, + .ide-pipeline .top-bar, + .ide-terminal .top-bar { background-color: $background; } @@ -126,6 +150,18 @@ background-color: inherit; } + .bs-callout { + border-color: $dropdown-background; + + code { + background-color: $dropdown-background; + } + } + + .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover { + border-color: $dropdown-hover-background; + } + .ide-sidebar-link:hover, .multi-file-tabs li { background-color: $background-hover; @@ -144,7 +180,10 @@ .ide-sidebar-link.active::after, .ide-right-sidebar .multi-file-commit-panel-inner, .common-note-form .md-area, - .ide-commit-message-field { + .ide-commit-message-field, + .card, + .multi-file-commit-panel-success-message, + .ide-preview-header { background-color: $highlight-background; } @@ -163,7 +202,12 @@ .multi-file-tabs li, .ide-status-bar, .ide-commit-editor-header, - .ide-file-templates { + .ide-file-templates, + .card, + .card-header, + .ide-job-item:not(:last-child), + .ide-terminal .top-bar, + .ide-pipeline .top-bar { border-color: $border-color; } @@ -179,7 +223,9 @@ .multi-file-commit-form > form, .multi-file-commit-form hr, .ide-commit-list-container.is-first, - .multi-file-commit-form .nav-links:not(.quick-links) { + .multi-file-commit-form .nav-links:not(.quick-links), + .ide-pipeline-list .nav-links:not(.quick-links), + .ide-preview-header { border-color: $background; } @@ -201,7 +247,8 @@ } } - .nav-links li.active a { + .nav-links li.active a, + .nav-links li a.active { border-color: $highlight-accent; color: $text-color; } @@ -223,7 +270,7 @@ input[type='search'], .filtered-search-box { border-color: $input-border; - background-color: $input-background; + background: $input-background !important; } input[type='text'], @@ -252,16 +299,16 @@ background: $gray-800; } - .btn:not(.btn-link):hover { + .btn:not(.btn-link):not([disabled]):hover { border-width: 2px; padding: 5px 9px; } - .btn.btn-sm:hover { + .btn:not([disabled]).btn-sm:hover { padding: 3px 9px; } - .btn.btn-block:hover { + .btn:not([disabled]).btn-block:hover { padding: 5px 0; } diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb new file mode 100644 index 00000000000..eb0d2304ba3 --- /dev/null +++ b/app/graphql/mutations/alert_management/base.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + class Base < BaseMutation + include Mutations::ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: "The project the alert to mutate is in" + + argument :iid, GraphQL::STRING_TYPE, + required: true, + description: "The iid of the alert to mutate" + + field :alert, + Types::AlertManagement::AlertType, + null: true, + description: "The alert after mutation" + + authorize :update_alert_management_alerts + + private + + def find_object(project_path:, iid:) + project = resolve_project(full_path: project_path) + + return unless project + + resolver = Resolvers::AlertManagementAlertResolver.single.new(object: project, context: context, field: nil) + resolver.resolve(iid: iid) + end + end + end +end diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb new file mode 100644 index 00000000000..ac6a5412956 --- /dev/null +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + class UpdateAlertStatus < Base + graphql_name 'UpdateAlertStatus' + + argument :status, Types::AlertManagement::StatusEnum, + required: true, + description: 'The status to set the alert' + + def resolve(args) + alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + + result = update_status(alert, args[:status]) + + prepare_response(result) + end + + private + + def update_status(alert, status) + ::AlertManagement::UpdateAlertStatusService.new(alert, status).execute + end + + def prepare_response(result) + { + alert: result.payload[:alert], + errors: result.error? ? [result.message] : [] + } + end + end + end +end diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index 5055a9caef2..69fc2718f1e 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -18,6 +18,11 @@ module Types null: true, description: 'Title of the alert' + field :description, + GraphQL::STRING_TYPE, + null: true, + description: 'Description of the alert' + field :severity, AlertManagement::SeverityEnum, null: true, @@ -38,6 +43,11 @@ module Types null: true, description: 'Monitoring tool the alert came from' + field :hosts, + [GraphQL::STRING_TYPE], + null: true, + description: 'List of hosts the alert came from' + field :started_at, Types::TimeType, null: true, @@ -53,6 +63,21 @@ module Types null: true, description: 'Number of events of this alert', method: :events + + field :details, + GraphQL::Types::JSON, + null: true, + description: 'Alert details' + + field :created_at, + Types::TimeType, + null: true, + description: 'Timestamp the alert was created' + + field :updated_at, + Types::TimeType, + null: true, + description: 'Timestamp the alert was last updated' end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index c9b8f235ae1..b18a3968a03 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -7,6 +7,7 @@ module Types graphql_name 'Mutation' mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs + mount_mutation Mutations::AlertManagement::UpdateAlertStatus mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index 810ec8f5583..1263423e1c4 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -53,6 +53,12 @@ module AlertManagement end end + def details + details_payload = payload.except(*attributes.keys) + + Gitlab::Utils::InlineHash.merge_keys(details_payload) + end + private def hosts_length diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb index 76139f5d676..91163c85a9e 100644 --- a/app/models/ci/persistent_ref.rb +++ b/app/models/ci/persistent_ref.rb @@ -14,16 +14,12 @@ module Ci delegate :ref_exists?, :create_ref, :delete_refs, to: :repository def exist? - return unless enabled? - ref_exists?(path) rescue false end def create - return unless enabled? - create_ref(sha, path) rescue => e Gitlab::ErrorTracking @@ -31,8 +27,6 @@ module Ci end def delete - return unless enabled? - delete_refs(path) rescue Gitlab::Git::Repository::NoRepository # no-op @@ -44,11 +38,5 @@ module Ci def path "refs/#{Repository::REF_PIPELINES}/#{pipeline.id}" end - - private - - def enabled? - Feature.enabled?(:depend_on_persistent_pipeline_ref, project, default_enabled: true) - end end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 4e0bcfe0985..6aa3d791a0f 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy enable :read_prometheus enable :read_metrics_dashboard_annotation enable :read_alert_management_alerts + enable :update_alert_management_alerts enable :metrics_dashboard end diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index a409a3d7160..61fcbaf691a 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -34,7 +34,6 @@ module Ci def refspecs specs = [] - specs << refspec_for_pipeline_ref if should_expose_merge_request_ref? specs << refspec_for_persistent_ref if persistent_ref_exist? if git_depth > 0 @@ -50,19 +49,6 @@ module Ci private - # We will stop exposing merge request refs when we fully depend on persistent refs - # (i.e. remove `refspec_for_pipeline_ref` when we remove `depend_on_persistent_pipeline_ref` feature flag.) - # `ci_force_exposing_merge_request_refs` is an extra feature flag that allows us to - # forcibly expose MR refs even if the `depend_on_persistent_pipeline_ref` feature flag enabled. - # This is useful when we see an unexpected behaviors/reports from users. - # See https://gitlab.com/gitlab-org/gitlab/issues/35140. - def should_expose_merge_request_ref? - return false unless merge_request_ref? - return true if Feature.enabled?(:ci_force_exposing_merge_request_refs, project) - - Feature.disabled?(:depend_on_persistent_pipeline_ref, project, default_enabled: true) - end - def create_archive(artifacts) return unless artifacts[:untracked] || artifacts[:paths] @@ -100,10 +86,6 @@ module Ci "+#{Gitlab::Git::TAG_REF_PREFIX}#{ref}:#{RUNNER_REMOTE_TAG_PREFIX}#{ref}" end - def refspec_for_pipeline_ref - "+#{ref}:#{ref}" - end - def refspec_for_persistent_ref "+#{persistent_ref_path}:#{persistent_ref_path}" end diff --git a/app/services/alert_management/update_alert_status_service.rb b/app/services/alert_management/update_alert_status_service.rb new file mode 100644 index 00000000000..73ee654874f --- /dev/null +++ b/app/services/alert_management/update_alert_status_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module AlertManagement + class UpdateAlertStatusService + def initialize(alert, status) + @alert = alert + @status = status + end + + def execute + return error('Invalid status') unless AlertManagement::Alert.statuses.key?(status.to_s) + + alert.status = status + + if alert.save + success + else + error(alert.errors.full_messages.to_sentence) + end + end + + private + + attr_reader :alert, :status + + def success + ServiceResponse.success(payload: { alert: alert }) + end + + def error(message) + ServiceResponse.error(payload: { alert: alert }, message: message) + end + end +end diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb index 0c938ba00c2..f0a4aff4443 100644 --- a/app/services/spam/spam_action_service.rb +++ b/app/services/spam/spam_action_service.rb @@ -28,6 +28,7 @@ module Spam # update the spam log accordingly. SpamLog.verify_recaptcha!(user_id: user.id, id: spam_log_id) else + return if allowlisted?(user) return unless request return unless check_for_spam? @@ -39,6 +40,10 @@ module Spam private + def allowlisted?(user) + user.respond_to?(:gitlab_employee) && user.gitlab_employee? + end + def perform_spam_service_check(api) # since we can check for spam, and recaptcha is not verified, # ask the SpamVerdictService what to do with the target. diff --git a/app/views/projects/alert_management/details.html.haml b/app/views/projects/alert_management/details.html.haml index 1208c541a9d..766dbf7c128 100644 --- a/app/views/projects/alert_management/details.html.haml +++ b/app/views/projects/alert_management/details.html.haml @@ -1,3 +1,4 @@ -- page_title _('Alert Details') +- add_to_breadcrumbs s_('AlertManagement|Alerts'), project_alert_management_index_path(@project) +- page_title s_('AlertManagement|Alert detail') #js-alert_details diff --git a/changelogs/unreleased/212213-cablett-allowlist-gitlab-team-member.yml b/changelogs/unreleased/212213-cablett-allowlist-gitlab-team-member.yml new file mode 100644 index 00000000000..5f812369a74 --- /dev/null +++ b/changelogs/unreleased/212213-cablett-allowlist-gitlab-team-member.yml @@ -0,0 +1,5 @@ +--- +title: Skip spam check for GitLab team members on gitlab.com +merge_request: 31052 +author: +type: added diff --git a/changelogs/unreleased/212592-disable-binary-edit.yml b/changelogs/unreleased/212592-disable-binary-edit.yml new file mode 100644 index 00000000000..e115d75108e --- /dev/null +++ b/changelogs/unreleased/212592-disable-binary-edit.yml @@ -0,0 +1,5 @@ +--- +title: Disabled Edit button for binary snippets +merge_request: 30904 +author: +type: added diff --git a/changelogs/unreleased/214542-graphql-status-mutation.yml b/changelogs/unreleased/214542-graphql-status-mutation.yml new file mode 100644 index 00000000000..4a8b4cca37a --- /dev/null +++ b/changelogs/unreleased/214542-graphql-status-mutation.yml @@ -0,0 +1,5 @@ +--- +title: Add mutation for AlertManagement's Alert status +merge_request: 30576 +author: +type: added diff --git a/changelogs/unreleased/expose-more-fields-in-alert-management-alert-graphql.yml b/changelogs/unreleased/expose-more-fields-in-alert-management-alert-graphql.yml new file mode 100644 index 00000000000..7003180bb32 --- /dev/null +++ b/changelogs/unreleased/expose-more-fields-in-alert-management-alert-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Exposes description, hosts, details, and timestamps for Alert Management Alert GraphQL +merge_request: 31091 +author: +type: changed diff --git a/changelogs/unreleased/kubeclient-create-or-update.yml b/changelogs/unreleased/kubeclient-create-or-update.yml new file mode 100644 index 00000000000..143738c5195 --- /dev/null +++ b/changelogs/unreleased/kubeclient-create-or-update.yml @@ -0,0 +1,5 @@ +--- +title: Uses Kubernetes API conventions to create or update a resource leandrogs +merge_request: 29010 +author: Leandro Silva +type: performance diff --git a/changelogs/unreleased/remove-sidekiq-rake-tasks.yml b/changelogs/unreleased/remove-sidekiq-rake-tasks.yml new file mode 100644 index 00000000000..f2677b18146 --- /dev/null +++ b/changelogs/unreleased/remove-sidekiq-rake-tasks.yml @@ -0,0 +1,5 @@ +--- +title: Remove deprecated Sidekiq rake tasks +merge_request: +author: +type: removed diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md index 88a4f2c97a6..79d4a3328b1 100644 --- a/doc/administration/reference_architectures/index.md +++ b/doc/administration/reference_architectures/index.md @@ -62,11 +62,20 @@ This solution is appropriate for many teams that have a single server at their d You can also optionally configure GitLab to use an [external PostgreSQL service](../external_database.md) or an [external object storage service](../high_availability/object_storage.md) for added performance and reliability at a relatively low complexity cost. -<!-- ## Up to 2,000 users -For up to 2,000 users, defining the reference architecture is [being worked on](https://gitlab.com/gitlab-org/quality/performance/-/issues/223). ---> +> - **Supported users (approximate):** 2,000 +> - **High Availability:** False +> - **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS + +| Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) | +|--------------------------------------------------------------|-------|---------------------------------|---------------|----------------------------| +| GitLab Rails, Sidekiq, Consul ([1](#footnotes)) | 2 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | +| PostgreSQL | 1 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | +| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | +| Cloud Object Storage ([4](#footnotes)) | - | - | - | - | +| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | +| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | ## Up to 3,000 users @@ -79,7 +88,8 @@ server, a PostgreSQL server and a Redis server. A reference architecture with this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/quality/performance/-/issues/223). > - **Supported users (approximate):** 3,000 -> - **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS +> - **High Availability:** True +> - **Test RPS rates:** API: 60 RPS, Web: 6 RPS, Git: 6 RPS | Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) | |--------------------------------------------------------------|-------|---------------------------------|---------------|----------------------------| @@ -99,6 +109,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual ## Up to 5,000 users > - **Supported users (approximate):** 5,000 +> - **High Availability:** True > - **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS | Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) | @@ -119,6 +130,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual ## Up to 10,000 users > - **Supported users (approximate):** 10,000 +> - **High Availability:** True > - **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS | Service | Nodes | GCP Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) | @@ -142,6 +154,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual ## Up to 25,000 users > - **Supported users (approximate):** 25,000 +> - **High Availability:** True > - **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS | Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) | @@ -165,6 +178,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual ## Up to 50,000 users > - **Supported users (approximate):** 50,000 +> - **High Availability:** True > - **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS | Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) | @@ -288,7 +302,10 @@ column. ## Footnotes 1. In our architectures we run each GitLab Rails node using the Puma webserver - and have its number of workers set to 90% of available CPUs along with four threads. + and have its number of workers set to 90% of available CPUs along with four threads. For + nodes that are running Rails with other components the worker value should be reduced + accordingly where we've found 50% achieves a good balance but this is dependent + on workload. 1. Gitaly node requirements are dependent on customer data, specifically the number of projects and their sizes. We recommend two nodes as an absolute minimum for HA environments diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index b4cfd50422a..69ca80015a7 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -143,6 +143,21 @@ Describes an alert from the project's Alert Management """ type AlertManagementAlert { """ + Timestamp the alert was created + """ + createdAt: Time + + """ + Description of the alert + """ + description: String + + """ + Alert details + """ + details: JSON + + """ Timestamp the alert ended """ endedAt: Time @@ -153,6 +168,11 @@ type AlertManagementAlert { eventCount: Int """ + List of hosts the alert came from + """ + hosts: [String!] + + """ Internal ID of the alert """ iid: ID! @@ -186,6 +206,11 @@ type AlertManagementAlert { Title of the alert """ title: String + + """ + Timestamp the alert was last updated + """ + updatedAt: Time } """ @@ -6074,6 +6099,7 @@ type Mutation { todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload todosMarkAllDone(input: TodosMarkAllDoneInput!): TodosMarkAllDonePayload toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload + updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload updateEpic(input: UpdateEpicInput!): UpdateEpicPayload """ @@ -9729,6 +9755,51 @@ enum TypeEnum { project } +""" +Autogenerated input type of UpdateAlertStatus +""" +input UpdateAlertStatusInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The iid of the alert to mutate + """ + iid: String! + + """ + The project the alert to mutate is in + """ + projectPath: ID! + + """ + The status to set the alert + """ + status: AlertManagementStatus! +} + +""" +Autogenerated return type of UpdateAlertStatus +""" +type UpdateAlertStatusPayload { + """ + The alert after mutation + """ + alert: AlertManagementAlert + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Reasons why the mutation failed. + """ + errors: [String!]! +} + input UpdateDiffImagePositionInput { """ Total height of the image diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 7a4bc3f6360..4ea50ac353e 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -395,6 +395,48 @@ "description": "Describes an alert from the project's Alert Management", "fields": [ { + "name": "createdAt", + "description": "Timestamp the alert was created", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": "Description of the alert", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "details", + "description": "Alert details", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "endedAt", "description": "Timestamp the alert ended", "args": [ @@ -423,6 +465,28 @@ "deprecationReason": null }, { + "name": "hosts", + "description": "List of hosts the alert came from", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "iid", "description": "Internal ID of the alert", "args": [ @@ -523,6 +587,20 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "updatedAt", + "description": "Timestamp the alert was last updated", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -18130,6 +18208,33 @@ "deprecationReason": null }, { + "name": "updateAlertStatus", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateAlertStatusInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateAlertStatusPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updateEpic", "description": null, "args": [ @@ -29250,6 +29355,136 @@ }, { "kind": "INPUT_OBJECT", + "name": "UpdateAlertStatusInput", + "description": "Autogenerated input type of UpdateAlertStatus", + "fields": null, + "inputFields": [ + { + "name": "projectPath", + "description": "The project the alert to mutate is in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "iid", + "description": "The iid of the alert to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "status", + "description": "The status to set the alert", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "AlertManagementStatus", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateAlertStatusPayload", + "description": "Autogenerated return type of UpdateAlertStatus", + "fields": [ + { + "name": "alert", + "description": "The alert after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "AlertManagementAlert", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Reasons why the mutation failed.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", "name": "UpdateDiffImagePositionInput", "description": null, "fields": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8b47fbda02e..185df0c2fe4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -52,8 +52,12 @@ Describes an alert from the project's Alert Management | Name | Type | Description | | --- | ---- | ---------- | +| `createdAt` | Time | Timestamp the alert was created | +| `description` | String | Description of the alert | +| `details` | JSON | Alert details | | `endedAt` | Time | Timestamp the alert ended | | `eventCount` | Int | Number of events of this alert | +| `hosts` | String! => Array | List of hosts the alert came from | | `iid` | ID! | Internal ID of the alert | | `monitoringTool` | String | Monitoring tool the alert came from | | `service` | String | Service the alert came from | @@ -61,6 +65,7 @@ Describes an alert from the project's Alert Management | `startedAt` | Time | Timestamp the alert was raised | | `status` | AlertManagementStatus | Status of the alert | | `title` | String | Title of the alert | +| `updatedAt` | Time | Timestamp the alert was last updated | ## AwardEmoji @@ -1515,6 +1520,16 @@ Represents a directory | `type` | EntryType! | Type of tree entry | | `webUrl` | String | Web URL for the tree entry (directory) | +## UpdateAlertStatusPayload + +Autogenerated return type of UpdateAlertStatus + +| Name | Type | Description | +| --- | ---- | ---------- | +| `alert` | AlertManagementAlert | The alert after mutation | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Reasons why the mutation failed. | + ## UpdateEpicPayload Autogenerated return type of UpdateEpic diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index d2632dfe719..d4fa41ca19e 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -549,15 +549,3 @@ To illustrate its life cycle: even if the commit history of the `example` branch has been overwritten by force-push. 1. GitLab Runner fetches the persistent pipeline ref and gets source code from the checkout-SHA. 1. When the pipeline finished, its persistent ref is cleaned up in a background process. - -NOTE: **NOTE**: At this moment, this feature is on by default and can be manually disabled -by disabling `depend_on_persistent_pipeline_ref` feature flag. If you're interested in -manually disabling this behavior, please ask the administrator -to execute the following commands in rails console. - -```shell -> sudo gitlab-rails console # Login to Rails console of GitLab instance. -> project = Project.find_by_full_path('namespace/project-name') # Get the project instance. -> Feature.disable(:depend_on_persistent_pipeline_ref, project) # Disable the feature flag for specific project -> Feature.disable(:depend_on_persistent_pipeline_ref) # Disable the feature flag system-wide -``` diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index 8daa9b40111..05906520c1c 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -63,17 +63,52 @@ Here's a list of the AWS services we will use, with links to pricing information NOTE: **Note:** Please note that while we will be using EBS for storage, we do not recommend using EFS as it may negatively impact GitLab's performance. You can review the [relevant documentation](../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) for more details. -## Creating an IAM EC2 instance role and profile +## Create an IAM EC2 instance role and profile + +As we'll be using [Amazon S3 object storage](#amazon-s3-object-storage), our EC2 instances need to have read, write, and list permissions for our S3 buckets. To avoid embedding AWS keys in our GitLab config, we'll make use of an [IAM Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) to allow our GitLab instance with this access. We'll need to create an IAM policy to attach to our IAM role: + +### Create an IAM Policy + +1. Navigate to the IAM dashboard and click on **Policies** in the left menu. +1. Click **Create policy**, select the `JSON` tab, and add a policy. We want to [follow security best practices and grant _least privilege_](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege), giving our role only the permissions needed to perform the required actions. + 1. Assuming you prefix the S3 bucket names with `gl-` as shown in the diagram, add the following policy: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:AbortMultipartUpload", + "s3::CompleteMultipartUpload", + "s3:ListBucket", + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject", + "s3:PutObjectAcl" + ], + "Resource": [ + "arn:aws:s3:::gl-*/*" + ] + } + ] +} +``` + +1. Click **Review policy**, give your policy a name (we'll use `gl-s3-policy`), and click **Create policy**. -To minimize the permissions of the user, we'll create a new [IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) -role with limited access: +### Create an IAM Role -1. Navigate to the IAM dashboard <https://console.aws.amazon.com/iam/home>, click on **Roles** in the left menu, and +1. Still on the IAM dashboard, click on **Roles** in the left menu, and click **Create role**. 1. Create a new role by selecting **AWS service > EC2**, then click **Next: Permissions**. -1. Choose **AmazonEC2FullAccess** and **AmazonS3FullAccess**, click **Tags** and add tags if needed. -1. Click **Review**, give your role the name (we'll use `GitLabAdmin`), and click **Create role**. +1. In the policy filter, search for the `gl-s3-policy` we created above, select it, and click **Tags**. +1. Add tags if needed and click **Review**. +1. Give the role a name (we'll use `GitLabS3Access`) and click **Create Role**. + +We'll use this role when we [create a launch configuration](#create-a-launch-configuration) later on. ## Configuring the network @@ -575,7 +610,10 @@ HostKey /etc/ssh_static/ssh_host_ed25519_key #### Amazon S3 object storage -Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our [documentation includes configuration instructions](../../administration/object_storage.md) for each of these, and other information about using object storage with GitLab. +Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our documentation includes [instructions on how to configure object storage](../../administration/object_storage.md) for each of these data types, and other information about using object storage with GitLab. + +NOTE: **Note:** +Since we are using the [AWS IAM profile](#create-an-iam-role) we created earlier, be sure to omit the AWS access key and secret access key/value pairs when configuring object storage. Instead, use `'use_iam_profile' => true` in your configuration as shown in the object storage documentation linked above. Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file. @@ -611,7 +649,7 @@ From the EC2 dashboard: 1. Select an instance type best suited for your needs (at least a `c5.xlarge`) and click **Configure details**. 1. Enter a name for your launch configuration (we'll use `gitlab-ha-launch-config`). 1. **Do not** check **Request Spot Instance**. -1. From the **IAM Role** dropdown, pick the `GitLabAdmin` instance role we [created earlier](#creating-an-iam-ec2-instance-role-and-profile). +1. From the **IAM Role** dropdown, pick the `GitLabAdmin` instance role we [created earlier](#create-an-iam-ec2-instance-role-and-profile). 1. Leave the rest as defaults and click **Add Storage**. 1. The root volume is 8GiB by default and should be enough given that we won’t store any data there. Click **Configure Security Group**. 1. Check **Select and existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier. diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb index c7f430490d6..61668b634fd 100644 --- a/lib/gitlab/graphql/authorize/authorize_field_service.rb +++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb @@ -70,7 +70,10 @@ module Gitlab end def filter_allowed(current_user, resolved_type, authorizing_object) - if authorizing_object + if resolved_type.nil? + # We're not rendering anything, for example when a record was not found + # no need to do anything + elsif authorizing_object # Authorizing fields representing scalars, or a simple field with an object resolved_type if allowed_access?(current_user, authorizing_object) elsif @field.connection? @@ -83,9 +86,6 @@ module Gitlab resolved_type.select do |single_object_type| allowed_access?(current_user, single_object_type.object) end - elsif resolved_type.nil? - # We're not rendering anything, for example when a record was not found - # no need to do anything else raise "Can't authorize #{@field}" end diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb index 3b843799d66..ceda18442d6 100644 --- a/lib/gitlab/kubernetes/helm/api.rb +++ b/lib/gitlab/kubernetes/helm/api.rb @@ -99,11 +99,7 @@ module Gitlab command.cluster_role_binding_resource.tap do |cluster_role_binding_resource| break unless cluster_role_binding_resource - if cluster_role_binding_exists?(cluster_role_binding_resource) - kubeclient.update_cluster_role_binding(cluster_role_binding_resource) - else - kubeclient.create_cluster_role_binding(cluster_role_binding_resource) - end + kubeclient.update_cluster_role_binding(cluster_role_binding_resource) end end diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 2151e50a80d..7a2f2db7cb0 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -57,9 +57,7 @@ module Gitlab # RBAC methods delegates to the apis/rbac.authorization.k8s.io api # group client - delegate :create_cluster_role_binding, - :get_cluster_role_binding, - :update_cluster_role_binding, + delegate :update_cluster_role_binding, to: :rbac_client # RBAC methods delegates to the apis/rbac.authorization.k8s.io api @@ -71,9 +69,7 @@ module Gitlab # RBAC methods delegates to the apis/rbac.authorization.k8s.io api # group client - delegate :create_role_binding, - :get_role_binding, - :update_role_binding, + delegate :update_role_binding, to: :rbac_client # non-entity methods that can only work with the core client @@ -134,19 +130,11 @@ module Gitlab end def create_or_update_cluster_role_binding(resource) - if cluster_role_binding_exists?(resource) - update_cluster_role_binding(resource) - else - create_cluster_role_binding(resource) - end + update_cluster_role_binding(resource) end def create_or_update_role_binding(resource) - if role_binding_exists?(resource) - update_role_binding(resource) - else - create_role_binding(resource) - end + update_role_binding(resource) end def create_or_update_service_account(resource) @@ -173,18 +161,6 @@ module Gitlab Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false) end - def cluster_role_binding_exists?(resource) - get_cluster_role_binding(resource.metadata.name) - rescue ::Kubeclient::ResourceNotFoundError - false - end - - def role_binding_exists?(resource) - get_role_binding(resource.metadata.name, resource.metadata.namespace) - rescue ::Kubeclient::ResourceNotFoundError - false - end - def service_account_exists?(resource) get_service_account(resource.metadata.name, resource.metadata.namespace) rescue ::Kubeclient::ResourceNotFoundError diff --git a/lib/gitlab/sidekiq_config/cli_methods.rb b/lib/gitlab/sidekiq_config/cli_methods.rb index c49432f0fc6..0d0efe8ffbd 100644 --- a/lib/gitlab/sidekiq_config/cli_methods.rb +++ b/lib/gitlab/sidekiq_config/cli_methods.rb @@ -48,7 +48,6 @@ module Gitlab # rubocop:enable Gitlab/ModuleWithInstanceVariables def worker_queues(rails_path = Rails.root.to_s) - # https://gitlab.com/gitlab-org/gitlab/issues/199230 worker_names(all_queues(rails_path)) end @@ -75,7 +74,7 @@ module Gitlab private def worker_names(workers) - workers.map { |queue| queue.is_a?(Hash) ? queue[:name] : queue } + workers.map { |queue| queue[:name] } end def query_string_to_lambda(query_string) diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake deleted file mode 100644 index d74878835fd..00000000000 --- a/lib/tasks/sidekiq.rake +++ /dev/null @@ -1,38 +0,0 @@ -namespace :sidekiq do - def deprecation_warning! - warn <<~WARNING - This task is deprecated and will be removed in 13.0 as it is thought to be unused. - - If you are using this task, please comment on the below issue: - https://gitlab.com/gitlab-org/gitlab/issues/196731 - WARNING - end - - desc '[DEPRECATED] GitLab | Sidekiq | Stop sidekiq' - task :stop do - deprecation_warning! - - system(*%w(bin/background_jobs stop)) - end - - desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq' - task :start do - deprecation_warning! - - system(*%w(bin/background_jobs start)) - end - - desc '[DEPRECATED] GitLab | Sidekiq | Restart sidekiq' - task :restart do - deprecation_warning! - - system(*%w(bin/background_jobs restart)) - end - - desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq with launchd on Mac OS X' - task :launchd do - deprecation_warning! - - system(*%w(bin/background_jobs start_silent)) - end -end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 04d92df901e..f1c6e1569dd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1697,15 +1697,18 @@ msgid_plural "Alerts" msgstr[0] "" msgstr[1] "" -msgid "Alert Details" -msgstr "" - msgid "AlertManagement|Acknowledged" msgstr "" msgid "AlertManagement|Alert" msgstr "" +msgid "AlertManagement|Alert detail" +msgstr "" + +msgid "AlertManagement|Alerts" +msgstr "" + msgid "AlertManagement|Authorize external service" msgstr "" @@ -19264,6 +19267,9 @@ msgstr "" msgid "Snippets" msgstr "" +msgid "Snippets with non-text files can only be edited via Git." +msgstr "" + msgid "SnippetsEmptyState|Code snippets" msgstr "" diff --git a/package.json b/package.json index b5b772601eb..dd03299964b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/preset-env": "^7.8.4", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.125.0", - "@gitlab/ui": "13.9.0", + "@gitlab/ui": "14.0.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.2-2", "@sentry/browser": "^5.10.2", diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index d6741aa3141..0c579db52ea 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -4,7 +4,12 @@ import Tracking from '~/tracking'; import stubChildren from 'helpers/stub_children'; import component from '~/registry/explorer/pages/details.vue'; import { createStore } from '~/registry/explorer/stores/'; -import { SET_MAIN_LOADING, SET_INITIAL_STATE } from '~/registry/explorer/stores/mutation_types/'; +import { + SET_MAIN_LOADING, + SET_INITIAL_STATE, + SET_TAGS_LIST_SUCCESS, + SET_TAGS_PAGINATION, +} from '~/registry/explorer/stores/mutation_types/'; import { DELETE_TAG_SUCCESS_MESSAGE, DELETE_TAG_ERROR_MESSAGE, @@ -60,7 +65,9 @@ describe('Details Page', () => { beforeEach(() => { store = createStore(); dispatchSpy = jest.spyOn(store, 'dispatch'); - store.dispatch('receiveTagsListSuccess', tagsListResponse); + dispatchSpy.mockResolvedValue(); + store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data); + store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers); jest.spyOn(Tracking, 'event'); }); diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js index f69b849521d..1d530483093 100644 --- a/spec/frontend/registry/explorer/pages/list_spec.js +++ b/spec/frontend/registry/explorer/pages/list_spec.js @@ -1,5 +1,4 @@ -import VueRouter from 'vue-router'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import { GlPagination, GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui'; import Tracking from '~/tracking'; import component from '~/registry/explorer/pages/list.vue'; @@ -7,22 +6,25 @@ import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdo import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue'; import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue'; -import store from '~/registry/explorer/stores/'; -import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; +import { createStore } from '~/registry/explorer/stores/'; +import { + SET_MAIN_LOADING, + SET_IMAGES_LIST_SUCCESS, + SET_PAGINATION, + SET_INITIAL_STATE, +} from '~/registry/explorer/stores/mutation_types/'; import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE, } from '~/registry/explorer/constants'; import { imagesListResponse } from '../mock_data'; -import { GlModal, GlEmptyState } from '../stubs'; +import { GlModal, GlEmptyState, RouterLink } from '../stubs'; import { $toast } from '../../shared/mocks'; -const localVue = createLocalVue(); -localVue.use(VueRouter); - describe('List Page', () => { let wrapper; let dispatchSpy; + let store; const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' }); const findDeleteModal = () => wrapper.find(GlModal); @@ -39,21 +41,31 @@ describe('List Page', () => { const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert); const findDeleteAlert = () => wrapper.find(GlAlert); - beforeEach(() => { + const mountComponent = ({ mocks } = {}) => { wrapper = shallowMount(component, { - localVue, store, stubs: { GlModal, GlEmptyState, GlSprintf, + RouterLink, }, mocks: { $toast, + $route: { + name: 'foo', + }, + ...mocks, }, }); + }; + + beforeEach(() => { + store = createStore(); dispatchSpy = jest.spyOn(store, 'dispatch'); - store.dispatch('receiveImagesListSuccess', imagesListResponse); + dispatchSpy.mockResolvedValue(); + store.commit(SET_IMAGES_LIST_SUCCESS, imagesListResponse.data); + store.commit(SET_PAGINATION, imagesListResponse.headers); }); afterEach(() => { @@ -61,17 +73,38 @@ describe('List Page', () => { }); describe('Expiration policy notification', () => { + beforeEach(() => { + mountComponent(); + }); it('shows up on project page', () => { expect(findProjectPolicyAlert().exists()).toBe(true); }); it('does show up on group page', () => { - store.dispatch('setInitialState', { isGroupPage: true }); + store.commit(SET_INITIAL_STATE, { isGroupPage: true }); return wrapper.vm.$nextTick().then(() => { expect(findProjectPolicyAlert().exists()).toBe(false); }); }); }); + describe('API calls', () => { + it.each` + imageList | name | called + ${[]} | ${'foo'} | ${['requestImagesList']} + ${imagesListResponse.data} | ${undefined} | ${['requestImagesList']} + ${imagesListResponse.data} | ${'foo'} | ${undefined} + `( + 'with images equal $imageList and name $name dispatch calls $called', + ({ imageList, name, called }) => { + store.commit(SET_IMAGES_LIST_SUCCESS, imageList); + dispatchSpy.mockClear(); + mountComponent({ mocks: { $route: { name } } }); + + expect(dispatchSpy.mock.calls[0]).toEqual(called); + }, + ); + }); + describe('connection error', () => { const config = { characterError: true, @@ -79,12 +112,13 @@ describe('List Page', () => { helpPagePath: 'bar', }; - beforeAll(() => { - store.dispatch('setInitialState', config); + beforeEach(() => { + store.commit(SET_INITIAL_STATE, config); + mountComponent(); }); - afterAll(() => { - store.dispatch('setInitialState', {}); + afterEach(() => { + store.commit(SET_INITIAL_STATE, {}); }); it('should show an empty state', () => { @@ -106,9 +140,12 @@ describe('List Page', () => { }); describe('isLoading is true', () => { - beforeAll(() => store.commit(SET_MAIN_LOADING, true)); + beforeEach(() => { + store.commit(SET_MAIN_LOADING, true); + mountComponent(); + }); - afterAll(() => store.commit(SET_MAIN_LOADING, false)); + afterEach(() => store.commit(SET_MAIN_LOADING, false)); it('shows the skeleton loader', () => { expect(findSkeletonLoader().exists()).toBe(true); @@ -125,7 +162,8 @@ describe('List Page', () => { describe('list is empty', () => { beforeEach(() => { - store.dispatch('receiveImagesListSuccess', { data: [] }); + store.commit(SET_IMAGES_LIST_SUCCESS, []); + mountComponent(); }); it('quick start is not visible', () => { @@ -137,12 +175,13 @@ describe('List Page', () => { }); describe('is group page is true', () => { - beforeAll(() => { - store.dispatch('setInitialState', { isGroupPage: true }); + beforeEach(() => { + store.commit(SET_INITIAL_STATE, { isGroupPage: true }); + mountComponent(); }); - afterAll(() => { - store.dispatch('setInitialState', { isGroupPage: undefined }); + afterEach(() => { + store.commit(SET_INITIAL_STATE, { isGroupPage: undefined }); }); it('group empty state is visible', () => { @@ -156,6 +195,10 @@ describe('List Page', () => { }); describe('list is not empty', () => { + beforeEach(() => { + mountComponent(); + }); + it('quick start is visible', () => { expect(findQuickStartDropdown().exists()).toBe(true); }); diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/registry/explorer/stubs.js index 2c2c7587af9..0e178abfbed 100644 --- a/spec/frontend/registry/explorer/stubs.js +++ b/spec/frontend/registry/explorer/stubs.js @@ -9,3 +9,8 @@ export const GlEmptyState = { template: '<div><slot name="description"></slot></div>', name: 'GlEmptyStateSTub', }; + +export const RouterLink = { + template: `<div><slot></slot></div>`, + props: ['to'], +}; diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index 16a66c70d6a..fb04959a7bf 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -7,26 +7,27 @@ import { shallowMount } from '@vue/test-utils'; describe('Snippet header component', () => { let wrapper; const snippet = { - snippet: { - id: 'gid://gitlab/PersonalSnippet/50', - title: 'The property of Thor', - visibilityLevel: 'private', - webUrl: 'http://personal.dev.null/42', - userPermissions: { - adminSnippet: true, - updateSnippet: true, - reportSnippet: false, - }, - project: null, - author: { - name: 'Thor Odinson', - }, + id: 'gid://gitlab/PersonalSnippet/50', + title: 'The property of Thor', + visibilityLevel: 'private', + webUrl: 'http://personal.dev.null/42', + userPermissions: { + adminSnippet: true, + updateSnippet: true, + reportSnippet: false, + }, + project: null, + author: { + name: 'Thor Odinson', + }, + blob: { + binary: false, }, }; const mutationVariables = { mutation: DeleteSnippetMutation, variables: { - id: snippet.snippet.id, + id: snippet.id, }, }; const errorMsg = 'Foo bar'; @@ -46,10 +47,12 @@ describe('Snippet header component', () => { loading = false, permissions = {}, mutationRes = mutationTypes.RESOLVE, + snippetProps = {}, } = {}) { - const defaultProps = Object.assign({}, snippet); + // const defaultProps = Object.assign({}, snippet, snippetProps); + const defaultProps = Object.assign(snippet, snippetProps); if (permissions) { - Object.assign(defaultProps.snippet.userPermissions, { + Object.assign(defaultProps.userPermissions, { ...permissions, }); } @@ -65,7 +68,9 @@ describe('Snippet header component', () => { wrapper = shallowMount(SnippetHeader, { mocks: { $apollo }, propsData: { - ...defaultProps, + snippet: { + ...defaultProps, + }, }, stubs: { ApolloMutation, @@ -126,6 +131,17 @@ describe('Snippet header component', () => { expect(wrapper.find(GlModal).exists()).toBe(true); }); + it('renders Edit button as disabled for binary snippets', () => { + createComponent({ + snippetProps: { + blob: { + binary: true, + }, + }, + }); + expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(true); + }); + describe('Delete mutation', () => { const { location } = window; diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb new file mode 100644 index 00000000000..3cd5e217571 --- /dev/null +++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::AlertManagement::UpdateAlertStatus do + let_it_be(:current_user) { create(:user) } + let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') } + let_it_be(:project) { alert.project } + let(:new_status) { 'acknowledged' } + let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } } + + specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alerts) } + + describe '#resolve' do + subject(:resolve) { mutation_for(project, current_user).resolve(args) } + + context 'user has access to project' do + before do + project.add_developer(current_user) + end + + it 'changes the status' do + expect { resolve }.to change { alert.reload.status }.from(alert.status).to(new_status) + end + + it 'returns the alert with no errors' do + expect(resolve).to eq( + alert: alert, + errors: [] + ) + end + + context 'error occurs when updating' do + it 'returns the alert with errors' do + # Stub an error on the alert + allow_next_instance_of(Resolvers::AlertManagementAlertResolver) do |resolver| + allow(resolver).to receive(:resolve).and_return(alert) + end + + allow(alert).to receive(:save).and_return(false) + allow(alert).to receive(:errors).and_return( + double(full_messages: %w(foo bar)) + ) + expect(resolve).to eq( + alert: alert, + errors: ['foo and bar'] + ) + end + + context 'invalid status given' do + let(:new_status) { 'invalid_status' } + + it 'returns the alert with errors' do + expect(resolve).to eq( + alert: alert, + errors: ['Invalid status'] + ) + end + end + end + end + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + private + + def mutation_for(project, user) + described_class.new(object: project, context: { current_user: user }, field: nil) + end +end diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb index f66a135171e..169611b2e18 100644 --- a/spec/graphql/types/alert_management/alert_type_spec.rb +++ b/spec/graphql/types/alert_management/alert_type_spec.rb @@ -11,13 +11,18 @@ describe GitlabSchema.types['AlertManagementAlert'] do expected_fields = %i[ iid title + description severity status service monitoring_tool + hosts started_at ended_at event_count + details + created_at + updated_at ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb index 98659dbed57..c1dab5feb91 100644 --- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb +++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb @@ -84,6 +84,16 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do end end + context 'when the field is a connection' do + context 'when it resolves to nil' do + let(:field) { type_with_field(Types::QueryType.connection_type, :read_field, nil).fields['testField'].to_graphql } + + it 'does not fail when authorizing' do + expect(resolved).to be_nil + end + end + end + context 'when the field is a specific type' do let(:custom_type) { type(:read_type) } let(:object_in_field) { double('presented in field') } diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 8147990ecc3..1f925fd45af 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -92,7 +92,6 @@ describe Gitlab::Kubernetes::Helm::API do allow(client).to receive(:get_config_map).and_return(nil) allow(client).to receive(:create_config_map).and_return(nil) allow(client).to receive(:create_service_account).and_return(nil) - allow(client).to receive(:create_cluster_role_binding).and_return(nil) allow(client).to receive(:delete_pod).and_return(nil) allow(namespace).to receive(:ensure_exists!).once end @@ -136,7 +135,7 @@ describe Gitlab::Kubernetes::Helm::API do context 'without a service account' do it 'does not create a service account on kubeclient' do expect(client).not_to receive(:create_service_account) - expect(client).not_to receive(:create_cluster_role_binding) + expect(client).not_to receive(:update_cluster_role_binding) subject.install(command) end @@ -160,15 +159,14 @@ describe Gitlab::Kubernetes::Helm::API do ) end - context 'service account and cluster role binding does not exist' do + context 'service account does not exist' do before do expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil)) - expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil)) end it 'creates a service account, followed the cluster role binding on kubeclient' do expect(client).to receive(:create_service_account).with(service_account_resource).once.ordered - expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered + expect(client).to receive(:update_cluster_role_binding).with(cluster_role_binding_resource).once.ordered subject.install(command) end @@ -177,21 +175,6 @@ describe Gitlab::Kubernetes::Helm::API do context 'service account already exists' do before do expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource) - expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil)) - end - - it 'updates the service account, followed by creating the cluster role binding' do - expect(client).to receive(:update_service_account).with(service_account_resource).once.ordered - expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered - - subject.install(command) - end - end - - context 'service account and cluster role binding already exists' do - before do - expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource) - expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_return(cluster_role_binding_resource) end it 'updates the service account, followed by creating the cluster role binding' do @@ -216,7 +199,7 @@ describe Gitlab::Kubernetes::Helm::API do context 'legacy abac cluster' do it 'does not create a service account on kubeclient' do expect(client).not_to receive(:create_service_account) - expect(client).not_to receive(:create_cluster_role_binding) + expect(client).not_to receive(:update_cluster_role_binding) subject.install(command) end diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 56838f2cb7a..af328eaec9a 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -234,8 +234,6 @@ describe Gitlab::Kubernetes::KubeClient do :create_role, :get_role, :update_role, - :create_cluster_role_binding, - :get_cluster_role_binding, :update_cluster_role_binding ].each do |method| describe "##{method}" do @@ -354,6 +352,16 @@ describe Gitlab::Kubernetes::KubeClient do end end + shared_examples 'create_or_update method using put' do + let(:update_method) { "update_#{resource_type}" } + + it 'calls the update method' do + expect(client).to receive(update_method).with(resource) + + subject + end + end + shared_examples 'create_or_update method' do let(:get_method) { "get_#{resource_type}" } let(:update_method) { "update_#{resource_type}" } @@ -393,7 +401,7 @@ describe Gitlab::Kubernetes::KubeClient do subject { client.create_or_update_cluster_role_binding(resource) } - it_behaves_like 'create_or_update method' + it_behaves_like 'create_or_update method using put' end describe '#create_or_update_role_binding' do @@ -405,7 +413,7 @@ describe Gitlab::Kubernetes::KubeClient do subject { client.create_or_update_role_binding(resource) } - it_behaves_like 'create_or_update method' + it_behaves_like 'create_or_update method using put' end describe '#create_or_update_service_account' do diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb index 0aaff12f278..80e8da58f23 100644 --- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb +++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb @@ -54,14 +54,6 @@ describe Gitlab::SidekiqConfig::CliMethods do end end - context 'when the file contains an array of strings' do - before do - stub_contents(['queue_a'], ['queue_b']) - end - - include_examples 'valid file contents' - end - context 'when the file contains an array of hashes' do before do stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }]) diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb index 2e552875252..b40b18aeb4c 100644 --- a/spec/models/alert_management/alert_spec.rb +++ b/spec/models/alert_management/alert_spec.rb @@ -126,4 +126,30 @@ describe AlertManagement::Alert do it { is_expected.to match_array(alert_1) } end + + describe '.details' do + let(:payload) do + { + 'title' => 'Details title', + 'custom' => { + 'alert' => { + 'fields' => %w[one two] + } + }, + 'yet' => { + 'another' => 'field' + } + } + end + let(:alert) { build(:alert_management_alert, title: 'Details title', payload: payload) } + + subject { alert.details } + + it 'renders the payload as inline hash' do + is_expected.to eq( + 'custom.alert.fields' => %w[one two], + 'yet.another' => 'field' + ) + end + end end diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb index 4cece0664cf..89dd9b05331 100644 --- a/spec/models/ci/persistent_ref_spec.rb +++ b/spec/models/ci/persistent_ref_spec.rb @@ -45,18 +45,6 @@ describe Ci::PersistentRef do expect(pipeline.persistent_ref).to be_exist end - context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do - before do - stub_feature_flags(depend_on_persistent_pipeline_ref: false) - end - - it 'does not create a persistent ref' do - expect(project.repository).not_to receive(:create_ref) - - subject - end - end - context 'when sha does not exist in the repository' do let(:sha) { 'not-exist' } diff --git a/spec/policies/alert_management/alert_policy_spec.rb b/spec/policies/alert_management/alert_policy_spec.rb index 698264e4ac7..523464d8ff1 100644 --- a/spec/policies/alert_management/alert_policy_spec.rb +++ b/spec/policies/alert_management/alert_policy_spec.rb @@ -6,17 +6,20 @@ describe AlertManagement::AlertPolicy, :models do let(:alert) { create(:alert_management_alert) } let(:project) { alert.project } let(:user) { create(:user) } - let(:policy) { described_class.new(user, alert) } + + subject(:policy) { described_class.new(user, alert) } describe 'rules' do - it { expect(policy).to be_disallowed :read_alert_management_alerts } + it { is_expected.to be_disallowed :read_alert_management_alerts } + it { is_expected.to be_disallowed :update_alert_management_alerts } context 'when developer' do before do project.add_developer(user) end - it { expect(policy).to be_allowed :read_alert_management_alerts } + it { is_expected.to be_allowed :read_alert_management_alerts } + it { is_expected.to be_allowed :update_alert_management_alerts } end end end diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb index 28806a1c7e3..6b2b031dbd0 100644 --- a/spec/presenters/ci/build_runner_presenter_spec.rb +++ b/spec/presenters/ci/build_runner_presenter_spec.rb @@ -173,81 +173,34 @@ describe Ci::BuildRunnerPresenter do let(:pipeline) { merge_request.all_pipelines.first } let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) } - context 'when depend_on_persistent_pipeline_ref feature flag is enabled' do - before do - stub_feature_flags(ci_force_exposing_merge_request_refs: false) - pipeline.persistent_ref.create - end - - it 'returns the correct refspecs' do - is_expected - .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}") - end - - context 'when ci_force_exposing_merge_request_refs feature flag is enabled' do - before do - stub_feature_flags(ci_force_exposing_merge_request_refs: true) - end - - it 'returns the correct refspecs' do - is_expected - .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - '+refs/merge-requests/1/head:refs/merge-requests/1/head') - end - end - - context 'when GIT_DEPTH is zero' do - before do - create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline) - end - - it 'returns the correct refspecs' do - is_expected - .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - '+refs/heads/*:refs/remotes/origin/*', - '+refs/tags/*:refs/tags/*') - end - end - - context 'when pipeline is legacy detached merge request pipeline' do - let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) } + before do + pipeline.persistent_ref.create + end - it 'returns the correct refspecs' do - is_expected.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", - "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}") - end - end + it 'returns the correct refspecs' do + is_expected + .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}") end - context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do + context 'when GIT_DEPTH is zero' do before do - stub_feature_flags(depend_on_persistent_pipeline_ref: false) + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline) end it 'returns the correct refspecs' do is_expected - .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head') - end - - context 'when GIT_DEPTH is zero' do - before do - create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline) - end - - it 'returns the correct refspecs' do - is_expected - .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head', - '+refs/heads/*:refs/remotes/origin/*', - '+refs/tags/*:refs/tags/*') - end + .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", + '+refs/heads/*:refs/remotes/origin/*', + '+refs/tags/*:refs/tags/*') end + end - context 'when pipeline is legacy detached merge request pipeline' do - let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) } + context 'when pipeline is legacy detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) } - it 'returns the correct refspecs' do - is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}") - end + it 'returns the correct refspecs' do + is_expected.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}", + "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}") end end end diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb index 6722357f76d..5d60f054bf2 100644 --- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -4,10 +4,11 @@ require 'spec_helper' describe 'getting Alert Management Alerts' do include GraphqlHelpers + let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } } let_it_be(:project) { create(:project, :repository) } let_it_be(:current_user) { create(:user) } let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, project: project) } - let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, project: project) } + let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, project: project, payload: payload) } let(:fields) do <<~QUERY @@ -55,13 +56,18 @@ describe 'getting Alert Management Alerts' do expect(first_alert).to include( 'iid' => alert_2.iid.to_s, 'title' => alert_2.title, + 'description' => alert_2.description, 'severity' => alert_2.severity.upcase, 'status' => alert_2.status.upcase, 'monitoringTool' => alert_2.monitoring_tool, 'service' => alert_2.service, + 'hosts' => alert_2.hosts, 'eventCount' => alert_2.events, 'startedAt' => alert_2.started_at.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'endedAt' => alert_2.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ') + 'endedAt' => alert_2.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'details' => { 'custom.alert' => 'payload' }, + 'createdAt' => alert_2.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'updatedAt' => alert_2.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ') ) end diff --git a/spec/services/alert_management/update_alert_status_service_spec.rb b/spec/services/alert_management/update_alert_status_service_spec.rb new file mode 100644 index 00000000000..325b03840d3 --- /dev/null +++ b/spec/services/alert_management/update_alert_status_service_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AlertManagement::UpdateAlertStatusService do + let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') } + + describe '#execute' do + subject(:execute) { described_class.new(alert, new_status).execute } + + let(:new_status) { 'acknowledged' } + + it 'updates the status' do + expect { execute }.to change { alert.status }.to(new_status) + end + + context 'with unknown status' do + let(:new_status) { 'random_status' } + + it 'returns an error' do + expect(execute.status).to eq(:error) + end + + it 'does not update the status' do + expect { execute }.not_to change { alert.status } + end + end + end +end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 43dbea959a2..4d1548c9786 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -108,8 +108,7 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do } ) - stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin') - stub_kubeclient_create_cluster_role_binding(api_url) + stub_kubeclient_put_cluster_role_binding(api_url, 'gitlab-admin') end end diff --git a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb index 3982d2310d8..6d8b1617c17 100644 --- a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb @@ -28,7 +28,6 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do stub_kubeclient_get_secret_error(api_url, 'gitlab-token') stub_kubeclient_create_secret(api_url) - stub_kubeclient_get_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace) stub_kubeclient_put_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace) stub_kubeclient_get_namespace(api_url, namespace: namespace) stub_kubeclient_get_service_account_error(api_url, "#{namespace}-service-account", namespace: namespace) diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb index 8fa22422074..4bcd5c6933e 100644 --- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb @@ -83,8 +83,7 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do before do cluster.platform_kubernetes.rbac! - stub_kubeclient_get_cluster_role_binding_error(api_url, cluster_role_binding_name) - stub_kubeclient_create_cluster_role_binding(api_url) + stub_kubeclient_put_cluster_role_binding(api_url, cluster_role_binding_name) end it_behaves_like 'creates service account and token' @@ -92,9 +91,8 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do it 'creates a cluster role binding with cluster-admin access' do subject - expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with( + expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/gitlab-admin").with( body: hash_including( - kind: 'ClusterRoleBinding', metadata: { name: 'gitlab-admin' }, roleRef: { apiGroup: 'rbac.authorization.k8s.io', @@ -143,8 +141,7 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do before do cluster.platform_kubernetes.rbac! - stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace) - stub_kubeclient_create_role_binding(api_url, namespace: namespace) + stub_kubeclient_put_role_binding(api_url, role_binding_name, namespace: namespace) stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace) stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME, namespace: namespace) @@ -166,9 +163,8 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do it 'creates a namespaced role binding with edit access' do subject - expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings").with( + expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{role_binding_name}").with( body: hash_including( - kind: 'RoleBinding', metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" }, roleRef: { apiGroup: 'rbac.authorization.k8s.io', diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb index ff60eacc79d..560833aba97 100644 --- a/spec/services/spam/spam_action_service_spec.rb +++ b/spec/services/spam/spam_action_service_spec.rb @@ -73,11 +73,13 @@ describe Spam::SpamActionService do describe '#execute' do let(:request) { double(:request, env: env) } let(:fake_verdict_service) { double(:spam_verdict_service) } + let(:allowlisted) { false } let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) } subject do described_service = described_class.new(spammable: issue, request: request) + allow(described_service).to receive(:allowlisted?).and_return(allowlisted) described_service.execute(user: user, api: nil, recaptcha_verified: recaptcha_verified, spam_log_id: existing_spam_log.id) end @@ -121,6 +123,16 @@ describe Spam::SpamActionService do issue.description = 'SPAM!' end + context 'if allowlisted' do + let(:allowlisted) { true } + + it 'does not perform spam check' do + expect(Spam::SpamVerdictService).not_to receive(:new) + + subject + end + end + context 'when disallowed by the spam verdict service' do before do allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW) diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index ca910e47695..6d33d1f213f 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -201,28 +201,8 @@ module KubernetesHelpers .to_return(kube_response({})) end - def stub_kubeclient_get_cluster_role_binding_error(api_url, name, status: 404) - WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}") - .to_return(status: [status, "Internal Server Error"]) - end - - def stub_kubeclient_create_cluster_role_binding(api_url) - WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings') - .to_return(kube_response({})) - end - - def stub_kubeclient_get_role_binding(api_url, name, namespace: 'default') - WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}") - .to_return(kube_response({})) - end - - def stub_kubeclient_get_role_binding_error(api_url, name, namespace: 'default', status: 404) - WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}") - .to_return(status: [status, "Internal Server Error"]) - end - - def stub_kubeclient_create_role_binding(api_url, namespace: 'default') - WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings") + def stub_kubeclient_put_cluster_role_binding(api_url, name) + WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}") .to_return(kube_response({})) end diff --git a/yarn.lock b/yarn.lock index 0a4ef63d580..1fa766a6274 100644 --- a/yarn.lock +++ b/yarn.lock @@ -787,10 +787,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.125.0.tgz#59c667dae8f7e4c80b482f5f6cc35367c016387b" integrity sha512-MKfFYa8f+9P2tJ/JN/E9oDBSSo/gRz2zuGui4XHQPoaw/DkIMn7EyAzeSpRgbgs1LgMcEqqKsIEx+spCga3jsQ== -"@gitlab/ui@13.9.0": - version "13.9.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-13.9.0.tgz#c75c3c6adc92e71a5e7915fe6d1c9fa6e2d5f85d" - integrity sha512-fpjjMXAyOGIITR/Jb7zmw7ul5EAwdSdivmJsiQnwb9eetjNgVlguYu0ZZM0YAdgRXeeIRyVaS8OCqTeyD02yFQ== +"@gitlab/ui@14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.0.0.tgz#d478b1454659c0f54b72cdecce1c2014fc5f8564" + integrity sha512-R+unP0mOBYQ+uRJLm/tI+2znsbsHY2rumSYtMqM3vGCXasteySQIMZ8huWGa5Cf4ZUdy1lNa0J/zxKj6TLdjCQ== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |