Commit e6baeaba authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 5064bf8c
...@@ -34,7 +34,7 @@ export const LIST_KEY_CHECKBOX = 'checkbox'; ...@@ -34,7 +34,7 @@ export const LIST_KEY_CHECKBOX = 'checkbox';
export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag'); export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID'); export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = s__('ContainerRegistry|Size'); export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated'); export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
export const EXPIRATION_POLICY_ALERT_TITLE = s__( export const EXPIRATION_POLICY_ALERT_TITLE = s__(
......
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import RegistryExplorer from './pages/index.vue'; import RegistryExplorer from './pages/index.vue';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue'; import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
...@@ -6,6 +7,7 @@ import { createStore } from './stores'; ...@@ -6,6 +7,7 @@ import { createStore } from './stores';
import createRouter from './router'; import createRouter from './router';
Vue.use(Translate); Vue.use(Translate);
Vue.use(GlToast);
export default () => { export default () => {
const el = document.getElementById('js-container-registry'); const el = document.getElementById('js-container-registry');
......
...@@ -31,6 +31,10 @@ import { ...@@ -31,6 +31,10 @@ import {
LIST_LABEL_IMAGE_ID, LIST_LABEL_IMAGE_ID,
LIST_LABEL_SIZE, LIST_LABEL_SIZE,
LIST_LABEL_LAST_UPDATED, LIST_LABEL_LAST_UPDATED,
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
} from '../constants'; } from '../constants';
export default { export default {
...@@ -176,17 +180,37 @@ export default { ...@@ -176,17 +180,37 @@ export default {
}, },
handleSingleDelete(itemToDelete) { handleSingleDelete(itemToDelete) {
this.itemsToBeDeleted = []; this.itemsToBeDeleted = [];
this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id }); return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id })
.then(() =>
this.$toast.show(DELETE_TAG_SUCCESS_MESSAGE, {
type: 'success',
}),
)
.catch(() =>
this.$toast.show(DELETE_TAG_ERROR_MESSAGE, {
type: 'error',
}),
);
}, },
handleMultipleDelete() { handleMultipleDelete() {
const { itemsToBeDeleted } = this; const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = []; this.itemsToBeDeleted = [];
this.selectedItems = []; this.selectedItems = [];
this.requestDeleteTags({ return this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => this.tags[x].name), ids: itemsToBeDeleted.map(x => this.tags[x].name),
params: this.$route.params.id, params: this.$route.params.id,
}); })
.then(() =>
this.$toast.show(DELETE_TAGS_SUCCESS_MESSAGE, {
type: 'success',
}),
)
.catch(() =>
this.$toast.show(DELETE_TAGS_ERROR_MESSAGE, {
type: 'error',
}),
);
}, },
onDeletionConfirmed() { onDeletionConfirmed() {
this.track('confirm_delete'); this.track('confirm_delete');
......
<script> <script>
export default {}; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
export default {
components: {
GlAlert,
GlSprintf,
GlLink,
},
i18n: {
garbageCollectionTipText: s__(
'ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
),
},
computed: {
...mapState(['config']),
...mapGetters(['showGarbageCollection']),
},
methods: {
...mapActions(['setShowGarbageCollectionTip']),
},
};
</script> </script>
<template> <template>
<div> <div>
<gl-alert
v-if="showGarbageCollection"
variant="tip"
class="my-2"
@dismiss="setShowGarbageCollectionTip(false)"
>
<gl-sprintf :message="$options.i18n.garbageCollectionTipText">
<template #docLink="{content}">
<gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<transition name="slide"> <transition name="slide">
<router-view /> <router-view ref="router-view" />
</transition> </transition>
</div> </div>
</template> </template>
...@@ -12,11 +12,13 @@ import { ...@@ -12,11 +12,13 @@ import {
GlSkeletonLoader, GlSkeletonLoader,
} from '@gitlab/ui'; } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue'; import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue'; import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue'; import ProjectPolicyAlert from '../components/project_policy_alert.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue'; import QuickstartDropdown from '../components/quickstart_dropdown.vue';
import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE } from '../constants';
export default { export default {
name: 'RegistryListApp', name: 'RegistryListApp',
...@@ -44,6 +46,23 @@ export default { ...@@ -44,6 +46,23 @@ export default {
width: 1000, width: 1000,
height: 40, height: 40,
}, },
i18n: {
containerRegistryTitle: s__('ContainerRegistry|Container Registry'),
connectionErrorTitle: s__('ContainerRegistry|Docker connection error'),
connectionErrorMessage: s__(
`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}`,
),
introText: s__(
`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`,
),
deleteButtonDisabled: s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
),
removeRepositoryLabel: s__('ContainerRegistry|Remove repository'),
removeRepositoryModalText: s__(
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
),
},
data() { data() {
return { return {
itemToDelete: {}, itemToDelete: {},
...@@ -76,10 +95,22 @@ export default { ...@@ -76,10 +95,22 @@ export default {
this.itemToDelete = item; this.itemToDelete = item;
this.$refs.deleteModal.show(); this.$refs.deleteModal.show();
}, },
handleDeleteRepository() { handleDeleteImage() {
this.track('confirm_delete'); this.track('confirm_delete');
this.requestDeleteImage(this.itemToDelete.destroy_path); return this.requestDeleteImage(this.itemToDelete.destroy_path)
this.itemToDelete = {}; .then(() =>
this.$toast.show(DELETE_IMAGE_SUCCESS_MESSAGE, {
type: 'success',
}),
)
.catch(() =>
this.$toast.show(DELETE_IMAGE_ERROR_MESSAGE, {
type: 'error',
}),
)
.finally(() => {
this.itemToDelete = {};
});
}, },
encodeListItem(item) { encodeListItem(item) {
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id }); const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
...@@ -95,18 +126,12 @@ export default { ...@@ -95,18 +126,12 @@ export default {
<gl-empty-state <gl-empty-state
v-if="config.characterError" v-if="config.characterError"
:title="s__('ContainerRegistry|Docker connection error')" :title="$options.i18n.connectionErrorTitle"
:svg-path="config.containersErrorImage" :svg-path="config.containersErrorImage"
> >
<template #description> <template #description>
<p> <p>
<gl-sprintf <gl-sprintf :message="$options.i18n.connectionErrorMessage">
:message="
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path.
%{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}"> <template #docLink="{content}">
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank"> <gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
{{ content }} {{ content }}
...@@ -120,17 +145,11 @@ export default { ...@@ -120,17 +145,11 @@ export default {
<template v-else> <template v-else>
<div> <div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> <h4>{{ $options.i18n.containerRegistryTitle }}</h4>
<quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" /> <quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" />
</div> </div>
<p> <p>
<gl-sprintf <gl-sprintf :message="$options.i18n.introText">
:message="
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}"> <template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank"> <gl-link :href="config.helpPagePath" target="_blank">
{{ content }} {{ content }}
...@@ -180,16 +199,14 @@ export default { ...@@ -180,16 +199,14 @@ export default {
<div <div
v-gl-tooltip="{ disabled: listItem.destroy_path }" v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block" class="d-none d-sm-block"
:title=" :title="$options.i18n.deleteButtonDisabled"
s__('ContainerRegistry|Missing or insufficient permission, delete button disabled')
"
> >
<gl-button <gl-button
ref="deleteImageButton" ref="deleteImageButton"
v-gl-tooltip v-gl-tooltip
:disabled="!listItem.destroy_path" :disabled="!listItem.destroy_path"
:title="s__('ContainerRegistry|Remove repository')" :title="$options.i18n.removeRepositoryLabel"
:aria-label="s__('ContainerRegistry|Remove repository')" :aria-label="$options.i18n.removeRepositoryLabel"
class="btn-inverted" class="btn-inverted"
variant="danger" variant="danger"
@click="deleteImage(listItem)" @click="deleteImage(listItem)"
...@@ -217,16 +234,12 @@ export default { ...@@ -217,16 +234,12 @@ export default {
ref="deleteModal" ref="deleteModal"
modal-id="delete-image-modal" modal-id="delete-image-modal"
ok-variant="danger" ok-variant="danger"
@ok="handleDeleteRepository" @ok="handleDeleteImage"
@cancel="track('cancel_delete')" @cancel="track('cancel_delete')"
> >
<template #modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template> <template #modal-title>{{ $options.i18n.removeRepositoryLabel }}</template>
<p> <p>
<gl-sprintf <gl-sprintf :message="$options.i18n.removeRepositoryModalText">
:message=" s__(
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
),"
>
<template #title> <template #title>
<b>{{ itemToDelete.path }}</b> <b>{{ itemToDelete.path }}</b>
</template> </template>
......
...@@ -6,16 +6,12 @@ import { ...@@ -6,16 +6,12 @@ import {
DEFAULT_PAGE, DEFAULT_PAGE,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
FETCH_TAGS_LIST_ERROR_MESSAGE, FETCH_TAGS_LIST_ERROR_MESSAGE,
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
DELETE_IMAGE_SUCCESS_MESSAGE,
} from '../constants'; } from '../constants';
import { decodeAndParse } from '../utils'; import { decodeAndParse } from '../utils';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setShowGarbageCollectionTip = ({ commit }, data) =>
commit(types.SET_SHOW_GARBAGE_COLLECTION_TIP, data);
export const receiveImagesListSuccess = ({ commit }, { data, headers }) => { export const receiveImagesListSuccess = ({ commit }, { data, headers }) => {
commit(types.SET_IMAGES_LIST_SUCCESS, data); commit(types.SET_IMAGES_LIST_SUCCESS, data);
...@@ -67,11 +63,10 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) = ...@@ -67,11 +63,10 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) =
return axios return axios
.delete(tag.destroy_path) .delete(tag.destroy_path)
.then(() => { .then(() => {
createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success'); dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params }); return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .catch(() => {
createFlash(DELETE_TAG_ERROR_MESSAGE);
commit(types.SET_MAIN_LOADING, false); commit(types.SET_MAIN_LOADING, false);
}); });
}; };
...@@ -85,11 +80,10 @@ export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) ...@@ -85,11 +80,10 @@ export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params })
return axios return axios
.delete(url, { params: { ids } }) .delete(url, { params: { ids } })
.then(() => { .then(() => {
createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success'); dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params }); return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .catch(() => {
createFlash(DELETE_TAGS_ERROR_MESSAGE);
commit(types.SET_MAIN_LOADING, false); commit(types.SET_MAIN_LOADING, false);
}); });
}; };
...@@ -100,11 +94,8 @@ export const requestDeleteImage = ({ commit, dispatch, state }, destroyPath) => ...@@ -100,11 +94,8 @@ export const requestDeleteImage = ({ commit, dispatch, state }, destroyPath) =>
return axios return axios
.delete(destroyPath) .delete(destroyPath)
.then(() => { .then(() => {
dispatch('setShowGarbageCollectionTip', true);
dispatch('requestImagesList', { pagination: state.pagination }); dispatch('requestImagesList', { pagination: state.pagination });
createFlash(DELETE_IMAGE_SUCCESS_MESSAGE, 'success');
})
.catch(() => {
createFlash(DELETE_IMAGE_ERROR_MESSAGE);
}) })
.finally(() => { .finally(() => {
commit(types.SET_MAIN_LOADING, false); commit(types.SET_MAIN_LOADING, false);
......
...@@ -18,3 +18,7 @@ export const dockerLoginCommand = state => { ...@@ -18,3 +18,7 @@ export const dockerLoginCommand = state => {
/* eslint-disable @gitlab/require-i18n-strings */ /* eslint-disable @gitlab/require-i18n-strings */
return `docker login ${state.config.registryHostUrlWithPort}`; return `docker login ${state.config.registryHostUrlWithPort}`;
}; };
export const showGarbageCollection = state => {
return state.showGarbageCollectionTip && state.config.isAdmin;
};
...@@ -5,3 +5,4 @@ export const SET_PAGINATION = 'SET_PAGINATION'; ...@@ -5,3 +5,4 @@ export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING'; export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION'; export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION';
export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS'; export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS';
export const SET_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP';
...@@ -7,6 +7,7 @@ export default { ...@@ -7,6 +7,7 @@ export default {
...config, ...config,
expirationPolicy: config.expirationPolicy ? JSON.parse(config.expirationPolicy) : undefined, expirationPolicy: config.expirationPolicy ? JSON.parse(config.expirationPolicy) : undefined,
isGroupPage: config.isGroupPage !== undefined, isGroupPage: config.isGroupPage !== undefined,
isAdmin: config.isAdmin !== undefined,
}; };
}, },
...@@ -22,6 +23,10 @@ export default { ...@@ -22,6 +23,10 @@ export default {
state.isLoading = isLoading; state.isLoading = isLoading;
}, },
[types.SET_SHOW_GARBAGE_COLLECTION_TIP](state, showGarbageCollectionTip) {
state.showGarbageCollectionTip = showGarbageCollectionTip;
},
[types.SET_PAGINATION](state, headers) { [types.SET_PAGINATION](state, headers) {
const normalizedHeaders = normalizeHeaders(headers); const normalizedHeaders = normalizeHeaders(headers);
state.pagination = parseIntPagination(normalizedHeaders); state.pagination = parseIntPagination(normalizedHeaders);
......
export default () => ({ export default () => ({
isLoading: false, isLoading: false,
showGarbageCollectionTip: false,
config: {}, config: {},
images: [], images: [],
tags: [], tags: [],
......
...@@ -7,6 +7,10 @@ ...@@ -7,6 +7,10 @@
a { a {
color: $gl-text-color; color: $gl-text-color;
&.link {
color: $blue-600;
}
} }
.author-link { .author-link {
......
...@@ -68,10 +68,12 @@ module EventsHelper ...@@ -68,10 +68,12 @@ module EventsHelper
end end
def event_preposition(event) def event_preposition(event)
if event.push_action? || event.commented_action? || event.target if event.wiki_page?
"at" 'in the wiki for'
elsif event.milestone? elsif event.milestone?
"in" 'in'
elsif event.push_action? || event.commented_action? || event.target
'at'
end end
end end
...@@ -172,6 +174,19 @@ module EventsHelper ...@@ -172,6 +174,19 @@ module EventsHelper
end end
end end
def event_wiki_title_html(event)
capture do
concat content_tag(:span, _('wiki page'), class: "event-target-type append-right-4")
concat link_to(event.target_title, event_wiki_page_target_url(event),
title: event.target_title,
class: 'has-tooltip event-target-link append-right-4')
end
end
def event_wiki_page_target_url(event)
project_wiki_url(event.project, event.target.canonical_slug)
end
def event_note_title_html(event) def event_note_title_html(event)
if event.note_target if event.note_target
capture do capture do
......
...@@ -73,10 +73,10 @@ class BuildkiteService < CiService ...@@ -73,10 +73,10 @@ class BuildkiteService < CiService
end end
def calculate_reactive_cache(sha, ref) def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.get(commit_status_path(sha), verify: false) response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
status = status =
if response.code == 200 && response['status'] if response&.code == 200 && response['status']
response['status'] response['status']
else else
:error :error
...@@ -117,4 +117,8 @@ class BuildkiteService < CiService ...@@ -117,4 +117,8 @@ class BuildkiteService < CiService
ENDPOINT ENDPOINT
end end
end end
def request_options
{ verify: false, extra_log_info: { project_id: project_id } }
end
end end
...@@ -50,10 +50,12 @@ class DroneCiService < CiService ...@@ -50,10 +50,12 @@ class DroneCiService < CiService
end end
def calculate_reactive_cache(sha, ref) def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.get(commit_status_path(sha, ref), verify: enable_ssl_verification) response = Gitlab::HTTP.try_get(commit_status_path(sha, ref),
verify: enable_ssl_verification,
extra_log_info: { project_id: project_id })
status = status =
if response.code == 200 && response['status'] if response && response.code == 200 && response['status']
case response['status'] case response['status']
when 'killed' when 'killed'
:canceled :canceled
...@@ -68,8 +70,6 @@ class DroneCiService < CiService ...@@ -68,8 +70,6 @@ class DroneCiService < CiService
end end
{ commit_status: status } { commit_status: status }
rescue *Gitlab::HTTP::HTTP_ERRORS
{ commit_status: :error }
end end
def build_page(sha, ref) def build_page(sha, ref)
......
...@@ -293,12 +293,12 @@ class Snippet < ApplicationRecord ...@@ -293,12 +293,12 @@ class Snippet < ApplicationRecord
return if repository_exists? && snippet_repository return if repository_exists? && snippet_repository
repository.create_if_not_exists repository.create_if_not_exists
track_snippet_repository track_snippet_repository(repository.storage)
end end
def track_snippet_repository def track_snippet_repository(shard)
repository = snippet_repository || build_snippet_repository snippet_repo = snippet_repository || build_snippet_repository
repository.update!(shard_name: repository_storage, disk_path: disk_path) snippet_repo.update!(shard_name: shard, disk_path: disk_path)
end end
def can_cache_field?(field) def can_cache_field?(field)
......
...@@ -8,53 +8,30 @@ module Projects ...@@ -8,53 +8,30 @@ module Projects
return error('access denied') unless can_destroy? return error('access denied') unless can_destroy?
tags = container_repository.tags tags = container_repository.tags
tags_by_digest = group_by_digest(tags)
tags = without_latest(tags) tags = without_latest(tags)
tags = filter_by_name(tags) tags = filter_by_name(tags)
tags = with_manifest(tags)
tags = order_by_date(tags)
tags = filter_keep_n(tags) tags = filter_keep_n(tags)
tags = filter_by_older_than(tags) tags = filter_by_older_than(tags)
deleted_tags = delete_tags(tags, tags_by_digest) delete_tags(container_repository, tags)
success(deleted: deleted_tags.map(&:name))
end end
private private
def delete_tags(tags_to_delete, tags_by_digest) def delete_tags(container_repository, tags)
deleted_digests = group_by_digest(tags_to_delete).select do |digest, tags| return success(deleted: []) unless tags.any?
delete_tag_digest(tags, tags_by_digest[digest])
end
deleted_digests.values.flatten
end
def delete_tag_digest(tags, other_tags)
# Issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/21405
# we have to remove all tags due
# to Docker Distribution bug unable
# to delete single tag
return unless tags.count == other_tags.count
# delete all tags tag_names = tags.map(&:name)
tags.map(&:unsafe_delete)
end
def group_by_digest(tags) Projects::ContainerRepository::DeleteTagsService
tags.group_by(&:digest) .new(container_repository.project, current_user, tags: tag_names)
.execute(container_repository)
end end
def without_latest(tags) def without_latest(tags)
tags.reject(&:latest?) tags.reject(&:latest?)
end end
def with_manifest(tags)
tags.select(&:valid?)
end
def order_by_date(tags) def order_by_date(tags)
now = DateTime.now now = DateTime.now
tags.sort_by { |tag| tag.created_at || now }.reverse tags.sort_by { |tag| tag.created_at || now }.reverse
...@@ -74,6 +51,9 @@ module Projects ...@@ -74,6 +51,9 @@ module Projects
end end
def filter_keep_n(tags) def filter_keep_n(tags)
return tags unless params['keep_n']
tags = order_by_date(tags)
tags.drop(params['keep_n'].to_i) tags.drop(params['keep_n'].to_i)
end end
......
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
.event-item-timestamp .event-item-timestamp
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
- if event.created_project_action? - if event.wiki_page?
= render "events/event/wiki", event: event
- elsif event.created_project_action?
= render "events/event/created_project", event: event = render "events/event/created_project", event: event
- elsif event.push_action? - elsif event.push_action?
= render "events/event/push", event: event = render "events/event/push", event: event
......
= icon_for_profile_event(event)
= event_user_info(event)
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
= event_wiki_title_html(event)
= render "events/event_scope", event: event
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"registry_host_url_with_port" => escape_once(registry_config.host_port), "registry_host_url_with_port" => escape_once(registry_config.host_port),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"is_admin": current_user&.admin,
is_group_page: true, is_group_page: true,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
- else - else
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
"repository_url" => escape_once(@project.container_registry_url), "repository_url" => escape_once(@project.container_registry_url),
"registry_host_url_with_port" => escape_once(registry_config.host_port), "registry_host_url_with_port" => escape_once(registry_config.host_port),
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'), "expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"is_admin": current_user&.admin,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
- else - else
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json), #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
......
...@@ -15,4 +15,6 @@ ...@@ -15,4 +15,6 @@
= render_if_exists 'events/epics_filter' = render_if_exists 'events/epics_filter'
- if comments_visible? - if comments_visible?
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments') = event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
- if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?)
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
= event_filter_link EventFilter::TEAM, _('Team'), s_('EventFilterBy|Filter by team') = event_filter_link EventFilter::TEAM, _('Team'), s_('EventFilterBy|Filter by team')
---
title: Update detected languages for sast in no dind mode
merge_request: 27831
author:
type: fixed
---
title: Use Ruby 2.7 in specs to remove Ruby 2.1/2.2/2.3
merge_request: 27269
author: Takuya Noguchi
type: other
---
title: Improve performance of the container repository cleanup tags service
merge_request: 27441
author:
type: performance
---
title: Add cost factor fields to ci runners
merge_request: 27666
author:
type: added
---
title: Add auto_ssl_failed to pages_domains
merge_request: 27671
author:
type: added
---
title: Support wiki events in activity streams
merge_request: 23869
author:
type: changed
---
title: Fix bug tracking snippet shard name
merge_request: 27979
author:
type: fixed
# frozen_string_literal: true
class CreateVulnerabilityUserMentions < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :vulnerability_user_mentions do |t|
t.references :vulnerability, type: :bigint, index: false, null: false, foreign_key: { on_delete: :cascade }
t.references :note, type: :integer,
index: { where: 'note_id IS NOT NULL', unique: true }, null: true, foreign_key: { on_delete: :cascade }
t.integer :mentioned_users_ids, array: true
t.integer :mentioned_projects_ids, array: true
t.integer :mentioned_groups_ids, array: true
end
add_index :vulnerability_user_mentions, [:vulnerability_id], where: 'note_id is null', unique: true, name: 'index_vulns_user_mentions_on_vulnerability_id'
add_index :vulnerability_user_mentions, [:vulnerability_id, :note_id], unique: true, name: 'index_vulns_user_mentions_on_vulnerability_id_and_note_id'
end
end
# frozen_string_literal: true
class AddCostFactorFiledsToCiRunners < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:ci_runners, :public_projects_minutes_cost_factor, :float, allow_null: false, default: 0.0)
add_column_with_default(:ci_runners, :private_projects_minutes_cost_factor, :float, allow_null: false, default: 1.0)
end
def down
remove_column(:ci_runners, :public_projects_minutes_cost_factor)
remove_column(:ci_runners, :private_projects_minutes_cost_factor)
end
end
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddLetsencryptErrorsToPagesDomains < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :pages_domains, :auto_ssl_failed, :boolean, default: false
end
def down
remove_column :pages_domains, :auto_ssl_failed
end
end
...@@ -1310,7 +1310,9 @@ CREATE TABLE public.ci_runners ( ...@@ -1310,7 +1310,9 @@ CREATE TABLE public.ci_runners (
ip_address character varying, ip_address character varying,
maximum_timeout integer, maximum_timeout integer,
runner_type smallint NOT NULL, runner_type smallint NOT NULL,
token_encrypted character varying token_encrypted character varying,
public_projects_minutes_cost_factor double precision DEFAULT 0.0 NOT NULL,
private_projects_minutes_cost_factor double precision DEFAULT 1.0 NOT NULL
); );
CREATE SEQUENCE public.ci_runners_id_seq CREATE SEQUENCE public.ci_runners_id_seq
...@@ -4449,7 +4451,8 @@ CREATE TABLE public.pages_domains ( ...@@ -4449,7 +4451,8 @@ CREATE TABLE public.pages_domains (
certificate_source smallint DEFAULT 0 NOT NULL, certificate_source smallint DEFAULT 0 NOT NULL,
wildcard boolean DEFAULT false NOT NULL, wildcard boolean DEFAULT false NOT NULL,
usage smallint DEFAULT 0 NOT NULL, usage smallint DEFAULT 0 NOT NULL,
scope smallint DEFAULT 2 NOT NULL scope smallint DEFAULT 2 NOT NULL,
auto_ssl_failed boolean DEFAULT false NOT NULL
); );
CREATE SEQUENCE public.pages_domains_id_seq CREATE SEQUENCE public.pages_domains_id_seq
...@@ -6611,6 +6614,24 @@ CREATE SEQUENCE public.vulnerability_scanners_id_seq ...@@ -6611,6 +6614,24 @@ CREATE SEQUENCE public.vulnerability_scanners_id_seq
ALTER SEQUENCE public.vulnerability_scanners_id_seq OWNED BY public.vulnerability_scanners.id; ALTER SEQUENCE public.vulnerability_scanners_id_seq OWNED BY public.vulnerability_scanners.id;
CREATE TABLE public.vulnerability_user_mentions (
id bigint NOT NULL,
vulnerability_id bigint NOT NULL,
note_id integer,
mentioned_users_ids integer[],
mentioned_projects_ids integer[],
mentioned_groups_ids integer[]
);
CREATE SEQUENCE public.vulnerability_user_mentions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.vulnerability_user_mentions_id_seq OWNED BY public.vulnerability_user_mentions.id;
CREATE TABLE public.web_hook_logs ( CREATE TABLE public.web_hook_logs (
id integer NOT NULL, id integer NOT NULL,
web_hook_id integer NOT NULL, web_hook_id integer NOT NULL,
...@@ -7358,6 +7379,8 @@ ALTER TABLE ONLY public.vulnerability_occurrences ALTER COLUMN id SET DEFAULT ne ...@@ -7358,6 +7379,8 @@ ALTER TABLE ONLY public.vulnerability_occurrences ALTER COLUMN id SET DEFAULT ne
ALTER TABLE ONLY public.vulnerability_scanners ALTER COLUMN id SET DEFAULT nextval('public.vulnerability_scanners_id_seq'::regclass); ALTER TABLE ONLY public.vulnerability_scanners ALTER COLUMN id SET DEFAULT nextval('public.vulnerability_scanners_id_seq'::regclass);
ALTER TABLE ONLY public.vulnerability_user_mentions ALTER COLUMN id SET DEFAULT nextval('public.vulnerability_user_mentions_id_seq'::regclass);
ALTER TABLE ONLY public.web_hook_logs ALTER COLUMN id SET DEFAULT nextval('public.web_hook_logs_id_seq'::regclass); ALTER TABLE ONLY public.web_hook_logs ALTER COLUMN id SET DEFAULT nextval('public.web_hook_logs_id_seq'::regclass);
ALTER TABLE ONLY public.web_hooks ALTER COLUMN id SET DEFAULT nextval('public.web_hooks_id_seq'::regclass); ALTER TABLE ONLY public.web_hooks ALTER COLUMN id SET DEFAULT nextval('public.web_hooks_id_seq'::regclass);
...@@ -8286,6 +8309,9 @@ ALTER TABLE ONLY public.vulnerability_occurrences ...@@ -8286,6 +8309,9 @@ ALTER TABLE ONLY public.vulnerability_occurrences
ALTER TABLE ONLY public.vulnerability_scanners ALTER TABLE ONLY public.vulnerability_scanners
ADD CONSTRAINT vulnerability_scanners_pkey PRIMARY KEY (id); ADD CONSTRAINT vulnerability_scanners_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.vulnerability_user_mentions
ADD CONSTRAINT vulnerability_user_mentions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.web_hook_logs ALTER TABLE ONLY public.web_hook_logs
ADD CONSTRAINT web_hook_logs_pkey PRIMARY KEY (id); ADD CONSTRAINT web_hook_logs_pkey PRIMARY KEY (id);
...@@ -10128,6 +10154,12 @@ CREATE INDEX index_vulnerability_occurrences_on_vulnerability_id ON public.vulne ...@@ -10128,6 +10154,12 @@ CREATE INDEX index_vulnerability_occurrences_on_vulnerability_id ON public.vulne
CREATE UNIQUE INDEX index_vulnerability_scanners_on_project_id_and_external_id ON public.vulnerability_scanners USING btree (project_id, external_id); CREATE UNIQUE INDEX index_vulnerability_scanners_on_project_id_and_external_id ON public.vulnerability_scanners USING btree (project_id, external_id);
CREATE UNIQUE INDEX index_vulnerability_user_mentions_on_note_id ON public.vulnerability_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE UNIQUE INDEX index_vulns_user_mentions_on_vulnerability_id ON public.vulnerability_user_mentions USING btree (vulnerability_id) WHERE (note_id IS NULL);
CREATE UNIQUE INDEX index_vulns_user_mentions_on_vulnerability_id_and_note_id ON public.vulnerability_user_mentions USING btree (vulnerability_id, note_id);
CREATE INDEX index_web_hook_logs_on_created_at_and_web_hook_id ON public.web_hook_logs USING btree (created_at, web_hook_id); CREATE INDEX index_web_hook_logs_on_created_at_and_web_hook_id ON public.web_hook_logs USING btree (created_at, web_hook_id);
CREATE INDEX index_web_hook_logs_on_web_hook_id ON public.web_hook_logs USING btree (web_hook_id); CREATE INDEX index_web_hook_logs_on_web_hook_id ON public.web_hook_logs USING btree (web_hook_id);
...@@ -10843,6 +10875,9 @@ ALTER TABLE ONLY public.open_project_tracker_data ...@@ -10843,6 +10875,9 @@ ALTER TABLE ONLY public.open_project_tracker_data
ALTER TABLE ONLY public.gpg_signatures ALTER TABLE ONLY public.gpg_signatures
ADD CONSTRAINT fk_rails_19d4f1c6f9 FOREIGN KEY (gpg_key_subkey_id) REFERENCES public.gpg_key_subkeys(id) ON DELETE SET NULL; ADD CONSTRAINT fk_rails_19d4f1c6f9 FOREIGN KEY (gpg_key_subkey_id) REFERENCES public.gpg_key_subkeys(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.vulnerability_user_mentions
ADD CONSTRAINT fk_rails_1a41c485cd FOREIGN KEY (vulnerability_id) REFERENCES public.vulnerabilities(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.board_assignees ALTER TABLE ONLY public.board_assignees
ADD CONSTRAINT fk_rails_1c0ff59e82 FOREIGN KEY (assignee_id) REFERENCES public.users(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_1c0ff59e82 FOREIGN KEY (assignee_id) REFERENCES public.users(id) ON DELETE CASCADE;
...@@ -11380,6 +11415,9 @@ ALTER TABLE ONLY public.namespace_root_storage_statistics ...@@ -11380,6 +11415,9 @@ ALTER TABLE ONLY public.namespace_root_storage_statistics
ALTER TABLE ONLY public.project_aliases ALTER TABLE ONLY public.project_aliases
ADD CONSTRAINT fk_rails_a1804f74a7 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_a1804f74a7 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.vulnerability_user_mentions
ADD CONSTRAINT fk_rails_a18600f210 FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.todos ALTER TABLE ONLY public.todos
ADD CONSTRAINT fk_rails_a27c483435 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_a27c483435 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
...@@ -12742,6 +12780,7 @@ INSERT INTO "schema_migrations" (version) VALUES ...@@ -12742,6 +12780,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200316162648'), ('20200316162648'),
('20200316173312'), ('20200316173312'),
('20200317142110'), ('20200317142110'),
('20200318140400'),
('20200318152134'), ('20200318152134'),
('20200318162148'), ('20200318162148'),
('20200318163148'), ('20200318163148'),
...@@ -12750,6 +12789,8 @@ INSERT INTO "schema_migrations" (version) VALUES ...@@ -12750,6 +12789,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200318175008'), ('20200318175008'),
('20200319123041'), ('20200319123041'),
('20200319203901'), ('20200319203901'),
('20200320112455'),
('20200320123839'),
('20200323075043'), ('20200323075043'),
('20200323122201'), ('20200323122201'),
('20200324115359'); ('20200324115359');
......
...@@ -22,7 +22,7 @@ For configuring GitLab to use Object Storage refer to the following guides: ...@@ -22,7 +22,7 @@ For configuring GitLab to use Object Storage refer to the following guides:
1. Configure [object storage for container registry](../packages/container_registry.md#container-registry-storage-driver) (optional feature). 1. Configure [object storage for container registry](../packages/container_registry.md#container-registry-storage-driver) (optional feature).
1. Configure [object storage for Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage) (optional feature). 1. Configure [object storage for Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage) (optional feature).
1. Configure [object storage for packages](../packages/index.md#using-object-storage) (optional feature). **(PREMIUM ONLY)** 1. Configure [object storage for packages](../packages/index.md#using-object-storage) (optional feature). **(PREMIUM ONLY)**
1. Configure [object storage for dependency proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature). **(ULTIMATE ONLY)** 1. Configure [object storage for dependency proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature). **(PREMIUM ONLY)**
1. Configure [object storage for Pseudonymizer](../pseudonymizer.md#configuration) (optional feature). **(ULTIMATE ONLY)** 1. Configure [object storage for Pseudonymizer](../pseudonymizer.md#configuration) (optional feature). **(ULTIMATE ONLY)**
NOTE: **Note:** NOTE: **Note:**
......
# GitLab Dependency Proxy administration **(ULTIMATE ONLY)** # GitLab Dependency Proxy administration **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
......
...@@ -70,7 +70,7 @@ mysec_dependency_scanning: ...@@ -70,7 +70,7 @@ mysec_dependency_scanning:
`gl-sast-report.json` is an example file path. See [the Output file section](#output-file) for more details. `gl-sast-report.json` is an example file path. See [the Output file section](#output-file) for more details.
It is processed as a SAST report because it is declared as such in the job definition. It is processed as a SAST report because it is declared as such in the job definition.
### Rules ### Policies
Scanning jobs should be skipped unless the corresponding feature is listed Scanning jobs should be skipped unless the corresponding feature is listed
in the `GITLAB_FEATURES` variable (comma-separated list of values). in the `GITLAB_FEATURES` variable (comma-separated list of values).
...@@ -103,11 +103,9 @@ mysec_dependency_scanning: ...@@ -103,11 +103,9 @@ mysec_dependency_scanning:
$CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bjava\b/ $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bjava\b/
``` ```
The [`only/except`](../../ci/yaml/README.md#onlyexcept-basic) keywords Any additional job policy should only be configured by users based on their needs.
as well as the new [`rules`](../../ci/yaml/README.md#rules) keyword For instance, predefined policies should not trigger the scanning job
make possible to trigger the job depending on the branch, or when some particular file changes. for a particular branch or when a particular set of files changes.
Such rules should be defined by users based on their needs,
and should not be predefined in the job definition of the scanner.
## Docker image ## Docker image
......
...@@ -170,6 +170,9 @@ back up the volume where the configuration files are stored. If you have created ...@@ -170,6 +170,9 @@ back up the volume where the configuration files are stored. If you have created
the GitLab container according to the documentation, it should be under the GitLab container according to the documentation, it should be under
`/srv/gitlab/config`. `/srv/gitlab/config`.
For [GitLab Helm chart Installations](https://gitlab.com/gitlab-org/charts/gitlab) on a
Kubernetes cluster, you must follow the [Backup the secrets](https://docs.gitlab.com/charts/backup-restore/backup.html#backup-the-secrets) instructions.
You may also want to back up any TLS keys and certificates, and your You may also want to back up any TLS keys and certificates, and your
[SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
......
...@@ -13,3 +13,61 @@ If you plan to deploy a GitLab instance on a physically-isolated and offline net ...@@ -13,3 +13,61 @@ If you plan to deploy a GitLab instance on a physically-isolated and offline net
Follow these best practices to use GitLab's features in an offline environment: Follow these best practices to use GitLab's features in an offline environment:
- [Operating the GitLab Secure scanners in an offline environment](../../user/application_security/offline_deployments/index.md). - [Operating the GitLab Secure scanners in an offline environment](../../user/application_security/offline_deployments/index.md).
## Loading Docker images onto your air-gapped host
To use many GitLab features, including
[security scans](../../user/application_security/index.md#working-in-an-offline-environment)
and [Auto Devops](../autodevops/), the GitLab Runner must be able to fetch the
relevant Docker images.
The process for making these images available without direct access to the public internet
involves downloading the images then packaging and transferring them to the air-gapped host.
Here's an example of such a transfer:
1. Download Docker images from public internet.
1. Package Docker images as tar archives.
1. Transfer images to air-gapped environment.
1. Load transferred images into air-gapped Docker registry.
### Example image packager script
```sh
#!/bin/bash
set -ux
# Specify needed analyzer images
analyzers=${SAST_ANALYZERS:-"bandit eslint gosec"}
gitlab=registry.gitlab.com/gitlab-org/security-products/analyzers/
for i in "${analyzers[@]}"
do
tarname="${i}_2.tar"
docker pull $gitlab$i:2
docker save $gitlab$i:2 -o ./analyzers/${tarname}
chmod +r ./analyzers/${tarname}
done
```
### Example image loader script
This example loads the images from a bastion host to an air-gapped host. In certain configurations,
physical media may be needed for such a transfer:
```sh
#!/bin/bash
set -ux
# Specify needed analyzer images
analyzers=${SAST_ANALYZERS:-"bandit eslint gosec"}
registry=$GITLAB_HOST:4567
for i in "${analyzers[@]}"
do
tarname="${i}_2.tar"
scp ./analyzers/${tarname} ${GITLAB_HOST}:~/${tarname}
ssh $GITLAB_HOST "sudo docker load -i ${tarname}"
ssh $GITLAB_HOST "sudo docker tag $(sudo docker images | grep $i | awk '{print $3}') ${registry}/analyzers/${i}:2"
ssh $GITLAB_HOST "sudo docker push ${registry}/analyzers/${i}:2"
done
```
# Dependency Proxy **(ULTIMATE ONLY)** # Dependency Proxy **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7934) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7934) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11.
......
...@@ -178,6 +178,7 @@ CAUTION: **Warning:** ...@@ -178,6 +178,7 @@ CAUTION: **Warning:**
By default, `nuget` checks the official source at `nuget.org` first. If you have a package in the By default, `nuget` checks the official source at `nuget.org` first. If you have a package in the
GitLab NuGet Repository with the same name as a package at `nuget.org`, you must specify the source GitLab NuGet Repository with the same name as a package at `nuget.org`, you must specify the source
name or the wrong package will be installed. name or the wrong package will be installed.
Install the latest version of a package using the following command: Install the latest version of a package using the following command:
```shell ```shell
......
...@@ -184,7 +184,7 @@ spotbugs-sast: ...@@ -184,7 +184,7 @@ spotbugs-sast:
variables: variables:
- $GITLAB_FEATURES =~ /\bsast\b/ && - $GITLAB_FEATURES =~ /\bsast\b/ &&
$SAST_DEFAULT_ANALYZERS =~ /spotbugs/ && $SAST_DEFAULT_ANALYZERS =~ /spotbugs/ &&
$CI_PROJECT_REPOSITORY_LANGUAGES =~ /java\b/ $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\b(groovy|java|scala)\b/
tslint-sast: tslint-sast:
extends: .sast-analyzer extends: .sast-analyzer
......
...@@ -30,7 +30,7 @@ module Gitlab ...@@ -30,7 +30,7 @@ module Gitlab
def create_repository_from_bundle def create_repository_from_bundle
repository.create_from_bundle(path_to_bundle) repository.create_from_bundle(path_to_bundle)
snippet.track_snippet_repository snippet.track_snippet_repository(repository.storage)
end end
def create_repository_from_db def create_repository_from_db
......
...@@ -5291,6 +5291,9 @@ msgstr "" ...@@ -5291,6 +5291,9 @@ msgstr ""
msgid "ContainerRegistry|Build an image" msgid "ContainerRegistry|Build an image"
msgstr "" msgstr ""
msgid "ContainerRegistry|Compressed Size"
msgstr ""
msgid "ContainerRegistry|Container Registry" msgid "ContainerRegistry|Container Registry"
msgstr "" msgstr ""
...@@ -5431,6 +5434,9 @@ msgstr "" ...@@ -5431,6 +5434,9 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project" msgid "ContainerRegistry|There are no container images stored for this project"
msgstr "" msgstr ""
msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
msgstr ""
msgid "ContainerRegistry|This image has no active tags" msgid "ContainerRegistry|This image has no active tags"
msgstr "" msgstr ""
...@@ -8178,6 +8184,9 @@ msgstr "" ...@@ -8178,6 +8184,9 @@ msgstr ""
msgid "EventFilterBy|Filter by team" msgid "EventFilterBy|Filter by team"
msgstr "" msgstr ""
msgid "EventFilterBy|Filter by wiki"
msgstr ""
msgid "Events" msgid "Events"
msgstr "" msgstr ""
...@@ -24705,6 +24714,9 @@ msgstr "" ...@@ -24705,6 +24714,9 @@ msgstr ""
msgid "vulnerability|dismissed" msgid "vulnerability|dismissed"
msgstr "" msgstr ""
msgid "wiki page"
msgstr ""
msgid "with %{additions} additions, %{deletions} deletions." msgid "with %{additions} additions, %{deletions} deletions."
msgstr "" msgstr ""
......
...@@ -31,6 +31,8 @@ describe DashboardController do ...@@ -31,6 +31,8 @@ describe DashboardController do
before do before do
create(:event, :created, project: project, target: create(:issue)) create(:event, :created, project: project, target: create(:issue))
create(:wiki_page_event, :created, project: project)
create(:wiki_page_event, :updated, project: project)
sign_in(user) sign_in(user)
...@@ -45,7 +47,7 @@ describe DashboardController do ...@@ -45,7 +47,7 @@ describe DashboardController do
it 'returns count' do it 'returns count' do
get :activity, params: { format: :json } get :activity, params: { format: :json }
expect(json_response['count']).to eq(1) expect(json_response['count']).to eq(3)
end end
end end
......
...@@ -16,7 +16,7 @@ FactoryBot.define do ...@@ -16,7 +16,7 @@ FactoryBot.define do
options do options do
{ {
image: 'ruby:2.1', image: 'ruby:2.7',
services: ['postgres'], services: ['postgres'],
script: ['ls -a'] script: ['ls -a']
} }
...@@ -336,7 +336,7 @@ FactoryBot.define do ...@@ -336,7 +336,7 @@ FactoryBot.define do
trait :extended_options do trait :extended_options do
options do options do
{ {
image: { name: 'ruby:2.1', entrypoint: '/bin/sh' }, image: { name: 'ruby:2.7', entrypoint: '/bin/sh' },
services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }], services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }],
script: %w(echo), script: %w(echo),
after_script: %w(ls date), after_script: %w(ls date),
......
...@@ -28,7 +28,7 @@ FactoryBot.define do ...@@ -28,7 +28,7 @@ FactoryBot.define do
bare_repo: TestEnv.factory_repo_path_bare, bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA) refs: TestEnv::BRANCH_SHA)
snippet.track_snippet_repository snippet.track_snippet_repository(snippet.repository.storage)
end end
end end
......
...@@ -5,8 +5,15 @@ import stubChildren from 'helpers/stub_children'; ...@@ -5,8 +5,15 @@ import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue'; import component from '~/registry/explorer/pages/details.vue';
import store from '~/registry/explorer/stores/'; import store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import {
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
} from '~/registry/explorer/constants';
import { tagsListResponse } from '../mock_data'; import { tagsListResponse } from '../mock_data';
import { GlModal } from '../stubs'; import { GlModal } from '../stubs';
import { $toast } from '../../shared/mocks';
describe('Details Page', () => { describe('Details Page', () => {
let wrapper; let wrapper;
...@@ -40,6 +47,7 @@ describe('Details Page', () => { ...@@ -40,6 +47,7 @@ describe('Details Page', () => {
id: routeId, id: routeId,
}, },
}, },
$toast,
}, },
}); });
dispatchSpy = jest.spyOn(store, 'dispatch'); dispatchSpy = jest.spyOn(store, 'dispatch');
...@@ -249,13 +257,11 @@ describe('Details Page', () => { ...@@ -249,13 +257,11 @@ describe('Details Page', () => {
}); });
}); });
it('when only one element is selected', () => { describe('when only one element is selected', () => {
const deleteModal = findDeleteModal(); it('execute the delete and remove selection', () => {
wrapper.setData({ itemsToBeDeleted: [0] });
wrapper.setData({ itemsToBeDeleted: [0] }); findDeleteModal().vm.$emit('ok');
deleteModal.vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', { expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
tag: store.state.tags[0], tag: store.state.tags[0],
params: wrapper.vm.$route.params.id, params: wrapper.vm.$route.params.id,
...@@ -264,15 +270,33 @@ describe('Details Page', () => { ...@@ -264,15 +270,33 @@ describe('Details Page', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0); expect(findCheckedCheckboxes()).toHaveLength(0);
}); });
it('show success toast on successful delete', () => {
return wrapper.vm.handleSingleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAG_SUCCESS_MESSAGE, {
type: 'success',
});
});
});
it('show error toast on erred delete', () => {
dispatchSpy.mockRejectedValue();
return wrapper.vm.handleSingleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAG_ERROR_MESSAGE, {
type: 'error',
});
});
});
}); });
it('when multiple elements are selected', () => { describe('when multiple elements are selected', () => {
const deleteModal = findDeleteModal(); beforeEach(() => {
wrapper.setData({ itemsToBeDeleted: [0, 1] });
});
wrapper.setData({ itemsToBeDeleted: [0, 1] }); it('execute the delete and remove selection', () => {
deleteModal.vm.$emit('ok'); findDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', { expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
ids: store.state.tags.map(t => t.name), ids: store.state.tags.map(t => t.name),
params: wrapper.vm.$route.params.id, params: wrapper.vm.$route.params.id,
...@@ -281,6 +305,23 @@ describe('Details Page', () => { ...@@ -281,6 +305,23 @@ describe('Details Page', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0); expect(findCheckedCheckboxes()).toHaveLength(0);
}); });
it('show success toast on successful delete', () => {
return wrapper.vm.handleMultipleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAGS_SUCCESS_MESSAGE, {
type: 'success',
});
});
});
it('show error toast on erred delete', () => {
dispatchSpy.mockRejectedValue();
return wrapper.vm.handleMultipleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAGS_ERROR_MESSAGE, {
type: 'error',
});
});
});
}); });
}); });
......
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import component from '~/registry/explorer/pages/index.vue';
import store from '~/registry/explorer/stores/';
describe('List Page', () => {
let wrapper;
let dispatchSpy;
const findRouterView = () => wrapper.find({ ref: 'router-view' });
const findAlert = () => wrapper.find(GlAlert);
const findLink = () => wrapper.find(GlLink);
const mountComponent = () => {
wrapper = shallowMount(component, {
store,
stubs: {
RouterView: true,
GlSprintf,
},
});
};
beforeEach(() => {
dispatchSpy = jest.spyOn(store, 'dispatch');
mountComponent();
});
it('has a router view', () => {
expect(findRouterView().exists()).toBe(true);
});
describe('garbageCollectionTip alert', () => {
beforeEach(() => {
store.dispatch('setInitialState', { isAdmin: true, garbageCollectionHelpPagePath: 'foo' });
store.dispatch('setShowGarbageCollectionTip', true);
});
afterEach(() => {
store.dispatch('setInitialState', {});
store.dispatch('setShowGarbageCollectionTip', false);
});
it('is visible when the user is an admin and the user performed a delete action', () => {
expect(findAlert().exists()).toBe(true);
});
it('on dismiss disappears ', () => {
findAlert().vm.$emit('dismiss');
expect(dispatchSpy).toHaveBeenCalledWith('setShowGarbageCollectionTip', false);
return wrapper.vm.$nextTick().then(() => {
expect(findAlert().exists()).toBe(false);
});
});
it('contains a link to the docs', () => {
const link = findLink();
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(store.state.config.garbageCollectionHelpPagePath);
});
});
});
...@@ -8,8 +8,13 @@ import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vu ...@@ -8,8 +8,13 @@ import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vu
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import store from '~/registry/explorer/stores/'; import store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
} from '~/registry/explorer/constants';
import { imagesListResponse } from '../mock_data'; import { imagesListResponse } from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs'; import { GlModal, GlEmptyState } from '../stubs';
import { $toast } from '../../shared/mocks';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueRouter); localVue.use(VueRouter);
...@@ -40,6 +45,9 @@ describe('List Page', () => { ...@@ -40,6 +45,9 @@ describe('List Page', () => {
GlEmptyState, GlEmptyState,
GlSprintf, GlSprintf,
}, },
mocks: {
$toast,
},
}); });
dispatchSpy = jest.spyOn(store, 'dispatch'); dispatchSpy = jest.spyOn(store, 'dispatch');
store.dispatch('receiveImagesListSuccess', imagesListResponse); store.dispatch('receiveImagesListSuccess', imagesListResponse);
...@@ -174,11 +182,29 @@ describe('List Page', () => { ...@@ -174,11 +182,29 @@ describe('List Page', () => {
const itemToDelete = wrapper.vm.images[0]; const itemToDelete = wrapper.vm.images[0];
wrapper.setData({ itemToDelete }); wrapper.setData({ itemToDelete });
findDeleteModal().vm.$emit('ok'); findDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => { expect(store.dispatch).toHaveBeenCalledWith(
expect(store.dispatch).toHaveBeenCalledWith( 'requestDeleteImage',
'requestDeleteImage', itemToDelete.destroy_path,
itemToDelete.destroy_path, );
); });
it('should show a success toast when delete request is successful', () => {
dispatchSpy.mockResolvedValue();
return wrapper.vm.handleDeleteImage().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_IMAGE_SUCCESS_MESSAGE, {
type: 'success',
});
expect(wrapper.vm.itemToDelete).toEqual({});
});
});
it('should show a error toast when delete request fails', () => {
dispatchSpy.mockRejectedValue();
return wrapper.vm.handleDeleteImage().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_IMAGE_ERROR_MESSAGE, {
type: 'error',
});
expect(wrapper.vm.itemToDelete).toEqual({});
}); });
}); });
}); });
...@@ -227,7 +253,7 @@ describe('List Page', () => { ...@@ -227,7 +253,7 @@ describe('List Page', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Tracking, 'event'); jest.spyOn(Tracking, 'event');
dispatchSpy.mockReturnValue(); dispatchSpy.mockResolvedValue();
}); });
it('send an event when delete button is clicked', () => { it('send an event when delete button is clicked', () => {
...@@ -235,13 +261,14 @@ describe('List Page', () => { ...@@ -235,13 +261,14 @@ describe('List Page', () => {
deleteBtn.vm.$emit('click'); deleteBtn.vm.$emit('click');
testTrackingCall('click_button'); testTrackingCall('click_button');
}); });
it('send an event when cancel is pressed on modal', () => { it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal(); const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel'); deleteModal.vm.$emit('cancel');
testTrackingCall('cancel_delete'); testTrackingCall('cancel_delete');
}); });
it('send an event when confirm is clicked on modal', () => { it('send an event when confirm is clicked on modal', () => {
dispatchSpy.mockReturnValue();
const deleteModal = findDeleteModal(); const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok'); deleteModal.vm.$emit('ok');
testTrackingCall('confirm_delete'); testTrackingCall('confirm_delete');
......
...@@ -38,6 +38,17 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -38,6 +38,17 @@ describe('Actions RegistryExplorer Store', () => {
); );
}); });
it('setShowGarbageCollectionTip', done => {
testAction(
actions.setShowGarbageCollectionTip,
true,
null,
[{ type: types.SET_SHOW_GARBAGE_COLLECTION_TIP, payload: true }],
[],
done,
);
});
describe('receives api responses', () => { describe('receives api responses', () => {
const response = { const response = {
data: [1, 2, 3], data: [1, 2, 3],
...@@ -182,19 +193,20 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -182,19 +193,20 @@ describe('Actions RegistryExplorer Store', () => {
}, },
[{ type: types.SET_MAIN_LOADING, payload: true }], [{ type: types.SET_MAIN_LOADING, payload: true }],
[ [
{
type: 'setShowGarbageCollectionTip',
payload: true,
},
{ {
type: 'requestTagsList', type: 'requestTagsList',
payload: { pagination: {}, params }, payload: { pagination: {}, params },
}, },
], ],
() => { done,
expect(createFlash).toHaveBeenCalled();
done();
},
); );
}); });
it('should show flash message on error', done => { it('should turn off loading on error', done => {
testAction( testAction(
actions.requestDeleteTag, actions.requestDeleteTag,
{ {
...@@ -208,10 +220,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -208,10 +220,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false }, { type: types.SET_MAIN_LOADING, payload: false },
], ],
[], [],
() => { done,
expect(createFlash).toHaveBeenCalled();
done();
},
); );
}); });
}); });
...@@ -234,19 +243,20 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -234,19 +243,20 @@ describe('Actions RegistryExplorer Store', () => {
}, },
[{ type: types.SET_MAIN_LOADING, payload: true }], [{ type: types.SET_MAIN_LOADING, payload: true }],
[ [
{
type: 'setShowGarbageCollectionTip',
payload: true,
},
{ {
type: 'requestTagsList', type: 'requestTagsList',
payload: { pagination: {}, params }, payload: { pagination: {}, params },
}, },
], ],
() => { done,
expect(createFlash).toHaveBeenCalled();
done();
},
); );
}); });
it('should show flash message on error', done => { it('should turn off loading on error', done => {
mock.onDelete(url).replyOnce(500); mock.onDelete(url).replyOnce(500);
testAction( testAction(
...@@ -263,17 +273,14 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -263,17 +273,14 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false }, { type: types.SET_MAIN_LOADING, payload: false },
], ],
[], [],
() => { done,
expect(createFlash).toHaveBeenCalled();
done();
},
); );
}); });
}); });
describe('request delete single image', () => { describe('request delete single image', () => {
const deletePath = 'delete/path';
it('successfully performs the delete request', done => { it('successfully performs the delete request', done => {
const deletePath = 'delete/path';
mock.onDelete(deletePath).replyOnce(200); mock.onDelete(deletePath).replyOnce(200);
testAction( testAction(
...@@ -287,33 +294,33 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -287,33 +294,33 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false }, { type: types.SET_MAIN_LOADING, payload: false },
], ],
[ [
{
type: 'setShowGarbageCollectionTip',
payload: true,
},
{ {
type: 'requestImagesList', type: 'requestImagesList',
payload: { pagination: {} }, payload: { pagination: {} },
}, },
], ],
() => { done,
expect(createFlash).toHaveBeenCalled();
done();
},
); );
}); });
it('should show flash message on error', done => { it('should turn off loading on error', done => {
mock.onDelete(deletePath).replyOnce(400);
testAction( testAction(
actions.requestDeleteImage, actions.requestDeleteImage,
null, deletePath,
{}, {},
[ [
{ type: types.SET_MAIN_LOADING, payload: true }, { type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false }, { type: types.SET_MAIN_LOADING, payload: false },
], ],
[], [],
() => { ).catch(() => {
expect(createFlash).toHaveBeenCalled(); done();
done(); });
},
);
}); });
}); });
}); });
...@@ -49,4 +49,22 @@ describe('Getters RegistryExplorer store', () => { ...@@ -49,4 +49,22 @@ describe('Getters RegistryExplorer store', () => {
expect(getters[getter](state)).toBe(expectedPieces.join(' ')); expect(getters[getter](state)).toBe(expectedPieces.join(' '));
}); });
}); });
describe('showGarbageCollection', () => {
it.each`
result | showGarbageCollectionTip | isAdmin
${true} | ${true} | ${true}
${false} | ${true} | ${false}
${false} | ${false} | ${true}
`(
'return $result when showGarbageCollectionTip $showGarbageCollectionTip and isAdmin is $isAdmin',
({ result, showGarbageCollectionTip, isAdmin }) => {
state = {
config: { isAdmin },
showGarbageCollectionTip,
};
expect(getters.showGarbageCollection(state)).toBe(result);
},
);
});
}); });
...@@ -10,7 +10,12 @@ describe('Mutations Registry Explorer Store', () => { ...@@ -10,7 +10,12 @@ describe('Mutations Registry Explorer Store', () => {
describe('SET_INITIAL_STATE', () => { describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => { it('should set the initial state', () => {
const payload = { endpoint: 'foo', isGroupPage: true, expirationPolicy: { foo: 'bar' } }; const payload = {
endpoint: 'foo',
isGroupPage: true,
expirationPolicy: { foo: 'bar' },
isAdmin: true,
};
const expectedState = { ...mockState, config: payload }; const expectedState = { ...mockState, config: payload };
mutations[types.SET_INITIAL_STATE](mockState, { mutations[types.SET_INITIAL_STATE](mockState, {
...payload, ...payload,
...@@ -50,6 +55,15 @@ describe('Mutations Registry Explorer Store', () => { ...@@ -50,6 +55,15 @@ describe('Mutations Registry Explorer Store', () => {
}); });
}); });
describe('SET_SHOW_GARBAGE_COLLECTION_TIP', () => {
it('should set the showGarbageCollectionTip', () => {
const expectedState = { ...mockState, showGarbageCollectionTip: true };
mutations[types.SET_SHOW_GARBAGE_COLLECTION_TIP](mockState, true);
expect(mockState).toEqual(expectedState);
});
});
describe('SET_PAGINATION', () => { describe('SET_PAGINATION', () => {
const generatePagination = () => [ const generatePagination = () => [
{ {
......
// eslint-disable-next-line import/prefer-default-export
export const $toast = {
show: jest.fn(),
};
...@@ -88,6 +88,85 @@ describe EventsHelper do ...@@ -88,6 +88,85 @@ describe EventsHelper do
end end
end end
describe '#event_preposition' do
context 'for wiki page events' do
let(:event) { create(:wiki_page_event) }
it 'returns a suitable phrase' do
expect(helper.event_preposition(event)).to eq('in the wiki for')
end
end
context 'for push action events' do
let(:event) { create(:push_event) }
it 'returns a suitable phrase' do
expect(helper.event_preposition(event)).to eq('at')
end
end
context 'for commented actions' do
let(:event) { create(:event, :commented) }
it 'returns a suitable phrase' do
expect(helper.event_preposition(event)).to eq('at')
end
end
context 'for any event with a target' do
let(:event) { create(:event, target: create(:issue)) }
it 'returns a suitable phrase' do
expect(helper.event_preposition(event)).to eq('at')
end
end
context 'for milestone events' do
let(:event) { create(:event, target: create(:milestone)) }
it 'returns a suitable phrase' do
expect(helper.event_preposition(event)).to eq('in')
end
end
context 'for non-matching events' do
let(:event) { create(:event, :created) }
it 'returns no preposition' do
expect(helper.event_preposition(event)).to be_nil
end
end
end
describe 'event_wiki_page_target_url' do
let(:project) { create(:project) }
let(:wiki_page) { create(:wiki_page, wiki: create(:project_wiki, project: project)) }
let(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) }
it 'links to the wiki page' do
url = helper.project_wiki_url(project, wiki_page.slug)
expect(helper.event_wiki_page_target_url(event)).to eq(url)
end
end
describe '#event_wiki_title_html' do
let(:event) { create(:wiki_page_event) }
it 'produces a suitable title chunk' do
url = helper.event_wiki_page_target_url(event)
title = event.target_title
html = [
"<span class=\"event-target-type append-right-4\">wiki page</span>",
"<a title=\"#{title}\" class=\"has-tooltip event-target-link append-right-4\" href=\"#{url}\">",
title,
"</a>"
].join
expect(helper.event_wiki_title_html(event)).to eq(html)
end
end
describe '#event_note_target_url' do describe '#event_note_target_url' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:event) { create(:event, project: project) } let(:event) { create(:event, project: project) }
......
...@@ -9,7 +9,7 @@ describe Gitlab::Ci::Build::Image do ...@@ -9,7 +9,7 @@ describe Gitlab::Ci::Build::Image do
subject { described_class.from_image(job) } subject { described_class.from_image(job) }
context 'when image is defined in job' do context 'when image is defined in job' do
let(:image_name) { 'ruby:2.1' } let(:image_name) { 'ruby:2.7' }
let(:job) { create(:ci_build, options: { image: image_name } ) } let(:job) { create(:ci_build, options: { image: image_name } ) }
context 'when image is defined as string' do context 'when image is defined as string' do
......
...@@ -6,11 +6,11 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -6,11 +6,11 @@ describe Gitlab::Ci::Config::Entry::Image do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
context 'when configuration is a string' do context 'when configuration is a string' do
let(:config) { 'ruby:2.2' } let(:config) { 'ruby:2.7' }
describe '#value' do describe '#value' do
it 'returns image hash' do it 'returns image hash' do
expect(entry.value).to eq({ name: 'ruby:2.2' }) expect(entry.value).to eq({ name: 'ruby:2.7' })
end end
end end
...@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#image' do describe '#image' do
it "returns image's name" do it "returns image's name" do
expect(entry.name).to eq 'ruby:2.2' expect(entry.name).to eq 'ruby:2.7'
end end
end end
...@@ -46,7 +46,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -46,7 +46,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end end
context 'when configuration is a hash' do context 'when configuration is a hash' do
let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run) } } let(:config) { { name: 'ruby:2.7', entrypoint: %w(/bin/sh run) } }
describe '#value' do describe '#value' do
it 'returns image hash' do it 'returns image hash' do
...@@ -68,7 +68,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -68,7 +68,7 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#image' do describe '#image' do
it "returns image's name" do it "returns image's name" do
expect(entry.name).to eq 'ruby:2.2' expect(entry.name).to eq 'ruby:2.7'
end end
end end
...@@ -80,7 +80,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -80,7 +80,7 @@ describe Gitlab::Ci::Config::Entry::Image do
context 'when configuration has ports' do context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run), ports: ports } } let(:config) { { name: 'ruby:2.7', entrypoint: %w(/bin/sh run), ports: ports } }
let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
let(:image_ports) { false } let(:image_ports) { false }
...@@ -112,7 +112,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -112,7 +112,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end end
context 'when entry value is not correct' do context 'when entry value is not correct' do
let(:config) { ['ruby:2.2'] } let(:config) { ['ruby:2.7'] }
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
...@@ -129,7 +129,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -129,7 +129,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end end
context 'when unexpected key is specified' do context 'when unexpected key is specified' do
let(:config) { { name: 'ruby:2.2', non_existing: 'test' } } let(:config) { { name: 'ruby:2.7', non_existing: 'test' } }
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
......
...@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Root do
let(:hash) do let(:hash) do
{ {
before_script: %w(ls pwd), before_script: %w(ls pwd),
image: 'ruby:2.2', image: 'ruby:2.7',
default: {}, default: {},
services: ['postgres:9.1', 'mysql:5.5'], services: ['postgres:9.1', 'mysql:5.5'],
variables: { VAR: 'root' }, variables: { VAR: 'root' },
...@@ -124,7 +124,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -124,7 +124,7 @@ describe Gitlab::Ci::Config::Entry::Root do
{ name: :rspec, { name: :rspec,
script: %w[rspec ls], script: %w[rspec ls],
before_script: %w(ls pwd), before_script: %w(ls pwd),
image: { name: 'ruby:2.2' }, image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test', stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' }, cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
...@@ -138,7 +138,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -138,7 +138,7 @@ describe Gitlab::Ci::Config::Entry::Root do
{ name: :spinach, { name: :spinach,
before_script: [], before_script: [],
script: %w[spinach], script: %w[spinach],
image: { name: 'ruby:2.2' }, image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test', stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' }, cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
...@@ -154,7 +154,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -154,7 +154,7 @@ describe Gitlab::Ci::Config::Entry::Root do
before_script: [], before_script: [],
script: ["make changelog | tee release_changelog.txt"], script: ["make changelog | tee release_changelog.txt"],
release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" },
image: { name: "ruby:2.2" }, image: { name: "ruby:2.7" },
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push" }, cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push" },
only: { refs: %w(branches tags) }, only: { refs: %w(branches tags) },
...@@ -173,7 +173,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -173,7 +173,7 @@ describe Gitlab::Ci::Config::Entry::Root do
{ before_script: %w(ls pwd), { before_script: %w(ls pwd),
after_script: ['make clean'], after_script: ['make clean'],
default: { default: {
image: 'ruby:2.1', image: 'ruby:2.7',
services: ['postgres:9.1', 'mysql:5.5'] services: ['postgres:9.1', 'mysql:5.5']
}, },
variables: { VAR: 'root' }, variables: { VAR: 'root' },
...@@ -200,7 +200,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -200,7 +200,7 @@ describe Gitlab::Ci::Config::Entry::Root do
rspec: { name: :rspec, rspec: { name: :rspec,
script: %w[rspec ls], script: %w[rspec ls],
before_script: %w(ls pwd), before_script: %w(ls pwd),
image: { name: 'ruby:2.1' }, image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test', stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" }, cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
...@@ -212,7 +212,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -212,7 +212,7 @@ describe Gitlab::Ci::Config::Entry::Root do
spinach: { name: :spinach, spinach: { name: :spinach,
before_script: [], before_script: [],
script: %w[spinach], script: %w[spinach],
image: { name: 'ruby:2.1' }, image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test', stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" }, cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
......
...@@ -71,7 +71,7 @@ describe Gitlab::Ci::Config::External::File::Project do ...@@ -71,7 +71,7 @@ describe Gitlab::Ci::Config::External::File::Project do
let(:root_ref_sha) { project.repository.root_ref_sha } let(:root_ref_sha) { project.repository.root_ref_sha }
before do before do
stub_project_blob(root_ref_sha, '/file.yml') { 'image: ruby:2.1' } stub_project_blob(root_ref_sha, '/file.yml') { 'image: ruby:2.7' }
end end
it 'returns true' do it 'returns true' do
...@@ -96,7 +96,7 @@ describe Gitlab::Ci::Config::External::File::Project do ...@@ -96,7 +96,7 @@ describe Gitlab::Ci::Config::External::File::Project do
let(:ref_sha) { project.commit('master').sha } let(:ref_sha) { project.commit('master').sha }
before do before do
stub_project_blob(ref_sha, '/file.yml') { 'image: ruby:2.1' } stub_project_blob(ref_sha, '/file.yml') { 'image: ruby:2.7' }
end end
it 'returns true' do it 'returns true' do
......
...@@ -15,7 +15,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -15,7 +15,7 @@ describe Gitlab::Ci::Config::External::Mapper do
let(:file_content) do let(:file_content) do
<<~HEREDOC <<~HEREDOC
image: 'ruby:2.2' image: 'ruby:2.7'
HEREDOC HEREDOC
end end
...@@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the string is a local file' do context 'when the string is a local file' do
let(:values) do let(:values) do
{ include: local_file, { include: local_file,
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns File instances' do it 'returns File instances' do
...@@ -46,7 +46,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -46,7 +46,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a local file hash' do context 'when the key is a local file hash' do
let(:values) do let(:values) do
{ include: { 'local' => local_file }, { include: { 'local' => local_file },
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns File instances' do it 'returns File instances' do
...@@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the string is a remote file' do context 'when the string is a remote file' do
let(:values) do let(:values) do
{ include: remote_url, image: 'ruby:2.2' } { include: remote_url, image: 'ruby:2.7' }
end end
it 'returns File instances' do it 'returns File instances' do
...@@ -69,7 +69,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -69,7 +69,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a remote file hash' do context 'when the key is a remote file hash' do
let(:values) do let(:values) do
{ include: { 'remote' => remote_url }, { include: { 'remote' => remote_url },
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns File instances' do it 'returns File instances' do
...@@ -81,7 +81,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -81,7 +81,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a template file hash' do context 'when the key is a template file hash' do
let(:values) do let(:values) do
{ include: { 'template' => template_file }, { include: { 'template' => template_file },
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns File instances' do it 'returns File instances' do
...@@ -93,7 +93,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -93,7 +93,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a hash of file and remote' do context 'when the key is a hash of file and remote' do
let(:values) do let(:values) do
{ include: { 'local' => local_file, 'remote' => remote_url }, { include: { 'local' => local_file, 'remote' => remote_url },
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns ambigious specification error' do it 'returns ambigious specification error' do
...@@ -105,7 +105,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -105,7 +105,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context "when 'include' is defined as an array" do context "when 'include' is defined as an array" do
let(:values) do let(:values) do
{ include: [remote_url, local_file], { include: [remote_url, local_file],
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns Files instances' do it 'returns Files instances' do
...@@ -117,7 +117,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -117,7 +117,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context "when 'include' is defined as an array of hashes" do context "when 'include' is defined as an array of hashes" do
let(:values) do let(:values) do
{ include: [{ remote: remote_url }, { local: local_file }], { include: [{ remote: remote_url }, { local: local_file }],
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns Files instances' do it 'returns Files instances' do
...@@ -128,7 +128,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -128,7 +128,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when it has ambigious match' do context 'when it has ambigious match' do
let(:values) do let(:values) do
{ include: [{ remote: remote_url, local: local_file }], { include: [{ remote: remote_url, local: local_file }],
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'returns ambigious specification error' do it 'returns ambigious specification error' do
...@@ -140,7 +140,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -140,7 +140,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context "when 'include' is not defined" do context "when 'include' is not defined" do
let(:values) do let(:values) do
{ {
image: 'ruby:2.2' image: 'ruby:2.7'
} }
end end
...@@ -155,7 +155,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -155,7 +155,7 @@ describe Gitlab::Ci::Config::External::Mapper do
{ 'local' => local_file }, { 'local' => local_file },
{ 'local' => local_file } { 'local' => local_file }
], ],
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
it 'raises an exception' do it 'raises an exception' do
...@@ -169,7 +169,7 @@ describe Gitlab::Ci::Config::External::Mapper do ...@@ -169,7 +169,7 @@ describe Gitlab::Ci::Config::External::Mapper do
{ 'local' => local_file }, { 'local' => local_file },
{ 'remote' => remote_url } { 'remote' => remote_url }
], ],
image: 'ruby:2.2' } image: 'ruby:2.7' }
end end
before do before do
......
...@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::External::Processor do
subject { processor.perform } subject { processor.perform }
context 'when no external files defined' do context 'when no external files defined' do
let(:values) { { image: 'ruby:2.2' } } let(:values) { { image: 'ruby:2.7' } }
it 'returns the same values' do it 'returns the same values' do
expect(processor.perform).to eq(values) expect(processor.perform).to eq(values)
...@@ -32,7 +32,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -32,7 +32,7 @@ describe Gitlab::Ci::Config::External::Processor do
end end
context 'when an invalid local file is defined' do context 'when an invalid local file is defined' do
let(:values) { { include: '/lib/gitlab/ci/templates/non-existent-file.yml', image: 'ruby:2.2' } } let(:values) { { include: '/lib/gitlab/ci/templates/non-existent-file.yml', image: 'ruby:2.7' } }
it 'raises an error' do it 'raises an error' do
expect { processor.perform }.to raise_error( expect { processor.perform }.to raise_error(
...@@ -44,7 +44,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -44,7 +44,7 @@ describe Gitlab::Ci::Config::External::Processor do
context 'when an invalid remote file is defined' do context 'when an invalid remote file is defined' do
let(:remote_file) { 'http://doesntexist.com/.gitlab-ci-1.yml' } let(:remote_file) { 'http://doesntexist.com/.gitlab-ci-1.yml' }
let(:values) { { include: remote_file, image: 'ruby:2.2' } } let(:values) { { include: remote_file, image: 'ruby:2.7' } }
before do before do
stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error')) stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error'))
...@@ -60,7 +60,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -60,7 +60,7 @@ describe Gitlab::Ci::Config::External::Processor do
context 'with a valid remote external file is defined' do context 'with a valid remote external file is defined' do
let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:values) { { include: remote_file, image: 'ruby:2.2' } } let(:values) { { include: remote_file, image: 'ruby:2.7' } }
let(:external_file_content) do let(:external_file_content) do
<<-HEREDOC <<-HEREDOC
before_script: before_script:
...@@ -94,7 +94,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -94,7 +94,7 @@ describe Gitlab::Ci::Config::External::Processor do
end end
context 'with a valid local external file is defined' do context 'with a valid local external file is defined' do
let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'ruby:2.2' } } let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'ruby:2.7' } }
let(:local_file_content) do let(:local_file_content) do
<<-HEREDOC <<-HEREDOC
before_script: before_script:
...@@ -131,7 +131,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -131,7 +131,7 @@ describe Gitlab::Ci::Config::External::Processor do
let(:values) do let(:values) do
{ {
include: external_files, include: external_files,
image: 'ruby:2.2' image: 'ruby:2.7'
} }
end end
...@@ -163,7 +163,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -163,7 +163,7 @@ describe Gitlab::Ci::Config::External::Processor do
end end
context 'when external files are defined but not valid' do context 'when external files are defined but not valid' do
let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'ruby:2.2' } } let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'ruby:2.7' } }
let(:local_file_content) { 'invalid content file ////' } let(:local_file_content) { 'invalid content file ////' }
...@@ -185,7 +185,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -185,7 +185,7 @@ describe Gitlab::Ci::Config::External::Processor do
let(:values) do let(:values) do
{ {
include: remote_file, include: remote_file,
image: 'ruby:2.2' image: 'ruby:2.7'
} }
end end
...@@ -198,7 +198,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -198,7 +198,7 @@ describe Gitlab::Ci::Config::External::Processor do
it 'takes precedence' do it 'takes precedence' do
stub_full_request(remote_file).to_return(body: remote_file_content) stub_full_request(remote_file).to_return(body: remote_file_content)
expect(processor.perform[:image]).to eq('ruby:2.2') expect(processor.perform[:image]).to eq('ruby:2.7')
end end
end end
...@@ -208,7 +208,7 @@ describe Gitlab::Ci::Config::External::Processor do ...@@ -208,7 +208,7 @@ describe Gitlab::Ci::Config::External::Processor do
include: [ include: [
{ local: '/local/file.yml' } { local: '/local/file.yml' }
], ],
image: 'ruby:2.2' image: 'ruby:2.7'
} }
end end
......
...@@ -20,7 +20,7 @@ describe Gitlab::Ci::Config do ...@@ -20,7 +20,7 @@ describe Gitlab::Ci::Config do
context 'when config is valid' do context 'when config is valid' do
let(:yml) do let(:yml) do
<<-EOS <<-EOS
image: ruby:2.2 image: ruby:2.7
rspec: rspec:
script: script:
...@@ -32,7 +32,7 @@ describe Gitlab::Ci::Config do ...@@ -32,7 +32,7 @@ describe Gitlab::Ci::Config do
describe '#to_hash' do describe '#to_hash' do
it 'returns hash created from string' do it 'returns hash created from string' do
hash = { hash = {
image: 'ruby:2.2', image: 'ruby:2.7',
rspec: { rspec: {
script: ['gem install rspec', script: ['gem install rspec',
'rspec'] 'rspec']
...@@ -85,7 +85,7 @@ describe Gitlab::Ci::Config do ...@@ -85,7 +85,7 @@ describe Gitlab::Ci::Config do
context 'when using extendable hash' do context 'when using extendable hash' do
let(:yml) do let(:yml) do
<<-EOS <<-EOS
image: ruby:2.2 image: ruby:2.7
rspec: rspec:
script: rspec script: rspec
...@@ -98,7 +98,7 @@ describe Gitlab::Ci::Config do ...@@ -98,7 +98,7 @@ describe Gitlab::Ci::Config do
it 'correctly extends the hash' do it 'correctly extends the hash' do
hash = { hash = {
image: 'ruby:2.2', image: 'ruby:2.7',
rspec: { script: 'rspec' }, rspec: { script: 'rspec' },
test: { test: {
extends: 'rspec', extends: 'rspec',
...@@ -188,7 +188,7 @@ describe Gitlab::Ci::Config do ...@@ -188,7 +188,7 @@ describe Gitlab::Ci::Config do
let(:yml) do let(:yml) do
<<-EOS <<-EOS
image: image:
name: ruby:2.2 name: ruby:2.7
ports: ports:
- 80 - 80
EOS EOS
...@@ -202,12 +202,12 @@ describe Gitlab::Ci::Config do ...@@ -202,12 +202,12 @@ describe Gitlab::Ci::Config do
context 'in the job image' do context 'in the job image' do
let(:yml) do let(:yml) do
<<-EOS <<-EOS
image: ruby:2.2 image: ruby:2.7
test: test:
script: rspec script: rspec
image: image:
name: ruby:2.2 name: ruby:2.7
ports: ports:
- 80 - 80
EOS EOS
...@@ -221,11 +221,11 @@ describe Gitlab::Ci::Config do ...@@ -221,11 +221,11 @@ describe Gitlab::Ci::Config do
context 'in the services' do context 'in the services' do
let(:yml) do let(:yml) do
<<-EOS <<-EOS
image: ruby:2.2 image: ruby:2.7
test: test:
script: rspec script: rspec
image: ruby:2.2 image: ruby:2.7
services: services:
- name: test - name: test
alias: test alias: test
...@@ -266,7 +266,7 @@ describe Gitlab::Ci::Config do ...@@ -266,7 +266,7 @@ describe Gitlab::Ci::Config do
- #{local_location} - #{local_location}
- #{remote_location} - #{remote_location}
image: ruby:2.2 image: ruby:2.7
HEREDOC HEREDOC
end end
...@@ -296,7 +296,7 @@ describe Gitlab::Ci::Config do ...@@ -296,7 +296,7 @@ describe Gitlab::Ci::Config do
} }
composed_hash = { composed_hash = {
before_script: before_script_values, before_script: before_script_values,
image: "ruby:2.2", image: "ruby:2.7",
rspec: { script: ["bundle exec rspec"] }, rspec: { script: ["bundle exec rspec"] },
variables: variables variables: variables
} }
...@@ -381,7 +381,7 @@ describe Gitlab::Ci::Config do ...@@ -381,7 +381,7 @@ describe Gitlab::Ci::Config do
include: include:
- #{remote_location} - #{remote_location}
image: ruby:2.2 image: ruby:2.7
HEREDOC HEREDOC
end end
...@@ -392,7 +392,7 @@ describe Gitlab::Ci::Config do ...@@ -392,7 +392,7 @@ describe Gitlab::Ci::Config do
end end
it 'takes precedence' do it 'takes precedence' do
expect(config.to_hash).to eq({ image: 'ruby:2.2' }) expect(config.to_hash).to eq({ image: 'ruby:2.7' })
end end
end end
......
...@@ -665,7 +665,7 @@ module Gitlab ...@@ -665,7 +665,7 @@ module Gitlab
describe "Image and service handling" do describe "Image and service handling" do
context "when extended docker configuration is used" do context "when extended docker configuration is used" do
it "returns image and service when defined" do it "returns image and service when defined" do
config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, config = YAML.dump({ image: { name: "ruby:2.7", entrypoint: ["/usr/local/bin/init", "run"] },
services: ["mysql", { name: "docker:dind", alias: "docker", services: ["mysql", { name: "docker:dind", alias: "docker",
entrypoint: ["/usr/local/bin/init", "run"], entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }], command: ["/usr/local/bin/init", "run"] }],
...@@ -683,7 +683,7 @@ module Gitlab ...@@ -683,7 +683,7 @@ module Gitlab
options: { options: {
before_script: ["pwd"], before_script: ["pwd"],
script: ["rspec"], script: ["rspec"],
image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, image: { name: "ruby:2.7", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "mysql" }, services: [{ name: "mysql" },
{ name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }] command: ["/usr/local/bin/init", "run"] }]
...@@ -696,7 +696,7 @@ module Gitlab ...@@ -696,7 +696,7 @@ module Gitlab
end end
it "returns image and service when overridden for job" do it "returns image and service when overridden for job" do
config = YAML.dump({ image: "ruby:2.1", config = YAML.dump({ image: "ruby:2.7",
services: ["mysql"], services: ["mysql"],
before_script: ["pwd"], before_script: ["pwd"],
rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
...@@ -731,7 +731,7 @@ module Gitlab ...@@ -731,7 +731,7 @@ module Gitlab
context "when etended docker configuration is not used" do context "when etended docker configuration is not used" do
it "returns image and service when defined" do it "returns image and service when defined" do
config = YAML.dump({ image: "ruby:2.1", config = YAML.dump({ image: "ruby:2.7",
services: ["mysql", "docker:dind"], services: ["mysql", "docker:dind"],
before_script: ["pwd"], before_script: ["pwd"],
rspec: { script: "rspec" } }) rspec: { script: "rspec" } })
...@@ -747,7 +747,7 @@ module Gitlab ...@@ -747,7 +747,7 @@ module Gitlab
options: { options: {
before_script: ["pwd"], before_script: ["pwd"],
script: ["rspec"], script: ["rspec"],
image: { name: "ruby:2.1" }, image: { name: "ruby:2.7" },
services: [{ name: "mysql" }, { name: "docker:dind" }] services: [{ name: "mysql" }, { name: "docker:dind" }]
}, },
allow_failure: false, allow_failure: false,
...@@ -758,7 +758,7 @@ module Gitlab ...@@ -758,7 +758,7 @@ module Gitlab
end end
it "returns image and service when overridden for job" do it "returns image and service when overridden for job" do
config = YAML.dump({ image: "ruby:2.1", config = YAML.dump({ image: "ruby:2.7",
services: ["mysql"], services: ["mysql"],
before_script: ["pwd"], before_script: ["pwd"],
rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
...@@ -1292,7 +1292,7 @@ module Gitlab ...@@ -1292,7 +1292,7 @@ module Gitlab
describe "Artifacts" do describe "Artifacts" do
it "returns artifacts when defined" do it "returns artifacts when defined" do
config = YAML.dump({ config = YAML.dump({
image: "ruby:2.1", image: "ruby:2.7",
services: ["mysql"], services: ["mysql"],
before_script: ["pwd"], before_script: ["pwd"],
rspec: { rspec: {
...@@ -1318,7 +1318,7 @@ module Gitlab ...@@ -1318,7 +1318,7 @@ module Gitlab
options: { options: {
before_script: ["pwd"], before_script: ["pwd"],
script: ["rspec"], script: ["rspec"],
image: { name: "ruby:2.1" }, image: { name: "ruby:2.7" },
services: [{ name: "mysql" }], services: [{ name: "mysql" }],
artifacts: { artifacts: {
name: "custom_name", name: "custom_name",
...@@ -1945,7 +1945,7 @@ module Gitlab ...@@ -1945,7 +1945,7 @@ module Gitlab
context 'when hidden job have a script definition' do context 'when hidden job have a script definition' do
let(:config) do let(:config) do
YAML.dump({ YAML.dump({
'.hidden_job' => { image: 'ruby:2.1', script: 'test' }, '.hidden_job' => { image: 'ruby:2.7', script: 'test' },
'normal_job' => { script: 'test' } 'normal_job' => { script: 'test' }
}) })
end end
...@@ -1956,7 +1956,7 @@ module Gitlab ...@@ -1956,7 +1956,7 @@ module Gitlab
context "when hidden job doesn't have a script definition" do context "when hidden job doesn't have a script definition" do
let(:config) do let(:config) do
YAML.dump({ YAML.dump({
'.hidden_job' => { image: 'ruby:2.1' }, '.hidden_job' => { image: 'ruby:2.7' },
'normal_job' => { script: 'test' } 'normal_job' => { script: 'test' }
}) })
end end
......
...@@ -6,7 +6,7 @@ describe Gitlab::Config::Loader::Yaml do ...@@ -6,7 +6,7 @@ describe Gitlab::Config::Loader::Yaml do
let(:loader) { described_class.new(yml) } let(:loader) { described_class.new(yml) }
context 'when yaml syntax is correct' do context 'when yaml syntax is correct' do
let(:yml) { 'image: ruby:2.2' } let(:yml) { 'image: ruby:2.7' }
describe '#valid?' do describe '#valid?' do
it 'returns true' do it 'returns true' do
...@@ -16,7 +16,7 @@ describe Gitlab::Config::Loader::Yaml do ...@@ -16,7 +16,7 @@ describe Gitlab::Config::Loader::Yaml do
describe '#load!' do describe '#load!' do
it 'returns a valid hash' do it 'returns a valid hash' do
expect(loader.load!).to eq(image: 'ruby:2.2') expect(loader.load!).to eq(image: 'ruby:2.7')
end end
end end
end end
......
...@@ -55,9 +55,11 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do ...@@ -55,9 +55,11 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do
let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") } let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") }
let(:result) { exporter.save } let(:result) { exporter.save }
it 'creates the repository from the bundle' do before do
expect(exporter.save).to be_truthy expect(exporter.save).to be_truthy
end
it 'creates the repository from the bundle' do
expect(snippet.repository_exists?).to be_falsey expect(snippet.repository_exists?).to be_falsey
expect(snippet.snippet_repository).to be_nil expect(snippet.snippet_repository).to be_nil
expect(snippet.repository).to receive(:create_from_bundle).and_call_original expect(snippet.repository).to receive(:create_from_bundle).and_call_original
...@@ -66,5 +68,14 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do ...@@ -66,5 +68,14 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do
expect(snippet.repository_exists?).to be_truthy expect(snippet.repository_exists?).to be_truthy
expect(snippet.snippet_repository).not_to be_nil expect(snippet.snippet_repository).not_to be_nil
end end
it 'sets same shard in snippet repository as in the repository storage' do
expect(snippet).to receive(:repository_storage).and_return('picked')
expect(snippet.repository).to receive(:create_from_bundle)
restorer.restore
expect(snippet.snippet_repository.shard_name).to eq 'picked'
end
end end
end end
...@@ -1882,7 +1882,7 @@ describe Ci::Build do ...@@ -1882,7 +1882,7 @@ describe Ci::Build do
describe '#options' do describe '#options' do
let(:options) do let(:options) do
{ {
image: "ruby:2.1", image: "ruby:2.7",
services: ["postgres"], services: ["postgres"],
script: ["ls -a"] script: ["ls -a"]
} }
...@@ -1893,11 +1893,11 @@ describe Ci::Build do ...@@ -1893,11 +1893,11 @@ describe Ci::Build do
end end
it 'allows to access with keys' do it 'allows to access with keys' do
expect(build.options[:image]).to eq('ruby:2.1') expect(build.options[:image]).to eq('ruby:2.7')
end end
it 'allows to access with strings' do it 'allows to access with strings' do
expect(build.options['image']).to eq('ruby:2.1') expect(build.options['image']).to eq('ruby:2.7')
end end
context 'when ci_build_metadata_config is set' do context 'when ci_build_metadata_config is set' do
......
...@@ -84,6 +84,10 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do ...@@ -84,6 +84,10 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
describe '#calculate_reactive_cache' do describe '#calculate_reactive_cache' do
describe '#commit_status' do describe '#commit_status' do
let(:buildkite_full_url) do
'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
end
subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
it 'sets commit status to :error when status is 500' do it 'sets commit status to :error when status is 500' do
...@@ -103,13 +107,25 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do ...@@ -103,13 +107,25 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
is_expected.to eq('Great Success') is_expected.to eq('Great Success')
end end
Gitlab::HTTP::HTTP_ERRORS.each do |http_error|
it "sets commit status to :error with a #{http_error.name} error" do
WebMock.stub_request(:get, buildkite_full_url)
.to_raise(http_error)
expect(Gitlab::ErrorTracking)
.to receive(:log_exception)
.with(instance_of(http_error), project_id: project.id)
is_expected.to eq(:error)
end
end
end end
end end
end end
def stub_request(status: 200, body: nil) def stub_request(status: 200, body: nil)
body ||= %q({"status":"success"}) body ||= %q({"status":"success"})
buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
stub_full_request(buildkite_full_url) stub_full_request(buildkite_full_url)
.to_return(status: status, .to_return(status: status,
......
...@@ -106,6 +106,10 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do ...@@ -106,6 +106,10 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do
WebMock.stub_request(:get, commit_status_path) WebMock.stub_request(:get, commit_status_path)
.to_raise(http_error) .to_raise(http_error)
expect(Gitlab::ErrorTracking)
.to receive(:log_exception)
.with(instance_of(http_error), project_id: project.id)
is_expected.to eq(:error) is_expected.to eq(:error)
end end
end end
......
...@@ -16,7 +16,7 @@ describe SnippetRepository do ...@@ -16,7 +16,7 @@ describe SnippetRepository do
describe '.find_snippet' do describe '.find_snippet' do
it 'finds snippet by disk path' do it 'finds snippet by disk path' do
snippet = create(:snippet, author: user) snippet = create(:snippet, author: user)
snippet.track_snippet_repository snippet.track_snippet_repository(snippet.repository.storage)
expect(described_class.find_snippet(snippet.disk_path)).to eq(snippet) expect(described_class.find_snippet(snippet.disk_path)).to eq(snippet)
end end
......
...@@ -567,18 +567,21 @@ describe Snippet do ...@@ -567,18 +567,21 @@ describe Snippet do
describe '#track_snippet_repository' do describe '#track_snippet_repository' do
let(:snippet) { create(:snippet) } let(:snippet) { create(:snippet) }
let(:shard_name) { 'foo' }
subject { snippet.track_snippet_repository(shard_name) }
context 'when a snippet repository entry does not exist' do context 'when a snippet repository entry does not exist' do
it 'creates a new entry' do it 'creates a new entry' do
expect { snippet.track_snippet_repository }.to change(snippet, :snippet_repository) expect { subject }.to change(snippet, :snippet_repository)
end end
it 'tracks the snippet storage location' do it 'tracks the snippet storage location' do
snippet.track_snippet_repository subject
expect(snippet.snippet_repository).to have_attributes( expect(snippet.snippet_repository).to have_attributes(
disk_path: snippet.disk_path, disk_path: snippet.disk_path,
shard_name: snippet.repository_storage shard_name: shard_name
) )
end end
end end
...@@ -586,21 +589,20 @@ describe Snippet do ...@@ -586,21 +589,20 @@ describe Snippet do
context 'when a tracking entry exists' do context 'when a tracking entry exists' do
let!(:snippet) { create(:snippet, :repository) } let!(:snippet) { create(:snippet, :repository) }
let(:snippet_repository) { snippet.snippet_repository } let(:snippet_repository) { snippet.snippet_repository }
let!(:shard) { create(:shard, name: 'foo') } let(:shard_name) { 'bar' }
it 'does not create a new entry in the database' do it 'does not create a new entry in the database' do
expect { snippet.track_snippet_repository }.not_to change(snippet, :snippet_repository) expect { subject }.not_to change(snippet, :snippet_repository)
end end
it 'updates the snippet storage location' do it 'updates the snippet storage location' do
allow(snippet).to receive(:disk_path).and_return('fancy/new/path') allow(snippet).to receive(:disk_path).and_return('fancy/new/path')
allow(snippet).to receive(:repository_storage).and_return('foo')
snippet.track_snippet_repository subject
expect(snippet.snippet_repository).to have_attributes( expect(snippet.snippet_repository).to have_attributes(
disk_path: 'fancy/new/path', disk_path: 'fancy/new/path',
shard_name: 'foo' shard_name: shard_name
) )
end end
end end
...@@ -609,19 +611,31 @@ describe Snippet do ...@@ -609,19 +611,31 @@ describe Snippet do
describe '#create_repository' do describe '#create_repository' do
let(:snippet) { create(:snippet) } let(:snippet) { create(:snippet) }
subject { snippet.create_repository }
it 'creates the repository' do it 'creates the repository' do
expect(snippet.repository).to receive(:after_create).and_call_original expect(snippet.repository).to receive(:after_create).and_call_original
expect(snippet.create_repository).to be_truthy expect(subject).to be_truthy
expect(snippet.repository.exists?).to be_truthy expect(snippet.repository.exists?).to be_truthy
end end
it 'tracks snippet repository' do it 'tracks snippet repository' do
expect do expect do
snippet.create_repository subject
end.to change(SnippetRepository, :count).by(1) end.to change(SnippetRepository, :count).by(1)
end end
it 'sets same shard in snippet repository as in the repository storage' do
expect(snippet).to receive(:repository_storage).and_return('picked')
expect(snippet).to receive(:repository_exists?).and_return(false)
expect(snippet.repository).to receive(:create_if_not_exists)
subject
expect(snippet.snippet_repository.shard_name).to eq 'picked'
end
context 'when repository exists' do context 'when repository exists' do
let!(:snippet) { create(:snippet, :repository) } let!(:snippet) { create(:snippet, :repository) }
......
...@@ -29,7 +29,7 @@ describe API::Lint do ...@@ -29,7 +29,7 @@ describe API::Lint do
end end
it "responds with errors about invalid configuration" do it "responds with errors about invalid configuration" do
post api('/ci/lint'), params: { content: '{ image: "ruby:2.1", services: ["postgres"] }' } post api('/ci/lint'), params: { content: '{ image: "ruby:2.7", services: ["postgres"] }' }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid') expect(json_response['status']).to eq('invalid')
......
...@@ -523,7 +523,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -523,7 +523,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['token']).to eq(job.token) expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['job_info']).to eq(expected_job_info)
expect(json_response['git_info']).to eq(expected_git_info) expect(json_response['git_info']).to eq(expected_git_info)
expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh', 'ports' => [] }) expect(json_response['image']).to eq({ 'name' => 'ruby:2.7', 'entrypoint' => '/bin/sh', 'ports' => [] })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
'alias' => nil, 'command' => nil, 'ports' => [] }, 'alias' => nil, 'command' => nil, 'ports' => [] },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
......
...@@ -555,7 +555,7 @@ describe Ci::CreatePipelineService do ...@@ -555,7 +555,7 @@ describe Ci::CreatePipelineService do
let(:ci_yaml) do let(:ci_yaml) do
<<-EOS <<-EOS
image: image:
name: ruby:2.2 name: ruby:2.7
ports: ports:
- 80 - 80
EOS EOS
...@@ -567,12 +567,12 @@ describe Ci::CreatePipelineService do ...@@ -567,12 +567,12 @@ describe Ci::CreatePipelineService do
context 'in the job image' do context 'in the job image' do
let(:ci_yaml) do let(:ci_yaml) do
<<-EOS <<-EOS
image: ruby:2.2 image: ruby:2.7
test: test:
script: rspec script: rspec
image: image:
name: ruby:2.2 name: ruby:2.7
ports: ports:
- 80 - 80
EOS EOS
...@@ -584,11 +584,11 @@ describe Ci::CreatePipelineService do ...@@ -584,11 +584,11 @@ describe Ci::CreatePipelineService do
context 'in the service' do context 'in the service' do
let(:ci_yaml) do let(:ci_yaml) do
<<-EOS <<-EOS
image: ruby:2.2 image: ruby:2.7
test: test:
script: rspec script: rspec
image: ruby:2.2 image: ruby:2.7
services: services:
- name: test - name: test
ports: ports:
......
...@@ -41,7 +41,8 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -41,7 +41,8 @@ describe Projects::ContainerRepository::CleanupTagsService do
let(:params) { {} } let(:params) { {} }
it 'does not remove anything' do it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest) expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
.not_to receive(:execute)
is_expected.to include(status: :success, deleted: []) is_expected.to include(status: :success, deleted: [])
end end
...@@ -49,15 +50,10 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -49,15 +50,10 @@ describe Projects::ContainerRepository::CleanupTagsService do
context 'when regex matching everything is specified' do context 'when regex matching everything is specified' do
shared_examples 'removes all matches' do shared_examples 'removes all matches' do
it 'does remove B* and C' do it 'does remove all tags except latest' do
# The :A cannot be removed as config is shared with :latest expect_delete(%w(A Ba Bb C D E))
# The :E cannot be removed as it does not have valid manifest
expect_delete('sha256:configB').twice is_expected.to include(status: :success, deleted: %w(A Ba Bb C D E))
expect_delete('sha256:configC')
expect_delete('sha256:configD')
is_expected.to include(status: :success, deleted: %w(D Bb Ba C))
end end
end end
...@@ -82,10 +78,9 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -82,10 +78,9 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
it 'does remove C and D' do it 'does remove C and D' do
expect_delete('sha256:configC') expect_delete(%w(C D))
expect_delete('sha256:configD')
is_expected.to include(status: :success, deleted: %w(D C)) is_expected.to include(status: :success, deleted: %w(C D))
end end
context 'with overriding allow regex' do context 'with overriding allow regex' do
...@@ -95,7 +90,7 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -95,7 +90,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
it 'does not remove C' do it 'does not remove C' do
expect_delete('sha256:configD') expect_delete(%w(D))
is_expected.to include(status: :success, deleted: %w(D)) is_expected.to include(status: :success, deleted: %w(D))
end end
...@@ -108,36 +103,52 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -108,36 +103,52 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
it 'does not remove C' do it 'does not remove C' do
expect_delete('sha256:configD') expect_delete(%w(D))
is_expected.to include(status: :success, deleted: %w(D)) is_expected.to include(status: :success, deleted: %w(D))
end end
end end
end end
context 'when removing a tagged image that is used by another tag' do context 'with allow regex value' do
let(:params) do let(:params) do
{ 'name_regex_delete' => 'Ba' } { 'name_regex_delete' => '.*',
'name_regex_keep' => 'B.*' }
end end
it 'does not remove the tag' do it 'does not remove B*' do
# Issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/21405 expect_delete(%w(A C D E))
is_expected.to include(status: :success, deleted: []) is_expected.to include(status: :success, deleted: %w(A C D E))
end end
end end
context 'with allow regex value' do context 'when keeping only N tags' do
let(:params) do let(:params) do
{ 'name_regex_delete' => '.*', { 'name_regex' => 'A|B.*|C',
'name_regex_keep' => 'B.*' } 'keep_n' => 1 }
end end
it 'does not remove B*' do it 'sorts tags by date' do
expect_delete('sha256:configC') expect_delete(%w(Bb Ba C))
expect_delete('sha256:configD')
expect(service).to receive(:order_by_date).and_call_original
is_expected.to include(status: :success, deleted: %w(D C)) is_expected.to include(status: :success, deleted: %w(Bb Ba C))
end
end
context 'when not keeping N tags' do
let(:params) do
{ 'name_regex' => 'A|B.*|C' }
end
it 'does not sort tags by date' do
expect_delete(%w(A Ba Bb C))
expect(service).not_to receive(:order_by_date)
is_expected.to include(status: :success, deleted: %w(A Ba Bb C))
end end
end end
...@@ -147,10 +158,10 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -147,10 +158,10 @@ describe Projects::ContainerRepository::CleanupTagsService do
'keep_n' => 3 } 'keep_n' => 3 }
end end
it 'does remove C as it is oldest' do it 'does remove B* and C as they are the oldest' do
expect_delete('sha256:configC') expect_delete(%w(Bb Ba C))
is_expected.to include(status: :success, deleted: %w(C)) is_expected.to include(status: :success, deleted: %w(Bb Ba C))
end end
end end
...@@ -161,10 +172,9 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -161,10 +172,9 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
it 'does remove B* and C as they are older than 1 day' do it 'does remove B* and C as they are older than 1 day' do
expect_delete('sha256:configB').twice expect_delete(%w(Ba Bb C))
expect_delete('sha256:configC')
is_expected.to include(status: :success, deleted: %w(Bb Ba C)) is_expected.to include(status: :success, deleted: %w(Ba Bb C))
end end
end end
...@@ -176,8 +186,7 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -176,8 +186,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
it 'does remove B* and C' do it 'does remove B* and C' do
expect_delete('sha256:configB').twice expect_delete(%w(Bb Ba C))
expect_delete('sha256:configC')
is_expected.to include(status: :success, deleted: %w(Bb Ba C)) is_expected.to include(status: :success, deleted: %w(Bb Ba C))
end end
...@@ -195,8 +204,7 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -195,8 +204,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
it 'succeeds without a user' do it 'succeeds without a user' do
expect_delete('sha256:configB').twice expect_delete(%w(Bb Ba C))
expect_delete('sha256:configC')
is_expected.to include(status: :success, deleted: %w(Bb Ba C)) is_expected.to include(status: :success, deleted: %w(Bb Ba C))
end end
...@@ -238,9 +246,14 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -238,9 +246,14 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
end end
def expect_delete(digest) def expect_delete(tags)
expect_any_instance_of(ContainerRegistry::Client) expect(Projects::ContainerRepository::DeleteTagsService)
.to receive(:delete_repository_tag_by_digest) .to receive(:new)
.with(repository.path, digest) { true } .with(repository.project, user, tags: tags)
.and_call_original
expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
.to receive(:execute)
.with(repository) { { status: :success, deleted: tags } }
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment