Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-13 18:10:55 +00:00
parent 9b1b702f0f
commit 716896e8ca
61 changed files with 788 additions and 322 deletions

View file

@ -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))

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -1,3 +1,4 @@
import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
// eslint-disable-next-line no-new
new Labels();

View file

@ -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

View file

@ -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]

View file

@ -147,3 +147,5 @@ module Emails
end
end
end
Emails::Members.prepend_if_ee('EE::Emails::Members')

View file

@ -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

View file

@ -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')

View file

@ -3,6 +3,7 @@
class Wiki
extend ::Gitlab::Utils::Override
include HasRepository
include CanHousekeepRepository
include Gitlab::Utils::StrongMemoize
include GlobalID::Identification

View file

@ -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

View 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 %>

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: Allow group owners and auditors to login to SSO-enforced groups without SSO
merge_request: 50199
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add one welcome email for account provisioned by group
merge_request: 51271
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Enable reviewer_approval_rules by default
merge_request: 51183
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Drop tmp_index_for_email_unconfirmation index from the emails table again
merge_request: 51440
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Backfill artifact expiry date.
merge_request: 47723
author:
type: other

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -0,0 +1 @@
68971e7f9a722e98d9e611f614b5465de83ff3d4dc8c7a8078ed1db8f21e6590

View file

@ -0,0 +1 @@
e8e26d49a8292e31ef0ea88a0262f0386b8deda83658ea4de7d464d79c5428e4

View file

@ -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);

View file

@ -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.

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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)
```

View file

@ -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)

View file

@ -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).

View file

@ -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`

View file

@ -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

View file

@ -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 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -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**

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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 ""

View file

@ -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

View file

@ -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)

View file

@ -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,
});
});
});

View file

@ -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>
`;

View file

@ -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);

View file

@ -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',
});
});
});

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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])