Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-19 18:09:21 +00:00
parent 9c8e8b5ffc
commit f602da84d1
59 changed files with 1602 additions and 234 deletions

View File

@ -1 +1 @@
76dabc8174f7978025f48adcfab0a19c85416531
ca638e23ca921cf2f2f3cdc8a6ff033af667170b

View File

@ -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 }));
},

View File

@ -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,

View File

@ -1,5 +1,5 @@
<script>
import { MEMBER_TYPES } from '../../constants';
import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
import {
isGroup,
isDirectMember,

View File

@ -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';

View File

@ -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) => {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 },

View File

@ -0,0 +1,4 @@
fragment PackagesCleanupPolicyFields on PackagesCleanupPolicy {
keepNDuplicatedPackageFiles
nextRunAt
}

View File

@ -0,0 +1,10 @@
#import "../fragments/packages_cleanup_policy.fragment.graphql"
mutation updatePackagesCleanupPolicy($input: UpdatePackagesCleanupPolicyInput!) {
updatePackagesCleanupPolicy(input: $input) {
packagesCleanupPolicy {
...PackagesCleanupPolicyFields
}
errors
}
}

View File

@ -0,0 +1,10 @@
#import "../fragments/packages_cleanup_policy.fragment.graphql"
query getProjectPackagesCleanupPolicy($projectPath: ID!) {
project(fullPath: $projectPath) {
id
packagesCleanupPolicy {
...PackagesCleanupPolicyFields
}
}
}

View File

@ -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', {});

View File

@ -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,
};
};

View File

@ -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();

View File

@ -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"

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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?

View File

@ -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?

View File

@ -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')

View File

@ -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'),

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
f0bba8e67c97d6dea461d8626a07820c52e20ab6578ad40e8873ad0031a2ce62

View File

@ -0,0 +1 @@
db2c19f15a03a6222627875d8bd27368de43fb6485961f866de61b3017796e28

View File

@ -0,0 +1 @@
42387b8524845aeb76d8b6584ffa480819f682538ca9578492eed53baa49bc09

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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);
});
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
},
);
});

View File

@ -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,
},
},
});

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -1480,45 +1480,144 @@ 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 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
context 'with registry enabled' do
before do
stub_config(registry: { enabled: true })
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(: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
%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) }
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
context 'with registry disabled' do
before do
stub_config(registry: { enabled: false })
end
end
%i[developer reporter guest non_member anonymous].each do |role|
context "with #{role}" do
let(:current_user) { public_send(role) }
context 'with admin user' do
let(:current_user) { admin }
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
context 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
end
%i[owner maintainer developer reporter guest non_member anonymous].each do |role|
context "with #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
end
end
end
context 'with registry disabled' do
context 'with registry disabled and' do
before do
stub_config(registry: { enabled: false })
end
context 'with packages enabled' do
before do
stub_config(packages: { enabled: true })
end
context 'with an admin user' do
let(:current_user) { admin }
context 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
end
%i[owner maintainer].each do |role|
context "with #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
end
end
%i[developer reporter guest non_member anonymous].each do |role|
context "with #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
end
end
context 'with packages disabled' do
before do
stub_config(packages: { enabled: false })
end
context 'with admin user' do
let(:current_user) { admin }
context 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
end
%i[owner maintainer developer reporter guest non_member anonymous].each do |role|
context "with #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
end
end
end
end
context 'with registry & packages both disabled' do
before do
stub_config(registry: { enabled: false })
stub_config(packages: { enabled: false })
end
context 'with admin user' do
let(:current_user) { admin }

View File

@ -10,78 +10,128 @@ 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
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') }
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')
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
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
})
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
context 'with orphan ci_runner_version' do
let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) }
before do
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
.to receive(:check_runner_upgrade_status)
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
end
it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
result = nil
expect { result = execute }
.to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil)
.and not_change { runner_version_14_0_1.reload.status }.from('not_available')
expect(result).to eq({
status: :success,
total_inserted: 0,
total_updated: 0,
total_deleted: 1 # 14.0.2 is deleted
})
end
end
context 'with no runner version changes' do
before do
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
.to receive(:check_runner_upgrade_status)
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 1) })
end
it 'does not modify ci_runner_versions entries', :aggregate_failures do
result = nil
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
expect(result).to eq({
status: :success,
total_inserted: 0,
total_updated: 0,
total_deleted: 0
})
end
end
context 'with failing version check' do
before do
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
.to receive(:check_runner_upgrade_status)
.and_return({ error: ::Gitlab::VersionInfo.new(14, 0, 1) })
end
it 'makes no changes to ci_runner_versions', :aggregate_failures do
result = nil
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
expect(result).to eq({
status: :success,
total_inserted: 0,
total_updated: 0,
total_deleted: 0
})
end
end
end
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 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do
let(:available_runner_releases) do
%w[14.0.0 14.0.1]
end
it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
result = nil
expect { result = execute }
.to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil)
.and not_change { runner_version_14_0_1.reload.status }.from('not_available')
expect(result).to eq({
status: :success,
total_inserted: 0,
total_updated: 0,
total_deleted: 1 # 14.0.2 is deleted
})
end
end
context 'with no runner version changes' do
before do
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
.to receive(:check_runner_upgrade_status)
.and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 1) })
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 'does not modify ci_runner_versions entries', :aggregate_failures do
@ -96,24 +146,4 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
})
end
end
context 'with failing version check' do
before do
allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
.to receive(:check_runner_upgrade_status)
.and_return({ error: ::Gitlab::VersionInfo.new(14, 0, 1) })
end
it 'makes no changes to ci_runner_versions', :aggregate_failures do
result = nil
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
expect(result).to eq({
status: :success,
total_inserted: 0,
total_updated: 0,
total_deleted: 0
})
end
end
end

View File

@ -110,6 +110,7 @@ RSpec.shared_context 'project navbar structure' do
_('Access Tokens'),
_('Repository'),
_('CI/CD'),
_('Packages & Registries'),
_('Monitor'),
s_('UsageQuota|Usage Quotas')
]

View File

@ -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

View File

@ -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