diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-03 21:08:23 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-03 21:08:23 +0000 |
commit | fc92738a0245f1be88250448bebd9c20e9849444 (patch) | |
tree | 5e66f8a08c2b5dfa9cd76d28b0fe0a6a409ca616 | |
parent | 7484304eaa266f22f048a76d490494b6337c9555 (diff) | |
download | gitlab-ce-fc92738a0245f1be88250448bebd9c20e9849444.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 |