diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-27 15:10:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-27 15:10:33 +0300 |
commit | fdf32113c3924f7faec91101282fc28ec42fc869 (patch) | |
tree | 388fdb9982d5ae80c8bc9b9bdcc0dde98cd6ead9 | |
parent | 5add82515889cf332b65bbf59394079222dc66b3 (diff) |
Add latest changes from gitlab-org/gitlab@master
46 files changed, 1393 insertions, 896 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index d3d63955b9e..3a55d0f6747 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -4,7 +4,6 @@ when: - api_failure - data_integrity_failure - - job_execution_timeout - runner_system_failure - scheduler_failure - stuck_or_timeout_failure diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 6c3bb88f439..1e0795f98af 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -14.23.0 +14.24.1 diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index d1e5e4eea13..aceae188b73 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -15,6 +15,7 @@ export const DATETIME_RANGE_TYPES = { export const BV_SHOW_MODAL = 'bv::show::modal'; export const BV_HIDE_MODAL = 'bv::hide::modal'; export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip'; +export const BV_SHOW_TOOLTIP = 'bv::show::tooltip'; export const BV_DROPDOWN_SHOW = 'bv::dropdown::show'; export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide'; diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index a34ed065323..bdb78bab909 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -21,7 +21,13 @@ import projectInfoQuery from '../queries/project_info.query.graphql'; import getRefMixin from '../mixins/get_ref'; import userInfoQuery from '../queries/user_info.query.graphql'; import applicationInfoQuery from '../queries/application_info.query.graphql'; -import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants'; +import { + DEFAULT_BLOB_INFO, + TEXT_FILE_TYPE, + LFS_STORAGE, + LEGACY_FILE_TYPES, + CODEOWNERS_FILE_NAME, +} from '../constants'; import BlobButtonGroup from './blob_button_group.vue'; import ForkSuggestion from './fork_suggestion.vue'; import { loadViewer } from './blob_viewers'; @@ -32,6 +38,7 @@ export default { BlobButtonGroup, BlobContent, GlLoadingIcon, + CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'), GlButton, ForkSuggestion, WebIdeLink, @@ -79,7 +86,7 @@ export default { const queryVariables = { projectPath: this.projectPath, filePath: this.path, - ref: this.originalBranch || this.ref, + ref: this.currentRef, refType: this.refType?.toUpperCase() || null, shouldFetchRawText: true, }; @@ -171,6 +178,12 @@ export default { return nodes[0] || {}; }, + currentRef() { + return this.originalBranch || this.ref; + }, + isCodeownersFile() { + return this.path.includes(CODEOWNERS_FILE_NAME); + }, viewer() { const { richViewer, simpleViewer } = this.blobInfo; return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer; @@ -402,6 +415,12 @@ export default { :fork-path="forkPath" @cancel="setForkTarget(null)" /> + <codeowners-validation + v-if="isCodeownersFile" + :current-ref="currentRef" + :project-path="projectPath" + :file-path="path" + /> <blob-content v-if="!blobViewer" class="js-syntax-highlight" diff --git a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue index 014f1abc121..9a8bb8e4aa6 100644 --- a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue @@ -8,7 +8,7 @@ export default { }, data() { return { - url: this.blob.rawPath, + url: this.blob.externalStorageUrl || this.blob.rawPath, alt: this.blob.name, }; }, diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js index b711f671850..4327b237c8c 100644 --- a/app/assets/javascripts/repository/constants.js +++ b/app/assets/javascripts/repository/constants.js @@ -114,3 +114,5 @@ export const POLLING_INTERVAL_BACKOFF = 2; export const CONFLICTS_MODAL_ID = 'fork-sync-conflicts-modal'; export const FORK_UPDATED_EVENT = 'fork:updated'; + +export const CODEOWNERS_FILE_NAME = 'CODEOWNERS'; diff --git a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue index feff3f77dd2..bca049e56c7 100644 --- a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue @@ -27,7 +27,7 @@ export default { }, }, computed: { - ...mapState(['query', 'useNewNavigation']), + ...mapState(['query', 'useSidebarNavigation']), ...mapGetters(['queryLanguageFilters']), dataFilters() { return Object.values(this.filtersData?.filters || []); @@ -69,7 +69,9 @@ export default { <template> <div class="gl-mx-5"> - <h5 class="gl-mt-0" :class="{ 'gl-font-sm': useNewNavigation }">{{ filtersData.header }}</h5> + <h5 class="gl-mt-0" :class="{ 'gl-font-sm': useSidebarNavigation }"> + {{ filtersData.header }} + </h5> <gl-form-checkbox-group v-model="selectedFilter"> <gl-form-checkbox v-for="f in dataFilters" diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue index 7909aa9234b..312092b9904 100644 --- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue @@ -10,7 +10,7 @@ export default { RadioFilter, }, computed: { - ...mapState(['useNewNavigation']), + ...mapState(['useSidebarNavigation']), }, confidentialFilterData, HR_DEFAULT_CLASSES, diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue index 3eb025327a2..5fd8b6418d7 100644 --- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue +++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue @@ -28,7 +28,7 @@ export default { }, mixins: [glFeatureFlagsMixin()], computed: { - ...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']), + ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']), ...mapGetters(['currentScope']), showReset() { return this.urlQuery.state || this.urlQuery.confidential || this.urlQuery.labels; @@ -69,12 +69,12 @@ export default { <template> <form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking"> - <hr v-if="!useNewNavigation" :class="hrClasses" /> + <hr v-if="!useSidebarNavigation" :class="hrClasses" /> <status-filter v-if="showStatusFilter" class="gl-mb-5" /> - <hr v-if="!useNewNavigation" :class="hrClasses" /> + <hr v-if="!useSidebarNavigation" :class="hrClasses" /> <confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" /> <hr - v-if="!useNewNavigation && showConfidentialityFilter && showLabelFilter" + v-if="!useSidebarNavigation && showConfidentialityFilter && showLabelFilter" :class="hrClasses" /> <label-filter v-if="showLabelFilter" /> diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue index b820ca837bc..b42fe9185cb 100644 --- a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue @@ -27,7 +27,7 @@ export default { }, }, computed: { - ...mapState(['query', 'useNewNavigation']), + ...mapState(['query', 'useSidebarNavigation']), ...mapGetters(['queryLanguageFilters']), dataFilters() { return Object.values(this.filtersData?.filters || []); diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue index c10b14bd116..e5560dd5b55 100644 --- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue @@ -36,7 +36,7 @@ export default { reset: s__('GlobalSearch|Reset filters'), }, computed: { - ...mapState(['aggregations', 'sidebarDirty', 'useNewNavigation']), + ...mapState(['aggregations', 'sidebarDirty', 'useSidebarNavigation']), ...mapGetters([ 'languageAggregationBuckets', 'currentUrlQueryHasLanguageFilters', @@ -120,8 +120,8 @@ export default { class="gl-m-5 gl-my-0 language-filter-checkbox" @submit.prevent="submitQuery" > - <hr v-if="!useNewNavigation" :class="dividerClassesTop" /> - <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useNewNavigation }"> + <hr v-if="!useSidebarNavigation" :class="dividerClassesTop" /> + <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }"> {{ $options.languageFilterData.header }} </h5> <div @@ -153,7 +153,7 @@ export default { </gl-button> </div> <div v-if="!aggregations.error"> - <hr v-if="!useNewNavigation" :class="dividerClassesBottom" /> + <hr v-if="!useSidebarNavigation" :class="dividerClassesBottom" /> <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-mt-4"> <gl-button category="primary" diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue index 10ece1b82eb..8ad403ab31b 100644 --- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue @@ -16,7 +16,7 @@ export default { }, }, computed: { - ...mapState(['query', 'useNewNavigation']), + ...mapState(['query', 'useSidebarNavigation']), ...mapGetters(['currentScope']), ANY() { return this.filterData.filters.ANY; @@ -56,7 +56,7 @@ export default { <template> <div> - <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useNewNavigation }"> + <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }"> {{ filterData.header }} </h5> <gl-form-radio-group v-model="selectedFilter"> diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue index a9addb87f7b..88e434cf99e 100644 --- a/app/assets/javascripts/search/sidebar/components/results_filters.vue +++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue @@ -16,7 +16,7 @@ export default { ConfidentialityFilter, }, computed: { - ...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']), + ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']), ...mapGetters(['currentScope']), showReset() { return this.urlQuery.state || this.urlQuery.confidential; @@ -39,7 +39,7 @@ export default { <template> <form class="gl-pt-5 gl-md-pt-0" @submit.prevent="applyQuery"> - <hr v-if="!useNewNavigation" :class="hrClasses" /> + <hr v-if="!useSidebarNavigation" :class="hrClasses" /> <status-filter v-if="showStatusFilter" /> <confidentiality-filter v-if="showConfidentialityFilter" /> <div class="gl-display-flex gl-align-items-center gl-mt-4 gl-px-5"> diff --git a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue index 494d75db6ce..010cfbad590 100644 --- a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue @@ -10,7 +10,7 @@ export default { RadioFilter, }, computed: { - ...mapState(['useNewNavigation']), + ...mapState(['useSidebarNavigation']), }, statusFilterData, HR_DEFAULT_CLASSES, diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 2e88b45d646..613e504c771 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -263,6 +263,11 @@ span.idiff { } } +.file-validation { + // we use $gray-light variable instead of utility class, because it's value is dynamic per color theme + background-color: $gray-light; +} + .blob-content-holder .file-actions { @include media-breakpoint-down(sm) { .btn { diff --git a/app/assets/stylesheets/framework/new_card.scss b/app/assets/stylesheets/framework/new_card.scss index 48a834858c6..411f5300120 100644 --- a/app/assets/stylesheets/framework/new_card.scss +++ b/app/assets/stylesheets/framework/new_card.scss @@ -95,6 +95,10 @@ // Table adjustments @mixin new-card-table-adjustments { tbody > tr { + &:first-of-type > td[data-label] { + @include gl-border-t-0; + } + > td[data-label] { @include gl-border-left-0; @include gl-border-l-none; @@ -119,8 +123,15 @@ table.b-table-stacked-sm, table.b-table-stacked-md { - @include gl-mt-n1; - @include gl-mb-n2; + @include gl-mb-0; + + tr:first-of-type th { + @include gl-border-t-0; + } + + tr:last-of-type td { + @include gl-border-b-0; + } } table.gl-table.b-table.b-table-stacked-sm { diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index da70361743b..1ce852d4f71 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -5,6 +5,7 @@ module Ci prepend Ci::BulkInsertableTags include Ci::Metadatable include Ci::Contextable + include Ci::Deployable include TokenAuthenticatable include AfterCommitQueue include Presentable @@ -34,7 +35,6 @@ module Ci DEPLOYMENT_NAMES = %w[deploy release rollout].freeze - has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable has_one :pending_state, class_name: 'Ci::BuildPendingState', foreign_key: :build_id, inverse_of: :build has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id, inverse_of: :build has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id, inverse_of: :build @@ -327,7 +327,6 @@ module Ci after_transition any => [:success] do |build| build.run_after_commit do - BuildSuccessWorker.perform_async(id) PagesWorker.perform_async(:deploy, id) if build.pages_generator? end end @@ -345,18 +344,6 @@ module Ci end end end - - # Synchronize Deployment Status - # Please note that the data integirty is not assured because we can't use - # a database transaction due to DB decomposition. - after_transition do |build, transition| - next if transition.loopback? - next unless build.project - - build.run_after_commit do - build.deployment&.sync_status_with(build) - end - end end def self.build_matchers(project) @@ -428,15 +415,6 @@ module Ci action? && !archived? && (manual? || scheduled? || retryable?) end - def outdated_deployment? - strong_memoize(:outdated_deployment) do - deployment_job? && - project.ci_forward_deployment_enabled? && - (!project.ci_forward_deployment_rollback_allowed? || incomplete?) && - deployment&.older_than_last_successful_deployment? - end - end - def schedulable? self.when == 'delayed' && options[:start_in].present? end @@ -478,94 +456,10 @@ module Ci Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet end - def persisted_environment - return unless has_environment_keyword? - - strong_memoize(:persisted_environment) do - # This code path has caused N+1s in the past, since environments are only indirectly - # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445 - # We therefore batch-load them to prevent dormant N+1s until we found a proper solution. - BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args| - Environment.where(name: names, project: args[:key]).find_each do |environment| - loader.call(environment.name, environment) - end - end - end - end - - def persisted_environment=(environment) - strong_memoize(:persisted_environment) { environment } - end - - # If build.persisted_environment is a BatchLoader, we need to remove - # the method proxy in order to clone into new item here - # https://github.com/exAspArk/batch-loader/issues/31 - def actual_persisted_environment - persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment - end - - def expanded_environment_name - return unless has_environment_keyword? - - strong_memoize(:expanded_environment_name) do - # We're using a persisted expanded environment name in order to avoid - # variable expansion per request. - if metadata&.expanded_environment_name.present? - metadata.expanded_environment_name - else - ExpandVariables.expand(environment, -> { simple_variables.sort_and_expand_all }) - end - end - end - - def expanded_kubernetes_namespace - return unless has_environment_keyword? - - namespace = options.dig(:environment, :kubernetes, :namespace) - - if namespace.present? - strong_memoize(:expanded_kubernetes_namespace) do - ExpandVariables.expand(namespace, -> { simple_variables }) - end - end - end - - def has_environment_keyword? - environment.present? - end - - def deployment_job? - has_environment_keyword? && environment_action == 'start' - end - - def stops_environment? - has_environment_keyword? && environment_action == 'stop' - end - - def environment_action - options.fetch(:environment, {}).fetch(:action, 'start') if options - end - - def environment_tier_from_options - options.dig(:environment, :deployment_tier) if options - end - - def environment_tier - environment_tier_from_options || persisted_environment.try(:tier) - end - def triggered_by?(current_user) user == current_user end - def on_stop - options&.dig(:environment, :on_stop) - end - - def stop_action_successful? - success? - end - ## # All variables, including persisted environment variables. # @@ -1033,19 +927,6 @@ module Ci job_artifacts.all_reports end - # Virtual deployment status depending on the environment status. - def deployment_status - return unless deployment_job? - - if success? - return successful_deployment_status - elsif failed? - return :failed - end - - :creating - end - # Consider this object to have a structural integrity problems def doom! transaction do @@ -1206,31 +1087,11 @@ module Ci strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) } end - def successful_deployment_status - if deployment&.last? - :last - else - :out_of_date - end - end - def job_artifacts_for_types(report_types) # Use select to leverage cached associations and avoid N+1 queries job_artifacts.select { |artifact| artifact.file_type.in?(report_types) } end - def environment_url - options&.dig(:environment, :url) || persisted_environment&.external_url - end - - def environment_status - strong_memoize(:environment_status) do - if has_environment_keyword? && merge_request - EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha) - end - end - end - def has_expiring_artifacts? artifacts_expire_at.present? && artifacts_expire_at > Time.current end diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb new file mode 100644 index 00000000000..ffdb38f76ac --- /dev/null +++ b/app/models/concerns/ci/deployable.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +# rubocop:disable Gitlab/StrongMemoizeAttr +module Ci + module Deployable + extend ActiveSupport::Concern + + included do + has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable + + state_machine :status do + after_transition any => [:success] do |job| + job.run_after_commit do + Environments::StopJobSuccessWorker.perform_async(id) + end + end + + # Synchronize Deployment Status + # Please note that the data integirty is not assured because we can't use + # a database transaction due to DB decomposition. + after_transition do |job, transition| + next if transition.loopback? + next unless job.project + + job.run_after_commit do + job.deployment&.sync_status_with(job) + end + end + end + end + + def outdated_deployment? + strong_memoize(:outdated_deployment) do + deployment_job? && + project.ci_forward_deployment_enabled? && + (!project.ci_forward_deployment_rollback_allowed? || incomplete?) && + deployment&.older_than_last_successful_deployment? + end + end + + # Virtual deployment status depending on the environment status. + def deployment_status + return unless deployment_job? + + if success? + return successful_deployment_status + elsif failed? + return :failed + end + + :creating + end + + def successful_deployment_status + if deployment&.last? + :last + else + :out_of_date + end + end + + def persisted_environment + return unless has_environment_keyword? + + strong_memoize(:persisted_environment) do + # This code path has caused N+1s in the past, since environments are only indirectly + # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445 + # We therefore batch-load them to prevent dormant N+1s until we found a proper solution. + BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args| + Environment.where(name: names, project: args[:key]).find_each do |environment| + loader.call(environment.name, environment) + end + end + end + end + + def persisted_environment=(environment) + strong_memoize(:persisted_environment) { environment } + end + + # If build.persisted_environment is a BatchLoader, we need to remove + # the method proxy in order to clone into new item here + # https://github.com/exAspArk/batch-loader/issues/31 + def actual_persisted_environment + persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment + end + + def expanded_environment_name + return unless has_environment_keyword? + + strong_memoize(:expanded_environment_name) do + # We're using a persisted expanded environment name in order to avoid + # variable expansion per request. + if metadata&.expanded_environment_name.present? + metadata.expanded_environment_name + else + ExpandVariables.expand(environment, -> { simple_variables.sort_and_expand_all }) + end + end + end + + def expanded_kubernetes_namespace + return unless has_environment_keyword? + + namespace = options.dig(:environment, :kubernetes, :namespace) + + if namespace.present? # rubocop:disable Style/GuardClause + strong_memoize(:expanded_kubernetes_namespace) do + ExpandVariables.expand(namespace, -> { simple_variables }) + end + end + end + + def has_environment_keyword? + environment.present? + end + + def deployment_job? + has_environment_keyword? && environment_action == 'start' + end + + def stops_environment? + has_environment_keyword? && environment_action == 'stop' + end + + def environment_action + options.fetch(:environment, {}).fetch(:action, 'start') if options + end + + def environment_tier_from_options + options.dig(:environment, :deployment_tier) if options + end + + def environment_tier + environment_tier_from_options || persisted_environment.try(:tier) + end + + def environment_url + options&.dig(:environment, :url) || persisted_environment&.external_url + end + + def environment_status + strong_memoize(:environment_status) do + if has_environment_keyword? && merge_request + EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha) + end + end + end + + def on_stop + options&.dig(:environment, :on_stop) + end + + def stop_action_successful? + success? + end + end +end +# rubocop:enable Gitlab/StrongMemoizeAttr diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index e32a50e252d..063045033b6 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -1,52 +1,51 @@ - page_title s_('AdminArea|Instance OAuth applications') -%h1.page-title.gl-font-size-h-display - = s_('AdminArea|Instance OAuth applications') -%p.light - - docs_link_path = help_page_path('integration/oauth_provider') - - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: docs_link_path } - = s_('AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe } += render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-toggle-container' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c| + - c.with_header do + .gl-new-card-title-wrapper.gl-flex-direction-column + %h3.gl-new-card-title + = s_('AdminArea|Instance OAuth applications') + .gl-new-card-count + = sprite_icon('applications', css_class: 'gl-mr-2') + = @applications.size + %p.gl-new-card-description + - docs_link_path = help_page_path('integration/oauth_provider') + - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: docs_link_path } + = s_('AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe } -- if @applications.empty? - %section.empty-state.gl-text-center.gl-display-flex.gl-flex-direction-column - .svg-content.svg-150 - = image_tag 'illustrations/empty-state/empty-admin-apps-md.svg', class: 'gl-max-w-full' - - .gl-max-w-full.gl-m-auto - %h1.h4.gl-font-size-h-display= s_('AdminArea|No applications found') - = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm, button_options: { data: { qa_selector: 'new_application_button' } }) do + .gl-new-card-actions + = render Pajamas::ButtonComponent.new(size: :small, href: new_admin_application_path, button_options: { data: { qa_selector: 'new_application_button' } }) do = _('New application') + - c.with_body do + - if @applications.empty? + %section.empty-state.gl-my-5.gl-text-center.gl-display-flex.gl-flex-direction-column + .svg-content.svg-150 + = image_tag 'illustrations/empty-state/empty-admin-apps-md.svg', class: 'gl-max-w-full' -- else - %hr - = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm, button_options: { data: { qa_selector: 'new_application_button' } }) do - = _('New application') - - .table-responsive - %table.b-table.gl-table.gl-w-full{ role: 'table' } - %thead - %tr - %th - = _('Name') - %th - = _('Callback URL') - %th - = _('Trusted') - %th - = _('Confidential') - %th - %th - %tbody.oauth-applications - - @applications.each do |application| - %tr{ id: "application_#{application.id}" } - %td= link_to application.name, admin_application_path(application) - %td= application.redirect_uri - %td= application.trusted? ? _('Yes'): _('No') - %td= application.confidential? ? _('Yes'): _('No') - %td - = render Pajamas::ButtonComponent.new(href: edit_admin_application_path(application), variant: :link) do - = _('Edit') - %td= render 'delete_form', application: application + .gl-max-w-full.gl-m-auto + %h1.h4.gl-font-size-h-display= s_('AdminArea|No applications found') + %p.gl-text-secondary.gl-mt-3= s_('AdminArea|Manage applications for your instance that can use GitLab as an OAuth provider, start by creating a new one above.') + - else + .table-holder + %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' } + %thead + %tr + %th= _('Name') + %th= _('Callback URL') + %th= _('Trusted') + %th= _('Confidential') + %th= _('Actions') + %tbody.oauth-applications + - @applications.each do |application| + %tr{ id: "application_#{application.id}" } + %td{ data: { label: _('Name') } }= link_to application.name, admin_application_path(application) + %td{ data: { label: _('Callback URL') } }= application.redirect_uri + %td{ data: { label: _('Trusted') } }= application.trusted? ? _('Yes'): _('No') + %td{ data: { label: _('Confidential') } }= application.confidential? ? _('Yes'): _('No') + %td{ data: { label: _('Actions') } } + = render Pajamas::ButtonComponent.new(href: edit_admin_application_path(application), size: :small, button_options: { class: 'gl-mr-3' }) do + = _('Edit') + = render 'delete_form', application: application = paginate @applications, theme: 'gitlab' diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index e41c85d1550..89bb1d11d1d 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2712,6 +2712,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: environments_stop_job_success + :worker_name: Environments::StopJobSuccessWorker + :feature_category: :continuous_delivery + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: error_tracking_issue_link :worker_name: ErrorTrackingIssueLinkWorker :feature_category: :error_tracking diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index 247105d2a1a..f5baa220715 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# Deprecated and will be removed in 17.0. +# Use `Environments::StopJobSuccessWorker` instead. class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker include ApplicationWorker diff --git a/app/workers/environments/stop_job_success_worker.rb b/app/workers/environments/stop_job_success_worker.rb new file mode 100644 index 00000000000..cc7d83512f3 --- /dev/null +++ b/app/workers/environments/stop_job_success_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Environments + class StopJobSuccessWorker + include ApplicationWorker + + data_consistency :delayed + idempotent! + feature_category :continuous_delivery + + def perform(job_id, _params = {}) + Ci::Build.find_by_id(job_id).try do |build| + stop_environment(build) if build.stops_environment? && build.stop_action_successful? + end + end + + private + + def stop_environment(build) + build.persisted_environment.fire_state_event(:stop_complete) + end + end +end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 4f6889bf445..31b058e9e85 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -227,6 +227,8 @@ - 1 - - environments_canary_ingress_update - 1 +- - environments_stop_job_success + - 1 - - epics - 2 - - epics_new_epic_issue diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md index 4102d114066..870a342ef9a 100644 --- a/doc/administration/dedicated/index.md +++ b/doc/administration/dedicated/index.md @@ -63,6 +63,8 @@ The keys provided have to reside in the same primary, secondary and backup regio For instructions on how to create and manage KMS keys, visit [Managing keys](https://docs.aws.amazon.com/kms/latest/developerguide/getting-started.html) in the AWS KMS documentation. +GitLab Dedicated supports only AWS managed KMS keys with KMS [as key material](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-origin). + To create a KMS key using the AWS Console: 1. In `Configure key`, select: diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 85b42379c5c..a1c3b1220eb 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -259,6 +259,10 @@ Use **cannot** instead of **can not**. See also [contractions](index.md#contractions). +## Chat, GitLab Duo Chat + +Use **Chat** with a capital `c` for **Chat** or **GitLab Duo Chat**. + ## checkbox Use one word for **checkbox**. Do not use **check box**. @@ -621,6 +625,10 @@ Use title case for **Geo**. Do not make **GitLab** possessive (GitLab's). This guidance follows [GitLab Trademark Guidelines](https://about.gitlab.com/handbook/marketing/brand-and-product-marketing/brand/brand-activation/trademark-guidelines/). +## GitLab Duo + +Do not use **Duo** by itself. Always use **GitLab Duo**. + ## GitLab Flavored Markdown When possible, spell out [**GitLab Flavored Markdown**](../../../user/markdown.md). diff --git a/doc/topics/release_your_application.md b/doc/topics/release_your_application.md index 3cc5e9a66b3..c3a504c59f2 100644 --- a/doc/topics/release_your_application.md +++ b/doc/topics/release_your_application.md @@ -28,7 +28,6 @@ release features incrementally. - [Auto Deploy](autodevops/stages.md#auto-deploy) is the DevOps stage dedicated to software deployment using GitLab CI/CD. Auto Deploy has built-in support for EC2 and ECS deployments. - Deploy to Kubernetes clusters by using the [GitLab agent](../user/clusters/agent/install/index.md). -- View an example of [how to structure a GitOps deployment repository](../user/clusters/agent/gitops/example_repository_structure.md). - Use Docker images to run AWS commands from GitLab CI/CD, and a template to facilitate [deployment to AWS](../ci/cloud_deployment). - Use GitLab CI/CD to target any type of infrastructure accessible by GitLab Runner. diff --git a/doc/user/clusters/agent/gitops/example_repository_structure.md b/doc/user/clusters/agent/gitops/example_repository_structure.md index a5bc3b153fe..5c5970c9a54 100644 --- a/doc/user/clusters/agent/gitops/example_repository_structure.md +++ b/doc/user/clusters/agent/gitops/example_repository_structure.md @@ -1,78 +1,177 @@ --- stage: Deploy group: Environments -info: An example of how to structure a repository for GitOps deployments +info: A tutorial for structuring a repository for GitOps deployments --- -# Example GitOps repository structure **(FREE)** +# Tutorial: Structure your repository for GitOps deployments **(FREE)** -This page describes an example structure for a project that builds and deploys an application -to a Kubernetes cluster with [GitOps](https://about.gitlab.com/topics/gitops) and the -[GitLab agent for Kubernetes](../../agent/gitops.md). +In this tutorial, you'll create a GitLab project that builds and deploys an application +to a Kubernetes cluster using Flux. You'll set up a sample manifest project, configure it to +push manifests to a deployment branch, and configure Flux to sync the deployment branch. + +This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md). + +To set up a repository for GitOps deployments: + +1. [Create the Kubernetes manifest repository](#create-the-kubernetes-manifest-repository) +1. [Create a deployment branch](#create-a-deployment-branch) +1. [Configure GitLab CI/CD to push to your branch](#configure-gitlab-cicd-to-push-to-your-branch) +1. [Configure Flux to sync your manifests](#configure-flux-to-sync-your-manifests) +1. [Verify your configuration](#verify-your-configuration) + +Prerequisites: -You can find an example project that uses this structure -[in this GitLab repository](https://gitlab.com/tigerwnz/minimal-gitops-app). You can use the example project -as a starting point to create your own deployment project. +- You have a Flux repository connected to a Kubernetes cluster. + If you're starting from scratch, see [Set up Flux for GitOps](flux_tutorial.md). -## Deployment workflow +## Create the Kubernetes manifest repository -The default branch is the single source of truth for your application and the -Kubernetes manifests that deploy it. To be reflected in a Kubernetes cluster, -a code or configuration change must exist in the default branch. +First, create a repository for your Kubernetes manifests: -A GitLab agent for Kubernetes is installed in every Kubernetes cluster. The agent -is configured to sync manifests from a corresponding branch in the repository. -These branches represent the state of each cluster, and contain only commits that -exist in the default branch. +1. In GitLab, create a new repository called `web-app-manifests`. +1. In `web-app-manifests`, add a file named `src/nginx-deployment.yaml` with the following contents: -Changes are deployed by merging the default branch into the branch of a cluster. -The agent that watches the branch picks up the change and syncs it to the cluster. - -For the actual deployment, the example project uses the GitLab agent for Kubernetes, -but you can also use other GitOps tools. - -### Review apps - -Ephemeral environments such as [review apps](../../../../ci/review_apps/index.md) -are deployed differently. Their configuration does not exist on the default branch, -and the changes are not meant to be deployed to a permanent environment. Review app -manifests are generated and deployed in a merge request feature branch, which is removed -when the MR is merged. - -## Example deployment - -The example project deploys to two permanent environments, staging and production, -which each have a dedicated Kubernetes cluster. A third cluster is used for ephemeral -review apps. - -Each cluster has a corresponding branch that represents the current state of the cluster: -`_gitlab/agents/staging`, `_gitlab/agents/production` and `_gitlab/agents/review`. Each branch is -[protected](../../../../user/project/protected_branches.md) and -a [project access token](../../../../user/project/settings/project_access_tokens.md) -is created for each branch with a configuration that allows only the corresponding token to push to the branch. -This ensures that environment branches are updated only through the configured process. - -Deployment branches are updated by CI/CD jobs. The access token that allows pushing to each -branch is configured as a [CI/CD variable](../../../../ci/variables/index.md). These variables -are protected, and only available to pipelines running on a protected branch. -The CI/CD job merges the default branch `main` into the deployment branch, and pushes -the deployment branch back to the repository using the provided token. To preserve the -commit history between both branches, the CI/CD job uses a fast-forward merge. - -Each cluster has an agent for Kubernetes, and each agent is configured to -sync manifests from the branch corresponding to its cluster. -In your own project, you can different GitOps tool like Flux, or use the same configuration to deploy -to virtual machines with GitLab CI/CD. - -### Application changes - -The example project follows this process to deploy an application change: - -1. A new feature branch is created with the desired changes. The pipeline builds an image, - runs the test suite, and deploy the changes to a review app in the `review` cluster. -1. The feature branch is merged to `main` and the review app is removed. -1. Manifests are updated on `main` (either directly or via merge request) to point to an updated - version of the deployed image. The pipeline automatically merges `main` into the `_gitlab/agents/staging` - branch, which updates the `staging` cluster. -1. The `production` job is triggered manually, and merges `main` into the `_gitlab/agents/production` branch, - deploying to the `production` cluster. + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx + spec: + replicas: 1 + template: + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 + ``` + +1. In `web-app-manifests`, add a file named `src/kustomization.yaml` with the following contents: + + ```yaml + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + resources: + - nginx-deployment.yaml + commonLabels: + app: flux-branches-tutorial + ``` + +## Create a deployment branch + +Next, create a branch to reflect the current state of your cluster. + +In this workflow, the default branch is the single source of truth for your application. +To be reflected in a Kubernetes cluster, a code or configuration change must exist in the default branch. +In a later step, you'll configure CI/CD to merge changes from the default branch into the deployment branch. + +To create a deployment branch: + +1. In `web-app-manifests`, create a branch named `_gitlab/deploy/example` from the default branch. The branch name in this example is chosen to + differentiate the deployment branch from feature branches, but this is not required. You can name the deployment branch whatever you like. +1. Create a [project](../../../../user/project/settings/project_access_tokens.md), + [group](../../../../user/group/settings/group_access_tokens.md) or + [personal access token](../../../../user/profile/personal_access_tokens.md) with the `write_repository` scope. +1. Create a [CI/CD variable](../../../../ci/variables/index.md) with a token value named `DEPLOYMENT_TOKEN`. + Remember to [mask](../../../../ci/variables/index.md#mask-a-cicd-variable) the value so that it won't show in + job logs. +1. Add a rule to [protect](../../../../user/project/protected_branches.md) + your deployment branch with the following values: + + - Allowed to merge: No one. + - Allowed to push and merge: Select the token you created in the previous step, or your user if you created + a personal access token. + - Allowed to force push: Turn off the toggle. + - Require approval from code owners: Turn off the toggle. + +This configuration ensures that only the corresponding token can push to the branch. + +You've successfully created a repository with a protected deployment branch! + +## Configure GitLab CI/CD to push to your branch + +Next, you'll configure CI/CD to merge changes from the default branch to your deployment branch. + +In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/yaml/gitlab_ci_yaml.md) file with the following contents: + + ```yaml + deploy: + stage: deploy + environment: production + variables: + DEPLOYMENT_BRANCH: _gitlab/deploy/example + script: + - | + git config user.name "Deploy Example Bot" + git config user.email "test@example.com" + git fetch origin $DEPLOYMENT_BRANCH + git checkout $DEPLOYMENT_BRANCH + git merge $CI_COMMIT_SHA --ff-only + git push https://deploy:$DEPLOYMENT_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git HEAD:$DEPLOYMENT_BRANCH + resource_group: $CI_ENVIRONMENT_SLUG + ``` + +This creates a CI/CD pipeline with a single `deploy` job that: + +1. Checks out your deployment branch. +1. Merges new changes from the default branch into the deployment branch. +1. Pushes the changes to your repository with the configured token. + +## Configure Flux to sync your manifests + +Next, configure your Flux repository to sync the deployment branch in by the `web-app-manifests` repository. + +To configure, create a [`GitRepository`](https://fluxcd.io/flux/components/source/gitrepositories/) resource: + +1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-source.yaml` + with the following contents: + + ```yaml + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + name: web-app-manifests + namespace: flux-system + spec: + interval: 5m0s + url: https://gitlab.com/gitlab-org/configure/examples/flux/web-app-manifests-branches + ref: + branch: _gitlab/deploy/example + ``` + + You will need to substitute the `url` with the URL of your `web-app-manifests` project. + +1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-kustomization.yaml` + with the following contents: + + ```yaml + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: nginx-source-kustomization + namespace: flux-system + spec: + interval: 1m0s + path: ./src + prune: true + sourceRef: + kind: GitRepository + name: web-app-manifests + targetNamespace: default + ``` + + This file adds a [Kustomization](https://fluxcd.io/flux/components/kustomize/kustomization/) resource that tells Flux to sync the manifests in the artifact fetched from the registry. + +1. Commit the new files and push. + +## Verify your configuration + +After the pipeline completes, you should see a newly created `nginx` pod in your cluster. + +If you want to see the deployment sync again, try updating the number of replicas in the +`src/nginx-deployment.yaml` file and push to the default branch. If all is working well, the change +will sync to the cluster when the pipeline has finished. + +Congratulations! You successfully configured a project to deploy an application and synchronize your changes! diff --git a/doc/user/clusters/agent/gitops/flux_oci_tutorial.md b/doc/user/clusters/agent/gitops/flux_oci_tutorial.md new file mode 100644 index 00000000000..71f3293fcd8 --- /dev/null +++ b/doc/user/clusters/agent/gitops/flux_oci_tutorial.md @@ -0,0 +1,152 @@ +--- +stage: Deploy +group: Environments +info: A tutorial for deploying an OCI artifact using Flux +--- + +# Tutorial: Deploy an OCI artifact using Flux **(FREE)** + +This tutorial teaches you how to package your Kubernetes manifests into an [OCI](https://opencontainers.org/) +artifact and deploy them to your cluster using Flux. You'll set up a sample manifest project, configure it to +store manifests as an artifact in the project's Container Registry, and configure Flux to sync the artifact. + +This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md). + +To deploy an OCI artifact using Flux: + +1. [Create the Kubernetes manifest repository](#create-the-kubernetes-manifest-repository) +1. [Configure the manifest repository to create an OCI artifact](#configure-the-manifest-repository-to-create-an-oci-artifact) +1. [Configure Flux to sync your artifact](#configure-flux-to-sync-your-artifact) +1. [Verify your configuration](#verify-your-configuration) + +Prerequisites: + +- You have a Flux repository connected to a Kubernetes cluster. + If you're starting from scratch, see [Set up Flux for GitOps](flux_tutorial.md). + +## Create the Kubernetes manifest repository + +First, create a repository for your Kubernetes manifests: + +1. In GitLab, create a new repository called `web-app-manifests`. +1. In `web-app-manifests`, add a file named `src/nginx-deployment.yaml` with the following contents: + + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx + spec: + replicas: 1 + template: + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 + ``` + +1. In `web-app-manifests`, add a file named `src/kustomization.yaml` with the following contents: + + ```yaml + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + resources: + - nginx-deployment.yaml + commonLabels: + app: flux-oci-tutorial + ``` + +## Configure the manifest repository to create an OCI artifact + +Next, configure [GitLab CI/CD](../../../../ci/index.md) to package your manifests into an OCI artifact, +and push the artifact to the [GitLab Container Registry](../../../packages/container_registry/index.md): + +1. In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/yaml/gitlab_ci_yaml.md) file with the following contents: + + ```yaml + package: + stage: deploy + image: + name: fluxcd/flux-cli:v2.0.0-rc.1 + entrypoint: [""] + script: + - mkdir -p manifests + - kubectl kustomize ./src --output ./manifests + - | + flux push artifact oci://$CI_REGISTRY_IMAGE:latest \ + --path="./manifests" \ + --source="$CI_REPOSITORY_URL" \ + --revision="$CI_COMMIT_SHORT_SHA" \ + --creds="$CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD" \ + --annotations="org.opencontainers.image.url=$CI_PROJECT_URL" \ + --annotations="org.opencontainers.image.title=$CI_PROJECT_NAME" \ + --annotations="com.gitlab.job.id=$CI_JOB_ID" \ + --annotations="com.gitlab.job.url=$CI_JOB_URL" + ``` + + When the file is pushed to GitLab, a CI/CD pipeline with a single `package` job is created. This job: + + - Uses `kustomization.yaml` to render your final Kubernetes manifests. + - Packages your manifests into an OCI artifact. + - Pushes the OCI artifact to the Container Registry. + + After the pipeline has completed, you can check your OCI artifact with the Container Registry UI. + +## Configure Flux to sync your artifact + +Next, configure your Flux repository to sync the artifact produced by the `web-app-manifests` repository. + +To configure, create an [`OCIRepository`](https://fluxcd.io/flux/components/source/ocirepositories/) resource: + +1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-source.yaml` + with the following contents: + + ```yaml + apiVersion: source.toolkit.fluxcd.io/v1 + kind: OCIRepository + metadata: + name: web-app-manifests + namespace: flux-system + spec: + interval: 1m0s + url: oci://registry.gitlab.com/gitlab-org/configure/examples/flux/web-app-manifests-oci + ref: + tag: latest + ``` + + You will need to substitute the `url` with the URL of your `web-app-manifests` project's container registry. + +1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-kustomization.yaml` + with the following contents: + + ```yaml + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: nginx-source-kustomization + namespace: flux-system + spec: + interval: 1m0s + path: ./ + prune: true + sourceRef: + kind: OCIRepository + name: web-app-manifests + targetNamespace: default + ``` + + This file adds a [Kustomization](https://fluxcd.io/flux/components/kustomize/kustomization/) resource that tells Flux to sync the manifests in the artifact fetched from the registry. + +1. Commit the new files and push. + +## Verify your configuration + +You should see a newly created `nginx` pod in your cluster. + +If you want to see the deployment sync again, try updating the number of replicas in the +`src/nginx-deployment.yaml` file and push to the default branch. If all is working well, the change +should sync to the cluster when the pipeline has finished. + +Congratulations! You successfully configured a project to deploy an application and synchronize your changes! diff --git a/doc/user/group/insights/index.md b/doc/user/group/insights/index.md index ab967c8b12c..61ec055c6ee 100644 --- a/doc/user/group/insights/index.md +++ b/doc/user/group/insights/index.md @@ -47,8 +47,8 @@ Insights display data from the last 90 days. You can zoom in to display data onl To do this, select the pause icons (**{status-paused}**) and slide them along the horizontal axis: -- To select a later start date, slide the left pause icon to the right. -- To select an earlier end date, slide the right pause icon to the left. +- To change the start date, slide the left pause icon to the left or right. +- To change the end date, slide the right pause icon to the left or right. ### Exclude dimensions from charts diff --git a/lefthook.yml b/lefthook.yml index bed3593ba3d..53f1b6201ad 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -41,11 +41,6 @@ pre-push: files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: '*.{js,vue,graphql}' run: yarn run prettier --check {files} - rubocop: - tags: backend style - files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD - glob: '*.{rb,rake}' - run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files} sidekiq-queues: tags: backend files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD @@ -115,6 +110,11 @@ pre-push: pre-commit: parallel: true commands: + rubocop: + tags: backend style + files: git diff --name-only --diff-filter=d --staged + glob: '*.{rb,rake}' + run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files} secrets-detection: tags: secrets files: git diff --name-only --diff-filter=d --staged diff --git a/lib/sidebars/menu.rb b/lib/sidebars/menu.rb index 5f9255c06d0..73d6f733da5 100644 --- a/lib/sidebars/menu.rb +++ b/lib/sidebars/menu.rb @@ -123,6 +123,10 @@ module Sidebars insert_element_after(@items, after_item, new_item) end + def remove_item(item) + remove_element(@items, item.item_id) + end + def replace_placeholder(item) idx = @items.index { |e| e.item_id == item.item_id && e.is_a?(::Sidebars::NilMenuItem) } if idx.nil? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ddf2e681513..772d563c6dc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3163,6 +3163,9 @@ msgstr "" msgid "AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}." msgstr "" +msgid "AdminArea|Manage applications for your instance that can use GitLab as an OAuth provider, start by creating a new one above." +msgstr "" + msgid "AdminArea|Minimal access" msgstr "" @@ -11353,6 +11356,47 @@ msgstr "" msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions currently uses third-party AI services unless those are %{third_party_features_link_start}disabled%{link_end}." msgstr "" +msgid "CodeownersValidation|An error occurred while loading the validation errors. Please try again later." +msgstr "" + +msgid "CodeownersValidation|Contains %d syntax error." +msgid_plural "CodeownersValidation|Contains %d syntax errors." +msgstr[0] "" +msgstr[1] "" + +msgid "CodeownersValidation|Entries with spaces" +msgstr "" + +msgid "CodeownersValidation|Hide errors" +msgstr "" + +msgid "CodeownersValidation|How are errors handled?" +msgstr "" + +msgid "CodeownersValidation|Inaccessible owners" +msgstr "" + +msgid "CodeownersValidation|Less than 1 required approvals" +msgstr "" + +msgid "CodeownersValidation|Line" +msgstr "" + +msgid "CodeownersValidation|Missing section name" +msgstr "" + +msgid "CodeownersValidation|Show errors" +msgstr "" + +msgid "CodeownersValidation|Syntax is valid." +msgstr "" + +msgid "CodeownersValidation|Unparsable sections" +msgstr "" + +msgid "CodeownersValidation|Zero owners" +msgstr "" + msgid "Cohorts|Inactive users" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb index c2db5062962..3c656d9ca75 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb @@ -2,16 +2,18 @@ module QA RSpec.describe 'Package' do - describe 'SaaS Container Registry', :smoke, only: { subdomain: %i[staging] }, product_group: :container_registry do - let(:project) do - Resource::Project.init do |project| - project.path_with_namespace = 'gitlab-qa/container-registry-sanity' - end.reload! + describe 'SaaS Container Registry', :smoke, + only: { subdomain: :staging }, product_group: :container_registry do + before do + Flow::Login.sign_in end it 'pulls an image from an existing repository', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412799' do - Flow::Login.sign_in + project = Resource::Project.init do |project| + project.path_with_namespace = 'gitlab-qa/container-registry-sanity' + end.reload! + project.visit! Page::Project::Menu.perform(&:go_to_pipelines) diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js index 0217835b2a3..53a43626691 100644 --- a/spec/frontend/__helpers__/shared_test_setup.js +++ b/spec/frontend/__helpers__/shared_test_setup.js @@ -35,7 +35,7 @@ Vue.config.productionTip = false; Vue.use(Translate); -const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist']; +const JQUERY_MATCHERS_TO_EXCLUDE = ['toBeEmpty', 'toHaveLength', 'toExist']; // custom-jquery-matchers was written for an old Jest version, we need to make it compatible Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => { diff --git a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js index c23de0efdfd..4455851529d 100644 --- a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js +++ b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js @@ -7,19 +7,35 @@ describe('Image Viewer', () => { const DEFAULT_BLOB_DATA = { rawPath: 'some/image.png', name: 'image.png', + externalStorageUrl: '', }; - const createComponent = () => { - wrapper = shallowMount(ImageViewer, { propsData: { blob: DEFAULT_BLOB_DATA } }); + const createComponent = (blobData = DEFAULT_BLOB_DATA) => { + wrapper = shallowMount(ImageViewer, { propsData: { blob: blobData } }); }; const findImage = () => wrapper.find('[data-testid="image"]'); - it('renders a Source Editor component', () => { - createComponent(); + describe('When blob has externalStorageUrl', () => { + const externalStorageUrl = 'http://img.server.com/lfs-object/21/45/foo_bar'; - expect(findImage().exists()).toBe(true); - expect(findImage().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath); - expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name); + it('renders a Source Editor component with externalStorageUrl', () => { + const blobData = { ...DEFAULT_BLOB_DATA, externalStorageUrl }; + createComponent(blobData); + + expect(findImage().exists()).toBe(true); + expect(findImage().attributes('src')).toBe(externalStorageUrl); + expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name); + }); + }); + + describe('When blob does not have an externalStorageUrl', () => { + it('renders a Source Editor component with rawPath', () => { + createComponent(DEFAULT_BLOB_DATA); + + expect(findImage().exists()).toBe(true); + expect(findImage().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath); + expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name); + }); }); }); diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js index ef7f3359bad..68054a341a2 100644 --- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js +++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js @@ -23,7 +23,7 @@ describe('ConfidentialityFilter', () => { describe('old sidebar', () => { beforeEach(() => { - createComponent({ useNewNavigation: false }); + createComponent({ useSidebarNavigation: false }); }); it('renders the component', () => { @@ -33,7 +33,7 @@ describe('ConfidentialityFilter', () => { describe('new sidebar', () => { beforeEach(() => { - createComponent({ useNewNavigation: true }); + createComponent({ useSidebarNavigation: true }); }); it('renders the component', () => { diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js index 2cf5ae2a70a..fd705d5976b 100644 --- a/spec/frontend/search/sidebar/components/status_filter_spec.js +++ b/spec/frontend/search/sidebar/components/status_filter_spec.js @@ -23,7 +23,7 @@ describe('StatusFilter', () => { describe('old sidebar', () => { beforeEach(() => { - createComponent({ useNewNavigation: false }); + createComponent({ useSidebarNavigation: false }); }); it('renders the component', () => { @@ -33,7 +33,7 @@ describe('StatusFilter', () => { describe('new sidebar', () => { beforeEach(() => { - createComponent({ useNewNavigation: true }); + createComponent({ useSidebarNavigation: true }); }); it('renders the component', () => { diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js index 08a9c2a42d8..271c99be57a 100644 --- a/spec/frontend/vue_shared/components/clipboard_button_spec.js +++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js @@ -1,7 +1,8 @@ import { GlButton } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { mount, createWrapper as makeWrapper } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { BV_HIDE_TOOLTIP, BV_SHOW_TOOLTIP } from '~/lib/utils/constants'; import initCopyToClipboard, { CLIPBOARD_SUCCESS_EVENT, CLIPBOARD_ERROR_EVENT, @@ -31,7 +32,7 @@ describe('clipboard button', () => { title, }); - wrapper.vm.$root.$emit = jest.fn(); + const rootWrapper = makeWrapper(wrapper.vm.$root); const button = findButton(); @@ -42,7 +43,7 @@ describe('clipboard button', () => { await button.trigger(event); - expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::show::tooltip', 'clipboard-button-1'); + expect(rootWrapper.emitted(BV_SHOW_TOOLTIP)[0]).toContain('clipboard-button-1'); expect(button.attributes()).toMatchObject({ title: message, @@ -56,7 +57,7 @@ describe('clipboard button', () => { title, 'aria-label': title, }); - expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1'); + expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)[0]).toContain('clipboard-button-1'); }; describe('without gfm', () => { diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js index 2f8f97c5b95..7f3cf9820db 100644 --- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js +++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js @@ -27,16 +27,19 @@ describe('modal copy button', () => { wrapper.trigger('click'); await nextTick(); - expect(wrapper.emitted().success).not.toBeEmpty(); + expect(wrapper.emitted('error')).toBeUndefined(); + expect(wrapper.emitted('success')).toHaveLength(1); expect(document.execCommand).toHaveBeenCalledWith('copy'); expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]); }); + it("should propagate the clipboard error event if execCommand doesn't work", async () => { document.execCommand = jest.fn(() => false); wrapper.trigger('click'); await nextTick(); - expect(wrapper.emitted().error).not.toBeEmpty(); + expect(wrapper.emitted('success')).toBeUndefined(); + expect(wrapper.emitted('error')).toHaveLength(1); expect(document.execCommand).toHaveBeenCalledWith('copy'); }); }); diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb index 4f77cb3aed4..00202ac7d2b 100644 --- a/spec/lib/sidebars/menu_spec.rb +++ b/spec/lib/sidebars/menu_spec.rb @@ -302,6 +302,19 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do end end + describe "#remove_item" do + let(:item) { Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}, item_id: :foo1) } + + before do + menu.add_item(item) + end + + it 'removes the item from the menu' do + menu.remove_item(item) + expect(menu.has_items?).to be false + end + end + describe '#container_html_options' do before do allow(menu).to receive(:title).and_return('Foo Menu') diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index d3ef92eafec..bfd6360527f 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -38,7 +38,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def it { is_expected.to have_many(:report_results).with_foreign_key(:build_id) } it { is_expected.to have_many(:pages_deployments).with_foreign_key(:ci_build_id) } - it { is_expected.to have_one(:deployment) } it { is_expected.to have_one(:runner_manager).through(:runner_manager_build) } it { is_expected.to have_one(:runner_session).with_foreign_key(:build_id) } it { is_expected.to have_one(:trace_metadata).with_foreign_key(:build_id) } @@ -67,14 +66,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) } - shared_examples 'calling proper BuildFinishedWorker' do - it 'calls Ci::BuildFinishedWorker' do - expect(Ci::BuildFinishedWorker).to receive(:perform_async) - - subject - end - end - describe 'associations' do it 'has a bidirectional relationship with projects' do expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:builds) @@ -109,6 +100,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def it_behaves_like 'has ID tokens', :ci_build it_behaves_like 'a retryable job' + it_behaves_like 'a deployable job' describe '.manual_actions' do let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) } @@ -657,64 +649,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end end - describe '#outdated_deployment?' do - subject { build.outdated_deployment? } - - let(:build) { create(:ci_build, :created, :with_deployment, pipeline: pipeline, environment: 'production') } - - context 'when build has no environment' do - let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: nil) } - - it { expect(subject).to be_falsey } - end - - context 'when project has forward deployment disabled' do - before do - project.ci_cd_settings.update!(forward_deployment_enabled: false) - end - - it { expect(subject).to be_falsey } - end - - context 'when build is not an outdated deployment' do - before do - allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false) - end - - it { expect(subject).to be_falsey } - end - - context 'when build is older than the latest deployment and still pending status' do - before do - allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true) - end - - it { expect(subject).to be_truthy } - end - - context 'when build is older than the latest deployment but succeeded once' do - let(:build) { create(:ci_build, :success, :with_deployment, pipeline: pipeline, environment: 'production') } - - before do - allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true) - end - - it 'returns false for allowing rollback' do - expect(subject).to be_falsey - end - - context 'when forward_deployment_rollback_allowed option is disabled' do - before do - project.ci_cd_settings.update!(forward_deployment_rollback_allowed: false) - end - - it 'returns true for disallowing rollback' do - expect(subject).to eq(true) - end - end - end - end - describe '#schedulable?' do subject { build.schedulable? } @@ -1598,430 +1532,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end end - describe 'state transition as a deployable' do - subject { build.send(event) } - - let!(:build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline) } - let(:deployment) { build.deployment } - let(:environment) { deployment.environment } - - before do - allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - allow(Deployments::HooksWorker).to receive(:perform_async) - end - - it 'has deployments record with created status' do - expect(deployment).to be_created - expect(environment.name).to eq('review/master') - end - - shared_examples_for 'avoid deadlock' do - it 'executes UPDATE in the right order' do - recorded = with_cross_database_modification_prevented do - ActiveRecord::QueryRecorder.new { subject } - end - - index_for_build = recorded.log.index { |l| l.include?("UPDATE #{described_class.quoted_table_name}") } - index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") } - - expect(index_for_build).to be < index_for_deployment - end - end - - context 'when transits to running' do - let(:event) { :run! } - - it_behaves_like 'avoid deadlock' - - it 'transits deployment status to running' do - with_cross_database_modification_prevented do - subject - end - - expect(deployment).to be_running - end - - context 'when deployment is already running state' do - before do - build.deployment.success! - end - - it 'does not change deployment status and tracks an error' do - expect(Gitlab::ErrorTracking) - .to receive(:track_exception).with( - instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id) - - with_cross_database_modification_prevented do - expect { subject }.not_to change { deployment.reload.status } - end - end - end - end - - context 'when transits to success' do - let(:event) { :success! } - - before do - allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) - allow(Deployments::HooksWorker).to receive(:perform_async) - end - - it_behaves_like 'avoid deadlock' - it_behaves_like 'calling proper BuildFinishedWorker' - - it 'transits deployment status to success' do - with_cross_database_modification_prevented do - subject - end - - expect(deployment).to be_success - end - end - - context 'when transits to failed' do - let(:event) { :drop! } - - it_behaves_like 'avoid deadlock' - it_behaves_like 'calling proper BuildFinishedWorker' - - it 'transits deployment status to failed' do - with_cross_database_modification_prevented do - subject - end - - expect(deployment).to be_failed - end - end - - context 'when transits to skipped' do - let(:event) { :skip! } - - it_behaves_like 'avoid deadlock' - - it 'transits deployment status to skipped' do - with_cross_database_modification_prevented do - subject - end - - expect(deployment).to be_skipped - end - end - - context 'when transits to canceled' do - let(:event) { :cancel! } - - it_behaves_like 'avoid deadlock' - it_behaves_like 'calling proper BuildFinishedWorker' - - it 'transits deployment status to canceled' do - with_cross_database_modification_prevented do - subject - end - - expect(deployment).to be_canceled - end - end - - # Mimic playing a manual job that needs another job. - # `needs + when:manual` scenario, see: https://gitlab.com/gitlab-org/gitlab/-/issues/347502 - context 'when transits from skipped to created to running' do - before do - build.skip! - end - - context 'during skipped to created' do - let(:event) { :process! } - - it 'transitions to created' do - subject - - expect(deployment).to be_created - end - end - - context 'during created to running' do - let(:event) { :run! } - - before do - build.process! - build.enqueue! - end - - it 'transitions to running and calls webhook' do - freeze_time do - expect(Deployments::HooksWorker) - .to receive(:perform_async).with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s })) - - subject - end - - expect(deployment).to be_running - end - end - end - end - - describe '#on_stop' do - subject { build.on_stop } - - context 'when a job has a specification that it can be stopped from the other job' do - let(:build) { create(:ci_build, :start_review_app, pipeline: pipeline) } - - it 'returns the other job name' do - is_expected.to eq('stop_review_app') - end - end - - context 'when a job does not have environment information' do - let(:build) { create(:ci_build, pipeline: pipeline) } - - it 'returns nil' do - is_expected.to be_nil - end - end - end - - describe '#environment_tier_from_options' do - subject { build.environment_tier_from_options } - - let(:build) { described_class.new(options: options) } - let(:options) { { environment: { deployment_tier: 'production' } } } - - it { is_expected.to eq('production') } - - context 'when options does not include deployment_tier' do - let(:options) { { environment: { name: 'production' } } } - - it { is_expected.to be_nil } - end - end - - describe '#environment_tier' do - subject { build.environment_tier } - - let(:options) { { environment: { deployment_tier: 'production' } } } - let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) } - let(:build) { described_class.new(options: options, environment: 'production', project: project) } - - it { is_expected.to eq('production') } - - context 'when options does not include deployment_tier' do - let(:options) { { environment: { name: 'production' } } } - - it 'uses tier from environment' do - is_expected.to eq('development') - end - - context 'when persisted environment is absent' do - let(:environment) { nil } - - it { is_expected.to be_nil } - end - end - end - - describe 'environment' do - describe '#has_environment_keyword?' do - subject { build.has_environment_keyword? } - - context 'when environment is defined' do - before do - build.update!(environment: 'review') - end - - it { is_expected.to be_truthy } - end - - context 'when environment is not defined' do - before do - build.update!(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#expanded_environment_name' do - subject { build.expanded_environment_name } - - context 'when environment uses $CI_COMMIT_REF_NAME' do - let(:build) do - create( - :ci_build, - ref: 'master', - environment: 'review/$CI_COMMIT_REF_NAME', - pipeline: pipeline - ) - end - - it { is_expected.to eq('review/master') } - end - - context 'when environment uses yaml_variables containing symbol keys' do - let(:build) do - create( - :ci_build, - yaml_variables: [{ key: :APP_HOST, value: 'host' }], - environment: 'review/$APP_HOST', - pipeline: pipeline - ) - end - - it 'returns an expanded environment name with a list of variables' do - is_expected.to eq('review/host') - end - - context 'when build metadata has already persisted the expanded environment name' do - before do - build.metadata.expanded_environment_name = 'review/foo' - end - - it 'returns a persisted expanded environment name without a list of variables' do - expect(build).not_to receive(:simple_variables) - - is_expected.to eq('review/foo') - end - end - end - - context 'when using persisted variables' do - let(:build) do - create(:ci_build, environment: 'review/x$CI_JOB_ID', pipeline: pipeline) - end - - it { is_expected.to eq('review/x') } - end - - context 'when environment name uses a nested variable' do - let(:yaml_variables) do - [ - { key: 'ENVIRONMENT_NAME', value: '${CI_COMMIT_REF_NAME}' } - ] - end - - let(:build) do - create( - :ci_build, - ref: 'master', - yaml_variables: yaml_variables, - environment: 'review/$ENVIRONMENT_NAME', - pipeline: pipeline - ) - end - - it { is_expected.to eq('review/master') } - end - end - - describe '#expanded_kubernetes_namespace' do - let(:build) { create(:ci_build, environment: environment, options: options, pipeline: pipeline) } - - subject { build.expanded_kubernetes_namespace } - - context 'environment and namespace are not set' do - let(:environment) { nil } - let(:options) { nil } - - it { is_expected.to be_nil } - end - - context 'environment is specified' do - let(:environment) { 'production' } - - context 'namespace is not set' do - let(:options) { nil } - - it { is_expected.to be_nil } - end - - context 'namespace is provided' do - let(:options) do - { - environment: { - name: environment, - kubernetes: { - namespace: namespace - } - } - } - end - - context 'with a static value' do - let(:namespace) { 'production' } - - it { is_expected.to eq namespace } - end - - context 'with a dynamic value' do - let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME' } - - it { is_expected.to eq 'deploy-master' } - end - end - end - end - - describe '#deployment_job?' do - subject { build.deployment_job? } - - context 'when environment is defined' do - before do - build.update!(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_truthy } - end - - context 'and start action is defined' do - before do - build.update!(options: { environment: { action: 'start' } }) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update!(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#stops_environment?' do - subject { build.stops_environment? } - - context 'when environment is defined' do - before do - build.update!(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_falsey } - end - - context 'and stop action is defined' do - before do - build.update!(options: { environment: { action: 'stop' } }) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update!(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - end - describe 'erasable build' do shared_examples 'erasable' do it 'removes artifact file' do @@ -2560,44 +2070,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end end - describe '#persisted_environment' do - let!(:environment) do - create(:environment, project: project, name: "foo-#{project.default_branch}") - end - - subject { build.persisted_environment } - - context 'when referenced literally' do - let(:build) do - create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") - end - - it { is_expected.to eq(environment) } - end - - context 'when referenced with a variable' do - let(:build) do - create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") - end - - it { is_expected.to eq(environment) } - end - - context 'when there is no environment' do - it { is_expected.to be_nil } - end - - context 'when build has a stop environment' do - let(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") } - - it 'expands environment name' do - expect(build).to receive(:expanded_environment_name).and_call_original - - is_expected.to eq(environment) - end - end - end - describe '#play' do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } @@ -5061,45 +4533,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end end - describe '#deployment_status' do - before do - allow_any_instance_of(described_class).to receive(:create_deployment) - end - - context 'when build is a last deployment' do - let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) } - let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } - - it { expect(build.deployment_status).to eq(:last) } - end - - context 'when there is a newer build with deployment' do - let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) } - let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } - let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) } - - it { expect(build.deployment_status).to eq(:out_of_date) } - end - - context 'when build with deployment has failed' do - let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline) } - let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } - - it { expect(build.deployment_status).to eq(:failed) } - end - - context 'when build with deployment is running' do - let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline) } - let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } - - it { expect(build.deployment_status).to eq(:creating) } - end - end - describe '#degenerated?' do context 'when build is degenerated' do subject { create(:ci_build, :degenerated, pipeline: pipeline) } diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb new file mode 100644 index 00000000000..682e408566b --- /dev/null +++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb @@ -0,0 +1,576 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/LineLength +# rubocop:disable RSpec/ContextWording +RSpec.shared_examples 'a deployable job' do + it { is_expected.to have_one(:deployment) } + + shared_examples 'calling proper BuildFinishedWorker' do + it 'calls Ci::BuildFinishedWorker' do + expect(Ci::BuildFinishedWorker).to receive(:perform_async) + + subject + end + end + + describe '#outdated_deployment?' do + subject { build.outdated_deployment? } + + let(:build) { create(:ci_build, :created, :with_deployment, pipeline: pipeline, environment: 'production') } + + context 'when build has no environment' do + let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: nil) } + + it { expect(subject).to be_falsey } + end + + context 'when project has forward deployment disabled' do + before do + project.ci_cd_settings.update!(forward_deployment_enabled: false) + end + + it { expect(subject).to be_falsey } + end + + context 'when build is not an outdated deployment' do + before do + allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false) + end + + it { expect(subject).to be_falsey } + end + + context 'when build is older than the latest deployment and still pending status' do + before do + allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true) + end + + it { expect(subject).to be_truthy } + end + + context 'when build is older than the latest deployment but succeeded once' do + let(:build) { create(:ci_build, :success, :with_deployment, pipeline: pipeline, environment: 'production') } + + before do + allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true) + end + + it 'returns false for allowing rollback' do + expect(subject).to be_falsey + end + + context 'when forward_deployment_rollback_allowed option is disabled' do + before do + project.ci_cd_settings.update!(forward_deployment_rollback_allowed: false) + end + + it 'returns true for disallowing rollback' do + expect(subject).to eq(true) + end + end + end + end + + describe 'state transition as a deployable' do + subject { build.send(event) } + + let!(:build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline) } + let(:deployment) { build.deployment } + let(:environment) { deployment.environment } + + before do + allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) + allow(Deployments::HooksWorker).to receive(:perform_async) + end + + it 'has deployments record with created status' do + expect(deployment).to be_created + expect(environment.name).to eq('review/master') + end + + shared_examples_for 'avoid deadlock' do + it 'executes UPDATE in the right order' do + recorded = with_cross_database_modification_prevented do + ActiveRecord::QueryRecorder.new { subject } + end + + index_for_build = recorded.log.index { |l| l.include?("UPDATE #{Ci::Build.quoted_table_name}") } + index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") } + + expect(index_for_build).to be < index_for_deployment + end + end + + context 'when transits to running' do + let(:event) { :run! } + + it_behaves_like 'avoid deadlock' + + it 'transits deployment status to running' do + with_cross_database_modification_prevented do + subject + end + + expect(deployment).to be_running + end + + context 'when deployment is already running state' do + before do + build.deployment.success! + end + + it 'does not change deployment status and tracks an error' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception).with( + instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id) + + with_cross_database_modification_prevented do + expect { subject }.not_to change { deployment.reload.status } + end + end + end + end + + context 'when transits to success' do + let(:event) { :success! } + + before do + allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) + allow(Deployments::HooksWorker).to receive(:perform_async) + end + + it_behaves_like 'avoid deadlock' + it_behaves_like 'calling proper BuildFinishedWorker' + + it 'transits deployment status to success' do + with_cross_database_modification_prevented do + subject + end + + expect(deployment).to be_success + end + end + + context 'when transits to failed' do + let(:event) { :drop! } + + it_behaves_like 'avoid deadlock' + it_behaves_like 'calling proper BuildFinishedWorker' + + it 'transits deployment status to failed' do + with_cross_database_modification_prevented do + subject + end + + expect(deployment).to be_failed + end + end + + context 'when transits to skipped' do + let(:event) { :skip! } + + it_behaves_like 'avoid deadlock' + + it 'transits deployment status to skipped' do + with_cross_database_modification_prevented do + subject + end + + expect(deployment).to be_skipped + end + end + + context 'when transits to canceled' do + let(:event) { :cancel! } + + it_behaves_like 'avoid deadlock' + it_behaves_like 'calling proper BuildFinishedWorker' + + it 'transits deployment status to canceled' do + with_cross_database_modification_prevented do + subject + end + + expect(deployment).to be_canceled + end + end + + # Mimic playing a manual job that needs another job. + # `needs + when:manual` scenario, see: https://gitlab.com/gitlab-org/gitlab/-/issues/347502 + context 'when transits from skipped to created to running' do + before do + build.skip! + end + + context 'during skipped to created' do + let(:event) { :process! } + + it 'transitions to created' do + subject + + expect(deployment).to be_created + end + end + + context 'during created to running' do + let(:event) { :run! } + + before do + build.process! + build.enqueue! + end + + it 'transitions to running and calls webhook' do + freeze_time do + expect(Deployments::HooksWorker) + .to receive(:perform_async).with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s })) + + subject + end + + expect(deployment).to be_running + end + end + end + end + + describe '#on_stop' do + subject { build.on_stop } + + context 'when a job has a specification that it can be stopped from the other job' do + let(:build) { create(:ci_build, :start_review_app, pipeline: pipeline) } + + it 'returns the other job name' do + is_expected.to eq('stop_review_app') + end + end + + context 'when a job does not have environment information' do + let(:build) { create(:ci_build, pipeline: pipeline) } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe '#environment_tier_from_options' do + subject { build.environment_tier_from_options } + + let(:build) { Ci::Build.new(options: options) } + let(:options) { { environment: { deployment_tier: 'production' } } } + + it { is_expected.to eq('production') } + + context 'when options does not include deployment_tier' do + let(:options) { { environment: { name: 'production' } } } + + it { is_expected.to be_nil } + end + end + + describe '#environment_tier' do + subject { build.environment_tier } + + let(:options) { { environment: { deployment_tier: 'production' } } } + let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) } + let(:build) { Ci::Build.new(options: options, environment: 'production', project: project) } + + it { is_expected.to eq('production') } + + context 'when options does not include deployment_tier' do + let(:options) { { environment: { name: 'production' } } } + + it 'uses tier from environment' do + is_expected.to eq('development') + end + + context 'when persisted environment is absent' do + let(:environment) { nil } + + it { is_expected.to be_nil } + end + end + end + + describe 'environment' do + describe '#has_environment_keyword?' do + subject { build.has_environment_keyword? } + + context 'when environment is defined' do + before do + build.update!(environment: 'review') + end + + it { is_expected.to be_truthy } + end + + context 'when environment is not defined' do + before do + build.update!(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#expanded_environment_name' do + subject { build.expanded_environment_name } + + context 'when environment uses $CI_COMMIT_REF_NAME' do + let(:build) do + create( + :ci_build, + ref: 'master', + environment: 'review/$CI_COMMIT_REF_NAME', + pipeline: pipeline + ) + end + + it { is_expected.to eq('review/master') } + end + + context 'when environment uses yaml_variables containing symbol keys' do + let(:build) do + create( + :ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + environment: 'review/$APP_HOST', + pipeline: pipeline + ) + end + + it 'returns an expanded environment name with a list of variables' do + is_expected.to eq('review/host') + end + + context 'when build metadata has already persisted the expanded environment name' do + before do + build.metadata.expanded_environment_name = 'review/foo' + end + + it 'returns a persisted expanded environment name without a list of variables' do + expect(build).not_to receive(:simple_variables) + + is_expected.to eq('review/foo') + end + end + end + + context 'when using persisted variables' do + let(:build) do + create(:ci_build, environment: 'review/x$CI_JOB_ID', pipeline: pipeline) + end + + it { is_expected.to eq('review/x') } + end + + context 'when environment name uses a nested variable' do + let(:yaml_variables) do + [ + { key: 'ENVIRONMENT_NAME', value: '${CI_COMMIT_REF_NAME}' } + ] + end + + let(:build) do + create( + :ci_build, + ref: 'master', + yaml_variables: yaml_variables, + environment: 'review/$ENVIRONMENT_NAME', + pipeline: pipeline + ) + end + + it { is_expected.to eq('review/master') } + end + end + + describe '#expanded_kubernetes_namespace' do + let(:build) { create(:ci_build, environment: environment, options: options, pipeline: pipeline) } + + subject { build.expanded_kubernetes_namespace } + + context 'environment and namespace are not set' do + let(:environment) { nil } + let(:options) { nil } + + it { is_expected.to be_nil } + end + + context 'environment is specified' do + let(:environment) { 'production' } + + context 'namespace is not set' do + let(:options) { nil } + + it { is_expected.to be_nil } + end + + context 'namespace is provided' do + let(:options) do + { + environment: { + name: environment, + kubernetes: { + namespace: namespace + } + } + } + end + + context 'with a static value' do + let(:namespace) { 'production' } + + it { is_expected.to eq namespace } + end + + context 'with a dynamic value' do + let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME' } + + it { is_expected.to eq 'deploy-master' } + end + end + end + end + + describe '#deployment_job?' do + subject { build.deployment_job? } + + context 'when environment is defined' do + before do + build.update!(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_truthy } + end + + context 'and start action is defined' do + before do + build.update!(options: { environment: { action: 'start' } }) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update!(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#stops_environment?' do + subject { build.stops_environment? } + + context 'when environment is defined' do + before do + build.update!(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_falsey } + end + + context 'and stop action is defined' do + before do + build.update!(options: { environment: { action: 'stop' } }) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update!(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + end + + describe '#persisted_environment' do + let!(:environment) do + create(:environment, project: project, name: "foo-#{project.default_branch}") + end + + subject { build.persisted_environment } + + context 'when referenced literally' do + let(:build) do + create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") + end + + it { is_expected.to eq(environment) } + end + + context 'when referenced with a variable' do + let(:build) do + create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") + end + + it { is_expected.to eq(environment) } + end + + context 'when there is no environment' do + it { is_expected.to be_nil } + end + + context 'when build has a stop environment' do + let(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + + it 'expands environment name' do + expect(build).to receive(:expanded_environment_name).and_call_original + + is_expected.to eq(environment) + end + end + end + + describe '#deployment_status' do + before do + allow_any_instance_of(Ci::Build).to receive(:create_deployment) # rubocop:disable RSpec/AnyInstanceOf + end + + context 'when build is a last deployment' do + let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } + + it { expect(build.deployment_status).to eq(:last) } + end + + context 'when there is a newer build with deployment' do + let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } + let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) } + + it { expect(build.deployment_status).to eq(:out_of_date) } + end + + context 'when build with deployment has failed' do + let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline) } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } + + it { expect(build.deployment_status).to eq(:failed) } + end + + context 'when build with deployment is running' do + let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline) } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } + + it { expect(build.deployment_status).to eq(:creating) } + end + end +end +# rubocop:enable Layout/LineLength +# rubocop:enable RSpec/ContextWording diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/environments/stop_job_success_worker_spec.rb index be9802eb2ce..3a2db8cfb77 100644 --- a/spec/workers/build_success_worker_spec.rb +++ b/spec/workers/environments/stop_job_success_worker_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' -RSpec.describe BuildSuccessWorker, feature_category: :continuous_integration do +RSpec.describe Environments::StopJobSuccessWorker, feature_category: :continuous_delivery do describe '#perform' do subject { described_class.new.perform(build.id) } context 'when build exists' do context 'when the build will stop an environment' do - let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project, status: :success) } + let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project, status: :success) } # rubocop:disable Layout/LineLength let(:environment) { create(:environment, state: :available) } it 'stops the environment' do diff --git a/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb b/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb index 01da04ce27c..0f1249dcf71 100644 --- a/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb +++ b/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb @@ -7,35 +7,86 @@ module BundlerChecksum::Command extend self def execute - linted = true + definition = Bundler.definition + definition.validate_runtime! + definition.resolve_only_locally! - Bundler.definition.resolve.sort_by(&:name).each do |spec| + errors = lint_specs(definition.specs.sort_by(&:name)) + show_errors(errors) + + !errors.any? + end + + private + + def lint_specs(specs) + specs.filter_map do |spec| next unless spec.source.is_a?(Bundler::Source::Rubygems) + next if default_gem_without_cache_file?(spec) + + lint_spec(spec) + end + end + + def lint_spec(spec) + expected_checksum = expected_checksum_for(spec) - unless checksum_for?(spec.name) - $stderr.puts "ERROR: Missing checksum for gem `#{spec.name}`" - linted = false + if expected_checksum + actual_checksum = actual_checksum_for(spec) + + if expected_checksum != actual_checksum + <<~ERROR + #{error_message_for(spec, 'Invalid checksum')} + + Expected: #{expected_checksum} + Actual: #{actual_checksum} + ERROR end + else + error_message_for(spec, 'Missing checksum') end + end - unless linted - $stderr.puts <<~MSG + def error_message_for(spec, message) + "ERROR: #{message} for gem `#{spec.name}` (#{spec.version} #{spec.platform})" + end + + def show_errors(errors) + return if errors.none? + + errors.each { |error| $stderr.puts error } + + $stderr.puts <<~MSG + + Please run `bundle exec bundler-checksum init` to add correct checksums. + MSG + end - Please run `bundle exec bundler-checksum init` to add missing checksums. - MSG + def default_gem_without_cache_file?(spec) + spec.default_gem? && !File.exist?(spec.cache_file) + end + + def expected_checksum_for(spec) + info_list = gems_with_checksums.fetch(spec.name, []) + + info = info_list.find do |hash| + hash[:version] == spec.version.to_s && + hash[:platform] == spec.platform.to_s end - linted + info&.fetch(:checksum) end - private + def actual_checksum_for(spec) + path = spec.cache_file - def checksum_for?(name) - gems_with_checksums.include?(name) + Bundler::SharedHelpers.filesystem_access(path, :read) do + Bundler::SharedHelpers.digest(:SHA256).hexdigest(File.read(path)) + end end def gems_with_checksums - @gems_with_checksums ||= local_checksums.map { |hash| hash[:name] }.to_set + @gems_with_checksums ||= local_checksums.group_by { |hash| hash[:name] } end def local_checksums diff --git a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test index 9b6d83591f8..7006a98c626 100755 --- a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test +++ b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test @@ -1,10 +1,6 @@ #!/bin/sh -set -x -set -e - -# Ensure that each gem has a checksum entry -ruby -I ../../lib ../../bin/bundler-checksum lint +set -xe # Check there's no differences after re-initialising ruby -I ../../lib ../../bin/bundler-checksum init @@ -16,3 +12,6 @@ ruby -I ../../lib ../../bin/bundler-checksum verify # Test installing with bundler-checksum export BUNDLER_CHECKSUM_VERIFICATION_OPT_IN=1 bundle install + +# Ensure that each gem has a valid checksum entry +ruby -I ../../lib ../../bin/bundler-checksum lint diff --git a/vendor/languages.yml b/vendor/languages.yml index 5e7a955b1bb..0a6f78ebe5d 100755 --- a/vendor/languages.yml +++ b/vendor/languages.yml @@ -6098,6 +6098,7 @@ XML: - wsdl extensions: - ".xml" + - ".arxml" - ".adml" - ".admx" - ".ant" |