summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-03 21:08:23 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-03 21:08:23 +0000
commitfc92738a0245f1be88250448bebd9c20e9849444 (patch)
tree5e66f8a08c2b5dfa9cd76d28b0fe0a6a409ca616
parent7484304eaa266f22f048a76d490494b6337c9555 (diff)
downloadgitlab-ce-fc92738a0245f1be88250448bebd9c20e9849444.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS8
-rw-r--r--app/assets/javascripts/authentication/u2f/authenticate.js10
-rw-r--r--app/assets/javascripts/authentication/u2f/index.js4
-rw-r--r--app/assets/javascripts/authentication/u2f/register.js2
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue23
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue15
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss3
-rw-r--r--app/graphql/mutations/container_expiration_policies/update.rb62
-rw-r--r--app/graphql/types/container_expiration_policy_type.rb2
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/services/container_expiration_policies/update_service.rb38
-rw-r--r--app/views/admin/sessions/_two_factor_u2f.html.haml17
-rw-r--r--app/views/admin/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/u2f/_authenticate.html.haml19
-rw-r--r--app/views/u2f/_register.html.haml2
-rwxr-xr-xbin/secpick12
-rw-r--r--changelogs/unreleased/196784-graphql-mutations-for-container-expiration-policies.yml5
-rw-r--r--changelogs/unreleased/219582-fix-ambiguous_string_concat_on_cleanup_projects_with_missing_names.yml5
-rw-r--r--changelogs/unreleased/fix-vertically-center-action-icon.yml5
-rw-r--r--db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb4
-rw-r--r--doc/administration/geo/disaster_recovery/background_verification.md7
-rw-r--r--doc/administration/geo/disaster_recovery/bring_primary_back.md7
-rw-r--r--doc/administration/geo/disaster_recovery/index.md7
-rw-r--r--doc/administration/geo/disaster_recovery/planned_failover.md7
-rw-r--r--doc/administration/geo/replication/configuration.md7
-rw-r--r--doc/administration/geo/replication/database.md7
-rw-r--r--doc/administration/geo/replication/datatypes.md7
-rw-r--r--doc/administration/geo/replication/docker_registry.md7
-rw-r--r--doc/administration/geo/replication/external_database.md7
-rw-r--r--doc/administration/geo/replication/faq.md7
-rw-r--r--doc/administration/geo/replication/geo_validation_tests.md7
-rw-r--r--doc/administration/geo/replication/index.md7
-rw-r--r--doc/administration/geo/replication/location_aware_git_url.md7
-rw-r--r--doc/administration/geo/replication/multiple_servers.md7
-rw-r--r--doc/administration/geo/replication/object_storage.md7
-rw-r--r--doc/administration/geo/replication/remove_geo_node.md7
-rw-r--r--doc/administration/geo/replication/security_review.md7
-rw-r--r--doc/administration/geo/replication/troubleshooting.md7
-rw-r--r--doc/administration/geo/replication/tuning.md7
-rw-r--r--doc/administration/geo/replication/updating_the_geo_nodes.md7
-rw-r--r--doc/administration/geo/replication/using_a_geo_server.md7
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md7
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql58
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json171
-rw-r--r--doc/api/graphql/reference/index.md12
-rw-r--r--doc/development/documentation/styleguide.md5
-rw-r--r--doc/development/testing_guide/best_practices.md2
-rw-r--r--doc/development/testing_guide/index.md2
-rw-r--r--doc/user/project/pages/getting_started/new_or_existing_website.md4
-rw-r--r--doc/user/project/pages/getting_started_part_four.md377
-rw-r--r--locale/gitlab.pot2
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/features/u2f_spec.rb2
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js6
-rw-r--r--spec/frontend/clusters/components/applications_spec.js286
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js1
-rw-r--r--spec/graphql/mutations/container_expiration_policies/update_spec.rb96
-rw-r--r--spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb110
-rw-r--r--spec/services/concerns/exclusive_lease_guard_spec.rb121
-rw-r--r--spec/services/container_expiration_policies/update_service_spec.rb101
-rw-r--r--spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb40
65 files changed, 1328 insertions, 477 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index e71e74fd4d3..0ba21896dbc 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -8,11 +8,15 @@
# Technical writing team are the default reviewers for all markdown docs
/doc/ @gl-docsteam
-# Dev and Doc guidelines
+# Doc subpaths
+/doc/administration/monitoring/ @aqualls
/doc/development/ @marcia @mjang1
/doc/development/documentation/ @mikelewis
/doc/ci @marcel.amirault @sselhorn
-/doc/.linting @marcel.amirault @eread @aqualls @mikelewis
+/doc/user/clusters @aqualls
+/doc/user/infrastructure @aqualls
+/doc/user/project/clusters @aqualls
+/doc/.vale/ @marcel.amirault @eread @aqualls @mikelewis
# Frontend maintainers should see everything in `app/assets/`
*.scss @annabeldunstone @gitlab-org/maintainers/frontend
diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js
index 6244df1180e..201cd5c2e61 100644
--- a/app/assets/javascripts/authentication/u2f/authenticate.js
+++ b/app/assets/javascripts/authentication/u2f/authenticate.js
@@ -40,10 +40,10 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
this.templates = {
- setup: '#js-authenticate-u2f-setup',
- inProgress: '#js-authenticate-u2f-in-progress',
- error: '#js-authenticate-u2f-error',
- authenticated: '#js-authenticate-u2f-authenticated',
+ setup: '#js-authenticate-token-2fa-setup',
+ inProgress: '#js-authenticate-token-2fa-in-progress',
+ error: '#js-authenticate-token-2fa-error',
+ authenticated: '#js-authenticate-token-2fa-authenticated',
};
}
@@ -88,7 +88,7 @@ export default class U2FAuthenticate {
error_message: error.message(),
error_code: error.errorCode,
});
- return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
+ return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress);
}
renderAuthenticated(deviceResponse) {
diff --git a/app/assets/javascripts/authentication/u2f/index.js b/app/assets/javascripts/authentication/u2f/index.js
index 6e0d1c308f6..f129acca1c3 100644
--- a/app/assets/javascripts/authentication/u2f/index.js
+++ b/app/assets/javascripts/authentication/u2f/index.js
@@ -5,8 +5,8 @@ export default () => {
if (!gon.u2f) return;
const u2fAuthenticate = new U2FAuthenticate(
- $('#js-authenticate-u2f'),
- '#js-login-u2f-form',
+ $('#js-authenticate-token-2fa'),
+ '#js-login-token-2fa-form',
gon.u2f,
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js
index f5a422727ad..52c0ce1fc04 100644
--- a/app/assets/javascripts/authentication/u2f/register.js
+++ b/app/assets/javascripts/authentication/u2f/register.js
@@ -78,7 +78,7 @@ export default class U2FRegister {
error_message: error.message(),
error_code: error.errorCode,
});
- return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
+ return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup);
}
renderRegistered(deviceResponse) {
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index a71244fdc13..334d6df088c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -196,7 +196,7 @@ export default {
s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
Helm Tiller is required to install any of the following applications.`)
}}
- <a :href="helpPath">{{ __('More information') }}</a>
+ <gl-link :href="helpPath">{{ __('More information') }}</gl-link>
</p>
<div class="cluster-application-list prepend-top-10">
@@ -306,9 +306,9 @@ export default {
generated endpoint in order to access
your application after it has been deployed.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ <gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
- </a>
+ </gl-link>
</p>
</div>
@@ -318,9 +318,9 @@ export default {
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ <gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
- </a>
+ </gl-link>
</p>
</template>
<template v-else>
@@ -397,11 +397,10 @@ export default {
s__(`ClusterIntegration|Issuers represent a certificate authority.
You must provide an email address for your Issuer. `)
}}
- <a
+ <gl-link
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
target="_blank"
- rel="noopener noreferrer"
- >{{ __('More information') }}</a
+ >{{ __('More information') }}</gl-link
>
</p>
</div>
@@ -578,9 +577,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ <gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
- </a>
+ </gl-link>
</p>
</div>
</template>
@@ -617,9 +616,7 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
- <a :href="helpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <gl-link :href="helpPath" target="_blank">{{ __('More information') }}</gl-link>
</p>
<p>
{{
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 86714471823..aac58f285f0 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -168,7 +168,7 @@ export const convertToCamelCase = string =>
* @param {*} string
*/
export const convertToSnakeCase = string =>
- slugifyWithUnderscore(string.match(/([a-zA-Z][^A-Z]*)/g).join(' '));
+ slugifyWithUnderscore((string.match(/([a-zA-Z][^A-Z]*)/g) || [string]).join(' '));
/**
* Converts a sentence to lower case from the second word onwards
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 2e3ed15e50f..08078fa6b62 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -90,9 +90,7 @@ export default {
footer-primary-button-variant="warning"
@submit="onSubmit"
>
- <template #title>
- <div class="modal-title-with-label" v-html="title">{{ title }}</div>
- </template>
+ <div slot="title" class="modal-title-with-label" v-html="title"></div>
{{ text }}
</gl-modal>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index fd45ac52647..6f3f2aa0e8e 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -24,28 +24,23 @@ export default {
},
showNoResultsMessage: {
type: Boolean,
- required: false,
- default: false,
+ required: true,
},
showMinimumSearchQueryMessage: {
type: Boolean,
- required: false,
- default: false,
+ required: true,
},
showLoadingIndicator: {
type: Boolean,
- required: false,
- default: false,
+ required: true,
},
showSearchErrorMessage: {
type: Boolean,
- required: false,
- default: false,
+ required: true,
},
totalResults: {
type: Number,
- required: false,
- default: 0,
+ required: true,
},
},
data() {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 47c6a9a44ca..43d766db9e0 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -669,7 +669,8 @@
.ci-action-icon-container {
position: absolute;
right: 5px;
- top: 5px;
+ top: 50%;
+ transform: translateY(-50%);
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
diff --git a/app/graphql/mutations/container_expiration_policies/update.rb b/app/graphql/mutations/container_expiration_policies/update.rb
new file mode 100644
index 00000000000..ee28f3dc81e
--- /dev/null
+++ b/app/graphql/mutations/container_expiration_policies/update.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ContainerExpirationPolicies
+ class Update < Mutations::BaseMutation
+ include ResolvesProject
+
+ graphql_name 'UpdateContainerExpirationPolicy'
+
+ authorize :destroy_container_image
+
+ argument :project_path,
+ GraphQL::ID_TYPE,
+ required: true,
+ description: 'The project path where the container expiration policy is located'
+
+ argument :enabled,
+ GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: copy_field_description(Types::ContainerExpirationPolicyType, :enabled)
+
+ argument :cadence,
+ Types::ContainerExpirationPolicyCadenceEnum,
+ required: false,
+ description: copy_field_description(Types::ContainerExpirationPolicyType, :cadence)
+
+ argument :older_than,
+ Types::ContainerExpirationPolicyOlderThanEnum,
+ required: false,
+ description: copy_field_description(Types::ContainerExpirationPolicyType, :older_than)
+
+ argument :keep_n,
+ Types::ContainerExpirationPolicyKeepEnum,
+ required: false,
+ description: copy_field_description(Types::ContainerExpirationPolicyType, :keep_n)
+
+ field :container_expiration_policy,
+ Types::ContainerExpirationPolicyType,
+ null: true,
+ description: 'The container expiration policy after mutation'
+
+ def resolve(project_path:, **args)
+ project = authorized_find!(full_path: project_path)
+
+ result = ::ContainerExpirationPolicies::UpdateService
+ .new(container: project, current_user: current_user, params: args)
+ .execute
+
+ {
+ container_expiration_policy: result.payload[:container_expiration_policy],
+ errors: result.error? ? [result.message] : []
+ }
+ end
+
+ private
+
+ def find_object(full_path:)
+ resolve_project(full_path: full_path)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/container_expiration_policy_type.rb b/app/graphql/types/container_expiration_policy_type.rb
index 4b380767fbd..da53dbcbd39 100644
--- a/app/graphql/types/container_expiration_policy_type.rb
+++ b/app/graphql/types/container_expiration_policy_type.rb
@@ -10,7 +10,7 @@ module Types
field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was created'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was updated'
- field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates if this container expiration policy is enabled'
+ field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether this container expiration policy is enabled'
field :older_than, Types::ContainerExpirationPolicyOlderThanEnum, null: true, description: 'Tags older that this will expire'
field :cadence, Types::ContainerExpirationPolicyCadenceEnum, null: false, description: 'This container expiration policy schedule'
field :keep_n, Types::ContainerExpirationPolicyKeepEnum, null: true, description: 'Number of tags to retain'
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 590ed7e960a..a983231c78a 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -49,6 +49,7 @@ module Types
mount_mutation Mutations::JiraImport::Start
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
+ mount_mutation Mutations::ContainerExpirationPolicies::Update
end
end
diff --git a/app/services/container_expiration_policies/update_service.rb b/app/services/container_expiration_policies/update_service.rb
new file mode 100644
index 00000000000..2f34941d692
--- /dev/null
+++ b/app/services/container_expiration_policies/update_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ContainerExpirationPolicies
+ class UpdateService < BaseContainerService
+ include Gitlab::Utils::StrongMemoize
+
+ ALLOWED_ATTRIBUTES = %i[enabled cadence older_than keep_n name_regex name_regex_keep].freeze
+
+ def execute
+ return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?
+
+ if container_expiration_policy.update(container_expiration_policy_params)
+ ServiceResponse.success(payload: { container_expiration_policy: container_expiration_policy })
+ else
+ ServiceResponse.error(
+ message: container_expiration_policy.errors.full_messages.to_sentence || 'Bad request',
+ http_status: 400
+ )
+ end
+ end
+
+ private
+
+ def container_expiration_policy
+ strong_memoize(:container_expiration_policy) do
+ @container.container_expiration_policy || @container.build_container_expiration_policy
+ end
+ end
+
+ def allowed?
+ Ability.allowed?(current_user, :destroy_container_image, @container)
+ end
+
+ def container_expiration_policy_params
+ @params.slice(*ALLOWED_ATTRIBUTES)
+ end
+ end
+end
diff --git a/app/views/admin/sessions/_two_factor_u2f.html.haml b/app/views/admin/sessions/_two_factor_u2f.html.haml
deleted file mode 100644
index 09b91d76295..00000000000
--- a/app/views/admin/sessions/_two_factor_u2f.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-#js-authenticate-u2f
-%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' }= _("Sign in via 2FA code")
-
-%script#js-authenticate-u2f-in-progress{ type: "text/template" }
- %p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.")
-
--# haml-lint:disable NoPlainNodes
-%script#js-authenticate-u2f-error{ type: "text/template" }
- %div
- %p <%= error_message %> (#{_("error code:")} <%= error_code %>)
- %a.btn.btn-block.btn-warning#js-u2f-try-again= _("Try again?")
-
-%script#js-authenticate-u2f-authenticated{ type: "text/template" }
- %div
- %p= _("We heard back from your U2F device. You have been authenticated.")
- = form_tag(admin_session_path, method: :post, id: 'js-login-u2f-form') do |f|
- = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml
index 57a3452cf35..746d57dbad1 100644
--- a/app/views/admin/sessions/two_factor.html.haml
+++ b/app/views/admin/sessions/two_factor.html.haml
@@ -12,4 +12,4 @@
- if current_user.two_factor_otp_enabled?
= render 'admin/sessions/two_factor_otp'
- if current_user.two_factor_u2f_enabled?
- = render 'admin/sessions/two_factor_u2f'
+ = render 'u2f/authenticate', render_remember_me: false, target_path: admin_session_path
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index f49cdfbf8da..126d8450568 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -14,4 +14,4 @@
= f.submit "Verify code", class: "btn btn-success"
- if @user.two_factor_u2f_enabled?
- = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
+ = render "u2f/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 51018428b1b..6658d70df8d 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -1,18 +1,19 @@
-#js-authenticate-u2f
+#js-authenticate-token-2fa
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' }= _("Sign in via 2FA code")
-%script#js-authenticate-u2f-in-progress{ type: "text/template" }
+%script#js-authenticate-token-2fa-in-progress{ type: "text/template" }
%p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.")
-%script#js-authenticate-u2f-error{ type: "text/template" }
+%script#js-authenticate-token-2fa-error{ type: "text/template" }
%div
%p <%= error_message %> (#{_("error code:")} <%= error_code %>)
- %a.btn.btn-block.btn-warning#js-u2f-try-again= _("Try again?")
+ %a.btn.btn-block.btn-warning#js-token-2fa-try-again= _("Try again?")
-%script#js-authenticate-u2f-authenticated{ type: "text/template" }
+%script#js-authenticate-token-2fa-authenticated{ type: "text/template" }
%div
- %p= _("We heard back from your U2F device. You have been authenticated.")
- = form_tag(new_user_session_path, method: :post, id: 'js-login-u2f-form') do |f|
- - resource_params = params[resource_name].presence || params
- = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
+ %p= _("We heard back from your device. You have been authenticated.")
+ = form_tag(target_path, method: :post, id: 'js-login-token-2fa-form') do |f|
+ - if render_remember_me
+ - resource_params = params[resource_name].presence || params
+ = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index ef3835332a7..6f3f4c4981c 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -25,7 +25,7 @@
%div
%p
%span <%= error_message %> (#{_("error code:")} <%= error_code %>)
- %a.btn.btn-warning#js-u2f-try-again= _("Try again?")
+ %a.btn.btn-warning#js-token-2fa-try-again= _("Try again?")
%script#js-register-u2f-registered{ type: "text/template" }
.row.append-bottom-10
diff --git a/bin/secpick b/bin/secpick
index 4d056ceecaf..517465d3f5d 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -25,12 +25,8 @@ module Secpick
@options[:try] == true
end
- def original_branch
- @options[:branch].strip
- end
-
def source_branch
- branch = "#{original_branch}-#{@options[:version]}"
+ branch = "#{@options[:branch]}-#{@options[:version]}"
branch.prepend("#{BRANCH_PREFIX}-") unless branch.start_with?("#{BRANCH_PREFIX}-")
branch.freeze
end
@@ -44,7 +40,7 @@ module Secpick
"git checkout -B #{source_branch} #{@options[:remote]}/#{stable_branch} --no-track",
"git cherry-pick #{@options[:sha]}",
"git push #{@options[:remote]} #{source_branch}",
- "git checkout #{original_branch}"]
+ "git checkout #{@options[:branch]}"]
end
def gitlab_params
@@ -121,8 +117,8 @@ module Secpick
parser.parse!
- options[:sha] ||= `git rev-parse HEAD`
- options[:branch] ||= `git rev-parse --abbrev-ref HEAD`
+ options[:sha] ||= `git rev-parse HEAD`.strip
+ options[:branch] ||= `git rev-parse --abbrev-ref HEAD`.strip
options[:remote] ||= DEFAULT_REMOTE
nil_options = options.select {|_, v| v.nil? }
diff --git a/changelogs/unreleased/196784-graphql-mutations-for-container-expiration-policies.yml b/changelogs/unreleased/196784-graphql-mutations-for-container-expiration-policies.yml
new file mode 100644
index 00000000000..3205401f5db
--- /dev/null
+++ b/changelogs/unreleased/196784-graphql-mutations-for-container-expiration-policies.yml
@@ -0,0 +1,5 @@
+---
+title: Add container expiration policy objects to the GraphQL API
+merge_request: 32944
+author:
+type: added
diff --git a/changelogs/unreleased/219582-fix-ambiguous_string_concat_on_cleanup_projects_with_missing_names.yml b/changelogs/unreleased/219582-fix-ambiguous_string_concat_on_cleanup_projects_with_missing_names.yml
new file mode 100644
index 00000000000..4a4e268171e
--- /dev/null
+++ b/changelogs/unreleased/219582-fix-ambiguous_string_concat_on_cleanup_projects_with_missing_names.yml
@@ -0,0 +1,5 @@
+---
+title: Fix ambiguous string concatenation on CleanupProjectsWithMissingNamespace
+merge_request: 33497
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-vertically-center-action-icon.yml b/changelogs/unreleased/fix-vertically-center-action-icon.yml
new file mode 100644
index 00000000000..40c97bbc73b
--- /dev/null
+++ b/changelogs/unreleased/fix-vertically-center-action-icon.yml
@@ -0,0 +1,5 @@
+---
+title: vertically center action icon in the CI pipeline
+merge_request: 33427
+author: Nathanael Weber
+type: fixed
diff --git a/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb b/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb
index 442acfc6d16..1ead10a4de6 100644
--- a/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb
+++ b/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb
@@ -249,8 +249,8 @@ class CleanupProjectsWithMissingNamespace < ActiveRecord::Migration[6.0]
-- Names are expected to be unique inside their namespace
-- (uniqueness validation on namespace_id, name)
-- Attach the id to the name and path to make sure that they are unique
- name = name || '_' || id,
- path = path || '_' || id
+ name = name || '_' || id::text,
+ path = path || '_' || id::text
SQL
end
end
diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md
index 5b24c222f06..88218d24b2f 100644
--- a/doc/administration/geo/disaster_recovery/background_verification.md
+++ b/doc/administration/geo/disaster_recovery/background_verification.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Automatic background verification **(PREMIUM ONLY)**
NOTE: **Note:**
diff --git a/doc/administration/geo/disaster_recovery/bring_primary_back.md b/doc/administration/geo/disaster_recovery/bring_primary_back.md
index 43089237a75..b19e55595e7 100644
--- a/doc/administration/geo/disaster_recovery/bring_primary_back.md
+++ b/doc/administration/geo/disaster_recovery/bring_primary_back.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Bring a demoted primary node back online **(PREMIUM ONLY)**
After a failover, it is possible to fail back to the demoted **primary** node to
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index 8a27f5f7a4e..4d88643e538 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Disaster Recovery (Geo) **(PREMIUM ONLY)**
Geo replicates your database, your Git repositories, and few other assets.
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
index abaa2b0c0d8..0ce1325a537 100644
--- a/doc/administration/geo/disaster_recovery/planned_failover.md
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Disaster recovery for planned failover **(PREMIUM ONLY)**
The primary use-case of Disaster Recovery is to ensure business continuity in
diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md
index 591f08a26fe..3d08ed81700 100644
--- a/doc/administration/geo/replication/configuration.md
+++ b/doc/administration/geo/replication/configuration.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo configuration **(PREMIUM ONLY)**
## Configuring a new **secondary** node
diff --git a/doc/administration/geo/replication/database.md b/doc/administration/geo/replication/database.md
index b5debf644b0..2a87291c801 100644
--- a/doc/administration/geo/replication/database.md
+++ b/doc/administration/geo/replication/database.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo database replication **(PREMIUM ONLY)**
NOTE: **Note:**
diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md
index b44abd37290..f50da27d82f 100644
--- a/doc/administration/geo/replication/datatypes.md
+++ b/doc/administration/geo/replication/datatypes.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo data types support
A Geo data type is a specific class of data that is required by one or more GitLab features to
diff --git a/doc/administration/geo/replication/docker_registry.md b/doc/administration/geo/replication/docker_registry.md
index 3a1de67e88a..75632e839fe 100644
--- a/doc/administration/geo/replication/docker_registry.md
+++ b/doc/administration/geo/replication/docker_registry.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Docker Registry for a secondary node **(PREMIUM ONLY)**
You can set up a [Docker Registry](https://docs.docker.com/registry/) on your
diff --git a/doc/administration/geo/replication/external_database.md b/doc/administration/geo/replication/external_database.md
index ae3069a0e40..28750854e0d 100644
--- a/doc/administration/geo/replication/external_database.md
+++ b/doc/administration/geo/replication/external_database.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo with external PostgreSQL instances **(PREMIUM ONLY)**
This document is relevant if you are using a PostgreSQL instance that is *not
diff --git a/doc/administration/geo/replication/faq.md b/doc/administration/geo/replication/faq.md
index 2405e2cbfd2..522ad32c352 100644
--- a/doc/administration/geo/replication/faq.md
+++ b/doc/administration/geo/replication/faq.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo Frequently Asked Questions **(PREMIUM ONLY)**
## What are the minimum requirements to run Geo?
diff --git a/doc/administration/geo/replication/geo_validation_tests.md b/doc/administration/geo/replication/geo_validation_tests.md
index 6619f114a9f..7b186d15fae 100644
--- a/doc/administration/geo/replication/geo_validation_tests.md
+++ b/doc/administration/geo/replication/geo_validation_tests.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo validation tests
The Geo team performs manual testing and validation on common deployment configurations to ensure
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index 76fe6aef918..5b4b476bfa8 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Replication (Geo) **(PREMIUM ONLY)**
> - Introduced in GitLab Enterprise Edition 8.9.
diff --git a/doc/administration/geo/replication/location_aware_git_url.md b/doc/administration/geo/replication/location_aware_git_url.md
index d5314d0cfd2..49c83ee1718 100644
--- a/doc/administration/geo/replication/location_aware_git_url.md
+++ b/doc/administration/geo/replication/location_aware_git_url.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Location-aware Git remote URL with AWS Route53 **(PREMIUM ONLY)**
You can provide GitLab users with a single remote URL that automatically uses
diff --git a/doc/administration/geo/replication/multiple_servers.md b/doc/administration/geo/replication/multiple_servers.md
index 9322c4cc417..98acdde844e 100644
--- a/doc/administration/geo/replication/multiple_servers.md
+++ b/doc/administration/geo/replication/multiple_servers.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo for multiple servers **(PREMIUM ONLY)**
This document describes a minimal reference architecture for running Geo
diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md
index e646623c58a..3cc0ade414e 100644
--- a/doc/administration/geo/replication/object_storage.md
+++ b/doc/administration/geo/replication/object_storage.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo with Object storage **(PREMIUM ONLY)**
Geo can be used in combination with Object Storage (AWS S3, or other compatible object storage).
diff --git a/doc/administration/geo/replication/remove_geo_node.md b/doc/administration/geo/replication/remove_geo_node.md
index c04c7aec858..539132776b3 100644
--- a/doc/administration/geo/replication/remove_geo_node.md
+++ b/doc/administration/geo/replication/remove_geo_node.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Removing secondary Geo nodes **(PREMIUM ONLY)**
**Secondary** nodes can be removed from the Geo cluster using the Geo admin page of the **primary** node. To remove a **secondary** node:
diff --git a/doc/administration/geo/replication/security_review.md b/doc/administration/geo/replication/security_review.md
index e81e17bf531..f5edf79c6e4 100644
--- a/doc/administration/geo/replication/security_review.md
+++ b/doc/administration/geo/replication/security_review.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo security review (Q&A) **(PREMIUM ONLY)**
The following security review of the Geo feature set focuses on security aspects of
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index a3ad2329dbd..d66ee4682b2 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Geo Troubleshooting **(PREMIUM ONLY)**
Setting up Geo requires careful attention to details and sometimes it's easy to
diff --git a/doc/administration/geo/replication/tuning.md b/doc/administration/geo/replication/tuning.md
index 972bf002935..63a8f81eecb 100644
--- a/doc/administration/geo/replication/tuning.md
+++ b/doc/administration/geo/replication/tuning.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Tuning Geo **(PREMIUM ONLY)**
## Changing the sync capacity values
diff --git a/doc/administration/geo/replication/updating_the_geo_nodes.md b/doc/administration/geo/replication/updating_the_geo_nodes.md
index fa1576e19eb..6c2778ad0fe 100644
--- a/doc/administration/geo/replication/updating_the_geo_nodes.md
+++ b/doc/administration/geo/replication/updating_the_geo_nodes.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Updating the Geo nodes **(PREMIUM ONLY)**
Updating Geo nodes involves performing:
diff --git a/doc/administration/geo/replication/using_a_geo_server.md b/doc/administration/geo/replication/using_a_geo_server.md
index 2fec2b2b59c..3f2895f1c71 100644
--- a/doc/administration/geo/replication/using_a_geo_server.md
+++ b/doc/administration/geo/replication/using_a_geo_server.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
<!-- Please update EE::GitLab::GeoGitAccess::GEO_SERVER_DOCS_URL if this file is moved) -->
# Using a Geo Server **(PREMIUM ONLY)**
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index 4058c420d8d..777de715a8c 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -1,3 +1,10 @@
+---
+stage: Enablement
+group: Geo
+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/#designated-technical-writers
+type: howto
+---
+
# Version specific update instructions
Check this document if it includes instructions for the version you are updating.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 95fc71a3e97..0a08d113e37 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -1084,7 +1084,7 @@ type ContainerExpirationPolicy {
createdAt: Time!
"""
- Indicates if this container expiration policy is enabled
+ Indicates whether this container expiration policy is enabled
"""
enabled: Boolean!
@@ -7244,6 +7244,7 @@ type Mutation {
todosMarkAllDone(input: TodosMarkAllDoneInput!): TodosMarkAllDonePayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload
+ updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
"""
@@ -11788,6 +11789,61 @@ type UpdateAlertStatusPayload {
issue: Issue
}
+"""
+Autogenerated input type of UpdateContainerExpirationPolicy
+"""
+input UpdateContainerExpirationPolicyInput {
+ """
+ This container expiration policy schedule
+ """
+ cadence: ContainerExpirationPolicyCadenceEnum
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Indicates whether this container expiration policy is enabled
+ """
+ enabled: Boolean
+
+ """
+ Number of tags to retain
+ """
+ keepN: ContainerExpirationPolicyKeepEnum
+
+ """
+ Tags older that this will expire
+ """
+ olderThan: ContainerExpirationPolicyOlderThanEnum
+
+ """
+ The project path where the container expiration policy is located
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of UpdateContainerExpirationPolicy
+"""
+type UpdateContainerExpirationPolicyPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The container expiration policy after mutation
+ """
+ containerExpirationPolicy: ContainerExpirationPolicy
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+}
+
input UpdateDiffImagePositionInput {
"""
Total height of the image
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 3b1230bd40f..506c060f5e0 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -2885,7 +2885,7 @@
},
{
"name": "enabled",
- "description": "Indicates if this container expiration policy is enabled",
+ "description": "Indicates whether this container expiration policy is enabled",
"args": [
],
@@ -21332,6 +21332,33 @@
"deprecationReason": null
},
{
+ "name": "updateContainerExpirationPolicy",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "UpdateContainerExpirationPolicyInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "UpdateContainerExpirationPolicyPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "updateEpic",
"description": null,
"args": [
@@ -34863,6 +34890,148 @@
},
{
"kind": "INPUT_OBJECT",
+ "name": "UpdateContainerExpirationPolicyInput",
+ "description": "Autogenerated input type of UpdateContainerExpirationPolicy",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project path where the container expiration policy is located",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "enabled",
+ "description": "Indicates whether this container expiration policy is enabled",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "cadence",
+ "description": "This container expiration policy schedule",
+ "type": {
+ "kind": "ENUM",
+ "name": "ContainerExpirationPolicyCadenceEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "olderThan",
+ "description": "Tags older that this will expire",
+ "type": {
+ "kind": "ENUM",
+ "name": "ContainerExpirationPolicyOlderThanEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "keepN",
+ "description": "Number of tags to retain",
+ "type": {
+ "kind": "ENUM",
+ "name": "ContainerExpirationPolicyKeepEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "UpdateContainerExpirationPolicyPayload",
+ "description": "Autogenerated return type of UpdateContainerExpirationPolicy",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "containerExpirationPolicy",
+ "description": "The container expiration policy after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ContainerExpirationPolicy",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
"name": "UpdateDiffImagePositionInput",
"description": null,
"fields": null,
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 742940d3477..405a32be0e8 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -196,7 +196,7 @@ A tag expiration policy designed to keep only the images that matter most
| --- | ---- | ---------- |
| `cadence` | ContainerExpirationPolicyCadenceEnum! | This container expiration policy schedule |
| `createdAt` | Time! | Timestamp of when the container expiration policy was created |
-| `enabled` | Boolean! | Indicates if this container expiration policy is enabled |
+| `enabled` | Boolean! | Indicates whether this container expiration policy is enabled |
| `keepN` | ContainerExpirationPolicyKeepEnum | Number of tags to retain |
| `nameRegex` | String | Tags with names matching this regex pattern will expire |
| `nameRegexKeep` | String | Tags with names matching this regex pattern will be preserved |
@@ -1755,6 +1755,16 @@ Autogenerated return type of UpdateAlertStatus
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue created after mutation |
+## UpdateContainerExpirationPolicyPayload
+
+Autogenerated return type of UpdateContainerExpirationPolicy
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy after mutation |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
## UpdateEpicPayload
Autogenerated return type of UpdateEpic
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 6f9a84cfbd4..ebdd35ee34d 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -1723,9 +1723,8 @@ Use `%2F` for slashes (`/`).
#### Pass arrays to API calls
The GitLab API sometimes accepts arrays of strings or integers. For example, to
-restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
-`example.net`, you would do something like this:
+exclude specific users when requesting a list of users for a project, you would do something like this:
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "skip_users[]=<user_id>" --data "skip_users[]=<user_id>" https://gitlab.example.com/api/v4/projects/<project_id>/users
```
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 2280dc3d7c6..cb1cb6ea7ea 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -66,7 +66,7 @@ FDOC=1 bin/rspec spec/[path]/[to]/[spec].rb
### General guidelines
-- Use a single, top-level `describe ClassName` block.
+- Use a single, top-level `RSpec.describe ClassName` block.
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic.
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index f0fb06910f8..62e6fcb2aa1 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -58,7 +58,7 @@ Everything you should know about how to test Rake tasks.
## [End-to-end tests](end_to_end/index.md)
Everything you should know about how to run end-to-end tests using
-[GitLab QA](ttps://gitlab.com/gitlab-org/gitlab-qa) testing framework.
+[GitLab QA](https://gitlab.com/gitlab-org/gitlab-qa) testing framework.
## [Migrations tests](testing_migrations_guide.md)
diff --git a/doc/user/project/pages/getting_started/new_or_existing_website.md b/doc/user/project/pages/getting_started/new_or_existing_website.md
index e47868913bc..5d7126ab22e 100644
--- a/doc/user/project/pages/getting_started/new_or_existing_website.md
+++ b/doc/user/project/pages/getting_started/new_or_existing_website.md
@@ -13,6 +13,10 @@ the CI/CD pipeline to generate a Pages website.
Use a `.gitlab-ci.yml` template when you have an existing project that you want to add a Pages site to.
+Your GitLab repository should contain files specific to an SSG, or plain HTML.
+After you complete these steps, you may need to do additional
+configuration for the Pages site to generate properly.
+
1. In the left sidebar, click **Project overview**.
1. Click **Set up CI/CD**.
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index 4e95b5d5a69..492d675eaac 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -6,137 +6,131 @@ group: Release Management
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/#designated-technical-writers
---
-# Creating and Tweaking GitLab CI/CD for GitLab Pages
-
-To [get started with GitLab Pages](index.md#getting-started), you can
-use one of the project templates, a `.gitlab-ci.yml` template,
-or fork an existing example project. Therefore, you don't need to
-understand _all_ the ins and odds of GitLab CI/CD to get your site
-deployed. Still, there are cases where you want to write your own
-script or tweak an existing one. This document guides you through
-this process.
-
-This guide also provides a general overview and clear introduction
-for **getting familiar with the `.gitlab-ci.yml` file and writing
-one for the first time.**
-
-[GitLab CI/CD](../../../ci/README.md) serves
-numerous purposes, to build, test, and deploy your app
-from GitLab through
-[Continuous Integration, Continuous Delivery, and Continuous Deployment](../../../ci/introduction/index.md#introduction-to-cicd-methodologies)
-methods. You will need it to build your website with GitLab Pages,
-and deploy it to the Pages server.
-
-To implement GitLab CI/CD, the first thing you need is a configuration
-file called `.gitlab-ci.yml` placed at your website's root directory.
-
-What this file actually does is telling the
-[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts
-as you would do from the command line. The Runner acts as your
-terminal. GitLab CI/CD tells the Runner which commands to run.
-Both are built-in in GitLab, and you don't need to set up
-anything for them to work.
-
-Explaining [every detail of GitLab CI/CD](../../../ci/yaml/README.md)
-and GitLab Runner is out of the scope of this guide, but we'll
-need to understand just a few things to be able to write our own
-`.gitlab-ci.yml` or tweak an existing one. It's a
-[YAML](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html) file,
-with its own syntax. You can always check your CI syntax with
-the [GitLab CI/CD Lint Tool](https://gitlab.com/ci/lint).
-
-## Practical example
-
-Let's consider you have a [Jekyll](https://jekyllrb.com/) site.
-To build it locally, you would open your terminal, and run `jekyll build`.
-Of course, before building it, you had to install Jekyll in your computer.
-For that, you had to open your terminal and run `gem install jekyll`.
-Right? GitLab CI/CD + GitLab Runner do the same thing. But you need to
-write in the `.gitlab-ci.yml` the script you want to run so
-GitLab Runner will do it for you. It looks more complicated than it
-is. What you need to tell the Runner:
-
-```shell
-gem install jekyll
-jekyll build
-```
+# Create a GitLab Pages website from scratch
+
+This tutorial shows you how to create a Pages site from scratch. You will start with
+a blank project and create your own CI file, which gives instruction to the
+[GitLab Runner](https://docs.gitlab.com/runner/). When your CI/CD
+[pipeline](../../../ci/pipelines/index.md) runs, the Pages site is created.
+
+This example uses the [Jekyll](https://jekyllrb.com/) Static Site Generator (SSG).
+Other SSGs follow similar steps. You do not need to be familiar with Jekyll or SSGs
+to complete this tutorial.
+
+## Prerequisites
+
+To follow along with this example, start with a blank project in GitLab.
+Create three files in the root (top-level) directory.
+
+- `.gitlab-ci.yml` A YAML file that contains the commands you want to run.
+ For now, leave the file's contents blank.
+
+- `index.html` An HTML file you can populate with whatever HTML content you'd like,
+ for example:
-### Script
+ ```html
+ <html>
+ <head>
+ <title>Home</title>
+ </head>
+ <body>
+ <h1>Hello World!</h1>
+ </body>
+ </html>
+ ```
-To transpose this script to YAML, it would be like this:
+- [`Gemfile`](https://bundler.io/gemfile.html) A file that describes dependencies for Ruby programs.
+ Populate it with this content:
+
+ ```ruby
+ source "https://rubygems.org"
+
+ gem "jekyll"
+ ```
+
+## Choose a Docker image
+
+In this example, the Runner uses a [Docker image](../../../ci/docker/using_docker_images.md)
+to run scripts and deploy the site.
+
+This specific Ruby image is maintained on [DockerHub](https://hub.docker.com/_/ruby).
+
+Edit your `.gitlab-ci.yml` and add this text as the first line.
```yaml
-script:
- - gem install jekyll
- - jekyll build
+image: ruby:2.7
```
-### Job
+If your SSG needs [NodeJS](https://nodejs.org/) to build, you must specify an
+image that contains NodeJS as part of its file system. For example, for a
+[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:12.17.0`.
+
+## Install Jekyll
+
+To run [Jekyll](https://jekyllrb.com/) locally, you would open your terminal and:
+
+- Install [Bundler](https://bundler.io/) by running `gem install bundler`.
+- Create `Gemfile.lock` by running `bundle install`.
+- Install Jekyll by running `bundle exec jekyll build`.
-So far so good. Now, each `script`, in GitLab is organized by
-a `job`, which is a bunch of scripts and settings you want to
-apply to that specific task.
+In the `.gitlab-ci.yml` file, this is written as:
```yaml
-job:
- script:
- - gem install jekyll
- - jekyll build
+script:
+ - gem install bundler
+ - bundle install
+ - bundle exec jekyll build
```
-For GitLab Pages, this `job` has a specific name, called `pages`,
-which tells the Runner you want that task to deploy your website
-with GitLab Pages:
+In addition, in the `.gitlab-ci.yml` file, each `script` is organized by a `job`.
+A `job` includes the scripts and settings you want to apply to that specific
+task.
```yaml
-pages:
+job:
script:
- - gem install jekyll
- - jekyll build
+ - gem install bundler
+ - bundle install
+ - bundle exec jekyll build
```
-### The `public` directory
-
-We also need to tell Jekyll where do you want the website to build,
-and GitLab Pages will only consider files in a directory called `public`.
-To do that with Jekyll, we need to add a flag specifying the
-[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the
-built website: `jekyll build -d public`. Of course, we need
-to tell this to our Runner:
+For GitLab Pages, this `job` has a specific name, called `pages`.
+This setting tells the Runner you want the job to deploy your website
+with GitLab Pages:
```yaml
pages:
script:
- - gem install jekyll
- - jekyll build -d public
+ - gem install bundler
+ - bundle install
+ - bundle exec jekyll build
```
-### Artifacts
+## Specify the `public` directory for output
-We also need to tell the Runner that this _job_ generates
-_artifacts_, which is the site built by Jekyll.
-Where are these artifacts stored? In the `public` directory:
+Jekyll needs to know where to generate its output.
+GitLab Pages only considers files in a directory called `public`.
+
+Jekyll uses destination (`-d`) to specify an output directory for the built website:
```yaml
pages:
script:
- - gem install jekyll
- - jekyll build -d public
- artifacts:
- paths:
- - public
+ - gem install bundler
+ - bundle install
+ - bundle exec jekyll build -d public
```
-The script above would be enough to build your Jekyll
-site with GitLab Pages. But, from Jekyll 3.4.0 on, its default
-template originated by `jekyll new project` requires
-[Bundler](https://bundler.io) to install Jekyll dependencies
-and the default theme. To adjust our script to meet these new
-requirements, we only need to install and build Jekyll with Bundler:
+## Specify the `public` directory for artifacts
+
+Now that Jekyll has output the files to the `public` directory,
+the Runner needs to know where to get them. The artifacts are stored
+in the `public` directory:
```yaml
pages:
script:
+ - gem install bundler
- bundle install
- bundle exec jekyll build -d public
artifacts:
@@ -144,27 +138,14 @@ pages:
- public
```
-That's it! A `.gitlab-ci.yml` with the content above would deploy
-your Jekyll 3.4.0 site with GitLab Pages. This is the minimum
-configuration for our example. On the steps below, we'll refine
-the script by adding extra options to our GitLab CI/CD.
-
-Artifacts will be automatically deleted once GitLab Pages got deployed.
-You can preserve artifacts for limited time by specifying the expiry time.
-
-### Image
-
-At this point, you probably ask yourself: "okay, but to install Jekyll
-I need Ruby. Where is Ruby on that script?". The answer is simple: the
-first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a
-[Docker](https://www.docker.com/) image specifying what do you need in
-your container to run that script:
+Paste this into `.gitlab-ci.yml` file, so it now looks like this:
```yaml
image: ruby:2.7
pages:
script:
+ - gem install bundler
- bundle install
- bundle exec jekyll build -d public
artifacts:
@@ -172,39 +153,31 @@ pages:
- public
```
-In this case, you're telling the Runner to pull this image, which
-contains Ruby 2.7 as part of its file system. When you don't specify
-this image in your configuration, the Runner will use a default
-image, which is Ruby 2.6.
+Now save and commit the `.gitlab-ci.yml` file. You can watch the pipeline run
+by going to **CI / CD > Pipelines**.
+
+When it succeeds, go to **Settings > Pages** to view the URL where your site
+is now available.
-If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll
-need to specify which image you want to use, and this image should
-contain NodeJS as part of its file system. E.g., for a
-[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`.
+If you want to do more advanced tasks, you can update your `.gitlab-ci.yml` file
+with [any of the available settings](../../../ci/yaml/README.md). You can check
+your CI syntax with the [GitLab CI/CD Lint Tool](https://gitlab.com/ci/lint).
->**Note:**
-We're not trying to explain what a Docker image is,
-we just need to introduce the concept with a minimum viable
-explanation. To know more about Docker images, please visit
-their website or take a look at a
-[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here.
+The following topics show other examples of other options you can add to your CI/CD file.
-Let's go a little further.
+## Deploy specific branches to a Pages site
-### Branching
+You may want to deploy to a Pages site only from specific branches.
-If you use GitLab as a version control platform, you will have your
-branching strategy to work on your project. Meaning, you will have
-other branches in your project, but you'll want only pushes to the
-default branch (usually `master`) to be deployed to your website.
-To do that, we need to add another line to our CI, telling the Runner
-to only perform that _job_ called `pages` on the `master` branch `only`:
+You can add another line to `.gitlab-ci.yml`, which tells the Runner
+to perform the job called `pages` on the `master` branch **only**:
```yaml
-image: ruby:2.6
+image: ruby:2.7
pages:
script:
+ - gem install bundler
- bundle install
- bundle exec jekyll build -d public
artifacts:
@@ -214,21 +187,25 @@ pages:
- master
```
-### Stages
+## Specify a stage to deploy
-Another interesting concept to keep in mind are build stages.
-Your web app can pass through a lot of tests and other tasks
-until it's deployed to staging or production environments.
-There are three default stages on GitLab CI/CD: build, test,
-and deploy. To specify which stage your _job_ is running,
-simply add another line to your CI:
+There are three default stages for GitLab CI/CD: build, test,
+and deploy.
+
+If you want to test your script and check the built site before deploying
+to production, you can run the test exactly as it will run when you
+push to `master`.
+
+To specify a stage for your job to run in,
+add a `stage` line to your CI file:
```yaml
-image: ruby:2.6
+image: ruby:2.7
pages:
stage: deploy
script:
+ - gem install bundler
- bundle install
- bundle exec jekyll build -d public
artifacts:
@@ -238,20 +215,16 @@ pages:
- master
```
-You might ask yourself: "why should I bother with stages
-at all?" Well, let's say you want to be able to test your
-script and check the built site before deploying your site
-to production. You want to run the test exactly as your
-script will do when you push to `master`. It's simple,
-let's add another task (_job_) to our CI, telling it to
-test every push to other branches, `except` the `master` branch:
+Now add another job to the CI file, telling it to
+test every push to other branches, **except** the `master` branch:
```yaml
-image: ruby:2.6
+image: ruby:2.7
pages:
stage: deploy
script:
+ - gem install bundler
- bundle install
- bundle exec jekyll build -d public
artifacts:
@@ -263,6 +236,7 @@ pages:
test:
stage: test
script:
+ - gem install bundler
- bundle install
- bundle exec jekyll build -d test
artifacts:
@@ -272,34 +246,31 @@ test:
- master
```
-The `test` job is running on the stage `test`, Jekyll
-will build the site in a directory called `test`, and
-this job will affect all the branches except `master`.
-
-The best benefit of applying _stages_ to different
-_jobs_ is that every job in the same stage builds in
-parallel. So, if your web app needs more than one test
-before being deployed, you can run all your test at the
-same time, it's not necessary to wait one test to finish
-to run the other. Of course, this is just a brief
-introduction of GitLab CI/CD and GitLab Runner, which are
-tools much more powerful than that. This is what you
-need to be able to create and tweak your builds for
-your GitLab Pages site.
-
-### Before Script
-
-To avoid running the same script multiple times across
-your _jobs_, you can add the parameter `before_script`,
-in which you specify which commands you want to run for
-every single _job_. In our example, notice that we run
-`bundle install` for both jobs, `pages` and `test`.
-We don't need to repeat it:
+When the `test` job runs in the `test` stage, Jekyll
+builds the site in a directory called `test`. The job affects
+all branches except `master`.
+
+When you apply stages to different jobs, every job in the same
+stage builds in parallel. If your web application needs more than
+one test before being deployed, you can run all your tests at the
+same time.
+
+## Remove duplicate commands
+
+To avoid running the same scripts for each job, you can add the
+parameter `before_script`. In this section, specify the commands
+you want to run for every job.
+
+In the example, `gem install bundler` and `bundle install` were running
+for both jobs, `pages` and `test`.
+
+Move these commands to a `before_script` section:
```yaml
-image: ruby:2.6
+image: ruby:2.7
before_script:
+ - gem install bundler
- bundle install
pages:
@@ -323,22 +294,23 @@ test:
- master
```
-### Caching Dependencies
+## Build faster with cached dependencies
-If you want to cache the installation files for your
-projects dependencies, for building faster, you can
-use the parameter `cache`. For this example, we'll
-cache Jekyll dependencies in a `vendor` directory
-when we run `bundle install`:
+To build faster, you can cache the installation files for your
+project's dependencies by using the `cache` parameter.
+
+This example caches Jekyll dependencies in a `vendor` directory
+when you run `bundle install`:
```yaml
-image: ruby:2.6
+image: ruby:2.7
cache:
paths:
- vendor/
before_script:
+ - gem install bundler
- bundle install --path vendor
pages:
@@ -362,36 +334,31 @@ test:
- master
```
-For this specific case, we need to exclude `/vendor`
-from Jekyll `_config.yml` file, otherwise Jekyll will
-understand it as a regular directory to build
-together with the site:
+In this case, we need to exclude the `/vendor`
+directory from the list of folders Jekyll builds. Otherwise, Jekyll
+will try to build the directory contents along with the site.
+
+In the root directory, create a file called `_config.yml`
+and add this content:
```yaml
exclude:
- vendor
```
-There we go! Now our GitLab CI/CD not only builds our website,
-but also **continuously test** pushes to feature-branches,
+Now GitLab CI/CD not only builds the website,
+but also pushes with **continuous tests** to feature-branches,
**caches** dependencies installed with Bundler, and
-**continuously deploy** every push to the `master` branch.
+**continuously deploys** every push to the `master` branch.
-## Advanced GitLab CI for GitLab Pages
+## Related topics
-What you can do with GitLab CI/CD is pretty much up to your
-creativity. Once you get used to it, you start creating
-awesome scripts that automate most of tasks you'd do
-manually in the past. Read through the
-[documentation of GitLab CI/CD](../../../ci/yaml/README.md)
-to understand how to go even further on your scripts.
+For more information, see the following blog posts.
-- On this blog post, understand the concept of
- [using GitLab CI/CD `environments` to deploy your
+- [Use GitLab CI/CD `environments` to deploy your
web app to staging and production](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/).
-- On this post, learn [how to run jobs sequentially,
- in parallel, or build a custom pipeline](https://about.gitlab.com/blog/2016/07/29/the-basics-of-gitlab-ci/)
-- On this blog post, we go through the process of
- [pulling specific directories from different projects](https://about.gitlab.com/blog/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
- to deploy this website you're looking at, <https://docs.gitlab.com>.
-- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/blog/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
+- Learn [how to run jobs sequentially,
+ in parallel, or build a custom pipeline](https://about.gitlab.com/blog/2016/07/29/the-basics-of-gitlab-ci/).
+- Learn [how to pull specific directories from different projects](https://about.gitlab.com/blog/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+ to deploy this website, <https://docs.gitlab.com>.
+- Learn [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/blog/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index db0751e3b6b..e47e59c9e98 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -24830,7 +24830,7 @@ msgstr ""
msgid "We have found the following errors:"
msgstr ""
-msgid "We heard back from your U2F device. You have been authenticated."
+msgid "We heard back from your device. You have been authenticated."
msgstr ""
msgid "We recommend that you buy more Pipeline minutes to avoid any interruption of service."
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 45caa7a2b6a..4affab295b8 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -297,6 +297,12 @@ FactoryBot.define do
trait :auto_devops_disabled do
association :auto_devops, factory: [:project_auto_devops, :disabled]
end
+
+ trait :without_container_expiration_policy do
+ after :create do |project|
+ project.container_expiration_policy.destroy!
+ end
+ end
end
# Project with empty repository
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index bb18703f90e..2cb4600ded0 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -257,7 +257,7 @@ describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
expect(page).to have_button('Verify code')
expect(page).to have_css('#user_otp_attempt')
expect(page).not_to have_link('Sign in via 2FA code')
- expect(page).not_to have_css('#js-authenticate-u2f')
+ expect(page).not_to have_css('#js-authenticate-token-2fa')
end
before do
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
index 36cc6fb7c63..8abef2ae1b2 100644
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ b/spec/frontend/authentication/u2f/authenticate_spec.js
@@ -13,10 +13,10 @@ describe('U2FAuthenticate', () => {
beforeEach(() => {
loadFixtures('u2f/authenticate.html');
u2fDevice = new MockU2FDevice();
- container = $('#js-authenticate-u2f');
+ container = $('#js-authenticate-token-2fa');
component = new U2FAuthenticate(
container,
- '#js-login-u2f-form',
+ '#js-login-token-2fa-form',
{
sign_requests: [],
},
@@ -92,7 +92,7 @@ describe('U2FAuthenticate', () => {
u2fDevice.respondToAuthenticateRequest({
errorCode: 'error!',
});
- const retryButton = container.find('#js-u2f-try-again');
+ const retryButton = container.find('#js-token-2fa-try-again');
retryButton.trigger('click');
setupButton = container.find('#js-login-u2f-device');
setupButton.trigger('click');
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 81b84d4d1e2..59a62aa7381 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -1,182 +1,175 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import { shallowMount } from '@vue/test-utils';
-import applications from '~/clusters/components/applications.vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import Applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
import eventHub from '~/clusters/event_hub';
+import ApplicationRow from '~/clusters/components/application_row.vue';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
describe('Applications', () => {
- let vm;
- let Applications;
- const ApplicationRowStub = {
- name: 'application-row-stub',
- template: `
- <div>
- <slot name="description"></slot>
- </div>
- `,
- };
+ let wrapper;
beforeEach(() => {
- Applications = Vue.extend(applications);
-
gon.features = gon.features || {};
gon.features.managedAppsLocalTiller = false;
});
+ const createApp = ({ applications, type } = {}, isShallow) => {
+ const mountMethod = isShallow ? shallowMount : mount;
+
+ wrapper = mountMethod(Applications, {
+ stubs: { ApplicationRow },
+ propsData: {
+ type,
+ applications: { ...APPLICATIONS_MOCK_STATE, ...applications },
+ },
+ });
+ };
+
+ const createShallowApp = options => createApp(options, true);
+
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('Project cluster applications', () => {
beforeEach(() => {
- vm = mountComponent(Applications, {
- applications: APPLICATIONS_MOCK_STATE,
- type: CLUSTER_TYPE.PROJECT,
- });
+ createApp({ type: CLUSTER_TYPE.PROJECT });
});
it('renders a row for Helm Tiller', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(true);
});
it('renders a row for Ingress', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-crossplane')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Fluentd', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
});
});
describe('Group cluster applications', () => {
beforeEach(() => {
- vm = mountComponent(Applications, {
- type: CLUSTER_TYPE.GROUP,
- applications: APPLICATIONS_MOCK_STATE,
- });
+ createApp({ type: CLUSTER_TYPE.GROUP });
});
it('renders a row for Helm Tiller', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(true);
});
it('renders a row for Ingress', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-crossplane')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Fluentd', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
});
});
describe('Instance cluster applications', () => {
beforeEach(() => {
- vm = mountComponent(Applications, {
- type: CLUSTER_TYPE.INSTANCE,
- applications: APPLICATIONS_MOCK_STATE,
- });
+ createApp({ type: CLUSTER_TYPE.INSTANCE });
});
it('renders a row for Helm Tiller', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(true);
});
it('renders a row for Ingress', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-crossplane')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Fluentd', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
+ expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
});
});
@@ -187,11 +180,8 @@ describe('Applications', () => {
});
it('does not render a row for Helm Tiller', () => {
- vm = mountComponent(Applications, {
- applications: APPLICATIONS_MOCK_STATE,
- });
-
- expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeNull();
+ createApp();
+ expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
});
});
});
@@ -200,7 +190,6 @@ describe('Applications', () => {
describe('with nested component', () => {
const propsData = {
applications: {
- ...APPLICATIONS_MOCK_STATE,
ingress: {
title: 'Ingress',
status: 'installed',
@@ -208,18 +197,8 @@ describe('Applications', () => {
},
};
- let wrapper;
- beforeEach(() => {
- wrapper = shallowMount(Applications, {
- propsData,
- stubs: {
- ApplicationRow: ApplicationRowStub,
- },
- });
- });
- afterEach(() => {
- wrapper.destroy();
- });
+ beforeEach(() => createShallowApp(propsData));
+
it('renders IngressModsecuritySettings', () => {
const modsecuritySettings = wrapper.find(IngressModsecuritySettings);
expect(modsecuritySettings.exists()).toBe(true);
@@ -229,9 +208,8 @@ describe('Applications', () => {
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
ingress: {
title: 'Ingress',
status: 'installed',
@@ -240,17 +218,16 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-endpoint').value).toEqual('0.0.0.0');
-
- expect(
- vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
- ).toEqual('0.0.0.0');
+ expect(wrapper.find('.js-endpoint').element.value).toEqual('0.0.0.0');
+ expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
+ '0.0.0.0',
+ );
});
});
describe('with hostname', () => {
it('renders hostname with a clipboard button', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
ingress: {
title: 'Ingress',
@@ -270,19 +247,18 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-endpoint').value).toEqual('localhost.localdomain');
+ expect(wrapper.find('.js-endpoint').element.value).toEqual('localhost.localdomain');
- expect(
- vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
- ).toEqual('localhost.localdomain');
+ expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
+ 'localhost.localdomain',
+ );
});
});
describe('without ip address', () => {
it('renders an input text with a loading icon and an alert text', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
ingress: {
title: 'Ingress',
status: 'installed',
@@ -290,29 +266,26 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-ingress-ip-loading-icon')).not.toBe(null);
- expect(vm.$el.querySelector('.js-no-endpoint-message')).not.toBe(null);
+ expect(wrapper.find('.js-ingress-ip-loading-icon').exists()).toBe(true);
+ expect(wrapper.find('.js-no-endpoint-message').exists()).toBe(true);
});
});
});
describe('before installing', () => {
it('does not render the IP address', () => {
- vm = mountComponent(Applications, {
- applications: APPLICATIONS_MOCK_STATE,
- });
+ createApp();
- expect(vm.$el.textContent).not.toContain('Ingress IP Address');
- expect(vm.$el.querySelector('.js-endpoint')).toBe(null);
+ expect(wrapper.text()).not.toContain('Ingress IP Address');
+ expect(wrapper.find('.js-endpoint').exists()).toBe(false);
});
});
describe('Cert-Manager application', () => {
describe('when not installed', () => {
it('renders email & allows editing', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
cert_manager: {
title: 'Cert-Manager',
email: 'before@example.com',
@@ -321,16 +294,15 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-email').value).toEqual('before@example.com');
- expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toBe(null);
+ expect(wrapper.find('.js-email').element.value).toEqual('before@example.com');
+ expect(wrapper.find('.js-email').attributes('readonly')).toBe(undefined);
});
});
describe('when installed', () => {
it('renders email in readonly', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
cert_manager: {
title: 'Cert-Manager',
email: 'after@example.com',
@@ -339,8 +311,8 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-email').value).toEqual('after@example.com');
- expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toEqual('readonly');
+ expect(wrapper.find('.js-email').element.value).toEqual('after@example.com');
+ expect(wrapper.find('.js-email').attributes('readonly')).toEqual('readonly');
});
});
});
@@ -348,9 +320,8 @@ describe('Applications', () => {
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
ingress: {
title: 'Ingress',
status: 'installed',
@@ -360,66 +331,56 @@ describe('Applications', () => {
});
expect(
- vm.$el
- .querySelector('.js-cluster-application-row-jupyter .js-hostname')
- .getAttribute('readonly'),
- ).toEqual(null);
+ wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
+ ).toEqual(undefined);
});
});
describe('with ingress installed without external ip', () => {
it('does not render hostname input', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed' },
},
});
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter .js-hostname')).toBe(
- null,
+ expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
+ false,
);
});
});
describe('with ingress & jupyter installed', () => {
it('renders readonly input', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
},
});
expect(
- vm.$el
- .querySelector('.js-cluster-application-row-jupyter .js-hostname')
- .getAttribute('readonly'),
+ wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
).toEqual('readonly');
});
});
describe('without ingress installed', () => {
beforeEach(() => {
- vm = mountComponent(Applications, {
- applications: APPLICATIONS_MOCK_STATE,
- });
+ createApp();
});
it('does not render input', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter .js-hostname')).toBe(
- null,
+ expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
+ false,
);
});
it('renders disabled install button', () => {
expect(
- vm.$el
- .querySelector(
- '.js-cluster-application-row-jupyter .js-cluster-application-install-button',
- )
- .getAttribute('disabled'),
+ wrapper
+ .find('.js-cluster-application-row-jupyter .js-cluster-application-install-button')
+ .attributes('disabled'),
).toEqual('disabled');
});
});
@@ -433,7 +394,6 @@ describe('Applications', () => {
};
const propsData = {
applications: {
- ...APPLICATIONS_MOCK_STATE,
knative: {
title: 'Knative',
hostname: 'example.com',
@@ -445,23 +405,15 @@ describe('Applications', () => {
},
},
};
- let wrapper;
let knativeDomainEditor;
beforeEach(() => {
- wrapper = shallowMount(Applications, {
- propsData,
- stubs: { ApplicationRow: ApplicationRowStub },
- });
+ createShallowApp(propsData);
jest.spyOn(eventHub, '$emit');
knativeDomainEditor = wrapper.find(KnativeDomainEditor);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
propsData.applications.knative.hostname = availableDomain.domain;
propsData.applications.knative.pagesDomain = availableDomain;
@@ -508,7 +460,6 @@ describe('Applications', () => {
describe('Crossplane application', () => {
const propsData = {
applications: {
- ...APPLICATIONS_MOCK_STATE,
crossplane: {
title: 'Crossplane',
stack: {
@@ -518,16 +469,8 @@ describe('Applications', () => {
},
};
- let wrapper;
- beforeEach(() => {
- wrapper = shallowMount(Applications, {
- propsData,
- stubs: { ApplicationRow: ApplicationRowStub },
- });
- });
- afterEach(() => {
- wrapper.destroy();
- });
+ beforeEach(() => createShallowApp(propsData));
+
it('renders the correct Component', () => {
const crossplane = wrapper.find(CrossplaneProviderStack);
expect(crossplane.exists()).toBe(true);
@@ -537,61 +480,42 @@ describe('Applications', () => {
describe('Elastic Stack application', () => {
describe('with elastic stack installable', () => {
it('renders hostname active input', () => {
- vm = mountComponent(Applications, {
- applications: {
- ...APPLICATIONS_MOCK_STATE,
- },
- });
+ createApp();
expect(
- vm.$el
- .querySelector(
+ wrapper
+ .find(
'.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
)
- .getAttribute('disabled'),
+ .attributes('disabled'),
).toEqual('disabled');
});
});
describe('elastic stack installed', () => {
it('renders uninstall button', () => {
- vm = mountComponent(Applications, {
+ createApp({
applications: {
- ...APPLICATIONS_MOCK_STATE,
elastic_stack: { title: 'Elastic Stack', status: 'installed' },
},
});
expect(
- vm.$el
- .querySelector(
+ wrapper
+ .find(
'.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
)
- .getAttribute('disabled'),
+ .attributes('disabled'),
).toEqual('disabled');
});
});
});
describe('Fluentd application', () => {
- const propsData = {
- applications: {
- ...APPLICATIONS_MOCK_STATE,
- },
- };
+ beforeEach(() => createShallowApp());
- let wrapper;
- beforeEach(() => {
- wrapper = shallowMount(Applications, {
- propsData,
- stubs: { ApplicationRow: ApplicationRowStub },
- });
- });
- afterEach(() => {
- wrapper.destroy();
- });
it('renders the correct Component', () => {
- expect(wrapper.contains(FluentdOutputSettings)).toBe(true);
+ expect(wrapper.find(FluentdOutputSettings).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 4969c591dcd..d63e793c43f 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -126,6 +126,8 @@ describe('text_utility', () => {
${'snake case'} | ${'snake_case'}
${'snake_case'} | ${'snake_case'}
${'snakeCasesnake Case'} | ${'snake_casesnake_case'}
+ ${'123'} | ${'123'}
+ ${'123 456'} | ${'123_456'}
`('converts string $txt to $result string', ({ txt, result }) => {
expect(textUtils.convertToSnakeCase(txt)).toEqual(result);
});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index 29bced394dc..691e98236e4 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -29,6 +29,7 @@ describe('ProjectSelector component', () => {
showMinimumSearchQueryMessage: false,
showLoadingIndicator: false,
showSearchErrorMessage: false,
+ totalResults: searchResults.length,
},
attachToDocument: true,
});
diff --git a/spec/graphql/mutations/container_expiration_policies/update_spec.rb b/spec/graphql/mutations/container_expiration_policies/update_spec.rb
new file mode 100644
index 00000000000..fc90f437576
--- /dev/null
+++ b/spec/graphql/mutations/container_expiration_policies/update_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::ContainerExpirationPolicies::Update do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:container_expiration_policy) { project.container_expiration_policy }
+ let(:params) { { project_path: project.full_path, cadence: '3month', keep_n: 100, older_than: '14d' } }
+
+ specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
+
+ describe '#resolve' do
+ subject { described_class.new(object: project, context: { current_user: user }, field: nil).resolve(params) }
+
+ RSpec.shared_examples 'returning a success' do
+ it 'returns the container expiration policy with no errors' do
+ expect(subject).to eq(
+ container_expiration_policy: container_expiration_policy,
+ errors: []
+ )
+ end
+ end
+
+ RSpec.shared_examples 'updating the container expiration policy' do
+ it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
+
+ it_behaves_like 'returning a success'
+
+ context 'with invalid params' do
+ let_it_be(:params) { { project_path: project.full_path, cadence: '20d' } }
+
+ it_behaves_like 'not creating the container expiration policy'
+
+ it "doesn't update the cadence" do
+ expect { subject }
+ .not_to change { container_expiration_policy.reload.cadence }
+ end
+
+ it 'returns an error' do
+ expect(subject).to eq(
+ container_expiration_policy: nil,
+ errors: ['Cadence is not included in the list']
+ )
+ end
+ end
+ end
+
+ RSpec.shared_examples 'denying access to container expiration policy' do
+ it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with existing container expiration policy' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'updating the container expiration policy'
+ :developer | 'updating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'without existing container expiration policy' do
+ let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) }
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'creating the container expiration policy'
+ :developer | 'creating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
new file mode 100644
index 00000000000..bc256a08f00
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Updating the container expiration policy' do
+ include GraphqlHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:container_expiration_policy) { project.container_expiration_policy.reload }
+ let(:params) do
+ {
+ project_path: project.full_path,
+ cadence: 'EVERY_THREE_MONTHS',
+ keep_n: 'ONE_HUNDRED_TAGS',
+ older_than: 'FOURTEEN_DAYS'
+ }
+ end
+ let(:mutation) do
+ graphql_mutation(:update_container_expiration_policy, params,
+ <<~QL
+ containerExpirationPolicy {
+ cadence
+ keepN
+ nameRegexKeep
+ nameRegex
+ olderThan
+ }
+ errors
+ QL
+ )
+ end
+ let(:mutation_response) { graphql_mutation_response(:update_container_expiration_policy) }
+ let(:container_expiration_policy_response) { mutation_response['containerExpirationPolicy'] }
+
+ RSpec.shared_examples 'returning a success' do
+ it_behaves_like 'returning response status', :success
+
+ it 'returns the updated container expiration policy' do
+ subject
+
+ expect(mutation_response['errors']).to be_empty
+ expect(container_expiration_policy_response['cadence']).to eq(params[:cadence])
+ expect(container_expiration_policy_response['keepN']).to eq(params[:keep_n])
+ expect(container_expiration_policy_response['olderThan']).to eq(params[:older_than])
+ end
+ end
+
+ RSpec.shared_examples 'updating the container expiration policy' do
+ it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
+
+ it_behaves_like 'returning a success'
+ end
+
+ RSpec.shared_examples 'denying access to container expiration policy' do
+ it_behaves_like 'not creating the container expiration policy'
+
+ it_behaves_like 'returning response status', :success
+
+ it 'returns no response' do
+ subject
+
+ expect(mutation_response).to be_nil
+ end
+ end
+
+ describe 'post graphql mutation' do
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ context 'with existing container expiration policy' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'updating the container expiration policy'
+ :developer | 'updating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'without existing container expiration policy' do
+ let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) }
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'creating the container expiration policy'
+ :developer | 'creating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+ end
+end
diff --git a/spec/services/concerns/exclusive_lease_guard_spec.rb b/spec/services/concerns/exclusive_lease_guard_spec.rb
new file mode 100644
index 00000000000..a38facc7520
--- /dev/null
+++ b/spec/services/concerns/exclusive_lease_guard_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExclusiveLeaseGuard, :clean_gitlab_redis_shared_state do
+ subject :subject_class do
+ Class.new do
+ include ExclusiveLeaseGuard
+
+ def self.name
+ 'ExclusiveLeaseGuardTestClass'
+ end
+
+ def call(&block)
+ try_obtain_lease do
+ internal_method(&block)
+ end
+ end
+
+ def internal_method
+ yield
+ end
+
+ def lease_timeout
+ 1.second
+ end
+ end
+ end
+
+ describe '#try_obtain_lease' do
+ let(:subject) { subject_class.new }
+
+ it 'obtains the lease, calls internal_method and releases the lease', :aggregate_failures do
+ expect(subject).to receive(:internal_method).and_call_original
+
+ subject.call do
+ expect(subject.exclusive_lease.exists?).to be_truthy
+ end
+
+ expect(subject.exclusive_lease.exists?).to be_falsey
+ end
+
+ context 'when the lease is already obtained' do
+ before do
+ subject.exclusive_lease.try_obtain
+ end
+
+ after do
+ subject.exclusive_lease.cancel
+ end
+
+ it 'does not call internal_method but logs error', :aggregate_failures do
+ expect(subject).not_to receive(:internal_method)
+ expect(Gitlab::AppLogger).to receive(:error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+
+ subject.call
+ end
+ end
+
+ context 'with overwritten lease_release?' do
+ subject :overwritten_subject_class do
+ Class.new(subject_class) do
+ def lease_release?
+ false
+ end
+ end
+ end
+
+ let(:subject) { overwritten_subject_class.new }
+
+ it 'does not release the lease after execution', :aggregate_failures do
+ subject.call do
+ expect(subject.exclusive_lease.exists?).to be_truthy
+ end
+
+ expect(subject.exclusive_lease.exists?).to be_truthy
+ end
+ end
+ end
+
+ describe '#exclusive_lease' do
+ it 'uses the class name as lease key' do
+ expect(Gitlab::ExclusiveLease).to receive(:new).with('exclusive_lease_guard_test_class', timeout: 1.second)
+
+ subject_class.new.exclusive_lease
+ end
+
+ context 'with overwritten lease_key' do
+ subject :overwritten_class do
+ Class.new(subject_class) do
+ def lease_key
+ 'other_lease_key'
+ end
+ end
+ end
+
+ it 'uses the custom lease key' do
+ expect(Gitlab::ExclusiveLease).to receive(:new).with('other_lease_key', timeout: 1.second)
+
+ overwritten_class.new.exclusive_lease
+ end
+ end
+ end
+
+ describe '#release_lease' do
+ it 'sends a cancel message to ExclusiveLease' do
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).with('exclusive_lease_guard_test_class', 'some_uuid')
+
+ subject_class.new.release_lease('some_uuid')
+ end
+ end
+
+ describe '#renew_lease!' do
+ let(:subject) { subject_class.new }
+
+ it 'sends a renew message to the exclusive_lease instance' do
+ expect(subject.exclusive_lease).to receive(:renew)
+ subject.renew_lease!
+ end
+ end
+end
diff --git a/spec/services/container_expiration_policies/update_service_spec.rb b/spec/services/container_expiration_policies/update_service_spec.rb
new file mode 100644
index 00000000000..ec178f3830f
--- /dev/null
+++ b/spec/services/container_expiration_policies/update_service_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ContainerExpirationPolicies::UpdateService do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:params) { { cadence: '3month', keep_n: 100, older_than: '14d', extra_key: 'will_not_be_processed' } }
+
+ let(:container_expiration_policy) { project.container_expiration_policy }
+
+ describe '#execute' do
+ subject { described_class.new(container: project, current_user: user, params: params).execute }
+
+ RSpec.shared_examples 'returning a success' do
+ it 'returns a success' do
+ result = subject
+
+ expect(result.payload[:container_expiration_policy]).to be_present
+ expect(result.success?).to be_truthy
+ end
+ end
+
+ RSpec.shared_examples 'returning an error' do |message, http_status|
+ it 'returns an error' do
+ result = subject
+
+ expect(result.message).to eq(message)
+ expect(result.status).to eq(:error)
+ expect(result.http_status).to eq(http_status)
+ end
+ end
+
+ RSpec.shared_examples 'updating the container expiration policy' do
+ it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
+
+ it_behaves_like 'returning a success'
+
+ context 'with invalid params' do
+ let_it_be(:params) { { cadence: '20d' } }
+
+ it_behaves_like 'not creating the container expiration policy'
+
+ it "doesn't update the cadence" do
+ expect { subject }
+ .not_to change { container_expiration_policy.reload.cadence }
+ end
+
+ it_behaves_like 'returning an error', 'Cadence is not included in the list', 400
+ end
+ end
+
+ RSpec.shared_examples 'denying access to container expiration policy' do
+ context 'with existing container expiration policy' do
+ it_behaves_like 'not creating the container expiration policy'
+
+ it_behaves_like 'returning an error', 'Access Denied', 403
+ end
+ end
+
+ context 'with existing container expiration policy' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'updating the container expiration policy'
+ :developer | 'updating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'without existing container expiration policy' do
+ let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) }
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'creating the container expiration policy'
+ :developer | 'creating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb
new file mode 100644
index 00000000000..28bf46a57d5
--- /dev/null
+++ b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'updating the container expiration policy attributes' do |mode:, from: {}, to:|
+ if mode == :create
+ it 'creates a new container expiration policy' do
+ expect { subject }
+ .to change { project.reload.container_expiration_policy.present? }.from(false).to(true)
+ .and change { ContainerExpirationPolicy.count }.by(1)
+ end
+ else
+ it_behaves_like 'not creating the container expiration policy'
+ end
+
+ it 'updates the container expiration policy' do
+ if from.empty?
+ subject
+
+ expect(container_expiration_policy.reload.cadence).to eq(to[:cadence])
+ expect(container_expiration_policy.keep_n).to eq(to[:keep_n])
+ expect(container_expiration_policy.older_than).to eq(to[:older_than])
+ else
+ expect { subject }
+ .to change { container_expiration_policy.reload.cadence }.from(from[:cadence]).to(to[:cadence])
+ .and change { container_expiration_policy.reload.keep_n }.from(from[:keep_n]).to(to[:keep_n])
+ .and change { container_expiration_policy.reload.older_than }.from(from[:older_than]).to(to[:older_than])
+ end
+ end
+end
+
+RSpec.shared_examples 'not creating the container expiration policy' do
+ it "doesn't create the container expiration policy" do
+ expect { subject }.not_to change { ContainerExpirationPolicy.count }
+ end
+end
+
+RSpec.shared_examples 'creating the container expiration policy' do
+ it_behaves_like 'updating the container expiration policy attributes', mode: :create, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
+
+ it_behaves_like 'returning a success'
+end