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';
export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
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 EXPIRATION_POLICY_ALERT_TITLE = s__(
......
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate';
import RegistryExplorer from './pages/index.vue';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
......@@ -6,6 +7,7 @@ import { createStore } from './stores';
import createRouter from './router';
Vue.use(Translate);
Vue.use(GlToast);
export default () => {
const el = document.getElementById('js-container-registry');
......
......@@ -31,6 +31,10 @@ import {
LIST_LABEL_IMAGE_ID,
LIST_LABEL_SIZE,
LIST_LABEL_LAST_UPDATED,
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
} from '../constants';
export default {
......@@ -176,17 +180,37 @@ export default {
},
handleSingleDelete(itemToDelete) {
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() {
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
this.selectedItems = [];
this.requestDeleteTags({
return this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => this.tags[x].name),
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() {
this.track('confirm_delete');
......
<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>
<template>
<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">
<router-view />
<router-view ref="router-view" />
</transition>
</div>
</template>
......@@ -12,11 +12,13 @@ import {
GlSkeletonLoader,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE } from '../constants';
export default {
name: 'RegistryListApp',
......@@ -44,6 +46,23 @@ export default {
width: 1000,
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() {
return {
itemToDelete: {},
......@@ -76,10 +95,22 @@ export default {
this.itemToDelete = item;
this.$refs.deleteModal.show();
},
handleDeleteRepository() {
handleDeleteImage() {
this.track('confirm_delete');
this.requestDeleteImage(this.itemToDelete.destroy_path);
this.itemToDelete = {};
return this.requestDeleteImage(this.itemToDelete.destroy_path)
.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) {
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
......@@ -95,18 +126,12 @@ export default {
<gl-empty-state
v-if="config.characterError"
:title="s__('ContainerRegistry|Docker connection error')"
:title="$options.i18n.connectionErrorTitle"
:svg-path="config.containersErrorImage"
>
<template #description>
<p>
<gl-sprintf
: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}`)
"
>
<gl-sprintf :message="$options.i18n.connectionErrorMessage">
<template #docLink="{content}">
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
{{ content }}
......@@ -120,17 +145,11 @@ export default {
<template v-else>
<div>
<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" />
</div>
<p>
<gl-sprintf
: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}`)
"
>
<gl-sprintf :message="$options.i18n.introText">
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">
{{ content }}
......@@ -180,16 +199,14 @@ export default {
<div
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="
s__('ContainerRegistry|Missing or insufficient permission, delete button disabled')
"
:title="$options.i18n.deleteButtonDisabled"
>
<gl-button
ref="deleteImageButton"
v-gl-tooltip
:disabled="!listItem.destroy_path"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
:title="$options.i18n.removeRepositoryLabel"
:aria-label="$options.i18n.removeRepositoryLabel"
class="btn-inverted"
variant="danger"
@click="deleteImage(listItem)"
......@@ -217,16 +234,12 @@ export default {
ref="deleteModal"
modal-id="delete-image-modal"
ok-variant="danger"
@ok="handleDeleteRepository"
@ok="handleDeleteImage"
@cancel="track('cancel_delete')"
>
<template #modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
<template #modal-title>{{ $options.i18n.removeRepositoryLabel }}</template>
<p>
<gl-sprintf
:message=" s__(
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
),"
>
<gl-sprintf :message="$options.i18n.removeRepositoryModalText">
<template #title>
<b>{{ itemToDelete.path }}</b>
</template>
......
......@@ -6,16 +6,12 @@ import {
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
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';
import { decodeAndParse } from '../utils';
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 }) => {
commit(types.SET_IMAGES_LIST_SUCCESS, data);
......@@ -67,11 +63,10 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) =
return axios
.delete(tag.destroy_path)
.then(() => {
createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success');
dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
createFlash(DELETE_TAG_ERROR_MESSAGE);
commit(types.SET_MAIN_LOADING, false);
});
};
......@@ -85,11 +80,10 @@ export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params })
return axios
.delete(url, { params: { ids } })
.then(() => {
createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success');
dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
createFlash(DELETE_TAGS_ERROR_MESSAGE);
commit(types.SET_MAIN_LOADING, false);
});
};
......@@ -100,11 +94,8 @@ export const requestDeleteImage = ({ commit, dispatch, state }, destroyPath) =>
return axios
.delete(destroyPath)
.then(() => {
dispatch('setShowGarbageCollectionTip', true);
dispatch('requestImagesList', { pagination: state.pagination });
createFlash(DELETE_IMAGE_SUCCESS_MESSAGE, 'success');
})
.catch(() => {
createFlash(DELETE_IMAGE_ERROR_MESSAGE);
})
.finally(() => {
commit(types.SET_MAIN_LOADING, false);
......
......@@ -18,3 +18,7 @@ export const dockerLoginCommand = state => {
/* eslint-disable @gitlab/require-i18n-strings */
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';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION';
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 {
...config,
expirationPolicy: config.expirationPolicy ? JSON.parse(config.expirationPolicy) : undefined,
isGroupPage: config.isGroupPage !== undefined,
isAdmin: config.isAdmin !== undefined,
};
},
......@@ -22,6 +23,10 @@ export default {
state.isLoading = isLoading;
},
[types.SET_SHOW_GARBAGE_COLLECTION_TIP](state, showGarbageCollectionTip) {
state.showGarbageCollectionTip = showGarbageCollectionTip;
},
[types.SET_PAGINATION](state, headers) {
const normalizedHeaders = normalizeHeaders(headers);
state.pagination = parseIntPagination(normalizedHeaders);
......
export default () => ({
isLoading: false,
showGarbageCollectionTip: false,
config: {},
images: [],
tags: [],
......
......@@ -7,6 +7,10 @@
a {
color: $gl-text-color;
&.link {
color: $blue-600;
}
}
.author-link {
......
......@@ -68,10 +68,12 @@ module EventsHelper
end
def event_preposition(event)
if event.push_action? || event.commented_action? || event.target
"at"
if event.wiki_page?
'in the wiki for'
elsif event.milestone?
"in"
'in'
elsif event.push_action? || event.commented_action? || event.target
'at'
end
end
......@@ -172,6 +174,19 @@ module EventsHelper
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)
if event.note_target
capture do
......
......@@ -73,10 +73,10 @@ class BuildkiteService < CiService
end
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 =
if response.code == 200 && response['status']
if response&.code == 200 && response['status']
response['status']
else
:error
......@@ -117,4 +117,8 @@ class BuildkiteService < CiService
ENDPOINT
end
end
def request_options
{ verify: false, extra_log_info: { project_id: project_id } }
end
end
......@@ -50,10 +50,12 @@ class DroneCiService < CiService
end
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 =
if response.code == 200 && response['status']
if response && response.code == 200 && response['status']
case response['status']
when 'killed'
:canceled
......@@ -68,8 +70,6 @@ class DroneCiService < CiService
end
{ commit_status: status }
rescue *Gitlab::HTTP::HTTP_ERRORS
{ commit_status: :error }
end
def build_page(sha, ref)
......
......@@ -293,12 +293,12 @@ class Snippet < ApplicationRecord
return if repository_exists? && snippet_repository
repository.create_if_not_exists
track_snippet_repository
track_snippet_repository(repository.storage)
end
def track_snippet_repository
repository = snippet_repository || build_snippet_repository
repository.update!(shard_name: repository_storage, disk_path: disk_path)
def track_snippet_repository(shard)
snippet_repo = snippet_repository || build_snippet_repository
snippet_repo.update!(shard_name: shard, disk_path: disk_path)
end
def can_cache_field?(field)
......
......@@ -8,53 +8,30 @@ module Projects
return error('access denied') unless can_destroy?
tags = container_repository.tags
tags_by_digest = group_by_digest(tags)
tags = without_latest(tags)
tags = filter_by_name(tags)
tags = with_manifest(tags)
tags = order_by_date(tags)
tags = filter_keep_n(tags)
tags = filter_by_older_than(tags)
deleted_tags = delete_tags(tags, tags_by_digest)
success(deleted: deleted_tags.map(&:name))
delete_tags(container_repository, tags)
end
private
def delete_tags(tags_to_delete, tags_by_digest)
deleted_digests = group_by_digest(tags_to_delete).select do |digest, tags|
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
def delete_tags(container_repository, tags)
return success(deleted: []) unless tags.any?
# delete all tags
tags.map(&:unsafe_delete)
end
tag_names = tags.map(&:name)
def group_by_digest(tags)
tags.group_by(&:digest)
Projects::ContainerRepository::DeleteTagsService
.new(container_repository.project, current_user, tags: tag_names)
.execute(container_repository)
end
def without_latest(tags)
tags.reject(&:latest?)
end
def with_manifest(tags)
tags.select(&:valid?)
end
def order_by_date(tags)
now = DateTime.now
tags.sort_by { |tag| tag.created_at || now }.reverse
......@@ -74,6 +51,9 @@ module Projects
end
def filter_keep_n(tags)
return tags unless params['keep_n']
tags = order_by_date(tags)
tags.drop(params['keep_n'].to_i)
end
......
......@@ -5,7 +5,9 @@
.event-item-timestamp
#{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
- elsif event.push_action?
= 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 @@
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"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,
character_error: @character_error.to_s } }
- else
......
......@@ -16,6 +16,8 @@
"repository_url" => escape_once(@project.container_registry_url),
"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'),
"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 } }
- else
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
......
......@@ -15,4 +15,6 @@
= render_if_exists 'events/epics_filter'
- if comments_visible?
= 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')
---
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 (
ip_address character varying,
maximum_timeout integer,
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
......@@ -4449,7 +4451,8 @@ CREATE TABLE public.pages_domains (
certificate_source smallint DEFAULT 0 NOT NULL,
wildcard boolean DEFAULT false 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
......@@ -6611,6 +6614,24 @@ CREATE SEQUENCE public.vulnerability_scanners_id_seq
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 (
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
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_hooks ALTER COLUMN id SET DEFAULT nextval('public.web_hooks_id_seq'::regclass);
......@@ -8286,6 +8309,9 @@ ALTER TABLE ONLY public.vulnerability_occurrences
ALTER TABLE ONLY public.vulnerability_scanners
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
ADD CONSTRAINT web_hook_logs_pkey PRIMARY KEY (id);
......@@ -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_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_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
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;
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
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
ALTER TABLE ONLY public.project_aliases
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
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
('20200316162648'),
('20200316173312'),
('20200317142110'),
('20200318140400'),
('20200318152134'),
('20200318162148'),
('20200318163148'),
......@@ -12750,6 +12789,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200318175008'),
('20200319123041'),
('20200319203901'),
('20200320112455'),
('20200320123839'),
('20200323075043'),
('20200323122201'),
('20200324115359');
......
......@@ -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 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 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)**
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.
......
......@@ -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.
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
in the `GITLAB_FEATURES` variable (comma-separated list of values).
......@@ -103,11 +103,9 @@ mysec_dependency_scanning:
$CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bjava\b/
```
The [`only/except`](../../ci/yaml/README.md#onlyexcept-basic) keywords
as well as the new [`rules`](../../ci/yaml/README.md#rules) keyword
make possible to trigger the job depending on the branch, or when some particular file changes.
Such rules should be defined by users based on their needs,
and should not be predefined in the job definition of the scanner.
Any additional job policy should only be configured by users based on their needs.
For instance, predefined policies should not trigger the scanning job
for a particular branch or when a particular set of files changes.
## Docker image
......
......@@ -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
`/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
[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
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).
## 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.
......
......@@ -178,6 +178,7 @@ CAUTION: **Warning:**
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
name or the wrong package will be installed.
Install the latest version of a package using the following command:
```shell
......
......@@ -184,7 +184,7 @@ spotbugs-sast:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
$SAST_DEFAULT_ANALYZERS =~ /spotbugs/ &&
$CI_PROJECT_REPOSITORY_LANGUAGES =~ /java\b/
$CI_PROJECT_REPOSITORY_LANGUAGES =~ /\b(groovy|java|scala)\b/
tslint-sast:
extends: .sast-analyzer
......
......@@ -30,7 +30,7 @@ module Gitlab
def create_repository_from_bundle
repository.create_from_bundle(path_to_bundle)
snippet.track_snippet_repository
snippet.track_snippet_repository(repository.storage)
end
def create_repository_from_db
......
......@@ -5291,6 +5291,9 @@ msgstr ""
msgid "ContainerRegistry|Build an image"
msgstr ""
msgid "ContainerRegistry|Compressed Size"
msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
......@@ -5431,6 +5434,9 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
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"
msgstr ""
......@@ -8178,6 +8184,9 @@ msgstr ""
msgid "EventFilterBy|Filter by team"
msgstr ""
msgid "EventFilterBy|Filter by wiki"
msgstr ""
msgid "Events"
msgstr ""
......@@ -24705,6 +24714,9 @@ msgstr ""
msgid "vulnerability|dismissed"
msgstr ""
msgid "wiki page"
msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
......
......@@ -31,6 +31,8 @@ describe DashboardController do
before do
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)
......@@ -45,7 +47,7 @@ describe DashboardController do
it 'returns count' do
get :activity, params: { format: :json }
expect(json_response['count']).to eq(1)
expect(json_response['count']).to eq(3)
end
end
......
......@@ -16,7 +16,7 @@ FactoryBot.define do
options do
{
image: 'ruby:2.1',
image: 'ruby:2.7',
services: ['postgres'],
script: ['ls -a']
}
......@@ -336,7 +336,7 @@ FactoryBot.define do
trait :extended_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' }],
script: %w(echo),
after_script: %w(ls date),
......
......@@ -28,7 +28,7 @@ FactoryBot.define do
bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA)
snippet.track_snippet_repository
snippet.track_snippet_repository(snippet.repository.storage)
end
end
......
......@@ -5,8 +5,15 @@ import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue';
import store from '~/registry/explorer/stores/';
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 { GlModal } from '../stubs';
import { $toast } from '../../shared/mocks';
describe('Details Page', () => {
let wrapper;
......@@ -40,6 +47,7 @@ describe('Details Page', () => {
id: routeId,
},
},
$toast,
},
});
dispatchSpy = jest.spyOn(store, 'dispatch');
......@@ -249,13 +257,11 @@ describe('Details Page', () => {
});
});
it('when only one element is selected', () => {
const deleteModal = findDeleteModal();
wrapper.setData({ itemsToBeDeleted: [0] });
deleteModal.vm.$emit('ok');
describe('when only one element is selected', () => {
it('execute the delete and remove selection', () => {
wrapper.setData({ itemsToBeDeleted: [0] });
findDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
tag: store.state.tags[0],
params: wrapper.vm.$route.params.id,
......@@ -264,15 +270,33 @@ describe('Details Page', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
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', () => {
const deleteModal = findDeleteModal();
describe('when multiple elements are selected', () => {
beforeEach(() => {
wrapper.setData({ itemsToBeDeleted: [0, 1] });
});
wrapper.setData({ itemsToBeDeleted: [0, 1] });
deleteModal.vm.$emit('ok');
it('execute the delete and remove selection', () => {
findDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
ids: store.state.tags.map(t => t.name),
params: wrapper.vm.$route.params.id,
......@@ -281,6 +305,23 @@ describe('Details Page', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
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
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import store from '~/registry/explorer/stores/';
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 { GlModal, GlEmptyState } from '../stubs';
import { $toast } from '../../shared/mocks';
const localVue = createLocalVue();
localVue.use(VueRouter);
......@@ -40,6 +45,9 @@ describe('List Page', () => {
GlEmptyState,
GlSprintf,
},
mocks: {
$toast,
},
});
dispatchSpy = jest.spyOn(store, 'dispatch');
store.dispatch('receiveImagesListSuccess', imagesListResponse);
......@@ -174,11 +182,29 @@ describe('List Page', () => {
const itemToDelete = wrapper.vm.images[0];
wrapper.setData({ itemToDelete });
findDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'requestDeleteImage',
itemToDelete.destroy_path,
);
expect(store.dispatch).toHaveBeenCalledWith(
'requestDeleteImage',
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', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
dispatchSpy.mockReturnValue();
dispatchSpy.mockResolvedValue();
});
it('send an event when delete button is clicked', () => {
......@@ -235,13 +261,14 @@ describe('List Page', () => {
deleteBtn.vm.$emit('click');
testTrackingCall('click_button');
});
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
testTrackingCall('cancel_delete');
});
it('send an event when confirm is clicked on modal', () => {
dispatchSpy.mockReturnValue();
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
testTrackingCall('confirm_delete');
......
......@@ -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', () => {
const response = {
data: [1, 2, 3],
......@@ -182,19 +193,20 @@ describe('Actions RegistryExplorer Store', () => {
},
[{ type: types.SET_MAIN_LOADING, payload: true }],
[
{
type: 'setShowGarbageCollectionTip',
payload: true,
},
{
type: 'requestTagsList',
payload: { pagination: {}, params },
},
],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
done,
);
});
it('should show flash message on error', done => {
it('should turn off loading on error', done => {
testAction(
actions.requestDeleteTag,
{
......@@ -208,10 +220,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false },
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
done,
);
});
});
......@@ -234,19 +243,20 @@ describe('Actions RegistryExplorer Store', () => {
},
[{ type: types.SET_MAIN_LOADING, payload: true }],
[
{
type: 'setShowGarbageCollectionTip',
payload: true,
},
{
type: 'requestTagsList',
payload: { pagination: {}, params },
},
],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
done,
);
});
it('should show flash message on error', done => {
it('should turn off loading on error', done => {
mock.onDelete(url).replyOnce(500);
testAction(
......@@ -263,17 +273,14 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false },
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
done,
);
});
});
describe('request delete single image', () => {
const deletePath = 'delete/path';
it('successfully performs the delete request', done => {
const deletePath = 'delete/path';
mock.onDelete(deletePath).replyOnce(200);
testAction(
......@@ -287,33 +294,33 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false },
],
[
{
type: 'setShowGarbageCollectionTip',
payload: true,
},
{
type: 'requestImagesList',
payload: { pagination: {} },
},
],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
done,
);
});
it('should show flash message on error', done => {
it('should turn off loading on error', done => {
mock.onDelete(deletePath).replyOnce(400);
testAction(
actions.requestDeleteImage,
null,
deletePath,
{},
[
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
).catch(() => {
done();
});
});
});
});
......@@ -49,4 +49,22 @@ describe('Getters RegistryExplorer store', () => {
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', () => {
describe('SET_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 };
mutations[types.SET_INITIAL_STATE](mockState, {
...payload,
......@@ -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', () => {
const generatePagination = () => [
{
......
// eslint-disable-next-line import/prefer-default-export
export const $toast = {
show: jest.fn(),
};
......@@ -88,6 +88,85 @@ describe EventsHelper do
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
let(:project) { create(:project, :public, :repository) }
let(:event) { create(:event, project: project) }
......
......@@ -9,7 +9,7 @@ describe Gitlab::Ci::Build::Image do
subject { described_class.from_image(job) }
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 } ) }
context 'when image is defined as string' do
......
......@@ -6,11 +6,11 @@ describe Gitlab::Ci::Config::Entry::Image do
let(:entry) { described_class.new(config) }
context 'when configuration is a string' do
let(:config) { 'ruby:2.2' }
let(:config) { 'ruby:2.7' }
describe '#value' 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
......@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#image' 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
......@@ -46,7 +46,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end
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
it 'returns image hash' do
......@@ -68,7 +68,7 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#image' 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
......@@ -80,7 +80,7 @@ describe Gitlab::Ci::Config::Entry::Image do
context 'when configuration has ports' do
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(:image_ports) { false }
......@@ -112,7 +112,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end
context 'when entry value is not correct' do
let(:config) { ['ruby:2.2'] }
let(:config) { ['ruby:2.7'] }
describe '#errors' do
it 'saves errors' do
......@@ -129,7 +129,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end
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
it 'saves errors' do
......
......@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Root do
let(:hash) do
{
before_script: %w(ls pwd),
image: 'ruby:2.2',
image: 'ruby:2.7',
default: {},
services: ['postgres:9.1', 'mysql:5.5'],
variables: { VAR: 'root' },
......@@ -124,7 +124,7 @@ describe Gitlab::Ci::Config::Entry::Root do
{ name: :rspec,
script: %w[rspec ls],
before_script: %w(ls pwd),
image: { name: 'ruby:2.2' },
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
......@@ -138,7 +138,7 @@ describe Gitlab::Ci::Config::Entry::Root do
{ name: :spinach,
before_script: [],
script: %w[spinach],
image: { name: 'ruby:2.2' },
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
......@@ -154,7 +154,7 @@ describe Gitlab::Ci::Config::Entry::Root do
before_script: [],
script: ["make changelog | tee 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" }],
cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push" },
only: { refs: %w(branches tags) },
......@@ -173,7 +173,7 @@ describe Gitlab::Ci::Config::Entry::Root do
{ before_script: %w(ls pwd),
after_script: ['make clean'],
default: {
image: 'ruby:2.1',
image: 'ruby:2.7',
services: ['postgres:9.1', 'mysql:5.5']
},
variables: { VAR: 'root' },
......@@ -200,7 +200,7 @@ describe Gitlab::Ci::Config::Entry::Root do
rspec: { name: :rspec,
script: %w[rspec ls],
before_script: %w(ls pwd),
image: { name: 'ruby:2.1' },
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
......@@ -212,7 +212,7 @@ describe Gitlab::Ci::Config::Entry::Root do
spinach: { name: :spinach,
before_script: [],
script: %w[spinach],
image: { name: 'ruby:2.1' },
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
......
......@@ -71,7 +71,7 @@ describe Gitlab::Ci::Config::External::File::Project do
let(:root_ref_sha) { project.repository.root_ref_sha }
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
it 'returns true' do
......@@ -96,7 +96,7 @@ describe Gitlab::Ci::Config::External::File::Project do
let(:ref_sha) { project.commit('master').sha }
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
it 'returns true' do
......
......@@ -15,7 +15,7 @@ describe Gitlab::Ci::Config::External::Mapper do
let(:file_content) do
<<~HEREDOC
image: 'ruby:2.2'
image: 'ruby:2.7'
HEREDOC
end
......@@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the string is a local file' do
let(:values) do
{ include: local_file,
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns File instances' do
......@@ -46,7 +46,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a local file hash' do
let(:values) do
{ include: { 'local' => local_file },
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns File instances' do
......@@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the string is a remote file' do
let(:values) do
{ include: remote_url, image: 'ruby:2.2' }
{ include: remote_url, image: 'ruby:2.7' }
end
it 'returns File instances' do
......@@ -69,7 +69,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a remote file hash' do
let(:values) do
{ include: { 'remote' => remote_url },
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns File instances' do
......@@ -81,7 +81,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a template file hash' do
let(:values) do
{ include: { 'template' => template_file },
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns File instances' do
......@@ -93,7 +93,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when the key is a hash of file and remote' do
let(:values) do
{ include: { 'local' => local_file, 'remote' => remote_url },
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns ambigious specification error' do
......@@ -105,7 +105,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context "when 'include' is defined as an array" do
let(:values) do
{ include: [remote_url, local_file],
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns Files instances' do
......@@ -117,7 +117,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context "when 'include' is defined as an array of hashes" do
let(:values) do
{ include: [{ remote: remote_url }, { local: local_file }],
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns Files instances' do
......@@ -128,7 +128,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context 'when it has ambigious match' do
let(:values) do
{ include: [{ remote: remote_url, local: local_file }],
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'returns ambigious specification error' do
......@@ -140,7 +140,7 @@ describe Gitlab::Ci::Config::External::Mapper do
context "when 'include' is not defined" do
let(:values) do
{
image: 'ruby:2.2'
image: 'ruby:2.7'
}
end
......@@ -155,7 +155,7 @@ describe Gitlab::Ci::Config::External::Mapper do
{ 'local' => local_file },
{ 'local' => local_file }
],
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
it 'raises an exception' do
......@@ -169,7 +169,7 @@ describe Gitlab::Ci::Config::External::Mapper do
{ 'local' => local_file },
{ 'remote' => remote_url }
],
image: 'ruby:2.2' }
image: 'ruby:2.7' }
end
before do
......
......@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::External::Processor do
subject { processor.perform }
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
expect(processor.perform).to eq(values)
......@@ -32,7 +32,7 @@ describe Gitlab::Ci::Config::External::Processor do
end
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
expect { processor.perform }.to raise_error(
......@@ -44,7 +44,7 @@ describe Gitlab::Ci::Config::External::Processor do
context 'when an invalid remote file is defined' do
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
stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error'))
......@@ -60,7 +60,7 @@ describe Gitlab::Ci::Config::External::Processor 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(:values) { { include: remote_file, image: 'ruby:2.2' } }
let(:values) { { include: remote_file, image: 'ruby:2.7' } }
let(:external_file_content) do
<<-HEREDOC
before_script:
......@@ -94,7 +94,7 @@ describe Gitlab::Ci::Config::External::Processor do
end
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
<<-HEREDOC
before_script:
......@@ -131,7 +131,7 @@ describe Gitlab::Ci::Config::External::Processor do
let(:values) do
{
include: external_files,
image: 'ruby:2.2'
image: 'ruby:2.7'
}
end
......@@ -163,7 +163,7 @@ describe Gitlab::Ci::Config::External::Processor do
end
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 ////' }
......@@ -185,7 +185,7 @@ describe Gitlab::Ci::Config::External::Processor do
let(:values) do
{
include: remote_file,
image: 'ruby:2.2'
image: 'ruby:2.7'
}
end
......@@ -198,7 +198,7 @@ describe Gitlab::Ci::Config::External::Processor do
it 'takes precedence' do
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
......@@ -208,7 +208,7 @@ describe Gitlab::Ci::Config::External::Processor do
include: [
{ local: '/local/file.yml' }
],
image: 'ruby:2.2'
image: 'ruby:2.7'
}
end
......
......@@ -20,7 +20,7 @@ describe Gitlab::Ci::Config do
context 'when config is valid' do
let(:yml) do
<<-EOS
image: ruby:2.2
image: ruby:2.7
rspec:
script:
......@@ -32,7 +32,7 @@ describe Gitlab::Ci::Config do
describe '#to_hash' do
it 'returns hash created from string' do
hash = {
image: 'ruby:2.2',
image: 'ruby:2.7',
rspec: {
script: ['gem install rspec',
'rspec']
......@@ -85,7 +85,7 @@ describe Gitlab::Ci::Config do
context 'when using extendable hash' do
let(:yml) do
<<-EOS
image: ruby:2.2
image: ruby:2.7
rspec:
script: rspec
......@@ -98,7 +98,7 @@ describe Gitlab::Ci::Config do
it 'correctly extends the hash' do
hash = {
image: 'ruby:2.2',
image: 'ruby:2.7',
rspec: { script: 'rspec' },
test: {
extends: 'rspec',
......@@ -188,7 +188,7 @@ describe Gitlab::Ci::Config do
let(:yml) do
<<-EOS
image:
name: ruby:2.2
name: ruby:2.7
ports:
- 80
EOS
......@@ -202,12 +202,12 @@ describe Gitlab::Ci::Config do
context 'in the job image' do
let(:yml) do
<<-EOS
image: ruby:2.2
image: ruby:2.7
test:
script: rspec
image:
name: ruby:2.2
name: ruby:2.7
ports:
- 80
EOS
......@@ -221,11 +221,11 @@ describe Gitlab::Ci::Config do
context 'in the services' do
let(:yml) do
<<-EOS
image: ruby:2.2
image: ruby:2.7
test:
script: rspec
image: ruby:2.2
image: ruby:2.7
services:
- name: test
alias: test
......@@ -266,7 +266,7 @@ describe Gitlab::Ci::Config do
- #{local_location}
- #{remote_location}
image: ruby:2.2
image: ruby:2.7
HEREDOC
end
......@@ -296,7 +296,7 @@ describe Gitlab::Ci::Config do
}
composed_hash = {
before_script: before_script_values,
image: "ruby:2.2",
image: "ruby:2.7",
rspec: { script: ["bundle exec rspec"] },
variables: variables
}
......@@ -381,7 +381,7 @@ describe Gitlab::Ci::Config do
include:
- #{remote_location}
image: ruby:2.2
image: ruby:2.7
HEREDOC
end
......@@ -392,7 +392,7 @@ describe Gitlab::Ci::Config do
end
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
......
......@@ -665,7 +665,7 @@ module Gitlab
describe "Image and service handling" do
context "when extended docker configuration is used" 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",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }],
......@@ -683,7 +683,7 @@ module Gitlab
options: {
before_script: ["pwd"],
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" },
{ name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }]
......@@ -696,7 +696,7 @@ module Gitlab
end
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"],
before_script: ["pwd"],
rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
......@@ -731,7 +731,7 @@ module Gitlab
context "when etended docker configuration is not used" 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"],
before_script: ["pwd"],
rspec: { script: "rspec" } })
......@@ -747,7 +747,7 @@ module Gitlab
options: {
before_script: ["pwd"],
script: ["rspec"],
image: { name: "ruby:2.1" },
image: { name: "ruby:2.7" },
services: [{ name: "mysql" }, { name: "docker:dind" }]
},
allow_failure: false,
......@@ -758,7 +758,7 @@ module Gitlab
end
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"],
before_script: ["pwd"],
rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
......@@ -1292,7 +1292,7 @@ module Gitlab
describe "Artifacts" do
it "returns artifacts when defined" do
config = YAML.dump({
image: "ruby:2.1",
image: "ruby:2.7",
services: ["mysql"],
before_script: ["pwd"],
rspec: {
......@@ -1318,7 +1318,7 @@ module Gitlab
options: {
before_script: ["pwd"],
script: ["rspec"],
image: { name: "ruby:2.1" },
image: { name: "ruby:2.7" },
services: [{ name: "mysql" }],
artifacts: {
name: "custom_name",
......@@ -1945,7 +1945,7 @@ module Gitlab
context 'when hidden job have a script definition' do
let(:config) do
YAML.dump({
'.hidden_job' => { image: 'ruby:2.1', script: 'test' },
'.hidden_job' => { image: 'ruby:2.7', script: 'test' },
'normal_job' => { script: 'test' }
})
end
......@@ -1956,7 +1956,7 @@ module Gitlab
context "when hidden job doesn't have a script definition" do
let(:config) do
YAML.dump({
'.hidden_job' => { image: 'ruby:2.1' },
'.hidden_job' => { image: 'ruby:2.7' },
'normal_job' => { script: 'test' }
})
end
......
......@@ -6,7 +6,7 @@ describe Gitlab::Config::Loader::Yaml do
let(:loader) { described_class.new(yml) }
context 'when yaml syntax is correct' do
let(:yml) { 'image: ruby:2.2' }
let(:yml) { 'image: ruby:2.7' }
describe '#valid?' do
it 'returns true' do
......@@ -16,7 +16,7 @@ describe Gitlab::Config::Loader::Yaml do
describe '#load!' 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
......
......@@ -55,9 +55,11 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do
let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") }
let(:result) { exporter.save }
it 'creates the repository from the bundle' do
before do
expect(exporter.save).to be_truthy
end
it 'creates the repository from the bundle' do
expect(snippet.repository_exists?).to be_falsey
expect(snippet.snippet_repository).to be_nil
expect(snippet.repository).to receive(:create_from_bundle).and_call_original
......@@ -66,5 +68,14 @@ describe Gitlab::ImportExport::SnippetRepoRestorer do
expect(snippet.repository_exists?).to be_truthy
expect(snippet.snippet_repository).not_to be_nil
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
......@@ -1882,7 +1882,7 @@ describe Ci::Build do
describe '#options' do
let(:options) do
{
image: "ruby:2.1",
image: "ruby:2.7",
services: ["postgres"],
script: ["ls -a"]
}
......@@ -1893,11 +1893,11 @@ describe Ci::Build do
end
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
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
context 'when ci_build_metadata_config is set' do
......
......@@ -84,6 +84,10 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
describe '#calculate_reactive_cache' 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] }
it 'sets commit status to :error when status is 500' do
......@@ -103,13 +107,25 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
is_expected.to eq('Great Success')
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
def stub_request(status: 200, body: nil)
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)
.to_return(status: status,
......
......@@ -106,6 +106,10 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do
WebMock.stub_request(:get, commit_status_path)
.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
......
......@@ -16,7 +16,7 @@ describe SnippetRepository do
describe '.find_snippet' do
it 'finds snippet by disk path' do
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)
end
......
......@@ -567,18 +567,21 @@ describe Snippet do
describe '#track_snippet_repository' do
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
it 'creates a new entry' do
expect { snippet.track_snippet_repository }.to change(snippet, :snippet_repository)
expect { subject }.to change(snippet, :snippet_repository)
end
it 'tracks the snippet storage location' do
snippet.track_snippet_repository
subject
expect(snippet.snippet_repository).to have_attributes(
disk_path: snippet.disk_path,
shard_name: snippet.repository_storage
shard_name: shard_name
)
end
end
......@@ -586,21 +589,20 @@ describe Snippet do
context 'when a tracking entry exists' do
let!(:snippet) { create(: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
expect { snippet.track_snippet_repository }.not_to change(snippet, :snippet_repository)
expect { subject }.not_to change(snippet, :snippet_repository)
end
it 'updates the snippet storage location' do
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(
disk_path: 'fancy/new/path',
shard_name: 'foo'
shard_name: shard_name
)
end
end
......@@ -609,19 +611,31 @@ describe Snippet do
describe '#create_repository' do
let(:snippet) { create(:snippet) }
subject { snippet.create_repository }
it 'creates the repository' do
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
end
it 'tracks snippet repository' do
expect do
snippet.create_repository
subject
end.to change(SnippetRepository, :count).by(1)
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
let!(:snippet) { create(:snippet, :repository) }
......
......@@ -29,7 +29,7 @@ describe API::Lint do
end
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(json_response['status']).to eq('invalid')
......
......@@ -523,7 +523,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_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,
'alias' => nil, 'command' => nil, 'ports' => [] },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
......
......@@ -555,7 +555,7 @@ describe Ci::CreatePipelineService do
let(:ci_yaml) do
<<-EOS
image:
name: ruby:2.2
name: ruby:2.7
ports:
- 80
EOS
......@@ -567,12 +567,12 @@ describe Ci::CreatePipelineService do
context 'in the job image' do
let(:ci_yaml) do
<<-EOS
image: ruby:2.2
image: ruby:2.7
test:
script: rspec
image:
name: ruby:2.2
name: ruby:2.7
ports:
- 80
EOS
......@@ -584,11 +584,11 @@ describe Ci::CreatePipelineService do
context 'in the service' do
let(:ci_yaml) do
<<-EOS
image: ruby:2.2
image: ruby:2.7
test:
script: rspec
image: ruby:2.2
image: ruby:2.7
services:
- name: test
ports:
......
......@@ -41,7 +41,8 @@ describe Projects::ContainerRepository::CleanupTagsService do
let(:params) { {} }
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: [])
end
......@@ -49,15 +50,10 @@ describe Projects::ContainerRepository::CleanupTagsService do
context 'when regex matching everything is specified' do
shared_examples 'removes all matches' do
it 'does remove B* and C' do
# The :A cannot be removed as config is shared with :latest
# The :E cannot be removed as it does not have valid manifest
it 'does remove all tags except latest' do
expect_delete(%w(A Ba Bb C D E))
expect_delete('sha256:configB').twice
expect_delete('sha256:configC')
expect_delete('sha256:configD')
is_expected.to include(status: :success, deleted: %w(D Bb Ba C))
is_expected.to include(status: :success, deleted: %w(A Ba Bb C D E))
end
end
......@@ -82,10 +78,9 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
it 'does remove C and D' do
expect_delete('sha256:configC')
expect_delete('sha256:configD')
expect_delete(%w(C D))
is_expected.to include(status: :success, deleted: %w(D C))
is_expected.to include(status: :success, deleted: %w(C D))
end
context 'with overriding allow regex' do
......@@ -95,7 +90,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
it 'does not remove C' do
expect_delete('sha256:configD')
expect_delete(%w(D))
is_expected.to include(status: :success, deleted: %w(D))
end
......@@ -108,36 +103,52 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
it 'does not remove C' do
expect_delete('sha256:configD')
expect_delete(%w(D))
is_expected.to include(status: :success, deleted: %w(D))
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
{ 'name_regex_delete' => 'Ba' }
{ 'name_regex_delete' => '.*',
'name_regex_keep' => 'B.*' }
end
it 'does not remove the tag' do
# Issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/21405
it 'does not remove B*' do
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
context 'with allow regex value' do
context 'when keeping only N tags' do
let(:params) do
{ 'name_regex_delete' => '.*',
'name_regex_keep' => 'B.*' }
{ 'name_regex' => 'A|B.*|C',
'keep_n' => 1 }
end
it 'does not remove B*' do
expect_delete('sha256:configC')
expect_delete('sha256:configD')
it 'sorts tags by date' do
expect_delete(%w(Bb Ba C))
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
......@@ -147,10 +158,10 @@ describe Projects::ContainerRepository::CleanupTagsService do
'keep_n' => 3 }
end
it 'does remove C as it is oldest' do
expect_delete('sha256:configC')
it 'does remove B* and C as they are the oldest' do
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
......@@ -161,10 +172,9 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
it 'does remove B* and C as they are older than 1 day' do
expect_delete('sha256:configB').twice
expect_delete('sha256:configC')
expect_delete(%w(Ba Bb C))
is_expected.to include(status: :success, deleted: %w(Bb Ba C))
is_expected.to include(status: :success, deleted: %w(Ba Bb C))
end
end
......@@ -176,8 +186,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
it 'does remove B* and C' do
expect_delete('sha256:configB').twice
expect_delete('sha256:configC')
expect_delete(%w(Bb Ba C))
is_expected.to include(status: :success, deleted: %w(Bb Ba C))
end
......@@ -195,8 +204,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
it 'succeeds without a user' do
expect_delete('sha256:configB').twice
expect_delete('sha256:configC')
expect_delete(%w(Bb Ba C))
is_expected.to include(status: :success, deleted: %w(Bb Ba C))
end
......@@ -238,9 +246,14 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
end
def expect_delete(digest)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag_by_digest)
.with(repository.path, digest) { true }
def expect_delete(tags)
expect(Projects::ContainerRepository::DeleteTagsService)
.to receive(:new)
.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
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