-
diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js
index 83d6ac9fd14..2e8308fe084 100644
--- a/app/assets/javascripts/pages/projects/labels/new/index.js
+++ b/app/assets/javascripts/pages/projects/labels/new/index.js
@@ -1,3 +1,4 @@
import Labels from 'ee_else_ce/labels';
-document.addEventListener('DOMContentLoaded', () => new Labels());
+// eslint-disable-next-line no-new
+new Labels();
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 7d3e7759081..858bdc066c1 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -14,7 +14,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
before_action do
push_frontend_feature_flag(:merge_request_reviewers, @project, default_enabled: true)
push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
- push_frontend_feature_flag(:reviewer_approval_rules, @project)
+ push_frontend_feature_flag(:reviewer_approval_rules, @project, default_enabled: :yaml)
end
def new
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 59f2a1539ef..7bb3d2e6912 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -33,7 +33,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
- push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
@@ -53,7 +52,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
push_frontend_feature_flag(:merge_request_reviewers, @project, default_enabled: true)
push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
- push_frontend_feature_flag(:reviewer_approval_rules, @project)
+ push_frontend_feature_flag(:reviewer_approval_rules, @project, default_enabled: :yaml)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 759181bd3cb..02726c4354e 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -147,3 +147,5 @@ module Emails
end
end
end
+
+Emails::Members.prepend_if_ee('EE::Emails::Members')
diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb
index 68f281f825e..2919d466073 100644
--- a/app/mailers/previews/devise_mailer_preview.rb
+++ b/app/mailers/previews/devise_mailer_preview.rb
@@ -31,6 +31,6 @@ class DeviseMailerPreview < ActionMailer::Preview
private
def unsaved_user
- User.new(name: 'Jane Doe', email: 'jdoe@example.com')
+ User.new(name: 'Jane Doe', email: 'jdoe@example.com', created_at: 1.minute.ago)
end
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 2bbcdbbe5ce..dc300e8f540 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -62,7 +62,9 @@ class GroupMember < Member
end
def post_create_hook
- run_after_commit_or_now { notification_service.new_group_member(self) }
+ if send_welcome_email?
+ run_after_commit_or_now { notification_service.new_group_member(self) }
+ end
super
end
@@ -87,6 +89,10 @@ class GroupMember < Member
super
end
+
+ def send_welcome_email?
+ true
+ end
end
GroupMember.prepend_if_ee('EE::GroupMember')
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index e329a094319..855fc1d0893 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -3,6 +3,7 @@
class Wiki
extend ::Gitlab::Utils::Override
include HasRepository
+ include CanHousekeepRepository
include Gitlab::Utils::StrongMemoize
include GlobalID::Identification
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 11f33287cd9..cef16b1881e 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -44,6 +44,7 @@
%small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
.nav-controls
= render_if_exists 'admin/users/admin_email_users'
+ = render_if_exists 'admin/users/admin_export_user_permissions'
= link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn gl-button btn-success btn-search float-right'
.filtered-search-block.row-content-block.border-top-0
diff --git a/app/views/notify/provisioned_member_access_granted_email.erb b/app/views/notify/provisioned_member_access_granted_email.erb
new file mode 100644
index 00000000000..485ee5a5242
--- /dev/null
+++ b/app/views/notify/provisioned_member_access_granted_email.erb
@@ -0,0 +1,14 @@
+<% source_link = member_source.web_url %>
+
+<%= _('An Enterprise User GitLab account has been created for you by your organization:') %>
+<%= _('Username: %{username}') % { username: @user.username } %>
+<%= _('Email: %{email}') % { email: @user.email } %>
+<%= _('GitLab group: %{source_link}').html_safe % { source_link: source_link } %>
+
+
+<%= _('By authenticating with an account tied to an Enterprise e-mail address, it is understood that this account is an Enterprise User. ') %>
+<%= _('To ensure no loss of personal content, an Individual User should create a separate account under their own personal email address, not tied to the Enterprise email domain or name-space.') %>
+<%- unless @user.confirmed? %>
+ <%= _('To get started, click the link below to confirm your account.') %>
+ <%= confirmation_url(@user, confirmation_token: @user.confirmation_token) %>
+<%- end %>
diff --git a/app/views/notify/provisioned_member_access_granted_email.haml b/app/views/notify/provisioned_member_access_granted_email.haml
new file mode 100644
index 00000000000..2f2fd33145a
--- /dev/null
+++ b/app/views/notify/provisioned_member_access_granted_email.haml
@@ -0,0 +1,24 @@
+- source_link = link_to(member_source.human_name, member_source.web_url, target: '_blank', rel: 'noopener noreferrer', class: :highlight)
+- confirmation_link = confirmation_url(@user, confirmation_token: @user.confirmation_token)
+
+%tr
+ %td.text-content
+ %p
+ = _('An Enterprise User GitLab account has been created for you by your organization:')
+ %p
+ = _('Username: %{username}') % { username: @user.username }
+ %br
+ = _('Email: %{email}') % { email: @user.email }
+ %br
+ = _('GitLab group: %{source_link}').html_safe % { source_link: source_link }
+
+%tr
+ %td.text-content
+ %p
+ = _('By authenticating with an account tied to an Enterprise e-mail address, it is understood that this account is an Enterprise User. ')
+ = _('To ensure no loss of personal content, an Individual User should create a separate account under their own personal email address, not tied to the Enterprise email domain or name-space.')
+ - unless @user.confirmed?
+ %p
+ = _('To get started, click the link below to confirm your account.')
+ %p
+ = link_to 'Confirm your account', confirmation_link
diff --git a/changelogs/unreleased/211962-allow-group-owners-to-bypass-sso-enforce.yml b/changelogs/unreleased/211962-allow-group-owners-to-bypass-sso-enforce.yml
new file mode 100644
index 00000000000..01e67f61bca
--- /dev/null
+++ b/changelogs/unreleased/211962-allow-group-owners-to-bypass-sso-enforce.yml
@@ -0,0 +1,5 @@
+---
+title: Allow group owners and auditors to login to SSO-enforced groups without SSO
+merge_request: 50199
+author:
+type: changed
diff --git a/changelogs/unreleased/276018-provisoned-accounts-welcome-email.yml b/changelogs/unreleased/276018-provisoned-accounts-welcome-email.yml
new file mode 100644
index 00000000000..ef37187b061
--- /dev/null
+++ b/changelogs/unreleased/276018-provisoned-accounts-welcome-email.yml
@@ -0,0 +1,5 @@
+---
+title: Add one welcome email for account provisioned by group
+merge_request: 51271
+author:
+type: other
diff --git a/changelogs/unreleased/default-enabled-reviewer-approval-rules.yml b/changelogs/unreleased/default-enabled-reviewer-approval-rules.yml
new file mode 100644
index 00000000000..9d0fef09c2b
--- /dev/null
+++ b/changelogs/unreleased/default-enabled-reviewer-approval-rules.yml
@@ -0,0 +1,5 @@
+---
+title: Enable reviewer_approval_rules by default
+merge_request: 51183
+author:
+type: changed
diff --git a/changelogs/unreleased/drop-tmp-index-on-emails-again.yml b/changelogs/unreleased/drop-tmp-index-on-emails-again.yml
new file mode 100644
index 00000000000..a198bae26a7
--- /dev/null
+++ b/changelogs/unreleased/drop-tmp-index-on-emails-again.yml
@@ -0,0 +1,5 @@
+---
+title: Drop tmp_index_for_email_unconfirmation index from the emails table again
+merge_request: 51440
+author:
+type: other
diff --git a/changelogs/unreleased/mc-backstage-create-artifact-expiry-backfill-migration.yml b/changelogs/unreleased/mc-backstage-create-artifact-expiry-backfill-migration.yml
new file mode 100644
index 00000000000..04c0a8dd3fb
--- /dev/null
+++ b/changelogs/unreleased/mc-backstage-create-artifact-expiry-backfill-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Backfill artifact expiry date.
+merge_request: 47723
+author:
+type: other
diff --git a/config/feature_flags/development/hide_jump_to_next_unresolved_in_threads.yml b/config/feature_flags/development/hide_jump_to_next_unresolved_in_threads.yml
deleted file mode 100644
index 0eda7b1ca4f..00000000000
--- a/config/feature_flags/development/hide_jump_to_next_unresolved_in_threads.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: hide_jump_to_next_unresolved_in_threads
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37873
-rollout_issue_url:
-milestone: '13.3'
-type: development
-group: group::code review
-default_enabled: true
diff --git a/config/feature_flags/development/reviewer_approval_rules.yml b/config/feature_flags/development/reviewer_approval_rules.yml
index 97181ef2a36..78bc52de5b3 100644
--- a/config/feature_flags/development/reviewer_approval_rules.yml
+++ b/config/feature_flags/development/reviewer_approval_rules.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/293742
milestone: '13.7'
type: development
group: group::code review
-default_enabled: false
+default_enabled: true
diff --git a/db/migrate/20210112084512_drop_tmp_index_on_emails_again.rb b/db/migrate/20210112084512_drop_tmp_index_on_emails_again.rb
new file mode 100644
index 00000000000..6c2f9cbcb32
--- /dev/null
+++ b/db/migrate/20210112084512_drop_tmp_index_on_emails_again.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class DropTmpIndexOnEmailsAgain < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ EMAIL_INDEX_NAME = 'tmp_index_for_email_unconfirmation_migration'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name(:emails, EMAIL_INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(:emails, :id, where: 'confirmed_at IS NOT NULL', name: EMAIL_INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20201208175117_schedule_backfilling_artifact_expiry_migration.rb b/db/post_migrate/20201208175117_schedule_backfilling_artifact_expiry_migration.rb
new file mode 100644
index 00000000000..f11c0bbe33a
--- /dev/null
+++ b/db/post_migrate/20201208175117_schedule_backfilling_artifact_expiry_migration.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class ScheduleBackfillingArtifactExpiryMigration < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ SWITCH_DATE = Time.utc(2020, 6, 22).freeze
+ INDEX_NAME = 'expired_artifacts_temp_index'.freeze
+ INDEX_CONDITION = "expire_at IS NULL AND created_at < '#{SWITCH_DATE}'"
+
+ disable_ddl_transaction!
+
+ class JobArtifact < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'ci_job_artifacts'
+
+ scope :without_expiry_date, -> { where(expire_at: nil) }
+ scope :before_switch, -> { where('created_at < ?', SWITCH_DATE) }
+ end
+
+ def up
+ # Create temporary index for expired artifacts
+ # Needs to be removed in a later migration
+ add_concurrent_index(:ci_job_artifacts, %i(id created_at), where: INDEX_CONDITION, name: INDEX_NAME)
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ JobArtifact.without_expiry_date.before_switch,
+ ::Gitlab::BackgroundMigration::BackfillArtifactExpiryDate,
+ 2.minutes,
+ batch_size: 200_000
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name :ci_job_artifacts, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20201208175117 b/db/schema_migrations/20201208175117
new file mode 100644
index 00000000000..caa1bcaedd8
--- /dev/null
+++ b/db/schema_migrations/20201208175117
@@ -0,0 +1 @@
+68971e7f9a722e98d9e611f614b5465de83ff3d4dc8c7a8078ed1db8f21e6590
\ No newline at end of file
diff --git a/db/schema_migrations/20210112084512 b/db/schema_migrations/20210112084512
new file mode 100644
index 00000000000..3091c8e2542
--- /dev/null
+++ b/db/schema_migrations/20210112084512
@@ -0,0 +1 @@
+e8e26d49a8292e31ef0ea88a0262f0386b8deda83658ea4de7d464d79c5428e4
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3d0da282548..209849ec5d0 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -20807,6 +20807,8 @@ CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_and_note_id_index ON epic_user
CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_index ON epic_user_mentions USING btree (epic_id) WHERE (note_id IS NULL);
+CREATE INDEX expired_artifacts_temp_index ON ci_job_artifacts USING btree (id, created_at) WHERE ((expire_at IS NULL) AND (created_at < '2020-06-22 00:00:00+00'::timestamp with time zone));
+
CREATE INDEX finding_links_on_vulnerability_occurrence_id ON vulnerability_finding_links USING btree (vulnerability_occurrence_id);
CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_events_archived USING btree (entity_id, entity_type, id DESC, author_id, created_at);
@@ -23297,8 +23299,6 @@ CREATE INDEX temporary_index_vulnerabilities_on_id ON vulnerabilities USING btre
CREATE UNIQUE INDEX term_agreements_unique_index ON term_agreements USING btree (user_id, term_id);
-CREATE INDEX tmp_index_for_email_unconfirmation_migration ON emails USING btree (id) WHERE (confirmed_at IS NOT NULL);
-
CREATE INDEX tmp_index_oauth_applications_on_id_where_trusted ON oauth_applications USING btree (id) WHERE (trusted = true);
CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING btree (id) WHERE (state <> 2);
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 4fce03c1b8c..6b33c3ef966 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -73,7 +73,7 @@ to your needs:
![Use a `.gitlab-ci.yml` template](img/add_file_template_11_10.png)
-While building your `.gitlab-ci.yml`, you can use the [CI/CD configuration visualization](yaml/visualization.md) to facilitate your writing experience.
+While building your `.gitlab-ci.yml`, you can use the [CI/CD configuration visualization](pipeline_editor/index.md#visualize-ci-configuration) to facilitate your writing experience.
For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide.
diff --git a/doc/ci/yaml/img/ci_config_visualization_hover_v13_7.png b/doc/ci/pipeline_editor/img/ci_config_visualization_hover_v13_7.png
similarity index 100%
rename from doc/ci/yaml/img/ci_config_visualization_hover_v13_7.png
rename to doc/ci/pipeline_editor/img/ci_config_visualization_hover_v13_7.png
diff --git a/doc/ci/yaml/img/ci_config_visualization_v13_7.png b/doc/ci/pipeline_editor/img/ci_config_visualization_v13_7.png
similarity index 100%
rename from doc/ci/yaml/img/ci_config_visualization_v13_7.png
rename to doc/ci/pipeline_editor/img/ci_config_visualization_v13_7.png
diff --git a/doc/ci/pipeline_editor/img/pipeline_editor_commit_v13_8.png b/doc/ci/pipeline_editor/img/pipeline_editor_commit_v13_8.png
new file mode 100644
index 00000000000..cc1f666f319
Binary files /dev/null and b/doc/ci/pipeline_editor/img/pipeline_editor_commit_v13_8.png differ
diff --git a/doc/ci/pipeline_editor/img/pipeline_editor_lint_v13_8.png b/doc/ci/pipeline_editor/img/pipeline_editor_lint_v13_8.png
new file mode 100644
index 00000000000..28d21f71378
Binary files /dev/null and b/doc/ci/pipeline_editor/img/pipeline_editor_lint_v13_8.png differ
diff --git a/doc/ci/pipeline_editor/img/pipeline_editor_validate_v13_8.png b/doc/ci/pipeline_editor/img/pipeline_editor_validate_v13_8.png
new file mode 100644
index 00000000000..a4140d5220a
Binary files /dev/null and b/doc/ci/pipeline_editor/img/pipeline_editor_validate_v13_8.png differ
diff --git a/doc/ci/pipeline_editor/index.md b/doc/ci/pipeline_editor/index.md
new file mode 100644
index 00000000000..5e7802feae4
--- /dev/null
+++ b/doc/ci/pipeline_editor/index.md
@@ -0,0 +1,132 @@
+---
+stage: Verify
+group: Pipeline Authoring
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+type: reference
+---
+
+# Pipeline Editor **(CORE)**
+
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4540) in GitLab 13.8.
+> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-pipeline-editor). **(CORE ONLY)**
+
+WARNING:
+This feature might not be available to you. Check the **version history** note above for details.
+
+The pipeline editor is the primary place to edit the GitLab CI/CD configuration in
+your `.gitlab-ci.yml` file. To access it, go to **CI/CD > Editor**.
+
+From the pipeline editor page you can:
+
+- [Validate](#validate-ci-configuration) your configuration syntax while editing the file.
+- Do a deeper [lint](#lint-ci-configuration) of your configuration, that verifies it with any configuration
+ added with the [`include`](../yaml/README.md#include) keyword.
+- See a [visualization](#visualize-ci-configuration) of the current configuration.
+- [Commit](#commit-changes-to-ci-configuration) the changes to a specific branch.
+
+NOTE:
+You must have already [created a CI/CD configuration file](../quick_start/README.md#create-a-gitlab-ciyml-file)
+to use the editor.
+
+## Validate CI configuration
+
+As you edit your pipeline configuration, it is continually validated against the GitLab CI/CD
+pipeline schema. It checks the syntax of your CI YAML configuration, and also runs
+some basic logical validations.
+
+The result of this validation is shown at the top of the editor page. If your configuration
+is invalid, a tip is shown to help you fix the problem:
+
+![Errors in a CI configuration validation](img/pipeline_editor_validate_v13_8.png)
+
+## Lint CI configuration
+
+To test the validity of your GitLab CI/CD configuration before committing the changes,
+you can use the CI lint tool. To access it, go to **CI/CD > Editor** and select the **Lint** tab.
+
+This tool checks for syntax and logical errors but goes into more detail than the
+automatic [validation](#validate-ci-configuration) in the editor.
+
+The results are updated in real-time. Any changes you make to the configuration are
+reflected in the CI lint. It displays the same results as the existing [CI Lint tool](../lint.md).
+
+![Linting errors in a CI configuration](img/pipeline_editor_lint_v13_8.png)
+
+## Visualize CI configuration
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241722) in GitLab 13.5.
+> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
+> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cicd-configuration-visualization). **(CORE ONLY)**
+
+WARNING:
+This feature might not be available to you. Check the **version history** note above for details.
+
+To see a visualization of your `gitlab-ci.yml` configuration, navigate to **CI/CD > Editor**
+and select the `visualization` tab. The visualization shows all stages and jobs.
+[`needs`](../yaml/README.md#needs) relationships are displayed as lines connecting jobs together, showing the hierarchy of execution:
+
+![CI configuration Visualization](img/ci_config_visualization_v13_7.png)
+
+Hovering on a job highlights its `needs` relationships:
+
+![CI configuration visualization on hover](img/ci_config_visualization_hover_v13_7.png)
+
+If the configuration does not have any `needs` relationships, then no lines are drawn because
+each job depends only on the previous stage being completed successfully.
+
+### Enable or disable CI/CD configuration visualization **(CORE ONLY)**
+
+CI/CD configuration visualization is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:ci_config_visualization_tab)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:ci_config_visualization_tab)
+```
+
+## Commit changes to CI configuration
+
+The commit form appears at the bottom of each tab in the editor so you can commit
+your changes at any time.
+
+When you are satisfied with your changes, add a descriptive commit message and enter
+a branch. The branch field defaults to your project's default branch.
+
+If you enter a new branch name, the **Start a new merge request with these changes**
+checkbox appears. Select it to start a new merge request after you commit the changes.
+
+![The commit form with a new branch](img/pipeline_editor_commit_v13_8.png)
+
+## Enable or disable pipeline editor **(CORE ONLY)**
+
+The pipeline editor is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:ci_pipeline_editor_page)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:ci_pipeline_editor_page)
+```
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index 27cbde9285f..97745f1d01f 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -350,7 +350,7 @@ in the GitLab UI to do this:
![Job artifacts browser button](img/job_artifacts_browser_button.png)
1. While on the details page of a merge request, you can see the download
- icon for each job's artifacts on the right side of the pipeline widget:
+ icon for each job's artifacts on the right side of the merge request widget:
![Job artifacts in Merge Request](img/job_artifacts_merge_request.png)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 7fd88d011b3..e9c85353db3 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -129,7 +129,7 @@ The pipeline starts when the commit is committed.
- To validate your `.gitlab-ci.yml` file, use the
[CI Lint tool](../lint.md), which is available in every project.
-- You can also use [CI/CD configuration visualization](../yaml/visualization.md) to
+- You can also use [CI/CD configuration visualization](../pipeline_editor/index.md#visualize-ci-configuration) to
view a graphical representation of your `.gitlab-ci.yml` file.
- For the complete `.gitlab-ci.yml` syntax, see
[the `.gitlab-ci.yml` reference topic](../yaml/README.md).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index cfc69d473de..19b8f0f1c89 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -3058,8 +3058,6 @@ larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-ci
Job artifacts are only collected for successful jobs by default, and
artifacts are restored after [caches](#cache).
-[Not all executors can use caches](https://docs.gitlab.com/runner/executors/#compatibility-chart).
-
[Read more about artifacts](../pipelines/job_artifacts.md).
#### `artifacts:paths`
diff --git a/doc/ci/yaml/gitlab_ci_yaml.md b/doc/ci/yaml/gitlab_ci_yaml.md
index 602e02dbe6b..e4ede9cf699 100644
--- a/doc/ci/yaml/gitlab_ci_yaml.md
+++ b/doc/ci/yaml/gitlab_ci_yaml.md
@@ -27,7 +27,7 @@ The scripts are grouped into **jobs**, and jobs run as part of a larger
**pipeline**. You can group multiple independent jobs into **stages** that run in a defined order.
You should organize your jobs in a sequence that suits your application and is in accordance with
-the tests you wish to perform. To [visualize](visualization.md) the process, imagine
+the tests you wish to perform. To [visualize](../pipeline_editor/index.md#visualize-ci-configuration) the process, imagine
the scripts you add to jobs are the same as CLI commands you run on your computer.
When you add a `.gitlab-ci.yml` file to your
diff --git a/doc/ci/yaml/visualization.md b/doc/ci/yaml/visualization.md
index 77deae8fb6e..ff3b0456eca 100644
--- a/doc/ci/yaml/visualization.md
+++ b/doc/ci/yaml/visualization.md
@@ -1,52 +1,8 @@
---
-stage: Verify
-group: Pipeline Authoring
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+redirect_to: '../pipeline_editor/index.md#visualize-ci-configuration'
---
-# Visualize your CI/CD configuration
+This document was moved to [another location](../pipeline_editor/index.md#visualize-ci-configuration).
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241722) in GitLab 13.5.
-> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
-> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
-> - It's disabled on GitLab.com.
-> - It's not recommended for production use.
-> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cicd-configuration-visualization). **(CORE ONLY)**
-
-WARNING:
-This feature might not be available to you. Check the **version history** note above for details.
-
-To see a visualization of your `gitlab-ci.yml` configuration, navigate to **CI/CD > Editor**
-and select the `Visualization` tab. The visualization shows all stages and jobs.
-[`needs`](README.md#needs) relationships are displayed as lines connecting jobs together, showing the hierarchy of execution:
-
-![CI Config Visualization](img/ci_config_visualization_v13_7.png)
-
-Hovering on a job highlights its `needs` relationships:
-
-![CI Config Visualization on hover](img/ci_config_visualization_hover_v13_7.png)
-
-If the configuration does not have any `needs` relationships, then no lines are drawn because
-each job depends only on the previous stage being completed successfully.
-
-You can only preview one `gitlab-ci.yml` file at a time. Configuration imported with
-[`includes`](README.md#include) is ignored and not included in the visualization.
-
-## Enable or disable CI/CD configuration visualization **(CORE ONLY)**
-
-CI/CD configuration visualization is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can enable it.
-
-To enable it:
-
-```ruby
-Feature.enable(:ci_config_visualization_tab)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:ci_config_visualization_tab)
-```
+
+
diff --git a/doc/user/discussions/img/threads_resolved.png b/doc/user/discussions/img/threads_resolved.png
deleted file mode 100644
index ffb1233f2ee..00000000000
Binary files a/doc/user/discussions/img/threads_resolved.png and /dev/null differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 945c082bba9..bf3e907bd24 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -90,27 +90,6 @@ When a link of a commit reference is found in a thread inside a merge
request, it will be automatically converted to a link in the context of the
current merge request.
-### Jumping between unresolved threads (deprecated)
-
-> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/199718) in GitLab 13.3.
-> - This button's removal is behind a feature flag enabled by default.
-> - For GitLab self-managed instances, GitLab administrators with access to the
- [GitLab Rails console](../../administration/feature_flags.md) can opt to disable it by running
- `Feature.disable(:hide_jump_to_next_unresolved_in_threads)` (for the instance) or
- `Feature.disable(:hide_jump_to_next_unresolved_in_threads, Project.find())`
- (per project.) **(CORE ONLY)**
-
-When a merge request has a large number of comments it can be difficult to track
-what remains unresolved. You can jump between unresolved threads with the
-Jump button next to the Reply field on a thread.
-
-You can also use keyboard shortcuts to navigate among threads:
-
-- Use n to jump to the next unresolved thread.
-- Use p to jump to the previous unresolved thread.
-
-!["8/9 threads resolved"](img/threads_resolved.png)
-
### Marking a comment or thread as resolved
You can mark a thread as resolved by clicking the **Resolve thread**
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 7ce91ecb093..8b62f0e297f 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -81,6 +81,7 @@ Please note that the certificate [fingerprint algorithm](#additional-providers-a
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5291) in GitLab 11.8.
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/9255) in GitLab 11.11 with ongoing enforcement in the GitLab UI.
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/292811) in GitLab 13.8, with an updated timeout experience.
+- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/211962) in GitLab 13.8 with allowing group owners to not go through SSO.
With this option enabled, users must go through your group's GitLab single sign-on URL. They may also be added via SCIM, if configured. Users can't be added manually, and may only access project/group resources via the UI by signing in through the SSO URL.
diff --git a/lib/gitlab/background_migration/backfill_artifact_expiry_date.rb b/lib/gitlab/background_migration/backfill_artifact_expiry_date.rb
new file mode 100644
index 00000000000..0a8c203421b
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_artifact_expiry_date.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Backfill expire_at for a range of Ci::JobArtifact
+ class BackfillArtifactExpiryDate
+ include Gitlab::Utils::StrongMemoize
+
+ BATCH_SIZE = 1_000
+ DEFAULT_EXPIRATION_SWITCH_DATE = Date.new(2020, 6, 22).freeze
+ OLD_ARTIFACT_AGE = 15.months
+ OLD_ARTIFACT_EXPIRY_OFFSET = 3.months
+ RECENT_ARTIFACT_EXPIRY_OFFSET = 1.year
+
+ # Ci::JobArtifact model
+ class Ci::JobArtifact < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'ci_job_artifacts'
+
+ scope :between, -> (start_id, end_id) { where(id: start_id..end_id) }
+ scope :before_default_expiration_switch, -> { where('created_at < ?', DEFAULT_EXPIRATION_SWITCH_DATE) }
+ scope :without_expiry_date, -> { where(expire_at: nil) }
+ scope :old, -> { where(self.arel_table[:created_at].lt(OLD_ARTIFACT_AGE.ago)) }
+ scope :recent, -> { where(self.arel_table[:created_at].gt(OLD_ARTIFACT_AGE.ago)) }
+ end
+
+ def perform(start_id, end_id)
+ Ci::JobArtifact.between(start_id, end_id)
+ .without_expiry_date.before_default_expiration_switch
+ .each_batch(of: BATCH_SIZE) do |batch|
+ batch.old.update_all(expire_at: old_artifact_expiry_date)
+ batch.recent.update_all(expire_at: recent_artifact_expiry_date)
+ end
+ end
+
+ private
+
+ def offset_date
+ strong_memoize(:offset_date) do
+ current_date = Time.current
+ target_date = Time.zone.local(current_date.year, current_date.month, 22, 0, 0, 0)
+
+ current_date.day < 22 ? target_date : target_date.next_month
+ end
+ end
+
+ def old_artifact_expiry_date
+ offset_date + OLD_ARTIFACT_EXPIRY_OFFSET
+ end
+
+ def recent_artifact_expiry_date
+ offset_date + RECENT_ARTIFACT_EXPIRY_OFFSET
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index f4ee8ebd47e..56c6fbd96bc 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -10,6 +10,7 @@ variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, sobelow, pmd-apex, kubesec, mobsf"
+ SAST_EXCLUDED_ANALYZERS: ""
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
SAST_ANALYZER_IMAGE_TAG: 2
SCAN_KUBERNETES_MANIFESTS: "false"
@@ -44,6 +45,8 @@ bandit-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /bandit/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /bandit/
exists:
@@ -58,6 +61,8 @@ brakeman-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /brakeman/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /brakeman/
exists:
@@ -72,6 +77,8 @@ eslint-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /eslint/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /eslint/
exists:
@@ -90,6 +97,8 @@ flawfinder-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /flawfinder/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /flawfinder/
exists:
@@ -105,6 +114,8 @@ kubesec-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /kubesec/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /kubesec/ &&
$SCAN_KUBERNETES_MANIFESTS == 'true'
@@ -118,6 +129,8 @@ gosec-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /gosec/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /gosec/
exists:
@@ -136,6 +149,8 @@ mobsf-android-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /mobsf/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /mobsf/ &&
$SAST_EXPERIMENTAL_FEATURES == 'true'
@@ -155,6 +170,8 @@ mobsf-ios-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /mobsf/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /mobsf/ &&
$SAST_EXPERIMENTAL_FEATURES == 'true'
@@ -170,6 +187,8 @@ nodejs-scan-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /nodejs-scan/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/
exists:
@@ -184,6 +203,8 @@ phpcs-security-audit-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /phpcs-security-audit/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /phpcs-security-audit/
exists:
@@ -198,6 +219,8 @@ pmd-apex-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /pmd-apex/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /pmd-apex/
exists:
@@ -212,6 +235,8 @@ security-code-scan-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /security-code-scan/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /security-code-scan/
exists:
@@ -227,6 +252,8 @@ sobelow-sast:
rules:
- if: $SAST_DISABLED
when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /sobelow/
+ when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /sobelow/
exists:
@@ -239,6 +266,8 @@ spotbugs-sast:
variables:
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
rules:
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /spotbugs/
+ when: never
- if: $SAST_DEFAULT_ANALYZERS =~ /mobsf/ &&
$SAST_EXPERIMENTAL_FEATURES == 'true'
exists:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a96f1f00c43..f3313ee1c7c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2215,6 +2215,9 @@ msgstr ""
msgid "AdminUsers|Delete user and contributions"
msgstr ""
+msgid "AdminUsers|Export permissions as CSV"
+msgstr ""
+
msgid "AdminUsers|External"
msgstr ""
@@ -2994,6 +2997,9 @@ msgstr ""
msgid "An %{link_start}alert%{link_end} with the same fingerprint is already open. To change the status of this alert, resolve the linked alert."
msgstr ""
+msgid "An Enterprise User GitLab account has been created for you by your organization:"
+msgstr ""
+
msgid "An administrator changed the password for your GitLab account on %{link_to}."
msgstr ""
@@ -4897,6 +4903,9 @@ msgstr ""
msgid "By %{user_name}"
msgstr ""
+msgid "By authenticating with an account tied to an Enterprise e-mail address, it is understood that this account is an Enterprise User. "
+msgstr ""
+
msgid "By clicking Register, I agree that I have read and accepted the %{company_name} %{linkStart}Terms of Use and Privacy Policy%{linkEnd}"
msgstr ""
@@ -8797,6 +8806,12 @@ msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again."
msgstr ""
+msgid "DastProfiles|Could not delete saved scan. Please refresh the page, or try again later."
+msgstr ""
+
+msgid "DastProfiles|Could not delete saved scans:"
+msgstr ""
+
msgid "DastProfiles|Could not delete scanner profile. Please refresh the page, or try again later."
msgstr ""
@@ -8809,6 +8824,9 @@ msgstr ""
msgid "DastProfiles|Could not delete site profiles:"
msgstr ""
+msgid "DastProfiles|Could not fetch saved scans. Please refresh the page, or try again later."
+msgstr ""
+
msgid "DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
@@ -8821,6 +8839,9 @@ msgstr ""
msgid "DastProfiles|Could not update the site profile. Please try again."
msgstr ""
+msgid "DastProfiles|DAST Scan"
+msgstr ""
+
msgid "DastProfiles|Debug messages"
msgstr ""
@@ -8863,7 +8884,7 @@ msgstr ""
msgid "DastProfiles|Include debug messages in the DAST console output."
msgstr ""
-msgid "DastProfiles|Manage Profiles"
+msgid "DastProfiles|Manage DAST scans"
msgstr ""
msgid "DastProfiles|Manage profiles"
@@ -8875,9 +8896,6 @@ msgstr ""
msgid "DastProfiles|Minimum = 1 second, Maximum = 3600 seconds"
msgstr ""
-msgid "DastProfiles|New Profile"
-msgstr ""
-
msgid "DastProfiles|New scanner profile"
msgstr ""
@@ -8917,6 +8935,12 @@ msgstr ""
msgid "DastProfiles|Save profile"
msgstr ""
+msgid "DastProfiles|Saved Scans"
+msgstr ""
+
+msgid "DastProfiles|Scan"
+msgstr ""
+
msgid "DastProfiles|Scan mode"
msgstr ""
@@ -8944,6 +8968,9 @@ msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
+msgid "DastProfiles|Target"
+msgstr ""
+
msgid "DastProfiles|Target URL"
msgstr ""
@@ -13226,6 +13253,9 @@ msgstr ""
msgid "GitLab for Slack"
msgstr ""
+msgid "GitLab group: %{source_link}"
+msgstr ""
+
msgid "GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way Development, Security, and Ops teams collaborate"
msgstr ""
@@ -17658,9 +17688,6 @@ msgstr ""
msgid "MergeRequests|Failed to squash. Should be done manually."
msgstr ""
-msgid "MergeRequests|Jump to next unresolved thread"
-msgstr ""
-
msgid "MergeRequests|Reply..."
msgstr ""
@@ -29493,6 +29520,9 @@ msgstr ""
msgid "To define internal users, first enable new users set to external"
msgstr ""
+msgid "To ensure no loss of personal content, an Individual User should create a separate account under their own personal email address, not tied to the Enterprise email domain or name-space."
+msgstr ""
+
msgid "To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method."
msgstr ""
@@ -29502,6 +29532,9 @@ msgstr ""
msgid "To get started you enter your FogBugz URL and login information below. In the next steps, you'll be able to map users and select the projects you want to import."
msgstr ""
+msgid "To get started, click the link below to confirm your account."
+msgstr ""
+
msgid "To get started, link this page to your Jaeger server, or find out how to %{link_start_tag}install Jaeger%{link_end_tag}"
msgstr ""
diff --git a/rubocop/cop/lint/last_keyword_argument.rb b/rubocop/cop/lint/last_keyword_argument.rb
index 0910577ad67..9652c1ace8d 100644
--- a/rubocop/cop/lint/last_keyword_argument.rb
+++ b/rubocop/cop/lint/last_keyword_argument.rb
@@ -52,11 +52,14 @@ module RuboCop
def known_match?(file_path, line_number, method_name)
file_path_from_root = file_path.sub(File.expand_path('../../..', __dir__), '')
+ file_and_line = "#{file_path_from_root}:#{line_number}"
method_name = 'initialize' if method_name == 'new'
- self.class.keyword_warnings.any? do |warning|
- warning.include?("#{file_path_from_root}:#{line_number}") && warning.include?("called method `#{method_name}'")
+ return unless self.class.keyword_warnings[method_name]
+
+ self.class.keyword_warnings[method_name].any? do |warning|
+ warning.include?(file_and_line)
end
end
@@ -69,7 +72,16 @@ module RuboCop
hash.merge!(YAML.safe_load(File.read(file)))
end
- hash.values.flatten.select { |str| str.include?(KEYWORD_DEPRECATION_STR) }.uniq
+ hash.values.flatten.each_with_object({}) do |str, results|
+ next unless str.include?(KEYWORD_DEPRECATION_STR)
+
+ match_data = str.match(/called method `([^\s]+)'/)
+ next unless match_data
+
+ key = match_data[1]
+ results[key] ||= []
+ results[key] << str
+ end
end
end
end
diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb
index e40c6cd7276..bef5b7ad5ee 100755
--- a/scripts/review_apps/automated_cleanup.rb
+++ b/scripts/review_apps/automated_cleanup.rb
@@ -115,6 +115,10 @@ class AutomatedCleanup
delete_helm_releases(releases_to_delete)
end
+ def perform_stale_pvc_cleanup!(days:)
+ kubernetes.cleanup_by_created_at(resource_type: 'pvc', created_before: threshold_time(days: days), wait: false)
+ end
+
private
def fetch_environment(environment)
@@ -155,7 +159,7 @@ class AutomatedCleanup
releases_names = releases.map(&:name)
helm.delete(release_name: releases_names)
- kubernetes.cleanup(release_name: releases_names, wait: false)
+ kubernetes.cleanup_by_release(release_name: releases_names, wait: false)
rescue Tooling::Helm3Client::CommandFailedError => ex
raise ex unless ignore_exception?(ex.message, IGNORED_HELM_ERRORS)
@@ -198,4 +202,8 @@ timed('Helm releases cleanup') do
automated_cleanup.perform_helm_releases_cleanup!(days: 7)
end
+timed('Stale PVC cleanup') do
+ automated_cleanup.perform_stale_pvc_cleanup!(days: 30)
+end
+
exit(0)
diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
index f13196d89b5..1684d26bc89 100644
--- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
+++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
@@ -1,29 +1,58 @@
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue';
+import GetKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql';
import UpdateKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const keepLatestArtifactMock = {
+ data: {
+ project: {
+ ciCdSettings: { keepLatestArtifact: true },
+ },
+ },
+};
+
+const keepLatestArtifactMockResponse = {
+ data: { ciCdSettingsUpdate: { errors: [], __typename: 'CiCdSettingsUpdatePayload' } },
+};
+
describe('Keep latest artifact checkbox', () => {
let wrapper;
+ let apolloProvider;
+ let requestHandlers;
- const mutate = jest.fn().mockResolvedValue();
const fullPath = 'gitlab-org/gitlab';
const helpPagePath = '/help/ci/pipelines/job_artifacts';
const findCheckbox = () => wrapper.find(GlFormCheckbox);
const findHelpLink = () => wrapper.find(GlLink);
- const createComponent = () => {
+ const createComponent = (handlers) => {
+ requestHandlers = {
+ keepLatestArtifactQueryHandler: jest.fn().mockResolvedValue(keepLatestArtifactMock),
+ keepLatestArtifactMutationHandler: jest
+ .fn()
+ .mockResolvedValue(keepLatestArtifactMockResponse),
+ ...handlers,
+ };
+
+ apolloProvider = createMockApollo([
+ [GetKeepLatestArtifactProjectSetting, requestHandlers.keepLatestArtifactQueryHandler],
+ [UpdateKeepLatestArtifactProjectSetting, requestHandlers.keepLatestArtifactMutationHandler],
+ ]);
+
wrapper = shallowMount(KeepLatestArtifactCheckbox, {
provide: {
fullPath,
helpPagePath,
},
- mocks: {
- $apollo: {
- mutate,
- },
- },
+ localVue,
+ apolloProvider,
});
};
@@ -34,6 +63,7 @@ describe('Keep latest artifact checkbox', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ apolloProvider = null;
});
it('displays the checkbox and the help link', () => {
@@ -42,21 +72,17 @@ describe('Keep latest artifact checkbox', () => {
});
it('sets correct setting value in checkbox with query result', async () => {
- await wrapper.setData({ keepLatestArtifact: true });
+ await wrapper.vm.$nextTick();
+
expect(wrapper.element).toMatchSnapshot();
});
it('calls mutation on artifact setting change with correct payload', () => {
findCheckbox().vm.$emit('change', false);
- const expected = {
- mutation: UpdateKeepLatestArtifactProjectSetting,
- variables: {
- fullPath,
- keepLatestArtifact: false,
- },
- };
-
- expect(mutate).toHaveBeenCalledWith(expected);
+ expect(requestHandlers.keepLatestArtifactMutationHandler).toHaveBeenCalledWith({
+ fullPath,
+ keepLatestArtifact: false,
+ });
});
});
diff --git a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
deleted file mode 100644
index 13af29821d8..00000000000
--- a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`JumpToNextDiscussionButton matches the snapshot 1`] = `
-
-
-
-`;
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index 32ebcd13b8a..48e569720e9 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -4,7 +4,6 @@ import DiscussionActions from '~/notes/components/discussion_actions.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
-import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
import createStore from '~/notes/stores';
// NOTE: clone mock_data so that it is not accidentally mutated
@@ -21,7 +20,7 @@ const createUnallowedNote = () =>
describe('DiscussionActions', () => {
let wrapper;
- const createComponentFactory = (shallow = true) => (props, options) => {
+ const createComponentFactory = (shallow = true) => (props) => {
const store = createStore();
const mountFn = shallow ? shallowMount : mount;
@@ -35,11 +34,6 @@ describe('DiscussionActions', () => {
shouldShowJumpToNextDiscussion: true,
...props,
},
- provide: {
- glFeatures: {
- hideJumpToNextUnresolvedInThreads: options?.hideJumpToNextUnresolvedInThreads,
- },
- },
});
};
@@ -55,7 +49,6 @@ describe('DiscussionActions', () => {
expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(true);
expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(true);
- expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(true);
});
it('only renders reply placholder if disccusion is not resolvable', () => {
@@ -66,7 +59,6 @@ describe('DiscussionActions', () => {
expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(false);
expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
- expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
});
it('does not render resolve with issue button if resolveWithIssuePath is falsy', () => {
@@ -75,12 +67,6 @@ describe('DiscussionActions', () => {
expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
});
- it('does not render jump to next discussion button if shouldShowJumpToNextDiscussion is false', () => {
- createComponent({ shouldShowJumpToNextDiscussion: false });
-
- expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
- });
-
describe.each`
desc | notes | shouldRender
${'with no notes'} | ${[]} | ${true}
@@ -101,13 +87,6 @@ describe('DiscussionActions', () => {
});
});
- it('does not render jump to next discussion button if feature flag is enabled', () => {
- const createComponent = createComponentFactory();
- createComponent({}, { hideJumpToNextUnresolvedInThreads: true });
-
- expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
- });
-
describe('events handling', () => {
const createComponent = createComponentFactory(false);
diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
deleted file mode 100644
index 8810a9f8c02..00000000000
--- a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
-import { mockTracking } from '../../helpers/tracking_helper';
-
-describe('JumpToNextDiscussionButton', () => {
- const fromDiscussionId = 'abc123';
- let wrapper;
- let trackingSpy;
- let jumpFn;
-
- beforeEach(() => {
- jumpFn = jest.fn();
- wrapper = shallowMount(JumpToNextDiscussionButton, {
- propsData: { fromDiscussionId },
- });
-
- jest.spyOn(wrapper.vm, 'jumpToNextRelativeDiscussion').mockImplementation(jumpFn);
-
- trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('matches the snapshot', () => {
- expect(wrapper.vm.$el).toMatchSnapshot();
- });
-
- it('calls jumpToNextRelativeDiscussion when clicked', () => {
- wrapper.find({ ref: 'button' }).trigger('click');
-
- expect(jumpFn).toHaveBeenCalledWith(fromDiscussionId);
- });
-
- it('sends the correct tracking event when clicked', () => {
- wrapper.find({ ref: 'button' }).trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
- label: 'mr_next_unresolved_thread',
- property: 'click_next_unresolved_thread',
- });
- });
-});
diff --git a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
new file mode 100644
index 00000000000..49fa7b41916
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillArtifactExpiryDate, :migration, schema: 20201111152859 do
+ subject(:perform) { migration.perform(1, 99) }
+
+ let(:migration) { described_class.new }
+ let(:artifact_outside_id_range) { create_artifact!(id: 100, created_at: 1.year.ago, expire_at: nil) }
+ let(:artifact_outside_date_range) { create_artifact!(id: 40, created_at: Time.current, expire_at: nil) }
+ let(:old_artifact) { create_artifact!(id: 10, created_at: 16.months.ago, expire_at: nil) }
+ let(:recent_artifact) { create_artifact!(id: 20, created_at: 1.year.ago, expire_at: nil) }
+ let(:artifact_with_expiry) { create_artifact!(id: 30, created_at: 1.year.ago, expire_at: Time.current + 1.day) }
+
+ before do
+ table(:namespaces).create!(id: 1, name: 'the-namespace', path: 'the-path')
+ table(:projects).create!(id: 1, name: 'the-project', namespace_id: 1)
+ table(:ci_builds).create!(id: 1, allow_failure: false)
+ end
+
+ context 'when current date is before the 22nd' do
+ before do
+ travel_to(Time.zone.local(2020, 1, 1, 0, 0, 0))
+ end
+
+ it 'backfills the expiry date for old artifacts' do
+ expect(old_artifact.reload.expire_at).to eq(nil)
+
+ perform
+
+ expect(old_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2020, 4, 22, 0, 0, 0))
+ end
+
+ it 'backfills the expiry date for recent artifacts' do
+ expect(recent_artifact.reload.expire_at).to eq(nil)
+
+ perform
+
+ expect(recent_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2021, 1, 22, 0, 0, 0))
+ end
+ end
+
+ context 'when current date is after the 22nd' do
+ before do
+ travel_to(Time.zone.local(2020, 1, 23, 0, 0, 0))
+ end
+
+ it 'backfills the expiry date for old artifacts' do
+ expect(old_artifact.reload.expire_at).to eq(nil)
+
+ perform
+
+ expect(old_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2020, 5, 22, 0, 0, 0))
+ end
+
+ it 'backfills the expiry date for recent artifacts' do
+ expect(recent_artifact.reload.expire_at).to eq(nil)
+
+ perform
+
+ expect(recent_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2021, 2, 22, 0, 0, 0))
+ end
+ end
+
+ it 'does not touch artifacts with expiry date' do
+ expect { perform }.not_to change { artifact_with_expiry.reload.expire_at }
+ end
+
+ it 'does not touch artifacts outside id range' do
+ expect { perform }.not_to change { artifact_outside_id_range.reload.expire_at }
+ end
+
+ it 'does not touch artifacts outside date range' do
+ expect { perform }.not_to change { artifact_outside_date_range.reload.expire_at }
+ end
+
+ private
+
+ def create_artifact!(**args)
+ table(:ci_job_artifacts).create!(**args, project_id: 1, job_id: 1, file_type: 1)
+ end
+end
diff --git a/spec/lib/gitlab/config/entry/composable_hash_spec.rb b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
index 15bbf2047c5..f64b39231a3 100644
--- a/spec/lib/gitlab/config/entry/composable_hash_spec.rb
+++ b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do
end
let(:entry) do
- parent_entry = composable_hash_parent_class.new(secrets: config)
+ parent_entry = composable_hash_parent_class.new({ secrets: config })
parent_entry.compose!
parent_entry[:secrets]
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index d4174a34433..783f0a9ccf7 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -58,7 +58,7 @@ EOT
context 'using a diff that is too large' do
it 'prunes the diff' do
- diff = described_class.new(diff: 'a' * 204800)
+ diff = described_class.new({ diff: 'a' * 204800 })
expect(diff.diff).to be_empty
expect(diff).to be_too_large
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 2e82fcf5511..8001d009901 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -42,4 +42,10 @@ RSpec.describe ProjectWiki do
end
end
end
+
+ it_behaves_like 'can housekeep repository' do
+ let_it_be(:resource) { create(:project_wiki) }
+
+ let(:resource_key) { 'project_wikis' }
+ end
end
diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
index c4f3c3aace2..d20078c8904 100644
--- a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
@@ -4,7 +4,8 @@ include:
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2"
SAST_EXCLUDED_PATHS: "spec, executables"
- SAST_DEFAULT_ANALYZERS: "bandit, gosec"
+ SAST_DEFAULT_ANALYZERS: "bandit, brakeman"
+ SAST_EXCLUDED_ANALYZERS: "brakeman"
stages:
- our_custom_security_stage
diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml
new file mode 100644
index 00000000000..c4f3c3aace2
--- /dev/null
+++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml
@@ -0,0 +1,15 @@
+include:
+ - template: SAST.gitlab-ci.yml
+
+variables:
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2"
+ SAST_EXCLUDED_PATHS: "spec, executables"
+ SAST_DEFAULT_ANALYZERS: "bandit, gosec"
+
+stages:
+ - our_custom_security_stage
+sast:
+ stage: our_custom_security_stage
+ variables:
+ SEARCH_MAX_DEPTH: 8
+ SAST_BRAKEMAN_LEVEL: 2
diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml
new file mode 100644
index 00000000000..b665de5f982
--- /dev/null
+++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml
@@ -0,0 +1,14 @@
+include:
+ - template: SAST.gitlab-ci.yml
+
+variables:
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2"
+ SAST_EXCLUDED_PATHS: "spec, executables"
+ SAST_EXCLUDED_ANALYZERS: "brakeman"
+
+stages:
+ - our_custom_security_stage
+sast:
+ stage: our_custom_security_stage
+ variables:
+ SEARCH_MAX_DEPTH: 8
diff --git a/spec/support/shared_contexts/read_ci_configuration_shared_context.rb b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb
index f8f33e2a745..04c50171766 100644
--- a/spec/support/shared_contexts/read_ci_configuration_shared_context.rb
+++ b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb
@@ -5,5 +5,13 @@ RSpec.shared_context 'read ci configuration for sast enabled project' do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast.yml'))
end
+ let_it_be(:gitlab_ci_yml_default_analyzers_content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml'))
+ end
+
+ let_it_be(:gitlab_ci_yml_excluded_analyzers_content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml'))
+ end
+
let_it_be(:project) { create(:project, :repository) }
end
diff --git a/spec/tooling/lib/tooling/kubernetes_client_spec.rb b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
index fdd56aa0189..2511295206c 100644
--- a/spec/tooling/lib/tooling/kubernetes_client_spec.rb
+++ b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
@@ -17,85 +17,112 @@ RSpec.describe Tooling::KubernetesClient do
end
end
- describe '#cleanup' do
+ describe '#cleanup_by_release' do
before do
allow(subject).to receive(:raw_resource_names).and_return(raw_resource_names)
end
+ shared_examples 'a kubectl command to delete resources' do
+ let(:wait) { true }
+ let(:release_names_in_command) { release_name.respond_to?(:join) ? %(-l 'release in (#{release_name.join(', ')})') : %(-l release="#{release_name}") }
+
+ specify do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=#{wait} #{release_names_in_command})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+
+ # We're not verifying the output here, just silencing it
+ expect { subject.cleanup_by_release(release_name: release_name) }.to output.to_stdout
+ end
+ end
+
it 'raises an error if the Kubernetes command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["kubectl delete #{described_class::RESOURCE_LIST} " +
%(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l release="#{release_name}")])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
- expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
+ expect { subject.cleanup_by_release(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
end
- it 'calls kubectl with the correct arguments' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l release="#{release_name}")])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- # We're not verifying the output here, just silencing it
- expect { subject.cleanup(release_name: release_name) }.to output.to_stdout
- end
+ it_behaves_like 'a kubectl command to delete resources'
context 'with multiple releases' do
let(:release_name) { %w[my-release my-release-2] }
- it 'raises an error if the Kubernetes command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})')])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
-
- expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
- end
-
- it 'calls kubectl with the correct arguments' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})')])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- # We're not verifying the output here, just silencing it
- expect { subject.cleanup(release_name: release_name) }.to output.to_stdout
- end
+ it_behaves_like 'a kubectl command to delete resources'
end
context 'with `wait: false`' do
- it 'raises an error if the Kubernetes command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=false -l release="#{release_name}")])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+ let(:wait) { false }
- expect { subject.cleanup(release_name: release_name, wait: false) }.to raise_error(described_class::CommandFailedError)
- end
+ it_behaves_like 'a kubectl command to delete resources'
+ end
+ end
- it 'calls kubectl with the correct arguments' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=false -l release="#{release_name}")])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ describe '#cleanup_by_created_at' do
+ let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
+ let(:resource_type) { 'pvc' }
+ let(:resource_names) { [pod_for_release] }
+ before do
+ allow(subject).to receive(:resource_names_created_before).with(resource_type: resource_type, created_before: two_days_ago).and_return(resource_names)
+ end
+
+ shared_examples 'a kubectl command to delete resources by older than given creation time' do
+ let(:wait) { true }
+ let(:release_names_in_command) { resource_names.join(' ') }
+
+ specify do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})])
+ .with(["kubectl delete #{resource_type} ".squeeze(' ') +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=#{wait} #{release_names_in_command})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
- expect { subject.cleanup(release_name: release_name, wait: false) }.to output.to_stdout
+ expect { subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) }.to output.to_stdout
end
end
+
+ it 'raises an error if the Kubernetes command fails' do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(["kubectl delete #{resource_type} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true #{pod_for_release})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+
+ expect { subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) }.to raise_error(described_class::CommandFailedError)
+ end
+
+ it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+
+ context 'with multiple resource names' do
+ let(:resource_names) { %w[pod-1 pod-2] }
+
+ it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+ end
+
+ context 'with `wait: false`' do
+ let(:wait) { false }
+
+ it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+ end
+
+ context 'with no resource_type given' do
+ let(:resource_type) { nil }
+
+ it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+ end
+
+ context 'with multiple resource_type given' do
+ let(:resource_type) { 'pvc,service' }
+
+ it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+ end
end
describe '#raw_resource_names' do
@@ -108,4 +135,59 @@ RSpec.describe Tooling::KubernetesClient do
expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names)
end
end
+
+ describe '#resource_names_created_before' do
+ let(:three_days_ago) { Time.now - 3600 * 24 * 3 }
+ let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
+ let(:pvc_created_three_days_ago) { 'pvc-created-three-days-ago' }
+ let(:resource_type) { 'pvc' }
+ let(:raw_resources) do
+ {
+ items: [
+ {
+ apiVersion: "v1",
+ kind: "PersistentVolumeClaim",
+ metadata: {
+ creationTimestamp: three_days_ago,
+ name: pvc_created_three_days_ago
+ }
+ },
+ {
+ apiVersion: "v1",
+ kind: "PersistentVolumeClaim",
+ metadata: {
+ creationTimestamp: Time.now,
+ name: 'another-pvc'
+ }
+ }
+ ]
+ }.to_json
+ end
+
+ shared_examples 'a kubectl command to retrieve resource names sorted by creationTimestamp' do
+ specify do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(["kubectl get #{resource_type} ".squeeze(' ') +
+ %(--namespace "#{namespace}" ) +
+ "--sort-by='{.metadata.creationTimestamp}' -o json"])
+ .and_return(Gitlab::Popen::Result.new([], raw_resources, '', double(success?: true)))
+
+ expect(subject.__send__(:resource_names_created_before, resource_type: resource_type, created_before: two_days_ago)).to contain_exactly(pvc_created_three_days_ago)
+ end
+ end
+
+ it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp'
+
+ context 'with no resource_type given' do
+ let(:resource_type) { nil }
+
+ it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp'
+ end
+
+ context 'with multiple resource_type given' do
+ let(:resource_type) { 'pvc,service' }
+
+ it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp'
+ end
+ end
end
diff --git a/tooling/lib/tooling/helm3_client.rb b/tooling/lib/tooling/helm3_client.rb
index d6671688794..3743138f27e 100644
--- a/tooling/lib/tooling/helm3_client.rb
+++ b/tooling/lib/tooling/helm3_client.rb
@@ -66,13 +66,15 @@ module Tooling
%(--output json),
*args
]
- releases = JSON.parse(run_command(command)) # rubocop:disable Gitlab/Json
+
+ response = run_command(command)
+ releases = JSON.parse(response) # rubocop:disable Gitlab/Json
releases.map do |release|
Release.new(*release.values_at(*RELEASE_JSON_ATTRIBUTES))
end
rescue ::JSON::ParserError => ex
- puts "Ignoring this JSON parsing error: #{ex}" # rubocop:disable Rails/Output
+ puts "Ignoring this JSON parsing error: #{ex}\n\nResponse was:\n#{response}" # rubocop:disable Rails/Output
[]
end
diff --git a/tooling/lib/tooling/kubernetes_client.rb b/tooling/lib/tooling/kubernetes_client.rb
index 14b96addf87..f7abc5ac4cf 100644
--- a/tooling/lib/tooling/kubernetes_client.rb
+++ b/tooling/lib/tooling/kubernetes_client.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
+require 'json'
+require 'time'
require_relative '../../../lib/gitlab/popen' unless defined?(Gitlab::Popen)
-require_relative '../../../lib/gitlab/json' unless defined?(Gitlab::JSON)
module Tooling
class KubernetesClient
@@ -14,11 +15,16 @@ module Tooling
@namespace = namespace
end
- def cleanup(release_name:, wait: true)
+ def cleanup_by_release(release_name:, wait: true)
delete_by_selector(release_name: release_name, wait: wait)
delete_by_matching_name(release_name: release_name)
end
+ def cleanup_by_created_at(resource_type:, created_before:, wait: true)
+ resource_names = resource_names_created_before(resource_type: resource_type, created_before: created_before)
+ delete_by_exact_names(resource_type: resource_type, resource_names: resource_names, wait: wait)
+ end
+
private
def delete_by_selector(release_name:, wait:)
@@ -45,6 +51,21 @@ module Tooling
run_command(command)
end
+ def delete_by_exact_names(resource_names:, wait:, resource_type: nil)
+ command = [
+ 'delete',
+ resource_type,
+ %(--namespace "#{namespace}"),
+ '--now',
+ '--ignore-not-found',
+ '--include-uninitialized',
+ %(--wait=#{wait}),
+ resource_names.join(' ')
+ ]
+
+ run_command(command)
+ end
+
def delete_by_matching_name(release_name:)
resource_names = raw_resource_names
command = [
@@ -70,8 +91,26 @@ module Tooling
run_command(command).lines.map(&:strip)
end
+ def resource_names_created_before(resource_type:, created_before:)
+ command = [
+ 'get',
+ resource_type,
+ %(--namespace "#{namespace}"),
+ "--sort-by='{.metadata.creationTimestamp}'",
+ '-o json'
+ ]
+
+ response = run_command(command)
+ JSON.parse(response)['items'] # rubocop:disable Gitlab/Json
+ .map { |resource| resource.dig('metadata', 'name') if Time.parse(resource.dig('metadata', 'creationTimestamp')) < created_before }
+ .compact
+ rescue ::JSON::ParserError => ex
+ puts "Ignoring this JSON parsing error: #{ex}\n\nResponse was:\n#{response}" # rubocop:disable Rails/Output
+ []
+ end
+
def run_command(command)
- final_command = ['kubectl', *command].join(' ')
+ final_command = ['kubectl', *command.compact].join(' ')
puts "Running command: `#{final_command}`" # rubocop:disable Rails/Output
result = Gitlab::Popen.popen_with_detail([final_command])