Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9c8e8b5ffc
commit
f602da84d1
|
@ -1 +1 @@
|
||||||
76dabc8174f7978025f48adcfab0a19c85416531
|
ca638e23ca921cf2f2f3cdc8a6ff033af667170b
|
||||||
|
|
|
@ -1,40 +1,48 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTabs, GlTab, GlBadge, GlButton } from '@gitlab/ui';
|
import { GlTabs, GlTab, GlBadge, GlButton } from '@gitlab/ui';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import { queryToObject } from '~/lib/utils/url_utility';
|
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES, ACTIVE_TAB_QUERY_PARAM_NAME } from '../constants';
|
import { queryToObject } from '~/lib/utils/url_utility';
|
||||||
|
import {
|
||||||
|
MEMBER_TYPES,
|
||||||
|
ACTIVE_TAB_QUERY_PARAM_NAME,
|
||||||
|
TAB_QUERY_PARAM_VALUES,
|
||||||
|
EE_TABS,
|
||||||
|
} from 'ee_else_ce/members/constants';
|
||||||
import MembersApp from './app.vue';
|
import MembersApp from './app.vue';
|
||||||
|
|
||||||
const countComputed = (state, namespace) => state[namespace]?.pagination?.totalItems || 0;
|
const countComputed = (state, namespace) => state[namespace]?.pagination?.totalItems || 0;
|
||||||
|
|
||||||
|
export const TABS = [
|
||||||
|
{
|
||||||
|
namespace: MEMBER_TYPES.user,
|
||||||
|
title: __('Members'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: MEMBER_TYPES.group,
|
||||||
|
title: __('Groups'),
|
||||||
|
attrs: { 'data-qa-selector': 'groups_list_tab' },
|
||||||
|
queryParamValue: TAB_QUERY_PARAM_VALUES.group,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: MEMBER_TYPES.invite,
|
||||||
|
title: __('Invited'),
|
||||||
|
canManageMembersPermissionsRequired: true,
|
||||||
|
queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: MEMBER_TYPES.accessRequest,
|
||||||
|
title: __('Access requests'),
|
||||||
|
canManageMembersPermissionsRequired: true,
|
||||||
|
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
|
||||||
|
},
|
||||||
|
...EE_TABS,
|
||||||
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MembersTabs',
|
name: 'MembersTabs',
|
||||||
ACTIVE_TAB_QUERY_PARAM_NAME,
|
ACTIVE_TAB_QUERY_PARAM_NAME,
|
||||||
TABS: [
|
TABS,
|
||||||
{
|
|
||||||
namespace: MEMBER_TYPES.user,
|
|
||||||
title: __('Members'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namespace: MEMBER_TYPES.group,
|
|
||||||
title: __('Groups'),
|
|
||||||
attrs: { 'data-qa-selector': 'groups_list_tab' },
|
|
||||||
queryParamValue: TAB_QUERY_PARAM_VALUES.group,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namespace: MEMBER_TYPES.invite,
|
|
||||||
title: __('Invited'),
|
|
||||||
canManageMembersPermissionsRequired: true,
|
|
||||||
queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namespace: MEMBER_TYPES.accessRequest,
|
|
||||||
title: __('Access requests'),
|
|
||||||
canManageMembersPermissionsRequired: true,
|
|
||||||
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
|
components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
|
||||||
inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
|
inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
|
||||||
data() {
|
data() {
|
||||||
|
@ -43,20 +51,17 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState(
|
||||||
userCount(state) {
|
Object.values(MEMBER_TYPES).reduce((getters, memberType) => {
|
||||||
return countComputed(state, MEMBER_TYPES.user);
|
return {
|
||||||
},
|
...getters,
|
||||||
groupCount(state) {
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
return countComputed(state, MEMBER_TYPES.group);
|
[`${memberType}Count`](state) {
|
||||||
},
|
return countComputed(state, memberType);
|
||||||
inviteCount(state) {
|
},
|
||||||
return countComputed(state, MEMBER_TYPES.invite);
|
};
|
||||||
},
|
}, {}),
|
||||||
accessRequestCount(state) {
|
),
|
||||||
return countComputed(state, MEMBER_TYPES.accessRequest);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
urlParams() {
|
urlParams() {
|
||||||
return Object.keys(queryToObject(window.location.search, { gatherArrays: true }));
|
return Object.keys(queryToObject(window.location.search, { gatherArrays: true }));
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,13 @@ import UserAvatar from '../avatars/user_avatar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MemberAvatar',
|
name: 'MemberAvatar',
|
||||||
components: { UserAvatar, InviteAvatar, GroupAvatar, AccessRequestAvatar: UserAvatar },
|
components: {
|
||||||
|
UserAvatar,
|
||||||
|
InviteAvatar,
|
||||||
|
GroupAvatar,
|
||||||
|
AccessRequestAvatar: UserAvatar,
|
||||||
|
BannedAvatar: UserAvatar,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
memberType: {
|
memberType: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { MEMBER_TYPES } from '../../constants';
|
import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
|
||||||
import {
|
import {
|
||||||
isGroup,
|
isGroup,
|
||||||
isDirectMember,
|
isDirectMember,
|
||||||
|
|
|
@ -3,6 +3,12 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
|
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||||
|
|
||||||
|
// Overridden in EE
|
||||||
|
export const EE_APP_OPTIONS = {};
|
||||||
|
|
||||||
|
// Overridden in EE
|
||||||
|
export const EE_TABS = [];
|
||||||
|
|
||||||
export const FIELD_KEY_ACCOUNT = 'account';
|
export const FIELD_KEY_ACCOUNT = 'account';
|
||||||
export const FIELD_KEY_SOURCE = 'source';
|
export const FIELD_KEY_SOURCE = 'source';
|
||||||
export const FIELD_KEY_GRANTED = 'granted';
|
export const FIELD_KEY_GRANTED = 'granted';
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { GlToast } from '@gitlab/ui';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { parseDataAttributes } from '~/members/utils';
|
import { parseDataAttributes } from '~/members/utils';
|
||||||
|
import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
|
||||||
import MembersTabs from './components/members_tabs.vue';
|
import MembersTabs from './components/members_tabs.vue';
|
||||||
import { MEMBER_TYPES } from './constants';
|
|
||||||
import membersStore from './store';
|
import membersStore from './store';
|
||||||
|
|
||||||
export const initMembersApp = (el, options) => {
|
export const initMembersApp = (el, options) => {
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<settings-block :collapsible="false">
|
<settings-block data-testid="container-expiration-policy-project-settings">
|
||||||
<template #title> {{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</template>
|
<template #title> {{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -30,6 +30,11 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -46,5 +51,10 @@ export default {
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</option>
|
</option>
|
||||||
</gl-form-select>
|
</gl-form-select>
|
||||||
|
<template v-if="description" #description>
|
||||||
|
<span data-testid="description" class="gl-text-gray-400">
|
||||||
|
{{ description }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</gl-form-group>
|
</gl-form-group>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import { GlAlert, GlSprintf } from '@gitlab/ui';
|
||||||
|
import {
|
||||||
|
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||||
|
PACKAGES_CLEANUP_POLICY_TITLE,
|
||||||
|
PACKAGES_CLEANUP_POLICY_DESCRIPTION,
|
||||||
|
} from '~/packages_and_registries/settings/project/constants';
|
||||||
|
import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql';
|
||||||
|
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||||
|
|
||||||
|
import PackagesCleanupPolicyForm from './packages_cleanup_policy_form.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SettingsBlock,
|
||||||
|
GlAlert,
|
||||||
|
GlSprintf,
|
||||||
|
PackagesCleanupPolicyForm,
|
||||||
|
},
|
||||||
|
inject: ['projectPath'],
|
||||||
|
i18n: {
|
||||||
|
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||||
|
PACKAGES_CLEANUP_POLICY_TITLE,
|
||||||
|
PACKAGES_CLEANUP_POLICY_DESCRIPTION,
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
packagesCleanupPolicy: {
|
||||||
|
query: packagesCleanupPolicyQuery,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
projectPath: this.projectPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update: (data) => data.project?.packagesCleanupPolicy || {},
|
||||||
|
error(e) {
|
||||||
|
this.fetchSettingsError = e;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fetchSettingsError: false,
|
||||||
|
packagesCleanupPolicy: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<settings-block>
|
||||||
|
<template #title> {{ $options.i18n.PACKAGES_CLEANUP_POLICY_TITLE }}</template>
|
||||||
|
<template #description>
|
||||||
|
<span data-testid="description">
|
||||||
|
<gl-sprintf :message="$options.i18n.PACKAGES_CLEANUP_POLICY_DESCRIPTION" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<gl-alert v-if="fetchSettingsError" variant="warning" :dismissible="false">
|
||||||
|
<gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
|
||||||
|
</gl-alert>
|
||||||
|
<packages-cleanup-policy-form
|
||||||
|
v-else
|
||||||
|
v-model="packagesCleanupPolicy"
|
||||||
|
:is-loading="$apollo.queries.packagesCleanupPolicy.loading"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</settings-block>
|
||||||
|
</template>
|
|
@ -0,0 +1,137 @@
|
||||||
|
<script>
|
||||||
|
import { GlButton } from '@gitlab/ui';
|
||||||
|
import {
|
||||||
|
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||||
|
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||||
|
SET_CLEANUP_POLICY_BUTTON,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
|
||||||
|
} from '~/packages_and_registries/settings/project/constants';
|
||||||
|
import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql';
|
||||||
|
import { formOptionsGenerator } from '~/packages_and_registries/settings/project/utils';
|
||||||
|
import Tracking from '~/tracking';
|
||||||
|
import ExpirationDropdown from './expiration_dropdown.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlButton,
|
||||||
|
ExpirationDropdown,
|
||||||
|
},
|
||||||
|
mixins: [Tracking.mixin()],
|
||||||
|
inject: ['projectPath'],
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formOptions: formOptionsGenerator(),
|
||||||
|
i18n: {
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
|
||||||
|
SET_CLEANUP_POLICY_BUTTON,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tracking: {
|
||||||
|
label: 'packages_cleanup_policies',
|
||||||
|
},
|
||||||
|
mutationLoading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
prefilledForm() {
|
||||||
|
return {
|
||||||
|
...this.value,
|
||||||
|
keepNDuplicatedPackageFiles: this.findDefaultOption(
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
showLoadingIcon() {
|
||||||
|
return this.isLoading || this.mutationLoading;
|
||||||
|
},
|
||||||
|
isSubmitButtonDisabled() {
|
||||||
|
return this.showLoadingIcon;
|
||||||
|
},
|
||||||
|
isFieldDisabled() {
|
||||||
|
return this.showLoadingIcon;
|
||||||
|
},
|
||||||
|
mutationVariables() {
|
||||||
|
return {
|
||||||
|
projectPath: this.projectPath,
|
||||||
|
keepNDuplicatedPackageFiles: this.prefilledForm.keepNDuplicatedPackageFiles,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
findDefaultOption(option) {
|
||||||
|
return this.value[option] || this.$options.formOptions[option].find((f) => f.default)?.key;
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
this.track('submit_packages_cleanup_form');
|
||||||
|
this.mutationLoading = true;
|
||||||
|
return this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: updatePackagesCleanupPolicyMutation,
|
||||||
|
variables: {
|
||||||
|
input: this.mutationVariables,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
const [errorMessage] = data?.updatePackagesCleanupPolicy?.errors ?? [];
|
||||||
|
if (errorMessage) {
|
||||||
|
throw errorMessage;
|
||||||
|
} else {
|
||||||
|
this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.mutationLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onModelChange(newValue, model) {
|
||||||
|
this.$emit('input', { ...this.value, [model]: newValue });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form ref="form-element" @submit.prevent="submit">
|
||||||
|
<div class="gl-md-max-w-50p">
|
||||||
|
<expiration-dropdown
|
||||||
|
v-model="prefilledForm.keepNDuplicatedPackageFiles"
|
||||||
|
:disabled="isFieldDisabled"
|
||||||
|
:form-options="$options.formOptions.keepNDuplicatedPackageFiles"
|
||||||
|
:label="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL"
|
||||||
|
:description="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION"
|
||||||
|
name="keep-n-duplicated-package-files"
|
||||||
|
data-testid="keep-n-duplicated-package-files-dropdown"
|
||||||
|
@input="onModelChange($event, 'keepNDuplicatedPackageFiles')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="gl-mt-7 gl-display-flex gl-align-items-center">
|
||||||
|
<gl-button
|
||||||
|
data-testid="save-button"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isSubmitButtonDisabled"
|
||||||
|
:loading="showLoadingIcon"
|
||||||
|
category="primary"
|
||||||
|
variant="confirm"
|
||||||
|
class="js-no-auto-disable gl-mr-4"
|
||||||
|
>
|
||||||
|
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
|
||||||
|
</gl-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
|
@ -1,15 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import ContainerExpirationPolicy from './container_expiration_policy.vue';
|
import ContainerExpirationPolicy from './container_expiration_policy.vue';
|
||||||
|
import PackagesCleanupPolicy from './packages_cleanup_policy.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ContainerExpirationPolicy,
|
ContainerExpirationPolicy,
|
||||||
|
PackagesCleanupPolicy,
|
||||||
},
|
},
|
||||||
|
inject: ['showContainerRegistrySettings', 'showPackageRegistrySettings'],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section data-testid="registry-settings-app">
|
<div>
|
||||||
<container-expiration-policy />
|
<packages-cleanup-policy v-if="showPackageRegistrySettings" />
|
||||||
</section>
|
<container-expiration-policy v-if="showContainerRegistrySettings" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -55,6 +55,31 @@ export const EXPIRATION_POLICY_FOOTER_NOTE = s__(
|
||||||
'ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time',
|
'ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const PACKAGES_CLEANUP_POLICY_TITLE = s__(
|
||||||
|
'PackageRegistry|Manage storage used by package assets',
|
||||||
|
);
|
||||||
|
export const PACKAGES_CLEANUP_POLICY_DESCRIPTION = s__(
|
||||||
|
'PackageRegistry|When a package with same name and version is uploaded to the registry, more assets are added to the package. To save storage space, keep only the most recent assets.',
|
||||||
|
);
|
||||||
|
export const KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL = s__(
|
||||||
|
'PackageRegistry|Number of duplicate assets to keep',
|
||||||
|
);
|
||||||
|
export const KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION = s__(
|
||||||
|
'PackageRegistry|Examples of assets include .pom & .jar files',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME = 'keepNDuplicatedPackageFiles';
|
||||||
|
|
||||||
|
export const KEEP_N_DUPLICATED_PACKAGE_FILES_OPTIONS = [
|
||||||
|
{ key: 'ONE_PACKAGE_FILE', label: 1, default: false },
|
||||||
|
{ key: 'TEN_PACKAGE_FILES', label: 10, default: false },
|
||||||
|
{ key: 'TWENTY_PACKAGE_FILES', label: 20, default: false },
|
||||||
|
{ key: 'THIRTY_PACKAGE_FILES', label: 30, default: false },
|
||||||
|
{ key: 'FORTY_PACKAGE_FILES', label: 40, default: false },
|
||||||
|
{ key: 'FIFTY_PACKAGE_FILES', label: 50, default: false },
|
||||||
|
{ key: 'ALL_PACKAGE_FILES', label: __('All'), default: true },
|
||||||
|
];
|
||||||
|
|
||||||
export const KEEP_N_OPTIONS = [
|
export const KEEP_N_OPTIONS = [
|
||||||
{ key: 'ONE_TAG', variable: 1, default: false },
|
{ key: 'ONE_TAG', variable: 1, default: false },
|
||||||
{ key: 'FIVE_TAGS', variable: 5, default: false },
|
{ key: 'FIVE_TAGS', variable: 5, default: false },
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
fragment PackagesCleanupPolicyFields on PackagesCleanupPolicy {
|
||||||
|
keepNDuplicatedPackageFiles
|
||||||
|
nextRunAt
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
#import "../fragments/packages_cleanup_policy.fragment.graphql"
|
||||||
|
|
||||||
|
mutation updatePackagesCleanupPolicy($input: UpdatePackagesCleanupPolicyInput!) {
|
||||||
|
updatePackagesCleanupPolicy(input: $input) {
|
||||||
|
packagesCleanupPolicy {
|
||||||
|
...PackagesCleanupPolicyFields
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
#import "../fragments/packages_cleanup_policy.fragment.graphql"
|
||||||
|
|
||||||
|
query getProjectPackagesCleanupPolicy($projectPath: ID!) {
|
||||||
|
project(fullPath: $projectPath) {
|
||||||
|
id
|
||||||
|
packagesCleanupPolicy {
|
||||||
|
...PackagesCleanupPolicyFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ export default () => {
|
||||||
adminSettingsPath,
|
adminSettingsPath,
|
||||||
tagsRegexHelpPagePath,
|
tagsRegexHelpPagePath,
|
||||||
helpPagePath,
|
helpPagePath,
|
||||||
|
showContainerRegistrySettings,
|
||||||
|
showPackageRegistrySettings,
|
||||||
} = el.dataset;
|
} = el.dataset;
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
|
@ -34,6 +36,8 @@ export default () => {
|
||||||
adminSettingsPath,
|
adminSettingsPath,
|
||||||
tagsRegexHelpPagePath,
|
tagsRegexHelpPagePath,
|
||||||
helpPagePath,
|
helpPagePath,
|
||||||
|
showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings),
|
||||||
|
showPackageRegistrySettings: parseBoolean(showPackageRegistrySettings),
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement('registry-settings-app', {});
|
return createElement('registry-settings-app', {});
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { n__ } from '~/locale';
|
import { n__ } from '~/locale';
|
||||||
import { KEEP_N_OPTIONS, CADENCE_OPTIONS, OLDER_THAN_OPTIONS } from './constants';
|
import {
|
||||||
|
KEEP_N_OPTIONS,
|
||||||
|
CADENCE_OPTIONS,
|
||||||
|
OLDER_THAN_OPTIONS,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_OPTIONS,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
export const findDefaultOption = (options) => {
|
export const findDefaultOption = (options) => {
|
||||||
const item = options.find((o) => o.default);
|
const item = options.find((o) => o.default);
|
||||||
|
@ -25,5 +31,6 @@ export const formOptionsGenerator = () => {
|
||||||
olderThan: optionLabelGenerator(OLDER_THAN_OPTIONS, olderThanTranslationGenerator),
|
olderThan: optionLabelGenerator(OLDER_THAN_OPTIONS, olderThanTranslationGenerator),
|
||||||
cadence: CADENCE_OPTIONS,
|
cadence: CADENCE_OPTIONS,
|
||||||
keepN: optionLabelGenerator(KEEP_N_OPTIONS, keepNTranslationGenerator),
|
keepN: optionLabelGenerator(KEEP_N_OPTIONS, keepNTranslationGenerator),
|
||||||
|
[KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME]: KEEP_N_DUPLICATED_PACKAGE_FILES_OPTIONS,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,12 +5,11 @@ import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||||
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import { initMembersApp } from '~/members';
|
import { initMembersApp } from '~/members';
|
||||||
import { MEMBER_TYPES } from '~/members/constants';
|
import { MEMBER_TYPES, EE_APP_OPTIONS } from 'ee_else_ce/members/constants';
|
||||||
import { groupLinkRequestFormatter } from '~/members/utils';
|
import { groupLinkRequestFormatter } from '~/members/utils';
|
||||||
|
|
||||||
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
|
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
|
||||||
|
const APP_OPTIONS = {
|
||||||
initMembersApp(document.querySelector('.js-group-members-list-app'), {
|
|
||||||
[MEMBER_TYPES.user]: {
|
[MEMBER_TYPES.user]: {
|
||||||
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
|
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
|
||||||
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
|
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
|
||||||
|
@ -61,7 +60,10 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
|
||||||
tableFields: SHARED_FIELDS.concat('requested'),
|
tableFields: SHARED_FIELDS.concat('requested'),
|
||||||
requestFormatter: groupMemberRequestFormatter,
|
requestFormatter: groupMemberRequestFormatter,
|
||||||
},
|
},
|
||||||
});
|
...EE_APP_OPTIONS,
|
||||||
|
};
|
||||||
|
|
||||||
|
initMembersApp(document.querySelector('.js-group-members-list-app'), APP_OPTIONS);
|
||||||
|
|
||||||
initInviteMembersModal();
|
initInviteMembersModal();
|
||||||
initInviteGroupsModal();
|
initInviteGroupsModal();
|
||||||
|
|
|
@ -61,6 +61,10 @@ export default {
|
||||||
GlFormCheckbox,
|
GlFormCheckbox,
|
||||||
GlToggle,
|
GlToggle,
|
||||||
ConfirmDanger,
|
ConfirmDanger,
|
||||||
|
otherProjectSettings: () =>
|
||||||
|
import(
|
||||||
|
'jh_component/pages/projects/shared/permissions/components/other_project_settings.vue'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
mixins: [settingsMixin, glFeatureFlagsMixin()],
|
mixins: [settingsMixin, glFeatureFlagsMixin()],
|
||||||
|
|
||||||
|
@ -905,6 +909,7 @@ export default {
|
||||||
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
|
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
|
||||||
</gl-form-checkbox>
|
</gl-form-checkbox>
|
||||||
</project-setting-row>
|
</project-setting-row>
|
||||||
|
<other-project-settings />
|
||||||
<confirm-danger
|
<confirm-danger
|
||||||
v-if="isVisibilityReduced"
|
v-if="isVisibilityReduced"
|
||||||
button-variant="confirm"
|
button-variant="confirm"
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AwarenessChannel < ApplicationCable::Channel # rubocop:disable Gitlab/NamespacedClass
|
||||||
|
REFRESH_INTERVAL = ENV.fetch("GITLAB_AWARENESS_REFRESH_INTERVAL_SEC", 60)
|
||||||
|
private_constant :REFRESH_INTERVAL
|
||||||
|
|
||||||
|
# Produces a refresh interval value, based of the
|
||||||
|
# GITLAB_AWARENESS_REFRESH_INTERVAL_SEC environment variable or the given
|
||||||
|
# default. Makes sure, that the interval after a jitter is applied, is never
|
||||||
|
# less than half the predefined interval.
|
||||||
|
def self.refresh_interval(range: -10..10)
|
||||||
|
min = REFRESH_INTERVAL / 2.to_f
|
||||||
|
[min.to_i, REFRESH_INTERVAL.to_i + rand(range)].max.seconds
|
||||||
|
end
|
||||||
|
private_class_method :refresh_interval
|
||||||
|
|
||||||
|
# keep clients updated about session membership
|
||||||
|
periodically every: self.refresh_interval do
|
||||||
|
transmit payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribed
|
||||||
|
reject unless valid_subscription?
|
||||||
|
return if subscription_rejected?
|
||||||
|
|
||||||
|
stream_for session, coder: ActiveSupport::JSON
|
||||||
|
|
||||||
|
session.join(current_user)
|
||||||
|
AwarenessChannel.broadcast_to(session, payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribed
|
||||||
|
return if subscription_rejected?
|
||||||
|
|
||||||
|
session.leave(current_user)
|
||||||
|
AwarenessChannel.broadcast_to(session, payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Allows a client to let the server know they are still around. This is not
|
||||||
|
# like a heartbeat mechanism. This can be triggered by any action that results
|
||||||
|
# in a meaningful "presence" update. Like scrolling the screen (debounce),
|
||||||
|
# window becoming active, user starting to type in a text field, etc.
|
||||||
|
def touch
|
||||||
|
session.touch!(current_user)
|
||||||
|
|
||||||
|
transmit payload
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def valid_subscription?
|
||||||
|
current_user.present? && path.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload
|
||||||
|
{ collaborators: collaborators }
|
||||||
|
end
|
||||||
|
|
||||||
|
def collaborators
|
||||||
|
session.online_users_with_last_activity.map do |user, last_activity|
|
||||||
|
collaborator(user, last_activity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def collaborator(user, last_activity)
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
avatar_url: user.avatar_url(size: 36),
|
||||||
|
last_activity: last_activity,
|
||||||
|
last_activity_humanized: ActionController::Base.helpers.distance_of_time_in_words(
|
||||||
|
Time.zone.now, last_activity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def session
|
||||||
|
@session ||= AwarenessSession.for(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def path
|
||||||
|
params[:path]
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,7 +9,7 @@ module Groups::GroupMembersHelper
|
||||||
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
|
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def group_members_app_data(group, members:, invited:, access_requests:, include_relations:, search:)
|
def group_members_app_data(group, members:, invited:, access_requests:, banned:, include_relations:, search:)
|
||||||
{
|
{
|
||||||
user: group_members_list_data(group, members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }),
|
user: group_members_list_data(group, members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }),
|
||||||
group: group_group_links_list_data(group, include_relations, search),
|
group: group_group_links_list_data(group, include_relations, search),
|
||||||
|
|
|
@ -53,4 +53,14 @@ module PackagesHelper
|
||||||
project.container_expiration_policy.nil? &&
|
project.container_expiration_policy.nil? &&
|
||||||
project.container_repositories.exists?
|
project.container_repositories.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show_container_registry_settings(project)
|
||||||
|
Gitlab.config.registry.enabled &&
|
||||||
|
Ability.allowed?(current_user, :admin_container_image, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_package_registry_settings(project)
|
||||||
|
Gitlab.config.packages.enabled &&
|
||||||
|
Ability.allowed?(current_user, :admin_package, project)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -143,17 +143,34 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
id&.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"awareness_session=#{id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def online_users_with_last_activity(threshold: PRESENCE_LIFETIME)
|
||||||
|
users_with_last_activity.filter do |_user, last_activity|
|
||||||
|
user_online?(last_activity, threshold: threshold)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def users
|
def users
|
||||||
User.where(id: user_ids)
|
User.where(id: user_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
def users_with_last_activity
|
def users_with_last_activity
|
||||||
# where in (x, y, [...z]) is a set and does not maintain any order, we need to
|
# where in (x, y, [...z]) is a set and does not maintain any order, we need
|
||||||
# make sure to establish a stable order for both, the pairs returned from
|
# to make sure to establish a stable order for both, the pairs returned from
|
||||||
# redis and the ActiveRecord query. Using IDs in ascending order.
|
# redis and the ActiveRecord query. Using IDs in ascending order.
|
||||||
user_ids, last_activities = user_ids_with_last_activity
|
user_ids, last_activities = user_ids_with_last_activity
|
||||||
.sort_by(&:first)
|
.sort_by(&:first)
|
||||||
.transpose
|
.transpose
|
||||||
|
|
||||||
|
return [] if user_ids.blank?
|
||||||
|
|
||||||
users = User.where(id: user_ids).order(id: :asc)
|
users = User.where(id: user_ids).order(id: :asc)
|
||||||
users.zip(last_activities)
|
users.zip(last_activities)
|
||||||
end
|
end
|
||||||
|
@ -162,6 +179,10 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
|
||||||
|
|
||||||
attr_reader :id
|
attr_reader :id
|
||||||
|
|
||||||
|
def user_online?(last_activity, threshold:)
|
||||||
|
last_activity.to_i + threshold.to_i > Time.zone.now.to_i
|
||||||
|
end
|
||||||
|
|
||||||
# converts session id from hex to integer representation
|
# converts session id from hex to integer representation
|
||||||
def id_i
|
def id_i
|
||||||
Integer(id, 16) if id.present?
|
Integer(id, 16) if id.present?
|
||||||
|
|
|
@ -225,6 +225,10 @@ class ProjectPolicy < BasePolicy
|
||||||
Gitlab.config.registry.enabled
|
Gitlab.config.registry.enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
|
condition :packages_enabled do
|
||||||
|
Gitlab.config.packages.enabled
|
||||||
|
end
|
||||||
|
|
||||||
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
|
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
|
||||||
# not.
|
# not.
|
||||||
rule { guest | admin }.enable :read_project_for_iids
|
rule { guest | admin }.enable :read_project_for_iids
|
||||||
|
@ -795,6 +799,10 @@ class ProjectPolicy < BasePolicy
|
||||||
enable :view_package_registry_project_settings
|
enable :view_package_registry_project_settings
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule { packages_enabled & can?(:admin_package) }.policy do
|
||||||
|
enable :view_package_registry_project_settings
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def user_is_user?
|
def user_is_user?
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
members: @members,
|
members: @members,
|
||||||
invited: @invited_members,
|
invited: @invited_members,
|
||||||
access_requests: @requesters,
|
access_requests: @requesters,
|
||||||
|
banned: @banned || [],
|
||||||
include_relations: @include_relations,
|
include_relations: @include_relations,
|
||||||
search: params[:search_groups]).to_json } }
|
search: params[:search_groups]).to_json } }
|
||||||
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
|
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
keep_n_options: keep_n_options.to_json,
|
keep_n_options: keep_n_options.to_json,
|
||||||
older_than_options: older_than_options.to_json,
|
older_than_options: older_than_options.to_json,
|
||||||
is_admin: current_user&.admin.to_s,
|
is_admin: current_user&.admin.to_s,
|
||||||
|
show_container_registry_settings: show_container_registry_settings(@project).to_s,
|
||||||
|
show_package_registry_settings: show_package_registry_settings(@project).to_s,
|
||||||
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
|
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
|
||||||
enable_historic_entries: container_expiration_policies_historic_entry_enabled?.to_s,
|
enable_historic_entries: container_expiration_policies_historic_entry_enabled?.to_s,
|
||||||
help_page_path: help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'cleanup-policy'),
|
help_page_path: help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'cleanup-policy'),
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UpdateInsertOrUpdateVulnerabilityReadsFunction < Gitlab::Database::Migration[2.0]
|
||||||
|
FUNCTION_NAME = 'insert_or_update_vulnerability_reads'
|
||||||
|
|
||||||
|
enable_lock_retries!
|
||||||
|
|
||||||
|
def up
|
||||||
|
execute(<<~SQL)
|
||||||
|
CREATE OR REPLACE FUNCTION #{FUNCTION_NAME}() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
severity smallint;
|
||||||
|
state smallint;
|
||||||
|
report_type smallint;
|
||||||
|
resolved_on_default_branch boolean;
|
||||||
|
present_on_default_branch boolean;
|
||||||
|
BEGIN
|
||||||
|
IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch, vulnerabilities.present_on_default_branch
|
||||||
|
INTO
|
||||||
|
severity, state, report_type, resolved_on_default_branch, present_on_default_branch
|
||||||
|
FROM
|
||||||
|
vulnerabilities
|
||||||
|
WHERE
|
||||||
|
vulnerabilities.id = NEW.vulnerability_id;
|
||||||
|
|
||||||
|
IF present_on_default_branch IS NOT true THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||||
|
VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id', CAST(NEW.location->'kubernetes_resource'->>'agent_id' AS bigint))
|
||||||
|
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute(<<~SQL)
|
||||||
|
CREATE OR REPLACE FUNCTION #{FUNCTION_NAME}() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
severity smallint;
|
||||||
|
state smallint;
|
||||||
|
report_type smallint;
|
||||||
|
resolved_on_default_branch boolean;
|
||||||
|
BEGIN
|
||||||
|
IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch
|
||||||
|
INTO
|
||||||
|
severity, state, report_type, resolved_on_default_branch
|
||||||
|
FROM
|
||||||
|
vulnerabilities
|
||||||
|
WHERE
|
||||||
|
vulnerabilities.id = NEW.vulnerability_id;
|
||||||
|
|
||||||
|
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||||
|
VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id', CAST(NEW.location->'kubernetes_resource'->>'agent_id' AS bigint))
|
||||||
|
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateVulnerabilityReadsForAnExistingVulnerabilityRecord < Gitlab::Database::Migration[2.0]
|
||||||
|
include Gitlab::Database::SchemaHelpers
|
||||||
|
|
||||||
|
FUNCTION_NAME = 'insert_vulnerability_reads_from_vulnerability'
|
||||||
|
TRIGGER_NAME = 'trigger_insert_vulnerability_reads_from_vulnerability'
|
||||||
|
|
||||||
|
enable_lock_retries!
|
||||||
|
|
||||||
|
def up
|
||||||
|
execute(<<~SQL)
|
||||||
|
CREATE FUNCTION #{FUNCTION_NAME}() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
scanner_id bigint;
|
||||||
|
uuid uuid;
|
||||||
|
location_image text;
|
||||||
|
cluster_agent_id text;
|
||||||
|
casted_cluster_agent_id bigint;
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
v_o.scanner_id, v_o.uuid, v_o.location->>'image', v_o.location->'kubernetes_resource'->>'agent_id', CAST(v_o.location->'kubernetes_resource'->>'agent_id' AS bigint)
|
||||||
|
INTO
|
||||||
|
scanner_id, uuid, location_image, cluster_agent_id, casted_cluster_agent_id
|
||||||
|
FROM
|
||||||
|
vulnerability_occurrences v_o
|
||||||
|
WHERE
|
||||||
|
v_o.vulnerability_id = NEW.id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||||
|
VALUES (NEW.id, NEW.project_id, scanner_id, NEW.report_type, NEW.severity, NEW.state, NEW.resolved_on_default_branch, uuid::uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||||
|
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute(<<~SQL)
|
||||||
|
CREATE TRIGGER #{TRIGGER_NAME}
|
||||||
|
AFTER UPDATE ON vulnerabilities
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (
|
||||||
|
OLD.present_on_default_branch IS NOT true AND NEW.present_on_default_branch IS true
|
||||||
|
)
|
||||||
|
EXECUTE PROCEDURE #{FUNCTION_NAME}();
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_trigger(:vulnerabilities, TRIGGER_NAME)
|
||||||
|
drop_function(FUNCTION_NAME)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,48 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UpdateTriggerUpdateVulnerabilityReadsOnVulnerabilityUpdate < Gitlab::Database::Migration[2.0]
|
||||||
|
include Gitlab::Database::SchemaHelpers
|
||||||
|
|
||||||
|
TRIGGER_NAME = 'trigger_update_vulnerability_reads_on_vulnerability_update'
|
||||||
|
FUNCTION_NAME = 'update_vulnerability_reads_from_vulnerability'
|
||||||
|
|
||||||
|
enable_lock_retries!
|
||||||
|
|
||||||
|
def up
|
||||||
|
drop_trigger(:vulnerabilities, TRIGGER_NAME)
|
||||||
|
|
||||||
|
# If the vulnerability record was not already marked as `present_on_default_branch`,
|
||||||
|
# we shouldn't try to update `vulnerability_records` since there will be no records
|
||||||
|
# anyway.
|
||||||
|
execute(<<~SQL)
|
||||||
|
CREATE TRIGGER #{TRIGGER_NAME}
|
||||||
|
AFTER UPDATE ON vulnerabilities
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (
|
||||||
|
OLD.present_on_default_branch IS TRUE AND
|
||||||
|
(
|
||||||
|
OLD.severity IS DISTINCT FROM NEW.severity OR
|
||||||
|
OLD.state IS DISTINCT FROM NEW.state OR
|
||||||
|
OLD.resolved_on_default_branch IS DISTINCT FROM NEW.resolved_on_default_branch
|
||||||
|
)
|
||||||
|
)
|
||||||
|
EXECUTE PROCEDURE #{FUNCTION_NAME}();
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_trigger(:vulnerabilities, TRIGGER_NAME)
|
||||||
|
|
||||||
|
execute(<<~SQL)
|
||||||
|
CREATE TRIGGER #{TRIGGER_NAME}
|
||||||
|
AFTER UPDATE ON vulnerabilities
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (
|
||||||
|
OLD.severity IS DISTINCT FROM NEW.severity OR
|
||||||
|
OLD.state IS DISTINCT FROM NEW.state OR
|
||||||
|
OLD.resolved_on_default_branch IS DISTINCT FROM NEW.resolved_on_default_branch
|
||||||
|
)
|
||||||
|
EXECUTE PROCEDURE #{FUNCTION_NAME}();
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
f0bba8e67c97d6dea461d8626a07820c52e20ab6578ad40e8873ad0031a2ce62
|
|
@ -0,0 +1 @@
|
||||||
|
db2c19f15a03a6222627875d8bd27368de43fb6485961f866de61b3017796e28
|
|
@ -0,0 +1 @@
|
||||||
|
42387b8524845aeb76d8b6584ffa480819f682538ca9578492eed53baa49bc09
|
|
@ -53,6 +53,7 @@ DECLARE
|
||||||
state smallint;
|
state smallint;
|
||||||
report_type smallint;
|
report_type smallint;
|
||||||
resolved_on_default_branch boolean;
|
resolved_on_default_branch boolean;
|
||||||
|
present_on_default_branch boolean;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
|
IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
|
||||||
RETURN NULL;
|
RETURN NULL;
|
||||||
|
@ -63,14 +64,18 @@ BEGIN
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch
|
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch, vulnerabilities.present_on_default_branch
|
||||||
INTO
|
INTO
|
||||||
severity, state, report_type, resolved_on_default_branch
|
severity, state, report_type, resolved_on_default_branch, present_on_default_branch
|
||||||
FROM
|
FROM
|
||||||
vulnerabilities
|
vulnerabilities
|
||||||
WHERE
|
WHERE
|
||||||
vulnerabilities.id = NEW.vulnerability_id;
|
vulnerabilities.id = NEW.vulnerability_id;
|
||||||
|
|
||||||
|
IF present_on_default_branch IS NOT true THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||||
VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id', CAST(NEW.location->'kubernetes_resource'->>'agent_id' AS bigint))
|
VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id', CAST(NEW.location->'kubernetes_resource'->>'agent_id' AS bigint))
|
||||||
ON CONFLICT(vulnerability_id) DO NOTHING;
|
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||||
|
@ -89,6 +94,33 @@ RETURN NULL;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
CREATE FUNCTION insert_vulnerability_reads_from_vulnerability() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
scanner_id bigint;
|
||||||
|
uuid uuid;
|
||||||
|
location_image text;
|
||||||
|
cluster_agent_id text;
|
||||||
|
casted_cluster_agent_id bigint;
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
v_o.scanner_id, v_o.uuid, v_o.location->>'image', v_o.location->'kubernetes_resource'->>'agent_id', CAST(v_o.location->'kubernetes_resource'->>'agent_id' AS bigint)
|
||||||
|
INTO
|
||||||
|
scanner_id, uuid, location_image, cluster_agent_id, casted_cluster_agent_id
|
||||||
|
FROM
|
||||||
|
vulnerability_occurrences v_o
|
||||||
|
WHERE
|
||||||
|
v_o.vulnerability_id = NEW.id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||||
|
VALUES (NEW.id, NEW.project_id, scanner_id, NEW.report_type, NEW.severity, NEW.state, NEW.resolved_on_default_branch, uuid::uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||||
|
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
CREATE FUNCTION next_traversal_ids_sibling(traversal_ids integer[]) RETURNS integer[]
|
CREATE FUNCTION next_traversal_ids_sibling(traversal_ids integer[]) RETURNS integer[]
|
||||||
LANGUAGE plpgsql IMMUTABLE STRICT
|
LANGUAGE plpgsql IMMUTABLE STRICT
|
||||||
AS $$
|
AS $$
|
||||||
|
@ -31645,6 +31677,8 @@ CREATE TRIGGER trigger_has_external_wiki_on_update AFTER UPDATE ON integrations
|
||||||
|
|
||||||
CREATE TRIGGER trigger_insert_or_update_vulnerability_reads_from_occurrences AFTER INSERT OR UPDATE ON vulnerability_occurrences FOR EACH ROW EXECUTE FUNCTION insert_or_update_vulnerability_reads();
|
CREATE TRIGGER trigger_insert_or_update_vulnerability_reads_from_occurrences AFTER INSERT OR UPDATE ON vulnerability_occurrences FOR EACH ROW EXECUTE FUNCTION insert_or_update_vulnerability_reads();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_insert_vulnerability_reads_from_vulnerability AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.present_on_default_branch IS NOT TRUE) AND (new.present_on_default_branch IS TRUE))) EXECUTE FUNCTION insert_vulnerability_reads_from_vulnerability();
|
||||||
|
|
||||||
CREATE TRIGGER trigger_namespaces_parent_id_on_insert AFTER INSERT ON namespaces FOR EACH ROW EXECUTE FUNCTION insert_namespaces_sync_event();
|
CREATE TRIGGER trigger_namespaces_parent_id_on_insert AFTER INSERT ON namespaces FOR EACH ROW EXECUTE FUNCTION insert_namespaces_sync_event();
|
||||||
|
|
||||||
CREATE TRIGGER trigger_namespaces_parent_id_on_update AFTER UPDATE ON namespaces FOR EACH ROW WHEN ((old.parent_id IS DISTINCT FROM new.parent_id)) EXECUTE FUNCTION insert_namespaces_sync_event();
|
CREATE TRIGGER trigger_namespaces_parent_id_on_update AFTER UPDATE ON namespaces FOR EACH ROW WHEN ((old.parent_id IS DISTINCT FROM new.parent_id)) EXECUTE FUNCTION insert_namespaces_sync_event();
|
||||||
|
@ -31659,7 +31693,7 @@ CREATE TRIGGER trigger_update_has_issues_on_vulnerability_issue_links_update AFT
|
||||||
|
|
||||||
CREATE TRIGGER trigger_update_location_on_vulnerability_occurrences_update AFTER UPDATE ON vulnerability_occurrences FOR EACH ROW WHEN (((new.report_type = ANY (ARRAY[2, 7])) AND (((old.location ->> 'image'::text) IS DISTINCT FROM (new.location ->> 'image'::text)) OR (((old.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text) IS DISTINCT FROM ((new.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text))))) EXECUTE FUNCTION update_location_from_vulnerability_occurrences();
|
CREATE TRIGGER trigger_update_location_on_vulnerability_occurrences_update AFTER UPDATE ON vulnerability_occurrences FOR EACH ROW WHEN (((new.report_type = ANY (ARRAY[2, 7])) AND (((old.location ->> 'image'::text) IS DISTINCT FROM (new.location ->> 'image'::text)) OR (((old.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text) IS DISTINCT FROM ((new.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text))))) EXECUTE FUNCTION update_location_from_vulnerability_occurrences();
|
||||||
|
|
||||||
CREATE TRIGGER trigger_update_vulnerability_reads_on_vulnerability_update AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.severity IS DISTINCT FROM new.severity) OR (old.state IS DISTINCT FROM new.state) OR (old.resolved_on_default_branch IS DISTINCT FROM new.resolved_on_default_branch))) EXECUTE FUNCTION update_vulnerability_reads_from_vulnerability();
|
CREATE TRIGGER trigger_update_vulnerability_reads_on_vulnerability_update AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.present_on_default_branch IS TRUE) AND ((old.severity IS DISTINCT FROM new.severity) OR (old.state IS DISTINCT FROM new.state) OR (old.resolved_on_default_branch IS DISTINCT FROM new.resolved_on_default_branch)))) EXECUTE FUNCTION update_vulnerability_reads_from_vulnerability();
|
||||||
|
|
||||||
CREATE TRIGGER users_loose_fk_trigger AFTER DELETE ON users REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
|
CREATE TRIGGER users_loose_fk_trigger AFTER DELETE ON users REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
|
||||||
|
|
||||||
|
|
|
@ -7,18 +7,32 @@ type: reference
|
||||||
|
|
||||||
# Password storage **(FREE)**
|
# Password storage **(FREE)**
|
||||||
|
|
||||||
|
> PBKDF2 and SHA512 [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/360658) in GitLab 15.2 [with flags](../administration/feature_flags.md) named `pbkdf2_password_encryption` and `pbkdf2_password_encryption_write`. Disabled by default.
|
||||||
|
|
||||||
GitLab stores user passwords in a hashed format to prevent passwords from being
|
GitLab stores user passwords in a hashed format to prevent passwords from being
|
||||||
stored as plain text.
|
stored as plain text.
|
||||||
|
|
||||||
GitLab uses the [Devise](https://github.com/heartcombo/devise) authentication
|
GitLab uses the [Devise](https://github.com/heartcombo/devise) authentication
|
||||||
library to hash user passwords. Created password hashes have these attributes:
|
library to hash user passwords. Created password hashes have these attributes:
|
||||||
|
|
||||||
- **Hashing**: The [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt) hashing
|
- **Hashing**:
|
||||||
function is used to generate the hash of the provided password. This is a
|
- **BCrypt**: By default, the [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt) hashing
|
||||||
strong, industry-standard cryptographic hashing function.
|
function is used to generate the hash of the provided password. This is a
|
||||||
|
strong, industry-standard cryptographic hashing function.
|
||||||
|
- **PBKDF2 and SHA512**: Starting in GitLab 15.2, PBKDF2 and SHA512 are supported
|
||||||
|
behind the following feature flags (disabled by default):
|
||||||
|
- `pbkdf2_password_encryption` - Enables reading and comparison of PBKDF2 + SHA512
|
||||||
|
hashed passwords and supports fallback for BCrypt hashed passwords.
|
||||||
|
- `pbkdf2_password_encryption_write` - Enables new passwords to be saved
|
||||||
|
using PBKDF2 and SHA512, and existing BCrypt passwords to be migrated when users sign in.
|
||||||
|
|
||||||
|
FLAG:
|
||||||
|
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||||
|
ask an administrator to [enable the feature flags](../administration/feature_flags.md) named `pbkdf2_password_encryption` and `pbkdf2_password_encryption_write`.
|
||||||
|
|
||||||
- **Stretching**: Password hashes are [stretched](https://en.wikipedia.org/wiki/Key_stretching)
|
- **Stretching**: Password hashes are [stretched](https://en.wikipedia.org/wiki/Key_stretching)
|
||||||
to harden against brute-force attacks. By default, GitLab uses a stretching
|
to harden against brute-force attacks. By default, GitLab uses a stretching
|
||||||
factor of 10.
|
factor of 10 for BCrypt and 20,000 for PBKDF2 + SHA512.
|
||||||
- **Salting**: A [cryptographic salt](https://en.wikipedia.org/wiki/Salt_(cryptography))
|
- **Salting**: A [cryptographic salt](https://en.wikipedia.org/wiki/Salt_(cryptography))
|
||||||
is added to each password to harden against pre-computed hash and dictionary
|
is added to each password to harden against pre-computed hash and dictionary
|
||||||
attacks. To increase security, each salt is randomly generated for each
|
attacks. To increase security, each salt is randomly generated for each
|
||||||
|
|
|
@ -243,29 +243,13 @@ See [Multiple Kubernetes clusters for Auto DevOps](multiple_clusters_auto_devops
|
||||||
|
|
||||||
## Customizing the Kubernetes namespace
|
## Customizing the Kubernetes namespace
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27630) in GitLab 12.6.
|
In GitLab 14.5 and earlier, you could use `environment:kubernetes:namespace`
|
||||||
|
to specify a namespace for the environment.
|
||||||
|
However, this feature was [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8),
|
||||||
|
along with certificate-based integration.
|
||||||
|
|
||||||
For clusters not managed by GitLab, you can customize the namespace in
|
You should now use the `KUBE_NAMESPACE` environment variable and
|
||||||
`.gitlab-ci.yml` by specifying
|
[limit the environments it is available for](../../ci/environments/index.md#scope-environments-with-specs).
|
||||||
[`environment:kubernetes:namespace`](../../ci/environments/index.md#configure-kubernetes-deployments-deprecated).
|
|
||||||
For example, the following configuration overrides the namespace used for
|
|
||||||
`production` deployments:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
include:
|
|
||||||
- template: Auto-DevOps.gitlab-ci.yml
|
|
||||||
|
|
||||||
production:
|
|
||||||
environment:
|
|
||||||
kubernetes:
|
|
||||||
namespace: production
|
|
||||||
```
|
|
||||||
|
|
||||||
When deploying to a custom namespace with Auto DevOps, the service account
|
|
||||||
provided with the cluster needs at least the `edit` role within the namespace.
|
|
||||||
|
|
||||||
- If the service account can create namespaces, then the namespace can be created on-demand.
|
|
||||||
- Otherwise, the namespace must exist prior to deployment.
|
|
||||||
|
|
||||||
## Using components of Auto DevOps
|
## Using components of Auto DevOps
|
||||||
|
|
||||||
|
|
|
@ -303,7 +303,9 @@ support `-fips`.
|
||||||
Starting with GitLab 14.10, `-fips` is automatically added to `CS_ANALYZER_IMAGE` when FIPS mode is
|
Starting with GitLab 14.10, `-fips` is automatically added to `CS_ANALYZER_IMAGE` when FIPS mode is
|
||||||
enabled in the GitLab instance.
|
enabled in the GitLab instance.
|
||||||
|
|
||||||
Container scanning of images in authenticated registries is not supported when [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) is enabled.
|
Container scanning of images in authenticated registries is not supported when [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode)
|
||||||
|
is enabled. When `CI_GITLAB_FIPS_MODE` is `"true"`, and `DOCKER_USER` or `DOCKER_PASSWORD` is set,
|
||||||
|
the analyzer exits with an error and does not perform the scan.
|
||||||
|
|
||||||
### Enable Container Scanning through an automatic merge request
|
### Enable Container Scanning through an automatic merge request
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ Where `<namespace>` is the [namespace](../../../user/group/index.md#namespaces)
|
||||||
To work with Terraform modules in [GitLab CI/CD](../../../ci/index.md), you can use
|
To work with Terraform modules in [GitLab CI/CD](../../../ci/index.md), you can use
|
||||||
`CI_JOB_TOKEN` in place of the personal access token in your commands.
|
`CI_JOB_TOKEN` in place of the personal access token in your commands.
|
||||||
|
|
||||||
For example:
|
For example, this job uploads a new module for the `local` [system provider](https://registry.terraform.io/browse/providers) and uses the module version from the Git commit tag:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
stages:
|
stages:
|
||||||
|
@ -121,15 +121,18 @@ upload:
|
||||||
TERRAFORM_MODULE_DIR: ${CI_PROJECT_DIR} # The path to your Terraform module
|
TERRAFORM_MODULE_DIR: ${CI_PROJECT_DIR} # The path to your Terraform module
|
||||||
TERRAFORM_MODULE_NAME: ${CI_PROJECT_NAME} # The name of your Terraform module
|
TERRAFORM_MODULE_NAME: ${CI_PROJECT_NAME} # The name of your Terraform module
|
||||||
TERRAFORM_MODULE_SYSTEM: local # The system or provider your Terraform module targets (ex. local, aws, google)
|
TERRAFORM_MODULE_SYSTEM: local # The system or provider your Terraform module targets (ex. local, aws, google)
|
||||||
TERRAFORM_MODULE_VERSION: ${CI_COMMIT_TAG} # The version of your Terraform module to be published to your project's registry
|
TERRAFORM_MODULE_VERSION: ${CI_COMMIT_TAG} # Tag commits with SemVer for the version of your Terraform module to be published
|
||||||
script:
|
script:
|
||||||
- tar -cvzf ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz -C ${TERRAFORM_MODULE_DIR} --exclude=./.git .
|
- TERRAFORM_MODULE_NAME=$(echo "${TERRAFORM_MODULE_NAME}" | tr " _" -) # module-name must not have spaces or underscores, so translate them to hyphens
|
||||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/terraform/modules/${TERRAFORM_MODULE_NAME}/${TERRAFORM_MODULE_SYSTEM}/${TERRAFORM_MODULE_VERSION}/file'
|
- tar -vczf ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz -C ${TERRAFORM_MODULE_DIR} --exclude=./.git .
|
||||||
|
- 'curl --location --header "JOB-TOKEN: ${CI_JOB_TOKEN}"
|
||||||
|
--upload-file ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz
|
||||||
|
${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/terraform/modules/${TERRAFORM_MODULE_NAME}/${TERRAFORM_MODULE_SYSTEM}/${TERRAFORM_MODULE_VERSION}/file'
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG
|
- if: $CI_COMMIT_TAG
|
||||||
```
|
```
|
||||||
|
|
||||||
To trigger this upload job, add a Git tag to your commit. The `rules:if: $CI_COMMIT_TAG` defines this so that not every commit to your repo triggers the upload.
|
To trigger this upload job, add a Git tag to your commit. Ensure the tag follows the [Semantic Versioning Specification](https://semver.org/) that Terraform requires. The `rules:if: $CI_COMMIT_TAG` ensures that only tagged commits to your repo trigger the module upload job.
|
||||||
For other ways to control jobs in your CI/CD pipeline, refer to the [`.gitlab-ci.yml`](../../../ci/yaml/index.md) keyword reference.
|
For other ways to control jobs in your CI/CD pipeline, refer to the [`.gitlab-ci.yml`](../../../ci/yaml/index.md) keyword reference.
|
||||||
|
|
||||||
## Example projects
|
## Example projects
|
||||||
|
|
|
@ -5802,6 +5802,9 @@ msgstr ""
|
||||||
msgid "BambooService|The user with API access to the Bamboo server."
|
msgid "BambooService|The user with API access to the Bamboo server."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Banned"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Banner message"
|
msgid "Banner message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27707,6 +27710,9 @@ msgstr ""
|
||||||
msgid "PackageRegistry|Error publishing"
|
msgid "PackageRegistry|Error publishing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|Examples of assets include .pom & .jar files"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|Failed to load the package data"
|
msgid "PackageRegistry|Failed to load the package data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27764,6 +27770,9 @@ msgstr ""
|
||||||
msgid "PackageRegistry|License information located at %{link}"
|
msgid "PackageRegistry|License information located at %{link}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|Manage storage used by package assets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|Manually Published"
|
msgid "PackageRegistry|Manually Published"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27782,6 +27791,9 @@ msgstr ""
|
||||||
msgid "PackageRegistry|NuGet Command"
|
msgid "PackageRegistry|NuGet Command"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|Number of duplicate assets to keep"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|Package Registry"
|
msgid "PackageRegistry|Package Registry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27907,6 +27919,9 @@ msgstr ""
|
||||||
msgid "PackageRegistry|Unable to load package"
|
msgid "PackageRegistry|Unable to load package"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|When a package with same name and version is uploaded to the registry, more assets are added to the package. To save storage space, keep only the most recent assets."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?"
|
msgid "PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,8 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
Runtime::Feature.disable(:simulate_pipeline)
|
||||||
|
|
||||||
Flow::Login.sign_in
|
Flow::Login.sign_in
|
||||||
project.visit!
|
project.visit!
|
||||||
Page::Project::Menu.perform(&:go_to_pipeline_editor)
|
Page::Project::Menu.perform(&:go_to_pipeline_editor)
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe AwarenessChannel, :clean_gitlab_redis_shared_state, type: :channel do
|
||||||
|
before do
|
||||||
|
stub_action_cable_connection(current_user: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with user" do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
describe "when no path parameter given" do
|
||||||
|
it "rejects subscription" do
|
||||||
|
subscribe path: nil
|
||||||
|
|
||||||
|
expect(subscription).to be_rejected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with valid path parameter" do
|
||||||
|
it "successfully subscribes" do
|
||||||
|
subscribe path: "/test"
|
||||||
|
|
||||||
|
session = AwarenessSession.for("/test")
|
||||||
|
|
||||||
|
expect(subscription).to be_confirmed
|
||||||
|
# check if we can use session object instead
|
||||||
|
expect(subscription).to have_stream_from("awareness:#{session.to_param}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "broadcasts set of collaborators when subscribing" do
|
||||||
|
session = AwarenessSession.for("/test")
|
||||||
|
|
||||||
|
freeze_time do
|
||||||
|
collaborator = {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
avatar_url: user.avatar_url(size: 36),
|
||||||
|
last_activity: Time.zone.now,
|
||||||
|
last_activity_humanized: ActionController::Base.helpers.distance_of_time_in_words(
|
||||||
|
Time.zone.now, Time.zone.now
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect do
|
||||||
|
subscribe path: "/test"
|
||||||
|
end.to have_broadcasted_to("awareness:#{session.to_param}")
|
||||||
|
.with(collaborators: [collaborator])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "transmits payload when user is touched" do
|
||||||
|
subscribe path: "/test"
|
||||||
|
|
||||||
|
perform :touch
|
||||||
|
|
||||||
|
expect(transmissions.size).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it "unsubscribes from channel" do
|
||||||
|
subscribe path: "/test"
|
||||||
|
session = AwarenessSession.for("/test")
|
||||||
|
|
||||||
|
expect { subscription.unsubscribe_from_channel }
|
||||||
|
.to change { session.size}.by(-1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with guest" do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
it "rejects subscription" do
|
||||||
|
subscribe path: "/test"
|
||||||
|
|
||||||
|
expect(subscription).to be_rejected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -49,7 +49,7 @@ RSpec.describe 'Project navbar' do
|
||||||
stub_config(pages: { enabled: true })
|
stub_config(pages: { enabled: true })
|
||||||
|
|
||||||
insert_after_sub_nav_item(
|
insert_after_sub_nav_item(
|
||||||
_('CI/CD'),
|
_('Packages & Registries'),
|
||||||
within: _('Settings'),
|
within: _('Settings'),
|
||||||
new_sub_nav_item_name: _('Pages')
|
new_sub_nav_item_name: _('Pages')
|
||||||
)
|
)
|
||||||
|
@ -60,18 +60,22 @@ RSpec.describe 'Project navbar' do
|
||||||
it_behaves_like 'verified navigation bar'
|
it_behaves_like 'verified navigation bar'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when package registry is available' do
|
||||||
|
before do
|
||||||
|
stub_config(packages: { enabled: true })
|
||||||
|
|
||||||
|
visit project_path(project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'verified navigation bar'
|
||||||
|
end
|
||||||
|
|
||||||
context 'when container registry is available' do
|
context 'when container registry is available' do
|
||||||
before do
|
before do
|
||||||
stub_config(registry: { enabled: true })
|
stub_config(registry: { enabled: true })
|
||||||
|
|
||||||
insert_container_nav
|
insert_container_nav
|
||||||
|
|
||||||
insert_after_sub_nav_item(
|
|
||||||
_('CI/CD'),
|
|
||||||
within: _('Settings'),
|
|
||||||
new_sub_nav_item_name: _('Packages & Registries')
|
|
||||||
)
|
|
||||||
|
|
||||||
visit project_path(project)
|
visit project_path(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration policy', :js do
|
RSpec.describe 'Project > Settings > Packages & Registries > Container registry tag expiration policy', :js do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
|
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
|
||||||
|
|
||||||
|
@ -23,14 +23,15 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
||||||
it 'shows available section' do
|
it 'shows available section' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
settings_block = find('[data-testid="registry-settings-app"]')
|
settings_block = find('[data-testid="container-expiration-policy-project-settings"]')
|
||||||
expect(settings_block).to have_text 'Clean up image tags'
|
expect(settings_block).to have_text 'Clean up image tags'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'saves cleanup policy submit the form' do
|
it 'saves cleanup policy submit the form' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
within '[data-testid="registry-settings-app"]' do
|
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||||
|
click_button('Expand')
|
||||||
select('Every day', from: 'Run cleanup')
|
select('Every day', from: 'Run cleanup')
|
||||||
select('50 tags per image name', from: 'Keep the most recent:')
|
select('50 tags per image name', from: 'Keep the most recent:')
|
||||||
fill_in('Keep tags matching:', with: 'stable')
|
fill_in('Keep tags matching:', with: 'stable')
|
||||||
|
@ -48,7 +49,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
||||||
it 'does not save cleanup policy submit form with invalid regex' do
|
it 'does not save cleanup policy submit form with invalid regex' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
within '[data-testid="registry-settings-app"]' do
|
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||||
|
click_button('Expand')
|
||||||
fill_in('Remove tags matching:', with: '*-production')
|
fill_in('Remove tags matching:', with: '*-production')
|
||||||
|
|
||||||
submit_button = find('[data-testid="save-button"')
|
submit_button = find('[data-testid="save-button"')
|
||||||
|
@ -73,7 +75,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
||||||
it 'displays the related section' do
|
it 'displays the related section' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
within '[data-testid="registry-settings-app"]' do
|
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||||
|
click_button('Expand')
|
||||||
expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
|
expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -87,7 +90,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
||||||
it 'does not display the related section' do
|
it 'does not display the related section' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
within '[data-testid="registry-settings-app"]' do
|
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||||
|
click_button('Expand')
|
||||||
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
|
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -100,7 +104,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
||||||
it 'does not exists' do
|
it 'does not exists' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
|
expect(page).not_to have_selector('[data-testid="container-expiration-policy-project-settings"]')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -110,7 +114,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
||||||
it 'does not exists' do
|
it 'does not exists' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
|
expect(page).not_to have_selector('[data-testid="container-expiration-policy-project-settings"]')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`]
|
||||||
<expiration-dropdown-stub
|
<expiration-dropdown-stub
|
||||||
class="gl-mr-7 gl-mb-0!"
|
class="gl-mr-7 gl-mb-0!"
|
||||||
data-testid="cadence-dropdown"
|
data-testid="cadence-dropdown"
|
||||||
|
description=""
|
||||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
|
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||||
label="Run cleanup:"
|
label="Run cleanup:"
|
||||||
name="cadence"
|
name="cadence"
|
||||||
|
@ -22,6 +23,7 @@ exports[`Container Expiration Policy Settings Form Enable matches snapshot 1`] =
|
||||||
exports[`Container Expiration Policy Settings Form Keep N matches snapshot 1`] = `
|
exports[`Container Expiration Policy Settings Form Keep N matches snapshot 1`] = `
|
||||||
<expiration-dropdown-stub
|
<expiration-dropdown-stub
|
||||||
data-testid="keep-n-dropdown"
|
data-testid="keep-n-dropdown"
|
||||||
|
description=""
|
||||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||||
label="Keep the most recent:"
|
label="Keep the most recent:"
|
||||||
name="keep-n"
|
name="keep-n"
|
||||||
|
@ -44,6 +46,7 @@ exports[`Container Expiration Policy Settings Form Keep Regex matches snapshot 1
|
||||||
exports[`Container Expiration Policy Settings Form OlderThan matches snapshot 1`] = `
|
exports[`Container Expiration Policy Settings Form OlderThan matches snapshot 1`] = `
|
||||||
<expiration-dropdown-stub
|
<expiration-dropdown-stub
|
||||||
data-testid="older-than-dropdown"
|
data-testid="older-than-dropdown"
|
||||||
|
description=""
|
||||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||||
label="Remove tags older than:"
|
label="Remove tags older than:"
|
||||||
name="older-than"
|
name="older-than"
|
||||||
|
|
|
@ -43,11 +43,6 @@ describe('Container expiration policy project settings', () => {
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
SettingsBlock,
|
SettingsBlock,
|
||||||
},
|
},
|
||||||
mocks: {
|
|
||||||
$toast: {
|
|
||||||
show: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
provide,
|
provide,
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
|
@ -98,7 +93,7 @@ describe('Container expiration policy project settings', () => {
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(findFormComponent().exists()).toBe(true);
|
expect(findFormComponent().exists()).toBe(true);
|
||||||
expect(findSettingsBlock().props('collapsible')).toBe(false);
|
expect(findSettingsBlock().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the form is disabled', () => {
|
describe('the form is disabled', () => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ describe('ExpirationDropdown', () => {
|
||||||
|
|
||||||
const findFormSelect = () => wrapper.find(GlFormSelect);
|
const findFormSelect = () => wrapper.find(GlFormSelect);
|
||||||
const findFormGroup = () => wrapper.find(GlFormGroup);
|
const findFormGroup = () => wrapper.find(GlFormGroup);
|
||||||
|
const findDescription = () => wrapper.find('[data-testid="description"]');
|
||||||
const findOptions = () => wrapper.findAll('[data-testid="option"]');
|
const findOptions = () => wrapper.findAll('[data-testid="option"]');
|
||||||
|
|
||||||
const mountComponent = (props) => {
|
const mountComponent = (props) => {
|
||||||
|
@ -47,6 +48,14 @@ describe('ExpirationDropdown', () => {
|
||||||
|
|
||||||
expect(findOptions()).toHaveLength(defaultProps.formOptions.length);
|
expect(findOptions()).toHaveLength(defaultProps.formOptions.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the description if passed', () => {
|
||||||
|
mountComponent({
|
||||||
|
description: 'test description',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findDescription().html()).toContain('test description');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('model', () => {
|
describe('model', () => {
|
||||||
|
|
|
@ -0,0 +1,267 @@
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import { GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
|
||||||
|
import component from '~/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue';
|
||||||
|
import {
|
||||||
|
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||||
|
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
|
||||||
|
KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
|
||||||
|
} from '~/packages_and_registries/settings/project/constants';
|
||||||
|
import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql';
|
||||||
|
import Tracking from '~/tracking';
|
||||||
|
import { packagesCleanupPolicyPayload, packagesCleanupPolicyMutationPayload } from '../mock_data';
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
|
describe('Packages Cleanup Policy Settings Form', () => {
|
||||||
|
let wrapper;
|
||||||
|
let fakeApollo;
|
||||||
|
|
||||||
|
const defaultProvidedValues = {
|
||||||
|
projectPath: 'path',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
project: { packagesCleanupPolicy },
|
||||||
|
},
|
||||||
|
} = packagesCleanupPolicyPayload();
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
value: { ...packagesCleanupPolicy },
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackingPayload = {
|
||||||
|
label: 'packages_cleanup_policies',
|
||||||
|
};
|
||||||
|
|
||||||
|
const findForm = () => wrapper.find({ ref: 'form-element' });
|
||||||
|
const findSaveButton = () => wrapper.findByTestId('save-button');
|
||||||
|
const findKeepNDuplicatedPackageFilesDropdown = () =>
|
||||||
|
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
findForm().trigger('submit');
|
||||||
|
return waitForPromises();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mountComponent = ({
|
||||||
|
props = defaultProps,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
provide = defaultProvidedValues,
|
||||||
|
} = {}) => {
|
||||||
|
wrapper = shallowMountExtended(component, {
|
||||||
|
stubs: {
|
||||||
|
GlLoadingIcon,
|
||||||
|
},
|
||||||
|
propsData: { ...props },
|
||||||
|
provide,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mocks: {
|
||||||
|
$toast: {
|
||||||
|
show: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mountComponentWithApollo = ({
|
||||||
|
provide = defaultProvidedValues,
|
||||||
|
mutationResolver,
|
||||||
|
queryPayload = packagesCleanupPolicyPayload(),
|
||||||
|
} = {}) => {
|
||||||
|
const requestHandlers = [[updatePackagesCleanupPolicyMutation, mutationResolver]];
|
||||||
|
|
||||||
|
fakeApollo = createMockApollo(requestHandlers);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
project: { packagesCleanupPolicy: value },
|
||||||
|
},
|
||||||
|
} = queryPayload;
|
||||||
|
|
||||||
|
mountComponent({
|
||||||
|
provide,
|
||||||
|
props: {
|
||||||
|
...defaultProps,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
apolloProvider: fakeApollo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(Tracking, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
fakeApollo = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keepNDuplicatedPackageFiles', () => {
|
||||||
|
it('renders dropdown', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
const element = findKeepNDuplicatedPackageFilesDropdown();
|
||||||
|
|
||||||
|
expect(element.exists()).toBe(true);
|
||||||
|
expect(element.props('label')).toMatchInterpolatedText(KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL);
|
||||||
|
expect(element.props('description')).toEqual(KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input event triggers a model update', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
findKeepNDuplicatedPackageFilesDropdown().vm.$emit('input', 'foo');
|
||||||
|
expect(wrapper.emitted('input')[0][0]).toMatchObject({
|
||||||
|
keepNDuplicatedPackageFiles: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the default option when none are selected', () => {
|
||||||
|
mountComponent({ props: { value: {} } });
|
||||||
|
expect(findKeepNDuplicatedPackageFilesDropdown().props('value')).toEqual('ALL_PACKAGE_FILES');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
isLoading | mutationLoading
|
||||||
|
${true} | ${false}
|
||||||
|
${true} | ${true}
|
||||||
|
${false} | ${true}
|
||||||
|
`(
|
||||||
|
'is disabled when is loading is $isLoading and mutationLoading is $mutationLoading',
|
||||||
|
({ isLoading, mutationLoading }) => {
|
||||||
|
mountComponent({
|
||||||
|
props: { isLoading, value: {} },
|
||||||
|
data: { mutationLoading },
|
||||||
|
});
|
||||||
|
expect(findKeepNDuplicatedPackageFilesDropdown().props('disabled')).toEqual(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('has the correct formOptions', () => {
|
||||||
|
mountComponent();
|
||||||
|
expect(findKeepNDuplicatedPackageFilesDropdown().props('formOptions')).toEqual(
|
||||||
|
wrapper.vm.$options.formOptions.keepNDuplicatedPackageFiles,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('form', () => {
|
||||||
|
describe('actions', () => {
|
||||||
|
describe('submit button', () => {
|
||||||
|
it('has type submit', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(findSaveButton().attributes('type')).toBe('submit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
isLoading | mutationLoading | disabled
|
||||||
|
${true} | ${true} | ${true}
|
||||||
|
${true} | ${false} | ${true}
|
||||||
|
${false} | ${true} | ${true}
|
||||||
|
${false} | ${false} | ${false}
|
||||||
|
`(
|
||||||
|
'when isLoading is $isLoading and mutationLoading is $mutationLoading is disabled',
|
||||||
|
({ isLoading, mutationLoading, disabled }) => {
|
||||||
|
mountComponent({
|
||||||
|
props: { ...defaultProps, isLoading },
|
||||||
|
data: { mutationLoading },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findSaveButton().props('disabled')).toBe(disabled);
|
||||||
|
expect(findKeepNDuplicatedPackageFilesDropdown().props('disabled')).toBe(disabled);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
isLoading | mutationLoading | showLoading
|
||||||
|
${true} | ${true} | ${true}
|
||||||
|
${true} | ${false} | ${true}
|
||||||
|
${false} | ${true} | ${true}
|
||||||
|
${false} | ${false} | ${false}
|
||||||
|
`(
|
||||||
|
'when isLoading is $isLoading and mutationLoading is $mutationLoading is $showLoading that the loading icon is shown',
|
||||||
|
({ isLoading, mutationLoading, showLoading }) => {
|
||||||
|
mountComponent({
|
||||||
|
props: { ...defaultProps, isLoading },
|
||||||
|
data: { mutationLoading },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findSaveButton().props('loading')).toBe(showLoading);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('form submit event', () => {
|
||||||
|
it('dispatches the correct apollo mutation', () => {
|
||||||
|
const mutationResolver = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(packagesCleanupPolicyMutationPayload());
|
||||||
|
mountComponentWithApollo({
|
||||||
|
mutationResolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
findForm().trigger('submit');
|
||||||
|
|
||||||
|
expect(mutationResolver).toHaveBeenCalledWith({
|
||||||
|
input: {
|
||||||
|
keepNDuplicatedPackageFiles: 'ALL_PACKAGE_FILES',
|
||||||
|
projectPath: 'path',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks the submit event', () => {
|
||||||
|
mountComponentWithApollo({
|
||||||
|
mutationResolver: jest.fn().mockResolvedValue(packagesCleanupPolicyMutationPayload()),
|
||||||
|
});
|
||||||
|
|
||||||
|
findForm().trigger('submit');
|
||||||
|
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(
|
||||||
|
undefined,
|
||||||
|
'submit_packages_cleanup_form',
|
||||||
|
trackingPayload,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show a success toast when submit succeed', async () => {
|
||||||
|
mountComponentWithApollo({
|
||||||
|
mutationResolver: jest.fn().mockResolvedValue(packagesCleanupPolicyMutationPayload()),
|
||||||
|
});
|
||||||
|
|
||||||
|
await submitForm();
|
||||||
|
|
||||||
|
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when submit fails', () => {
|
||||||
|
it('shows an error', async () => {
|
||||||
|
mountComponentWithApollo({
|
||||||
|
mutationResolver: jest.fn().mockRejectedValue(packagesCleanupPolicyMutationPayload()),
|
||||||
|
});
|
||||||
|
|
||||||
|
await submitForm();
|
||||||
|
|
||||||
|
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { GlAlert, GlSprintf } from '@gitlab/ui';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import component from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue';
|
||||||
|
import PackagesCleanupPolicyForm from '~/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue';
|
||||||
|
import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/packages_and_registries/settings/project/constants';
|
||||||
|
import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql';
|
||||||
|
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||||
|
|
||||||
|
import { packagesCleanupPolicyPayload, packagesCleanupPolicyData } from '../mock_data';
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
|
describe('Packages cleanup policy project settings', () => {
|
||||||
|
let wrapper;
|
||||||
|
let fakeApollo;
|
||||||
|
|
||||||
|
const defaultProvidedValues = {
|
||||||
|
projectPath: 'path',
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||||
|
const findFormComponent = () => wrapper.findComponent(PackagesCleanupPolicyForm);
|
||||||
|
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
|
||||||
|
|
||||||
|
const mountComponent = (provide = defaultProvidedValues, config) => {
|
||||||
|
wrapper = shallowMount(component, {
|
||||||
|
stubs: {
|
||||||
|
GlSprintf,
|
||||||
|
SettingsBlock,
|
||||||
|
},
|
||||||
|
provide,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
|
||||||
|
const requestHandlers = [[packagesCleanupPolicyQuery, resolver]];
|
||||||
|
|
||||||
|
fakeApollo = createMockApollo(requestHandlers);
|
||||||
|
mountComponent(provide, {
|
||||||
|
apolloProvider: fakeApollo,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
fakeApollo = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the setting form', async () => {
|
||||||
|
mountComponentWithApollo({
|
||||||
|
resolver: jest.fn().mockResolvedValue(packagesCleanupPolicyPayload()),
|
||||||
|
});
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findFormComponent().exists()).toBe(true);
|
||||||
|
expect(findFormComponent().props('value')).toEqual(packagesCleanupPolicyData);
|
||||||
|
expect(findSettingsBlock().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchSettingsError', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mountComponentWithApollo({
|
||||||
|
resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
|
||||||
|
});
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the form is hidden', () => {
|
||||||
|
expect(findFormComponent().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an alert', () => {
|
||||||
|
expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,19 +1,41 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
|
import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
|
||||||
import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
|
import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
|
||||||
|
import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue';
|
||||||
|
|
||||||
describe('Registry Settings app', () => {
|
describe('Registry Settings app', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
|
const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
|
||||||
|
const findPackagesCleanupPolicy = () => wrapper.find(PackagesCleanupPolicy);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
wrapper = null;
|
wrapper = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders container expiration policy component', () => {
|
const mountComponent = (provide) => {
|
||||||
wrapper = shallowMount(component);
|
wrapper = shallowMount(component, {
|
||||||
|
provide,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
expect(findContainerExpirationPolicy().exists()).toBe(true);
|
it.each`
|
||||||
});
|
showContainerRegistrySettings | showPackageRegistrySettings
|
||||||
|
${true} | ${false}
|
||||||
|
${true} | ${true}
|
||||||
|
${false} | ${true}
|
||||||
|
${false} | ${false}
|
||||||
|
`(
|
||||||
|
'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
|
||||||
|
({ showContainerRegistrySettings, showPackageRegistrySettings }) => {
|
||||||
|
mountComponent({
|
||||||
|
showContainerRegistrySettings,
|
||||||
|
showPackageRegistrySettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings);
|
||||||
|
expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,3 +40,33 @@ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const packagesCleanupPolicyData = {
|
||||||
|
keepNDuplicatedPackageFiles: 'ALL_PACKAGE_FILES',
|
||||||
|
nextRunAt: '2020-11-19T07:37:03.941Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const packagesCleanupPolicyPayload = (override) => ({
|
||||||
|
data: {
|
||||||
|
project: {
|
||||||
|
id: '1',
|
||||||
|
packagesCleanupPolicy: {
|
||||||
|
__typename: 'PackagesCleanupPolicy',
|
||||||
|
...packagesCleanupPolicyData,
|
||||||
|
...override,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const packagesCleanupPolicyMutationPayload = ({ override, errors = [] } = {}) => ({
|
||||||
|
data: {
|
||||||
|
updatePackagesCleanupPolicy: {
|
||||||
|
packagesCleanupPolicy: {
|
||||||
|
...packagesCleanupPolicyData,
|
||||||
|
...override,
|
||||||
|
},
|
||||||
|
errors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -44,6 +44,7 @@ RSpec.describe Groups::GroupMembersHelper do
|
||||||
members: present_members(members_collection),
|
members: present_members(members_collection),
|
||||||
invited: present_members(invited),
|
invited: present_members(invited),
|
||||||
access_requests: present_members(access_requests),
|
access_requests: present_members(access_requests),
|
||||||
|
banned: [],
|
||||||
include_relations: [:inherited, :direct],
|
include_relations: [:inherited, :direct],
|
||||||
search: nil
|
search: nil
|
||||||
)
|
)
|
||||||
|
@ -117,6 +118,7 @@ RSpec.describe Groups::GroupMembersHelper do
|
||||||
members: present_members(members_collection),
|
members: present_members(members_collection),
|
||||||
invited: present_members(invited),
|
invited: present_members(invited),
|
||||||
access_requests: present_members(access_requests),
|
access_requests: present_members(access_requests),
|
||||||
|
banned: [],
|
||||||
include_relations: include_relations,
|
include_relations: include_relations,
|
||||||
search: nil
|
search: nil
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,16 +31,14 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with available_runner_releases configured' do
|
context 'with available_runner_releases configured' do
|
||||||
let(:runner_releases_double) { instance_double(Gitlab::Ci::RunnerReleases) }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(Gitlab::Ci::RunnerReleases).to receive(:instance).and_return(runner_releases_double)
|
url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
|
||||||
allow(runner_releases_double).to receive(:releases)
|
|
||||||
.and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) })
|
WebMock.stub_request(:get, url).to_return(
|
||||||
allow(runner_releases_double).to receive(:releases_by_minor)
|
body: available_runner_releases.map { |v| { name: v } }.to_json,
|
||||||
.and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) }
|
status: 200,
|
||||||
.group_by(&:without_patch)
|
headers: { 'Content-Type' => 'application/json' }
|
||||||
.transform_values(&:max))
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with no available runner releases' do
|
context 'with no available runner releases' do
|
||||||
|
|
|
@ -135,18 +135,20 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
|
||||||
|
|
||||||
describe 'Packages & Registries' do
|
describe 'Packages & Registries' do
|
||||||
let(:item_id) { :packages_and_registries }
|
let(:item_id) { :packages_and_registries }
|
||||||
|
let(:packages_enabled) { false }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_container_registry_config(enabled: container_enabled)
|
stub_container_registry_config(enabled: container_enabled)
|
||||||
|
stub_config(packages: { enabled: packages_enabled })
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'when config registry setting is disabled' do
|
describe 'when container registry setting is disabled' do
|
||||||
let(:container_enabled) { false }
|
let(:container_enabled) { false }
|
||||||
|
|
||||||
specify { is_expected.to be_nil }
|
specify { is_expected.to be_nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'when config registry setting is enabled' do
|
describe 'when container registry setting is enabled' do
|
||||||
let(:container_enabled) { true }
|
let(:container_enabled) { true }
|
||||||
|
|
||||||
specify { is_expected.not_to be_nil }
|
specify { is_expected.not_to be_nil }
|
||||||
|
@ -157,6 +159,19 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
|
||||||
specify { is_expected.to be_nil }
|
specify { is_expected.to be_nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'when package registry setting is enabled' do
|
||||||
|
let(:container_enabled) { false }
|
||||||
|
let(:packages_enabled) { true }
|
||||||
|
|
||||||
|
specify { is_expected.not_to be_nil }
|
||||||
|
|
||||||
|
describe 'when the user does not have access' do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
specify { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Usage Quotas' do
|
describe 'Usage Quotas' do
|
||||||
|
|
|
@ -2,14 +2,24 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe AwarenessSession do
|
RSpec.describe AwarenessSession, :clean_gitlab_redis_shared_state do
|
||||||
subject { AwarenessSession.for(session_id) }
|
subject { AwarenessSession.for(session_id) }
|
||||||
|
|
||||||
let!(:user) { create(:user) }
|
let!(:user) { create(:user) }
|
||||||
let(:session_id) { 1 }
|
let(:session_id) { 1 }
|
||||||
|
|
||||||
after do
|
describe "when initiating a session" do
|
||||||
redis_shared_state_cleanup!
|
it "provides a string representation of the model instance" do
|
||||||
|
expected = "awareness_session=6b86b273ff34fce"
|
||||||
|
|
||||||
|
expect(subject.to_s).to eql(expected)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "provides a parameterized version of the session identifier" do
|
||||||
|
expected = "6b86b273ff34fce"
|
||||||
|
|
||||||
|
expect(subject.to_param).to eql(expected)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when a user joins a session" do
|
describe "when a user joins a session" do
|
||||||
|
@ -103,6 +113,26 @@ RSpec.describe AwarenessSession do
|
||||||
expect(ttl_user).to be > 0
|
expect(ttl_user).to be > 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "fetches user(s) from database" do
|
||||||
|
subject.join(user)
|
||||||
|
|
||||||
|
expect(subject.users.first).to eql(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fetches and filters online user(s) from database" do
|
||||||
|
subject.join(user)
|
||||||
|
|
||||||
|
travel 2.hours do
|
||||||
|
subject.join(user2)
|
||||||
|
|
||||||
|
online_users = subject.online_users_with_last_activity
|
||||||
|
online_user, _ = online_users.first
|
||||||
|
|
||||||
|
expect(online_users.size).to be 1
|
||||||
|
expect(online_user).to eql(user2)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when a user leaves a session" do
|
describe "when a user leaves a session" do
|
||||||
|
|
|
@ -2,15 +2,11 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Awareness do
|
RSpec.describe Awareness, :clean_gitlab_redis_shared_state do
|
||||||
subject { create(:user) }
|
subject { create(:user) }
|
||||||
|
|
||||||
let(:session) { AwarenessSession.for(1) }
|
let(:session) { AwarenessSession.for(1) }
|
||||||
|
|
||||||
after do
|
|
||||||
redis_shared_state_cleanup!
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "when joining a session" do
|
describe "when joining a session" do
|
||||||
it "increases the number of sessions" do
|
it "increases the number of sessions" do
|
||||||
expect { subject.join(session) }
|
expect { subject.join(session) }
|
||||||
|
|
|
@ -1480,45 +1480,144 @@ RSpec.describe ProjectPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'view_package_registry_project_settings' do
|
describe 'view_package_registry_project_settings' do
|
||||||
context 'with registry enabled' do
|
context 'with packages disabled and' do
|
||||||
before do
|
before do
|
||||||
stub_config(registry: { enabled: true })
|
stub_config(packages: { enabled: false })
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an admin user' do
|
context 'with registry enabled' do
|
||||||
let(:current_user) { admin }
|
before do
|
||||||
|
stub_config(registry: { enabled: true })
|
||||||
context 'when admin mode enabled', :enable_admin_mode do
|
|
||||||
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when admin mode disabled' do
|
context 'with an admin user' do
|
||||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
let(:current_user) { admin }
|
||||||
|
|
||||||
|
context 'when admin mode enabled', :enable_admin_mode do
|
||||||
|
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when admin mode disabled' do
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[owner maintainer].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[developer reporter guest non_member anonymous].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
%i[owner maintainer].each do |role|
|
context 'with registry disabled' do
|
||||||
context "with #{role}" do
|
before do
|
||||||
let(:current_user) { public_send(role) }
|
stub_config(registry: { enabled: false })
|
||||||
|
|
||||||
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
%i[developer reporter guest non_member anonymous].each do |role|
|
context 'with admin user' do
|
||||||
context "with #{role}" do
|
let(:current_user) { admin }
|
||||||
let(:current_user) { public_send(role) }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
context 'when admin mode enabled', :enable_admin_mode do
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when admin mode disabled' do
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[owner maintainer developer reporter guest non_member anonymous].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with registry disabled' do
|
context 'with registry disabled and' do
|
||||||
before do
|
before do
|
||||||
stub_config(registry: { enabled: false })
|
stub_config(registry: { enabled: false })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with packages enabled' do
|
||||||
|
before do
|
||||||
|
stub_config(packages: { enabled: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an admin user' do
|
||||||
|
let(:current_user) { admin }
|
||||||
|
|
||||||
|
context 'when admin mode enabled', :enable_admin_mode do
|
||||||
|
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when admin mode disabled' do
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[owner maintainer].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[developer reporter guest non_member anonymous].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with packages disabled' do
|
||||||
|
before do
|
||||||
|
stub_config(packages: { enabled: false })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with admin user' do
|
||||||
|
let(:current_user) { admin }
|
||||||
|
|
||||||
|
context 'when admin mode enabled', :enable_admin_mode do
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when admin mode disabled' do
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[owner maintainer developer reporter guest non_member anonymous].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with registry & packages both disabled' do
|
||||||
|
before do
|
||||||
|
stub_config(registry: { enabled: false })
|
||||||
|
stub_config(packages: { enabled: false })
|
||||||
|
end
|
||||||
|
|
||||||
context 'with admin user' do
|
context 'with admin user' do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
||||||
|
|
|
@ -10,78 +10,128 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
|
||||||
create(:ci_runner_version, version: '14.0.1', status: :not_available)
|
create(:ci_runner_version, version: '14.0.1', status: :not_available)
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
context 'with RunnerUpgradeCheck recommending 14.0.2' do
|
||||||
stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1)
|
|
||||||
|
|
||||||
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
|
||||||
.to receive(:check_runner_upgrade_status)
|
|
||||||
.and_return({ recommended: ::Gitlab::VersionInfo.new(14, 0, 2) })
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with runner with new version' do
|
|
||||||
let!(:runner_14_0_2) { create(:ci_runner, version: '14.0.2') }
|
|
||||||
let!(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :not_available) }
|
|
||||||
let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1)
|
||||||
|
|
||||||
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
||||||
.to receive(:check_runner_upgrade_status)
|
.to receive(:check_runner_upgrade_status)
|
||||||
.with('14.0.2')
|
.and_return({ recommended: ::Gitlab::VersionInfo.new(14, 0, 2) })
|
||||||
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
|
|
||||||
.once
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates and updates expected ci_runner_versions entries', :aggregate_failures do
|
context 'with runner with new version' do
|
||||||
expect(Ci::RunnerVersion).to receive(:insert_all)
|
let!(:runner_14_0_2) { create(:ci_runner, version: '14.0.2') }
|
||||||
.ordered
|
let!(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :not_available) }
|
||||||
.with([{ version: '14.0.2' }], anything)
|
let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
|
||||||
.once
|
|
||||||
.and_call_original
|
|
||||||
|
|
||||||
result = nil
|
before do
|
||||||
expect { result = execute }
|
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
||||||
.to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended')
|
.to receive(:check_runner_upgrade_status)
|
||||||
.and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended')
|
.with('14.0.2')
|
||||||
.and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available')
|
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
|
||||||
|
.once
|
||||||
|
end
|
||||||
|
|
||||||
expect(result).to eq({
|
it 'creates and updates expected ci_runner_versions entries', :aggregate_failures do
|
||||||
status: :success,
|
expect(Ci::RunnerVersion).to receive(:insert_all)
|
||||||
total_inserted: 1, # 14.0.2 is inserted
|
.ordered
|
||||||
total_updated: 3, # 14.0.0, 14.0.1 are updated, and newly inserted 14.0.2's status is calculated
|
.with([{ version: '14.0.2' }], anything)
|
||||||
total_deleted: 0
|
.once
|
||||||
})
|
.and_call_original
|
||||||
|
|
||||||
|
result = nil
|
||||||
|
expect { result = execute }
|
||||||
|
.to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended')
|
||||||
|
.and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended')
|
||||||
|
.and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available')
|
||||||
|
|
||||||
|
expect(result).to eq({
|
||||||
|
status: :success,
|
||||||
|
total_inserted: 1, # 14.0.2 is inserted
|
||||||
|
total_updated: 3, # 14.0.0, 14.0.1 are updated, and newly inserted 14.0.2's status is calculated
|
||||||
|
total_deleted: 0
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with orphan ci_runner_version' do
|
||||||
|
let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
||||||
|
.to receive(:check_runner_upgrade_status)
|
||||||
|
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
|
||||||
|
result = nil
|
||||||
|
expect { result = execute }
|
||||||
|
.to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil)
|
||||||
|
.and not_change { runner_version_14_0_1.reload.status }.from('not_available')
|
||||||
|
|
||||||
|
expect(result).to eq({
|
||||||
|
status: :success,
|
||||||
|
total_inserted: 0,
|
||||||
|
total_updated: 0,
|
||||||
|
total_deleted: 1 # 14.0.2 is deleted
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with no runner version changes' do
|
||||||
|
before do
|
||||||
|
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
||||||
|
.to receive(:check_runner_upgrade_status)
|
||||||
|
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 1) })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not modify ci_runner_versions entries', :aggregate_failures do
|
||||||
|
result = nil
|
||||||
|
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
|
||||||
|
|
||||||
|
expect(result).to eq({
|
||||||
|
status: :success,
|
||||||
|
total_inserted: 0,
|
||||||
|
total_updated: 0,
|
||||||
|
total_deleted: 0
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with failing version check' do
|
||||||
|
before do
|
||||||
|
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
||||||
|
.to receive(:check_runner_upgrade_status)
|
||||||
|
.and_return({ error: ::Gitlab::VersionInfo.new(14, 0, 1) })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'makes no changes to ci_runner_versions', :aggregate_failures do
|
||||||
|
result = nil
|
||||||
|
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
|
||||||
|
|
||||||
|
expect(result).to eq({
|
||||||
|
status: :success,
|
||||||
|
total_inserted: 0,
|
||||||
|
total_updated: 0,
|
||||||
|
total_deleted: 0
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with orphan ci_runner_version' do
|
context 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do
|
||||||
let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) }
|
let(:available_runner_releases) do
|
||||||
|
%w[14.0.0 14.0.1]
|
||||||
before do
|
|
||||||
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
|
||||||
.to receive(:check_runner_upgrade_status)
|
|
||||||
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
|
|
||||||
result = nil
|
|
||||||
expect { result = execute }
|
|
||||||
.to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil)
|
|
||||||
.and not_change { runner_version_14_0_1.reload.status }.from('not_available')
|
|
||||||
|
|
||||||
expect(result).to eq({
|
|
||||||
status: :success,
|
|
||||||
total_inserted: 0,
|
|
||||||
total_updated: 0,
|
|
||||||
total_deleted: 1 # 14.0.2 is deleted
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with no runner version changes' do
|
|
||||||
before do
|
before do
|
||||||
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
|
||||||
.to receive(:check_runner_upgrade_status)
|
|
||||||
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 1) })
|
WebMock.stub_request(:get, url).to_return(
|
||||||
|
body: available_runner_releases.map { |v| { name: v } }.to_json,
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type' => 'application/json' }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not modify ci_runner_versions entries', :aggregate_failures do
|
it 'does not modify ci_runner_versions entries', :aggregate_failures do
|
||||||
|
@ -96,24 +146,4 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with failing version check' do
|
|
||||||
before do
|
|
||||||
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
|
|
||||||
.to receive(:check_runner_upgrade_status)
|
|
||||||
.and_return({ error: ::Gitlab::VersionInfo.new(14, 0, 1) })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'makes no changes to ci_runner_versions', :aggregate_failures do
|
|
||||||
result = nil
|
|
||||||
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
|
|
||||||
|
|
||||||
expect(result).to eq({
|
|
||||||
status: :success,
|
|
||||||
total_inserted: 0,
|
|
||||||
total_updated: 0,
|
|
||||||
total_deleted: 0
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -110,6 +110,7 @@ RSpec.shared_context 'project navbar structure' do
|
||||||
_('Access Tokens'),
|
_('Access Tokens'),
|
||||||
_('Repository'),
|
_('Repository'),
|
||||||
_('CI/CD'),
|
_('CI/CD'),
|
||||||
|
_('Packages & Registries'),
|
||||||
_('Monitor'),
|
_('Monitor'),
|
||||||
s_('UsageQuota|Usage Quotas')
|
s_('UsageQuota|Usage Quotas')
|
||||||
]
|
]
|
||||||
|
|
|
@ -37,4 +37,16 @@ RSpec.describe 'groups/group_members/index', :aggregate_failures do
|
||||||
expect(rendered).not_to have_content('You can invite a new member')
|
expect(rendered).not_to have_content('You can invite a new member')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when @banned is nil' do
|
||||||
|
before do
|
||||||
|
assign(:banned, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls group_members_app_data with { banned: [] }' do
|
||||||
|
expect(view).to receive(:group_members_app_data).with(group, a_hash_including(banned: []))
|
||||||
|
|
||||||
|
render
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -909,8 +909,11 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Packages & Registries' do
|
describe 'Packages & Registries' do
|
||||||
|
let(:packages_enabled) { false }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_container_registry_config(enabled: registry_enabled)
|
stub_container_registry_config(enabled: registry_enabled)
|
||||||
|
stub_config(packages: { enabled: packages_enabled })
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when registry is enabled' do
|
context 'when registry is enabled' do
|
||||||
|
@ -932,6 +935,17 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
|
||||||
expect(rendered).not_to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
|
expect(rendered).not_to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when packages config is enabled' do
|
||||||
|
let(:registry_enabled) { false }
|
||||||
|
let(:packages_enabled) { true }
|
||||||
|
|
||||||
|
it 'has a link to the Packages & Registries settings' do
|
||||||
|
render
|
||||||
|
|
||||||
|
expect(rendered).to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Usage Quotas' do
|
describe 'Usage Quotas' do
|
||||||
|
|
Loading…
Reference in New Issue