diff options
57 files changed, 715 insertions, 196 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 66f8e86e48d..c742253cb00 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -48d7984d9912c935a2c2abba3b55593cf0be2d8e +de019fc19eeb8bc6a65a6dbd8bf236669c777815 diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 1641da3e009..6ff2af47dd8 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -232,7 +232,7 @@ export function insertMarkdownText({ .join('\n'); } } else if (tag.indexOf(textPlaceholder) > -1) { - textToInsert = tag.replace(textPlaceholder, () => selected); + textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n')); } else { textToInsert = String(startChar) + tag + selected + (wrap ? tag : ''); } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 5173aeb824e..bc4dbf695cf 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -250,6 +250,10 @@ .commit-row-description { display: none; flex: 1; + + a { + color: $blue-600; + } } &.inline-commit { diff --git a/app/controllers/admin/background_migrations_controller.rb b/app/controllers/admin/background_migrations_controller.rb index 65b47308e4c..e21e6fd2dcb 100644 --- a/app/controllers/admin/background_migrations_controller.rb +++ b/app/controllers/admin/background_migrations_controller.rb @@ -29,9 +29,16 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController redirect_back fallback_location: { action: 'index' } end + def retry + migration = batched_migration_class.find(params[:id]) + migration.retry_failed_jobs! if migration.failed? + + redirect_back fallback_location: { action: 'index' } + end + private def batched_migration_class - Gitlab::Database::BackgroundMigration::BatchedMigration + @batched_migration_class ||= Gitlab::Database::BackgroundMigration::BatchedMigration end end diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index 9fb8cd024c5..8c19a691144 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -10,6 +10,9 @@ module Clusters has_many :agent_tokens, class_name: 'Clusters::AgentToken' has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent + has_many :group_authorizations, class_name: 'Clusters::Agents::GroupAuthorization' + has_many :authorized_groups, class_name: '::Group', through: :group_authorizations, source: :group + scope :ordered_by_name, -> { order(:name) } scope :with_name, -> (name) { where(name: name) } diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb new file mode 100644 index 00000000000..96a9997be75 --- /dev/null +++ b/app/models/clusters/agents/group_authorization.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Clusters + module Agents + class GroupAuthorization < ApplicationRecord + self.table_name = 'agent_group_authorizations' + + belongs_to :agent, class_name: 'Clusters::Agent', optional: false + belongs_to :group, class_name: '::Group', optional: false + + validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' } + end + end +end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index a54de3c82d1..107b1914af4 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -201,30 +201,55 @@ class InternalId < ApplicationRecord InternalId.find_by(**scope, usage: usage_value) end + def initial_value(subject, scope) + raise ArgumentError, 'Cannot initialize without init!' unless init + + # `init` computes the maximum based on actual records. We use the + # primary to make sure we have up to date results + Gitlab::Database::LoadBalancing::Session.current.use_primary do + instance = subject.is_a?(::Class) ? nil : subject + + init.call(instance, scope) || 0 + end + end + def usage_value @usage_value ||= InternalId.usages[usage.to_s] end # Create InternalId record for (scope, usage) combination, if it doesn't exist # - # We blindly insert without synchronization. If another process - # was faster in doing this, we'll realize once we hit the unique key constraint - # violation. We can safely roll-back the nested transaction and perform - # a lookup instead to retrieve the record. + # We blindly insert ignoring conflicts on the unique key constraint. + # If another process was faster in doing this, we'll end up with that record + # when we do the lookup after the insert. def create_record - raise ArgumentError, 'Cannot initialize without init!' unless init - - instance = subject.is_a?(::Class) ? nil : subject + if Feature.enabled?(:use_insert_all_in_internal_id, default_enabled: :yaml) + scope[:project].save! if scope[:project] && !scope[:project].persisted? + scope[:namespace].save! if scope[:namespace] && !scope[:namespace].persisted? - subject.transaction(requires_new: true) do - InternalId.create!( - **scope, + attributes = { + project_id: scope[:project]&.id || scope[:project_id], + namespace_id: scope[:namespace]&.id || scope[:namespace_id], usage: usage_value, - last_value: init.call(instance, scope) || 0 - ) + last_value: initial_value(subject, scope) + } + + InternalId.insert_all([attributes]) + + lookup + else + begin + subject.transaction(requires_new: true) do + InternalId.create!( + **scope, + usage: usage_value, + last_value: initial_value(subject, scope) + ) + end + rescue ActiveRecord::RecordNotUnique + lookup + end end - rescue ActiveRecord::RecordNotUnique - lookup end end @@ -247,6 +272,8 @@ class InternalId < ApplicationRecord # init: Proc that accepts the subject and the scope and returns Integer|NilClass attr_reader :subject, :scope, :scope_attrs, :usage, :init + RecordAlreadyExists = Class.new(StandardError) + def initialize(subject, scope, usage, init = nil) @subject = subject @scope = scope @@ -270,10 +297,8 @@ class InternalId < ApplicationRecord return next_iid if next_iid - create_record!(subject, scope, usage, init) do |iid| - iid.last_value += 1 - end - rescue ActiveRecord::RecordNotUnique + create_record!(subject, scope, usage, initial_value(subject, scope) + 1) + rescue RecordAlreadyExists retry end @@ -302,10 +327,8 @@ class InternalId < ApplicationRecord next_iid = update_record!(subject, scope, usage, function) return next_iid if next_iid - create_record!(subject, scope, usage, init) do |object| - object.last_value = [object.last_value, new_value].max - end - rescue ActiveRecord::RecordNotUnique + create_record!(subject, scope, usage, [initial_value(subject, scope), new_value].max) + rescue RecordAlreadyExists retry end @@ -317,27 +340,56 @@ class InternalId < ApplicationRecord stmt.set(arel_table[:last_value] => new_value) stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints - ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value') # rubocop: disable Database/MultipleDatabases + InternalId.connection.insert(stmt, 'Update InternalId', 'last_value') end - def create_record!(subject, scope, usage, init) - raise ArgumentError, 'Cannot initialize without init!' unless init + def create_record!(subject, scope, usage, value) + if Feature.enabled?(:use_insert_all_in_internal_id, default_enabled: :yaml) + scope[:project].save! if scope[:project] && !scope[:project].persisted? + scope[:namespace].save! if scope[:namespace] && !scope[:namespace].persisted? - instance = subject.is_a?(::Class) ? nil : subject + attributes = { + project_id: scope[:project]&.id || scope[:project_id], + namespace_id: scope[:namespace]&.id || scope[:namespace_id], + usage: usage_value, + last_value: value + } - subject.transaction(requires_new: true) do - last_value = init.call(instance, scope) || 0 + result = InternalId.insert_all([attributes]) - internal_id = InternalId.create!(**scope, usage: usage, last_value: last_value) do |subject| - yield subject if block_given? - end + raise RecordAlreadyExists if result.empty? - internal_id.last_value + value + else + begin + subject.transaction(requires_new: true) do + internal_id = InternalId.create!(**scope, usage: usage, last_value: value) + internal_id.last_value + end + rescue ActiveRecord::RecordNotUnique + raise RecordAlreadyExists + end end end def arel_table InternalId.arel_table end + + def initial_value(subject, scope) + raise ArgumentError, 'Cannot initialize without init!' unless init + + # `init` computes the maximum based on actual records. We use the + # primary to make sure we have up to date results + Gitlab::Database::LoadBalancing::Session.current.use_primary do + instance = subject.is_a?(::Class) ? nil : subject + + init.call(instance, scope) || 0 + end + end + + def usage_value + @usage_value ||= InternalId.usages[usage.to_s] + end end end diff --git a/app/models/project.rb b/app/models/project.rb index 81b04e1316c..5804741f6b4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2093,6 +2093,10 @@ class Project < ApplicationRecord # Docker doesn't allow. The proxy expects it to be downcased. value: "#{Gitlab.host_with_port}/#{namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}" ) + variables.append( + key: 'CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX', + value: "#{Gitlab.host_with_port}/#{namespace.full_path.downcase}#{DependencyProxy::URL_SUFFIX}" + ) end end diff --git a/app/services/clusters/agents/refresh_authorization_service.rb b/app/services/clusters/agents/refresh_authorization_service.rb new file mode 100644 index 00000000000..0da012da861 --- /dev/null +++ b/app/services/clusters/agents/refresh_authorization_service.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Clusters + module Agents + class RefreshAuthorizationService + include Gitlab::Utils::StrongMemoize + + AUTHORIZED_GROUP_LIMIT = 100 + + delegate :project, to: :agent, private: true + + def initialize(agent, config:) + @agent = agent + @config = config + end + + def execute + if allowed_group_configurations.present? + group_ids = allowed_group_configurations.map { |config| config.fetch(:group_id) } + + agent.with_lock do + agent.group_authorizations.upsert_all(allowed_group_configurations, unique_by: [:agent_id, :group_id]) + agent.group_authorizations.where.not(group_id: group_ids).delete_all # rubocop: disable CodeReuse/ActiveRecord + end + else + agent.group_authorizations.delete_all(:delete_all) + end + + true + end + + private + + attr_reader :agent, :config + + def allowed_group_configurations + strong_memoize(:allowed_group_configurations) do + group_entries = config.dig('ci_access', 'groups')&.first(AUTHORIZED_GROUP_LIMIT) + + if group_entries + groups_by_path = group_entries.index_by { |config| config.delete('id') } + + allowed_groups.where_full_path_in(groups_by_path.keys).map do |group| + { group_id: group.id, config: groups_by_path[group.full_path] } + end + end + end + end + + def allowed_groups + if project.root_ancestor.group? + project.root_ancestor.self_and_descendants + else + ::Group.none + end + end + end + end +end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 4e52acfcdda..6ee94e014d8 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -54,28 +54,10 @@ module Issues end handle_assignee_changes(issue, old_assignees) - - if issue.previous_changes.include?('confidential') - # don't enqueue immediately to prevent todos removal in case of a mistake - TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential? - create_confidentiality_note(issue) - track_usage_event(:incident_management_incident_change_confidential, current_user.id) - end - - added_labels = issue.labels - old_labels - - if added_labels.present? - notification_service.async.relabeled_issue(issue, added_labels, current_user) - end - + handle_confidential_change(issue) + handle_added_labels(issue, old_labels) handle_milestone_change(issue) - - added_mentions = issue.mentioned_users(current_user) - old_mentioned_users - - if added_mentions.present? - notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user) - end - + handle_added_mentions(issue, old_mentioned_users) handle_severity_change(issue, old_severity) handle_issue_type_change(issue) end @@ -157,6 +139,23 @@ module Issues MergeRequests::CreateFromIssueService.new(project: project, current_user: current_user, mr_params: create_merge_request_params).execute end + def handle_confidential_change(issue) + if issue.previous_changes.include?('confidential') + # don't enqueue immediately to prevent todos removal in case of a mistake + TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential? + create_confidentiality_note(issue) + track_usage_event(:incident_management_incident_change_confidential, current_user.id) + end + end + + def handle_added_labels(issue, old_labels) + added_labels = issue.labels - old_labels + + if added_labels.present? + notification_service.async.relabeled_issue(issue, added_labels, current_user) + end + end + def handle_milestone_change(issue) return unless issue.previous_changes.include?('milestone_id') @@ -185,6 +184,14 @@ module Issues end end + def handle_added_mentions(issue, old_mentioned_users) + added_mentions = issue.mentioned_users(current_user) - old_mentioned_users + + if added_mentions.present? + notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user) + end + end + def handle_severity_change(issue, old_severity) return unless old_severity && issue.severity != old_severity diff --git a/app/validators/json_schemas/cluster_agent_authorization_configuration.json b/app/validators/json_schemas/cluster_agent_authorization_configuration.json new file mode 100644 index 00000000000..f3de0b7043b --- /dev/null +++ b/app/validators/json_schemas/cluster_agent_authorization_configuration.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Cluster Agent configuration for an authorized project or group", + "type": "object", + "additionalProperties": true +} diff --git a/app/views/admin/background_migrations/_migration.html.haml b/app/views/admin/background_migrations/_migration.html.haml index ddb2eb27705..4a7c0083bc7 100644 --- a/app/views/admin/background_migrations/_migration.html.haml +++ b/app/views/admin/background_migrations/_migration.html.haml @@ -17,3 +17,7 @@ = button_to resume_admin_background_migration_path(migration), class: 'gl-button btn btn-icon has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') do = sprite_icon('play', css_class: 'gl-button-icon gl-icon') + - elsif migration.failed? + = button_to retry_admin_background_migration_path(migration), + class: 'gl-button btn btn-icon has-tooltip', title: _('Retry'), 'aria-label' => _('Retry') do + = sprite_icon('retry', css_class: 'gl-button-icon gl-icon') diff --git a/config/feature_flags/development/use_insert_all_in_internal_id.yml b/config/feature_flags/development/use_insert_all_in_internal_id.yml new file mode 100644 index 00000000000..3529a6769a3 --- /dev/null +++ b/config/feature_flags/development/use_insert_all_in_internal_id.yml @@ -0,0 +1,8 @@ +--- +name: use_insert_all_in_internal_id +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68617 +rollout_issue_url: +milestone: '14.3' +type: development +group: group::project management +default_enabled: false diff --git a/config/routes/admin.rb b/config/routes/admin.rb index d7f73354d4c..e3b365ad276 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -93,6 +93,7 @@ namespace :admin do member do post :pause post :resume + post :retry end end diff --git a/db/migrate/20210809014850_create_agent_group_authorizations.rb b/db/migrate/20210809014850_create_agent_group_authorizations.rb new file mode 100644 index 00000000000..43d7e63e0a2 --- /dev/null +++ b/db/migrate/20210809014850_create_agent_group_authorizations.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateAgentGroupAuthorizations < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + def change + create_table :agent_group_authorizations do |t| + t.bigint :group_id, null: false + t.bigint :agent_id, null: false + t.jsonb :config, null: false + + t.index :group_id + t.index [:agent_id, :group_id], unique: true + end + end +end diff --git a/db/migrate/20210809014918_add_agent_group_authorizations_foreign_keys.rb b/db/migrate/20210809014918_add_agent_group_authorizations_foreign_keys.rb new file mode 100644 index 00000000000..2a3a51d0ca9 --- /dev/null +++ b/db/migrate/20210809014918_add_agent_group_authorizations_foreign_keys.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddAgentGroupAuthorizationsForeignKeys < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :agent_group_authorizations, :namespaces, column: :group_id + add_concurrent_foreign_key :agent_group_authorizations, :cluster_agents, column: :agent_id + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :agent_group_authorizations, column: :group_id + end + + with_lock_retries do + remove_foreign_key_if_exists :agent_group_authorizations, column: :agent_id + end + end +end diff --git a/db/migrate/20210818220234_add_default_project_approval_rules_vuln_allowed.rb b/db/migrate/20210818220234_add_default_project_approval_rules_vuln_allowed.rb new file mode 100644 index 00000000000..72d2755effa --- /dev/null +++ b/db/migrate/20210818220234_add_default_project_approval_rules_vuln_allowed.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AddDefaultProjectApprovalRulesVulnAllowed < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DEFAULT_VALUE = 0 + + def up + change_column_default :approval_project_rules, :vulnerabilities_allowed, DEFAULT_VALUE + + update_column_in_batches(:approval_project_rules, :vulnerabilities_allowed, DEFAULT_VALUE) do |table, query| + query.where(table[:vulnerabilities_allowed].eq(nil)) + end + + change_column_null :approval_project_rules, :vulnerabilities_allowed, false + end + + def down + change_column_default :approval_project_rules, :vulnerabilities_allowed, nil + change_column_null :approval_project_rules, :vulnerabilities_allowed, true + end +end diff --git a/db/schema_migrations/20210809014850 b/db/schema_migrations/20210809014850 new file mode 100644 index 00000000000..541d397d169 --- /dev/null +++ b/db/schema_migrations/20210809014850 @@ -0,0 +1 @@ +6f67e2bba5f42d48a9b21f8ab4d9abf4495ef7e0226ea903d51e77eed85ad0cb
\ No newline at end of file diff --git a/db/schema_migrations/20210809014918 b/db/schema_migrations/20210809014918 new file mode 100644 index 00000000000..099f032c76a --- /dev/null +++ b/db/schema_migrations/20210809014918 @@ -0,0 +1 @@ +d282a027d03920a53d49444f54745ab7d2c8bcccc485ac9407ff9dbbef77981f
\ No newline at end of file diff --git a/db/schema_migrations/20210818220234 b/db/schema_migrations/20210818220234 new file mode 100644 index 00000000000..e32f27029cf --- /dev/null +++ b/db/schema_migrations/20210818220234 @@ -0,0 +1 @@ +8d247218468ad383d1a8a2dc67d5e7e67ddad2a33a38203a41e49c4c018adc7e
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 3c06ee0977e..860b2d6f287 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -8958,6 +8958,22 @@ CREATE SEQUENCE abuse_reports_id_seq ALTER SEQUENCE abuse_reports_id_seq OWNED BY abuse_reports.id; +CREATE TABLE agent_group_authorizations ( + id bigint NOT NULL, + group_id bigint NOT NULL, + agent_id bigint NOT NULL, + config jsonb NOT NULL +); + +CREATE SEQUENCE agent_group_authorizations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE agent_group_authorizations_id_seq OWNED BY agent_group_authorizations.id; + CREATE TABLE alert_management_alert_assignees ( id bigint NOT NULL, user_id bigint NOT NULL, @@ -9746,7 +9762,7 @@ CREATE TABLE approval_project_rules ( name character varying NOT NULL, rule_type smallint DEFAULT 0 NOT NULL, scanners text[], - vulnerabilities_allowed smallint, + vulnerabilities_allowed smallint DEFAULT 0 NOT NULL, severity_levels text[] DEFAULT '{}'::text[] NOT NULL ); @@ -20066,6 +20082,8 @@ ALTER SEQUENCE zoom_meetings_id_seq OWNED BY zoom_meetings.id; ALTER TABLE ONLY abuse_reports ALTER COLUMN id SET DEFAULT nextval('abuse_reports_id_seq'::regclass); +ALTER TABLE ONLY agent_group_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_group_authorizations_id_seq'::regclass); + ALTER TABLE ONLY alert_management_alert_assignees ALTER COLUMN id SET DEFAULT nextval('alert_management_alert_assignees_id_seq'::regclass); ALTER TABLE ONLY alert_management_alert_user_mentions ALTER COLUMN id SET DEFAULT nextval('alert_management_alert_user_mentions_id_seq'::regclass); @@ -21190,6 +21208,9 @@ ALTER TABLE ONLY gitlab_partitions_static.product_analytics_events_experimental_ ALTER TABLE ONLY abuse_reports ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id); +ALTER TABLE ONLY agent_group_authorizations + ADD CONSTRAINT agent_group_authorizations_pkey PRIMARY KEY (id); + ALTER TABLE ONLY alert_management_alert_assignees ADD CONSTRAINT alert_management_alert_assignees_pkey PRIMARY KEY (id); @@ -23113,6 +23134,10 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id); +CREATE UNIQUE INDEX index_agent_group_authorizations_on_agent_id_and_group_id ON agent_group_authorizations USING btree (agent_id, group_id); + +CREATE INDEX index_agent_group_authorizations_on_group_id ON agent_group_authorizations USING btree (group_id); + CREATE INDEX index_alert_assignees_on_alert_id ON alert_management_alert_assignees USING btree (alert_id); CREATE UNIQUE INDEX index_alert_assignees_on_user_id_and_alert_id ON alert_management_alert_assignees USING btree (user_id, alert_id); @@ -26293,6 +26318,9 @@ ALTER TABLE ONLY geo_event_log ALTER TABLE ONLY deployments ADD CONSTRAINT fk_289bba3222 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE SET NULL; +ALTER TABLE ONLY agent_group_authorizations + ADD CONSTRAINT fk_2c9f941965 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY ci_freeze_periods ADD CONSTRAINT fk_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -26953,6 +26981,9 @@ ALTER TABLE ONLY dep_ci_build_trace_section_names ALTER TABLE ONLY ci_stages ADD CONSTRAINT fk_fb57e6cc56 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE; +ALTER TABLE ONLY agent_group_authorizations + ADD CONSTRAINT fk_fb70782616 FOREIGN KEY (agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY system_note_metadata ADD CONSTRAINT fk_fbd87415c9 FOREIGN KEY (description_version_id) REFERENCES description_versions(id) ON DELETE SET NULL; diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index b5999a555c9..ab9a548eb6e 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -41,7 +41,8 @@ There are also [Kubernetes-specific deployment variables](../../user/project/clu | `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to the CI/CD configuration file. Defaults to `.gitlab-ci.yml`. Read-only inside a running pipeline. | | `CI_DEBUG_TRACE` | all | 1.7 | `true` if [debug logging (tracing)](index.md#debug-logging) is enabled. | | `CI_DEFAULT_BRANCH` | 12.4 | all | The name of the project's default branch. | -| `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` | 13.7 | all | The image prefix for pulling images through the Dependency Proxy. | +| `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` | 13.7 | all | The top-level group image prefix for pulling images through the Dependency Proxy. | +| `CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX` | 14.3 | all | The direct group image prefix for pulling images through the Dependency Proxy. | | `CI_DEPENDENCY_PROXY_PASSWORD` | 13.7 | all | The password to pull images through the Dependency Proxy. | | `CI_DEPENDENCY_PROXY_SERVER` | 13.7 | all | The server for logging in to the Dependency Proxy. This is equivalent to `$CI_SERVER_HOST:$CI_SERVER_PORT`. | | `CI_DEPENDENCY_PROXY_USER` | 13.7 | all | The username to pull images through the Dependency Proxy. | diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index c76b0a6810f..ad25ec7edbf 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -94,8 +94,11 @@ Proxy. > - The prefix for group names containing uppercase letters was [fixed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54559) in GitLab 13.10. Runners log in to the Dependency Proxy automatically. To pull through -the Dependency Proxy, use the `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` -[predefined CI/CD variable](../../../ci/variables/predefined_variables.md): +the Dependency Proxy, use one of the [predefined variables](../../../ci/variables/predefined_variables.md): + +- `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` pulls through the top-level group. +- `CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX` pulls through the subgroup, or direct group the + project exists in. Example pulling the latest alpine image: @@ -109,7 +112,10 @@ There are other additional predefined CI/CD variables you can also use: - `CI_DEPENDENCY_PROXY_USER`: A CI/CD user for logging in to the Dependency Proxy. - `CI_DEPENDENCY_PROXY_PASSWORD`: A CI/CD password for logging in to the Dependency Proxy. - `CI_DEPENDENCY_PROXY_SERVER`: The server for logging in to the Dependency Proxy. -- `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`: The image prefix for pulling images through the Dependency Proxy. +- `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`: the image prefix for pulling images through the + dependency proxy from the top-level group. +- `CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX`: the image prefix for pulling images through the + dependency proxy from the direct group or subgroup that the project belongs to. `CI_DEPENDENCY_PROXY_SERVER` and `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` include the server port. If you explicitly include the Dependency Proxy diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 7af5c2ad2ee..d1ad3c1feb1 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -100,6 +100,23 @@ module API end end + namespace 'kubernetes/agent_configuration' do + desc 'POST agent configuration' do + detail 'Store configuration for an agent' + end + params do + requires :agent_id, type: Integer, desc: 'ID of the configured Agent' + requires :agent_config, type: JSON, desc: 'Configuration for the Agent' + end + post '/' do + agent = Clusters::Agent.find(params[:agent_id]) + + Clusters::Agents::RefreshAuthorizationService.new(agent, config: params[:agent_config]).execute + + no_content! + end + end + namespace 'kubernetes/usage_metrics' do desc 'POST usage metrics' do detail 'Updates usage metrics for agent' diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb index 03bd02d7554..32765cb6a56 100644 --- a/lib/gitlab/database/background_migration/batched_job.rb +++ b/lib/gitlab/database/background_migration/batched_job.rb @@ -4,6 +4,7 @@ module Gitlab module Database module BackgroundMigration class BatchedJob < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord + include EachBatch include FromUnion self.table_name = :batched_background_migration_jobs diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb index 9d66824da51..d9fc2ea48f6 100644 --- a/lib/gitlab/database/background_migration/batched_migration.rb +++ b/lib/gitlab/database/background_migration/batched_migration.rb @@ -68,6 +68,17 @@ module Gitlab ) end + def retry_failed_jobs! + batched_jobs.failed.each_batch(of: 100) do |batch| + self.class.transaction do + batch.lock.each(&:split_and_retry!) + self.active! + end + end + + self.active! + end + def next_min_value last_job&.max_value&.next || min_value end diff --git a/lib/sidebars/groups/menus/ci_cd_menu.rb b/lib/sidebars/groups/menus/ci_cd_menu.rb index e870bbf5ebc..f5bce57f496 100644 --- a/lib/sidebars/groups/menus/ci_cd_menu.rb +++ b/lib/sidebars/groups/menus/ci_cd_menu.rb @@ -11,11 +11,6 @@ module Sidebars true end - override :link - def link - renderable_items.first.link - end - override :title def title _('CI/CD') diff --git a/lib/sidebars/groups/menus/group_information_menu.rb b/lib/sidebars/groups/menus/group_information_menu.rb index b28cb927ad2..9656811455e 100644 --- a/lib/sidebars/groups/menus/group_information_menu.rb +++ b/lib/sidebars/groups/menus/group_information_menu.rb @@ -13,11 +13,6 @@ module Sidebars true end - override :link - def link - renderable_items.first.link - end - override :title def title context.group.subgroup? ? _('Subgroup information') : _('Group information') diff --git a/lib/sidebars/groups/menus/issues_menu.rb b/lib/sidebars/groups/menus/issues_menu.rb index 95641c09076..5f0254a0529 100644 --- a/lib/sidebars/groups/menus/issues_menu.rb +++ b/lib/sidebars/groups/menus/issues_menu.rb @@ -17,11 +17,6 @@ module Sidebars true end - override :link - def link - issues_group_path(context.group) - end - override :title def title _('Issues') diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb index e46e2820c04..e81e9355e7e 100644 --- a/lib/sidebars/groups/menus/packages_registries_menu.rb +++ b/lib/sidebars/groups/menus/packages_registries_menu.rb @@ -13,11 +13,6 @@ module Sidebars true end - override :link - def link - renderable_items.first.link - end - override :title def title _('Packages & Registries') diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb index 8bc6077d302..f0239ca6a1a 100644 --- a/lib/sidebars/groups/menus/settings_menu.rb +++ b/lib/sidebars/groups/menus/settings_menu.rb @@ -19,11 +19,6 @@ module Sidebars true end - override :link - def link - edit_group_path(context.group) - end - override :title def title _('Settings') diff --git a/lib/sidebars/menu.rb b/lib/sidebars/menu.rb index 3b8872fd572..1af3d024291 100644 --- a/lib/sidebars/menu.rb +++ b/lib/sidebars/menu.rb @@ -33,10 +33,9 @@ module Sidebars has_renderable_items? || menu_with_partial? end - # Menus might have or not a link override :link def link - nil + renderable_items.first&.link end # This method normalizes the information retrieved from the submenus and this menu diff --git a/lib/sidebars/projects/menus/analytics_menu.rb b/lib/sidebars/projects/menus/analytics_menu.rb index 29fd0609596..b13b25d1cfe 100644 --- a/lib/sidebars/projects/menus/analytics_menu.rb +++ b/lib/sidebars/projects/menus/analytics_menu.rb @@ -21,7 +21,7 @@ module Sidebars def link return cycle_analytics_menu_item.link if cycle_analytics_menu_item.render? - renderable_items.first.link + super end override :extra_container_html_options diff --git a/lib/sidebars/projects/menus/ci_cd_menu.rb b/lib/sidebars/projects/menus/ci_cd_menu.rb index f85a9faacd3..67e4209c382 100644 --- a/lib/sidebars/projects/menus/ci_cd_menu.rb +++ b/lib/sidebars/projects/menus/ci_cd_menu.rb @@ -15,11 +15,6 @@ module Sidebars add_item(pipeline_schedules_menu_item) end - override :link - def link - project_pipelines_path(context.project) - end - override :extra_container_html_options def extra_container_html_options { diff --git a/lib/sidebars/projects/menus/deployments_menu.rb b/lib/sidebars/projects/menus/deployments_menu.rb index fa6482562e8..110d78367b9 100644 --- a/lib/sidebars/projects/menus/deployments_menu.rb +++ b/lib/sidebars/projects/menus/deployments_menu.rb @@ -13,11 +13,6 @@ module Sidebars true end - override :link - def link - renderable_items.first.link - end - override :extra_container_html_options def extra_container_html_options { diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb index aad1ce60d0e..e26bb2237e6 100644 --- a/lib/sidebars/projects/menus/infrastructure_menu.rb +++ b/lib/sidebars/projects/menus/infrastructure_menu.rb @@ -15,11 +15,6 @@ module Sidebars true end - override :link - def link - renderable_items.first.link - end - override :extra_container_html_options def extra_container_html_options { diff --git a/lib/sidebars/projects/menus/issues_menu.rb b/lib/sidebars/projects/menus/issues_menu.rb index fd57f21db88..3774bec2f13 100644 --- a/lib/sidebars/projects/menus/issues_menu.rb +++ b/lib/sidebars/projects/menus/issues_menu.rb @@ -18,11 +18,6 @@ module Sidebars true end - override :link - def link - project_issues_path(context.project) - end - override :extra_container_html_options def extra_container_html_options { diff --git a/lib/sidebars/projects/menus/monitor_menu.rb b/lib/sidebars/projects/menus/monitor_menu.rb index 0d7e0776d5b..59554726263 100644 --- a/lib/sidebars/projects/menus/monitor_menu.rb +++ b/lib/sidebars/projects/menus/monitor_menu.rb @@ -19,11 +19,6 @@ module Sidebars true end - override :link - def link - renderable_items.first&.link - end - override :extra_container_html_options def extra_container_html_options { diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb index d49bb680853..f5f0da2992e 100644 --- a/lib/sidebars/projects/menus/packages_registries_menu.rb +++ b/lib/sidebars/projects/menus/packages_registries_menu.rb @@ -13,11 +13,6 @@ module Sidebars true end - override :link - def link - renderable_items.first.link - end - override :title def title _('Packages & Registries') diff --git a/lib/sidebars/projects/menus/project_information_menu.rb b/lib/sidebars/projects/menus/project_information_menu.rb index a5f06ebea20..44b94ee3522 100644 --- a/lib/sidebars/projects/menus/project_information_menu.rb +++ b/lib/sidebars/projects/menus/project_information_menu.rb @@ -13,11 +13,6 @@ module Sidebars true end - override :link - def link - renderable_items.first.link - end - override :extra_container_html_options def extra_container_html_options { class: 'shortcuts-project-information' } diff --git a/lib/sidebars/projects/menus/repository_menu.rb b/lib/sidebars/projects/menus/repository_menu.rb index a784aecc3dc..0a295f0f618 100644 --- a/lib/sidebars/projects/menus/repository_menu.rb +++ b/lib/sidebars/projects/menus/repository_menu.rb @@ -20,11 +20,6 @@ module Sidebars true end - override :link - def link - project_tree_path(context.project) - end - override :extra_container_html_options def extra_container_html_options { diff --git a/lib/sidebars/projects/menus/security_compliance_menu.rb b/lib/sidebars/projects/menus/security_compliance_menu.rb index 5616b466560..9367514cdca 100644 --- a/lib/sidebars/projects/menus/security_compliance_menu.rb +++ b/lib/sidebars/projects/menus/security_compliance_menu.rb @@ -15,11 +15,6 @@ module Sidebars true end - override :link - def link - renderable_items.first&.link - end - override :title def title _('Security & Compliance') diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb index 250143df649..ac41e534842 100644 --- a/lib/sidebars/projects/menus/settings_menu.rb +++ b/lib/sidebars/projects/menus/settings_menu.rb @@ -21,11 +21,6 @@ module Sidebars true end - override :link - def link - edit_project_path(context.project) - end - override :title def title _('Settings') diff --git a/spec/factories/clusters/agents/group_authorizations.rb b/spec/factories/clusters/agents/group_authorizations.rb new file mode 100644 index 00000000000..6ea3668dc66 --- /dev/null +++ b/spec/factories/clusters/agents/group_authorizations.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :agent_group_authorization, class: 'Clusters::Agents::GroupAuthorization' do + association :agent, factory: :cluster_agent + group + + config { { default_namespace: 'production' } } + end +end diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb index 11823195310..94fb3a0314f 100644 --- a/spec/features/admin/admin_sees_background_migrations_spec.rb +++ b/spec/features/admin/admin_sees_background_migrations_spec.rb @@ -10,7 +10,7 @@ RSpec.describe "Admin > Admin sees background migrations" do let_it_be(:finished_migration) { create(:batched_background_migration, table_name: 'finished', status: :finished) } before_all do - create(:batched_background_migration_job, batched_migration: failed_migration, batch_size: 30, status: :succeeded) + create(:batched_background_migration_job, batched_migration: failed_migration, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3) end before do @@ -53,22 +53,35 @@ RSpec.describe "Admin > Admin sees background migrations" do end end - it 'can view failed migrations' do - visit admin_background_migrations_path + context 'when there are failed migrations' do + before do + allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class| + allow(batch_class).to receive(:next_batch).with(anything, anything, batch_min_value: 6, batch_size: 5).and_return([6, 10]) + end + end - within '#content-body' do - tab = find_link 'Failed' - tab.click + it 'can view and retry them' do + visit admin_background_migrations_path - expect(page).to have_current_path(admin_background_migrations_path(tab: 'failed')) - expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo') + within '#content-body' do + tab = find_link 'Failed' + tab.click - expect(page).to have_selector('tbody tr', count: 1) + expect(page).to have_current_path(admin_background_migrations_path(tab: 'failed')) + expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo') + + expect(page).to have_selector('tbody tr', count: 1) + + expect(page).to have_content(failed_migration.job_class_name) + expect(page).to have_content(failed_migration.table_name) + expect(page).to have_content('0.00%') + expect(page).to have_content(failed_migration.status.humanize) - expect(page).to have_content(failed_migration.job_class_name) - expect(page).to have_content(failed_migration.table_name) - expect(page).to have_content('30.00%') - expect(page).to have_content(failed_migration.status.humanize) + click_button('Retry') + expect(page).not_to have_content(failed_migration.job_class_name) + expect(page).not_to have_content(failed_migration.table_name) + expect(page).not_to have_content('0.00%') + end end end diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 9fe2780fde2..beedb9b2eba 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -70,25 +70,6 @@ describe('init markdown', () => { expect(textArea.value).toContain('# Does not parse the `$` currently.'); }); - it('inserts a new line correctly', () => { - const initialValue = ''; - - textArea.value = initialValue; - textArea.selectionStart = 0; - textArea.selectionEnd = 0; - - insertMarkdownText({ - textArea, - text: textArea.value, - tag: '```suggestion:-0+0\n{text}\n```', - blockTag: true, - selected: '# Does not parse the \\n currently.', - wrap: false, - }); - - expect(textArea.value).toContain('# Does not parse the \\n currently.'); - }); - it('inserts the tag on a new line if the current one is not empty', () => { const initialValue = 'some text'; diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb index 3207e97a639..a1c2634f59c 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb @@ -234,6 +234,42 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '#retry_failed_jobs!' do + let(:batched_migration) { create(:batched_background_migration, status: 'failed') } + + subject(:retry_failed_jobs) { batched_migration.retry_failed_jobs! } + + context 'when there are failed migration jobs' do + let!(:batched_background_migration_job) { create(:batched_background_migration_job, batched_migration: batched_migration, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3) } + + before do + allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class| + allow(batch_class).to receive(:next_batch).with(anything, anything, batch_min_value: 6, batch_size: 5).and_return([6, 10]) + end + end + + it 'moves the status of the migration to active' do + retry_failed_jobs + + expect(batched_migration.status).to eql 'active' + end + + it 'changes the number of attempts to 0' do + retry_failed_jobs + + expect(batched_background_migration_job.reload.attempts).to be_zero + end + end + + context 'when there are no failed migration jobs' do + it 'moves the status of the migration to active' do + retry_failed_jobs + + expect(batched_migration.status).to eql 'active' + end + end + end + describe '#job_class_name=' do it_behaves_like 'an attr_writer that demodulizes assigned class names', :job_class_name end diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb index 1db80351e45..eb6a68f1afd 100644 --- a/spec/lib/sidebars/menu_spec.rb +++ b/spec/lib/sidebars/menu_spec.rb @@ -198,4 +198,27 @@ RSpec.describe Sidebars::Menu do end end end + + describe '#link' do + let(:foo_path) { '/foo_path'} + + let(:foo_menu) do + ::Sidebars::MenuItem.new( + title: 'foo', + link: foo_path, + active_routes: {}, + item_id: :foo + ) + end + + it 'returns first visible menu item link' do + menu.add_item(foo_menu) + + expect(menu.link).to eq foo_path + end + + it 'returns nil if there are no visible menu items' do + expect(menu.link).to be_nil + end + end end diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb index 381842be5ab..77efe99aaa9 100644 --- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb @@ -49,25 +49,6 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do end end - describe '#link' do - let(:foo_path) { '/foo_path'} - - let(:foo_menu) do - ::Sidebars::MenuItem.new( - title: 'foo', - link: foo_path, - active_routes: {}, - item_id: :foo - ) - end - - it 'returns first visible item link' do - subject.insert_element_before(subject.renderable_items, subject.renderable_items.first.item_id, foo_menu) - - expect(subject.link).to eq foo_path - end - end - context 'Menu items' do subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } } diff --git a/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb b/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb new file mode 100644 index 00000000000..057e95eb158 --- /dev/null +++ b/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddDefaultProjectApprovalRulesVulnAllowed do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') } + let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) } + let(:approval_project_rules) { table(:approval_project_rules) } + + it 'updates records when vulnerabilities_allowed is nil' do + records_to_migrate = 10 + + records_to_migrate.times do |i| + approval_project_rules.create!(name: "rule #{i}", project_id: project.id) + end + + expect { migrate! } + .to change { approval_project_rules.where(vulnerabilities_allowed: nil).count } + .from(records_to_migrate) + .to(0) + end + + it 'defaults vulnerabilities_allowed to 0' do + approval_project_rule = approval_project_rules.create!(name: "new rule", project_id: project.id) + + expect(approval_project_rule.vulnerabilities_allowed).to be_nil + + migrate! + + expect(approval_project_rule.reload.vulnerabilities_allowed).to eq(0) + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 26abc98656e..290dc7df430 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2632,6 +2632,10 @@ RSpec.describe Ci::Build do value: "#{Gitlab.host_with_port}/#{project.namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}", public: true, masked: false }, + { key: 'CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX', + value: "#{Gitlab.host_with_port}/#{project.namespace.full_path.downcase}#{DependencyProxy::URL_SUFFIX}", + public: true, + masked: false }, { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false }, { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false }, { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false }, diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb index ea7a55480a8..c8fc09565a9 100644 --- a/spec/models/clusters/agent_spec.rb +++ b/spec/models/clusters/agent_spec.rb @@ -9,6 +9,8 @@ RSpec.describe Clusters::Agent do it { is_expected.to belong_to(:project).class_name('::Project') } it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') } it { is_expected.to have_many(:last_used_agent_tokens).class_name('Clusters::AgentToken') } + it { is_expected.to have_many(:group_authorizations).class_name('Clusters::Agents::GroupAuthorization') } + it { is_expected.to have_many(:authorized_groups).through(:group_authorizations) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(63) } diff --git a/spec/models/clusters/agents/group_authorization_spec.rb b/spec/models/clusters/agents/group_authorization_spec.rb new file mode 100644 index 00000000000..2a99fb26e3f --- /dev/null +++ b/spec/models/clusters/agents/group_authorization_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::Agents::GroupAuthorization do + it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required } + it { is_expected.to belong_to(:group).class_name('::Group').required } + + it { expect(described_class).to validate_jsonb_schema(['config']) } +end diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 6aba91d9471..a8a5ac98a5a 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -238,16 +238,48 @@ RSpec.describe InternalId do end end - context 'when the feature flag is disabled' do - stub_feature_flags(generate_iids_without_explicit_locking: false) + context 'when the explicit locking feature flag is disabled' do + before do + stub_feature_flags(generate_iids_without_explicit_locking: false) + end - it_behaves_like 'a monotonically increasing id generator' + context 'when the insert all feature flag is enabled' do + before do + stub_feature_flags(use_insert_all_in_internal_id: true) + end + + it_behaves_like 'a monotonically increasing id generator' + end + + context 'when the insert all feature flag is disabled' do + before do + stub_feature_flags(use_insert_all_in_internal_id: false) + end + + it_behaves_like 'a monotonically increasing id generator' + end end - context 'when the feature flag is enabled' do - stub_feature_flags(generate_iids_without_explicit_locking: true) + context 'when the explicit locking feature flag is enabled' do + before do + stub_feature_flags(generate_iids_without_explicit_locking: true) + end - it_behaves_like 'a monotonically increasing id generator' + context 'when the insert all feature flag is enabled' do + before do + stub_feature_flags(use_insert_all_in_internal_id: true) + end + + it_behaves_like 'a monotonically increasing id generator' + end + + context 'when the insert all feature flag is disabled' do + before do + stub_feature_flags(use_insert_all_in_internal_id: false) + end + + it_behaves_like 'a monotonically increasing id generator' + end end describe '#increment_and_save!' do diff --git a/spec/requests/admin/background_migrations_controller_spec.rb b/spec/requests/admin/background_migrations_controller_spec.rb new file mode 100644 index 00000000000..c7d5d5cae08 --- /dev/null +++ b/spec/requests/admin/background_migrations_controller_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe 'POST #retry' do + let(:migration) { create(:batched_background_migration, status: 'failed') } + + before do + create(:batched_background_migration_job, batched_migration: migration, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3) + + allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class| + allow(batch_class).to receive(:next_batch).with(anything, anything, batch_min_value: 6, batch_size: 5).and_return([6, 10]) + end + end + + subject(:retry_migration) { post retry_admin_background_migration_path(migration) } + + it 'redirects the user to the admin migrations page' do + retry_migration + + expect(response).to redirect_to(admin_background_migrations_path) + end + + it 'retries the migration' do + retry_migration + + expect(migration.reload.status).to eql 'active' + end + + context 'when the migration is not failed' do + let(:migration) { create(:batched_background_migration, status: 'paused') } + + it 'keeps the same migration status' do + expect { retry_migration }.not_to change { migration.reload.status } + end + end + end +end diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 2acf6951d50..14d5b8d4744 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -93,6 +93,44 @@ RSpec.describe API::Internal::Kubernetes do end end + describe 'POST /internal/kubernetes/agent_configuration' do + def send_request(headers: {}, params: {}) + post api('/internal/kubernetes/agent_configuration'), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:agent) { create(:cluster_agent, project: project) } + let_it_be(:config) do + { + ci_access: { + groups: [ + { id: group.full_path, default_namespace: 'production' } + ] + } + } + end + + include_examples 'authorization' + + context 'agent exists' do + it 'configures the agent and returns a 204' do + send_request(params: { agent_id: agent.id, agent_config: config }) + + expect(response).to have_gitlab_http_status(:no_content) + expect(agent.authorized_groups).to contain_exactly(group) + end + end + + context 'agent does not exist' do + it 'returns a 404' do + send_request(params: { agent_id: -1, agent_config: config }) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'GET /internal/kubernetes/agent_info' do def send_request(headers: {}, params: {}) get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) diff --git a/spec/services/clusters/agents/refresh_authorization_service_spec.rb b/spec/services/clusters/agents/refresh_authorization_service_spec.rb new file mode 100644 index 00000000000..8a887b347bb --- /dev/null +++ b/spec/services/clusters/agents/refresh_authorization_service_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::Agents::RefreshAuthorizationService do + describe '#execute' do + let_it_be(:root_ancestor) { create(:group) } + let_it_be(:removed_group) { create(:group, parent: root_ancestor) } + let_it_be(:modified_group) { create(:group, parent: root_ancestor) } + let_it_be(:added_group) { create(:group, parent: root_ancestor) } + + let(:project) { create(:project, namespace: root_ancestor) } + let(:agent) { create(:cluster_agent, project: project) } + + let(:config) do + { + ci_access: { + groups: [ + { id: added_group.full_path, default_namespace: 'default' }, + { id: modified_group.full_path, default_namespace: 'new-namespace' } + ] + } + }.deep_stringify_keys + end + + subject { described_class.new(agent, config: config).execute } + + before do + default_config = { default_namespace: 'default' } + + agent.group_authorizations.create!(group: removed_group, config: default_config) + agent.group_authorizations.create!(group: modified_group, config: default_config) + end + + it 'refreshes authorizations for the agent' do + expect(subject).to be_truthy + expect(agent.authorized_groups).to contain_exactly(added_group, modified_group) + + added_authorization = agent.group_authorizations.find_by(group: added_group) + expect(added_authorization.config).to eq({ 'default_namespace' => 'default' }) + + modified_authorization = agent.group_authorizations.find_by(group: modified_group) + expect(modified_authorization.config).to eq({ 'default_namespace' => 'new-namespace' }) + end + + context 'config contains no groups' do + let(:config) { {} } + + it 'removes all authorizations' do + expect(subject).to be_truthy + expect(agent.authorized_groups).to be_empty + end + end + + context 'config contains groups outside of the configuration project hierarchy' do + let(:project) { create(:project, namespace: create(:group)) } + + it 'removes all authorizations' do + expect(subject).to be_truthy + expect(agent.authorized_groups).to be_empty + end + end + + context 'configuration project does not belong to a group' do + let(:project) { create(:project) } + + it 'removes all authorizations' do + expect(subject).to be_truthy + expect(agent.authorized_groups).to be_empty + end + end + + context 'config contains too many groups' do + before do + stub_const("#{described_class}::AUTHORIZED_GROUP_LIMIT", 1) + end + + it 'authorizes groups up to the limit' do + expect(subject).to be_truthy + expect(agent.authorized_groups).to contain_exactly(added_group) + end + end + end +end |