Add latest changes from gitlab-org/gitlab@master
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
!hideJumpToNextUnresolvedInThreads &&
|
||||
discussion.resolvable &&
|
||||
shouldShowJumpToNextDiscussion
|
||||
"
|
||||
class="btn-group discussion-actions ml-sm-2"
|
||||
>
|
||||
<jump-to-next-discussion-button :from-discussion-id="discussion.id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
<script>
|
||||
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import discussionNavigation from '../mixins/discussion_navigation';
|
||||
|
||||
export default {
|
||||
name: 'JumpToNextDiscussionButton',
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [discussionNavigation],
|
||||
props: {
|
||||
fromDiscussionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
ref="button"
|
||||
v-gl-tooltip
|
||||
class="btn btn-default discussion-next-btn"
|
||||
:title="s__('MergeRequests|Jump to next unresolved thread')"
|
||||
data-track-event="click_button"
|
||||
data-track-label="mr_next_unresolved_thread"
|
||||
data-track-property="click_next_unresolved_thread"
|
||||
@click="jumpToNextRelativeDiscussion(fromDiscussionId)"
|
||||
>
|
||||
<gl-icon name="comment-next" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -1,3 +1,4 @@
|
|||
import Labels from 'ee_else_ce/labels';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Labels());
|
||||
// eslint-disable-next-line no-new
|
||||
new Labels();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -147,3 +147,5 @@ module Emails
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Emails::Members.prepend_if_ee('EE::Emails::Members')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class Wiki
|
||||
extend ::Gitlab::Utils::Override
|
||||
include HasRepository
|
||||
include CanHousekeepRepository
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include GlobalID::Identification
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
14
app/views/notify/provisioned_member_access_granted_email.erb
Normal file
|
@ -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 %>
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow group owners and auditors to login to SSO-enforced groups without SSO
|
||||
merge_request: 50199
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add one welcome email for account provisioned by group
|
||||
merge_request: 51271
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable reviewer_approval_rules by default
|
||||
merge_request: 51183
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/drop-tmp-index-on-emails-again.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Drop tmp_index_for_email_unconfirmation index from the emails table again
|
||||
merge_request: 51440
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Backfill artifact expiry date.
|
||||
merge_request: 47723
|
||||
author:
|
||||
type: other
|
|
@ -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
|
|
@ -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
|
||||
|
|
18
db/migrate/20210112084512_drop_tmp_index_on_emails_again.rb
Normal file
|
@ -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
|
|
@ -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
|
1
db/schema_migrations/20201208175117
Normal file
|
@ -0,0 +1 @@
|
|||
68971e7f9a722e98d9e611f614b5465de83ff3d4dc8c7a8078ed1db8f21e6590
|
1
db/schema_migrations/20210112084512
Normal file
|
@ -0,0 +1 @@
|
|||
e8e26d49a8292e31ef0ea88a0262f0386b8deda83658ea4de7d464d79c5428e4
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
BIN
doc/ci/pipeline_editor/img/pipeline_editor_commit_v13_8.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
doc/ci/pipeline_editor/img/pipeline_editor_lint_v13_8.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
doc/ci/pipeline_editor/img/pipeline_editor_validate_v13_8.png
Normal file
After Width: | Height: | Size: 10 KiB |
132
doc/ci/pipeline_editor/index.md
Normal file
|
@ -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)
|
||||
```
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
```
|
||||
<!-- This redirect file can be deleted after 2021-04-13. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
|
||||
|
|
Before Width: | Height: | Size: 3.9 KiB |
|
@ -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(<project id>))`
|
||||
(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 <kbd>n</kbd> to jump to the next unresolved thread.
|
||||
- Use <kbd>p</kbd> 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**
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JumpToNextDiscussionButton matches the snapshot 1`] = `
|
||||
<div
|
||||
class="btn-group"
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
class="btn btn-default discussion-next-btn"
|
||||
data-track-event="click_button"
|
||||
data-track-label="mr_next_unresolved_thread"
|
||||
data-track-property="click_next_unresolved_thread"
|
||||
title="Jump to next unresolved thread"
|
||||
>
|
||||
<gl-icon-stub
|
||||
name="comment-next"
|
||||
size="16"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|