diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 5dc156fbcaa6d63f74339357b769c05f4073ed80..edacbee8cabc90da707ddf51fa1122d49b5dcf22 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -555,6 +555,7 @@ when: never - <<: *if-merge-request changes: *db-patterns + when: manual .rails:rules:ee-and-foss-unit: rules: diff --git a/.gitlab/ci/workhorse.gitlab-ci.yml b/.gitlab/ci/workhorse.gitlab-ci.yml index 8361d20d2b79c68a4d53dd749650ef3b556bec04..ba4523f3bf75cd8b39d08552a736681bc8cd38b3 100644 --- a/.gitlab/ci/workhorse.gitlab-ci.yml +++ b/.gitlab/ci/workhorse.gitlab-ci.yml @@ -1,6 +1,6 @@ workhorse:verify: extends: .workhorse:rules:workhorse - image: ${GITLAB_DEPENDENCY_PROXY}golang:1.15 + image: ${GITLAB_DEPENDENCY_PROXY}golang:1.16 stage: test needs: [] script: @@ -23,14 +23,10 @@ workhorse:verify: - apt-get update && apt-get -y install libimage-exiftool-perl - make -C workhorse test -workhorse:test using go 1.13: - extends: .workhorse:test - image: ${GITLAB_DEPENDENCY_PROXY}golang:1.13 - -workhorse:test using go 1.14: - extends: .workhorse:test - image: ${GITLAB_DEPENDENCY_PROXY}golang:1.14 - workhorse:test using go 1.15: extends: .workhorse:test image: ${GITLAB_DEPENDENCY_PROXY}golang:1.15 + +workhorse:test using go 1.16: + extends: .workhorse:test + image: ${GITLAB_DEPENDENCY_PROXY}golang:1.16 diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 8964f55d9e90002feae229e016b634ff9e3c056f..462896509eefdc6d44db3338ed192eee97de2bcf 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -431,8 +431,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - ee/spec/controllers/projects/merge_requests_controller_spec.rb - ee/spec/controllers/projects/mirrors_controller_spec.rb - ee/spec/controllers/projects/threat_monitoring_controller_spec.rb - - ee/spec/controllers/registrations/groups_controller_spec.rb - - ee/spec/controllers/registrations/projects_controller_spec.rb - ee/spec/controllers/subscriptions_controller_spec.rb - ee/spec/features/boards/group_boards/multiple_boards_spec.rb - ee/spec/features/ci_shared_runner_warnings_spec.rb @@ -932,9 +930,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - spec/services/audit_event_service_spec.rb - spec/services/auth/dependency_proxy_authentication_service_spec.rb - spec/services/auto_merge_service_spec.rb - - spec/services/award_emojis/add_service_spec.rb - - spec/services/award_emojis/destroy_service_spec.rb - - spec/services/award_emojis/toggle_service_spec.rb - spec/services/boards/destroy_service_spec.rb - spec/services/boards/issues/move_service_spec.rb - spec/services/bulk_create_integration_service_spec.rb @@ -968,9 +963,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - spec/services/design_management/save_designs_service_spec.rb - spec/services/discussions/resolve_service_spec.rb - spec/services/discussions/unresolve_service_spec.rb - - spec/services/environments/auto_stop_service_spec.rb - - spec/services/environments/canary_ingress/update_service_spec.rb - - spec/services/environments/reset_auto_stop_service_spec.rb - spec/services/feature_flags/create_service_spec.rb - spec/services/feature_flags/destroy_service_spec.rb - spec/services/feature_flags/disable_service_spec.rb diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js index 8170a1f8443670609842247303c665a5910bf87c..cadcab16f16afbee600d0e780443d25f7951b7a8 100644 --- a/app/assets/javascripts/persistent_user_callouts.js +++ b/app/assets/javascripts/persistent_user_callouts.js @@ -7,6 +7,7 @@ const PERSISTENT_USER_CALLOUTS = [ '.js-buy-pipeline-minutes-notification-callout', '.js-token-expiry-callout', '.js-registration-enabled-callout', + '.js-service-templates-deprecated-callout', '.js-new-user-signups-cap-reached', '.js-eoa-bronze-plan-banner', ]; diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 10c7b4032cf938d64b9f542007cc3e2283b57423..7bf3cb6230b7ea91539fdd45a7e823638b382f0d 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -190,12 +190,8 @@ def diff_file_changed_icon_color(diff_file) def render_overflow_warning?(diffs_collection) diff_files = diffs_collection.raw_diff_files - if diff_files.any?(&:too_large?) - Gitlab::Metrics.add_event(:diffs_overflow_single_file_limits) - end - diff_files.overflow?.tap do |overflown| - Gitlab::Metrics.add_event(:diffs_overflow_collection_limits) if overflown + log_overflow_limits(diff_files) end end @@ -286,4 +282,18 @@ def conflicts conflicts_service.conflicts.files.index_by(&:our_path) end + + def log_overflow_limits(diff_files) + if diff_files.any?(&:too_large?) + Gitlab::Metrics.add_event(:diffs_overflow_single_file_limits) + end + + Gitlab::Metrics.add_event(:diffs_overflow_collection_limits) if diff_files.overflow? + Gitlab::Metrics.add_event(:diffs_overflow_max_bytes_limits) if diff_files.overflow_max_bytes? + Gitlab::Metrics.add_event(:diffs_overflow_max_files_limits) if diff_files.overflow_max_files? + Gitlab::Metrics.add_event(:diffs_overflow_max_lines_limits) if diff_files.overflow_max_lines? + Gitlab::Metrics.add_event(:diffs_overflow_collapsed_bytes_limits) if diff_files.collapsed_safe_bytes? + Gitlab::Metrics.add_event(:diffs_overflow_collapsed_files_limits) if diff_files.collapsed_safe_files? + Gitlab::Metrics.add_event(:diffs_overflow_collapsed_lines_limits) if diff_files.collapsed_safe_lines? + end end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index d1db624432b45eb944393f0854b407679602ccab..7a90984cd777ee29f296100aa0f9a0723b8ea16f 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -5,7 +5,7 @@ module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration' GCP_SIGNUP_OFFER = 'gcp_signup_offer' SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed' - SERVICE_TEMPLATES_DEPRECATED = 'service_templates_deprecated' + SERVICE_TEMPLATES_DEPRECATED_CALLOUT = 'service_templates_deprecated_callout' TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight' WEBHOOKS_MOVED = 'webhooks_moved' CUSTOMIZE_HOMEPAGE = 'customize_homepage' @@ -41,8 +41,11 @@ def show_suggest_popover? !user_dismissed?(SUGGEST_POPOVER_DISMISSED) end - def show_service_templates_deprecated? - !user_dismissed?(SERVICE_TEMPLATES_DEPRECATED) + def show_service_templates_deprecated_callout? + !Gitlab.com? && + current_user&.admin? && + Service.for_template.active.exists? && + !user_dismissed?(SERVICE_TEMPLATES_DEPRECATED_CALLOUT) end def show_webhooks_moved_alert? diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 292d249cb23174b5dd295d03457be25322863833..c89a132bdd6d6d03f1c45136c565f0d1d8b4863f 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -179,18 +179,6 @@ class CommitStatus < ApplicationRecord ExpireJobCacheWorker.perform_async(id) end end - - after_transition any => :failed do |commit_status| - next if Feature.enabled?(:async_add_build_failure_todo, commit_status.project, default_enabled: :yaml) - next unless commit_status.project - - # rubocop: disable CodeReuse/ServiceClass - commit_status.run_after_commit do - MergeRequests::AddTodoWhenBuildFailsService - .new(project, nil).execute(self) - end - # rubocop: enable CodeReuse/ServiceClass - end end def self.names diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb index 37bbb9b875239956ffbc57b30a2304954dc939e5..d7adf63fde4cc6dcfa9ef6590e8f7c5457509d60 100644 --- a/app/models/project_services/discord_service.rb +++ b/app/models/project_services/discord_service.rb @@ -3,6 +3,8 @@ require "discordrb/webhooks" class DiscordService < ChatNotificationService + include ActionView::Helpers::UrlHelper + ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze def title @@ -10,7 +12,7 @@ def title end def description - s_("DiscordService|Receive event notifications in Discord") + s_("DiscordService|Send notifications about project events to a Discord channel.") end def self.to_param @@ -18,13 +20,8 @@ def self.to_param end def help - "This service sends notifications about project events to Discord channels.<br /> - To set up this service: - <ol> - <li><a href='https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks'>Setup a custom Incoming Webhook</a>.</li> - <li>Paste the <strong>Webhook URL</strong> into the field below.</li> - <li>Select events below to enable notifications.</li> - </ol>" + docs_link = link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer' + s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end def event_field(event) @@ -36,13 +33,12 @@ def default_channel_placeholder end def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] + %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] end def default_fields [ - { type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" }, + { type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." }, { type: "checkbox", name: "notify_only_broken_pipelines" }, { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } ] diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index a1192ceaeda13ee24f53b6b56309faad12266e5f..34996b771a083d0f425659824e8e15d978dd3d23 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -17,7 +17,7 @@ class UserCallout < ApplicationRecord threat_monitoring_info: 11, # EE-only account_recovery_regular_check: 12, # EE-only webhooks_moved: 13, - service_templates_deprecated: 14, + service_templates_deprecated_callout: 14, admin_integrations_moved: 15, web_ide_alert_dismissed: 16, # no longer in use active_user_count_threshold: 18, # EE-only diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1f2fcd1c70bf2cfc338b6031cb32bdf6970d9f23..c91d27e3ed1fa0c7ae74040f3a2dc3b6596d6293 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -11,6 +11,7 @@ = render "layouts/broadcast" = render "layouts/header/read_only_banner" = render "layouts/header/registration_enabled_callout" + = render "layouts/header/service_templates_deprecation_callout" = render "layouts/nav/classification_level_banner" = yield :flash_message = render "shared/ping_consent" diff --git a/app/views/layouts/header/_service_templates_deprecation_callout.html.haml b/app/views/layouts/header/_service_templates_deprecation_callout.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..056d4426d5a4bb7799187dbc1bb6ab76c28d15c9 --- /dev/null +++ b/app/views/layouts/header/_service_templates_deprecation_callout.html.haml @@ -0,0 +1,21 @@ +- return unless show_service_templates_deprecated_callout? + +- doc_link_start = "<a href=\"#{integrations_help_page_path}\" target='_blank' rel='noopener noreferrer'>".html_safe +- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe + +%div{ class: [container_class, @content_class, 'gl-pt-5!'] } + .gl-alert.gl-alert-warning.js-service-templates-deprecated-callout{ role: 'alert', data: { feature_id: UserCalloutsHelper::SERVICE_TEMPLATES_DEPRECATED_CALLOUT, dismiss_endpoint: user_callouts_path } } + = sprite_icon('warning', size: 16, css_class: 'gl-alert-icon') + %button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, data: { testid: 'close-service-templates-deprecated-callout' } } + = sprite_icon('close', size: 16) + .gl-alert-title + = s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.') + .gl-alert-body + = html_escape_once(s_('AdminSettings|You should migrate to %{doc_link_start}Project integration management%{link_end}, available at %{settings_link_start}Settings > Integrations.%{link_end}')).html_safe % { doc_link_start: doc_link_start, settings_link_start: settings_link_start, link_end: '</a>'.html_safe } + .gl-alert-actions + = link_to admin_application_settings_services_path, class: 'btn gl-alert-action btn-info btn-md gl-button' do + %span.gl-button-text + = s_('AdminSettings|See affected service templates') + = link_to "https://gitlab.com/gitlab-org/gitlab/-/issues/325905", class: 'btn gl-alert-action btn-default btn-md gl-button', target: '_blank', rel: 'noopener noreferrer' do + %span.gl-button-text + = _('Leave feedback') diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index c9cfb1da10bc13f7ca91fa08431d7712afef2ee6..aeda8d113aca91d262a128236590fe22d0c66f38 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -37,7 +37,7 @@ def process_build(build) ExpirePipelineCacheWorker.perform_async(build.pipeline_id) ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat? - if build.failed? && Feature.enabled?(:async_add_build_failure_todo, build.project, default_enabled: :yaml) + if build.failed? ::Ci::MergeRequests::AddTodoWhenBuildFailsWorker.perform_async(build.id) end diff --git a/changelogs/unreleased/31063-ensure-diff-collection-limits-are-configurable.yml b/changelogs/unreleased/31063-ensure-diff-collection-limits-are-configurable.yml new file mode 100644 index 0000000000000000000000000000000000000000..9e8918810c41d98544b2cfebd2d049d7e7b0fa36 --- /dev/null +++ b/changelogs/unreleased/31063-ensure-diff-collection-limits-are-configurable.yml @@ -0,0 +1,5 @@ +--- +title: Track the different overflows for diff collections +merge_request: 57790 +author: +type: other diff --git a/changelogs/unreleased/325196-service-template-deprecation-update.yml b/changelogs/unreleased/325196-service-template-deprecation-update.yml new file mode 100644 index 0000000000000000000000000000000000000000..4aefb4a083abd35cc50f27e9321624a897e9ef71 --- /dev/null +++ b/changelogs/unreleased/325196-service-template-deprecation-update.yml @@ -0,0 +1,5 @@ +--- +title: Add global callout for Service template deprecation +merge_request: 58613 +author: +type: changed diff --git a/changelogs/unreleased/325691-add-sync_code_onwers_approval_rules_worker.yml b/changelogs/unreleased/325691-add-sync_code_onwers_approval_rules_worker.yml new file mode 100644 index 0000000000000000000000000000000000000000..48f6211f02cf5daa624da3204659d408ad7652f8 --- /dev/null +++ b/changelogs/unreleased/325691-add-sync_code_onwers_approval_rules_worker.yml @@ -0,0 +1,5 @@ +--- +title: Add new MergeRequests::SyncCodeOwnerApprovalRulesWorker +merge_request: 58512 +author: +type: performance diff --git a/changelogs/unreleased/ab-remove-async_add_build_failure_todo-feature-flag.yml b/changelogs/unreleased/ab-remove-async_add_build_failure_todo-feature-flag.yml new file mode 100644 index 0000000000000000000000000000000000000000..76b5214d2782c2d48b293035189b1714f58c55f1 --- /dev/null +++ b/changelogs/unreleased/ab-remove-async_add_build_failure_todo-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance by moving TODO creation out of the jobs/request path +merge_request: 59022 +author: +type: performance diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-award-emojis.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-award-emojis.yml new file mode 100644 index 0000000000000000000000000000000000000000..5b846c00c08ab6a461e08258763e61b4f2e2e271 --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-award-emojis.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/award_emojis +merge_request: 58407 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-environments.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-environments.yml new file mode 100644 index 0000000000000000000000000000000000000000..33c8ea69627352ea95fa538102e3f543c5ac9107 --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-environments.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/environments +merge_request: 58418 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/pks-workhorse-go-1-16.yml b/changelogs/unreleased/pks-workhorse-go-1-16.yml new file mode 100644 index 0000000000000000000000000000000000000000..8404dd721b1d0339780abd203861ef2db7d09ac6 --- /dev/null +++ b/changelogs/unreleased/pks-workhorse-go-1-16.yml @@ -0,0 +1,5 @@ +--- +title: Bump minimum required Go version for workhorse to 1.15 +merge_request: 59347 +author: +type: other diff --git a/changelogs/unreleased/ui-text-discord-integration.yml b/changelogs/unreleased/ui-text-discord-integration.yml new file mode 100644 index 0000000000000000000000000000000000000000..df25bf624b9837ceb75191403967ce1396af419e --- /dev/null +++ b/changelogs/unreleased/ui-text-discord-integration.yml @@ -0,0 +1,5 @@ +--- +title: Update Discord integration UI text +merge_request: 58842 +author: +type: other diff --git a/config/feature_flags/development/async_add_build_failure_todo.yml b/config/feature_flags/development/async_add_build_failure_todo.yml deleted file mode 100644 index 3c1e056a50ecef277d34e1a5c83aeacf6307e055..0000000000000000000000000000000000000000 --- a/config/feature_flags/development/async_add_build_failure_todo.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: async_add_build_failure_todo -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57490/diffs -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326726 -milestone: '13.11' -type: development -group: group::continuous integration -default_enabled: true diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 7febb3770d1234d9f7d5506b3c1cba91633b0e4a..c0aab89fd4639ce2fc4c7ddae22299784f12bad0 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -220,6 +220,8 @@ - 1 - - merge_requests_resolve_todos - 1 +- - merge_requests_sync_code_owner_approval_rules + - 1 - - metrics_dashboard_prune_old_annotations - 1 - - metrics_dashboard_sync_dashboards diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 4ba1a85babcb1a3ec231b3d889423dd2b374de67..9f87192aab0862820fe0ceb52b6d6e4cae999b59 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -250,10 +250,11 @@ configuration option in `gitlab.yml`. These metrics are served from the The following metrics are available: -| Metric | Type | Since | Description | -|:--------------------------------- |:--------- |:------------------------------------------------------------- |:-------------------------------------- | -| `db_load_balancing_hosts` | Gauge | [12.3](https://gitlab.com/gitlab-org/gitlab/-/issues/13630) | Current number of load balancing hosts | - +| Metric | Type | Since | Description | Labels | +|:--------------------------------- |:--------- |:------------------------------------------------------------- |:-------------------------------------- |:--------------------------------------------------------- | +| `db_load_balancing_hosts` | Gauge | [12.3](https://gitlab.com/gitlab-org/gitlab/-/issues/13630) | Current number of load balancing hosts | | +| `sidekiq_load_balancing_count` | Counter | 13.11 | Sidekiq jobs using load balancing with data consistency set to :sticky or :delayed | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency`, `data_consistency`, `database_chosen` | + ## Database partitioning metrics **(PREMIUM SELF)** The following metrics are available: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8abeacd0d0e1225556f815043fff9d807d3e8f1c..4a3348e9609700c506b2666cca07d6be96e0d245 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -63,6 +63,12 @@ Returns [`ContainerRepositoryDetails`](#containerrepositorydetails). | ---- | ---- | ----------- | | `id` | [`ContainerRepositoryID!`](#containerrepositoryid) | The global ID of the container repository. | +### `currentLicense` + +Fields related to the current license. + +Returns [`CurrentLicense`](#currentlicense). + ### `currentUser` Get information about current user. @@ -181,6 +187,21 @@ Returns [`Iteration`](#iteration). | ---- | ---- | ----------- | | `id` | [`IterationID!`](#iterationid) | Find an iteration by its ID. | +### `licenseHistoryEntries` + +Fields related to entries in the license history. + +Returns [`LicenseHistoryEntryConnection`](#licensehistoryentryconnection). + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. | +| `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. | +| `first` | [`Int`](#int) | Returns the first _n_ elements from the list. | +| `last` | [`Int`](#int) | Returns the last _n_ elements from the list. | + ### `metadata` Metadata about GitLab. @@ -1872,6 +1893,27 @@ Autogenerated return type of CreateTestCase. | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `testCase` | [`Issue`](#issue) | The test case created. | +### `CurrentLicense` + +Represents the current license. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `activatedAt` | [`Date`](#date) | Date when the license was activated. | +| `billableUsersCount` | [`Int`](#int) | Number of billable users on the system. | +| `company` | [`String`](#string) | Company of the licensee. | +| `email` | [`String`](#string) | Email of the licensee. | +| `expiresAt` | [`Date`](#date) | Date when the license expires. | +| `id` | [`ID!`](#id) | ID of the license. | +| `lastSync` | [`Time`](#time) | Date when the license was last synced. | +| `maximumUserCount` | [`Int`](#int) | Highest number of billable users on the system during the term of the current license. | +| `name` | [`String`](#string) | Name of the licensee. | +| `plan` | [`String!`](#string) | Name of the subscription plan. | +| `startsAt` | [`Date`](#date) | Date when the license started. | +| `type` | [`String!`](#string) | Type of the license. | +| `usersInLicenseCount` | [`Int`](#int) | Number of paid users in the license. | +| `usersOverLicenseCount` | [`Int`](#int) | Number of users over the paid users in the license. | + ### `CustomEmoji` A custom emoji uploaded by user. @@ -3874,6 +3916,42 @@ An edge in a connection. | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`Label`](#label) | The item at the end of the edge. | +### `LicenseHistoryEntry` + +Represents an entry from the Cloud License history. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `activatedAt` | [`Date`](#date) | Date when the license was activated. | +| `company` | [`String`](#string) | Company of the licensee. | +| `email` | [`String`](#string) | Email of the licensee. | +| `expiresAt` | [`Date`](#date) | Date when the license expires. | +| `id` | [`ID!`](#id) | ID of the license. | +| `name` | [`String`](#string) | Name of the licensee. | +| `plan` | [`String!`](#string) | Name of the subscription plan. | +| `startsAt` | [`Date`](#date) | Date when the license started. | +| `type` | [`String!`](#string) | Type of the license. | +| `usersInLicenseCount` | [`Int`](#int) | Number of paid users in the license. | + +### `LicenseHistoryEntryConnection` + +The connection type for LicenseHistoryEntry. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `edges` | [`[LicenseHistoryEntryEdge]`](#licensehistoryentryedge) | A list of edges. | +| `nodes` | [`[LicenseHistoryEntry]`](#licensehistoryentry) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +### `LicenseHistoryEntryEdge` + +An edge in a connection. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`LicenseHistoryEntry`](#licensehistoryentry) | The item at the end of the edge. | + ### `MarkAsSpamSnippetPayload` Autogenerated return type of MarkAsSpamSnippet. @@ -8416,7 +8494,7 @@ Name of the feature that the callout is for. | `NEW_USER_SIGNUPS_CAP_REACHED` | Callout feature name for new_user_signups_cap_reached. | | `PERSONAL_ACCESS_TOKEN_EXPIRY` | Callout feature name for personal_access_token_expiry. | | `REGISTRATION_ENABLED_CALLOUT` | Callout feature name for registration_enabled_callout. | -| `SERVICE_TEMPLATES_DEPRECATED` | Callout feature name for service_templates_deprecated. | +| `SERVICE_TEMPLATES_DEPRECATED_CALLOUT` | Callout feature name for service_templates_deprecated_callout. | | `SUGGEST_PIPELINE` | Callout feature name for suggest_pipeline. | | `SUGGEST_POPOVER_DISMISSED` | Callout feature name for suggest_popover_dismissed. | | `TABS_POSITION_HIGHLIGHT` | Callout feature name for tabs_position_highlight. | diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md index 624c0252f23635cf63d6c7f7b7fbd4ddb5bcd00f..2ec657eec22e0edc5d30672a333039153ef73d02 100644 --- a/doc/user/project/integrations/discord_notifications.md +++ b/doc/user/project/integrations/discord_notifications.md @@ -10,7 +10,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w The Discord Notifications service sends event notifications from GitLab to the channel for which the webhook was created. -To send GitLab event notifications to a Discord channel, create a webhook in Discord and configure it in GitLab. +To send GitLab event notifications to a Discord channel, [create a webhook in Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) +and configure it in GitLab. ## Create webhook diff --git a/ee/app/assets/javascripts/approvals/components/approval_gate_icon.vue b/ee/app/assets/javascripts/approvals/components/approval_gate_icon.vue new file mode 100644 index 0000000000000000000000000000000000000000..cd8c479b60969c5f18c10691db66d7203952ab4f --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/approval_gate_icon.vue @@ -0,0 +1,43 @@ +<script> +import { GlIcon, GlPopover } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; +import { __ } from '~/locale'; + +export default { + components: { + GlIcon, + GlPopover, + }, + props: { + url: { + type: String, + required: true, + }, + }, + computed: { + iconId() { + return uniqueId('approval-icon-'); + }, + containerId() { + return uniqueId('approva-icon-container-'); + }, + }, + i18n: { + title: __('Approval Gate'), + }, +}; +</script> + +<template> + <div :id="containerId"> + <gl-icon :id="iconId" name="api" /> + <gl-popover + :target="iconId" + :container="containerId" + placement="top" + :title="$options.i18n.title" + triggers="hover focus" + :content="url" + /> + </div> +</template> diff --git a/ee/app/assets/javascripts/approvals/components/approver_type_select.vue b/ee/app/assets/javascripts/approvals/components/approver_type_select.vue new file mode 100644 index 0000000000000000000000000000000000000000..41f2155d58eefde43c208eafa8c8da8eec6668a5 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/approver_type_select.vue @@ -0,0 +1,53 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + props: { + approverTypeOptions: { + type: Array, + required: true, + }, + }, + data() { + return { + selected: null, + }; + }, + computed: { + dropdownText() { + return this.selected.text; + }, + }, + created() { + const [firstOption] = this.approverTypeOptions; + this.onSelect(firstOption); + }, + methods: { + isSelectedType(type) { + return this.selected.type === type; + }, + onSelect(option) { + this.selected = option; + this.$emit('input', option.type); + }, + }, +}; +</script> + +<template> + <gl-dropdown class="gl-w-full gl-dropdown-menu-full-width" :text="dropdownText"> + <gl-dropdown-item + v-for="option in approverTypeOptions" + :key="option.type" + :is-check-item="true" + :is-checked="isSelectedType(option.type)" + @click="onSelect(option)" + > + <span>{{ option.text }}</span> + </gl-dropdown-item> + </gl-dropdown> +</template> diff --git a/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue b/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue index d1169d4004642642bc19a6941f5c069c3ea7a068..8e6f66f50864da92d3a0be3cc6074859252c942b 100644 --- a/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue +++ b/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue @@ -3,14 +3,24 @@ import { GlSprintf } from '@gitlab/ui'; import { mapActions, mapState } from 'vuex'; import { n__, s__, __ } from '~/locale'; import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue'; +import { RULE_TYPE_EXTERNAL_APPROVAL } from '../constants'; const i18n = { cancelButtonText: __('Cancel'), - primaryButtonText: __('Remove approvers'), - modalTitle: __('Remove approvers?'), - removeWarningText: s__( - 'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}.', - ), + regularRule: { + primaryButtonText: __('Remove approvers'), + modalTitle: __('Remove approvers?'), + removeWarningText: s__( + 'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}.', + ), + }, + externalRule: { + primaryButtonText: s__('ApprovalRuleRemove|Remove approval gate'), + modalTitle: s__('ApprovalRuleRemove|Remove approval gate?'), + removeWarningText: s__( + 'ApprovalRuleRemove|You are about to remove the %{name} approval gate. Approval from this service is not revoked.', + ), + }, }; export default { @@ -28,6 +38,9 @@ export default { ...mapState('deleteModal', { rule: 'data', }), + isExternalApprovalRule() { + return this.rule?.ruleType === RULE_TYPE_EXTERNAL_APPROVAL; + }, membersText() { return n__( 'ApprovalRuleRemove|%d member', @@ -42,24 +55,38 @@ export default { this.rule.approvers.length, ); }, + modalTitle() { + return this.isExternalApprovalRule + ? i18n.externalRule.modalTitle + : i18n.regularRule.modalTitle; + }, modalText() { - return `${i18n.removeWarningText} ${this.revokeWarningText}`; + return this.isExternalApprovalRule + ? i18n.externalRule.removeWarningText + : `${i18n.regularRule.removeWarningText} ${this.revokeWarningText}`; + }, + primaryButtonProps() { + const text = this.isExternalApprovalRule + ? i18n.externalRule.primaryButtonText + : i18n.regularRule.primaryButtonText; + return { + text, + attributes: [{ variant: 'danger' }], + }; }, }, methods: { - ...mapActions(['deleteRule']), + ...mapActions(['deleteRule', 'deleteExternalApprovalRule']), submit() { - this.deleteRule(this.rule.id); + if (this.rule.externalUrl) { + this.deleteExternalApprovalRule(this.rule.id); + } else { + this.deleteRule(this.rule.id); + } }, }, - buttonActions: { - primary: { - text: i18n.primaryButtonText, - attributes: [{ variant: 'danger' }], - }, - cancel: { - text: i18n.cancelButtonText, - }, + cancelButtonProps: { + text: i18n.cancelButtonText, }, i18n, }; @@ -69,9 +96,9 @@ export default { <gl-modal-vuex modal-module="deleteModal" :modal-id="modalId" - :title="$options.i18n.modalTitle" - :action-primary="$options.buttonActions.primary" - :action-cancel="$options.buttonActions.cancel" + :title="modalTitle" + :action-primary="primaryButtonProps" + :action-cancel="$options.cancelButtonProps" @ok.prevent="submit" > <p v-if="rule"> @@ -82,9 +109,6 @@ export default { <template #nMembers> <strong>{{ membersText }}</strong> </template> - <template #revokeWarning> - {{ revokeWarningText }} - </template> </gl-sprintf> </p> </gl-modal-vuex> diff --git a/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue index 868a537a9ea9a597fa932e86a8ee0e10d0b50edf..bee900015a0dae8a0161be95adc3bcdde217609f 100644 --- a/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue +++ b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue @@ -4,8 +4,13 @@ import RuleName from 'ee/approvals/components/rule_name.vue'; import { n__, sprintf } from '~/locale'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { RULE_TYPE_ANY_APPROVER, RULE_TYPE_REGULAR } from '../../constants'; +import { + RULE_TYPE_EXTERNAL_APPROVAL, + RULE_TYPE_ANY_APPROVER, + RULE_TYPE_REGULAR, +} from '../../constants'; +import ApprovalGateIcon from '../approval_gate_icon.vue'; import EmptyRule from '../empty_rule.vue'; import RuleInput from '../mr_edit/rule_input.vue'; import RuleBranches from '../rule_branches.vue'; @@ -15,6 +20,7 @@ import UnconfiguredSecurityRules from '../security_configuration/unconfigured_se export default { components: { + ApprovalGateIcon, RuleControls, Rules, UserAvatarList, @@ -95,6 +101,9 @@ export default { return canEdit && (!allowMultiRule || !rule.hasSource); }, + isExternalApprovalRule({ ruleType }) { + return ruleType === RULE_TYPE_EXTERNAL_APPROVAL; + }, }, }; </script> @@ -132,13 +141,14 @@ export default { class="js-members" :class="settings.allowMultiRule ? 'd-none d-sm-table-cell' : null" > - <user-avatar-list :items="rule.approvers" :img-size="24" empty-text="" /> + <approval-gate-icon v-if="isExternalApprovalRule(rule)" :url="rule.externalUrl" /> + <user-avatar-list v-else :items="rule.approvers" :img-size="24" empty-text="" /> </td> <td v-if="settings.allowMultiRule" class="js-branches"> <rule-branches :rule="rule" /> </td> <td class="js-approvals-required"> - <rule-input :rule="rule" /> + <rule-input v-if="!isExternalApprovalRule(rule)" :rule="rule" /> </td> <td class="text-nowrap px-2 w-0 js-controls"> <rule-controls v-if="canEdit(rule)" :rule="rule" /> diff --git a/ee/app/assets/javascripts/approvals/components/rule_form.vue b/ee/app/assets/javascripts/approvals/components/rule_form.vue index ca32636c5f63046ede71f66110f4588dc5cc5737..75039865820ecb4fea3602dabf1580a5be1cadfe 100644 --- a/ee/app/assets/javascripts/approvals/components/rule_form.vue +++ b/ee/app/assets/javascripts/approvals/components/rule_form.vue @@ -1,8 +1,17 @@ <script> import { groupBy, isNumber } from 'lodash'; import { mapState, mapActions } from 'vuex'; -import { sprintf, __ } from '~/locale'; -import { TYPE_USER, TYPE_GROUP, TYPE_HIDDEN_GROUPS } from '../constants'; +import { isSafeURL } from '~/lib/utils/url_utility'; +import { sprintf, __, s__ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { + TYPE_USER, + TYPE_GROUP, + TYPE_HIDDEN_GROUPS, + RULE_TYPE_EXTERNAL_APPROVAL, + RULE_TYPE_USER_OR_GROUP_APPROVER, +} from '../constants'; +import ApproverTypeSelect from './approver_type_select.vue'; import ApproversList from './approvers_list.vue'; import ApproversSelect from './approvers_select.vue'; import BranchesSelect from './branches_select.vue'; @@ -21,7 +30,9 @@ export default { ApproversList, ApproversSelect, BranchesSelect, + ApproverTypeSelect, }, + mixins: [glFeatureFlagsMixin()], props: { initRule: { type: Object, @@ -44,6 +55,7 @@ export default { name: this.defaultRuleName, approvalsRequired: 1, minApprovalsRequired: 0, + externalUrl: null, approvers: [], approversToAdd: [], branches: [], @@ -52,6 +64,7 @@ export default { isFallback: false, containsHiddenGroups: false, serverValidationErrors: [], + ruleType: null, ...this.getInitialData(), }; @@ -59,6 +72,17 @@ export default { }, computed: { ...mapState(['settings']), + showApproverTypeSelect() { + return ( + this.glFeatures.ffComplianceApprovalGates && + !this.isEditing && + !this.isMrEdit && + !READONLY_NAMES.includes(this.name) + ); + }, + isExternalApprovalRule() { + return this.ruleType === RULE_TYPE_EXTERNAL_APPROVAL; + }, rule() { // If we are creating a new rule with a suggested approval name return this.defaultRuleName ? null : this.initRule; @@ -85,16 +109,32 @@ export default { const invalidObject = { name: this.invalidName, - approvalsRequired: this.invalidApprovalsRequired, - approvers: this.invalidApprovers, }; if (!this.isMrEdit) { invalidObject.branches = this.invalidBranches; } + if (this.isExternalApprovalRule) { + invalidObject.externalUrl = this.invalidApprovalGateUrl; + } else { + invalidObject.approvers = this.invalidApprovers; + invalidObject.approvalsRequired = this.invalidApprovalsRequired; + } + return invalidObject; }, + invalidApprovalGateUrl() { + let error = ''; + + if (this.serverValidationErrors.includes('External url has already been taken')) { + error = __('External url has already been taken'); + } else if (!this.externalUrl || !isSafeURL(this.externalUrl)) { + error = __('Please provide a valid URL'); + } + + return error; + }, invalidName() { let error = ''; @@ -175,9 +215,24 @@ export default { protectedBranchIds: this.branches, }; }, + isEditing() { + return Boolean(this.initRule); + }, + externalRuleSubmissionData() { + const { id, name, protectedBranchIds } = this.submissionData; + return { + id, + name, + protectedBranchIds, + externalUrl: this.externalUrl, + }; + }, showProtectedBranch() { return !this.isMrEdit && this.settings.allowMultiRule; }, + approvalGateLabel() { + return this.isEditing ? this.$options.i18n.approvalGate : this.$options.i18n.addApprovalGate; + }, }, watch: { approversToAdd(value) { @@ -188,7 +243,15 @@ export default { }, }, methods: { - ...mapActions(['putFallbackRule', 'postRule', 'putRule', 'deleteRule', 'postRegularRule']), + ...mapActions([ + 'putFallbackRule', + 'putExternalApprovalRule', + 'postExternalApprovalRule', + 'postRule', + 'putRule', + 'deleteRule', + 'postRegularRule', + ]), addSelection() { if (!this.approversToAdd.length) { return; @@ -219,9 +282,13 @@ export default { } submission.catch((failureResponse) => { - this.serverValidationErrors = mapServerResponseToValidationErrors( - failureResponse?.response?.data?.message || {}, - ); + if (this.isExternalApprovalRule) { + this.serverValidationErrors = failureResponse?.response?.data?.message || []; + } else { + this.serverValidationErrors = mapServerResponseToValidationErrors( + failureResponse?.response?.data?.message || {}, + ); + } }); return submission; @@ -230,12 +297,14 @@ export default { * Submit the rule, by either put-ing or post-ing. */ submitRule() { + if (this.isExternalApprovalRule) { + const data = this.externalRuleSubmissionData; + return data.id ? this.putExternalApprovalRule(data) : this.postExternalApprovalRule(data); + } const data = this.submissionData; - if (!this.settings.allowMultiRule && this.settings.prefix === 'mr-edit') { return data.id ? this.putRule(data) : this.postRegularRule(data); } - return data.id ? this.putRule(data) : this.postRule(data); }, /** @@ -248,7 +317,7 @@ export default { * Submit as a single rule. This is determined by the settings. */ submitSingleRule() { - if (!this.approvers.length) { + if (!this.approvers.length && !this.isExternalApprovalRule) { return this.submitEmptySingleRule(); } @@ -280,6 +349,16 @@ export default { }; } + if (this.initRule.ruleType === RULE_TYPE_EXTERNAL_APPROVAL) { + return { + name: this.initRule.name || '', + externalUrl: this.initRule.externalUrl, + branches: this.initRule.protectedBranches?.map((x) => x.id) || [], + ruleType: this.initRule.ruleType, + approvers: [], + }; + } + const { containsHiddenGroups = false, removeHiddenGroups = false } = this.initRule; const users = this.initRule.users.map((x) => ({ ...x, type: TYPE_USER })); @@ -290,6 +369,7 @@ export default { name: this.initRule.name || '', approvalsRequired: this.initRule.approvalsRequired || 0, minApprovalsRequired: this.initRule.minApprovalsRequired || 0, + ruleType: this.initRule.ruleType, containsHiddenGroups, approvers: groups .concat(users) @@ -300,6 +380,14 @@ export default { }; }, }, + i18n: { + approvalGate: s__('ApprovalRule|Approvel gate'), + addApprovalGate: s__('ApprovalRule|Add approvel gate'), + }, + approverTypeOptions: [ + { type: RULE_TYPE_USER_OR_GROUP_APPROVER, text: s__('ApprovalRule|Users or groups') }, + { type: RULE_TYPE_EXTERNAL_APPROVAL, text: s__('ApprovalRule|Approval service API') }, + ], }; </script> @@ -334,7 +422,14 @@ export default { {{ __('Apply this approval rule to any branch or a specific protected branch.') }} </small> </div> - <div class="form-group gl-form-group"> + <div v-if="showApproverTypeSelect" class="form-group gl-form-group"> + <label class="col-form-label">{{ s__('ApprovalRule|Approver Type') }}</label> + <approver-type-select + v-model="ruleType" + :approver-type-options="$options.approverTypeOptions" + /> + </div> + <div v-if="!isExternalApprovalRule" class="form-group gl-form-group"> <label class="col-form-label">{{ s__('ApprovalRule|Approvals required') }}</label> <input v-model.number="approvalsRequired" @@ -347,7 +442,7 @@ export default { /> <span class="invalid-feedback">{{ validation.approvalsRequired }}</span> </div> - <div class="form-group gl-form-group"> + <div v-if="!isExternalApprovalRule" class="form-group gl-form-group"> <label class="col-form-label">{{ s__('ApprovalRule|Add approvers') }}</label> <approvers-select v-model="approversToAdd" @@ -359,7 +454,22 @@ export default { /> <span class="invalid-feedback">{{ validation.approvers }}</span> </div> - <div class="bordered-box overflow-auto h-12em"> + <div v-if="isExternalApprovalRule" class="form-group gl-form-group"> + <label class="col-form-label">{{ approvalGateLabel }}</label> + <input + v-model="externalUrl" + :class="{ 'is-invalid': validation.externalUrl }" + class="gl-form-input form-control" + name="approval_gate_url" + type="url" + data-qa-selector="external_url_field" + /> + <span class="invalid-feedback">{{ validation.externalUrl }}</span> + <small class="form-text text-gl-muted"> + {{ s__('ApprovalRule|Invoke an external API as part of the approvals') }} + </small> + </div> + <div v-if="!isExternalApprovalRule" class="bordered-box overflow-auto h-12em"> <approvers-list v-model="approvers" /> </div> </form> diff --git a/ee/app/assets/javascripts/approvals/constants.js b/ee/app/assets/javascripts/approvals/constants.js index da1013904d8602d69f6509cf2017346263c843b9..e09704b68a2a62b61adecffefe828600cfbceb39 100644 --- a/ee/app/assets/javascripts/approvals/constants.js +++ b/ee/app/assets/javascripts/approvals/constants.js @@ -17,6 +17,7 @@ export const RULE_TYPE_CODE_OWNER = 'code_owner'; export const RULE_TYPE_ANY_APPROVER = 'any_approver'; export const RULE_TYPE_EXTERNAL_APPROVAL = 'external_approval'; export const RULE_NAME_ANY_APPROVER = 'All Members'; +export const RULE_TYPE_USER_OR_GROUP_APPROVER = 'user_or_group'; export const VULNERABILITY_CHECK_NAME = 'Vulnerability-Check'; export const LICENSE_CHECK_NAME = 'License-Check'; diff --git a/ee/app/assets/javascripts/approvals/mappers.js b/ee/app/assets/javascripts/approvals/mappers.js index 5390d1d1dd576899b17a090cddd5c7e9e1a9f90c..7006a21b93687540c70ee95f98fe736f12361bc7 100644 --- a/ee/app/assets/javascripts/approvals/mappers.js +++ b/ee/app/assets/javascripts/approvals/mappers.js @@ -70,7 +70,7 @@ export const mapExternalApprovalRuleResponse = (res) => ({ }); export const mapExternalApprovalResponse = (res) => ({ - rules: withDefaultEmptyRule(res.map(mapExternalApprovalRuleResponse)), + rules: res.map(mapExternalApprovalRuleResponse), }); export const mapApprovalSettingsResponse = (res) => ({ diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue index ad3307adb61805dbc759cc84345ba8b2ecce9d10..6f0a7a36db3e49a85f93c62f5375f4a9e3f1b1cc 100644 --- a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue @@ -200,7 +200,7 @@ export default { @filter="handleFilterIssues" > <template #nav-actions> - <gl-button :href="issueCreateUrl" target="_blank" + <gl-button :href="issueCreateUrl" target="_blank" class="gl-my-5" >{{ s__('Integrations|Create new issue in Jira') }}<gl-icon name="external-link" /></gl-button> </template> diff --git a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_row.vue b/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_row.vue deleted file mode 100644 index 91f3af8298ae19f77634d25d60ebe448b49aa9ec..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_row.vue +++ /dev/null @@ -1,20 +0,0 @@ -<script> -export default { - props: { - label: { - type: String, - required: true, - }, - }, -}; -</script> -<template> - <div - class="generic-report-row gl-display-grid gl-px-3 gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-gray-100" - > - <strong>{{ label }}</strong> - <div data-testid="reportContent"> - <slot></slot> - </div> - </div> -</template> diff --git a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue b/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue index dfcf3874cb8c133a5b67a12e232f404bc41af9d6..ef54f04a1b30125ef4c4e8b8de89947debeb2d68 100644 --- a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue +++ b/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue @@ -2,7 +2,6 @@ import { GlCollapse, GlIcon } from '@gitlab/ui'; import { s__ } from '~/locale'; import ReportItem from './report_item.vue'; -import ReportRow from './report_row.vue'; import { filterTypesAndLimitListDepth } from './types/utils'; const NESTED_LISTS_MAX_DEPTH = 4; @@ -15,7 +14,6 @@ export default { GlCollapse, GlIcon, ReportItem, - ReportRow, }, props: { details: { @@ -57,11 +55,14 @@ export default { </h3> </header> <gl-collapse :visible="showSection"> - <div data-testid="reports"> + <div class="generic-report-container" data-testid="reports"> <template v-for="[label, item] in detailsEntries"> - <report-row :key="label" :label="item.name" :data-testid="`report-row-${label}`"> - <report-item :item="item" /> - </report-row> + <div :key="label" class="generic-report-row" :data-testid="`report-row-${label}`"> + <strong class="generic-report-column">{{ item.name }}</strong> + <div class="generic-report-column" data-testid="reportContent"> + <report-item :item="item" :data-testid="`report-item-${label}`" /> + </div> + </div> </template> </div> </gl-collapse> diff --git a/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss b/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss index ab4e9e33e03626c039de5ad7b2aca9c6c3b3340b..1b7ffa12db0dd4685bc6505a37eea6a3c1f86af2 100644 --- a/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss +++ b/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss @@ -120,15 +120,32 @@ $selection-summary-with-error-height: 118px; } } +.generic-report-container { + @include gl-display-grid; + + grid-template-columns: max-content auto; +} + .generic-report-row { - grid-template-columns: minmax(150px, 1fr) 3fr; - grid-column-gap: $gl-spacing-scale-5; + display: contents; - &:last-child { + &:last-child .generic-report-column { @include gl-border-b-0; } } +.generic-report-column { + @include gl-px-3; + @include gl-py-5; + @include gl-border-b-1; + @include gl-border-b-solid; + @include gl-border-gray-100; + + &:first-child { + max-width: 15rem; + } +} + .generic-report-list { li { @include gl-ml-0; @@ -140,4 +157,3 @@ $selection-summary-with-error-height: 118px; list-style-type: disc; } } - diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb index 8332113817dcbf3b35836bd10de1f739740489fb..419e2401dc75610684f93cebfaaf83e25c1d03c9 100644 --- a/ee/app/controllers/ee/projects_controller.rb +++ b/ee/app/controllers/ee/projects_controller.rb @@ -10,6 +10,10 @@ module ProjectsController before_action :log_archive_audit_event, only: [:archive] before_action :log_unarchive_audit_event, only: [:unarchive] + before_action only: :edit do + push_frontend_feature_flag(:ff_compliance_approval_gates, project, default_enabled: :yaml) + end + before_action only: :show do push_frontend_feature_flag(:cve_id_request_button, project) end diff --git a/ee/app/graphql/ee/types/query_type.rb b/ee/app/graphql/ee/types/query_type.rb index 8cdc63c61fb433faef18fb61b4e997d9e9dee066..20db0eaf00356e649efd98f57b5d5464f6415ff6 100644 --- a/ee/app/graphql/ee/types/query_type.rb +++ b/ee/app/graphql/ee/types/query_type.rb @@ -61,6 +61,16 @@ module QueryType null: true, description: 'Get configured DevOps adoption segments on the instance.', resolver: ::Resolvers::Admin::Analytics::DevopsAdoption::SegmentsResolver + + field :current_license, ::Types::Admin::CloudLicenses::CurrentLicenseType, + null: true, + resolver: ::Resolvers::Admin::CloudLicenses::CurrentLicenseResolver, + description: 'Fields related to the current license.' + + field :license_history_entries, ::Types::Admin::CloudLicenses::LicenseHistoryEntryType.connection_type, + null: true, + resolver: ::Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver, + description: 'Fields related to entries in the license history.' end def vulnerability(id:) diff --git a/ee/app/graphql/resolvers/admin/cloud_licenses/current_license_resolver.rb b/ee/app/graphql/resolvers/admin/cloud_licenses/current_license_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..03ca1c8eb63ae7e36b2f6135e7ccde302b716ca2 --- /dev/null +++ b/ee/app/graphql/resolvers/admin/cloud_licenses/current_license_resolver.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Resolvers + module Admin + module CloudLicenses + class CurrentLicenseResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include ::Admin::LicenseRequest + + type ::Types::Admin::CloudLicenses::CurrentLicenseType, null: true + + def resolve + return unless application_settings.cloud_license_enabled? + + authorize! + + license + end + + private + + def application_settings + Gitlab::CurrentSettings.current_application_settings + end + + def authorize! + Ability.allowed?(context[:current_user], :read_licenses) || raise_resource_not_available_error! + end + end + end + end +end diff --git a/ee/app/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver.rb b/ee/app/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d93823f633df6e77e517c256762b5bf23e3de7a --- /dev/null +++ b/ee/app/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Resolvers + module Admin + module CloudLicenses + class LicenseHistoryEntriesResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type [::Types::Admin::CloudLicenses::LicenseHistoryEntryType], null: true + + def resolve + return unless application_settings.cloud_license_enabled? + + authorize! + + License.history + end + + private + + def application_settings + Gitlab::CurrentSettings.current_application_settings + end + + def authorize! + Ability.allowed?(context[:current_user], :read_licenses) || raise_resource_not_available_error! + end + end + end + end +end diff --git a/ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb b/ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..825c681648b7e6cc61c34fd441edb34b4ae577d7 --- /dev/null +++ b/ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Types + module Admin + module CloudLicenses + # rubocop: disable Graphql/AuthorizeTypes + class CurrentLicenseType < BaseObject + include ::Types::Admin::CloudLicenses::LicenseType + + graphql_name 'CurrentLicense' + description 'Represents the current license' + + field :last_sync, ::Types::TimeType, null: true, + description: 'Date when the license was last synced.', + method: :last_synced_at + + field :billable_users_count, GraphQL::INT_TYPE, null: true, + description: 'Number of billable users on the system.', + method: :daily_billable_users_count + + field :maximum_user_count, GraphQL::INT_TYPE, null: true, + description: 'Highest number of billable users on the system during the term of the current license.', + method: :maximum_user_count + + field :users_over_license_count, GraphQL::INT_TYPE, null: true, + description: 'Number of users over the paid users in the license.' + + def users_over_license_count + return 0 if object.trial? + + [object.overage_with_historical_max, 0].max + end + end + end + end +end diff --git a/ee/app/graphql/types/admin/cloud_licenses/license_history_entry_type.rb b/ee/app/graphql/types/admin/cloud_licenses/license_history_entry_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..85e50de344ab311d348856288df9050b6a6c65af --- /dev/null +++ b/ee/app/graphql/types/admin/cloud_licenses/license_history_entry_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Admin + module CloudLicenses + # rubocop: disable Graphql/AuthorizeTypes + class LicenseHistoryEntryType < BaseObject + include ::Types::Admin::CloudLicenses::LicenseType + + graphql_name 'LicenseHistoryEntry' + description 'Represents an entry from the Cloud License history' + end + end + end +end diff --git a/ee/app/graphql/types/admin/cloud_licenses/license_type.rb b/ee/app/graphql/types/admin/cloud_licenses/license_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..9fd51b4723d2e4aa5fafacc391327a510f1521ce --- /dev/null +++ b/ee/app/graphql/types/admin/cloud_licenses/license_type.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Types + module Admin + module CloudLicenses + module LicenseType + extend ActiveSupport::Concern + + included do + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the license.', + method: :license_id + + field :type, GraphQL::STRING_TYPE, null: false, + description: 'Type of the license.', + method: :license_type + + field :plan, GraphQL::STRING_TYPE, null: false, + description: 'Name of the subscription plan.' + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the licensee.', + method: :licensee_name + + field :email, GraphQL::STRING_TYPE, null: true, + description: 'Email of the licensee.', + method: :licensee_email + + field :company, GraphQL::STRING_TYPE, null: true, + description: 'Company of the licensee.', + method: :licensee_company + + field :starts_at, ::Types::DateType, null: true, + description: 'Date when the license started.' + + field :expires_at, ::Types::DateType, null: true, + description: 'Date when the license expires.' + + field :activated_at, ::Types::DateType, null: true, + description: 'Date when the license was activated.', + method: :created_at + + field :users_in_license_count, GraphQL::INT_TYPE, null: true, + description: 'Number of paid users in the license.', + method: :restricted_user_count + end + end + end + end +end diff --git a/ee/app/models/license.rb b/ee/app/models/license.rb index de94004a7a7e2c038e16b684295800aec48f3aae..e4e1d6de2dd778bc2179c083f35aaffe9b5e5f81 100644 --- a/ee/app/models/license.rb +++ b/ee/app/models/license.rb @@ -8,6 +8,7 @@ class License < ApplicationRecord PREMIUM_PLAN = 'premium' ULTIMATE_PLAN = 'ultimate' CLOUD_LICENSE_TYPE = 'cloud' + LEGACY_LICENSE_TYPE = 'legacy' ALLOWED_PERCENTAGE_OF_USERS_OVERAGE = (10 / 100.0) EE_ALL_PLANS = [STARTER_PLAN, PREMIUM_PLAN, ULTIMATE_PLAN].freeze @@ -237,6 +238,8 @@ class License < ApplicationRecord { range: (1000..nil), percentage: true, value: 5 } ].freeze + LICENSEE_ATTRIBUTES = %w[Name Email Company].freeze + validate :valid_license validate :check_users_limit, if: :new_record?, unless: :validate_with_trueup? validate :check_trueup, unless: :persisted?, if: :validate_with_trueup? @@ -550,6 +553,10 @@ def cloud? license&.type == CLOUD_LICENSE_TYPE end + def license_type + cloud? ? CLOUD_LICENSE_TYPE : LEGACY_LICENSE_TYPE + end + def auto_renew false end @@ -576,6 +583,12 @@ def remaining_user_count restricted_user_count - daily_billable_users_count end + LICENSEE_ATTRIBUTES.each do |attribute| + define_method "licensee_#{attribute.downcase}" do + licensee[attribute] + end + end + private def restricted_attr(name, default = nil) diff --git a/ee/app/serializers/integrations/jira/issue_entity.rb b/ee/app/serializers/integrations/jira/issue_entity.rb index c59137fe16144886dbc4e416498fbfff9f3ea81f..90f449dff2d5021587ac491c5b93ba9d3bd387a9 100644 --- a/ee/app/serializers/integrations/jira/issue_entity.rb +++ b/ee/app/serializers/integrations/jira/issue_entity.rb @@ -34,8 +34,8 @@ class IssueEntity < Grape::Entity { title: name, name: name, - color: '#EBECF0', - text_color: '#283856' + color: '#0052CC', + text_color: '#FFFFFF' } end end diff --git a/ee/app/services/ee/merge_requests/update_service.rb b/ee/app/services/ee/merge_requests/update_service.rb index e8f8d3af1964be7027c3ce112b842d9b07d31021..636b7b624eb02bc926d15e198ffe3a58936a7e58 100644 --- a/ee/app/services/ee/merge_requests/update_service.rb +++ b/ee/app/services/ee/merge_requests/update_service.rb @@ -39,7 +39,9 @@ def execute(merge_request) def after_update(merge_request) super - ::MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute + merge_request.run_after_commit do + ::MergeRequests::SyncCodeOwnerApprovalRulesWorker.perform_async(merge_request) + end end override :create_branch_change_note diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index dbe7694f337931de5cab6e4fffe97f0212b6d2a4..7d08829262cc16e73d3a4956f9f9f0ca3fbc1fb2 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -867,6 +867,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: merge_requests_sync_code_owner_approval_rules + :feature_category: :source_code_management + :has_external_dependencies: + :urgency: :high + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: new_epic :feature_category: :epics :has_external_dependencies: diff --git a/ee/app/workers/merge_requests/sync_code_owner_approval_rules_worker.rb b/ee/app/workers/merge_requests/sync_code_owner_approval_rules_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..2ea4ae21ad1165513b95819bafa7bedbdc706366 --- /dev/null +++ b/ee/app/workers/merge_requests/sync_code_owner_approval_rules_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module MergeRequests + class SyncCodeOwnerApprovalRulesWorker + include ApplicationWorker + + feature_category :source_code_management + urgency :high + deduplicate :until_executed + idempotent! + + def perform(merge_request_id) + merge_request = MergeRequest.find_by_id(merge_request_id) + return unless merge_request + + ::MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute + end + end +end diff --git a/ee/changelogs/unreleased/326225-add-load-balancing-metrics-on-request-level.yml b/ee/changelogs/unreleased/326225-add-load-balancing-metrics-on-request-level.yml new file mode 100644 index 0000000000000000000000000000000000000000..6324a789f6bea817d360c205bc53377a55703087 --- /dev/null +++ b/ee/changelogs/unreleased/326225-add-load-balancing-metrics-on-request-level.yml @@ -0,0 +1,5 @@ +--- +title: Add metric to track querying write ahead log on Request level +merge_request: 58673 +author: +type: other diff --git a/ee/changelogs/unreleased/326225-add-sidekiq-lb-server-metrics.yml b/ee/changelogs/unreleased/326225-add-sidekiq-lb-server-metrics.yml new file mode 100644 index 0000000000000000000000000000000000000000..5721c60df5b13dc077bc4508ccdc87622502b2c6 --- /dev/null +++ b/ee/changelogs/unreleased/326225-add-sidekiq-lb-server-metrics.yml @@ -0,0 +1,5 @@ +--- +title: Add load balancing Sidekiq metrics +merge_request: 58473 +author: +type: other diff --git a/ee/changelogs/unreleased/327553-jira-issue-label-colors-should-have-higher-contrast.yml b/ee/changelogs/unreleased/327553-jira-issue-label-colors-should-have-higher-contrast.yml new file mode 100644 index 0000000000000000000000000000000000000000..47313fcfeaaef4b3ca46bb2b656e7b72f1fca29e --- /dev/null +++ b/ee/changelogs/unreleased/327553-jira-issue-label-colors-should-have-higher-contrast.yml @@ -0,0 +1,5 @@ +--- +title: Update label color on Jira issues pages +merge_request: 59226 +author: +type: changed diff --git a/ee/changelogs/unreleased/dpisek-generic-reports-grid-cleanup.yml b/ee/changelogs/unreleased/dpisek-generic-reports-grid-cleanup.yml new file mode 100644 index 0000000000000000000000000000000000000000..814c535b49afb0fcc459eb720e40b105257837e4 --- /dev/null +++ b/ee/changelogs/unreleased/dpisek-generic-reports-grid-cleanup.yml @@ -0,0 +1,5 @@ +--- +title: 'Generic vulnerability reports: Remove extra margin between report columns' +merge_request: 58720 +author: +type: other diff --git a/ee/changelogs/unreleased/rspec-empty-lines-after-letitbe-ee-spec-controllers-registractions.yml b/ee/changelogs/unreleased/rspec-empty-lines-after-letitbe-ee-spec-controllers-registractions.yml new file mode 100644 index 0000000000000000000000000000000000000000..a0a0485dc6cd523e65d0a432fa9c98a170fd8f2e --- /dev/null +++ b/ee/changelogs/unreleased/rspec-empty-lines-after-letitbe-ee-spec-controllers-registractions.yml @@ -0,0 +1,5 @@ +--- +title: Fix RSpec/EmptyLineAfterFinalLetItBe rubocop offenses in ee/spec/controllers/registrations +merge_request: 58408 +author: Abdul Wadood @abdulwd +type: fixed diff --git a/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb b/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb index c856221c48a744cc992f4bb34fd1ebde66c081dd..529323ab30c5367e720dff86f40c4937fd881016 100644 --- a/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb +++ b/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb @@ -9,11 +9,13 @@ module ActiveRecord extend ::Gitlab::Utils::Override DB_LOAD_BALANCING_COUNTERS = %i{ - db_replica_count db_replica_cached_count - db_primary_count db_primary_cached_count + db_replica_count db_replica_cached_count db_replica_wal_count + db_primary_count db_primary_cached_count db_primary_wal_count }.freeze DB_LOAD_BALANCING_DURATIONS = %i{db_primary_duration_s db_replica_duration_s}.freeze + SQL_WAL_LOCATION_REGEX = /(pg_current_wal_insert_lsn\(\)::text|pg_last_wal_replay_lsn\(\)::text)/.freeze + class_methods do extend ::Gitlab::Utils::Override @@ -55,9 +57,14 @@ def sql(event) private + def wal_command?(payload) + payload[:sql].match(SQL_WAL_LOCATION_REGEX) + end + def increment_db_role_counters(db_role, payload) increment("db_#{db_role}_count".to_sym) increment("db_#{db_role}_cached_count".to_sym) if cached_query?(payload) + increment("db_#{db_role}_wal_count".to_sym) if !cached_query?(payload) && wal_command?(payload) end def observe_db_role_duration(db_role, event) diff --git a/ee/lib/ee/gitlab/sidekiq_middleware/server_metrics.rb b/ee/lib/ee/gitlab/sidekiq_middleware/server_metrics.rb new file mode 100644 index 0000000000000000000000000000000000000000..03d5f2b6c64a94f338d18a16d60eeb7497f40884 --- /dev/null +++ b/ee/lib/ee/gitlab/sidekiq_middleware/server_metrics.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +module EE + module Gitlab + module SidekiqMiddleware + module ServerMetrics + extend ::Gitlab::Utils::Override + + protected + + override :init_metrics + def init_metrics + super.merge(init_load_balancing_metrics) + end + + override :instrument + def instrument(job, labels) + super + ensure + record_load_balancing(job, labels) + end + + private + + def init_load_balancing_metrics + return {} unless ::Gitlab::Database::LoadBalancing.enable? + + { + sidekiq_load_balancing_count: ::Gitlab::Metrics.counter(:sidekiq_load_balancing_count, 'Sidekiq jobs with load balancing') + } + end + + def record_load_balancing(job, labels) + return unless ::Gitlab::Database::LoadBalancing.enable? + return unless job[:database_chosen] + + load_balancing_labels = { + database_chosen: job[:database_chosen], + data_consistency: job[:data_consistency] + } + + metrics[:sidekiq_load_balancing_count].increment(labels.merge(load_balancing_labels), 1) + end + end + end + end +end diff --git a/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb b/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb index 3044585285f6a4bfef757c94e83ec1c21ad536d2..7136175a5bc817079a7c59f74314b0c82b6af666 100644 --- a/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb +++ b/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb @@ -19,6 +19,9 @@ def mark_data_consistency_location(worker_class, job) return unless worker_class return unless worker_class.include?(::ApplicationWorker) return unless worker_class.get_data_consistency_feature_flag_enabled? + + job['worker_data_consistency'] = worker_class.get_data_consistency + return if worker_class.get_data_consistency == :always if Session.current.performed_write? diff --git a/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb b/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb index 125782927480185c65fb3b0a497aadf08bca9ce2..37cb4e35072ea5c06bcd96dd2839feaf98435992 100644 --- a/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb +++ b/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb @@ -25,18 +25,18 @@ def clear def requires_primary?(worker_class, job) return true unless worker_class.include?(::ApplicationWorker) - - job[:worker_data_consistency] = worker_class.get_data_consistency - return true if worker_class.get_data_consistency == :always return true unless worker_class.get_data_consistency_feature_flag_enabled? - if job['database_replica_location'] || replica_caught_up?(job['database_write_location'] ) + if job['database_replica_location'] || replica_caught_up?(job['database_write_location']) + job[:database_chosen] = 'replica' false elsif worker_class.get_data_consistency == :delayed && job['retry_count'].to_i == 0 + job[:database_chosen] = 'retry' raise JobReplicaNotUpToDate, "Sidekiq job #{worker_class} JID-#{job['jid']} couldn't use the replica."\ " Replica was not up to date." else + job[:database_chosen] = 'primary' true end end diff --git a/ee/spec/controllers/registrations/groups_controller_spec.rb b/ee/spec/controllers/registrations/groups_controller_spec.rb index 9be431afb180a81b93077a8b538d6a43ddf6abfd..512ef65cc89b316ad17859870a1c755c32d2c64c 100644 --- a/ee/spec/controllers/registrations/groups_controller_spec.rb +++ b/ee/spec/controllers/registrations/groups_controller_spec.rb @@ -78,6 +78,7 @@ let_it_be(:trial_form_params) { { trial: 'false' } } let_it_be(:trial_onboarding_issues_enabled) { false } let_it_be(:trial_onboarding_flow_params) { {} } + let(:signup_onboarding_enabled) { true } let(:group_params) { { name: 'Group name', path: 'group-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE, emails: ['', ''] } } let(:params) do diff --git a/ee/spec/controllers/registrations/projects_controller_spec.rb b/ee/spec/controllers/registrations/projects_controller_spec.rb index 229815df354d5bf3ba53da01a74a5db6ea0258dd..e2e744f96707eeeb89b8aa30ee628db7c0045237 100644 --- a/ee/spec/controllers/registrations/projects_controller_spec.rb +++ b/ee/spec/controllers/registrations/projects_controller_spec.rb @@ -54,6 +54,7 @@ subject { post :create, params: { project: params }.merge(trial_onboarding_flow_params) } let_it_be(:trial_onboarding_flow_params) { {} } + let(:params) { { namespace_id: namespace.id, name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } } let(:signup_onboarding_enabled) { true } diff --git a/ee/spec/features/projects/settings/merge_requests_settings_spec.rb b/ee/spec/features/projects/settings/merge_requests_settings_spec.rb index 4efb445239f831fb1414bd9ded3753270f38828d..19d7641f990f704053ce32953dfcf47758111fec 100644 --- a/ee/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/ee/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -5,15 +5,16 @@ include GitlabRoutingHelper include FeatureApprovalHelper - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:group) { create(:group) } - let(:group_member) { create(:user) } - let(:non_member) { create(:user) } - let!(:config_selector) { '.js-approval-rules' } - let!(:modal_selector) { '#project-settings-approvals-create-modal' } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:group_member) { create(:user) } + let_it_be(:non_member) { create(:user) } + let_it_be(:config_selector) { '.js-approval-rules' } + let_it_be(:modal_selector) { '#project-settings-approvals-create-modal' } before do + stub_licensed_features(compliance_approval_gates: true) sign_in(user) project.add_maintainer(user) group.add_developer(user) @@ -69,8 +70,8 @@ end context 'with an approver group' do - let(:non_group_approver) { create(:user) } - let!(:rule) { create(:approval_project_rule, project: project, groups: [group], users: [non_group_approver]) } + let_it_be(:non_group_approver) { create(:user) } + let_it_be(:rule) { create(:approval_project_rule, project: project, groups: [group], users: [non_group_approver]) } before do project.add_developer(non_group_approver) @@ -90,6 +91,64 @@ end end + it 'adds an approval gate' do + visit edit_project_path(project) + + open_modal(text: 'Add approval rule', expand: false) + + within('.modal-content') do + find('button', text: "Users or groups").click + find('button', text: "Approval service API").click + + find('[data-qa-selector="rule_name_field"]').set('My new rule') + find('[data-qa-selector="external_url_field"]').set('https://api.gitlab.com') + + click_button 'Add approval rule' + end + + wait_for_requests + + expect(first('.js-name')).to have_content('My new rule') + end + + context 'with an approval gate' do + let_it_be(:rule) { create(:external_approval_rule, project: project) } + + it 'updates the approval gate' do + visit edit_project_path(project) + + expect(first('.js-name')).to have_content(rule.name) + + open_modal(text: 'Edit', expand: false) + + within('.modal-content') do + find('[data-qa-selector="rule_name_field"]').set('Something new') + + click_button 'Update approval rule' + end + + wait_for_requests + + expect(first('.js-name')).to have_content('Something new') + end + + it 'removes the approval gate' do + visit edit_project_path(project) + + expect(first('.js-name')).to have_content(rule.name) + + first('.js-controls').find('[data-testid="remove-icon"]').click + + within('.modal-content') do + click_button 'Remove approval gate' + end + + wait_for_requests + + expect(first('.js-name')).not_to have_content(rule.name) + end + end + context 'issuable default templates feature not available' do before do stub_licensed_features(issuable_default_templates: false) diff --git a/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap b/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap index 134907f59d039ca87b29b2d65326862295eee857..33c2b39440395c6806d93219b27ce07ad149815a 100644 --- a/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap +++ b/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap @@ -1,6 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Approvals ModalRuleRemove shows message 1`] = ` +exports[`Approvals ModalRuleRemove matches the snapshot for external approval 1`] = ` +<div + title="Remove approval gate?" +> + <p> + You are about to remove the + <strong> + API Gate + </strong> + approval gate. Approval from this service is not revoked. + </p> +</div> +`; + +exports[`Approvals ModalRuleRemove matches the snapshot for multiple approvers 1`] = ` <div title="Remove approvers?" > @@ -18,7 +32,7 @@ exports[`Approvals ModalRuleRemove shows message 1`] = ` </div> `; -exports[`Approvals ModalRuleRemove shows singular message 1`] = ` +exports[`Approvals ModalRuleRemove matches the snapshot for singular approver 1`] = ` <div title="Remove approvers?" > diff --git a/ee/spec/frontend/approvals/components/approval_gate_icon_spec.js b/ee/spec/frontend/approvals/components/approval_gate_icon_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..69c2db2c63007c785ee1fd781c850ab0f431c775 --- /dev/null +++ b/ee/spec/frontend/approvals/components/approval_gate_icon_spec.js @@ -0,0 +1,42 @@ +import { GlPopover, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import ApprovalGateIcon from 'ee/approvals/components/approval_gate_icon.vue'; + +jest.mock('lodash/uniqueId', () => (id) => `${id}mock`); + +describe('ApprovalGateIcon', () => { + let wrapper; + + const findPopover = () => wrapper.findComponent(GlPopover); + const findIcon = () => wrapper.findComponent(GlIcon); + + const createComponent = () => { + return shallowMount(ApprovalGateIcon, { + propsData: { + url: 'https://gitlab.com/', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + wrapper = createComponent(); + }); + + it('renders the icon', () => { + expect(findIcon().props('name')).toBe('api'); + expect(findIcon().attributes('id')).toBe('approval-icon-mock'); + }); + + it('renders the popover with the URL for the icon', () => { + expect(findPopover().exists()).toBe(true); + expect(findPopover().attributes()).toMatchObject({ + content: 'https://gitlab.com/', + title: 'Approval Gate', + target: 'approval-icon-mock', + }); + }); +}); diff --git a/ee/spec/frontend/approvals/components/approver_type_select_spec.js b/ee/spec/frontend/approvals/components/approver_type_select_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..1583cbe35c542bc39bcc98fe8fc8bb7beb9d3315 --- /dev/null +++ b/ee/spec/frontend/approvals/components/approver_type_select_spec.js @@ -0,0 +1,60 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import ApprovalTypeSelect from 'ee/approvals/components/approver_type_select.vue'; + +jest.mock('lodash/uniqueId', () => (id) => `${id}mock`); + +const OPTIONS = [ + { type: 'x', text: 'foo' }, + { type: 'y', text: 'bar' }, +]; + +describe('ApprovalTypeSelect', () => { + let wrapper; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + + const createComponent = () => { + return shallowMount(ApprovalTypeSelect, { + propsData: { + approverTypeOptions: OPTIONS, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + wrapper = createComponent(); + }); + + it('should select the first option by default', () => { + expect(findDropdownItems().at(0).props('isChecked')).toBe(true); + }); + + it('renders the dropdown with the selected text', () => { + expect(findDropdown().props('text')).toBe(OPTIONS[0].text); + }); + + it('renders a dropdown item for each option', () => { + OPTIONS.forEach((option, idx) => { + expect(findDropdownItems().at(idx).text()).toBe(option.text); + }); + }); + + it('should select an item when clicked', async () => { + const item = findDropdownItems().at(1); + + expect(item.props('isChecked')).toBe(false); + + item.vm.$emit('click'); + + await nextTick(); + + expect(item.props('isChecked')).toBe(true); + }); +}); diff --git a/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js b/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js index 7b1ec6063fba83e25fc07136a5a9950437ab2e7e..17d6e0ce27dc5590afa6eb3d5367eb6cc4867954 100644 --- a/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js +++ b/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js @@ -4,6 +4,7 @@ import Vuex from 'vuex'; import ModalRuleRemove from 'ee/approvals/components/modal_rule_remove.vue'; import { stubComponent } from 'helpers/stub_component'; import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue'; +import { createExternalRule } from '../mocks'; const MODAL_MODULE = 'deleteModal'; const TEST_MODAL_ID = 'test-delete-modal-id'; @@ -14,6 +15,11 @@ const TEST_RULE = { .fill(1) .map((x, id) => ({ id })), }; +const SINGLE_APPROVER = { + ...TEST_RULE, + approvers: [{ id: 1 }], +}; +const EXTERNAL_RULE = createExternalRule(); const localVue = createLocalVue(); localVue.use(Vuex); @@ -61,6 +67,7 @@ describe('Approvals ModalRuleRemove', () => { }; actions = { deleteRule: jest.fn(), + deleteExternalApprovalRule: jest.fn(), }; }); @@ -83,30 +90,31 @@ describe('Approvals ModalRuleRemove', () => { ); }); - it('shows message', () => { - factory(); - - expect(findModal().element).toMatchSnapshot(); - }); - - it('shows singular message', () => { - deleteModalState.data = { - ...TEST_RULE, - approvers: [{ id: 1 }], - }; + it.each` + type | rule + ${'multiple approvers'} | ${TEST_RULE} + ${'singular approver'} | ${SINGLE_APPROVER} + ${'external approval'} | ${EXTERNAL_RULE} + `('matches the snapshot for $type', ({ rule }) => { + deleteModalState.data = rule; factory(); expect(findModal().element).toMatchSnapshot(); }); - it('deletes rule when modal is submitted', () => { + it.each` + typeType | action | rule + ${'regular'} | ${'deleteRule'} | ${TEST_RULE} + ${'external'} | ${'deleteExternalApprovalRule'} | ${EXTERNAL_RULE} + `('calls $action when the modal is submitted for a $typeType rule', ({ action, rule }) => { + deleteModalState.data = rule; factory(); - expect(actions.deleteRule).not.toHaveBeenCalled(); + expect(actions[action]).not.toHaveBeenCalled(); const modal = findModal(); modal.vm.$emit('ok', new Event('submit')); - expect(actions.deleteRule).toHaveBeenCalledWith(expect.anything(), TEST_RULE.id); + expect(actions[action]).toHaveBeenCalledWith(expect.anything(), rule.id); }); }); diff --git a/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js b/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js index 0ceb68ee67bcce1c34dfa3469fde14e1fd9b2ee6..ab3aaa87fdeb3a073036a986f6ed79024949312b 100644 --- a/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js +++ b/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js @@ -1,5 +1,6 @@ import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; +import ApprovalGateIcon from 'ee/approvals/components/approval_gate_icon.vue'; import RuleInput from 'ee/approvals/components/mr_edit/rule_input.vue'; import ProjectRules from 'ee/approvals/components/project_settings/project_rules.vue'; import RuleName from 'ee/approvals/components/rule_name.vue'; @@ -8,7 +9,7 @@ import UnconfiguredSecurityRules from 'ee/approvals/components/security_configur import { createStoreOptions } from 'ee/approvals/stores'; import projectSettingsModule from 'ee/approvals/stores/modules/project_settings'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; -import { createProjectRules } from '../../mocks'; +import { createProjectRules, createExternalRule } from '../../mocks'; const TEST_RULES = createProjectRules(); @@ -149,4 +150,26 @@ describe('Approvals ProjectRules', () => { expect(wrapper.find(UnconfiguredSecurityRules).exists()).toBe(true); }); }); + + describe('when the rule is external', () => { + const rule = createExternalRule(); + + beforeEach(() => { + store.modules.approvals.state.rules = [rule]; + + factory(); + }); + + it('renders the approval gate component with URL', () => { + expect(wrapper.findComponent(ApprovalGateIcon).props('url')).toBe(rule.externalUrl); + }); + + it('does not render a user avatar component', () => { + expect(wrapper.findComponent(UserAvatarList).exists()).toBe(false); + }); + + it('does not render the approvals required input', () => { + expect(wrapper.findComponent(RuleInput).exists()).toBe(false); + }); + }); }); diff --git a/ee/spec/frontend/approvals/components/rule_form_spec.js b/ee/spec/frontend/approvals/components/rule_form_spec.js index d0643e2776e3fcdb5729424c9d6f345246553f8a..5b36011f00078eb45b9a9c4695f8955f84d94b6a 100644 --- a/ee/spec/frontend/approvals/components/rule_form_spec.js +++ b/ee/spec/frontend/approvals/components/rule_form_spec.js @@ -1,12 +1,21 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; import Vuex from 'vuex'; +import ApproverTypeSelect from 'ee/approvals/components/approver_type_select.vue'; import ApproversList from 'ee/approvals/components/approvers_list.vue'; import ApproversSelect from 'ee/approvals/components/approvers_select.vue'; import BranchesSelect from 'ee/approvals/components/branches_select.vue'; import RuleForm from 'ee/approvals/components/rule_form.vue'; -import { TYPE_USER, TYPE_GROUP, TYPE_HIDDEN_GROUPS } from 'ee/approvals/constants'; +import { + TYPE_USER, + TYPE_GROUP, + TYPE_HIDDEN_GROUPS, + RULE_TYPE_EXTERNAL_APPROVAL, +} from 'ee/approvals/constants'; import { createStoreOptions } from 'ee/approvals/stores'; import projectSettingsModule from 'ee/approvals/stores/modules/project_settings'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createExternalRule } from '../mocks'; const TEST_PROJECT_ID = '7'; const TEST_RULE = { @@ -27,6 +36,10 @@ const TEST_FALLBACK_RULE = { approvalsRequired: 1, isFallback: true, }; +const TEST_EXTERNAL_APPROVAL_RULE = { + ...createExternalRule(), + protectedBranches: TEST_PROTECTED_BRANCHES, +}; const TEST_LOCKED_RULE_NAME = 'LOCKED_RULE'; const nameTakenError = { response: { @@ -37,6 +50,13 @@ const nameTakenError = { }, }, }; +const urlTakenError = { + response: { + data: { + message: ['External url has already been taken'], + }, + }, +}; const localVue = createLocalVue(); localVue.use(Vuex); @@ -54,7 +74,11 @@ describe('EE Approvals RuleForm', () => { store: new Vuex.Store(store), localVue, provide: { - glFeatures: { scopedApprovalRules: true, ...options.provide?.glFeatures }, + glFeatures: { + ffComplianceApprovalGates: true, + scopedApprovalRules: true, + ...options.provide?.glFeatures, + }, }, }); }; @@ -71,6 +95,9 @@ describe('EE Approvals RuleForm', () => { const findApproversValidation = () => findValidation(findApproversSelect(), true); const findApproversList = () => wrapper.find(ApproversList); const findBranchesSelect = () => wrapper.find(BranchesSelect); + const findApproverTypeSelect = () => wrapper.findComponent(ApproverTypeSelect); + const findExternalUrlInput = () => wrapper.find('input[name=approval_gate_url'); + const findExternalUrlValidation = () => findValidation(findExternalUrlInput(), false); const findBranchesValidation = () => findValidation(findBranchesSelect(), true); const findValidations = () => [ findNameValidation(), @@ -85,12 +112,20 @@ describe('EE Approvals RuleForm', () => { findBranchesValidation(), ]; + const findValidationForExternal = () => [ + findNameValidation(), + findExternalUrlValidation(), + findBranchesValidation(), + ]; + beforeEach(() => { store = createStoreOptions(projectSettingsModule(), { projectId: TEST_PROJECT_ID }); - ['postRule', 'putRule', 'deleteRule', 'putFallbackRule'].forEach((actionName) => { - jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {}); - }); + ['postRule', 'putRule', 'deleteRule', 'putFallbackRule', 'postExternalApprovalRule'].forEach( + (actionName) => { + jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {}); + }, + ); ({ actions } = store.modules.approvals); }); @@ -181,6 +216,119 @@ describe('EE Approvals RuleForm', () => { }); }); + describe('when the rule is an external rule', () => { + describe('with initial rule', () => { + beforeEach(() => { + createComponent({ + isMrEdit: false, + initRule: TEST_EXTERNAL_APPROVAL_RULE, + }); + }); + + it('does not render the approver type select input', () => { + expect(findApproverTypeSelect().exists()).toBe(false); + }); + + it('on load, it populates the external URL', () => { + expect(findExternalUrlInput().element.value).toBe( + TEST_EXTERNAL_APPROVAL_RULE.externalUrl, + ); + }); + }); + + describe('without an initial rule', () => { + beforeEach(() => { + createComponent({ + isMrEdit: false, + }); + findApproverTypeSelect().vm.$emit('input', RULE_TYPE_EXTERNAL_APPROVAL); + }); + + it('renders the approver type select input', () => { + expect(findApproverTypeSelect().exists()).toBe(true); + }); + + it('renders the inputs for external rules', () => { + expect(findNameInput().exists()).toBe(true); + expect(findExternalUrlInput().exists()).toBe(true); + expect(findBranchesSelect().exists()).toBe(true); + }); + + it('does not render the user and group input fields', () => { + expect(findApprovalsRequiredInput().exists()).toBe(false); + expect(findApproversList().exists()).toBe(false); + expect(findApproversSelect().exists()).toBe(false); + }); + + it('at first, shows no validation', () => { + const inputs = findValidationForExternal(); + const invalidInputs = inputs.filter((x) => !x.isValid); + const feedbacks = inputs.map((x) => x.feedback); + + expect(invalidInputs.length).toBe(0); + expect(feedbacks.every((str) => !str.length)).toBe(true); + }); + + it('on submit, does not dispatch action', () => { + wrapper.vm.submit(); + + expect(actions.postExternalApprovalRule).not.toHaveBeenCalled(); + }); + + it('on submit, shows name validation', async () => { + findExternalUrlInput().setValue(''); + + wrapper.vm.submit(); + + await nextTick(); + + expect(findExternalUrlValidation()).toEqual({ + isValid: false, + feedback: 'Please provide a valid URL', + }); + }); + + describe('with valid data', () => { + const branches = TEST_PROTECTED_BRANCHES.map((x) => x.id); + const expected = { + id: null, + name: 'Lorem', + externalUrl: 'https://gitlab.com/', + protectedBranchIds: branches, + }; + + beforeEach(() => { + findNameInput().setValue(expected.name); + findExternalUrlInput().setValue(expected.externalUrl); + wrapper.vm.branches = expected.protectedBranchIds; + }); + + it('on submit, posts external approval rule', () => { + wrapper.vm.submit(); + + expect(actions.postExternalApprovalRule).toHaveBeenCalledWith( + expect.anything(), + expected, + ); + }); + + it('when submitted with a duplicate external URL, shows the "url already taken" validation', async () => { + store.state.settings.prefix = 'project-settings'; + jest.spyOn(wrapper.vm, 'postExternalApprovalRule').mockRejectedValueOnce(urlTakenError); + + wrapper.vm.submit(); + + await waitForPromises(); + + expect(findExternalUrlValidation()).toEqual({ + isValid: false, + feedback: 'External url has already been taken', + }); + }); + }); + }); + }); + describe('without initRule', () => { beforeEach(() => { createComponent(); @@ -536,16 +684,17 @@ describe('EE Approvals RuleForm', () => { describe('with approval suggestions', () => { describe.each` - defaultRuleName | expectedDisabledAttribute - ${'Vulnerability-Check'} | ${'disabled'} - ${'License-Check'} | ${'disabled'} - ${'Foo Bar Baz'} | ${undefined} + defaultRuleName | expectedDisabledAttribute | approverTypeSelect + ${'Vulnerability-Check'} | ${'disabled'} | ${false} + ${'License-Check'} | ${'disabled'} | ${false} + ${'Foo Bar Baz'} | ${undefined} | ${true} `( 'with defaultRuleName set to $defaultRuleName', - ({ defaultRuleName, expectedDisabledAttribute }) => { + ({ defaultRuleName, expectedDisabledAttribute, approverTypeSelect }) => { beforeEach(() => { createComponent({ initRule: null, + isMrEdit: false, defaultRuleName, }); }); @@ -555,6 +704,12 @@ describe('EE Approvals RuleForm', () => { } the name text field`, () => { expect(findNameInput().attributes('disabled')).toBe(expectedDisabledAttribute); }); + + it(`${ + approverTypeSelect ? 'renders' : 'does not render' + } the approver type select`, () => { + expect(findApproverTypeSelect().exists()).toBe(approverTypeSelect); + }); }, ); }); @@ -727,4 +882,23 @@ describe('EE Approvals RuleForm', () => { }); }); }); + + describe('when the approval gates feature is disabled', () => { + it('does not render the approver type select input', async () => { + createComponent( + { isMrEdit: false }, + { + provide: { + glFeatures: { + ffComplianceApprovalGates: false, + }, + }, + }, + ); + + await nextTick(); + + expect(findApproverTypeSelect().exists()).toBe(false); + }); + }); }); diff --git a/ee/spec/frontend/approvals/mocks.js b/ee/spec/frontend/approvals/mocks.js index 073ba48f11c7645c07981faf4dd4614ea467f0d3..9366c88fe35d63ddcd5d7ac0ffcc53e31db8ae28 100644 --- a/ee/spec/frontend/approvals/mocks.js +++ b/ee/spec/frontend/approvals/mocks.js @@ -1,3 +1,10 @@ +export const createExternalRule = () => ({ + id: 9, + name: 'API Gate', + externalUrl: 'https://gitlab.com', + ruleType: 'external_approval', +}); + export const createProjectRules = () => [ { id: 1, diff --git a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js index c980041ab0c59ba5d6c4c2e1a315fba467a73c98..fcfb521666235a305df23d8bbd0900095da2126e 100644 --- a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js +++ b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js @@ -18,8 +18,8 @@ export const mockJiraIssue1 = { labels: [ { name: 'backend', - color: '#EBECF0', - text_color: '#283856', + color: '#0052CC', + text_color: '#FFFFFF', }, ], author: { diff --git a/ee/spec/frontend/integrations/jira/issues_show/mock_data.js b/ee/spec/frontend/integrations/jira/issues_show/mock_data.js index 652e461ed96865b40d763ec0f655dc061d9ae3ca..b37ada539a9e7f058befd2f136d06bafb7d80a10 100644 --- a/ee/spec/frontend/integrations/jira/issues_show/mock_data.js +++ b/ee/spec/frontend/integrations/jira/issues_show/mock_data.js @@ -20,8 +20,8 @@ export const mockJiraIssue = { { title: 'In Progress', description: 'Work that is still in progress', - color: '#EBECF0', - text_color: '#283856', + color: '#0052CC', + text_color: '#FFFFFF', }, ], references: { diff --git a/ee/spec/frontend/vulnerabilities/generic_report/report_row_spec.js b/ee/spec/frontend/vulnerabilities/generic_report/report_row_spec.js deleted file mode 100644 index df25d8ed3aea5d3816359e142906c8f32c16095d..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/vulnerabilities/generic_report/report_row_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import ReportRow from 'ee/vulnerabilities/components/generic_report/report_row.vue'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; - -describe('ee/vulnerabilities/components/generic_report/report_row.vue', () => { - let wrapper; - - const createWrapper = ({ ...options } = {}) => - extendedWrapper( - shallowMount(ReportRow, { - propsData: { - label: 'Foo', - }, - ...options, - }), - ); - - it('renders the default slot', () => { - const slotContent = 'foo bar'; - wrapper = createWrapper({ slots: { default: slotContent } }); - - expect(wrapper.findByTestId('reportContent').text()).toBe(slotContent); - }); -}); diff --git a/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js b/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js index db65b2d2b05fe4fd4880921835a668b9976681ff..9f4702f007c096a8004f2e5deec505892d337843 100644 --- a/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js +++ b/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js @@ -1,8 +1,6 @@ import { within, fireEvent } from '@testing-library/dom'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import ReportItem from 'ee/vulnerabilities/components/generic_report/report_item.vue'; -import ReportRow from 'ee/vulnerabilities/components/generic_report/report_row.vue'; import ReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue'; import { REPORT_TYPE_URL } from 'ee/vulnerabilities/components/generic_report/types/constants'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -47,9 +45,9 @@ describe('ee/vulnerabilities/components/generic_report/report_section.vue', () = name: /evidence/i, }); const findReportsSection = () => wrapper.findByTestId('reports'); - const findAllReportRows = () => wrapper.findAllComponents(ReportRow); + const findAllReportRows = () => wrapper.findAll('[data-testid*="report-row"]'); const findReportRowByLabel = (label) => wrapper.findByTestId(`report-row-${label}`); - const findItemWithinRow = (row) => row.findComponent(ReportItem); + const findReportItemByLabel = (label) => wrapper.findByTestId(`report-item-${label}`); const supportedReportTypesLabels = Object.keys(TEST_DATA.supportedTypes); describe('with supported report types', () => { @@ -77,20 +75,21 @@ describe('ee/vulnerabilities/components/generic_report/report_section.vue', () = expect(findAllReportRows()).toHaveLength(supportedReportTypesLabels.length); }); - it.each(supportedReportTypesLabels)('passes the correct props to report row: %s', (label) => { - expect(findReportRowByLabel(label).props()).toMatchObject({ - label: TEST_DATA.supportedTypes[label].name, - }); - }); + it.each(supportedReportTypesLabels)( + 'renders the correct label for report row: %s', + (label) => { + expect(within(findReportRowByLabel(label).element).getByText(label)).toBeInstanceOf( + HTMLElement, + ); + }, + ); }); describe('report items', () => { it.each(supportedReportTypesLabels)( 'passes the correct props to item for row: %s', (label) => { - const row = findReportRowByLabel(label); - - expect(findItemWithinRow(row).props()).toMatchObject({ + expect(findReportItemByLabel(label).props()).toMatchObject({ item: TEST_DATA.supportedTypes[label], }); }, diff --git a/ee/spec/graphql/resolvers/admin/cloud_licenses/current_license_resolver_spec.rb b/ee/spec/graphql/resolvers/admin/cloud_licenses/current_license_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d4a4591f62ec171507b98d60f0c13cd2317dddd8 --- /dev/null +++ b/ee/spec/graphql/resolvers/admin/cloud_licenses/current_license_resolver_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Admin::CloudLicenses::CurrentLicenseResolver do + include GraphqlHelpers + + specify do + expect(described_class).to have_nullable_graphql_type(::Types::Admin::CloudLicenses::CurrentLicenseType) + end + + describe '#resolve' do + subject(:result) { resolve_current_license } + + let_it_be(:admin) { create(:admin) } + let_it_be(:license) { create_current_license } + + def resolve_current_license(current_user: admin) + resolve(described_class, ctx: { current_user: current_user }) + end + + before do + stub_application_setting(cloud_license_enabled: true) + end + + context 'when application setting for cloud license is disabled', :enable_admin_mode do + it 'returns nil' do + stub_application_setting(cloud_license_enabled: false) + + expect(result).to be_nil + end + end + + context 'when current user is unauthorized' do + it 'raises error' do + unauthorized_user = create(:user) + + expect do + resolve_current_license(current_user: unauthorized_user) + end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when there is no current license', :enable_admin_mode do + it 'returns nil' do + License.delete_all # delete existing license + + expect(result).to be_nil + end + end + + it 'returns the current license', :enable_admin_mode do + expect(result).to eq(license) + end + end +end diff --git a/ee/spec/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver_spec.rb b/ee/spec/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..63b2229fd25c6db8e2cc3fa2e8d39b998cc1e1b1 --- /dev/null +++ b/ee/spec/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver do + include GraphqlHelpers + + describe '#resolve' do + subject(:result) { resolve_license_history_entries } + + let_it_be(:admin) { create(:admin) } + + def create_license(data: {}, license_options: { created_at: Time.current }) + gl_license = create(:gitlab_license, data) + create(:license, license_options.merge(data: gl_license.export)) + end + + def resolve_license_history_entries(current_user: admin) + resolve(described_class, ctx: { current_user: current_user }) + end + + before do + stub_application_setting(cloud_license_enabled: true) + end + + context 'when application setting for cloud license is disabled', :enable_admin_mode do + it 'returns nil' do + stub_application_setting(cloud_license_enabled: false) + + expect(result).to be_nil + end + end + + context 'when current user is unauthorized' do + it 'raises error' do + unauthorized_user = create(:user) + + expect do + resolve_license_history_entries(current_user: unauthorized_user) + end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when no licenses exist' do + it 'returns an empty array', :enable_admin_mode do + License.delete_all # delete license created with ee/spec/support/test_license.rb + + expect(result).to eq([]) + end + end + + it 'returns the license history entries', :enable_admin_mode do + today = Date.current + type = License::CLOUD_LICENSE_TYPE + + past_license = create_license( + data: { starts_at: today - 1.month, expires_at: today + 11.months }, + license_options: { created_at: Time.current - 1.month } + ) + expired_license = create_license(data: { starts_at: today - 1.year, expires_at: today - 1.month }) + another_license = create_license(data: { starts_at: today - 1.month, expires_at: today + 1.year }) + future_license = create_license(data: { starts_at: today + 1.month, expires_at: today + 13.months, type: type }) + current_license = create_license(data: { starts_at: today - 15.days, expires_at: today + 11.months, type: type }) + + expect(result).to eq( + [ + future_license, + current_license, + another_license, + past_license, + expired_license, + License.first # created with ee/spec/support/test_license.rb + ] + ) + end + end +end diff --git a/ee/spec/graphql/types/admin/cloud_licenses/current_license_type_spec.rb b/ee/spec/graphql/types/admin/cloud_licenses/current_license_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..cff1ae38b7556b1c577e6883ebaa04b256e520ab --- /dev/null +++ b/ee/spec/graphql/types/admin/cloud_licenses/current_license_type_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CurrentLicense'], :enable_admin_mode do + let_it_be(:admin) { create(:admin) } + let_it_be(:licensee) do + { + 'Name' => 'User Example', + 'Email' => 'user@example.com', + 'Company' => 'Example Inc.' + } + end + + let_it_be(:license) { create_current_license(licensee: licensee, type: License::CLOUD_LICENSE_TYPE) } + + let(:fields) do + %w[last_sync billable_users_count maximum_user_count users_over_license_count] + end + + def query(field_name) + %( + { + currentLicense { + #{field_name} + } + } + ) + end + + def query_field(field_name) + GitlabSchema.execute(query(field_name), context: { current_user: admin }).as_json + end + + before do + stub_application_setting(cloud_license_enabled: true) + end + + it { expect(described_class.graphql_name).to eq('CurrentLicense') } + it { expect(described_class).to include_graphql_fields(*fields) } + + include_examples 'license type fields', %w[data currentLicense] + + describe "#users_over_license_count" do + context 'when license is for a trial' do + it 'returns 0' do + create_current_license(licensee: licensee, restrictions: { trial: true }) + + result_as_json = query_field('usersOverLicenseCount') + + expect(result_as_json['data']['currentLicense']['usersOverLicenseCount']).to eq(0) + end + end + + it 'returns the number of users over the paid users in the license' do + create(:historical_data, active_user_count: 15) + create_current_license(licensee: licensee, restrictions: { active_user_count: 10 }) + + result_as_json = query_field('usersOverLicenseCount') + + expect(result_as_json['data']['currentLicense']['usersOverLicenseCount']).to eq(5) + end + end +end diff --git a/ee/spec/graphql/types/admin/cloud_licenses/license_history_entry_type_spec.rb b/ee/spec/graphql/types/admin/cloud_licenses/license_history_entry_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..03552ebb428913cbd873112c560ca4c15a212468 --- /dev/null +++ b/ee/spec/graphql/types/admin/cloud_licenses/license_history_entry_type_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['LicenseHistoryEntry'], :enable_admin_mode do + let_it_be(:admin) { create(:admin) } + let_it_be(:licensee) do + { + 'Name' => 'User Example', + 'Email' => 'user@example.com', + 'Company' => 'Example Inc.' + } + end + + let_it_be(:license) { create_current_license(licensee: licensee, type: License::CLOUD_LICENSE_TYPE) } + + def query(field_name) + %( + { + licenseHistoryEntries { + nodes { + #{field_name} + } + } + } + ) + end + + def query_field(field_name) + GitlabSchema.execute(query(field_name), context: { current_user: admin }).as_json + end + + before do + stub_application_setting(cloud_license_enabled: true) + end + + it { expect(described_class.graphql_name).to eq('LicenseHistoryEntry') } + + include_examples 'license type fields', ['data', 'licenseHistoryEntries', 'nodes', -1] +end diff --git a/ee/spec/graphql/types/query_type_spec.rb b/ee/spec/graphql/types/query_type_spec.rb index 06a4ed474a5ca0ea8c00e0063e203a5c854a7594..767a5e9e01b9a71e61cfff98b78662415d521219 100644 --- a/ee/spec/graphql/types/query_type_spec.rb +++ b/ee/spec/graphql/types/query_type_spec.rb @@ -10,7 +10,9 @@ :vulnerabilities, :vulnerability, :instance_security_dashboard, - :vulnerabilities_count_by_day_and_severity + :vulnerabilities_count_by_day_and_severity, + :current_license, + :license_history_entries ).at_least end end diff --git a/ee/spec/initializers/lograge_spec.rb b/ee/spec/initializers/lograge_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7bbbdb184297f5707c7a57b328fbb906acbc0aac --- /dev/null +++ b/ee/spec/initializers/lograge_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'lograge', type: :request do + context 'with a log subscriber' do + include_context 'parsed logs' + include_context 'clear DB Load Balancing configuration' + + let(:subscriber) { Lograge::LogSubscribers::ActionController.new } + + let(:event) do + ActiveSupport::Notifications::Event.new( + 'process_action.action_controller', + Time.now, + Time.now, + 2, + status: 200, + controller: 'HomeController', + action: 'index', + format: 'application/json', + method: 'GET', + path: '/home?foo=bar', + params: {}, + db_runtime: 0.02, + view_runtime: 0.01 + ) + end + + let(:logging_keys) do + %w[db_primary_wal_count + db_replica_wal_count + db_replica_count + db_replica_cached_count + db_primary_count + db_primary_cached_count + db_primary_duration_s + db_replica_duration_s] + end + + context 'when load balancing is enabled' do + before do + allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true) + end + + context 'with db payload' do + context 'when RequestStore is enabled', :request_store do + it 'includes db counters' do + subscriber.process_action(event) + expect(log_data).to include(*logging_keys) + end + end + + context 'when RequestStore is disabled' do + it 'does not include db counters' do + subscriber.process_action(event) + + expect(log_data).not_to include(*logging_keys) + end + end + end + end + + context 'when load balancing is disabled' do + before do + allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false) + end + + it 'does not include db counters' do + subscriber.process_action(event) + + expect(log_data).not_to include(*logging_keys) + end + end + end +end diff --git a/ee/spec/lib/ee/gitlab/sidekiq_middleware/server_metrics_spec.rb b/ee/spec/lib/ee/gitlab/sidekiq_middleware/server_metrics_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..54f6905047905f125741917b3d4be73fbb292318 --- /dev/null +++ b/ee/spec/lib/ee/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# rubocop: disable RSpec/MultipleMemoizedHelpers +RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do + using RSpec::Parameterized::TableSyntax + + subject { described_class.new } + + let(:queue) { :test } + let(:worker_class) { worker.class } + let(:job) { {} } + let(:job_status) { :done } + let(:labels_with_job_status) { default_labels.merge(job_status: job_status.to_s) } + let(:default_labels) do + { queue: queue.to_s, + worker: worker_class.to_s, + boundary: "", + external_dependencies: "no", + feature_category: "", + urgency: "low" } + end + + before do + stub_const('TestWorker', Class.new) + TestWorker.class_eval do + include Sidekiq::Worker + include WorkerAttributes + end + end + + let(:worker) { TestWorker.new } + + include_context 'server metrics with mocked prometheus' + + context 'when load_balancing is enabled' do + let(:load_balancing_metric) { double('load balancing metric') } + + include_context 'clear DB Load Balancing configuration' + + before do + allow(::Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric) + end + + describe '#initialize' do + it 'sets load_balancing metrics' do + expect(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric) + + subject + end + end + + describe '#call' do + include_context 'server metrics call' + + context 'when :database_chosen is provided' do + where(:database_chosen) do + %w[primary retry replica] + end + + with_them do + context "when #{params[:database_chosen]} is used" do + let(:labels_with_load_balancing) do + labels_with_job_status.merge(database_chosen: database_chosen, data_consistency: 'delayed') + end + + before do + job[:database_chosen] = database_chosen + job[:data_consistency] = 'delayed' + allow(load_balancing_metric).to receive(:increment) + end + + it 'increment sidekiq_load_balancing_count' do + expect(load_balancing_metric).to receive(:increment).with(labels_with_load_balancing, 1) + + described_class.new.call(worker, job, :test) { nil } + end + end + end + end + + context 'when :database_chosen is not provided' do + it 'does not increment sidekiq_load_balancing_count' do + expect(load_balancing_metric).not_to receive(:increment) + + described_class.new.call(worker, job, :test) { nil } + end + end + end + end + + context 'when load_balancing is disabled' do + include_context 'clear DB Load Balancing configuration' + + before do + allow(::Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false) + end + + describe '#initialize' do + it 'doesnt set load_balancing metrics' do + expect(Gitlab::Metrics).not_to receive(:counter).with(:sidekiq_load_balancing_count, anything) + + subject + end + end + end +end diff --git a/ee/spec/lib/gitlab/instrumentation_helper_spec.rb b/ee/spec/lib/gitlab/instrumentation_helper_spec.rb index 6b61425b0615b23cfef5750d38b0edfec1201135..6b096d9ec5b8124a85e27d56029303dac81fffe6 100644 --- a/ee/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/ee/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -21,7 +21,9 @@ expect(payload).to include(db_replica_count: 0, db_replica_cached_count: 0, db_primary_count: 0, - db_primary_cached_count: 0) + db_primary_cached_count: 0, + db_primary_wal_count: 0, + db_replica_wal_count: 0) end end @@ -30,13 +32,15 @@ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false) end - it 'includes DB counts' do + it 'does not include DB counts' do subject expect(payload).not_to include(db_replica_count: 0, db_replica_cached_count: 0, db_primary_count: 0, - db_primary_cached_count: 0) + db_primary_cached_count: 0, + db_primary_wal_count: 0, + db_replica_wal_count: 0) end end diff --git a/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index d83ddc96d1d52609ef32736b732338e822b866ab..e74f56aad00f81cce2d3d3cdf14f1564b4b2a065 100644 --- a/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -29,18 +29,20 @@ def sql(query, comments: true) end shared_examples 'track sql events for each role' do - where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query) do - 'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false - 'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false - 'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false - 'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false - 'SQL' | 'DELETE FROM users where id = 10' | true | true | false - 'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false - 'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false - 'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true - 'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false - nil | 'BEGIN' | false | false | false - nil | 'COMMIT' | false | false | false + where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query, :record_wal_query) do + 'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false | false + 'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false | false + 'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false | false + 'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false | false + 'SQL' | 'DELETE FROM users where id = 10' | true | true | false | false + 'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false | false + 'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false | false + 'SQL' | 'SELECT pg_current_wal_insert_lsn()::text AS location' | true | false | false | true + 'SQL' | 'SELECT pg_last_wal_replay_lsn()::text AS location' | true | false | false | true + 'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true | false + 'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false | false + nil | 'BEGIN' | false | false | false | false + nil | 'COMMIT' | false | false | false | false end with_them do diff --git a/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 791b1ebedc0d4354661b8094c1200111e4e27d02..d94d70641560c971fc43f97dd9c71e844a2b06fa 100644 --- a/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -76,7 +76,9 @@ 'db_replica_count' => 0, 'db_replica_cached_count' => 0, 'db_primary_count' => a_value >= 1, - 'db_primary_cached_count' => 0 + 'db_primary_cached_count' => 0, + 'db_primary_wal_count' => 0, + 'db_replica_wal_count' => 0 ) end @@ -94,7 +96,9 @@ 'db_replica_count' => 0, 'db_replica_cached_count' => 0, 'db_primary_count' => 0, - 'db_primary_cached_count' => 0 + 'db_primary_cached_count' => 0, + 'db_primary_wal_count' => 0, + 'db_replica_wal_count' => 0 ) end diff --git a/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb b/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb index daead45ade74ca00d83c6e3d184e296f35d605c0..9832e9eceea71f412f510d6b22b42fcec135bc37 100644 --- a/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb +++ b/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb @@ -9,7 +9,9 @@ :db_replica_count, :db_replica_cached_count, :db_primary_count, - :db_primary_cached_count + :db_primary_cached_count, + :db_primary_wal_count, + :db_replica_wal_count ] expect(described_class.keys).to include(*expected_keys) diff --git a/ee/spec/models/license_spec.rb b/ee/spec/models/license_spec.rb index 5b1bc73f2e680089e4c4fadabb0acbd5c27c3c43..ea82896f72843f11b2e1b8bf297f25c0c98328c2 100644 --- a/ee/spec/models/license_spec.rb +++ b/ee/spec/models/license_spec.rb @@ -1411,6 +1411,20 @@ def set_restrictions(opts) end end + describe '#license_type' do + subject { license.license_type } + + context 'when the license is not a cloud license' do + it { is_expected.to eq(described_class::LEGACY_LICENSE_TYPE) } + end + + context 'when the license is a cloud license' do + let(:gl_license) { build(:gitlab_license, type: described_class::CLOUD_LICENSE_TYPE) } + + it { is_expected.to eq(described_class::CLOUD_LICENSE_TYPE) } + end + end + describe '#auto_renew' do it 'is false' do expect(license.auto_renew).to be false @@ -1485,4 +1499,28 @@ def set_restrictions(opts) it { is_expected.to eq(result) } end end + + describe '#licensee_name' do + subject { license.licensee_name } + + let(:gl_license) { build(:gitlab_license, licensee: { 'Name' => 'User Example' }) } + + it { is_expected.to eq('User Example') } + end + + describe '#licensee_email' do + subject { license.licensee_email } + + let(:gl_license) { build(:gitlab_license, licensee: { 'Email' => 'user@example.com' }) } + + it { is_expected.to eq('user@example.com') } + end + + describe '#licensee_company' do + subject { license.licensee_company } + + let(:gl_license) { build(:gitlab_license, licensee: { 'Company' => 'Example Inc.' }) } + + it { is_expected.to eq('Example Inc.') } + end end diff --git a/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb b/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb index 616048882b4ccfca57e4aae25e84e6bca75f8944..00dca7094e953c8c2106c17a1420744b6a9837d8 100644 --- a/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb +++ b/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb @@ -88,8 +88,8 @@ { title: 'backend', name: 'backend', - color: '#EBECF0', - text_color: '#283856' + color: '#0052CC', + text_color: '#FFFFFF' } ], author: hash_including( diff --git a/ee/spec/serializers/integrations/jira/issue_entity_spec.rb b/ee/spec/serializers/integrations/jira/issue_entity_spec.rb index daa85b475be0a62a63810a255ff64396af010dcb..ac457b8979120cb36ff71a0e36bd27c82fc6ae21 100644 --- a/ee/spec/serializers/integrations/jira/issue_entity_spec.rb +++ b/ee/spec/serializers/integrations/jira/issue_entity_spec.rb @@ -55,8 +55,8 @@ { title: 'backend', name: 'backend', - color: '#EBECF0', - text_color: '#283856' + color: '#0052CC', + text_color: '#FFFFFF' } ], author: hash_including( diff --git a/ee/spec/services/ee/merge_requests/update_service_spec.rb b/ee/spec/services/ee/merge_requests/update_service_spec.rb index 44f079fc72078f498401eaf2a4962aa41a7b2338..9665aa888a5b18d5457a13e4f5bf3a7c72a5fbec 100644 --- a/ee/spec/services/ee/merge_requests/update_service_spec.rb +++ b/ee/spec/services/ee/merge_requests/update_service_spec.rb @@ -302,12 +302,13 @@ def update_merge_request(opts) end end - it 'updates code owner approval rules' do - expect_next_instance_of(::MergeRequests::SyncCodeOwnerApprovalRules) do |instance| - expect(instance).to receive(:execute) - end + context 'when called inside an ActiveRecord transaction' do + it 'does not attempt to update code owner approval rules' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true) + expect(::MergeRequests::SyncCodeOwnerApprovalRulesWorker).not_to receive(:perform_async) - update_merge_request(title: 'Title') + update_merge_request(title: 'Title') + end end context 'updating reviewers_ids' do diff --git a/ee/spec/services/milestones/destroy_service_spec.rb b/ee/spec/services/milestones/destroy_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..35872d1c16d00383d5d1d3ccf568602b313129b9 --- /dev/null +++ b/ee/spec/services/milestones/destroy_service_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Milestones::DestroyService do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) } + + before do + project.add_maintainer(user) + end + + def service + described_class.new(project, user, {}) + end + + describe '#execute' do + context 'with an existing merge request' do + let!(:issue) { create(:issue, project: project, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + + it 'manually queues MergeRequests::SyncCodeOwnerApprovalRulesWorker jobs' do + expect(::MergeRequests::SyncCodeOwnerApprovalRulesWorker).to receive(:perform_async) + + service.execute(milestone) + end + end + end +end diff --git a/ee/spec/support/shared_examples/graphql/types/admin/cloud_licenses/license_type_shared_examples.rb b/ee/spec/support/shared_examples/graphql/types/admin/cloud_licenses/license_type_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..88afbcb1d4fdd3856bb4ccaefde9b519e74c2faf --- /dev/null +++ b/ee/spec/support/shared_examples/graphql/types/admin/cloud_licenses/license_type_shared_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'license type fields' do |keys| + context 'with license type fields' do + let(:license_fields) do + %w[id type plan name email company starts_at expires_at activated_at users_in_license_count] + end + + it { expect(described_class).to include_graphql_fields(*license_fields) } + end +end diff --git a/ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb b/ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d6a6c365d8f2e9ff20e28352325fa4330607309 --- /dev/null +++ b/ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe MergeRequests::SyncCodeOwnerApprovalRulesWorker do + let_it_be(:merge_request) { create(:merge_request) } + + subject { described_class.new } + + describe "#perform" do + it_behaves_like 'an idempotent worker' do + let(:job_args) { [merge_request.id] } + end + + context "when merge request is not found" do + it "returns without attempting to sync code owner rules" do + expect(MergeRequests::SyncCodeOwnerApprovalRules).not_to receive(:new) + + subject.perform(non_existing_record_id) + end + end + + context "when merge request is found" do + it "attempts to sync code owner rules" do + expect_next_instance_of(::MergeRequests::SyncCodeOwnerApprovalRules) do |instance| + expect(instance).to receive(:execute) + end + + subject.perform(merge_request.id) + end + end + end +end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 19462e6cb023d9dae211f57875bfa213fd3116c5..b7b6343aec041226d6b7b4297aeadaf26e072b1b 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -82,6 +82,30 @@ def overflow? !!@overflow end + def overflow_max_lines? + !!@overflow_max_lines + end + + def overflow_max_bytes? + !!@overflow_max_bytes + end + + def overflow_max_files? + !!@overflow_max_files + end + + def collapsed_safe_lines? + !!@collapsed_safe_lines + end + + def collapsed_safe_files? + !!@collapsed_safe_files + end + + def collapsed_safe_bytes? + !!@collapsed_safe_bytes + end + def size @size ||= count # forces a loop using each method end @@ -121,7 +145,15 @@ def populate! end def over_safe_limits?(files) - files >= safe_max_files || @line_count > safe_max_lines || @byte_count >= safe_max_bytes + if files >= safe_max_files + @collapsed_safe_files = true + elsif @line_count > safe_max_lines + @collapsed_safe_lines = true + elsif @byte_count >= safe_max_bytes + @collapsed_safe_bytes = true + end + + @collapsed_safe_files || @collapsed_safe_lines || @collapsed_safe_bytes end def expand_diff? @@ -154,6 +186,7 @@ def each_serialized_patch if @enforce_limits && i >= max_files @overflow = true + @overflow_max_files = true break end @@ -166,10 +199,19 @@ def each_serialized_patch @line_count += diff.line_count @byte_count += diff.diff.bytesize - if @enforce_limits && (@line_count >= max_lines || @byte_count >= max_bytes) + if @enforce_limits && @line_count >= max_lines + # This last Diff instance pushes us over the lines limit. We stop and + # discard it. + @overflow = true + @overflow_max_lines = true + break + end + + if @enforce_limits && @byte_count >= max_bytes # This last Diff instance pushes us over the lines limit. We stop and # discard it. @overflow = true + @overflow_max_bytes = true break end diff --git a/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb b/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb index db7cd6a8679bdb01f81c3bb2a78f0208dac0571f..b542aa4fe4c701c77b5ec82c85bdab1e0e81ea17 100644 --- a/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb +++ b/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb @@ -13,7 +13,6 @@ def self.keys :elasticsearch_calls, :elasticsearch_duration_s, :elasticsearch_timed_out_count, - :worker_data_consistency, *::Gitlab::Memory::Instrumentation::KEY_MAPPING.values, *::Gitlab::Instrumentation::Redis.known_payload_keys, *::Gitlab::Metrics::Subscribers::ActiveRecord.known_payload_keys, diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb index 3d16cc70bf849c87481a5cd86e9859d2270b6f44..f5fee8050ac2afd1654b1508316db68db6e31b27 100644 --- a/lib/gitlab/sidekiq_middleware/server_metrics.rb +++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb @@ -21,6 +21,16 @@ def call(worker, job, queue) Thread.current.name ||= Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME labels = create_labels(worker.class, queue, job) + instrument(job, labels) do + yield + end + end + + protected + + attr_reader :metrics + + def instrument(job, labels) queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job) @metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration @@ -62,8 +72,6 @@ def call(worker, job, queue) end end - private - def init_metrics { sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS), @@ -82,6 +90,8 @@ def init_metrics } end + private + def get_thread_cputime defined?(Process::CLOCK_THREAD_CPUTIME_ID) ? Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) : 0 end @@ -108,3 +118,5 @@ def get_gitaly_time(payload) end end end + +Gitlab::SidekiqMiddleware::ServerMetrics.prepend_if_ee('EE::Gitlab::SidekiqMiddleware::ServerMetrics') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c5bb6cb8737fe9fe1101996905640f0bf6c4aa9e..cfd057403db03d51cac2cbe034752d9605bb8fe2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2327,6 +2327,9 @@ msgstr "" msgid "AdminSettings|Required pipeline configuration" msgstr "" +msgid "AdminSettings|See affected service templates" +msgstr "" + msgid "AdminSettings|Select a pipeline configuration file" msgstr "" @@ -2363,6 +2366,9 @@ msgstr "" msgid "AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings > Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}." msgstr "" +msgid "AdminSettings|You should migrate to %{doc_link_start}Project integration management%{link_end}, available at %{settings_link_start}Settings > Integrations.%{link_end}" +msgstr "" + msgid "AdminStatistics|Active Users" msgstr "" @@ -3982,6 +3988,9 @@ msgstr "" msgid "Applying suggestions..." msgstr "" +msgid "Approval Gate" +msgstr "" + msgid "Approval Status" msgstr "" @@ -4004,6 +4013,15 @@ msgid_plural "ApprovalRuleRemove|Approvals from these members are not revoked." msgstr[0] "" msgstr[1] "" +msgid "ApprovalRuleRemove|Remove approval gate" +msgstr "" + +msgid "ApprovalRuleRemove|Remove approval gate?" +msgstr "" + +msgid "ApprovalRuleRemove|You are about to remove the %{name} approval gate. Approval from this service is not revoked." +msgstr "" + msgid "ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}." msgstr "" @@ -4017,21 +4035,36 @@ msgid_plural "ApprovalRuleSummary|%{count} approvals required from %{membersCoun msgstr[0] "" msgstr[1] "" +msgid "ApprovalRule|Add approvel gate" +msgstr "" + msgid "ApprovalRule|Add approvers" msgstr "" msgid "ApprovalRule|Approval rules" msgstr "" +msgid "ApprovalRule|Approval service API" +msgstr "" + msgid "ApprovalRule|Approvals required" msgstr "" +msgid "ApprovalRule|Approvel gate" +msgstr "" + +msgid "ApprovalRule|Approver Type" +msgstr "" + msgid "ApprovalRule|Approvers" msgstr "" msgid "ApprovalRule|Examples: QA, Security." msgstr "" +msgid "ApprovalRule|Invoke an external API as part of the approvals" +msgstr "" + msgid "ApprovalRule|Name" msgstr "" @@ -4041,6 +4074,9 @@ msgstr "" msgid "ApprovalRule|Target branch" msgstr "" +msgid "ApprovalRule|Users or groups" +msgstr "" + msgid "ApprovalStatusTooltip|Adheres to separation of duties" msgstr "" @@ -11256,7 +11292,7 @@ msgstr "" msgid "DiscordService|Discord Notifications" msgstr "" -msgid "DiscordService|Receive event notifications in Discord" +msgid "DiscordService|Send notifications about project events to a Discord channel." msgstr "" msgid "Discover GitLab Geo" @@ -13013,6 +13049,9 @@ msgstr "" msgid "External storage authentication token" msgstr "" +msgid "External url has already been taken" +msgstr "" + msgid "ExternalAuthorizationService|Classification label" msgstr "" @@ -18672,6 +18711,9 @@ msgstr "" msgid "Leave edit mode? All unsaved changes will be lost." msgstr "" +msgid "Leave feedback" +msgstr "" + msgid "Leave group" msgstr "" @@ -28298,6 +28340,9 @@ msgstr "" msgid "Send notifications about project events to Mattermost channels. %{docs_link}" msgstr "" +msgid "Send notifications about project events to a Discord channel. %{docs_link}" +msgstr "" + msgid "Send report" msgstr "" diff --git a/spec/features/callouts/service_templates_deprecation_spec.rb b/spec/features/callouts/service_templates_deprecation_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b6403b54e290d4594b8341ead766b3bc4e674b2e --- /dev/null +++ b/spec/features/callouts/service_templates_deprecation_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Service templates deprecation callout' do + let_it_be(:admin) { create(:admin) } + let_it_be(:non_admin) { create(:user) } + let_it_be(:callout_content) { 'Service templates are deprecated and will be removed in GitLab 14.0.' } + + context 'when a non-admin is logged in' do + before do + sign_in(non_admin) + visit root_dashboard_path + end + + it 'does not display callout' do + expect(page).not_to have_content callout_content + end + end + + context 'when an admin is logged in' do + before do + sign_in(admin) + gitlab_enable_admin_mode_sign_in(admin) + + visit root_dashboard_path + end + + context 'with no active service templates' do + it 'does not display callout' do + expect(page).not_to have_content callout_content + end + end + + context 'with active service template' do + before do + create(:service, :template, type: 'MattermostService', active: true) + visit root_dashboard_path + end + + it 'displays callout' do + expect(page).to have_content callout_content + expect(page).to have_link 'See affected service templates', href: admin_application_settings_services_path + end + + context 'when callout is dismissed', :js do + before do + find('[data-testid="close-service-templates-deprecated-callout"]').click + + visit root_dashboard_path + end + + it 'does not display callout' do + expect(page).not_to have_content callout_content + end + end + end + end +end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 20fa8d628841aa793b5e353fcafdc09c2ca62a2a..dfea1020c5294fbcf440423d5db4a0de8f11fd17 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -291,6 +291,8 @@ end describe '#render_overflow_warning?' do + using RSpec::Parameterized::TableSyntax + let(:diffs_collection) { instance_double(Gitlab::Diff::FileCollection::MergeRequestDiff, raw_diff_files: diff_files) } let(:diff_files) { Gitlab::Git::DiffCollection.new(files) } let(:safe_file) { { too_large: false, diff: '' } } @@ -299,13 +301,42 @@ before do allow(diff_files).to receive(:overflow?).and_return(false) + allow(diff_files).to receive(:overflow_max_bytes?).and_return(false) + allow(diff_files).to receive(:overflow_max_files?).and_return(false) + allow(diff_files).to receive(:overflow_max_lines?).and_return(false) + allow(diff_files).to receive(:collapsed_safe_bytes?).and_return(false) + allow(diff_files).to receive(:collapsed_safe_files?).and_return(false) + allow(diff_files).to receive(:collapsed_safe_lines?).and_return(false) end - context 'when neither collection nor individual file hit the limit' do + context 'when no limits are hit' do it 'returns false and does not log any overflow events' do expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collection_limits) expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_single_file_limits) + expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_max_bytes_limits) + expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_max_files_limits) + expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_max_lines_limits) + expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collapsed_bytes_limits) + expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collapsed_files_limits) + expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collapsed_lines_limits) + + expect(render_overflow_warning?(diffs_collection)).to be false + end + end + + where(:overflow_method, :event_name) do + :overflow_max_bytes? | :diffs_overflow_max_bytes_limits + :overflow_max_files? | :diffs_overflow_max_files_limits + :overflow_max_lines? | :diffs_overflow_max_lines_limits + :collapsed_safe_bytes? | :diffs_overflow_collapsed_bytes_limits + :collapsed_safe_files? | :diffs_overflow_collapsed_files_limits + :collapsed_safe_lines? | :diffs_overflow_collapsed_lines_limits + end + with_them do + it 'returns false and only logs the correct collection overflow event' do + allow(diff_files).to receive(overflow_method).and_return(true) + expect(Gitlab::Metrics).to receive(:add_event).with(event_name).once expect(render_overflow_warning?(diffs_collection)).to be false end end @@ -315,9 +346,8 @@ allow(diff_files).to receive(:overflow?).and_return(true) end - it 'returns false and only logs collection overflow event' do - expect(Gitlab::Metrics).to receive(:add_event).with(:diffs_overflow_collection_limits).exactly(:once) - expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_single_file_limits) + it 'returns true and only logs all the correct collection overflow event' do + expect(Gitlab::Metrics).to receive(:add_event).with(:diffs_overflow_collection_limits).once expect(render_overflow_warning?(diffs_collection)).to be true end diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb index a2d9495ce6c280db7fd94dc32ba3744faf515c9f..3dbaa655aebc909c7d74c95c39ef4adcc0915329 100644 --- a/spec/helpers/user_callouts_helper_spec.rb +++ b/spec/helpers/user_callouts_helper_spec.rb @@ -81,23 +81,31 @@ end end - describe '.show_service_templates_deprecated?' do - subject { helper.show_service_templates_deprecated? } + describe '.show_service_templates_deprecated_callout?' do + using RSpec::Parameterized::TableSyntax - context 'when user has not dismissed' do - before do - allow(helper).to receive(:user_dismissed?).with(described_class::SERVICE_TEMPLATES_DEPRECATED) { false } - end + let_it_be(:admin) { create(:user, :admin) } + let_it_be(:non_admin) { create(:user) } - it { is_expected.to be true } + subject { helper.show_service_templates_deprecated_callout? } + + where(:self_managed, :is_admin_user, :has_active_service_template, :callout_dismissed, :should_show_callout) do + true | true | true | false | true + true | true | true | true | false + true | false | true | false | false + false | true | true | false | false + true | true | false | false | false end - context 'when user dismissed' do + with_them do before do - allow(helper).to receive(:user_dismissed?).with(described_class::SERVICE_TEMPLATES_DEPRECATED) { true } + allow(::Gitlab).to receive(:com?).and_return(!self_managed) + allow(helper).to receive(:current_user).and_return(is_admin_user ? admin : non_admin) + allow(helper).to receive(:user_dismissed?).with(described_class::SERVICE_TEMPLATES_DEPRECATED_CALLOUT) { callout_dismissed } + create(:service, :template, type: 'MattermostService', active: has_active_service_template) end - it { is_expected.to be false } + it { is_expected.to be should_show_callout } end end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 1a3c332a21bc5b89fb50ac5351e920027f7f60ea..114b3d01952e314c3c058aa8efb123e2a70c4cbb 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -31,6 +31,19 @@ def each end end + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + shared_examples 'overflow stuff' do + it 'returns the expected overflow values' do + subject.overflow? + expect(subject.overflow_max_bytes?).to eq(overflow_max_bytes) + expect(subject.overflow_max_files?).to eq(overflow_max_files) + expect(subject.overflow_max_lines?).to eq(overflow_max_lines) + end + end + subject do Gitlab::Git::DiffCollection.new( iterator, @@ -76,12 +89,19 @@ def each end context 'overflow handling' do + subject { super() } + + let(:collapsed_safe_files) { false } + let(:collapsed_safe_lines) { false } + context 'adding few enough files' do let(:file_count) { 3 } context 'and few enough lines' do let(:line_count) { 10 } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -117,6 +137,11 @@ def each context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -155,6 +180,9 @@ def each context 'and too many lines' do let(:line_count) { 1000 } + let(:overflow_max_lines) { true } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -184,6 +212,11 @@ def each context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -216,10 +249,13 @@ def each context 'adding too many files' do let(:file_count) { 11 } + let(:overflow_max_files) { true } context 'and few enough lines' do let(:line_count) { 1 } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -248,6 +284,11 @@ def each context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -279,6 +320,10 @@ def each context 'and too many lines' do let(:line_count) { 30 } + let(:overflow_max_lines) { true } + let(:overflow_max_files) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -308,6 +353,11 @@ def each context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -344,6 +394,8 @@ def each context 'and few enough lines' do let(:line_count) { 1 } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -375,6 +427,9 @@ def each context 'adding too many bytes' do let(:file_count) { 10 } let(:line_length) { 5200 } + let(:overflow_max_bytes) { true } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -404,6 +459,11 @@ def each context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -437,6 +497,8 @@ def each describe 'empty collection' do subject { Gitlab::Git::DiffCollection.new([]) } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -555,7 +617,7 @@ def each .and_return({ max_files: 2, max_lines: max_lines }) end - it 'prunes diffs by default even little ones' do + it 'prunes diffs by default even little ones and sets collapsed_safe_files true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') @@ -563,6 +625,8 @@ def each expect(d.diff).to eq('') end end + + expect(subject.collapsed_safe_files?).to eq(true) end end @@ -582,7 +646,7 @@ def each .and_return({ max_files: max_files, max_lines: 80 }) end - it 'prunes diffs by default even little ones' do + it 'prunes diffs by default even little ones and sets collapsed_safe_lines true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') @@ -590,26 +654,30 @@ def each expect(d.diff).to eq('') end end + + expect(subject.collapsed_safe_lines?).to eq(true) end end context 'when go over safe limits on bytes' do let(:iterator) do [ - fake_diff(1, 45), - fake_diff(1, 45), - fake_diff(1, 20480), - fake_diff(1, 1) + fake_diff(5, 10), + fake_diff(5000, 10), + fake_diff(5, 10), + fake_diff(5, 10) ] end before do + allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(1.megabyte) + allow(Gitlab::Git::DiffCollection) .to receive(:default_limits) - .and_return({ max_files: max_files, max_lines: 80 }) + .and_return({ max_files: 4, max_lines: 3000 }) end - it 'prunes diffs by default even little ones' do + it 'prunes diffs by default even little ones and sets collapsed_safe_bytes true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') @@ -617,6 +685,8 @@ def each expect(d.diff).to eq('') end end + + expect(subject.collapsed_safe_bytes?).to eq(true) end end end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 9939e680fa982242836060cb1938d16c381fbc46..6bfcfa2128956563400ab35b23905bb6c840d196 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -124,6 +124,7 @@ def sql(query, comments: true) with_them do let(:payload) { { name: name, sql: sql(sql_query, comments: comments), connection: connection } } + let(:record_wal_query) { false } it 'marks the current thread as using the database' do # since it would already have been toggled by other specs diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb index e2b36125b4ea3ebb356947649379524168a899b3..82ca84f06974464cb755cb33db4606d44cf20b7d 100644 --- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb @@ -3,156 +3,33 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqMiddleware::ClientMetrics do - context "with worker attribution" do - subject { described_class.new } + shared_examples "a metrics middleware" do + context "with mocked prometheus" do + let(:enqueued_jobs_metric) { double('enqueued jobs metric', increment: true) } - let(:queue) { :test } - let(:worker_class) { worker.class } - let(:job) { {} } - let(:default_labels) do - { queue: queue.to_s, - worker: worker_class.to_s, - boundary: "", - external_dependencies: "no", - feature_category: "", - urgency: "low" } - end - - shared_examples "a metrics client middleware" do - context "with mocked prometheus" do - let(:enqueued_jobs_metric) { double('enqueued jobs metric', increment: true) } - - before do - allow(Gitlab::Metrics).to receive(:counter).with(described_class::ENQUEUED, anything).and_return(enqueued_jobs_metric) - end - - describe '#call' do - it 'yields block' do - expect { |b| subject.call(worker_class, job, :test, double, &b) }.to yield_control.once - end - - it 'increments enqueued jobs metric with correct labels when worker is a string of the class' do - expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - - subject.call(worker_class.to_s, job, :test, double) { nil } - end - - it 'increments enqueued jobs metric with correct labels' do - expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - - subject.call(worker_class, job, :test, double) { nil } - end - end - end - end - - context "when workers are not attributed" do before do - stub_const('TestNonAttributedWorker', Class.new) - TestNonAttributedWorker.class_eval do - include Sidekiq::Worker - end - end - - it_behaves_like "a metrics client middleware" do - let(:worker) { TestNonAttributedWorker.new } - let(:labels) { default_labels.merge(urgency: "") } - end - end - - context "when a worker is wrapped into ActiveJob" do - before do - stub_const('TestWrappedWorker', Class.new) - TestWrappedWorker.class_eval do - include Sidekiq::Worker - end - end - - it_behaves_like "a metrics client middleware" do - let(:job) do - { - "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, - "wrapped" => TestWrappedWorker - } - end - - let(:worker) { TestWrappedWorker.new } - let(:labels) { default_labels.merge(urgency: "") } - end - end - - context "when workers are attributed" do - def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) - klass = Class.new do - include Sidekiq::Worker - include WorkerAttributes - - urgency urgency if urgency - worker_has_external_dependencies! if external_dependencies - worker_resource_boundary resource_boundary unless resource_boundary == :unknown - feature_category category unless category.nil? - end - stub_const("TestAttributedWorker", klass) - end - - let(:urgency) { nil } - let(:external_dependencies) { false } - let(:resource_boundary) { :unknown } - let(:feature_category) { nil } - let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) } - let(:worker) { worker_class.new } - - context "high urgency" do - it_behaves_like "a metrics client middleware" do - let(:urgency) { :high } - let(:labels) { default_labels.merge(urgency: "high") } - end + allow(Gitlab::Metrics).to receive(:counter).with(described_class::ENQUEUED, anything).and_return(enqueued_jobs_metric) end - context "no urgency" do - it_behaves_like "a metrics client middleware" do - let(:urgency) { :throttled } - let(:labels) { default_labels.merge(urgency: "throttled") } + describe '#call' do + it 'yields block' do + expect { |b| subject.call(worker_class, job, :test, double, &b) }.to yield_control.once end - end - context "external dependencies" do - it_behaves_like "a metrics client middleware" do - let(:external_dependencies) { true } - let(:labels) { default_labels.merge(external_dependencies: "yes") } - end - end + it 'increments enqueued jobs metric with correct labels when worker is a string of the class' do + expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - context "cpu boundary" do - it_behaves_like "a metrics client middleware" do - let(:resource_boundary) { :cpu } - let(:labels) { default_labels.merge(boundary: "cpu") } + subject.call(worker_class.to_s, job, :test, double) { nil } end - end - context "memory boundary" do - it_behaves_like "a metrics client middleware" do - let(:resource_boundary) { :memory } - let(:labels) { default_labels.merge(boundary: "memory") } - end - end + it 'increments enqueued jobs metric with correct labels' do + expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - context "feature category" do - it_behaves_like "a metrics client middleware" do - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(feature_category: "authentication") } - end - end - - context "combined" do - it_behaves_like "a metrics client middleware" do - let(:urgency) { :high } - let(:external_dependencies) { true } - let(:resource_boundary) { :cpu } - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(urgency: "high", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") } + subject.call(worker_class, job, :test, double) { nil } end end end end + + it_behaves_like 'metrics middleware with worker attribution' end diff --git a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb index 2d493a414994a7e843fdf5e2cf6b250f1d354eed..eb9ba50cdcd7d4903ccb21465829a4d0b0be47e9 100644 --- a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb @@ -35,7 +35,6 @@ def perform(*args) :elasticsearch_calls, :elasticsearch_duration_s, :elasticsearch_timed_out_count, - :worker_data_consistency, :mem_objects, :mem_bytes, :mem_mallocs, diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index df273f2598dce579792cbc78a12733363b0475e4..95be76ce351a07a84c81d9615a7b977c9cbc98f1 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -4,309 +4,108 @@ # rubocop: disable RSpec/MultipleMemoizedHelpers RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do - context "with worker attribution" do - subject { described_class.new } + shared_examples "a metrics middleware" do + context "with mocked prometheus" do + include_context 'server metrics with mocked prometheus' - let(:queue) { :test } - let(:worker_class) { worker.class } - let(:job) { {} } - let(:job_status) { :done } - let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } - let(:default_labels) do - { queue: queue.to_s, - worker: worker_class.to_s, - boundary: "", - external_dependencies: "no", - feature_category: "", - urgency: "low" } - end - - shared_examples "a metrics middleware" do - context "with mocked prometheus" do - let(:concurrency_metric) { double('concurrency metric') } - - let(:queue_duration_seconds) { double('queue duration seconds metric') } - let(:completion_seconds_metric) { double('completion seconds metric') } - let(:user_execution_seconds_metric) { double('user execution seconds metric') } - let(:db_seconds_metric) { double('db seconds metric') } - let(:gitaly_seconds_metric) { double('gitaly seconds metric') } - let(:failed_total_metric) { double('failed total metric') } - let(:retried_total_metric) { double('retried total metric') } - let(:redis_requests_total) { double('redis calls total metric') } - let(:running_jobs_metric) { double('running jobs metric') } - let(:redis_seconds_metric) { double('redis seconds metric') } - let(:elasticsearch_seconds_metric) { double('elasticsearch seconds metric') } - let(:elasticsearch_requests_total) { double('elasticsearch calls total metric') } + describe '#initialize' do + it 'sets concurrency metrics' do + expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i) - before do - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_db_seconds, anything, anything, anything).and_return(db_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything).and_return(gitaly_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_redis_requests_duration_seconds, anything, anything, anything).and_return(redis_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_elasticsearch_requests_duration_seconds, anything, anything, anything).and_return(elasticsearch_seconds_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_total, anything).and_return(redis_requests_total) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total) - allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric) - allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric) - - allow(concurrency_metric).to receive(:set) + subject end + end - describe '#initialize' do - it 'sets concurrency metrics' do - expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i) + describe '#call' do + include_context 'server metrics call' - subject - end + it 'yields block' do + expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once end - describe '#call' do - let(:thread_cputime_before) { 1 } - let(:thread_cputime_after) { 2 } - let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before } - - let(:monotonic_time_before) { 11 } - let(:monotonic_time_after) { 20 } - let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before } - - let(:queue_duration_for_job) { 0.01 } - - let(:db_duration) { 3 } - let(:gitaly_duration) { 4 } - - let(:redis_calls) { 2 } - let(:redis_duration) { 0.01 } - - let(:elasticsearch_calls) { 8 } - let(:elasticsearch_duration) { 0.54 } - - let(:instrumentation) do - { - gitaly_duration_s: gitaly_duration, - redis_calls: redis_calls, - redis_duration_s: redis_duration, - elasticsearch_calls: elasticsearch_calls, - elasticsearch_duration_s: elasticsearch_duration - } + it 'calls BackgroundTransaction' do + expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |instance| + expect(instance).to receive(:run) end - before do - allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after) - allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) - allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job) - allow(ActiveRecord::LogSubscriber).to receive(:runtime).and_return(db_duration * 1000) - job[:instrumentation] = instrumentation - - allow(running_jobs_metric).to receive(:increment) - allow(redis_requests_total).to receive(:increment) - allow(elasticsearch_requests_total).to receive(:increment) - allow(queue_duration_seconds).to receive(:observe) - allow(user_execution_seconds_metric).to receive(:observe) - allow(db_seconds_metric).to receive(:observe) - allow(gitaly_seconds_metric).to receive(:observe) - allow(completion_seconds_metric).to receive(:observe) - allow(redis_seconds_metric).to receive(:observe) - allow(elasticsearch_seconds_metric).to receive(:observe) - end - - it 'yields block' do - expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once - end + subject.call(worker, job, :test) {} + end - it 'calls BackgroundTransaction' do - expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |instance| - expect(instance).to receive(:run) - end + it 'sets queue specific metrics' do + expect(running_jobs_metric).to receive(:increment).with(labels, -1) + expect(running_jobs_metric).to receive(:increment).with(labels, 1) + expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job + expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration) + expect(db_seconds_metric).to receive(:observe).with(labels_with_job_status, db_duration) + expect(gitaly_seconds_metric).to receive(:observe).with(labels_with_job_status, gitaly_duration) + expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration) + expect(redis_seconds_metric).to receive(:observe).with(labels_with_job_status, redis_duration) + expect(elasticsearch_seconds_metric).to receive(:observe).with(labels_with_job_status, elasticsearch_duration) + expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls) + expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls) + + subject.call(worker, job, :test) { nil } + end - subject.call(worker, job, :test) {} - end + it 'sets the thread name if it was nil' do + allow(Thread.current).to receive(:name).and_return(nil) + expect(Thread.current).to receive(:name=).with(Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME) - it 'sets queue specific metrics' do - expect(running_jobs_metric).to receive(:increment).with(labels, -1) - expect(running_jobs_metric).to receive(:increment).with(labels, 1) - expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job - expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration) - expect(db_seconds_metric).to receive(:observe).with(labels_with_job_status, db_duration) - expect(gitaly_seconds_metric).to receive(:observe).with(labels_with_job_status, gitaly_duration) - expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration) - expect(redis_seconds_metric).to receive(:observe).with(labels_with_job_status, redis_duration) - expect(elasticsearch_seconds_metric).to receive(:observe).with(labels_with_job_status, elasticsearch_duration) - expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls) - expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls) + subject.call(worker, job, :test) { nil } + end - subject.call(worker, job, :test) { nil } - end + context 'when job_duration is not available' do + let(:queue_duration_for_job) { nil } - it 'sets the thread name if it was nil' do - allow(Thread.current).to receive(:name).and_return(nil) - expect(Thread.current).to receive(:name=).with(Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME) + it 'does not set the queue_duration_seconds histogram' do + expect(queue_duration_seconds).not_to receive(:observe) subject.call(worker, job, :test) { nil } end + end - context 'when job_duration is not available' do - let(:queue_duration_for_job) { nil } - - it 'does not set the queue_duration_seconds histogram' do - expect(queue_duration_seconds).not_to receive(:observe) - - subject.call(worker, job, :test) { nil } - end - end - - context 'when error is raised' do - let(:job_status) { :fail } - - it 'sets sidekiq_jobs_failed_total and reraises' do - expect(failed_total_metric).to receive(:increment).with(labels, 1) - - expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") - end - end - - context 'when job is retried' do - let(:job) { { 'retry_count' => 1 } } + context 'when error is raised' do + let(:job_status) { :fail } - it 'sets sidekiq_jobs_retried_total metric' do - expect(retried_total_metric).to receive(:increment) + it 'sets sidekiq_jobs_failed_total and reraises' do + expect(failed_total_metric).to receive(:increment).with(labels, 1) - subject.call(worker, job, :test) { nil } - end + expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") end end - end - context "with prometheus integrated" do - describe '#call' do - it 'yields block' do - expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once - end + context 'when job is retried' do + let(:job) { { 'retry_count' => 1 } } - context 'when error is raised' do - let(:job_status) { :fail } + it 'sets sidekiq_jobs_retried_total metric' do + expect(retried_total_metric).to receive(:increment) - it 'sets sidekiq_jobs_failed_total and reraises' do - expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") - end + subject.call(worker, job, :test) { nil } end end end end - context "when workers are not attributed" do - before do - stub_const('TestNonAttributedWorker', Class.new) - TestNonAttributedWorker.class_eval do - include Sidekiq::Worker + context "with prometheus integrated" do + describe '#call' do + it 'yields block' do + expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once end - end - let(:worker) { TestNonAttributedWorker.new } - let(:labels) { default_labels.merge(urgency: "") } + context 'when error is raised' do + let(:job_status) { :fail } - it_behaves_like "a metrics middleware" - end - - context "when a worker is wrapped into ActiveJob" do - before do - stub_const('TestWrappedWorker', Class.new) - TestWrappedWorker.class_eval do - include Sidekiq::Worker + it 'sets sidekiq_jobs_failed_total and reraises' do + expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") + end end end - - let(:job) do - { - "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, - "wrapped" => TestWrappedWorker - } - end - - let(:worker) { TestWrappedWorker.new } - let(:worker_class) { TestWrappedWorker } - let(:labels) { default_labels.merge(urgency: "") } - - it_behaves_like "a metrics middleware" - end - - context 'for ActionMailer::MailDeliveryJob' do - let(:job) { { 'class' => ActionMailer::MailDeliveryJob } } - let(:worker) { ActionMailer::MailDeliveryJob.new } - let(:worker_class) { ActionMailer::MailDeliveryJob } - let(:labels) { default_labels.merge(feature_category: 'issue_tracking') } - - it_behaves_like 'a metrics middleware' end + end - context "when workers are attributed" do - def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) - Class.new do - include Sidekiq::Worker - include WorkerAttributes - - urgency urgency if urgency - worker_has_external_dependencies! if external_dependencies - worker_resource_boundary resource_boundary unless resource_boundary == :unknown - feature_category category unless category.nil? - end - end - - let(:urgency) { nil } - let(:external_dependencies) { false } - let(:resource_boundary) { :unknown } - let(:feature_category) { nil } - let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) } - let(:worker) { worker_class.new } - - context "high urgency" do - let(:urgency) { :high } - let(:labels) { default_labels.merge(urgency: "high") } - - it_behaves_like "a metrics middleware" - end - - context "external dependencies" do - let(:external_dependencies) { true } - let(:labels) { default_labels.merge(external_dependencies: "yes") } - - it_behaves_like "a metrics middleware" - end - - context "cpu boundary" do - let(:resource_boundary) { :cpu } - let(:labels) { default_labels.merge(boundary: "cpu") } - - it_behaves_like "a metrics middleware" - end - - context "memory boundary" do - let(:resource_boundary) { :memory } - let(:labels) { default_labels.merge(boundary: "memory") } - - it_behaves_like "a metrics middleware" - end - - context "feature category" do - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(feature_category: "authentication") } - - it_behaves_like "a metrics middleware" - end - - context "combined" do - let(:urgency) { :throttled } - let(:external_dependencies) { true } - let(:resource_boundary) { :cpu } - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(urgency: "throttled", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") } - - it_behaves_like "a metrics middleware" - end - end + it_behaves_like 'metrics middleware with worker attribution' do + let(:job_status) { :done } + let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } end end # rubocop: enable RSpec/MultipleMemoizedHelpers diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb index 755f6004e52ee0b833d576032236df2917cd1a0b..0efdef0c999fac36883f5bd1b46a4f8987dceb0d 100644 --- a/spec/lib/gitlab/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb @@ -69,11 +69,13 @@ def perform(_arg) shared_examples "a server middleware chain" do it "passes through the right server middlewares" do enabled_sidekiq_middlewares.each do |middleware| - expect_any_instance_of(middleware).to receive(:call).with(*middleware_expected_args).once.and_call_original + expect_next_instance_of(middleware) do |middleware_instance| + expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original + end end disabled_sidekiq_middlewares.each do |middleware| - expect_any_instance_of(middleware).not_to receive(:call) + expect(middleware).not_to receive(:new) end worker_class.perform_async(*job_args) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a736d001138848315fc2ed16845c3895f34f4fbb..339dffa507f9f1e5b7d43a7b18d6fb6fdfb371e8 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3758,42 +3758,15 @@ def run_job_without_exception subject.drop! end - context 'when async_add_build_failure_todo flag enabled' do - it 'creates a todo async', :sidekiq_inline do - project.add_developer(user) - - expect_next_instance_of(TodoService) do |todo_service| - expect(todo_service) - .to receive(:merge_request_build_failed).with(merge_request) - end - - subject.drop! - end - - it 'does not create a sync todo' do - project.add_developer(user) - - expect(TodoService).not_to receive(:new) - - subject.drop! - end - end + it 'creates a todo async', :sidekiq_inline do + project.add_developer(user) - context 'when async_add_build_failure_todo flag disabled' do - before do - stub_feature_flags(async_add_build_failure_todo: false) + expect_next_instance_of(TodoService) do |todo_service| + expect(todo_service) + .to receive(:merge_request_build_failed).with(merge_request) end - it 'creates a todo sync' do - project.add_developer(user) - - expect_next_instance_of(TodoService) do |todo_service| - expect(todo_service) - .to receive(:merge_request_build_failed).with(merge_request) - end - - subject.drop! - end + subject.drop! end end diff --git a/spec/services/award_emojis/add_service_spec.rb b/spec/services/award_emojis/add_service_spec.rb index 85c390156146845cb242c39925f98b1e1af06b7d..0fbb785e2d69fd791552d3e00079f16ffa4bc3bc 100644 --- a/spec/services/award_emojis/add_service_spec.rb +++ b/spec/services/award_emojis/add_service_spec.rb @@ -6,6 +6,7 @@ let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:awardable) { create(:note, project: project) } + let(:name) { 'thumbsup' } subject(:service) { described_class.new(awardable, name, user) } diff --git a/spec/services/award_emojis/destroy_service_spec.rb b/spec/services/award_emojis/destroy_service_spec.rb index 2aba078b63885e98f745a21d9d4d3cdec8868b9d..f743de7c59e9811526b319b6a5d0ff425e44dbfe 100644 --- a/spec/services/award_emojis/destroy_service_spec.rb +++ b/spec/services/award_emojis/destroy_service_spec.rb @@ -6,6 +6,7 @@ let_it_be(:user) { create(:user) } let_it_be(:awardable) { create(:note) } let_it_be(:project) { awardable.project } + let(:name) { 'thumbsup' } let!(:award_from_other_user) do create(:award_emoji, name: name, awardable: awardable, user: create(:user)) diff --git a/spec/services/award_emojis/toggle_service_spec.rb b/spec/services/award_emojis/toggle_service_spec.rb index a7feeed50c6de260d0d27a083311c5877eb2da56..74e97c66193d48e7023d79527f01cbc28f4a118d 100644 --- a/spec/services/award_emojis/toggle_service_spec.rb +++ b/spec/services/award_emojis/toggle_service_spec.rb @@ -6,6 +6,7 @@ let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public) } let_it_be(:awardable) { create(:note, project: project) } + let(:name) { 'thumbsup' } subject(:service) { described_class.new(awardable, name, user) } diff --git a/spec/services/environments/auto_stop_service_spec.rb b/spec/services/environments/auto_stop_service_spec.rb index 8e56c7e642cc692ee980de80f0623f572bee0af9..93b1596586f1407daeff64b57eed02a1f45af217 100644 --- a/spec/services/environments/auto_stop_service_spec.rb +++ b/spec/services/environments/auto_stop_service_spec.rb @@ -8,6 +8,7 @@ let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:service) { described_class.new } before_all do @@ -19,6 +20,7 @@ let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:environments) { Environment.all } before_all do diff --git a/spec/services/environments/canary_ingress/update_service_spec.rb b/spec/services/environments/canary_ingress/update_service_spec.rb index 5ba62e7104cfe59bf7f951df74f2581c516d92c6..0e72fff1ed2dec3034cc9fa3eed2ee16c529e61b 100644 --- a/spec/services/environments/canary_ingress/update_service_spec.rb +++ b/spec/services/environments/canary_ingress/update_service_spec.rb @@ -8,6 +8,7 @@ let_it_be(:project, refind: true) { create(:project) } let_it_be(:maintainer) { create(:user) } let_it_be(:reporter) { create(:user) } + let(:user) { maintainer } let(:params) { {} } let(:service) { described_class.new(project, user, params) } diff --git a/spec/services/environments/reset_auto_stop_service_spec.rb b/spec/services/environments/reset_auto_stop_service_spec.rb index cab1bf2cc26b32c8e4588db53ddd4329800abbde..4a0b091c12d5e05e406909fc05fce7b1bcc1b8f4 100644 --- a/spec/services/environments/reset_auto_stop_service_spec.rb +++ b/spec/services/environments/reset_auto_stop_service_spec.rb @@ -6,6 +6,7 @@ let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } } + let(:user) { developer } let(:service) { described_class.new(project, user) } diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb index dd68471d9274e93b17446e66b2d8c69c955eca69..6c08b7db43a0d05d053f76e7f72f703bed8dd5b8 100644 --- a/spec/services/milestones/destroy_service_spec.rb +++ b/spec/services/milestones/destroy_service_spec.rb @@ -22,14 +22,16 @@ def service expect { milestone.reload }.to raise_error ActiveRecord::RecordNotFound end - it 'deletes milestone id from issuables' do - issue = create(:issue, project: project, milestone: milestone) - merge_request = create(:merge_request, source_project: project, milestone: milestone) + context 'with an existing merge request' do + let!(:issue) { create(:issue, project: project, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } - service.execute(milestone) + it 'deletes milestone id from issuables' do + service.execute(milestone) - expect(issue.reload.milestone).to be_nil - expect(merge_request.reload.milestone).to be_nil + expect(issue.reload.milestone).to be_nil + expect(merge_request.reload.milestone).to be_nil + end end it 'logs destroy event' do diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb new file mode 100644 index 0000000000000000000000000000000000000000..73de631e2939f93c951b3701b28767fc5d76fff5 --- /dev/null +++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.shared_context 'server metrics with mocked prometheus' do + let(:concurrency_metric) { double('concurrency metric') } + + let(:queue_duration_seconds) { double('queue duration seconds metric') } + let(:completion_seconds_metric) { double('completion seconds metric') } + let(:user_execution_seconds_metric) { double('user execution seconds metric') } + let(:db_seconds_metric) { double('db seconds metric') } + let(:gitaly_seconds_metric) { double('gitaly seconds metric') } + let(:failed_total_metric) { double('failed total metric') } + let(:retried_total_metric) { double('retried total metric') } + let(:redis_requests_total) { double('redis calls total metric') } + let(:running_jobs_metric) { double('running jobs metric') } + let(:redis_seconds_metric) { double('redis seconds metric') } + let(:elasticsearch_seconds_metric) { double('elasticsearch seconds metric') } + let(:elasticsearch_requests_total) { double('elasticsearch calls total metric') } + + before do + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_db_seconds, anything, anything, anything).and_return(db_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything).and_return(gitaly_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_redis_requests_duration_seconds, anything, anything, anything).and_return(redis_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_elasticsearch_requests_duration_seconds, anything, anything, anything).and_return(elasticsearch_seconds_metric) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_total, anything).and_return(redis_requests_total) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total) + allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric) + allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric) + + allow(concurrency_metric).to receive(:set) + end +end + +RSpec.shared_context 'server metrics call' do + let(:thread_cputime_before) { 1 } + let(:thread_cputime_after) { 2 } + let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before } + + let(:monotonic_time_before) { 11 } + let(:monotonic_time_after) { 20 } + let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before } + + let(:queue_duration_for_job) { 0.01 } + + let(:db_duration) { 3 } + let(:gitaly_duration) { 4 } + + let(:redis_calls) { 2 } + let(:redis_duration) { 0.01 } + + let(:elasticsearch_calls) { 8 } + let(:elasticsearch_duration) { 0.54 } + let(:instrumentation) do + { + gitaly_duration_s: gitaly_duration, + redis_calls: redis_calls, + redis_duration_s: redis_duration, + elasticsearch_calls: elasticsearch_calls, + elasticsearch_duration_s: elasticsearch_duration + } + end + + before do + allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after) + allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) + allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job) + allow(ActiveRecord::LogSubscriber).to receive(:runtime).and_return(db_duration * 1000) + + job[:instrumentation] = instrumentation + job[:gitaly_duration_s] = gitaly_duration + job[:redis_calls] = redis_calls + job[:redis_duration_s] = redis_duration + + job[:elasticsearch_calls] = elasticsearch_calls + job[:elasticsearch_duration_s] = elasticsearch_duration + + allow(running_jobs_metric).to receive(:increment) + allow(redis_requests_total).to receive(:increment) + allow(elasticsearch_requests_total).to receive(:increment) + allow(queue_duration_seconds).to receive(:observe) + allow(user_execution_seconds_metric).to receive(:observe) + allow(db_seconds_metric).to receive(:observe) + allow(gitaly_seconds_metric).to receive(:observe) + allow(completion_seconds_metric).to receive(:observe) + allow(redis_seconds_metric).to receive(:observe) + allow(elasticsearch_seconds_metric).to receive(:observe) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..48dc47e8e9b8e993040b294d6d121d4757ef7b38 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'metrics middleware with worker attribution' do + subject { described_class.new } + + let(:queue) { :test } + let(:worker_class) { worker.class } + let(:job) { {} } + let(:default_labels) do + { queue: queue.to_s, + worker: worker_class.to_s, + boundary: "", + external_dependencies: "no", + feature_category: "", + urgency: "low" } + end + + context "when workers are not attributed" do + before do + stub_const('TestNonAttributedWorker', Class.new) + TestNonAttributedWorker.class_eval do + include Sidekiq::Worker + end + end + + it_behaves_like "a metrics middleware" do + let(:worker) { TestNonAttributedWorker.new } + let(:labels) { default_labels.merge(urgency: "") } + end + end + + context "when a worker is wrapped into ActiveJob" do + before do + stub_const('TestWrappedWorker', Class.new) + TestWrappedWorker.class_eval do + include Sidekiq::Worker + end + end + + it_behaves_like "a metrics middleware" do + let(:job) do + { + "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, + "wrapped" => TestWrappedWorker + } + end + + let(:worker) { TestWrappedWorker.new } + let(:labels) { default_labels.merge(urgency: "") } + end + end + + context "when workers are attributed" do + def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) + klass = Class.new do + include Sidekiq::Worker + include WorkerAttributes + + urgency urgency if urgency + worker_has_external_dependencies! if external_dependencies + worker_resource_boundary resource_boundary unless resource_boundary == :unknown + feature_category category unless category.nil? + end + stub_const("TestAttributedWorker", klass) + end + + let(:urgency) { nil } + let(:external_dependencies) { false } + let(:resource_boundary) { :unknown } + let(:feature_category) { nil } + let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) } + let(:worker) { worker_class.new } + + context "high urgency" do + it_behaves_like "a metrics middleware" do + let(:urgency) { :high } + let(:labels) { default_labels.merge(urgency: "high") } + end + end + + context "no urgency" do + it_behaves_like "a metrics middleware" do + let(:urgency) { :throttled } + let(:labels) { default_labels.merge(urgency: "throttled") } + end + end + + context "external dependencies" do + it_behaves_like "a metrics middleware" do + let(:external_dependencies) { true } + let(:labels) { default_labels.merge(external_dependencies: "yes") } + end + end + + context "cpu boundary" do + it_behaves_like "a metrics middleware" do + let(:resource_boundary) { :cpu } + let(:labels) { default_labels.merge(boundary: "cpu") } + end + end + + context "memory boundary" do + it_behaves_like "a metrics middleware" do + let(:resource_boundary) { :memory } + let(:labels) { default_labels.merge(boundary: "memory") } + end + end + + context "feature category" do + it_behaves_like "a metrics middleware" do + let(:feature_category) { :authentication } + let(:labels) { default_labels.merge(feature_category: "authentication") } + end + end + + context "combined" do + it_behaves_like "a metrics middleware" do + let(:urgency) { :high } + let(:external_dependencies) { true } + let(:resource_boundary) { :cpu } + let(:feature_category) { :authentication } + let(:labels) do + default_labels.merge( + urgency: "high", + external_dependencies: "yes", + boundary: "cpu", + feature_category: "authentication") + end + end + end + end +end diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index 7bf2456c5480202fe35f47b2dba82e03663a7666..1b110ab02b58a190aa072f8bdced96840e246466 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -16,7 +16,9 @@ db_primary_duration_s: record_query ? 0.002 : 0, db_replica_cached_count: 0, db_replica_count: 0, - db_replica_duration_s: 0.0 + db_replica_duration_s: 0.0, + db_primary_wal_count: record_wal_query ? 1 : 0, + db_replica_wal_count: 0 ) elsif db_role == :replica expect(described_class.db_counter_payload).to eq( @@ -28,7 +30,9 @@ db_primary_duration_s: 0.0, db_replica_cached_count: record_cached_query ? 1 : 0, db_replica_count: record_query ? 1 : 0, - db_replica_duration_s: record_query ? 0.002 : 0 + db_replica_duration_s: record_query ? 0.002 : 0, + db_replica_wal_count: record_wal_query ? 1 : 0, + db_primary_wal_count: 0 ) else expect(described_class.db_counter_payload).to eq( @@ -66,6 +70,12 @@ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role end + if record_wal_query + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role + else + expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role + end + subscriber.sql(event) end diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index 638bf8ab12b64d066931697b03ac599caabc70a4..5aca5d686770ca38d8b26232adf4a62b43f0a1c2 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -39,18 +39,6 @@ subject end - - context 'when async_add_build_failure_todo disabled' do - before do - stub_feature_flags(async_add_build_failure_todo: false) - end - - it 'does not add a todo' do - expect(::Ci::MergeRequests::AddTodoWhenBuildFailsWorker).not_to receive(:perform_async) - - subject - end - end end context 'when build has a chat' do diff --git a/workhorse/.gitlab-ci.yml b/workhorse/.gitlab-ci.yml index 69f25fde9689fe0719fe995c5121033e8d9f59cc..60d51b868b70a55af80477066b16b245261e3b3b 100644 --- a/workhorse/.gitlab-ci.yml +++ b/workhorse/.gitlab-ci.yml @@ -10,7 +10,7 @@ workflow: - if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable$/' default: - image: golang:1.13 + image: golang:1.16 tags: - gitlab-org @@ -41,18 +41,14 @@ changelog: - apt-get update && apt-get -y install libimage-exiftool-perl - make test -test using go 1.13: - extends: .test - image: golang:1.13 - -test using go 1.14: - extends: .test - image: golang:1.14 - test using go 1.15: extends: .test image: golang:1.15 +test using go 1.16: + extends: .test + image: golang:1.16 + test:release: rules: - if: '$CI_COMMIT_TAG' diff --git a/workhorse/doc/operations/install.md b/workhorse/doc/operations/install.md index 28efc407515797d1cf09a3cd6802345226b8565c..3bee13e26839b791e8205ea580500cd1ea224858 100644 --- a/workhorse/doc/operations/install.md +++ b/workhorse/doc/operations/install.md @@ -1,6 +1,6 @@ # Installation -To install GitLab Workhorse you need [Go 1.13 or +To install GitLab Workhorse you need [Go 1.15 or newer](https://golang.org/dl) and [GNU Make](https://www.gnu.org/software/make/). diff --git a/workhorse/go.mod b/workhorse/go.mod index ee50ed690aa458225989db0a8974b59cbb4ff375..e565feef37da6e5b0b949eda2d232e310f8b1e33 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -1,6 +1,6 @@ module gitlab.com/gitlab-org/gitlab-workhorse -go 1.13 +go 1.15 require ( github.com/Azure/azure-storage-blob-go v0.11.1-0.20201209121048-6df5d9af221d @@ -34,13 +34,12 @@ require ( golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 golang.org/x/lint v0.0.0-20200302205851-738671d3881b golang.org/x/net v0.0.0-20201224014010-6772e930b67b - golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 // indirect golang.org/x/text v0.3.5 // indirect - golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516 + golang.org/x/tools v0.1.0 google.golang.org/genproto v0.0.0-20210111234610-22ae2b108f89 // indirect google.golang.org/grpc v1.34.1 google.golang.org/grpc/examples v0.0.0-20201226181154-53788aa5dcb4 // indirect - honnef.co/go/tools v0.0.1-2020.1.5 + honnef.co/go/tools v0.1.3 ) // go get tries to enforce semantic version compatibility via module paths. diff --git a/workhorse/go.sum b/workhorse/go.sum index 4796d40638b803da81384e62f38828a203b8337b..260cb9138df3bc9334fd8b1d7cfd39dab8b89abd 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -24,16 +24,13 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.4.0/go.mod h1:NjjGEnxCS3CAKYp+vmALu20QzcqasGodQp48WxJGAYc= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.9.0/go.mod h1:G3o6/kJvEMIEAN5urdkaP4be49WQsjNiykBIto9LFtY= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= @@ -183,7 +180,6 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6ps github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= @@ -214,7 +210,6 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -279,7 +274,6 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= @@ -308,7 +302,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -340,7 +333,6 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -357,7 +349,6 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -446,7 +437,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= @@ -705,7 +695,6 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= @@ -731,7 +720,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -770,7 +758,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -811,7 +798,6 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -819,7 +805,6 @@ golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -827,7 +812,6 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -839,7 +823,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -896,23 +879,19 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435 h1:25AvDqqB9PrNqj1FLf2/70I4W0L19qqoaFq3gjNwbKk= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 h1:DQmQoKxQWtyybCtX/3dIuDBcAhFszqq8YiNeS6sNu1c= -golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -981,11 +960,11 @@ golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4X golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516 h1:E8xavSjXY8LFvcMSu/8Fjztt+SerwKnuAUOdS+aCXUM= golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1020,7 +999,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -1063,7 +1041,6 @@ google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497 h1:jDYzwXmX9tLnuG4sL85HPmE1ruErXOopALp2i/0AHnI= google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210111234610-22ae2b108f89 h1:R2owLnwrU3BdTJ5R9cnHDNsnEmBQ7n5lZjKShnbISe4= google.golang.org/genproto v0.0.0-20210111234610-22ae2b108f89/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -1082,7 +1059,6 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= @@ -1102,7 +1078,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= @@ -1111,7 +1086,6 @@ gopkg.in/DataDog/dd-trace-go.v1 v1.7.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1146,8 +1120,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75k= -honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=