Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue85
-rw-r--r--app/assets/javascripts/members/components/table/member_avatar.vue8
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue2
-rw-r--r--app/assets/javascripts/members/constants.js6
-rw-r--r--app/assets/javascripts/members/index.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue68
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue137
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/constants.js25
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/utils.js9
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue5
-rw-r--r--app/channels/awareness_channel.rb84
-rw-r--r--app/helpers/groups/group_members_helper.rb2
-rw-r--r--app/helpers/packages_helper.rb10
-rw-r--r--app/models/awareness_session.rb25
-rw-r--r--app/policies/project_policy.rb8
-rw-r--r--app/views/groups/group_members/index.html.haml1
-rw-r--r--app/views/projects/settings/packages_and_registries/show.html.haml2
-rw-r--r--db/migrate/20220712025712_update_insert_or_update_vulnerability_reads_function.rb86
-rw-r--r--db/migrate/20220712031923_create_vulnerability_reads_for_an_existing_vulnerability_record.rb56
-rw-r--r--db/migrate/20220714142424_update_trigger_update_vulnerability_reads_on_vulnerability_update.rb48
-rw-r--r--db/schema_migrations/202207120257121
-rw-r--r--db/schema_migrations/202207120319231
-rw-r--r--db/schema_migrations/202207141424241
-rw-r--r--db/structure.sql40
-rw-r--r--doc/security/password_storage.md22
-rw-r--r--doc/topics/autodevops/customize.md28
-rw-r--r--doc/user/application_security/container_scanning/index.md4
-rw-r--r--doc/user/packages/terraform_module_registry/index.md13
-rw-r--r--locale/gitlab.pot15
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb2
-rw-r--r--spec/channels/awareness_channel_spec.rb80
-rw-r--r--spec/features/projects/navbar_spec.rb18
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb20
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap3
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js267
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js81
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js30
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/mock_data.js30
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/runner_upgrade_check_spec.rb16
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb19
-rw-r--r--spec/models/awareness_session_spec.rb36
-rw-r--r--spec/models/concerns/awareness_spec.rb6
-rw-r--r--spec/policies/project_policy_spec.rb133
-rw-r--r--spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb184
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/views/groups/group_members/index.html.haml_spec.rb12
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb14
59 files changed, 1597 insertions, 229 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 296a103cfe8..e21e2b1e446 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-76dabc8174f7978025f48adcfab0a19c85416531
+ca638e23ca921cf2f2f3cdc8a6ff033af667170b
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index 98995730df4..b824a013f3b 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -1,40 +1,48 @@
<script>
import { GlTabs, GlTab, GlBadge, GlButton } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { queryToObject } from '~/lib/utils/url_utility';
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';
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 {
name: 'MembersTabs',
ACTIVE_TAB_QUERY_PARAM_NAME,
- 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,
- },
- ],
+ TABS,
components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
data() {
@@ -43,20 +51,17 @@ export default {
};
},
computed: {
- ...mapState({
- userCount(state) {
- return countComputed(state, MEMBER_TYPES.user);
- },
- groupCount(state) {
- return countComputed(state, MEMBER_TYPES.group);
- },
- inviteCount(state) {
- return countComputed(state, MEMBER_TYPES.invite);
- },
- accessRequestCount(state) {
- return countComputed(state, MEMBER_TYPES.accessRequest);
- },
- }),
+ ...mapState(
+ Object.values(MEMBER_TYPES).reduce((getters, memberType) => {
+ return {
+ ...getters,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [`${memberType}Count`](state) {
+ return countComputed(state, memberType);
+ },
+ };
+ }, {}),
+ ),
urlParams() {
return Object.keys(queryToObject(window.location.search, { gatherArrays: true }));
},
diff --git a/app/assets/javascripts/members/components/table/member_avatar.vue b/app/assets/javascripts/members/components/table/member_avatar.vue
index 92b757ffcba..966eb90e402 100644
--- a/app/assets/javascripts/members/components/table/member_avatar.vue
+++ b/app/assets/javascripts/members/components/table/member_avatar.vue
@@ -6,7 +6,13 @@ import UserAvatar from '../avatars/user_avatar.vue';
export default {
name: 'MemberAvatar',
- components: { UserAvatar, InviteAvatar, GroupAvatar, AccessRequestAvatar: UserAvatar },
+ components: {
+ UserAvatar,
+ InviteAvatar,
+ GroupAvatar,
+ AccessRequestAvatar: UserAvatar,
+ BannedAvatar: UserAvatar,
+ },
props: {
memberType: {
type: String,
diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 3436bcab2fc..51eff428d63 100644
--- a/app/assets/javascripts/members/components/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -1,5 +1,5 @@
<script>
-import { MEMBER_TYPES } from '../../constants';
+import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
import {
isGroup,
isDirectMember,
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 8c40cc3f29d..2fe816c7ea2 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -3,6 +3,12 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import { __, s__ } from '~/locale';
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_SOURCE = 'source';
export const FIELD_KEY_GRANTED = 'granted';
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 0df876cabd7..34660f8f499 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -2,8 +2,8 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { parseDataAttributes } from '~/members/utils';
+import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
import MembersTabs from './components/members_tabs.vue';
-import { MEMBER_TYPES } from './constants';
import membersStore from './store';
export const initMembersApp = (el, options) => {
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
index fdc7bd39780..90a18d5cf5a 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
@@ -81,7 +81,7 @@ export default {
</script>
<template>
- <settings-block :collapsible="false">
+ <settings-block data-testid="container-expiration-policy-project-settings">
<template #title> {{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</template>
<template #description>
<span>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
index d75fb31fd98..7682754fdcb 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
@@ -30,6 +30,11 @@ export default {
type: String,
required: true,
},
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
};
</script>
@@ -46,5 +51,10 @@ export default {
{{ option.label }}
</option>
</gl-form-select>
+ <template v-if="description" #description>
+ <span data-testid="description" class="gl-text-gray-400">
+ {{ description }}
+ </span>
+ </template>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue
new file mode 100644
index 00000000000..1170407a349
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue
@@ -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>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue
new file mode 100644
index 00000000000..b1751d5174a
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue
@@ -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>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index 95af19e6d85..710cfe7b1eb 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -1,15 +1,19 @@
<script>
import ContainerExpirationPolicy from './container_expiration_policy.vue';
+import PackagesCleanupPolicy from './packages_cleanup_policy.vue';
export default {
components: {
ContainerExpirationPolicy,
+ PackagesCleanupPolicy,
},
+ inject: ['showContainerRegistrySettings', 'showPackageRegistrySettings'],
};
</script>
<template>
- <section data-testid="registry-settings-app">
- <container-expiration-policy />
- </section>
+ <div>
+ <packages-cleanup-policy v-if="showPackageRegistrySettings" />
+ <container-expiration-policy v-if="showContainerRegistrySettings" />
+ </div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index 40f980d15fb..948520151ce 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -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',
);
+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 = [
{ key: 'ONE_TAG', variable: 1, default: false },
{ key: 'FIVE_TAGS', variable: 5, default: false },
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql
new file mode 100644
index 00000000000..a77ede37884
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql
@@ -0,0 +1,4 @@
+fragment PackagesCleanupPolicyFields on PackagesCleanupPolicy {
+ keepNDuplicatedPackageFiles
+ nextRunAt
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql
new file mode 100644
index 00000000000..31cdd67e881
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/packages_cleanup_policy.fragment.graphql"
+
+mutation updatePackagesCleanupPolicy($input: UpdatePackagesCleanupPolicyInput!) {
+ updatePackagesCleanupPolicy(input: $input) {
+ packagesCleanupPolicy {
+ ...PackagesCleanupPolicyFields
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql
new file mode 100644
index 00000000000..0e9af253f2c
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/packages_cleanup_policy.fragment.graphql"
+
+query getProjectPackagesCleanupPolicy($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ id
+ packagesCleanupPolicy {
+ ...PackagesCleanupPolicyFields
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
index 17c33073668..daf1da6eac8 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
@@ -20,6 +20,8 @@ export default () => {
adminSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
+ showContainerRegistrySettings,
+ showPackageRegistrySettings,
} = el.dataset;
return new Vue({
el,
@@ -34,6 +36,8 @@ export default () => {
adminSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
+ showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings),
+ showPackageRegistrySettings: parseBoolean(showPackageRegistrySettings),
},
render(createElement) {
return createElement('registry-settings-app', {});
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/utils.js b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
index b577a051862..847965454e9 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/utils.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
@@ -1,5 +1,11 @@
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) => {
const item = options.find((o) => o.default);
@@ -25,5 +31,6 @@ export const formOptionsGenerator = () => {
olderThan: optionLabelGenerator(OLDER_THAN_OPTIONS, olderThanTranslationGenerator),
cadence: CADENCE_OPTIONS,
keepN: optionLabelGenerator(KEEP_N_OPTIONS, keepNTranslationGenerator),
+ [KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME]: KEEP_N_DUPLICATED_PACKAGE_FILES_OPTIONS,
};
};
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index c7c2f6f773e..62d47cb49b8 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -5,12 +5,11 @@ import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { s__ } from '~/locale';
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';
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
-
-initMembersApp(document.querySelector('.js-group-members-list-app'), {
+const APP_OPTIONS = {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
@@ -61,7 +60,10 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: groupMemberRequestFormatter,
},
-});
+ ...EE_APP_OPTIONS,
+};
+
+initMembersApp(document.querySelector('.js-group-members-list-app'), APP_OPTIONS);
initInviteMembersModal();
initInviteGroupsModal();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index fe51591c32d..f2c30870a68 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -61,6 +61,10 @@ export default {
GlFormCheckbox,
GlToggle,
ConfirmDanger,
+ otherProjectSettings: () =>
+ import(
+ 'jh_component/pages/projects/shared/permissions/components/other_project_settings.vue'
+ ),
},
mixins: [settingsMixin, glFeatureFlagsMixin()],
@@ -905,6 +909,7 @@ export default {
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
</gl-form-checkbox>
</project-setting-row>
+ <other-project-settings />
<confirm-danger
v-if="isVisibilityReduced"
button-variant="confirm"
diff --git a/app/channels/awareness_channel.rb b/app/channels/awareness_channel.rb
new file mode 100644
index 00000000000..554e057ca83
--- /dev/null
+++ b/app/channels/awareness_channel.rb
@@ -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
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index 37b23345d2a..2021961772a 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -9,7 +9,7 @@ module Groups::GroupMembersHelper
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
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 } }),
group: group_group_links_list_data(group, include_relations, search),
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
index 20d40626449..ec64746d6b6 100644
--- a/app/helpers/packages_helper.rb
+++ b/app/helpers/packages_helper.rb
@@ -53,4 +53,14 @@ module PackagesHelper
project.container_expiration_policy.nil? &&
project.container_repositories.exists?
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
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb
index 602888182b1..a84a3454a27 100644
--- a/app/models/awareness_session.rb
+++ b/app/models/awareness_session.rb
@@ -143,17 +143,34 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
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
User.where(id: user_ids)
end
def users_with_last_activity
- # where in (x, y, [...z]) is a set and does not maintain any order, we need to
- # make sure to establish a stable order for both, the pairs returned from
+ # where in (x, y, [...z]) is a set and does not maintain any order, we need
+ # to make sure to establish a stable order for both, the pairs returned from
# redis and the ActiveRecord query. Using IDs in ascending order.
user_ids, last_activities = user_ids_with_last_activity
.sort_by(&:first)
.transpose
+
+ return [] if user_ids.blank?
+
users = User.where(id: user_ids).order(id: :asc)
users.zip(last_activities)
end
@@ -162,6 +179,10 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
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
def id_i
Integer(id, 16) if id.present?
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 850f25a6089..54270dc186e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -225,6 +225,10 @@ class ProjectPolicy < BasePolicy
Gitlab.config.registry.enabled
end
+ condition :packages_enabled do
+ Gitlab.config.packages.enabled
+ end
+
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
# not.
rule { guest | admin }.enable :read_project_for_iids
@@ -795,6 +799,10 @@ class ProjectPolicy < BasePolicy
enable :view_package_registry_project_settings
end
+ rule { packages_enabled & can?(:admin_package) }.policy do
+ enable :view_package_registry_project_settings
+ end
+
private
def user_is_user?
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 4150766d3d3..d9fef8940eb 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -26,6 +26,7 @@
members: @members,
invited: @invited_members,
access_requests: @requesters,
+ banned: @banned || [],
include_relations: @include_relations,
search: params[:search_groups]).to_json } }
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
diff --git a/app/views/projects/settings/packages_and_registries/show.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml
index 378bb0f9306..1a7821d3268 100644
--- a/app/views/projects/settings/packages_and_registries/show.html.haml
+++ b/app/views/projects/settings/packages_and_registries/show.html.haml
@@ -8,6 +8,8 @@
keep_n_options: keep_n_options.to_json,
older_than_options: older_than_options.to_json,
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'),
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'),
diff --git a/db/migrate/20220712025712_update_insert_or_update_vulnerability_reads_function.rb b/db/migrate/20220712025712_update_insert_or_update_vulnerability_reads_function.rb
new file mode 100644
index 00000000000..55aeacfbecd
--- /dev/null
+++ b/db/migrate/20220712025712_update_insert_or_update_vulnerability_reads_function.rb
@@ -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
diff --git a/db/migrate/20220712031923_create_vulnerability_reads_for_an_existing_vulnerability_record.rb b/db/migrate/20220712031923_create_vulnerability_reads_for_an_existing_vulnerability_record.rb
new file mode 100644
index 00000000000..68769f9e4e3
--- /dev/null
+++ b/db/migrate/20220712031923_create_vulnerability_reads_for_an_existing_vulnerability_record.rb
@@ -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
diff --git a/db/migrate/20220714142424_update_trigger_update_vulnerability_reads_on_vulnerability_update.rb b/db/migrate/20220714142424_update_trigger_update_vulnerability_reads_on_vulnerability_update.rb
new file mode 100644
index 00000000000..00fc4ac1ed6
--- /dev/null
+++ b/db/migrate/20220714142424_update_trigger_update_vulnerability_reads_on_vulnerability_update.rb
@@ -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
diff --git a/db/schema_migrations/20220712025712 b/db/schema_migrations/20220712025712
new file mode 100644
index 00000000000..68e8a510feb
--- /dev/null
+++ b/db/schema_migrations/20220712025712
@@ -0,0 +1 @@
+f0bba8e67c97d6dea461d8626a07820c52e20ab6578ad40e8873ad0031a2ce62 \ No newline at end of file
diff --git a/db/schema_migrations/20220712031923 b/db/schema_migrations/20220712031923
new file mode 100644
index 00000000000..1cb2b63faa8
--- /dev/null
+++ b/db/schema_migrations/20220712031923
@@ -0,0 +1 @@
+db2c19f15a03a6222627875d8bd27368de43fb6485961f866de61b3017796e28 \ No newline at end of file
diff --git a/db/schema_migrations/20220714142424 b/db/schema_migrations/20220714142424
new file mode 100644
index 00000000000..1a7f5e7dcaf
--- /dev/null
+++ b/db/schema_migrations/20220714142424
@@ -0,0 +1 @@
+42387b8524845aeb76d8b6584ffa480819f682538ca9578492eed53baa49bc09 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 7080cf1936f..cb0d4696931 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -53,6 +53,7 @@ DECLARE
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;
@@ -63,14 +64,18 @@ BEGIN
END IF;
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
- severity, state, report_type, resolved_on_default_branch
+ 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;
@@ -89,6 +94,33 @@ RETURN NULL;
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[]
LANGUAGE plpgsql IMMUTABLE STRICT
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_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_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_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();
diff --git a/doc/security/password_storage.md b/doc/security/password_storage.md
index b4c2e27c952..d3db8cbe4f6 100644
--- a/doc/security/password_storage.md
+++ b/doc/security/password_storage.md
@@ -7,18 +7,32 @@ type: reference
# 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
stored as plain text.
GitLab uses the [Devise](https://github.com/heartcombo/devise) authentication
library to hash user passwords. Created password hashes have these attributes:
-- **Hashing**: The [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt) hashing
- function is used to generate the hash of the provided password. This is a
- strong, industry-standard cryptographic hashing function.
+- **Hashing**:
+ - **BCrypt**: By default, the [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt) hashing
+ 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)
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))
is added to each password to harden against pre-computed hash and dictionary
attacks. To increase security, each salt is randomly generated for each
diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md
index 13b83df148e..d8734ab5b13 100644
--- a/doc/topics/autodevops/customize.md
+++ b/doc/topics/autodevops/customize.md
@@ -243,29 +243,13 @@ See [Multiple Kubernetes clusters for Auto DevOps](multiple_clusters_auto_devops
## 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
-`.gitlab-ci.yml` by specifying
-[`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.
+You should now use the `KUBE_NAMESPACE` environment variable and
+[limit the environments it is available for](../../ci/environments/index.md#scope-environments-with-specs).
## Using components of Auto DevOps
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 700e0607ddc..cf864068e44 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -303,7 +303,9 @@ support `-fips`.
Starting with GitLab 14.10, `-fips` is automatically added to `CS_ANALYZER_IMAGE` when FIPS mode is
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
diff --git a/doc/user/packages/terraform_module_registry/index.md b/doc/user/packages/terraform_module_registry/index.md
index 42c85ae9d41..2668b8b35ac 100644
--- a/doc/user/packages/terraform_module_registry/index.md
+++ b/doc/user/packages/terraform_module_registry/index.md
@@ -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
`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
stages:
@@ -121,15 +121,18 @@ upload:
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_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:
- - tar -cvzf ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz -C ${TERRAFORM_MODULE_DIR} --exclude=./.git .
- - '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'
+ - TERRAFORM_MODULE_NAME=$(echo "${TERRAFORM_MODULE_NAME}" | tr " _" -) # module-name must not have spaces or underscores, so translate them to hyphens
+ - 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:
- 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.
## Example projects
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ece81a08f5a..2f11794b514 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5802,6 +5802,9 @@ msgstr ""
msgid "BambooService|The user with API access to the Bamboo server."
msgstr ""
+msgid "Banned"
+msgstr ""
+
msgid "Banner message"
msgstr ""
@@ -27707,6 +27710,9 @@ msgstr ""
msgid "PackageRegistry|Error publishing"
msgstr ""
+msgid "PackageRegistry|Examples of assets include .pom & .jar files"
+msgstr ""
+
msgid "PackageRegistry|Failed to load the package data"
msgstr ""
@@ -27764,6 +27770,9 @@ msgstr ""
msgid "PackageRegistry|License information located at %{link}"
msgstr ""
+msgid "PackageRegistry|Manage storage used by package assets"
+msgstr ""
+
msgid "PackageRegistry|Manually Published"
msgstr ""
@@ -27782,6 +27791,9 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Number of duplicate assets to keep"
+msgstr ""
+
msgid "PackageRegistry|Package Registry"
msgstr ""
@@ -27907,6 +27919,9 @@ msgstr ""
msgid "PackageRegistry|Unable to load package"
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?"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
index c58865fff03..04777bd7f01 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
@@ -37,6 +37,8 @@ module QA
end
before do
+ Runtime::Feature.disable(:simulate_pipeline)
+
Flow::Login.sign_in
project.visit!
Page::Project::Menu.perform(&:go_to_pipeline_editor)
diff --git a/spec/channels/awareness_channel_spec.rb b/spec/channels/awareness_channel_spec.rb
new file mode 100644
index 00000000000..8d6dc36f6bd
--- /dev/null
+++ b/spec/channels/awareness_channel_spec.rb
@@ -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
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 023601b0b1e..e07a5d09405 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe 'Project navbar' do
stub_config(pages: { enabled: true })
insert_after_sub_nav_item(
- _('CI/CD'),
+ _('Packages & Registries'),
within: _('Settings'),
new_sub_nav_item_name: _('Pages')
)
@@ -60,18 +60,22 @@ RSpec.describe 'Project navbar' do
it_behaves_like 'verified navigation bar'
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
before do
stub_config(registry: { enabled: true })
insert_container_nav
- insert_after_sub_nav_item(
- _('CI/CD'),
- within: _('Settings'),
- new_sub_nav_item_name: _('Packages & Registries')
- )
-
visit project_path(project)
end
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index ff28d59ed08..9468540736f 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -2,7 +2,7 @@
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(: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
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'
end
it 'saves cleanup policy submit the form' do
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('50 tags per image name', from: 'Keep the most recent:')
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
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')
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
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.')
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
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')
end
end
@@ -100,7 +104,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not exists' do
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
@@ -110,7 +114,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not exists' do
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
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
index faa313118f3..108d9478788 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
@@ -4,6 +4,7 @@ exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`]
<expiration-dropdown-stub
class="gl-mr-7 gl-mb-0!"
data-testid="cadence-dropdown"
+ description=""
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Run cleanup:"
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`] = `
<expiration-dropdown-stub
data-testid="keep-n-dropdown"
+ description=""
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Keep the most recent:"
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`] = `
<expiration-dropdown-stub
data-testid="older-than-dropdown"
+ description=""
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Remove tags older than:"
name="older-than"
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index aa3506771fa..d83c717da6a 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -43,11 +43,6 @@ describe('Container expiration policy project settings', () => {
GlSprintf,
SettingsBlock,
},
- mocks: {
- $toast: {
- show: jest.fn(),
- },
- },
provide,
...config,
});
@@ -98,7 +93,7 @@ describe('Container expiration policy project settings', () => {
await waitForPromises();
expect(findFormComponent().exists()).toBe(true);
- expect(findSettingsBlock().props('collapsible')).toBe(false);
+ expect(findSettingsBlock().exists()).toBe(true);
});
describe('the form is disabled', () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
index 5c9ade7f785..8b99ac6b06c 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
@@ -16,6 +16,7 @@ describe('ExpirationDropdown', () => {
const findFormSelect = () => wrapper.find(GlFormSelect);
const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findDescription = () => wrapper.find('[data-testid="description"]');
const findOptions = () => wrapper.findAll('[data-testid="option"]');
const mountComponent = (props) => {
@@ -47,6 +48,14 @@ describe('ExpirationDropdown', () => {
expect(findOptions()).toHaveLength(defaultProps.formOptions.length);
});
+
+ it('renders the description if passed', () => {
+ mountComponent({
+ description: 'test description',
+ });
+
+ expect(findDescription().html()).toContain('test description');
+ });
});
describe('model', () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
new file mode 100644
index 00000000000..86f45d78bae
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
@@ -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);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
new file mode 100644
index 00000000000..6dfeeca6862
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
@@ -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);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index 337991dfae0..f576bc79eae 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -1,19 +1,41 @@
import { shallowMount } from '@vue/test-utils';
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 PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue';
describe('Registry Settings app', () => {
let wrapper;
+
const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
+ const findPackagesCleanupPolicy = () => wrapper.find(PackagesCleanupPolicy);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- it('renders container expiration policy component', () => {
- wrapper = shallowMount(component);
+ const mountComponent = (provide) => {
+ 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);
+ },
+ );
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
index 33406c98f4b..d4b6c66ddeb 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
@@ -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,
+ },
+ },
+});
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index d308df3a017..89c26c21338 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -44,6 +44,7 @@ RSpec.describe Groups::GroupMembersHelper do
members: present_members(members_collection),
invited: present_members(invited),
access_requests: present_members(access_requests),
+ banned: [],
include_relations: [:inherited, :direct],
search: nil
)
@@ -117,6 +118,7 @@ RSpec.describe Groups::GroupMembersHelper do
members: present_members(members_collection),
invited: present_members(invited),
access_requests: present_members(access_requests),
+ banned: [],
include_relations: include_relations,
search: nil
)
diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
index a9fe293dd69..f2507a24b10 100644
--- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
+++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
@@ -31,16 +31,14 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
end
context 'with available_runner_releases configured' do
- let(:runner_releases_double) { instance_double(Gitlab::Ci::RunnerReleases) }
-
before do
- allow(Gitlab::Ci::RunnerReleases).to receive(:instance).and_return(runner_releases_double)
- allow(runner_releases_double).to receive(:releases)
- .and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) })
- allow(runner_releases_double).to receive(:releases_by_minor)
- .and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) }
- .group_by(&:without_patch)
- .transform_values(&:max))
+ url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
+
+ 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
context 'with no available runner releases' do
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index d6136dddf40..f41f7a01d88 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -135,18 +135,20 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
describe 'Packages & Registries' do
let(:item_id) { :packages_and_registries }
+ let(:packages_enabled) { false }
before do
stub_container_registry_config(enabled: container_enabled)
+ stub_config(packages: { enabled: packages_enabled })
end
- describe 'when config registry setting is disabled' do
+ describe 'when container registry setting is disabled' do
let(:container_enabled) { false }
specify { is_expected.to be_nil }
end
- describe 'when config registry setting is enabled' do
+ describe 'when container registry setting is enabled' do
let(:container_enabled) { true }
specify { is_expected.not_to be_nil }
@@ -157,6 +159,19 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
specify { is_expected.to be_nil }
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
describe 'Usage Quotas' do
diff --git a/spec/models/awareness_session_spec.rb b/spec/models/awareness_session_spec.rb
index 4dace7cb8de..854ce5957f7 100644
--- a/spec/models/awareness_session_spec.rb
+++ b/spec/models/awareness_session_spec.rb
@@ -2,14 +2,24 @@
require 'spec_helper'
-RSpec.describe AwarenessSession do
+RSpec.describe AwarenessSession, :clean_gitlab_redis_shared_state do
subject { AwarenessSession.for(session_id) }
let!(:user) { create(:user) }
let(:session_id) { 1 }
- after do
- redis_shared_state_cleanup!
+ describe "when initiating a session" do
+ 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
describe "when a user joins a session" do
@@ -103,6 +113,26 @@ RSpec.describe AwarenessSession do
expect(ttl_user).to be > 0
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
describe "when a user leaves a session" do
diff --git a/spec/models/concerns/awareness_spec.rb b/spec/models/concerns/awareness_spec.rb
index 9119fe2c458..67acacc7bb1 100644
--- a/spec/models/concerns/awareness_spec.rb
+++ b/spec/models/concerns/awareness_spec.rb
@@ -2,15 +2,11 @@
require 'spec_helper'
-RSpec.describe Awareness do
+RSpec.describe Awareness, :clean_gitlab_redis_shared_state do
subject { create(:user) }
let(:session) { AwarenessSession.for(1) }
- after do
- redis_shared_state_cleanup!
- end
-
describe "when joining a session" do
it "increases the number of sessions" do
expect { subject.join(session) }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index de36b755e88..c041c72a0be 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1480,43 +1480,142 @@ RSpec.describe ProjectPolicy do
end
describe 'view_package_registry_project_settings' do
- context 'with registry enabled' do
+ context 'with packages disabled and' do
before do
- stub_config(registry: { enabled: true })
+ stub_config(packages: { enabled: false })
end
- context 'with an admin user' do
- let(:current_user) { admin }
+ context 'with registry enabled' do
+ before do
+ stub_config(registry: { enabled: true })
+ end
- context 'when admin mode enabled', :enable_admin_mode do
- it { is_expected.to be_allowed(:view_package_registry_project_settings) }
+ 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
- context 'when admin mode disabled' do
- it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
+ %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
- %i[owner maintainer].each do |role|
- context "with #{role}" do
- let(:current_user) { public_send(role) }
+ context 'with registry disabled' do
+ before do
+ stub_config(registry: { 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
- it { is_expected.to be_allowed(:view_package_registry_project_settings) }
+ %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
- %i[developer reporter guest non_member anonymous].each do |role|
- context "with #{role}" do
- let(:current_user) { public_send(role) }
+ context 'with registry disabled and' do
+ before do
+ stub_config(registry: { enabled: false })
+ end
- it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
+ 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 disabled' do
+ 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
diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
index dc4f76b723f..f8313eaab90 100644
--- a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
+++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
@@ -10,101 +10,131 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
create(:ci_runner_version, version: '14.0.1', status: :not_available)
end
- before 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') }
-
+ context 'with RunnerUpgradeCheck recommending 14.0.2' do
before do
+ stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1)
+
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
.to receive(:check_runner_upgrade_status)
- .with('14.0.2')
- .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
- .once
+ .and_return({ recommended: ::Gitlab::VersionInfo.new(14, 0, 2) })
end
- it 'creates and updates expected ci_runner_versions entries', :aggregate_failures do
- expect(Ci::RunnerVersion).to receive(:insert_all)
- .ordered
- .with([{ version: '14.0.2' }], anything)
- .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
- })
+ 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
+ allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
+ .to receive(:check_runner_upgrade_status)
+ .with('14.0.2')
+ .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
+ .once
+ end
+
+ it 'creates and updates expected ci_runner_versions entries', :aggregate_failures do
+ expect(Ci::RunnerVersion).to receive(:insert_all)
+ .ordered
+ .with([{ version: '14.0.2' }], anything)
+ .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
- 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) })
+ 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
- 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
- })
+ 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
- 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) })
+ 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
- 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
- })
+ context 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do
+ let(:available_runner_releases) do
+ %w[14.0.0 14.0.1]
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) })
+ url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
+
+ 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
- it 'makes no changes to ci_runner_versions', :aggregate_failures do
+ 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')
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index b523af385f4..6c2ed79b343 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -110,6 +110,7 @@ RSpec.shared_context 'project navbar structure' do
_('Access Tokens'),
_('Repository'),
_('CI/CD'),
+ _('Packages & Registries'),
_('Monitor'),
s_('UsageQuota|Usage Quotas')
]
diff --git a/spec/views/groups/group_members/index.html.haml_spec.rb b/spec/views/groups/group_members/index.html.haml_spec.rb
index 40d4c9d33c9..2d7d50555d6 100644
--- a/spec/views/groups/group_members/index.html.haml_spec.rb
+++ b/spec/views/groups/group_members/index.html.haml_spec.rb
@@ -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')
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
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index e3c2466f807..9ae3f814679 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -909,8 +909,11 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Packages & Registries' do
+ let(:packages_enabled) { false }
+
before do
stub_container_registry_config(enabled: registry_enabled)
+ stub_config(packages: { enabled: packages_enabled })
end
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))
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
describe 'Usage Quotas' do