diff --git a/Guardfile b/Guardfile index baaa52bd204..1d9ec406c1d 100644 --- a/Guardfile +++ b/Guardfile @@ -6,7 +6,7 @@ require "guard/rspec/dsl" cmd = ENV['GUARD_CMD'] || (ENV['SPRING'] ? 'spring rspec' : 'bundle exec rspec') -directories %w(app ee lib spec) +directories %w(app ee lib rubocop tooling spec) rspec_context_for = proc do |context_path| OpenStruct.new(to_s: "spec").tap do |rspec| @@ -42,6 +42,8 @@ guard_setup = proc do |context_path| # Ruby files watch(%r{^#{context_path}(lib/.+)\.rb$}) { |m| rspec.spec.call(m[1]) } + watch(%r{^#{context_path}(rubocop/.+)\.rb$}) { |m| rspec.spec.call(m[1]) } + watch(%r{^#{context_path}(tooling/.+)\.rb$}) { |m| rspec.spec.call(m[1]) } # Rails files rails = rails_context_for.call(context_path, %w(erb haml slim)) diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue index bb9feb262f7..3965fd6b0c7 100644 --- a/app/assets/javascripts/issues_list/components/issuable.vue +++ b/app/assets/javascripts/issues_list/components/issuable.vue @@ -37,7 +37,6 @@ export default { openedAgoJira: __('opened %{timeAgoString} by %{user} in Jira'), openedAgoServiceDesk: __('opened %{timeAgoString} by %{email} via %{user}'), }, - inject: ['scopedLabelsAvailable'], components: { IssueAssignees, GlLink, @@ -51,6 +50,7 @@ export default { GlTooltip, SafeHtml, }, + inject: ['scopedLabelsAvailable'], props: { issuable: { type: Object, diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 7ad1538c3bb..da4134ab2c4 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -2,7 +2,6 @@ import ReplyPlaceholder from './discussion_reply_placeholder.vue'; import ResolveDiscussionButton from './discussion_resolve_button.vue'; import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue'; -import JumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { @@ -11,7 +10,6 @@ export default { ReplyPlaceholder, ResolveDiscussionButton, ResolveWithIssueButton, - JumpToNextDiscussionButton, }, mixins: [glFeatureFlagsMixin()], props: { @@ -38,9 +36,6 @@ export default { }, }, computed: { - hideJumpToNextUnresolvedInThreads() { - return this.glFeatures.hideJumpToNextUnresolvedInThreads; - }, resolvableNotes() { return this.discussion.notes.filter((x) => x.resolvable); }, @@ -74,15 +69,5 @@ export default { :url="resolveWithIssuePath" /> -
- -
diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue deleted file mode 100644 index f94d0060b41..00000000000 --- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - 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])