diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-07 21:09:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-07 21:09:24 +0300 |
commit | 139d707cfeb007f3cf30f39a38deb0eec6817a47 (patch) | |
tree | 6f0e6cb1fe56160656f3dc2c74163ae79ad73715 | |
parent | a93bf027c2619af8c11b030414c339346f13ead6 (diff) |
Add latest changes from gitlab-org/gitlab@master
39 files changed, 696 insertions, 315 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue new file mode 100644 index 00000000000..c4358b83e23 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue @@ -0,0 +1,26 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, + props: { + icon: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all" + > + <gl-icon :name="icon" class="gl-mr-4" /> + <span> + <slot></slot> + </span> + </div> +</template> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index da38521b055..d9816dc5102 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -4,13 +4,18 @@ import { n__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { formatDate } from '~/lib/utils/datetime_utility'; import DeleteButton from '../delete_button.vue'; import ListItem from '../list_item.vue'; +import DetailsRow from './details_row.vue'; import { REMOVE_TAG_BUTTON_TITLE, - SHORT_REVISION_LABEL, + DIGEST_LABEL, CREATED_AT_LABEL, REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, + PUBLISHED_DETAILS_ROW_TEXT, + MANIFEST_DETAILS_ROW_TEST, + CONFIGURATION_DETAILS_ROW_TEST, } from '../../constants/index'; export default { @@ -21,6 +26,7 @@ export default { ListItem, ClipboardButton, TimeAgoTooltip, + DetailsRow, }, directives: { GlTooltip: GlTooltipDirective, @@ -43,9 +49,12 @@ export default { }, i18n: { REMOVE_TAG_BUTTON_TITLE, - SHORT_REVISION_LABEL, + DIGEST_LABEL, CREATED_AT_LABEL, REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, + PUBLISHED_DETAILS_ROW_TEXT, + MANIFEST_DETAILS_ROW_TEST, + CONFIGURATION_DETAILS_ROW_TEST, }, computed: { formattedSize() { @@ -57,6 +66,25 @@ export default { mobileClasses() { return this.isDesktop ? '' : 'mw-s'; }, + shortDigest() { + // remove sha256: from the string, and show only the first 7 char + return this.tag.digest?.substring(7, 14); + }, + publishedDate() { + return formatDate(this.tag.created_at, 'isoDate'); + }, + publishedTime() { + return formatDate(this.tag.created_at, 'hh:MM Z'); + }, + formattedRevision() { + // to be removed when API response is adjusted + // see https://gitlab.com/gitlab-org/gitlab/-/issues/225324 + // eslint-disable-next-line @gitlab/require-i18n-strings + return `sha256:${this.tag.revision}`; + }, + tagLocation() { + return this.tag.path?.replace(`:${this.tag.name}`, ''); + }, }, }; </script> @@ -110,9 +138,9 @@ export default { </span> </template> <template #right-secondary> - <span data-testid="short-revision"> - <gl-sprintf :message="$options.i18n.SHORT_REVISION_LABEL"> - <template #imageId>{{ tag.short_revision }}</template> + <span data-testid="digest"> + <gl-sprintf :message="$options.i18n.DIGEST_LABEL"> + <template #imageId>{{ shortDigest }}</template> </gl-sprintf> </span> </template> @@ -126,5 +154,50 @@ export default { @delete="$emit('delete')" /> </template> + <template #details_published> + <details-row icon="clock" data-testid="published-date-detail"> + <gl-sprintf :message="$options.i18n.PUBLISHED_DETAILS_ROW_TEXT"> + <template #repositoryPath> + <i>{{ tagLocation }}</i> + </template> + <template #time> + {{ publishedTime }} + </template> + <template #date> + {{ publishedDate }} + </template> + </gl-sprintf> + </details-row> + </template> + <template #details_manifest_digest> + <details-row icon="log" data-testid="manifest-detail"> + <gl-sprintf :message="$options.i18n.MANIFEST_DETAILS_ROW_TEST"> + <template #digest> + {{ tag.digest }} + </template> + </gl-sprintf> + <clipboard-button + v-if="tag.digest" + :title="tag.digest" + :text="tag.digest" + css-class="btn-default btn-transparent btn-clipboard gl-p-0" + /> + </details-row> + </template> + <template #details_configuration_digest> + <details-row icon="cloud-gear" data-testid="configuration-detail"> + <gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST"> + <template #digest> + {{ formattedRevision }} + </template> + </gl-sprintf> + <clipboard-button + v-if="formattedRevision" + :title="formattedRevision" + :text="formattedRevision" + css-class="btn-default btn-transparent btn-clipboard gl-p-0" + /> + </details-row> + </template> </list-item> </template> diff --git a/app/assets/javascripts/registry/explorer/components/list_item.vue b/app/assets/javascripts/registry/explorer/components/list_item.vue index 82452e1d17a..97175b1c2ea 100644 --- a/app/assets/javascripts/registry/explorer/components/list_item.vue +++ b/app/assets/javascripts/registry/explorer/components/list_item.vue @@ -79,7 +79,7 @@ export default { :selected="isDetailsShown" icon="ellipsis_h" size="small" - class="gl-ml-2" + class="gl-ml-2 gl-display-none gl-display-sm-block" @click="toggleDetails" /> </div> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue index a29a9bd23c3..80cc392f86a 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue @@ -18,10 +18,9 @@ export default { <gl-empty-state :title="s__('ContainerRegistry|There are no container images available in this group')" :svg-path="config.noContainersImage" - class="container-message" > <template #description> - <p class="js-no-container-images-text"> + <p> <gl-sprintf :message=" s__( diff --git a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue index c27d53f4351..35eb0b11e40 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue @@ -1,5 +1,5 @@ <script> -import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlEmptyState, GlSprintf, GlLink, GlFormInputGroup, GlFormInput } from '@gitlab/ui'; import { mapState, mapGetters } from 'vuex'; import { s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -17,6 +17,8 @@ export default { GlEmptyState, GlSprintf, GlLink, + GlFormInputGroup, + GlFormInput, }, i18n: { quickStart: QUICK_START, @@ -43,10 +45,9 @@ export default { <gl-empty-state :title="s__('ContainerRegistry|There are no container images stored for this project')" :svg-path="config.noContainersImage" - class="container-message" > <template #description> - <p class="js-no-container-images-text"> + <p> <gl-sprintf :message="$options.i18n.introText"> <template #docLink="{content}"> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> @@ -54,7 +55,7 @@ export default { </gl-sprintf> </p> <h5>{{ $options.i18n.quickStart }}</h5> - <p class="js-not-logged-in-to-registry-text"> + <p> <gl-sprintf :message="$options.i18n.notLoggedInMessage"> <template #twofaDocLink="{content}"> <gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link> @@ -66,42 +67,49 @@ export default { </template> </gl-sprintf> </p> - <div class="input-group append-bottom-10"> - <input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly /> - <span class="input-group-append"> + <gl-form-input-group class="gl-mb-4"> + <gl-form-input + :value="dockerLoginCommand" + readonly + type="text" + class="gl-font-monospace!" + /> + <template #append> <clipboard-button :text="dockerLoginCommand" :title="$options.i18n.copyLoginTitle" - class="input-group-text" + class="gl-m-0!" /> - </span> - </div> - <p></p> - <p> + </template> + </gl-form-input-group> + <p class="gl-mb-4"> {{ $options.i18n.addImageText }} </p> - - <div class="input-group append-bottom-10"> - <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly /> - <span class="input-group-append"> + <gl-form-input-group class="gl-mb-4 "> + <gl-form-input + :value="dockerBuildCommand" + readonly + type="text" + class="gl-font-monospace!" + /> + <template #append> <clipboard-button :text="dockerBuildCommand" :title="$options.i18n.copyBuildTitle" - class="input-group-text" + class="gl-m-0!" /> - </span> - </div> - - <div class="input-group"> - <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly /> - <span class="input-group-append"> + </template> + </gl-form-input-group> + <gl-form-input-group> + <gl-form-input :value="dockerPushCommand" readonly type="text" class="gl-font-monospace!" /> + <template #append> <clipboard-button :text="dockerPushCommand" :title="$options.i18n.copyPushTitle" - class="input-group-text" + class="gl-m-0!" /> - </span> - </div> + </template> + </gl-form-input-group> </template> </gl-empty-state> </template> diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index f48fb6c5928..f414c5d3963 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -16,8 +16,15 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__( ); export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags'); -export const SHORT_REVISION_LABEL = s__('ContainerRegistry|Image ID: %{imageId}'); +export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}'); export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}'); +export const PUBLISHED_DETAILS_ROW_TEXT = s__( + 'ContainerRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}', +); +export const MANIFEST_DETAILS_ROW_TEST = s__('ContainerRegistry|Manifest digest: %{digest}'); +export const CONFIGURATION_DETAILS_ROW_TEST = s__( + 'ContainerRegistry|Configuration digest: %{digest}', +); export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag'); export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected'); diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index e8a26dc58f2..1d353651c38 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -217,7 +217,6 @@ export default { :svg-path="config.noContainersImage" data-testid="emptySearch" :title="$options.i18n.EMPTY_RESULT_TITLE" - class="container-message" > <template #description> {{ $options.i18n.EMPTY_RESULT_MESSAGE }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue index 7695570753c..dc16d46dd8e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue @@ -1,5 +1,5 @@ <script> -import { __ } from '~/locale'; +import { s__ } from '~/locale'; import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; export default { @@ -30,23 +30,23 @@ export default { }, reportChangeText() { if (this.validPlanValues) { - return __( + return s__( 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete', ); } - return __('Terraform|Generating the report caused an error.'); + return s__('Terraform|Generating the report caused an error.'); }, reportHeaderText() { if (this.validPlanValues) { return this.plan.job_name - ? __('Terraform|The Terraform report %{name} was generated in your pipelines.') - : __('Terraform|A Terraform report was generated in your pipelines.'); + ? s__('Terraform|The Terraform report %{name} was generated in your pipelines.') + : s__('Terraform|A Terraform report was generated in your pipelines.'); } return this.plan.job_name - ? __('Terraform|The Terraform report %{name} failed to generate.') - : __('Terraform|A Terraform report failed to generate.'); + ? s__('Terraform|The Terraform report %{name} failed to generate.') + : s__('Terraform|A Terraform report failed to generate.'); }, validPlanValues() { return this.addNum + this.changeNum + this.deleteNum >= 0; diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss deleted file mode 100644 index b88bd78cf3d..00000000000 --- a/app/assets/stylesheets/pages/container_registry.scss +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Container Registry - */ - -.container-message { - span .btn { - margin: 0; - } -} - -.container-image { - border-bottom: 1px solid $white-normal; -} - -.container-image-head { - padding: 0 16px; - line-height: 4em; - - .btn-link { - padding: 0; - - &:focus { - outline: none; - } - } -} - -.table.tags { - margin-bottom: 0; - - .registry-image-row { - .check { - padding-right: $gl-padding; - width: 5%; - } - - .action-buttons { - opacity: 0; - } - - &:hover { - .action-buttons { - opacity: 1; - } - } - } -} diff --git a/app/finders/projects/integrations/jira/issues_finder.rb b/app/finders/projects/integrations/jira/issues_finder.rb index 280ed7954de..50e9aabc60d 100644 --- a/app/finders/projects/integrations/jira/issues_finder.rb +++ b/app/finders/projects/integrations/jira/issues_finder.rb @@ -14,6 +14,7 @@ module Projects @jira_service = project.jira_service @page = params[:page].presence || 1 @params = params + @params = @params.reverse_merge(map_sort_values(@params.delete(:sort))) end def execute @@ -44,6 +45,15 @@ module Projects end end # rubocop: enable CodeReuse/ServiceClass + + def map_sort_values(sort) + case sort + when 'created_date' + { sort: 'created', sort_direction: 'DESC' } + else + { sort: ::Jira::JqlBuilderService::DEFAULT_SORT, sort_direction: ::Jira::JqlBuilderService::DEFAULT_SORT_DIRECTION } + end + end end end end diff --git a/app/services/jira/jql_builder_service.rb b/app/services/jira/jql_builder_service.rb index cb8cee8e38a..2a4b18fcc8c 100644 --- a/app/services/jira/jql_builder_service.rb +++ b/app/services/jira/jql_builder_service.rb @@ -2,29 +2,66 @@ module Jira class JqlBuilderService - DEFAULT_SORT = "created" - DEFAULT_SORT_DIRECTION = "DESC" + DEFAULT_SORT = 'created' + DEFAULT_SORT_DIRECTION = 'DESC' + + # https://confluence.atlassian.com/jirasoftwareserver082/search-syntax-for-text-fields-974359692.html + JQL_SPECIAL_CHARS = %w[" + . , ; ? | * / % ^ $ # @ [ ] \\].freeze def initialize(jira_project_key, params = {}) @jira_project_key = jira_project_key + @search = params[:search] + @labels = params[:labels] @sort = params[:sort] || DEFAULT_SORT @sort_direction = params[:sort_direction] || DEFAULT_SORT_DIRECTION end def execute - [by_project, order_by].join(' ') + [ + jql_filters, + order_by + ].join(' ') end private - attr_reader :jira_project_key, :sort, :sort_direction + attr_reader :jira_project_key, :sort, :sort_direction, :search, :labels + + def jql_filters + [ + by_project, + by_labels, + by_summary_and_description + ].compact.join(' AND ') + end + + def by_summary_and_description + return if search.blank? + + escaped_search = remove_special_chars(search) + %Q[(summary ~ "#{escaped_search}" OR description ~ "#{escaped_search}")] + end def by_project "project = #{jira_project_key}" end + def by_labels + return if labels.blank? + + labels.map { |label| %Q[labels = "#{escape_quotes(label)}"] }.join(' AND ') + end + def order_by "order by #{sort} #{sort_direction}" end + + def escape_quotes(param) + param.gsub('\\', '\\\\\\').gsub('"', '\\"') + end + + def remove_special_chars(param) + param.delete(JQL_SPECIAL_CHARS.join).downcase.squish + end end end diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb index 21081bd077f..5d4059710bb 100644 --- a/app/services/projects/container_repository/delete_tags_service.rb +++ b/app/services/projects/container_repository/delete_tags_service.rb @@ -3,6 +3,8 @@ module Projects module ContainerRepository class DeleteTagsService < BaseService + LOG_DATA_BASE = { service_class: self.to_s }.freeze + def execute(container_repository) return error('access denied') unless can?(current_user, :destroy_container_image, project) @@ -51,10 +53,27 @@ module Projects def smart_delete(container_repository, tag_names) fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true) - if fast_delete_enabled && container_repository.client.supports_tag_delete? - fast_delete(container_repository, tag_names) + response = if fast_delete_enabled && container_repository.client.supports_tag_delete? + fast_delete(container_repository, tag_names) + else + slow_delete(container_repository, tag_names) + end + + response.tap { |r| log_response(r, container_repository) } + end + + def log_response(response, container_repository) + log_data = LOG_DATA_BASE.merge( + container_repository_id: container_repository.id, + message: 'deleted tags' + ) + + if response[:status] == :success + log_data[:deleted_tags_count] = response[:deleted].size + log_info(log_data) else - slow_delete(container_repository, tag_names) + log_data[:message] = response[:message] + log_error(log_data) end end diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 68c2af336b3..bfb22aa8025 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,4 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout +- default_branch_name = Gitlab::CurrentSettings.default_branch_name.presence || "master" - breadcrumb_title _("Details") - page_title _("Details") @@ -47,7 +48,7 @@ git commit -m "add README" - if @project.can_current_user_push_to_default_branch? %span>< - git push -u origin master + git push -u origin #{ default_branch_name } %fieldset %h5= _('Push an existing folder') @@ -60,7 +61,7 @@ git commit -m "Initial commit" - if @project.can_current_user_push_to_default_branch? %span>< - git push -u origin master + git push -u origin #{ default_branch_name } %fieldset %h5= _('Push an existing Git repository') diff --git a/changelogs/unreleased/208193-add-logs-to-container-repository-delete-tags-service.yml b/changelogs/unreleased/208193-add-logs-to-container-repository-delete-tags-service.yml new file mode 100644 index 00000000000..7bef6203075 --- /dev/null +++ b/changelogs/unreleased/208193-add-logs-to-container-repository-delete-tags-service.yml @@ -0,0 +1,5 @@ +--- +title: Add log statements to Projects::ContainerRepository::DeleteTagsService +merge_request: 35539 +author: +type: added diff --git a/changelogs/unreleased/216962-add-an-expandable-tag-detail-view-to-the-image-repository-detail-v.yml b/changelogs/unreleased/216962-add-an-expandable-tag-detail-view-to-the-image-repository-detail-v.yml new file mode 100644 index 00000000000..62f49534864 --- /dev/null +++ b/changelogs/unreleased/216962-add-an-expandable-tag-detail-view-to-the-image-repository-detail-v.yml @@ -0,0 +1,5 @@ +--- +title: Add details rows to Container Registry Tags List +merge_request: 36036 +author: +type: changed diff --git a/changelogs/unreleased/emilyring-terraform-translation.yml b/changelogs/unreleased/emilyring-terraform-translation.yml new file mode 100644 index 00000000000..f106c161997 --- /dev/null +++ b/changelogs/unreleased/emilyring-terraform-translation.yml @@ -0,0 +1,5 @@ +--- +title: Fixed translation errors on MR Widget +merge_request: 35888 +author: +type: fixed diff --git a/doc/.vale/gitlab/Acronyms.yml b/doc/.vale/gitlab/Acronyms.yml index c432b0a734a..90ec3d9a830 100644 --- a/doc/.vale/gitlab/Acronyms.yml +++ b/doc/.vale/gitlab/Acronyms.yml @@ -18,6 +18,7 @@ exceptions: - ARN - ASCII - AWS + - CLI - CNAME - CPU - CSS @@ -34,9 +35,10 @@ exceptions: - HTTP - HTTPS - IAM + - IBM - IDE - IRC - - IBM + - ISO - JSON - LDAP - LDAPS diff --git a/doc/.vale/gitlab/Contractions.yml b/doc/.vale/gitlab/Contractions.yml index d5ef52fb242..dc48b876f40 100644 --- a/doc/.vale/gitlab/Contractions.yml +++ b/doc/.vale/gitlab/Contractions.yml @@ -20,7 +20,6 @@ swap: have not: haven't that is: that's we are: we're - will not: won't would not: wouldn't you are: you're you have: you've @@ -31,25 +30,16 @@ swap: didn't: did not doesn't: does not hasn't: has not - how'll: how will how's: how is isn't: is not - it'll: it will shouldn't: should not - that'll: that will - they'll: they will they're: they are wasn't: was not weren't: were not - we'll: we will we've: we have what's: what is - what'll: what will when's: when is - when'll: when will where's: where is - where'll: where will who's: who is - who'll: who will why's: why is - why'll: why will + diff --git a/doc/.vale/gitlab/FutureTense.yml b/doc/.vale/gitlab/FutureTense.yml index edb7e1afff1..a53a7dd29cc 100644 --- a/doc/.vale/gitlab/FutureTense.yml +++ b/doc/.vale/gitlab/FutureTense.yml @@ -8,17 +8,10 @@ extends: existence message: 'Avoid using future tense: "%s"' ignorecase: true -level: suggestion +level: warning link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#language-to-avoid -tokens: - - going to be - - going to - - he'll - - she'll - - they'll - - we'll - - will be - - will have - - will # Leave this word after the two-word 'will' variants as a catchall - - won't - - you'll +raw: + - "(going to( |\n|[[:punct:]])[a-zA-Z]*|" + - "will( |\n|[[:punct:]])[a-zA-Z]*|" + - "won't( |\n|[[:punct:]])[a-zA-Z]*|" + - "[a-zA-Z]*'ll( |\n|[[:punct:]])[a-zA-Z]*)" diff --git a/doc/api/import.md b/doc/api/import.md index a64d8783da4..3daf5e7be53 100644 --- a/doc/api/import.md +++ b/doc/api/import.md @@ -45,7 +45,6 @@ POST /import/bitbucket_server | Attribute | Type | Required | Description | |------------|---------|----------|---------------------| - | `bitbucket_server_url` | string | yes | Bitbucket Server URL | | `bitbucket_server_username` | string | yes | Bitbucket Server Username | | `personal_access_token` | string | yes | Bitbucket Server personal access token/password | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5bb0c7af49f..5b06afc0ab1 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1088,7 +1088,7 @@ The job attributes allowed by `rules` are: - If used as `when: delayed`, `start_in` is also required. - [`allow_failure`](#allow_failure): If not defined, defaults to `allow_failure: false`. -If `when` is evaluated to any value except `never`, the job is included in the pipeline. +If a rule evaluates to true, and `when` has any value except `never`, the job is included in the pipeline. For example: @@ -1189,10 +1189,11 @@ for more details. #### Differences between `rules` and `only`/`except` -A very important difference between `rules` and `only/except`, is that jobs defined -with `only/except` do not trigger merge request pipelines without explicit configuration. -`rules` *can* trigger all types of pipelines, without explicitly configuring each -type. +Jobs defined with `only/except` do not trigger merge request pipelines by default. +You must explicitly add `only: merge_requests`. + +Jobs defined with `rules` can trigger all types of pipelines. +You do not have to explicitly configure each type. For example: @@ -1259,6 +1260,8 @@ Some details regarding the logic that determines the `when` for the job: rule without `if` or `changes`, always matches, and is always used if reached. - If a rule matches and has no `when` defined, the rule uses the `when` defined for the job, which defaults to `on_success` if not defined. +- You can define `when` once per rule, or once at the job-level, which applies to + all rules. You can't mix `when` at the job-level with `when` in rules. For behavior similar to the [`only`/`except` keywords](#onlyexcept-basic), you can check the value of the `$CI_PIPELINE_SOURCE` variable. @@ -3626,30 +3629,21 @@ For more information, see [Deployments Safety](../environments/deployment_safety > [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/19298) in GitLab 13.2. -`release` indicates that the job will create a [Release](../../user/project/releases/index.md), -and optionally include URLs for Release assets. +`release` indicates that the job creates a [Release](../../user/project/releases/index.md), +and optionally includes URLs for Release assets. These methods are supported: -- [`name`](#releasename) -- [`description`](#releasedescription) - [`tag_name`](#releasetag_name) -- [`ref`](#releaseref) -- [`milestones`](#releasemilestones) -- [`released_at`](#releasereleased_at) +- [`name`](#releasename) (optional) +- [`description`](#releasedescription) (optional) +- [`ref`](#releaseref) (optional) +- [`milestones`](#releasemilestones) (optional) +- [`released_at`](#releasereleased_at) (optional) The Release is created only if the job processes without error. If the Rails API returns an error during Release creation, the `release` job fails. -#### Tags - -A `release` job should not be run against a tag commit, or it will continually re-trigger itself. This can be specified by including: - -```yaml -only: - - tags -``` - #### `release-cli` Docker image The Docker image to use for the `release-cli` must be specified, using the following directive: @@ -3674,23 +3668,25 @@ A pipeline can have multiple `release` jobs, for example: ```yaml ios-release: - script: release > changelog.md + script: + - echo 'iOS release job' release: tag_name: v1.0.0-ios - description: changelog.md + description: 'iOS release v1.0.0' android-release: - script: release > changelog.md + script: + - echo 'Android release job' release: tag_name: v1.0.0-android - description: changelog.md + description: 'Android release v1.0.0' ``` #### `release:tag_name` The `tag_name` must be specified. It can refer to an existing Git tag or can be specified by the user. -When the specified tag doesn't exist in repository, a new tag is created from the associated SHA of the pipeline. +When the specified tag doesn't exist in the repository, a new tag is created from the associated SHA of the pipeline. For example, when creating a Release from a Git tag: @@ -3699,8 +3695,6 @@ job: release: tag_name: $CI_COMMIT_TAG description: changelog.txt - only: - - tags ``` It is also possible to create any unique tag, in which case `only: tags` is not mandatory. @@ -3719,17 +3713,16 @@ job: #### `release:name` -The Release name. This is an optional field. If omitted, it is populated with -`release:tag_name`. +The Release name. If omitted, it is populated with the value of `release: tag_name`. #### `release:description` -Specifies a file containing the longer description of the Release. This is a mandatory -field and can point to a changelog. +Specifies the longer description of the Release. #### `release:ref` -When the `tag_name` does not exist, `release:ref` specifies the commit to be used instead of the pipeline `ref`. If `tag_name` doesnโt exist, the release will be created from `ref`. `ref` can be a commit SHA, another tag name, or a branch name. +If the `release: tag_name` doesnโt exist yet, the release is created from `ref`. +`ref` can be a commit SHA, another tag name, or a branch name. #### `release:milestones` @@ -3737,36 +3730,64 @@ The title of each milestone the release is associated with. #### `release:released_at` -The date when the release will be or was ready. Defaults to the current time. Expected in ISO 8601 format (2019-03-15T08:00:00Z). +The date and time when the release is ready. Defaults to the current date and time if not +defined. Expected in ISO 8601 format (2019-03-15T08:00:00Z). #### Complete example for `release` -Combining the individual examples given above for `release`, we'd have the following code snippet: +Combining the individual examples given above for `release` results in the following +code snippets. There are two options, depending on how you generate the +tags. These options cannot be used together, so choose one: -```yaml -stages: - - build - - test - - release-stg +- To create a release when you push a Git tag, or when you add a Git tag + in the UI by going to **Repository > Tags**: -release_job: - stage: release - image: registry.gitlab.com/gitlab-org/release-cli:latest - only: - - tags - script: - - echo 'running release_job' - release: - name: 'Release $CI_COMMIT_SHA' - description: 'Created using the release-cli $EXTRA_DESCRIPTION' - tag_name: 'release-$CI_COMMIT_SHA' - ref: '$CI_COMMIT_SHA' - milestones: - - 'm1' - - 'm2' - - 'm3' - released_at: '2020-07-15T08:00:00Z' -``` + ```yaml + release_job: + stage: release + image: registry.gitlab.com/gitlab-org/release-cli:latest + rules: + - if: $CI_COMMIT_TAG # Run this job when a tag is created manually + script: + - echo 'running release_job' + release: + name: 'Release $CI_COMMIT_TAG' + description: 'Created using the release-cli $EXTRA_DESCRIPTION' # $EXTRA_DESCRIPTION must be defined + tag_name: '$CI_COMMIT_TAG' # elsewhere in the pipeline. + ref: '$CI_COMMIT_TAG' + milestones: + - 'm1' + - 'm2' + - 'm3' + released_at: '2020-07-15T08:00:00Z' # Optional, will auto generate if not defined, + # or can use a variable. + ``` + +- To create a release automatically when changes are pushed to the default branch, + using a new Git tag that is defined with variables: + + ```yaml + release_job: + stage: release + image: registry.gitlab.com/gitlab-org/release-cli:latest + rules: + - if: $CI_COMMIT_TAG + when: never # Do not run this job when a tag is created manually + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when the default branch changes + script: + - echo 'running release_job' + release: + name: 'Release $CI_COMMIT_SHA' + description: 'Created using the release-cli $EXTRA_DESCRIPTION' # $EXTRA_DESCRIPTION and the tag_name + tag_name: 'v${MAJOR}.${MINOR}.${REVISION}' # variables must be defined elsewhere + ref: '$CI_COMMIT_SHA' # in the pipeline. + milestones: + - 'm1' + - 'm2' + - 'm3' + released_at: '2020-07-15T08:00:00Z' # Optional, will auto generate if not defined, + # or can use a variable. + ``` #### `releaser-cli` command line @@ -3774,10 +3795,10 @@ The entries under the `:release` node are transformed into a `bash` command line to the Docker container, which contains the [release-cli](https://gitlab.com/gitlab-org/release-cli). You can also call the `release-cli` directly from a `script` entry. -The YAML described above would be transferred into a command line like this: +The YAML described above would be translated into a CLI command like this: ```shell -release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using the release-cli $EXTRA_DESCRIPTION" --tag-name "release-$CI_COMMIT_SHA" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3" +release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using the release-cli $EXTRA_DESCRIPTION" --tag-name "v${MAJOR}.${MINOR}.${REVISION}" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3" ``` ### `pages` diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index e9b59b8c222..04de415b859 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -104,7 +104,7 @@ knowledge of the following: - [GitLab Runner](https://docs.gitlab.com/runner/) - [Prometheus](https://prometheus.io/docs/introduction/overview/) -Auto DevOps provides great defaults for all the stages; you can, however, +Auto DevOps provides great defaults for all the stages and makes use of [CI templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates); you can, however, [customize](customize.md) almost everything to your needs. For an overview on the creation of Auto DevOps, read more @@ -114,6 +114,10 @@ NOTE: **Note** Kubernetes clusters can [be used without](../../user/project/clusters/index.md) Auto DevOps. +## Kubernetes requirements + +See [Auto DevOps requirements for Kubernetes](requirements.md#auto-devops-requirements-for-kubernetes). + ## Auto DevOps base domain The Auto DevOps base domain is required to use @@ -163,6 +167,10 @@ set the Auto DevOps base domain to `1.2.3.4.nip.io`. After completing setup, all requests hit the load balancer, which routes requests to the Kubernetes pods running your application. +### AWS ECS + +See [Auto DevOps requirements for Amazon ECS](requirements.md#auto-devops-requirements-for-amazon-ecs). + ## Enabling/Disabling Auto DevOps When first using Auto DevOps, review the [requirements](#requirements) to ensure diff --git a/doc/topics/autodevops/requirements.md b/doc/topics/autodevops/requirements.md index c0b43caaf78..33db94be97e 100644 --- a/doc/topics/autodevops/requirements.md +++ b/doc/topics/autodevops/requirements.md @@ -103,9 +103,9 @@ After all requirements are met, you can [enable Auto DevOps](index.md#enablingdi > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208132) in GitLab 13.0. -You can choose to target [Amazon Elastic Container Service (ECS)](../../ci/cloud_deployment/index.md) as a deployment platform instead of using Kubernetes. +You can choose to target [AWS ECS](../../ci/cloud_deployment/index.md) as a deployment platform instead of using Kubernetes. -To get started on Auto DevOps to Amazon ECS, you'll have to add a specific Environment +To get started on Auto DevOps to AWS ECS, you'll have to add a specific Environment Variable. To do so, follow these steps: 1. In your project, go to **Settings > CI / CD** and expand the **Variables** @@ -116,9 +116,13 @@ Variable. To do so, follow these steps: - `FARGATE` if the service you're targeting must be of launch type FARGATE. - `ECS` if you're not enforcing any launch type check when deploying to ECS. -When you trigger a pipeline, if Auto DevOps is enabled and if you've correctly +When you trigger a pipeline, if you have Auto DevOps enabled and if you have correctly [entered AWS credentials as environment variables](../../ci/cloud_deployment/index.md#deploy-your-application-to-the-aws-elastic-container-service-ecs), -your application will be deployed to Amazon ECS. +your application will be deployed to AWS ECS. + +NOTE: **Note:** +[GitLab Managed Apps](../../user/clusters/applications.md) are not available when deploying to AWS ECS. +You must manually configure your application (such as Ingress or Help) on AWS ECS. NOTE: **Note:** If you have both a valid `AUTO_DEVOPS_PLATFORM_TARGET` variable and a Kubernetes cluster tied to your project, @@ -130,5 +134,5 @@ defined in the [`Jobs/Deploy/ECS.gitlab-ci.yml` template](https://gitlab.com/git However, it's not recommended to [include](../../ci/yaml/README.md#includetemplate) it on its own. This template is designed to be used with Auto DevOps only. It may change unexpectedly causing your pipeline to fail if included on its own. Also, the job -names within this template may also change. Don't override these jobs' names in your +names within this template may also change. Do not override these jobs' names in your own pipeline, as the override will stop working when the name changes. diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index bf35db8ea2c..9dd6eec7744 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -509,7 +509,7 @@ before this feature was deployed to production (February 2020). Support for pre-existing projects on GitLab.com [is planned](https://gitlab.com/gitlab-org/gitlab/-/issues/196124). For self-managed instances, cleanup policies may be enabled by an admin in the -[CI/CD Package Registry settings](./../../admin_area/settings/index.md#cicd). +[GitLab application settings](../../../api/settings.md#change-application-settings) by setting `container_expiration_policies_enable_historic_entries` to true. Note the inherent [risks involved](./index.md#use-with-external-container-registries). The cleanup policy algorithm starts by collecting all the tags for a given repository in a list, diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 1ebba7b2871..9f07c1c7607 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -754,6 +754,8 @@ When a query returns too many data points, the heatmap data bucket dimensions te ### Templating variables for metrics dashboards +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214539) in GitLab 13.0. + Templating variables can be used to make your metrics dashboard more versatile. #### Templating variable types diff --git a/doc/user/project/pages/getting_started/pages_from_scratch.md b/doc/user/project/pages/getting_started/pages_from_scratch.md index 86523ab9d10..7278c734b07 100644 --- a/doc/user/project/pages/getting_started/pages_from_scratch.md +++ b/doc/user/project/pages/getting_started/pages_from_scratch.md @@ -161,7 +161,7 @@ is now available. If you want to do more advanced tasks, you can update your `.gitlab-ci.yml` file with [any of the available settings](../../../../ci/yaml/README.md). You can check -your CI syntax with the [GitLab CI/CD Lint Tool](https://gitlab.com/ci/lint). +your CI syntax with the [GitLab CI/CD Lint Tool](../../../../ci/yaml/README.md#validate-the-gitlab-ciyml). The following topics show other examples of other options you can add to your CI/CD file. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e6967574212..6a8d4d60f1b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6220,6 +6220,9 @@ msgstr "" msgid "ContainerRegistry|Cleanup policy:" msgstr "" +msgid "ContainerRegistry|Configuration digest: %{digest}" +msgstr "" + msgid "ContainerRegistry|Container Registry" msgstr "" @@ -6235,6 +6238,9 @@ msgstr "" msgid "ContainerRegistry|Delete selected" msgstr "" +msgid "ContainerRegistry|Digest: %{imageId}" +msgstr "" + msgid "ContainerRegistry|Docker connection error" msgstr "" @@ -6259,9 +6265,6 @@ msgstr "" msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password." msgstr "" -msgid "ContainerRegistry|Image ID: %{imageId}" -msgstr "" - msgid "ContainerRegistry|Image Repositories" msgstr "" @@ -6271,6 +6274,9 @@ msgstr "" msgid "ContainerRegistry|Login" msgstr "" +msgid "ContainerRegistry|Manifest digest: %{digest}" +msgstr "" + msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled" msgstr "" @@ -6280,6 +6286,9 @@ msgstr "" msgid "ContainerRegistry|Published %{timeInfo}" msgstr "" +msgid "ContainerRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}" +msgstr "" + msgid "ContainerRegistry|Push an image" msgstr "" diff --git a/spec/features/clusters/installing_applications_shared_examples.rb b/spec/features/clusters/installing_applications_shared_examples.rb index d2f28f5b219..263877f3849 100644 --- a/spec/features/clusters/installing_applications_shared_examples.rb +++ b/spec/features/clusters/installing_applications_shared_examples.rb @@ -2,6 +2,9 @@ RSpec.shared_examples "installing applications for a cluster" do |managed_apps_local_tiller| before do + # Reduce interval from 10 seconds which is too long for an automated test + stub_const("#{Clusters::ClustersController}::STATUS_POLLING_INTERVAL", 500) + stub_feature_flags(managed_apps_local_tiller: managed_apps_local_tiller) visit cluster_path @@ -107,10 +110,6 @@ RSpec.shared_examples "installing applications for a cluster" do |managed_apps_l end describe 'when user clicks install button' do - def domainname_form_value - page.find('.js-knative-domainname').value - end - before do allow(ClusterInstallAppWorker).to receive(:perform_async) allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) @@ -135,7 +134,7 @@ RSpec.shared_examples "installing applications for a cluster" do |managed_apps_l it 'shows status transition' do page.within('.js-cluster-application-row-knative') do - expect(domainname_form_value).to eq('domain.example.org') + expect(page).to have_field('Knative Domain Name:', with: 'domain.example.org') expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') end @@ -147,7 +146,7 @@ RSpec.shared_examples "installing applications for a cluster" do |managed_apps_l page.within('.js-cluster-application-row-knative') do expect(ClusterPatchAppWorker).to receive(:perform_async) - expect(domainname_form_value).to eq('domain.example.org') + expect(page).to have_field('Knative Domain Name:', with: 'domain.example.org') page.find('.js-knative-domainname').set("new.domain.example.org") @@ -155,7 +154,7 @@ RSpec.shared_examples "installing applications for a cluster" do |managed_apps_l wait_for_requests - expect(domainname_form_value).to eq('new.domain.example.org') + expect(page).to have_field('Knative Domain Name:', with: 'new.domain.example.org') end end end @@ -173,6 +172,8 @@ RSpec.shared_examples "installing applications for a cluster" do |managed_apps_l page.within('.js-cluster-application-row-cert_manager') do click_button 'Install' end + + wait_for_requests end it 'shows status transition' do diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb index a35f6420bdc..f7f1d7e81d6 100644 --- a/spec/features/projects/show/user_sees_git_instructions_spec.rb +++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb @@ -18,6 +18,8 @@ RSpec.describe 'Projects > Show > User sees Git instructions' do page.within '.empty-wrapper' do expect(page).to have_content('Command line instructions') end + + expect(page).to have_content("git push -u origin master") end end @@ -59,6 +61,25 @@ RSpec.describe 'Projects > Show > User sees Git instructions' do include_examples 'shows details of empty project with no repo' end + context ":default_branch_name is specified" do + let_it_be(:project) { create(:project, :public) } + + before do + expect(Gitlab::CurrentSettings) + .to receive(:default_branch_name) + .and_return('example_branch') + + sign_in(project.owner) + visit project_path(project) + end + + it "recommends default_branch_name instead of master" do + click_link 'Create empty repository' + + expect(page).to have_content("git push -u origin example_branch") + end + end + context 'when project is empty' do let_it_be(:project) { create(:project_empty_repo, :public) } diff --git a/spec/finders/projects/integrations/jira/issues_finder_spec.rb b/spec/finders/projects/integrations/jira/issues_finder_spec.rb index dada0781f1c..266f1f82d2a 100644 --- a/spec/finders/projects/integrations/jira/issues_finder_spec.rb +++ b/spec/finders/projects/integrations/jira/issues_finder_spec.rb @@ -75,6 +75,30 @@ RSpec.describe Projects::Integrations::Jira::IssuesFinder do expect(service.total_count).to eq 375 expect(issues.map(&:key)).to eq(%w[TEST-1 TEST-2]) end + + context 'when sort by created_date' do + let(:params) { { sort: 'created_date' } } + + it 'maps sort correctly' do + expect(::Jira::JqlBuilderService).to receive(:new) + .with(jira_service.project_key, { sort: 'created', sort_direction: 'DESC' }) + .and_call_original + + subject + end + end + + context 'when sort by unknown_sort' do + let(:params) { { sort: 'unknown_sort' } } + + it 'maps sort to default' do + expect(::Jira::JqlBuilderService).to receive(:new) + .with(jira_service.project_key, { sort: 'created', sort_direction: 'DESC' }) + .and_call_original + + subject + end + end end end end diff --git a/spec/frontend/registry/explorer/components/details_page/details_row_spec.js b/spec/frontend/registry/explorer/components/details_page/details_row_spec.js new file mode 100644 index 00000000000..95b8e18d677 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/details_row_spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/details_row.vue'; + +describe('DetailsRow', () => { + let wrapper; + + const findIcon = () => wrapper.find(GlIcon); + const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); + + const mountComponent = () => { + wrapper = shallowMount(component, { + propsData: { + icon: 'clock', + }, + slots: { + default: '<div data-testid="default-slot"></div>', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('contains an icon', () => { + mountComponent(); + expect(findIcon().exists()).toBe(true); + }); + + it('icon has the correct props', () => { + mountComponent(); + expect(findIcon().props()).toMatchObject({ + name: 'clock', + }); + }); + + it('has a default slot', () => { + mountComponent(); + expect(findDefaultSlot().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/empty_tags_state.js b/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js index da80c75a26a..09afd9d2d84 100644 --- a/spec/frontend/registry/explorer/components/details_page/empty_tags_state.js +++ b/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js @@ -29,7 +29,7 @@ describe('EmptyTagsState component', () => { it('contains gl-empty-state', () => { mountComponent(); - expect(findEmptyState().exist()).toBe(true); + expect(findEmptyState().exists()).toBe(true); }); it('has the correct props', () => { diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js index 9af26b31847..5d465217d78 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js @@ -1,11 +1,11 @@ import { shallowMount } from '@vue/test-utils'; import { GlFormCheckbox, GlSprintf } from '@gitlab/ui'; -import component from '~/registry/explorer/components/details_page/tags_list_row.vue'; -import ListItem from '~/registry/explorer/components/list_item.vue'; -import DeleteButton from '~/registry/explorer/components/delete_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import component from '~/registry/explorer/components/details_page/tags_list_row.vue'; +import DeleteButton from '~/registry/explorer/components/delete_button.vue'; +import DetailsRow from '~/registry/explorer/components/details_page/details_row.vue'; import { REMOVE_TAG_BUTTON_TITLE, REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, @@ -13,6 +13,7 @@ import { import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { tagsListResponse } from '../../mock_data'; +import { ListItem } from '../../stubs'; describe('tags list row', () => { let wrapper; @@ -24,16 +25,21 @@ describe('tags list row', () => { const findName = () => wrapper.find('[data-testid="name"]'); const findSize = () => wrapper.find('[data-testid="size"]'); const findTime = () => wrapper.find('[data-testid="time"]'); - const findShortRevision = () => wrapper.find('[data-testid="short-revision"]'); + const findShortRevision = () => wrapper.find('[data-testid="digest"]'); const findClipboardButton = () => wrapper.find(ClipboardButton); const findDeleteButton = () => wrapper.find(DeleteButton); const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip); + const findDetailsRows = () => wrapper.findAll(DetailsRow); + const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]'); + const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]'); + const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]'); const mountComponent = (propsData = defaultProps) => { wrapper = shallowMount(component, { stubs: { GlSprintf, ListItem, + DetailsRow, }, propsData, directives: { @@ -114,6 +120,7 @@ describe('tags list row', () => { it('is hidden if tag does not have a location', () => { mountComponent({ ...defaultProps, tag: { ...tag, location: null } }); + expect(findClipboardButton().exists()).toBe(false); }); @@ -136,21 +143,25 @@ describe('tags list row', () => { it('contains the total_size and layers', () => { mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024 } }); + expect(findSize().text()).toMatchInterpolatedText('1.00 KiB ยท 10 layers'); }); it('when total_size is missing', () => { mountComponent(); + expect(findSize().text()).toMatchInterpolatedText('10 layers'); }); it('when layers are missing', () => { mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024, layers: null } }); + expect(findSize().text()).toMatchInterpolatedText('1.00 KiB'); }); it('when there is 1 layer', () => { mountComponent({ ...defaultProps, tag: { ...tag, layers: 1 } }); + expect(findSize().text()).toMatchInterpolatedText('1 layer'); }); }); @@ -181,7 +192,7 @@ describe('tags list row', () => { }); }); - describe('shortRevision', () => { + describe('digest', () => { it('exists', () => { mountComponent(); @@ -191,7 +202,7 @@ describe('tags list row', () => { it('has the correct text', () => { mountComponent(); - expect(findShortRevision().text()).toMatchInterpolatedText('Image ID: b118ab5b0'); + expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 1ab51d5'); }); }); @@ -226,4 +237,39 @@ describe('tags list row', () => { expect(wrapper.emitted('delete')).toEqual([[]]); }); }); + + describe('details rows', () => { + beforeEach(() => { + mountComponent(); + + return wrapper.vm.$nextTick(); + }); + + it('has 3 details rows', () => { + expect(findDetailsRows().length).toBe(3); + }); + + describe.each` + name | finderFunction | text | icon | clipboard + ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false} + ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true} + ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true} + `('$name details row', ({ finderFunction, text, icon, clipboard }) => { + it(`has ${text} as text`, () => { + expect(finderFunction().text()).toMatchInterpolatedText(text); + }); + + it(`has the ${icon} icon`, () => { + expect(finderFunction().props('icon')).toBe(icon); + }); + + it(`is ${clipboard} that clipboard button exist`, () => { + expect( + finderFunction() + .find(ClipboardButton) + .exists(), + ).toBe(clipboard); + }); + }); + }); }); diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap index 3761369c944..a8412e2bde9 100644 --- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap +++ b/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap @@ -2,13 +2,10 @@ exports[`Registry Group Empty state to match the default snapshot 1`] = ` <div - class="container-message" svg-path="foo" title="There are no container images available in this group" > - <p - class="js-no-container-images-text" - > + <p> With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. <gl-link-stub href="baz" diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap index d8ec9c3ca4d..8413e17c7b2 100644 --- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap +++ b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap @@ -2,13 +2,10 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` <div - class="container-message" svg-path="bazFoo" title="There are no container images stored for this project" > - <p - class="js-no-container-images-text" - > + <p> With the Container Registry, every project can have its own space to store its Docker images. <gl-link-stub href="baz" @@ -22,9 +19,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` CLI Commands </h5> - <p - class="js-not-logged-in-to-registry-text" - > + <p> If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have <gl-link-stub href="barBaz" @@ -42,78 +37,50 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` instead of a password. </p> - <div - class="input-group append-bottom-10" + <gl-form-input-group-stub + class="gl-mb-4" + predefinedoptions="[object Object]" + value="" > - <input - class="form-control monospace" - readonly="readonly" + <gl-form-input-stub + class="gl-font-monospace!" + readonly="" type="text" + value="docker login bar" /> - - <span - class="input-group-append" - > - <clipboard-button-stub - class="input-group-text" - cssclass="btn-default" - text="docker login bar" - title="Copy login command" - tooltipplacement="top" - /> - </span> - </div> + </gl-form-input-group-stub> - <p /> - - <p> + <p + class="gl-mb-4" + > You can add an image to this registry with the following commands: </p> - <div - class="input-group append-bottom-10" + <gl-form-input-group-stub + class="gl-mb-4 " + predefinedoptions="[object Object]" + value="" > - <input - class="form-control monospace" - readonly="readonly" + <gl-form-input-stub + class="gl-font-monospace!" + readonly="" type="text" + value="docker build -t foo ." /> - - <span - class="input-group-append" - > - <clipboard-button-stub - class="input-group-text" - cssclass="btn-default" - text="docker build -t foo ." - title="Copy build command" - tooltipplacement="top" - /> - </span> - </div> + </gl-form-input-group-stub> - <div - class="input-group" + <gl-form-input-group-stub + predefinedoptions="[object Object]" + value="" > - <input - class="form-control monospace" - readonly="readonly" + <gl-form-input-stub + class="gl-font-monospace!" + readonly="" type="text" + value="docker push foo" /> - - <span - class="input-group-append" - > - <clipboard-button-stub - class="input-group-text" - cssclass="btn-default" - text="docker push foo" - title="Copy push command" - tooltipplacement="top" - /> - </span> - </div> + </gl-form-input-group-stub> </div> `; diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js index e1b39f65961..a7ffed4c9fd 100644 --- a/spec/frontend/registry/explorer/mock_data.js +++ b/spec/frontend/registry/explorer/mock_data.js @@ -70,9 +70,10 @@ export const tagsListResponse = { size: 19, layers: 10, location: 'location', - path: 'bar', - created_at: '1505828744434', + path: 'bar:centos6', + created_at: '2020-06-29T10:23:51.766+00:00', destroy_path: 'path', + digest: 'sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c', }, { name: 'test-tag', @@ -80,9 +81,10 @@ export const tagsListResponse = { short_revision: 'b969de599', size: 19, layers: 10, - path: 'foo', + path: 'foo:test-tag', location: 'location-2', - created_at: '1505828744434', + created_at: '2020-06-29T10:23:51.766+00:00', + digest: 'sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7736dfd5c', }, ], headers, diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/registry/explorer/stubs.js index fae821014be..8f95fce2867 100644 --- a/spec/frontend/registry/explorer/stubs.js +++ b/spec/frontend/registry/explorer/stubs.js @@ -1,4 +1,5 @@ import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue'; +import RealListItem from '~/registry/explorer/components/list_item.vue'; export const GlModal = { template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>', @@ -29,3 +30,13 @@ export const GlSkeletonLoader = { template: `<div><slot></slot></div>`, props: ['width', 'height'], }; + +export const ListItem = { + ...RealListItem, + data() { + return { + detailsSlots: [], + isDetailsShown: true, + }; + }, +}; diff --git a/spec/services/jira/jql_builder_service_spec.rb b/spec/services/jira/jql_builder_service_spec.rb index bba84d2e4d5..310ba1a43fd 100644 --- a/spec/services/jira/jql_builder_service_spec.rb +++ b/spec/services/jira/jql_builder_service_spec.rb @@ -10,7 +10,47 @@ RSpec.describe Jira::JqlBuilderService do let(:params) { {} } it 'builds jql with default ordering' do - expect(subject).to eq("project = PROJECT_KEY order by created DESC") + expect(subject).to eq('project = PROJECT_KEY order by created DESC') + end + end + + context 'with search param' do + let(:params) { { search: 'new issue' } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND (summary ~ \"new issue\" OR description ~ \"new issue\") order by created DESC") + end + + context 'search param with single qoutes' do + let(:params) { { search: "new issue's" } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND (summary ~ \"new issue's\" OR description ~ \"new issue's\") order by created DESC") + end + end + + context 'search param with single double qoutes' do + let(:params) { { search: '"one \"more iss\'ue"' } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND (summary ~ \"one more iss'ue\" OR description ~ \"one more iss'ue\") order by created DESC") + end + end + + context 'search param with special characters' do + let(:params) { { search: 'issues' + Jira::JqlBuilderService::JQL_SPECIAL_CHARS.join(" AND ") } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND (summary ~ \"issues and and and and and and and and and and and and and and and and\" OR description ~ \"issues and and and and and and and and and and and and and and and and\") order by created DESC") + end + end + end + + context 'with labels param' do + let(:params) { { labels: ['label1', 'label2', "\"'try\"some'more\"quote'here\""] } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND labels = \"label1\" AND labels = \"label2\" AND labels = \"\\\"'try\\\"some'more\\\"quote'here\\\"\" order by created DESC") end end @@ -18,7 +58,7 @@ RSpec.describe Jira::JqlBuilderService do let(:params) { { sort: 'updated', sort_direction: 'ASC' } } it 'builds jql' do - expect(subject).to eq("project = PROJECT_KEY order by updated ASC") + expect(subject).to eq('project = PROJECT_KEY order by updated ASC') end end end diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb index 91d3e96cd53..3d065deefdf 100644 --- a/spec/services/projects/container_repository/delete_tags_service_spec.rb +++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb @@ -20,6 +20,31 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do tags: %w(latest A Ba Bb C D E)) end + RSpec.shared_examples 'logging a success response' do + it 'logs an info message' do + expect(service).to receive(:log_info).with( + service_class: 'Projects::ContainerRepository::DeleteTagsService', + message: 'deleted tags', + container_repository_id: repository.id, + deleted_tags_count: tags.size + ) + + subject + end + end + + RSpec.shared_examples 'logging an error response' do |message: 'could not delete tags'| + it 'logs an error message' do + expect(service).to receive(:log_error).with( + service_class: 'Projects::ContainerRepository::DeleteTagsService', + message: message, + container_repository_id: repository.id + ) + + subject + end + end + describe '#execute' do let(:tags) { %w[A] } @@ -47,11 +72,8 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do let_it_be(:tags) { %w[A Ba] } it 'deletes the tags by name' do - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/A") - .to_return(status: 200, body: "") - - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/Ba") - .to_return(status: 200, body: "") + stub_delete_reference_request('A') + stub_delete_reference_request('Ba') expect_delete_tag_by_name('A') expect_delete_tag_by_name('Ba') @@ -60,26 +82,29 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do end it 'succeeds when tag delete returns 404' do - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/A") - .to_return(status: 200, body: "") - - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/Ba") - .to_return(status: 404, body: "") + stub_delete_reference_request('A') + stub_delete_reference_request('Ba', 404) is_expected.to include(status: :success) end + it_behaves_like 'logging a success response' do + before do + stub_delete_reference_request('A') + stub_delete_reference_request('Ba') + end + end + context 'with failures' do context 'when the delete request fails' do before do - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/A") - .to_return(status: 500, body: "") - - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/Ba") - .to_return(status: 500, body: "") + stub_delete_reference_request('A', 500) + stub_delete_reference_request('Ba', 500) end it { is_expected.to include(status: :error) } + + it_behaves_like 'logging an error response' end end end @@ -104,19 +129,35 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do end end end + context 'and the feature is disabled' do + let_it_be(:tags) { %w[A Ba] } + before do stub_feature_flags(container_registry_fast_tag_delete: false) + stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') + stub_put_manifest_request('A') + stub_put_manifest_request('Ba') end it 'fallbacks to slow delete' do expect(service).not_to receive(:fast_delete) - expect(service).to receive(:slow_delete).with(repository, tags) + expect(service).to receive(:slow_delete).with(repository, tags).and_call_original + + expect_delete_tag_by_digest('sha256:dummy') subject end + + it_behaves_like 'logging a success response' do + before do + allow(service).to receive(:slow_delete).and_call_original + expect_delete_tag_by_digest('sha256:dummy') + end + end end end + context 'when the registry does not support fast delete' do let_it_be(:project) { create(:project, :private) } let_it_be(:repository) { create(:container_repository, :root, project: project) } @@ -155,11 +196,8 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do it 'deletes the tags using a dummy image' do stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) - - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + stub_put_manifest_request('A') + stub_put_manifest_request('Ba') expect_delete_tag_by_digest('sha256:dummy') @@ -169,11 +207,8 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do it 'succeeds when tag delete returns 404' do stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) - - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + stub_put_manifest_request('A') + stub_put_manifest_request('Ba') stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy") .to_return(status: 404, body: "", headers: {}) @@ -181,6 +216,15 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do is_expected.to include(status: :success) end + it_behaves_like 'logging a success response' do + before do + stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') + stub_put_manifest_request('A') + stub_put_manifest_request('Ba') + expect_delete_tag_by_digest('sha256:dummy') + end + end + context 'with failures' do context 'when the dummy manifest generation fails' do before do @@ -188,23 +232,23 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do end it { is_expected.to include(status: :error) } + + it_behaves_like 'logging an error response', message: 'could not generate manifest' end context 'when updating the tags fails' do before do stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") - .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) - - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") - .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + stub_put_manifest_request('A', 500) + stub_put_manifest_request('Ba', 500) stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3") .to_return(status: 200, body: "", headers: {}) end it { is_expected.to include(status: :error) } + it_behaves_like 'logging an error response' end end end @@ -214,6 +258,16 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do private + def stub_delete_reference_request(tag, status = 200) + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}") + .to_return(status: status, body: '') + end + + def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' }) + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") + .to_return(status: status, body: '', headers: headers) + end + def stub_tag_digest(tag, digest) stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") .to_return(status: 200, body: "", headers: { 'docker-content-digest' => digest }) |