Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-04-22 18:11:44 +00:00
parent 4edee7ef8b
commit 1f5a6f7200
98 changed files with 1364 additions and 502 deletions

View File

@ -49,7 +49,7 @@ gem 'responders', '~> 3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'sprockets', '~> 3.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'view_component', '~> 3.11.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'view_component', '~> 3.12.1' # rubocop:todo Gemfile/MissingFeatureCategory
# Supported DBs
gem 'pg', '~> 1.5.6' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -719,7 +719,7 @@
{"name":"validates_hostname","version":"1.0.13","platform":"ruby","checksum":"eac40178cc0b4f727df9cc6a5cb5bc2550718ad8d9bb3728df9aba6354bdda19"},
{"name":"version_gem","version":"1.1.0","platform":"ruby","checksum":"6b009518020db57f51ec7b410213fae2bf692baea9f1b51770db97fbc93d9a80"},
{"name":"version_sorter","version":"2.3.0","platform":"ruby","checksum":"2147f2a1a3804fbb8f60d268b7d7c1ec717e6dd727ffe2c165b4e05e82efe1da"},
{"name":"view_component","version":"3.11.0","platform":"ruby","checksum":"6994c3dcc6f8da1fab42420a367f5071c00291241bc5a56f73a34ec8c10fc5ff"},
{"name":"view_component","version":"3.12.1","platform":"ruby","checksum":"f2ce2ad2945389f4bbd4ff77465605e9019041e5c804d16d093791be2542b18b"},
{"name":"virtus","version":"2.0.0","platform":"ruby","checksum":"8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2"},
{"name":"vite_rails","version":"3.0.17","platform":"ruby","checksum":"b90e85a3e55802981cbdb43a4101d944b1e7055bfe85599d9cb7de0f1ea58bcc"},
{"name":"vite_ruby","version":"3.5.0","platform":"ruby","checksum":"a3e5da3fdd816f831cb1530c4001a790aac862c89f74c09f48d5a3cfed3dea73"},

View File

@ -1867,7 +1867,7 @@ GEM
activesupport (>= 3.0)
version_gem (1.1.0)
version_sorter (2.3.0)
view_component (3.11.0)
view_component (3.12.1)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
@ -2272,7 +2272,7 @@ DEPENDENCIES
valid_email (~> 0.1)
validates_hostname (~> 1.0.13)
version_sorter (~> 2.3)
view_component (~> 3.11.0)
view_component (~> 3.12.1)
vite_rails (~> 3.0.17)
vite_ruby (~> 3.5.0)
vmstat (~> 2.3.0)

View File

@ -126,7 +126,7 @@ export default {
this.noteData.noteable_id = this.getNoteableData.id;
},
methods: {
...mapActions('batchComments', ['publishReview']),
...mapActions('batchComments', ['publishReview', 'clearDrafts']),
repositionDropdown() {
this.$refs.submitDropdown?.$refs.dropdown?.updatePopper();
},
@ -157,6 +157,8 @@ export default {
scrollToElement(document.getElementById(`note_${this.getCurrentUserLastNote.id}`)),
);
}
this.clearDrafts();
} catch (e) {
if (e.data?.message) {
createAlert({ message: e.data.message, captureError: true });

View File

@ -41,7 +41,6 @@ export default {
},
[types.RECEIVE_PUBLISH_REVIEW_SUCCESS](state) {
state.isPublishing = false;
state.drafts = [];
},
[types.RECEIVE_PUBLISH_REVIEW_ERROR](state) {
state.isPublishing = false;

View File

@ -71,6 +71,7 @@ export default {
return {
id: runner.id,
search: search.length >= SHORT_SEARCH_LENGTH ? search : '',
sort: 'ID_ASC',
...getPaginationVariables(this.pagination, RUNNER_DETAILS_PROJECTS_PAGE_SIZE),
};
},

View File

@ -3,6 +3,7 @@
query getRunnerProjects(
$id: CiRunnerID!
$search: String
$sort: String
$first: Int
$last: Int
$before: String
@ -14,7 +15,14 @@ query getRunnerProjects(
id
}
projectCount
projects(search: $search, first: $first, last: $last, before: $before, after: $after) {
projects(
search: $search
sort: $sort
first: $first
last: $last
before: $before
after: $after
) {
nodes {
id
avatarUrl

View File

@ -1,22 +1,91 @@
<script>
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getModelVersionQuery from '~/ml/model_registry/graphql/queries/get_model_version.query.graphql';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { makeLoadVersionsErrorMessage } from '~/ml/model_registry/translations';
import ModelVersionDetail from '../components/model_version_detail.vue';
import LoadOrErrorOrShow from '../components/load_or_error_or_show.vue';
export default {
name: 'ShowMlModelVersionApp',
components: {
LoadOrErrorOrShow,
ModelVersionDetail,
TitleArea,
},
provide() {
return {
projectPath: this.projectPath,
};
},
props: {
modelVersion: {
type: Object,
modelId: {
type: Number,
required: true,
},
modelVersionId: {
type: Number,
required: true,
},
versionName: {
type: String,
required: true,
},
modelName: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
apollo: {
modelWithModelVersion: {
query: getModelVersionQuery,
variables() {
return this.queryVariables;
},
update(data) {
return data?.mlModel;
},
error(error) {
this.handleError(error);
},
},
},
data() {
return {
modelWithModelVersion: {},
errorMessage: '',
};
},
computed: {
modelVersion() {
return this.modelWithModelVersion?.version;
},
isLoading() {
return this.$apollo.queries.modelWithModelVersion.loading;
},
title() {
return `${this.modelVersion.model.name} / ${this.modelVersion.version}`;
return `${this.modelName} / ${this.versionName}`;
},
queryVariables() {
return {
modelId: convertToGraphQLId('Ml::Model', this.modelId),
modelVersionId: convertToGraphQLId('Ml::ModelVersion', this.modelVersionId),
};
},
},
methods: {
handleError(error) {
this.errorMessage = makeLoadVersionsErrorMessage(error.message);
Sentry.captureException(error, {
tags: {
vue_component: 'show_ml_model_version',
},
});
},
},
};
@ -25,6 +94,8 @@ export default {
<template>
<div>
<title-area :title="title" />
<model-version-detail :model-version="modelVersion" />
<load-or-error-or-show :is-loading="isLoading" :error-message="errorMessage">
<model-version-detail :model-version="modelVersion" />
</load-or-error-or-show>
</div>
</template>

View File

@ -1,5 +1,6 @@
<script>
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { convertCandidateFromGraphql } from '~/ml/model_registry/utils';
import { convertToGraphQLId, isGid } from '~/graphql_shared/utils';
import { TYPENAME_PACKAGES_PACKAGE } from '~/graphql_shared/constants';
import * as i18n from '../translations';
import CandidateDetail from './candidate_detail.vue';
@ -11,6 +12,7 @@ export default {
import('~/packages_and_registries/package_registry/components/details/package_files.vue'),
CandidateDetail,
},
inject: ['projectPath'],
props: {
modelVersion: {
type: Object,
@ -18,15 +20,26 @@ export default {
},
},
computed: {
packageId() {
return convertToGraphQLId(TYPENAME_PACKAGES_PACKAGE, this.modelVersion.packageId);
},
projectPath() {
return this.modelVersion.projectPath;
},
packageType() {
return 'ml_model';
},
isFromGraphql() {
return isGid(this.modelVersion.id);
},
candidate() {
if (this.isFromGraphql) {
return convertCandidateFromGraphql(this.modelVersion.candidate);
}
return this.modelVersion.candidate;
},
packageId() {
if (this.isFromGraphql) {
return this.modelVersion.packageId;
}
return convertToGraphQLId(TYPENAME_PACKAGES_PACKAGE, this.modelVersion.packageId);
},
},
i18n,
};
@ -53,9 +66,9 @@ export default {
<div class="gl-mt-5">
<span class="gl-font-weight-bold">{{ $options.i18n.MLFLOW_ID_LABEL }}:</span>
{{ modelVersion.candidate.info.eid }}
{{ candidate.info.eid }}
</div>
<candidate-detail :candidate="modelVersion.candidate" :show-info-section="false" />
<candidate-detail :candidate="candidate" :show-info-section="false" />
</div>
</template>

View File

@ -0,0 +1,69 @@
query getModelVersion($modelId: MlModelID!, $modelVersionId: MlModelVersionID!) {
mlModel(id: $modelId) {
id
name
version(modelVersionId: $modelVersionId) {
id
version
packageId
description
candidate {
id
name
iid
eid
status
params {
nodes {
id
name
value
}
}
metadata {
nodes {
id
name
value
}
}
metrics {
nodes {
id
name
value
step
}
}
ciJob {
id
webPath
name
pipeline {
id
mergeRequest {
id
iid
title
webUrl
}
user {
id
avatarUrl
webUrl
username
name
}
}
}
_links {
showPath
artifactPath
}
}
_links {
showPath
}
}
}
}

View File

@ -0,0 +1,53 @@
export function convertCandidateFromGraphql(graphqlCandidate) {
const { iid, eid, status, ciJob } = graphqlCandidate;
const links = graphqlCandidate._links;
let ciJobValues = null;
if (ciJob) {
let userInfo = null;
let mergeRequestInfo = null;
const user = ciJob?.pipeline.user;
const mr = ciJob?.pipeline.mergeRequest;
if (user) {
userInfo = {
avatar: user.avatarUrl,
path: user.webUrl,
username: user.username,
name: user.name,
};
}
if (mr) {
mergeRequestInfo = {
title: mr.title,
path: mr.webUrl,
iid: mr.iid,
};
}
ciJobValues = {
name: ciJob.name,
path: ciJob.webPath,
user: userInfo,
mergeRequest: mergeRequestInfo,
};
}
return {
info: {
iid,
eid,
status,
experimentName: '',
pathToExperiment: '',
pathToArtifact: links.artifactPath,
path: links.showPath,
ciJob: ciJobValues,
},
metrics: graphqlCandidate.metrics.nodes,
params: graphqlCandidate.params.nodes,
metadata: graphqlCandidate.metadata.nodes,
};
}

View File

@ -4,8 +4,8 @@ import createProtectionRuleMutation from '~/packages_and_registries/settings/pro
import { s__, __ } from '~/locale';
const GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER = 'MAINTAINER';
const GRAPHQL_ACCESS_LEVEL_VALUE_DEVELOPER = 'DEVELOPER';
const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
const GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN = 'ADMIN';
export default {
components: {
@ -26,8 +26,8 @@ export default {
return {
protectionRuleFormData: {
repositoryPathPattern: '',
pushProtectedUpToAccessLevel: GRAPHQL_ACCESS_LEVEL_VALUE_DEVELOPER,
deleteProtectedUpToAccessLevel: GRAPHQL_ACCESS_LEVEL_VALUE_DEVELOPER,
minimumAccessLevelForPush: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
minimumAccessLevelForDelete: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
},
updateInProgress: false,
alertErrorMessage: '',
@ -50,15 +50,15 @@ export default {
return {
projectPath: this.projectPath,
repositoryPathPattern: this.protectionRuleFormData.repositoryPathPattern,
pushProtectedUpToAccessLevel: this.protectionRuleFormData.pushProtectedUpToAccessLevel,
deleteProtectedUpToAccessLevel: this.protectionRuleFormData.deleteProtectedUpToAccessLevel,
minimumAccessLevelForPush: this.protectionRuleFormData.minimumAccessLevelForPush,
minimumAccessLevelForDelete: this.protectionRuleFormData.minimumAccessLevelForDelete,
};
},
protectedUpToAccessLevelOptions() {
minimumAccessLevelOptions() {
return [
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_DEVELOPER, text: __('Developer') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: __('Admin') },
];
},
},
@ -130,28 +130,28 @@ export default {
</gl-form-group>
<gl-form-group
:label="s__('ContainerRegistry|Maximum access level prevented from pushing')"
label-for="input-push-protected-up-to-access-level"
:label="s__('ContainerRegistry|Minimum access level for push')"
label-for="input-minimum-access-level-for-push"
:disabled="isFieldDisabled"
>
<gl-form-select
id="input-push-protected-up-to-access-level"
v-model="protectionRuleFormData.pushProtectedUpToAccessLevel"
:options="protectedUpToAccessLevelOptions"
id="input-minimum-access-level-for-push"
v-model="protectionRuleFormData.minimumAccessLevelForPush"
:options="minimumAccessLevelOptions"
:disabled="isFieldDisabled"
required
/>
</gl-form-group>
<gl-form-group
:label="s__('ContainerRegistry|Maximum access level prevented from deleting')"
label-for="input-delete-protected-up-to-access-level"
:label="s__('ContainerRegistry|Minimum access level for delete')"
label-for="input-minimum-access-level-for-delete"
:disabled="isFieldDisabled"
>
<gl-form-select
id="input-delete-protected-up-to-access-level"
v-model="protectionRuleFormData.deleteProtectedUpToAccessLevel"
:options="protectedUpToAccessLevelOptions"
id="input-minimum-access-level-for-delete"
v-model="protectionRuleFormData.minimumAccessLevelForDelete"
:options="minimumAccessLevelOptions"
:disabled="isFieldDisabled"
required
/>

View File

@ -17,17 +17,15 @@ import { s__, __ } from '~/locale';
const PAGINATION_DEFAULT_PER_PAGE = 10;
const I18N_PUSH_PROTECTED_UP_TO_ACCESS_LEVEL = s__(
'ContainerRegistry|Push protected up to access level',
);
const I18N_DELETE_PROTECTED_UP_TO_ACCESS_LEVEL = s__(
'ContainerRegistry|Delete protected up to access level',
const I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH = s__('ContainerRegistry|Minimum access level for push');
const I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE = s__(
'ContainerRegistry|Minimum access level for delete',
);
const ACCESS_LEVEL_GRAPHQL_VALUE_TO_LABEL = {
DEVELOPER: __('Developer'),
MAINTAINER: __('Maintainer'),
OWNER: __('Owner'),
ADMIN: __('Admin'),
};
export default {
@ -93,10 +91,10 @@ export default {
return this.protectionRulesQueryResult.map((protectionRule) => {
return {
id: protectionRule.id,
deleteProtectedUpToAccessLevel:
ACCESS_LEVEL_GRAPHQL_VALUE_TO_LABEL[protectionRule.deleteProtectedUpToAccessLevel],
pushProtectedUpToAccessLevel:
ACCESS_LEVEL_GRAPHQL_VALUE_TO_LABEL[protectionRule.pushProtectedUpToAccessLevel],
minimumAccessLevelForDelete:
ACCESS_LEVEL_GRAPHQL_VALUE_TO_LABEL[protectionRule.minimumAccessLevelForDelete],
minimumAccessLevelForPush:
ACCESS_LEVEL_GRAPHQL_VALUE_TO_LABEL[protectionRule.minimumAccessLevelForPush],
repositoryPathPattern: protectionRule.repositoryPathPattern,
};
});
@ -166,7 +164,7 @@ export default {
this.protectionRuleMutationItem = null;
this.protectionRuleMutationInProgress = false;
},
isProtectionRulePushProtectedUpToAccessLevelFormSelectDisabled(item) {
isProtectionRuleMinimumAccessLevelForPushFormSelectDisabled(item) {
return this.isProtectionRuleMutationInProgress(item);
},
isProtectionRuleDeleteButtonDisabled(item) {
@ -209,13 +207,13 @@ export default {
tdClass: 'gl-vertical-align-middle!',
},
{
key: 'pushProtectedUpToAccessLevel',
label: I18N_PUSH_PROTECTED_UP_TO_ACCESS_LEVEL,
key: 'minimumAccessLevelForPush',
label: I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH,
tdClass: 'gl-vertical-align-middle!',
},
{
key: 'deleteProtectedUpToAccessLevel',
label: I18N_DELETE_PROTECTED_UP_TO_ACCESS_LEVEL,
key: 'minimumAccessLevelForDelete',
label: I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE,
tdClass: 'gl-vertical-align-middle!',
},
{

View File

@ -3,8 +3,8 @@ mutation createContainerProtectionRule($input: CreateContainerRegistryProtection
containerRegistryProtectionRule {
id
repositoryPathPattern
pushProtectedUpToAccessLevel
deleteProtectedUpToAccessLevel
minimumAccessLevelForPush
minimumAccessLevelForDelete
}
errors
}

View File

@ -5,8 +5,8 @@ mutation deleteContainerRegistryProtectionRule(
containerRegistryProtectionRule {
id
repositoryPathPattern
pushProtectedUpToAccessLevel
deleteProtectedUpToAccessLevel
minimumAccessLevelForPush
minimumAccessLevelForDelete
}
errors
}

View File

@ -13,8 +13,8 @@ query getProjectContainerProtectionRules(
nodes {
id
repositoryPathPattern
pushProtectedUpToAccessLevel
deleteProtectedUpToAccessLevel
minimumAccessLevelForPush
minimumAccessLevelForDelete
}
pageInfo {
...PageInfo

View File

@ -7,18 +7,7 @@ export const todoLabel = (hasTodo) => {
return hasTodo ? __('Mark as done') : __('Add a to do');
};
export const updateGlobalTodoCount = (additionalTodoCount) => {
const countContainer = document.querySelector('.js-todos-count');
if (countContainer === null) return;
const currentCount = parseInt(countContainer.innerText, 10) || 0;
const todoToggleEvent = new CustomEvent('todo:toggle', {
detail: {
count: Math.max(currentCount + additionalTodoCount, 0),
},
});
document.dispatchEvent(todoToggleEvent);
export const updateGlobalTodoCount = (delta) => {
// Optimistic update of user counts
document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { delta } }));
};

View File

@ -86,17 +86,10 @@ export default {
Object.assign(userCounts, this.sidebarData.user_counts);
createUserCountsManager();
},
mounted() {
document.addEventListener('todo:toggle', this.updateTodos);
},
beforeDestroy() {
document.removeEventListener('todo:toggle', this.updateTodos);
destroyUserCountsManager();
},
methods: {
updateTodos(e) {
userCounts.todos = e.detail.count || 0;
},
hideSearchTooltip() {
this.searchTooltip = '';
},

View File

@ -41,8 +41,17 @@ async function retrieveUserCountsFromApi() {
}
}
function updateTodos(e) {
if (Number.isSafeInteger(e?.detail?.count)) {
userCounts.todos = Math.max(e.detail.count, 0);
} else if (Number.isSafeInteger(e?.detail?.delta)) {
userCounts.todos = Math.max(userCounts.todos + e.detail.delta, 0);
}
}
export function destroyUserCountsManager() {
document.removeEventListener('userCounts:fetch', retrieveUserCountsFromApi);
document.removeEventListener('todo:toggle', updateTodos);
broadcastChannel?.close();
broadcastChannel = null;
}
@ -58,6 +67,7 @@ export function destroyUserCountsManager() {
export function createUserCountsManager() {
destroyUserCountsManager();
document.addEventListener('userCounts:fetch', retrieveUserCountsFromApi);
document.addEventListener('todo:toggle', updateTodos);
if (window.BroadcastChannel && gon?.current_user_id) {
broadcastChannel = new BroadcastChannel(`user_counts_${gon?.current_user_id}`);

View File

@ -3,6 +3,7 @@ import produce from 'immer';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import { s__ } from '~/locale';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
import { updateGlobalTodoCount } from '~/sidebar/utils';
import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql';
import alertQuery from '../../graphql/queries/alert_sidebar_details.query.graphql';
@ -52,17 +53,6 @@ export default {
},
},
methods: {
updateToDoCount(add) {
const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10) || 0;
const count = add ? oldCount + 1 : oldCount - 1;
const headerTodoEvent = new CustomEvent('todo:toggle', {
detail: {
count: Math.max(count, 0),
},
});
document.dispatchEvent(headerTodoEvent);
},
addToDo() {
this.isUpdating = true;
return this.$apollo
@ -78,7 +68,7 @@ export default {
this.throwError(errors[0]);
return;
}
this.updateToDoCount(true);
updateGlobalTodoCount(1);
})
.catch(() => {
this.throwError();
@ -102,7 +92,7 @@ export default {
this.throwError(errors[0]);
return;
}
this.updateToDoCount(false);
updateGlobalTodoCount(-1);
})
.catch(() => {
this.throwError();

View File

@ -25,19 +25,19 @@ module Mutations
'Container repository path pattern protected by the protection rule. ' \
'For example `my-project/my-container-*`. Wildcard character `*` allowed.'
argument :push_protected_up_to_access_level,
argument :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: true,
description:
'Max GitLab access level to prevent from pushing container images to the container registry. ' \
'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
'Minimum GitLab access level to allow to push container images to the container registry. ' \
'For example `MAINTAINER`, `OWNER`, or `ADMIN`.'
argument :delete_protected_up_to_access_level,
argument :minimum_access_level_for_delete,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: true,
description:
'Max GitLab access level to prevent from deleting container images in the container registry. ' \
'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
'Minimum GitLab access level to allow to delete container images in the container registry. ' \
'For example `MAINTAINER`, `OWNER`, or `ADMIN`.'
field :container_registry_protection_rule,
Types::ContainerRegistry::Protection::RuleType,

View File

@ -26,21 +26,21 @@ module Mutations
'For example, `my-scope/my-project/container-dev-*`. ' \
'Wildcard character `*` allowed.'
argument :delete_protected_up_to_access_level,
argument :minimum_access_level_for_delete,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: false,
validates: { allow_blank: false },
description:
'Maximum GitLab access level prevented from deleting a container. ' \
'For example, `DEVELOPER`, `MAINTAINER`, `OWNER`.'
'Minimum GitLab access level allowed to delete container images to the container registry. ' \
'For example, `DEVELOPER`, `MAINTAINER`, `OWNER`, or `ADMIN`.'
argument :push_protected_up_to_access_level,
argument :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: false,
validates: { allow_blank: false },
description:
'Maximum GitLab access level prevented from pushing a container. ' \
'For example, `DEVELOPER`, `MAINTAINER`, `OWNER`.'
'Minimum GitLab access level allowed to push container images to the container registry. ' \
'For example, `DEVELOPER`, `MAINTAINER`, `OWNER`, or `ADMIN`.'
field :container_registry_protection_rule,
Types::ContainerRegistry::Protection::RuleType,

View File

@ -13,17 +13,6 @@ module Resolvers
alias_method :runner, :object
argument :sort, GraphQL::Types::String,
required: false,
default_value: 'id_asc', # TODO: Remove in %17.0 and move :sort to ProjectSearchArguments, see https://gitlab.com/gitlab-org/gitlab/-/issues/372117
deprecated: {
reason: 'Default sort order will change in GitLab 17.0. ' \
'Specify `"id_asc"` if you require the query results to be ordered by ascending IDs',
milestone: '15.4'
},
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
"for example: `id_desc` or `name_asc`"
def resolve_with_lookahead(**args)
return unless runner.project_type?

View File

@ -23,6 +23,12 @@ module ProjectSearchArguments
argument :personal, GraphQL::Types::Boolean,
required: false,
description: 'Return only personal projects.'
argument :sort, GraphQL::Types::String,
required: false,
default_value: 'id_desc',
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
"for example: `id_desc` or `name_asc`"
end
private

View File

@ -15,11 +15,6 @@ module Resolvers
required: false,
description: 'Filter projects by full paths. You cannot provide more than 50 full paths.'
argument :sort, GraphQL::Types::String,
required: false,
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
"for example: `id_desc` or `name_asc`"
argument :with_issues_enabled, GraphQL::Types::Boolean,
required: false,
description: "Return only projects with issues enabled."

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'ContainerRegistryProtectionRuleAccessLevel'
description 'Access level of a container registry protection rule resource'
::ContainerRegistry::Protection::Rule.push_protected_up_to_access_levels.each_key do |access_level_key|
::ContainerRegistry::Protection::Rule.minimum_access_level_for_pushes.each_key do |access_level_key|
value access_level_key.upcase, value: access_level_key.to_s,
description: "#{access_level_key.capitalize} access."
end

View File

@ -22,19 +22,19 @@ module Types
'Container repository path pattern protected by the protection rule. ' \
'For example `my-project/my-container-*`. Wildcard character `*` allowed.'
field :push_protected_up_to_access_level,
field :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
null: false,
description:
'Max GitLab access level to prevent from pushing container images to the container registry. ' \
'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
'Minimum GitLab access level to allow to push container images to the container registry. ' \
'For example `DEVELOPER`, `MAINTAINER`, or `OWNER`.'
field :delete_protected_up_to_access_level,
field :minimum_access_level_for_delete,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
null: false,
description:
'Max GitLab access level to prevent from pushing container images to the container registry. ' \
'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
'Minimum GitLab access level to allow to push container images to the container registry. ' \
'For example `DEVELOPER`, `MAINTAINER`, or `OWNER`.'
end
end
end

View File

@ -4,21 +4,10 @@ module Types
class Namespace::SharedRunnersSettingEnum < BaseEnum
graphql_name 'SharedRunnersSetting'
DEPRECATED_SETTINGS = [::Namespace::SR_DISABLED_WITH_OVERRIDE].freeze
::Namespace::SHARED_RUNNERS_SETTINGS.excluding(DEPRECATED_SETTINGS).each do |type|
::Namespace::SHARED_RUNNERS_SETTINGS.each do |type|
value type.upcase,
description: "Sharing of runners is #{type.tr('_', ' ')}.",
value: type
end
value ::Namespace::SR_DISABLED_WITH_OVERRIDE.upcase,
description: "Sharing of runners is disabled and overridable.",
value: ::Namespace::SR_DISABLED_WITH_OVERRIDE,
deprecated: {
reason: :renamed,
replacement: ::Namespace::SR_DISABLED_AND_OVERRIDABLE,
milestone: "17.0"
}
end
end

View File

@ -39,6 +39,21 @@ module Projects
to_json(data)
end
def show_ml_model_version_data(model_version, user)
project = model_version.project
data = {
project_path: project.full_path,
model_id: model_version.model.id,
model_version_id: model_version.id,
model_name: model_version.name,
version_name: model_version.version,
can_write_model_registry: can_write_model_registry?(user, project)
}
to_json(data)
end
private
def can_write_model_registry?(user, project)

View File

@ -3,12 +3,16 @@
module ContainerRegistry
module Protection
class Rule < ApplicationRecord
enum delete_protected_up_to_access_level:
Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer),
_prefix: :delete_protected_up_to
enum push_protected_up_to_access_level:
Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer),
_prefix: :push_protected_up_to
include IgnorableColumns
ignore_columns %i[push_protected_up_to_access_level delete_protected_up_to_access_level],
remove_with: '17.2', remove_after: '2024-06-22'
enum minimum_access_level_for_delete:
Gitlab::Access.sym_options_with_admin.slice(:maintainer, :owner, :admin),
_prefix: :minimum_access_level_for_delete
enum minimum_access_level_for_push:
Gitlab::Access.sym_options_with_admin.slice(:maintainer, :owner, :admin),
_prefix: :minimum_access_level_for_push
belongs_to :project, inverse_of: :container_registry_protection_rules
@ -21,8 +25,8 @@ module ContainerRegistry
message:
->(_object, _data) { _('should be a valid container repository path with optional wildcard characters.') }
}
validates :delete_protected_up_to_access_level, presence: true
validates :push_protected_up_to_access_level, presence: true
validates :minimum_access_level_for_delete, presence: true
validates :minimum_access_level_for_push, presence: true
validate :path_pattern_starts_with_project_full_path, if: :repository_path_pattern_changed?
@ -38,7 +42,7 @@ module ContainerRegistry
def self.for_push_exists?(access_level:, repository_path:)
return false if access_level.blank? || repository_path.blank?
where(push_protected_up_to_access_level: access_level..)
where(':access_level < minimum_access_level_for_push', access_level: access_level)
.for_repository_path(repository_path)
.exists?
end

View File

@ -38,11 +38,9 @@ class Namespace < ApplicationRecord
NUMBER_OF_ANCESTORS_ALLOWED = 20
SR_DISABLED_AND_UNOVERRIDABLE = 'disabled_and_unoverridable'
# DISABLED_WITH_OVERRIDE is deprecated in favour of DISABLED_AND_OVERRIDABLE.
SR_DISABLED_WITH_OVERRIDE = 'disabled_with_override'
SR_DISABLED_AND_OVERRIDABLE = 'disabled_and_overridable'
SR_ENABLED = 'enabled'
SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE, SR_ENABLED].freeze
SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_AND_OVERRIDABLE, SR_ENABLED].freeze
URL_MAX_LENGTH = 255
cache_markdown_field :description, pipeline: :description
@ -631,10 +629,10 @@ class Namespace < ApplicationRecord
case other_setting
when SR_ENABLED
false
when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE
when SR_DISABLED_AND_OVERRIDABLE
shared_runners_setting == SR_ENABLED
when SR_DISABLED_AND_UNOVERRIDABLE
shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_AND_OVERRIDABLE || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE
shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_AND_OVERRIDABLE
else
raise ArgumentError
end

View File

@ -5,8 +5,8 @@ module ContainerRegistry
class CreateRuleService < BaseService
ALLOWED_ATTRIBUTES = %i[
repository_path_pattern
push_protected_up_to_access_level
delete_protected_up_to_access_level
minimum_access_level_for_push
minimum_access_level_for_delete
].freeze
def execute

View File

@ -7,8 +7,8 @@ module ContainerRegistry
ALLOWED_ATTRIBUTES = %i[
repository_path_pattern
delete_protected_up_to_access_level
push_protected_up_to_access_level
minimum_access_level_for_delete
minimum_access_level_for_push
].freeze
def initialize(container_registry_protection_rule, current_user:, params:)

View File

@ -28,7 +28,7 @@ module Groups
case params[:shared_runners_setting]
when Namespace::SR_DISABLED_AND_UNOVERRIDABLE
set_shared_runners_enabled!(false)
when Namespace::SR_DISABLED_WITH_OVERRIDE, Namespace::SR_DISABLED_AND_OVERRIDABLE
when Namespace::SR_DISABLED_AND_OVERRIDABLE
disable_shared_runners_and_allow_override!
when Namespace::SR_ENABLED
set_shared_runners_enabled!(true)

View File

@ -3,4 +3,4 @@
- breadcrumb_title @model_version.version
- page_title "#{@model_version.name} / #{@model_version.version}"
= render(Projects::Ml::ShowMlModelVersionComponent.new(model_version: @model_version, current_user: current_user))
#js-mount-show-ml-model-version{ data: { view_model: show_ml_model_version_data(@model_version, @current_user) } }

View File

@ -0,0 +1,9 @@
---
name: internal_events_batching
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413064
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149979
rollout_issue_url:
milestone: '17.0'
group: group::analytics instrumentation
type: ops
default_enabled: true

View File

@ -1,15 +1,12 @@
---
data_category: optional
key_path: search_unique_visits.search_unique_visits_for_any_target_monthly
description: Total unique users for i_search_total, i_search_advanced, i_search_paid
for recent 28 days. This metric is redundant because advanced will be a subset of
paid and paid will be a subset of total. i_search_total is more appropriate if you
just want the total
description: Removed as duplicate of redis_hll_counters.search.search_total_unique_counts_monthly
product_section: enablement
product_stage: enablement
product_group: global_search
value_type: number
status: active
status: removed
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
@ -28,3 +25,5 @@ tier:
- premium
- ultimate
milestone: "<13.9"
milestone_removed: "17.0"
removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150302

View File

@ -1,12 +1,12 @@
---
data_category: optional
key_path: search_unique_visits.i_search_total
description: Calculated unique users to perform Basic or Advanced searches by week
description: Removed as duplicate of redis_hll_counters.search.i_search_total_weekly
product_section: enablement
product_stage: enablement
product_group: global_search
value_type: number
status: active
status: removed
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
@ -21,3 +21,5 @@ tier:
- premium
- ultimate
milestone: "<13.9"
milestone_removed: "17.0"
removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150302

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RenameContainerProtectionRulesProtectedAccessLevelToMinimumAccessLevel < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
TABLE = :container_registry_protection_rules
def up
rename_column_concurrently TABLE, :push_protected_up_to_access_level, :minimum_access_level_for_push
rename_column_concurrently TABLE, :delete_protected_up_to_access_level, :minimum_access_level_for_delete
end
def down
undo_rename_column_concurrently TABLE, :push_protected_up_to_access_level, :minimum_access_level_for_push
undo_rename_column_concurrently TABLE, :delete_protected_up_to_access_level, :minimum_access_level_for_delete
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
class MigrateContainerProtectionRulesMinimumAccessLevel < Gitlab::Database::Migration[2.2]
milestone '17.0'
restrict_gitlab_migration gitlab_schema: :gitlab_main
DEVELOPER = 30
MAINTAINER = 40
OWNER = 50
ADMIN = 60
class ContainerProtectionRule < MigrationRecord
self.table_name = 'container_registry_protection_rules'
end
def migrate_minimum_access_level_for_push(old_access_level, new_access_level)
ContainerProtectionRule.where(push_protected_up_to_access_level: old_access_level)
.update_all(minimum_access_level_for_push: new_access_level)
end
def undo_migrate_minimum_access_level_for_push(old_access_level, new_access_level)
ContainerProtectionRule.where(minimum_access_level_for_push: new_access_level)
.update_all(push_protected_up_to_access_level: old_access_level)
end
def migrate_minimum_access_level_for_delete(old_access_level, new_access_level)
ContainerProtectionRule.where(delete_protected_up_to_access_level: old_access_level)
.update_all(minimum_access_level_for_delete: new_access_level)
end
def undo_migrate_minimum_access_level_for_delete(old_access_level, new_access_level)
ContainerProtectionRule.where(minimum_access_level_for_delete: new_access_level)
.update_all(delete_protected_up_to_access_level: old_access_level)
end
def up
migrate_minimum_access_level_for_push(OWNER, ADMIN)
migrate_minimum_access_level_for_push(MAINTAINER, OWNER)
migrate_minimum_access_level_for_push(DEVELOPER, MAINTAINER)
migrate_minimum_access_level_for_delete(OWNER, ADMIN)
migrate_minimum_access_level_for_delete(MAINTAINER, OWNER)
migrate_minimum_access_level_for_delete(DEVELOPER, MAINTAINER)
end
def down
undo_migrate_minimum_access_level_for_push(DEVELOPER, MAINTAINER)
undo_migrate_minimum_access_level_for_push(MAINTAINER, OWNER)
undo_migrate_minimum_access_level_for_push(OWNER, ADMIN)
undo_migrate_minimum_access_level_for_delete(DEVELOPER, MAINTAINER)
undo_migrate_minimum_access_level_for_delete(MAINTAINER, OWNER)
undo_migrate_minimum_access_level_for_delete(OWNER, ADMIN)
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class CleanupContainerRegistryProtectionRuleProtectedUpToAccessLevelsRename < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
TABLE = :container_registry_protection_rules
def up
cleanup_concurrent_column_rename TABLE, :push_protected_up_to_access_level, :minimum_access_level_for_push
cleanup_concurrent_column_rename TABLE, :delete_protected_up_to_access_level, :minimum_access_level_for_delete
end
def down
undo_cleanup_concurrent_column_rename TABLE, :push_protected_up_to_access_level, :minimum_access_level_for_push
undo_cleanup_concurrent_column_rename TABLE, :delete_protected_up_to_access_level, :minimum_access_level_for_delete
end
end

View File

@ -0,0 +1 @@
547893dac5a7f811804bf94201da966c8d3d71804851c66db37ef76d7bfacf42

View File

@ -0,0 +1 @@
134ea93b7ebfcec84d0d5c5b19f1b2a1e11ff0d042c2bd52a86e680af85fa0dd

View File

@ -0,0 +1 @@
a166217f45ce83e9c0b5aea3292fc6f97f8f21719f1eee0f3879a333884c206c

View File

@ -7418,11 +7418,13 @@ CREATE TABLE container_registry_protection_rules (
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
delete_protected_up_to_access_level smallint NOT NULL,
push_protected_up_to_access_level smallint NOT NULL,
repository_path_pattern text,
minimum_access_level_for_push smallint,
minimum_access_level_for_delete smallint,
CONSTRAINT check_3658b31291 CHECK ((repository_path_pattern IS NOT NULL)),
CONSTRAINT check_d53a270af5 CHECK ((char_length(repository_path_pattern) <= 255))
CONSTRAINT check_d53a270af5 CHECK ((char_length(repository_path_pattern) <= 255)),
CONSTRAINT check_d82c1eb825 CHECK ((minimum_access_level_for_delete IS NOT NULL)),
CONSTRAINT check_f684912b48 CHECK ((minimum_access_level_for_push IS NOT NULL))
);
CREATE SEQUENCE container_registry_protection_rules_id_seq

View File

@ -2959,9 +2959,9 @@ Input type: `CreateContainerRegistryProtectionRuleInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcreatecontainerregistryprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcreatecontainerregistryprotectionruledeleteprotecteduptoaccesslevel"></a>`deleteProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from deleting container images in the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
| <a id="mutationcreatecontainerregistryprotectionruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to delete container images in the container registry. For example `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="mutationcreatecontainerregistryprotectionruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to push container images to the container registry. For example `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="mutationcreatecontainerregistryprotectionruleprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project where a protection rule is located. |
| <a id="mutationcreatecontainerregistryprotectionrulepushprotecteduptoaccesslevel"></a>`pushProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from pushing container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
| <a id="mutationcreatecontainerregistryprotectionrulerepositorypathpattern"></a>`repositoryPathPattern` | [`String!`](#string) | Container repository path pattern protected by the protection rule. For example `my-project/my-container-*`. Wildcard character `*` allowed. |
#### Fields
@ -8416,9 +8416,9 @@ Input type: `UpdateContainerRegistryProtectionRuleInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationupdatecontainerregistryprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationupdatecontainerregistryprotectionruledeleteprotecteduptoaccesslevel"></a>`deleteProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | Maximum GitLab access level prevented from deleting a container. For example, `DEVELOPER`, `MAINTAINER`, `OWNER`. |
| <a id="mutationupdatecontainerregistryprotectionruleid"></a>`id` | [`ContainerRegistryProtectionRuleID!`](#containerregistryprotectionruleid) | Global ID of the container registry protection rule to be updated. |
| <a id="mutationupdatecontainerregistryprotectionrulepushprotecteduptoaccesslevel"></a>`pushProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | Maximum GitLab access level prevented from pushing a container. For example, `DEVELOPER`, `MAINTAINER`, `OWNER`. |
| <a id="mutationupdatecontainerregistryprotectionruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level allowed to delete container images to the container registry. For example, `DEVELOPER`, `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="mutationupdatecontainerregistryprotectionruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level allowed to push container images to the container registry. For example, `DEVELOPER`, `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="mutationupdatecontainerregistryprotectionrulerepositorypathpattern"></a>`repositoryPathPattern` | [`String`](#string) | Container's repository path pattern of the protection rule. For example, `my-scope/my-project/container-dev-*`. Wildcard character `*` allowed. |
#### Fields
@ -17373,7 +17373,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="cirunnerprojectspersonal"></a>`personal` | [`Boolean`](#boolean) | Return only personal projects. |
| <a id="cirunnerprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
| <a id="cirunnerprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
| <a id="cirunnerprojectssort"></a>`sort` **{warning-solid}** | [`String`](#string) | **Deprecated** in GitLab 15.4. Default sort order will change in GitLab 17.0. Specify `"id_asc"` if you require the query results to be ordered by ascending IDs. |
| <a id="cirunnerprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. |
| <a id="cirunnerprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. |
##### `CiRunner.status`
@ -18038,9 +18038,9 @@ A container registry protection rule designed to prevent users with a certain ac
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="containerregistryprotectionruledeleteprotecteduptoaccesslevel"></a>`deleteProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from pushing container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
| <a id="containerregistryprotectionruleid"></a>`id` | [`ContainerRegistryProtectionRuleID!`](#containerregistryprotectionruleid) | ID of the container registry protection rule. |
| <a id="containerregistryprotectionrulepushprotecteduptoaccesslevel"></a>`pushProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from pushing container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
| <a id="containerregistryprotectionruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to push container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, or `OWNER`. |
| <a id="containerregistryprotectionruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to push container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, or `OWNER`. |
| <a id="containerregistryprotectionrulerepositorypathpattern"></a>`repositoryPathPattern` | [`String!`](#string) | Container repository path pattern protected by the protection rule. For example `my-project/my-container-*`. Wildcard character `*` allowed. |
### `ContainerRepository`
@ -32288,7 +32288,7 @@ Access level of a container registry protection rule resource.
| Value | Description |
| ----- | ----------- |
| <a id="containerregistryprotectionruleaccessleveldeveloper"></a>`DEVELOPER` | Developer access. |
| <a id="containerregistryprotectionruleaccessleveladmin"></a>`ADMIN` | Admin access. |
| <a id="containerregistryprotectionruleaccesslevelmaintainer"></a>`MAINTAINER` | Maintainer access. |
| <a id="containerregistryprotectionruleaccesslevelowner"></a>`OWNER` | Owner access. |
@ -33933,7 +33933,6 @@ How to format SHA strings.
| ----- | ----------- |
| <a id="sharedrunnerssettingdisabled_and_overridable"></a>`DISABLED_AND_OVERRIDABLE` | Sharing of runners is disabled and overridable. |
| <a id="sharedrunnerssettingdisabled_and_unoverridable"></a>`DISABLED_AND_UNOVERRIDABLE` | Sharing of runners is disabled and unoverridable. |
| <a id="sharedrunnerssettingdisabled_with_override"></a>`DISABLED_WITH_OVERRIDE` **{warning-solid}** | **Deprecated** in GitLab 17.0. This was renamed. Use: `disabled_and_overridable`. |
| <a id="sharedrunnerssettingenabled"></a>`ENABLED` | Sharing of runners is enabled. |
### `SnippetBlobActionEnum`

View File

@ -23,7 +23,7 @@ that should be kept confidential. Examples of a secret include:
Secrets that are the most sensitive and under the strictest policies should be stored
in a secrets manager. When using a secrets manager solution, secrets are stored outside
of the GitLab instance. There are a number of providres in this space, including
of the GitLab instance. There are a number of providers in this space, including
[HashiCorp's Vault](https://www.vaultproject.io), [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault),
and [Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager).

View File

@ -37,11 +37,12 @@ If you have any new or updated prompts, ask members of AI Framework team to revi
When working with Chat locally, you might run into an error. Most commons problems are documented in this section.
If you find an undocumented issue, you should document it in this section after you find a solution.
| Problem | Solution |
| ----------------------------------------------------- | -------- |
| There is no Chat button in the GitLab UI. | Make sure your user is a part of a group with enabled Experimental and Beta features. |
| Chat replies with "Forbidden by auth provider" error. | Backend can't access LLMs. Make sure your [AI Gateway](index.md#local-setup) is setup correctly. |
| Requests takes too long to appear in UI | Consider restarting Sidekiq by running `gdk restart rails-background-jobs`. If that doesn't work, try `gdk kill` and then `gdk start`. Alternatively, you can bypass Sidekiq entirely. To do that temporary alter `Llm::CompletionWorker.perform_async` statements with `Llm::CompletionWorker.perform_inline` |
| Problem | Solution |
|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| There is no Chat button in the GitLab UI. | Make sure your user is a part of a group with enabled Experimental and Beta features. |
| Chat replies with "Forbidden by auth provider" error. | Backend can't access LLMs. Make sure your [AI Gateway](index.md#local-setup) is set up correctly. |
| Requests take too long to appear in UI | Consider restarting Sidekiq by running `gdk restart rails-background-jobs`. If that doesn't work, try `gdk kill` and then `gdk start`. Alternatively, you can bypass Sidekiq entirely. To do that temporary alter `Llm::CompletionWorker.perform_async` statements with `Llm::CompletionWorker.perform_inline` |
| There is no chat button in GitLab UI when GDK is running on non-SaaS mode | You do not have cloud connector access token record or seat assigned. To create cloud connector access record, in rails console put following code: `CloudConnector::Access.new(data: { available_services: [{ name: "duo_chat", serviceStartTime: ":date_in_the_future" }] }).save`. |
## Contributing to GitLab Duo Chat

View File

@ -56,7 +56,7 @@ Prerequisites:
1. Select **New role**.
1. In **Base role to use as template**, select an existing default role.
1. In **Role name**, enter the custom role's title.
1. Optional. In **Description**, enter a description for the custom role. 255 characters max.
1. In **Description**, enter a description for the custom role. 255 characters max.
1. Select the **Permissions** for the new custom role.
1. Select **Create role**.
@ -80,7 +80,7 @@ After you create a custom role for your self-managed instance, you can assign th
1. Select **New role**.
1. In **Base role to use as template**, select an existing default role.
1. In **Role name**, enter the custom role's title.
1. Optional. In **Description**, enter a description for the custom role. 255 characters max.
1. In **Description**, enter a description for the custom role. 255 characters max.
1. Select the **Permissions** for the new custom role.
1. Select **Create role**.
@ -93,6 +93,36 @@ In **Settings > Roles and Permissions**, the list of all custom roles displays t
To create a custom role, you can also [use the API](../api/graphql/reference/index.md#mutationmemberrolecreate).
## Edit a custom role
Custom roles can be edited after they are created. The base role can't be changed. If you need to change the base role, you will need to create a new custom role.
### GitLab SaaS
Prerequisites:
- You must have the Owner role for the group.
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > Roles and Permissions**.
1. Select the three dots for the custom role, then select **Edit role**.
1. Modify the role as needed.
1. Select **Save role** to update the role.
### GitLab self-managed
Prerequisites:
- You must be an administrator for the self-managed instance.
1. On the left sidebar, at the bottom, select **Admin Area**.
1. Select **Settings > Roles and Permissions**.
1. Select the three dots for the custom role, then select **Edit role**.
1. Modify the role as needed.
1. Select **Save role** to update the role.
To edit a custom role, you can also [use the API](../api/graphql/reference/index.md#mutationmemberroleupdate).
## Delete the custom role
Prerequisites:

View File

@ -133,7 +133,7 @@ To create a group:
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For details about groups, watch [GitLab Namespaces (users, groups and subgroups)](https://youtu.be/r0sJgjR2f5A).
## Edit group name and description
## Edit group name, description, and avatar
You can edit your group details from the group general settings.
@ -148,6 +148,7 @@ To edit group details:
1. In the **Group name** text box, enter your group name. See the [limitations on group names](../../user/reserved_names.md).
1. Optional. In the **Group description (optional)** text box, enter your group description.
The description is limited to 500 characters.
1. Optional. Under **Group avatar**, select **Choose file**, then select an image. The ideal image size is 192 x 192 pixels, and the maximum file size allowed is 200 KB.
1. Select **Save changes**.
## Leave a group

View File

@ -407,6 +407,33 @@ To view your activity:
- **Designs**: Designs you added, updated, and removed in your projects.
- **Team**: Projects you joined and left.
## Sign-in services
Instead of using a regular username and password to sign in to GitLab, you can use a sign-in service instead.
### Connect a sign-in service
To connect a sign-in service to use for signing in to GitLab:
1. On the left sidebar, select your avatar.
1. Select **Edit profile**.
1. Select **Account**.
1. Locate the **Service sign-in** section.
1. Under the **Connected Accounts** section, select the button that corresponds with the service you want to sign in
with.
1. Follow the instructions for the selected service to start signing in with it.
### Disconnect a sign-in service
To disconnect a sign-in service used for signing in to GitLab:
1. On the left sidebar, select your avatar.
1. Select **Edit profile**.
1. Select **Account**.
1. Locate the **Service sign-in** section.
1. Under the **Connected Accounts** section, select **Disconnect** next to the button that corresponds with the service
you no longer want to sign in with.
## Session duration
### Stay signed in for two weeks

View File

@ -52,13 +52,11 @@ module API
end
# Stores some Git-specific env thread-safely
env = parse_env
Gitlab::Git::HookEnv.set(gl_repository, env) if container
#
# Snapshot repositories have different relative path than the main repository. For access
# checks that need quarantined objects the relative path in also sent with Gitaly RPCs
# calls as a header.
populate_relative_path(params[:relative_path])
Gitlab::Git::HookEnv.set(gl_repository, params[:relative_path], parse_env) if container
actor.update_last_used_at!
@ -104,12 +102,6 @@ module API
end
# rubocop: enable Metrics/AbcSize
def populate_relative_path(relative_path)
return unless Gitlab::SafeRequestStore.active?
Gitlab::SafeRequestStore[:gitlab_git_relative_path] = relative_path
end
def validate_actor(actor)
return 'Could not find the given key' unless actor.key

View File

@ -75,6 +75,10 @@ module Gitlab
sym_options.merge(owner: OWNER)
end
def sym_options_with_admin
sym_options_with_owner.merge(admin: ADMIN)
end
def protection_options
[
{

View File

@ -19,13 +19,28 @@ module Gitlab
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze
def self.set(gl_repository, env)
# set stores the quarantining variables into request store.
#
# relative_path is sent from Gitaly to Rails when invoking internal API. In production it points to the
# transaction's snapshot repository. Tests should pass the original relative path of the repository as
# Gitaly is stubbed out from the invokation loop and doesn't create a transaction snapshot.
def self.set(gl_repository, relative_path, env)
return unless Gitlab::SafeRequestStore.active?
raise "missing gl_repository" if gl_repository.blank?
Gitlab::SafeRequestStore[:gitlab_git_env] ||= {}
Gitlab::SafeRequestStore[:gitlab_git_env][gl_repository] = allowlist_git_env(env)
Gitlab::SafeRequestStore[:gitlab_git_relative_path] = relative_path
end
# get_relative_path returns the relative path of the repository this hook call is triggered for.
# This is the repository's relative path in the transaction's snapshot and is passed back to Gitaly
# in quarantined calls.
def self.get_relative_path
return unless Gitlab::SafeRequestStore.active?
Gitlab::SafeRequestStore.fetch(:gitlab_git_relative_path)
end
def self.all(gl_repository)

View File

@ -7,6 +7,7 @@ module Gitlab
InvalidPropertyTypeError = Class.new(StandardError)
SNOWPLOW_EMITTER_BUFFER_SIZE = 100
DEFAULT_BUFFER_SIZE = 1
ALLOWED_ADDITIONAL_PROPERTIES = {
label: [String],
property: [String],
@ -161,7 +162,8 @@ module Gitlab
return unless app_id.present? && host.present?
GitlabSDK::Client.new(app_id: app_id, host: host, buffer_size: SNOWPLOW_EMITTER_BUFFER_SIZE)
buffer_size = Feature.enabled?(:internal_events_batching) ? SNOWPLOW_EMITTER_BUFFER_SIZE : DEFAULT_BUFFER_SIZE
GitlabSDK::Client.new(app_id: app_id, host: host, buffer_size: buffer_size)
end
strong_memoize_attr :gitlab_sdk_client
end

View File

@ -16,7 +16,7 @@ module Gitlab
return @client.push(job_hash) unless Gitlab::SidekiqSharding::Router.enabled?
job_class = job_hash["class"].to_s.safe_constantize
store_name = if job_class.nil? || job_class.ancestors.exclude?(ApplicationWorker)
store_name = if unroutable_class?(job_class)
'main'
else
job_class.get_sidekiq_options['store']
@ -25,6 +25,15 @@ module Gitlab
_, pool = Gitlab::SidekiqSharding::Router.get_shard_instance(store_name)
Sidekiq::Client.new(config: @config, pool: pool).push(job_hash)
end
def unroutable_class?(klass)
klass.nil? ||
(klass.ancestors.exclude?(ApplicationWorker) &&
# ActionMailer's ActiveJob pushes a job hash with
# class: ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper into
# the schedule set.
klass != ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper)
end
end
def initialize(container)

View File

@ -3352,6 +3352,9 @@ msgstr ""
msgid "Adjust how frequently the GitLab UI polls for updates."
msgstr ""
msgid "Admin"
msgstr ""
msgid "Admin Area"
msgstr ""
@ -13970,9 +13973,6 @@ msgstr ""
msgid "ContainerRegistry|Delete image repository?"
msgstr ""
msgid "ContainerRegistry|Delete protected up to access level"
msgstr ""
msgid "ContainerRegistry|Delete rule"
msgstr ""
@ -14039,10 +14039,10 @@ msgstr ""
msgid "ContainerRegistry|Manifest digest: %{digest}"
msgstr ""
msgid "ContainerRegistry|Maximum access level prevented from deleting"
msgid "ContainerRegistry|Minimum access level for delete"
msgstr ""
msgid "ContainerRegistry|Maximum access level prevented from pushing"
msgid "ContainerRegistry|Minimum access level for push"
msgstr ""
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
@ -14078,9 +14078,6 @@ msgstr ""
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Push protected up to access level"
msgstr ""
msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
msgstr ""

View File

@ -8,6 +8,7 @@ require 'optparse'
require 'open3'
require 'fileutils'
require 'uri'
require 'rainbow/ext/string'
class SchemaRegenerator
##

View File

@ -4,7 +4,7 @@ FactoryBot.define do
factory :container_registry_protection_rule, class: 'ContainerRegistry::Protection::Rule' do
project
repository_path_pattern { project.full_path }
delete_protected_up_to_access_level { :developer }
push_protected_up_to_access_level { :developer }
minimum_access_level_for_delete { :maintainer }
minimum_access_level_for_push { :maintainer }
end
end

View File

@ -74,14 +74,6 @@ describe('Batch comments mutations', () => {
});
describe(types.RECEIVE_PUBLISH_REVIEW_SUCCESS, () => {
it('resets drafts', () => {
state.drafts.push('test');
mutations[types.RECEIVE_PUBLISH_REVIEW_SUCCESS](state);
expect(state.drafts).toEqual([]);
});
it('sets isPublishing to false', () => {
state.isPublishing = true;

View File

@ -67,6 +67,7 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenCalledWith({
id: mockRunner.id,
search: '',
sort: 'ID_ASC',
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
});
});
@ -108,7 +109,6 @@ describe('RunnerProjects', () => {
name,
fullName: nameWithNamespace,
avatarUrl,
isOwner: true, // first project is always owner
});
});
@ -124,6 +124,7 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
id: mockRunner.id,
search: '',
sort: 'ID_ASC',
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
after: 'AFTER_CURSOR',
});
@ -138,6 +139,7 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
id: mockRunner.id,
search: '',
sort: 'ID_ASC',
last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
before: 'BEFORE_CURSOR',
});
@ -151,6 +153,7 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
id: mockRunner.id,
search: 'my search',
sort: 'ID_ASC',
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
});
});
@ -167,6 +170,7 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
id: mockRunner.id,
search: 'my search',
sort: 'ID_ASC',
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
});
});

View File

@ -24,8 +24,6 @@ const defaultMockDiscussion = {
notes,
};
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
let wrapper;
@ -175,9 +173,7 @@ describe('Design discussions component', () => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: DEFAULT_TODO_COUNT,
});
createComponent({
props: {
discussion: {
@ -229,7 +225,7 @@ describe('Design discussions component', () => {
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: DEFAULT_TODO_COUNT });
expect(dispatchedEvent.detail).toEqual({ delta: 0 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});

View File

@ -70,9 +70,6 @@ describe('Design management design todo button', () => {
beforeEach(async () => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
});
createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount });
wrapper.trigger('click');
@ -96,7 +93,7 @@ describe('Design management design todo button', () => {
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: 1 });
expect(dispatchedEvent.detail).toEqual({ delta: -1 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
});
@ -116,9 +113,6 @@ describe('Design management design todo button', () => {
beforeEach(async () => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
});
createComponent({}, { mountFn: mount });
wrapper.trigger('click');
@ -147,7 +141,7 @@ describe('Design management design todo button', () => {
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: 3 });
expect(dispatchedEvent.detail).toEqual({ delta: 1 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
});

View File

@ -1,25 +1,84 @@
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { ShowMlModelVersion } from '~/ml/model_registry/apps';
import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { MODEL_VERSION } from '../mock_data';
import LoadOrErrorOrShow from '~/ml/model_registry/components/load_or_error_or_show.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import getModelVersionQuery from '~/ml/model_registry/graphql/queries/get_model_version.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { modelVersionQuery, modelVersionWithCandidate } from '../graphql_mock_data';
let wrapper;
const createWrapper = () => {
wrapper = shallowMount(ShowMlModelVersion, { propsData: { modelVersion: MODEL_VERSION } });
};
const findTitleArea = () => wrapper.findComponent(TitleArea);
const findModelVersionDetail = () => wrapper.findComponent(ModelVersionDetail);
Vue.use(VueApollo);
describe('ml/model_registry/apps/show_model_version.vue', () => {
beforeEach(() => createWrapper());
let wrapper;
let apolloProvider;
beforeEach(() => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
const createWrapper = (resolver = jest.fn().mockResolvedValue(modelVersionQuery)) => {
const requestHandlers = [[getModelVersionQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = mount(ShowMlModelVersion, {
propsData: {
modelName: 'blah',
versionName: '1.2.3',
modelId: 1,
modelVersionId: 2,
projectPath: 'path/to/project',
},
apolloProvider,
});
};
const findTitleArea = () => wrapper.findComponent(TitleArea);
const findModelVersionDetail = () => wrapper.findComponent(ModelVersionDetail);
const findLoadOrErrorOrShow = () => wrapper.findComponent(LoadOrErrorOrShow);
it('renders the title', () => {
createWrapper();
expect(findTitleArea().props('title')).toBe('blah / 1.2.3');
});
it('renders the model version detail', () => {
expect(findModelVersionDetail().props('modelVersion')).toBe(MODEL_VERSION);
it('Requests data with the right parameters', async () => {
const resolver = jest.fn().mockResolvedValue(modelVersionQuery);
createWrapper(resolver);
await waitForPromises();
expect(resolver).toHaveBeenLastCalledWith(
expect.objectContaining({
modelId: 'gid://gitlab/Ml::Model/1',
modelVersionId: 'gid://gitlab/Ml::ModelVersion/2',
}),
);
});
it('Displays data when loaded', async () => {
createWrapper();
await waitForPromises();
expect(findModelVersionDetail().props('modelVersion')).toMatchObject(modelVersionWithCandidate);
});
it('Shows error message on error', async () => {
const error = new Error('Failure!');
createWrapper(jest.fn().mockRejectedValue(error));
await waitForPromises();
expect(findLoadOrErrorOrShow().props('errorMessage')).toBe(
'Failed to load model versions with error: Failure!',
);
expect(Sentry.captureException).toHaveBeenCalled();
});
});

View File

@ -5,62 +5,125 @@ import ModelVersionDetail from '~/ml/model_registry/components/model_version_det
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import CandidateDetail from '~/ml/model_registry/components/candidate_detail.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { convertCandidateFromGraphql } from '~/ml/model_registry/utils';
import { modelVersionWithCandidate } from '../graphql_mock_data';
import { makeModelVersion, MODEL_VERSION } from '../mock_data';
Vue.use(VueApollo);
const makeGraphqlModelVersion = (overrides = {}) => {
return { ...modelVersionWithCandidate, ...overrides };
};
let wrapper;
const createWrapper = (modelVersion = MODEL_VERSION) => {
const createWrapper = (modelVersion = modelVersionWithCandidate) => {
const apolloProvider = createMockApollo([]);
wrapper = shallowMount(ModelVersionDetail, { apolloProvider, propsData: { modelVersion } });
wrapper = shallowMount(ModelVersionDetail, {
apolloProvider,
propsData: { modelVersion },
provide: {
projectPath: 'path/to/project',
},
});
};
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
const findCandidateDetail = () => wrapper.findComponent(CandidateDetail);
describe('ml/model_registry/components/model_version_detail.vue', () => {
describe('base behaviour', () => {
beforeEach(() => createWrapper());
describe('When passing modelVersion passed on page load', () => {
describe('base behaviour', () => {
beforeEach(() => createWrapper(MODEL_VERSION));
it('shows the description', () => {
expect(wrapper.text()).toContain(MODEL_VERSION.description);
it('shows the description', () => {
expect(wrapper.text()).toContain(MODEL_VERSION.description);
});
it('shows the candidate', () => {
expect(findCandidateDetail().props('candidate')).toMatchObject(MODEL_VERSION.candidate);
});
it('shows the mlflow label string', () => {
expect(wrapper.text()).toContain('MLflow run ID');
});
it('shows the mlflow id', () => {
expect(wrapper.text()).toContain(MODEL_VERSION.candidate.info.eid);
});
it('renders files', () => {
expect(findPackageFiles().props()).toEqual({
packageId: 'gid://gitlab/Packages::Package/12',
projectPath: 'path/to/project',
packageType: 'ml_model',
canDelete: false,
});
});
});
it('shows the candidate', () => {
expect(findCandidateDetail().props('candidate')).toBe(MODEL_VERSION.candidate);
describe('if package does not exist', () => {
beforeEach(() => createWrapper(makeModelVersion({ packageId: 0 })));
it('does not render files', () => {
expect(findPackageFiles().exists()).toBe(false);
});
});
it('shows the mlflow label string', () => {
expect(wrapper.text()).toContain('MLflow run ID');
});
describe('if model version does not have description', () => {
beforeEach(() => createWrapper(makeModelVersion({ description: null })));
it('shows the mlflow id', () => {
expect(wrapper.text()).toContain(MODEL_VERSION.candidate.info.eid);
});
it('renders files', () => {
expect(findPackageFiles().props()).toEqual({
packageId: 'gid://gitlab/Packages::Package/12',
projectPath: MODEL_VERSION.projectPath,
packageType: 'ml_model',
canDelete: false,
it('renders no description provided label', () => {
expect(wrapper.text()).toContain('No description provided');
});
});
});
describe('if package does not exist', () => {
beforeEach(() => createWrapper(makeModelVersion({ packageId: 0 })));
describe('When passing modelVersion fetched from graphql', () => {
describe('base behaviour', () => {
beforeEach(() => createWrapper());
it('does not render files', () => {
expect(findPackageFiles().exists()).toBe(false);
it('shows the description', () => {
expect(wrapper.text()).toContain('A model version description');
});
it('shows the candidate', () => {
expect(findCandidateDetail().props('candidate')).toMatchObject(
convertCandidateFromGraphql(modelVersionWithCandidate.candidate),
);
});
it('shows the mlflow label string', () => {
expect(wrapper.text()).toContain('MLflow run ID');
});
it('shows the mlflow id', () => {
expect(wrapper.text()).toContain(modelVersionWithCandidate.candidate.eid);
});
it('renders files', () => {
expect(findPackageFiles().props()).toEqual({
packageId: 'gid://gitlab/Packages::Package/12',
projectPath: 'path/to/project',
packageType: 'ml_model',
canDelete: false,
});
});
});
});
describe('if model version does not have description', () => {
beforeEach(() => createWrapper(makeModelVersion({ description: null })));
describe('if package does not exist', () => {
beforeEach(() => createWrapper(makeGraphqlModelVersion({ packageId: 0 })));
it('renders no description provided label', () => {
expect(wrapper.text()).toContain('No description provided');
it('does not render files', () => {
expect(findPackageFiles().exists()).toBe(false);
});
});
describe('if model version does not have description', () => {
beforeEach(() => createWrapper(makeGraphqlModelVersion({ description: null })));
it('renders no description provided label', () => {
expect(wrapper.text()).toContain('No description provided');
});
});
});
});

View File

@ -41,6 +41,78 @@ export const modelVersionsQuery = (versions = graphqlModelVersions) => ({
},
});
export const candidate = {
id: 'gid://gitlab/Ml::Candidate/1',
name: 'hare-zebra-cobra-9745',
iid: 1,
eid: 'e9a71521-45c6-4b0a-b0c3-21f0b4528a5c',
status: 'running',
params: {
nodes: [
{
id: 'gid://gitlab/Ml::CandidateParam/1',
name: 'param1',
value: 'value1',
},
],
},
metadata: {
nodes: [
{
id: 'gid://gitlab/Ml::CandidateMetadata/1',
name: 'metadata1',
value: 'metadataValue1',
},
],
},
metrics: {
nodes: [
{
id: 'gid://gitlab/Ml::CandidateMetric/1',
name: 'metric1',
value: 0.3,
step: 0,
},
],
},
ciJob: {
id: 'gid://gitlab/Ci::Build/1',
webPath: '/gitlab-org/gitlab-test/-/jobs/1',
name: 'build:linux',
pipeline: {
id: 'gid://gitlab/Ci::Pipeline/1',
mergeRequest: {
id: 'gid://gitlab/MergeRequest/1',
title: 'Merge Request 1',
webUrl: 'path/to/mr',
iid: 1,
},
user: {
id: 'gid://gitlab/User/1',
avatarUrl: 'path/to/avatar',
webUrl: 'path/to/user/1',
username: 'user1',
name: 'User 1',
},
},
},
_links: {
showPath: '/root/test-project/-/ml/candidates/1',
artifactPath: '/root/test-project/-/packages/1',
},
};
export const modelVersionWithCandidate = {
id: 'gid://gitlab/Ml::ModelVersion/1',
version: '1.0.4999',
packageId: 'gid://gitlab/Packages::Package/12',
description: 'A model version description',
candidate,
_links: {
showPath: '/root/test-project/-/ml/models/1/versions/5000',
},
};
export const graphqlCandidates = [
{
id: 'gid://gitlab/Ml::Candidate/1',
@ -201,6 +273,21 @@ export const modelWithoutVersion = {
},
};
export const model = {
id: 'gid://gitlab/Ml::Model/1',
description: 'A model description',
name: 'gitlab_amazing_model',
versionCount: 1,
candidateCount: 0,
latestVersion: modelVersionWithCandidate,
};
export const modelDetailQuery = {
data: {
mlModel: model,
},
};
export const modelsQuery = (
models = [modelWithOneVersion, modelWithoutVersion],
pageInfo = graphqlPageInfo,
@ -216,3 +303,13 @@ export const modelsQuery = (
},
},
});
export const modelVersionQuery = {
data: {
mlModel: {
id: 'gid://gitlab/Ml::Model/1',
name: 'blah',
version: modelVersionWithCandidate,
},
},
};

View File

@ -0,0 +1,60 @@
import { convertCandidateFromGraphql } from '~/ml/model_registry/utils';
import { candidate } from './graphql_mock_data';
describe('~/ml/model_registry/utils', () => {
describe('convertCandidateFromGraphql', () => {
it('converts from graphql response', () => {
const converted = convertCandidateFromGraphql(candidate);
const expectedResponse = {
info: {
iid: 1,
eid: 'e9a71521-45c6-4b0a-b0c3-21f0b4528a5c',
status: 'running',
experimentName: '',
pathToExperiment: '',
pathToArtifact: '/root/test-project/-/packages/1',
path: '/root/test-project/-/ml/candidates/1',
ciJob: {
mergeRequest: {
iid: 1,
path: 'path/to/mr',
title: 'Merge Request 1',
},
name: 'build:linux',
path: '/gitlab-org/gitlab-test/-/jobs/1',
user: {
avatar: 'path/to/avatar',
name: 'User 1',
path: 'path/to/user/1',
username: 'user1',
},
},
},
metrics: [
{
id: 'gid://gitlab/Ml::CandidateMetric/1',
name: 'metric1',
value: 0.3,
step: 0,
},
],
params: [
{
id: 'gid://gitlab/Ml::CandidateParam/1',
name: 'param1',
value: 'value1',
},
],
metadata: [
{
id: 'gid://gitlab/Ml::CandidateMetadata/1',
name: 'metadata1',
value: 'metadataValue1',
},
],
};
expect(converted).toEqual(expectedResponse);
});
});
});

View File

@ -25,10 +25,10 @@ describe('container Protection Rule Form', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findRepositoryPathPatternInput = () =>
wrapper.findByRole('textbox', { name: /repository path pattern/i });
const findPushProtectedUpToAccessLevelSelect = () =>
wrapper.findByRole('combobox', { name: /maximum access level prevented from pushing/i });
const findDeleteProtectedUpToAccessLevelSelect = () =>
wrapper.findByRole('combobox', { name: /maximum access level prevented from deleting/i });
const findMinimumAccessLevelForPushSelect = () =>
wrapper.findByRole('combobox', { name: /minimum access level for push/i });
const findMinimumAccessLevelForDeleteSelect = () =>
wrapper.findByRole('combobox', { name: /minimum access level for delete/i });
const findSubmitButton = () => wrapper.findByRole('button', { name: /add rule/i });
const mountComponent = ({ config, provide = defaultProvidedValues } = {}) => {
@ -52,21 +52,18 @@ describe('container Protection Rule Form', () => {
};
describe('form fields', () => {
describe('form field "pushProtectedUpToAccessLevelSelect"', () => {
const pushProtectedUpToAccessLevelSelectOptions = () =>
findPushProtectedUpToAccessLevelSelect()
describe('form field "minimumAccessLevelForPush"', () => {
const minimumAccessLevelForPushOptions = () =>
findMinimumAccessLevelForPushSelect()
.findAll('option')
.wrappers.map((option) => option.element.value);
it.each(['DEVELOPER', 'MAINTAINER', 'OWNER'])(
'includes the %s access level',
(accessLevel) => {
mountComponent();
it.each(['MAINTAINER', 'OWNER', 'ADMIN'])('includes the %s access level', (accessLevel) => {
mountComponent();
expect(findPushProtectedUpToAccessLevelSelect().exists()).toBe(true);
expect(pushProtectedUpToAccessLevelSelectOptions()).toContain(accessLevel);
},
);
expect(findMinimumAccessLevelForPushSelect().exists()).toBe(true);
expect(minimumAccessLevelForPushOptions()).toContain(accessLevel);
});
});
describe('when graphql mutation is in progress', () => {
@ -79,8 +76,8 @@ describe('container Protection Rule Form', () => {
it('disables all form fields', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
expect(findRepositoryPathPatternInput().attributes('disabled')).toBe('disabled');
expect(findPushProtectedUpToAccessLevelSelect().attributes('disabled')).toBe('disabled');
expect(findDeleteProtectedUpToAccessLevelSelect().attributes('disabled')).toBe('disabled');
expect(findMinimumAccessLevelForPushSelect().attributes('disabled')).toBe('disabled');
expect(findMinimumAccessLevelForDeleteSelect().attributes('disabled')).toBe('disabled');
});
it('displays a loading spinner', () => {
@ -90,7 +87,7 @@ describe('container Protection Rule Form', () => {
});
describe('form actions', () => {
describe('button "Protect"', () => {
describe('button "Add rule"', () => {
it.each`
repositoryPathPattern | submitButtonDisabled
${''} | ${true}

View File

@ -170,14 +170,14 @@ export const containerProtectionRulesData = [
...Array.from(Array(15)).map((_e, i) => ({
id: `gid://gitlab/ContainerRegistry::Protection::Rule/${i}`,
repositoryPathPattern: `@flight/flight/maintainer-${i}-*`,
pushProtectedUpToAccessLevel: 'MAINTAINER',
deleteProtectedUpToAccessLevel: 'MAINTAINER',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'MAINTAINER',
})),
{
id: 'gid://gitlab/ContainerRegistry::Protection::Rule/16',
repositoryPathPattern: '@flight/flight/owner-16-*',
pushProtectedUpToAccessLevel: 'OWNER',
deleteProtectedUpToAccessLevel: 'OWNER',
minimumAccessLevelForPush: 'OWNER',
minimumAccessLevelForDelete: 'OWNER',
},
];
@ -216,9 +216,9 @@ export const createContainerProtectionRuleMutationPayload = ({ override, errors
});
export const createContainerProtectionRuleMutationInput = {
repositoryPathPattern: `@flight/flight-developer-14-*`,
pushProtectedUpToAccessLevel: 'DEVELOPER',
deleteProtectedUpToAccessLevel: 'DEVELOPER',
repositoryPathPattern: `@flight/flight-maintainer-14-*`,
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'MAINTAINER',
};
export const createContainerProtectionRuleMutationPayloadErrors = [

View File

@ -16,9 +16,6 @@ describe('Todo Button', () => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
});
});
afterEach(() => {
@ -44,7 +41,7 @@ describe('Todo Button', () => {
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: 1 });
expect(dispatchedEvent.detail).toEqual({ delta: -1 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});

View File

@ -134,13 +134,21 @@ describe('UserBar component', () => {
expect(todosCounter.attributes('class')).toContain('shortcuts-todos');
});
it('should update todo counter when event is emitted', async () => {
it('should update todo counter when event with count is emitted', async () => {
createWrapper();
const count = 100;
document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { count } }));
await nextTick();
expect(findTodosCounter().props('count')).toBe(count);
});
it('should update todo counter when event with diff is emitted', async () => {
createWrapper();
expect(findTodosCounter().props('count')).toBe(3);
document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { delta: -2 } }));
await nextTick();
expect(findTodosCounter().props('count')).toBe(1);
});
});
it('renders branding logo', () => {

View File

@ -25,7 +25,7 @@ const userCountUpdate = {
review_requested_merge_requests: 101112,
};
describe('User Merge Requests', () => {
describe('User Count Manager', () => {
let channelMock;
let newBroadcastChannelMock;
@ -108,6 +108,11 @@ describe('User Merge Requests', () => {
'userCounts:fetch',
expect.any(Function),
);
expect(document.removeEventListener).toHaveBeenCalledWith(
'todo:toggle',
expect.any(Function),
);
expect(document.addEventListener).toHaveBeenCalledWith('todo:toggle', expect.any(Function));
});
});
@ -146,6 +151,59 @@ describe('User Merge Requests', () => {
});
});
describe('Event listener todo:toggle', () => {
beforeEach(() => {
createUserCountsManager();
userCounts.todos = 10;
});
describe('with total count', () => {
it.each([
{ count: 123, expected: 123 },
{ count: -500, expected: 0 },
{ count: 0, expected: 0 },
{ count: NaN, expected: 10 },
{ count: '99+', expected: 10 },
])(`with count: $count results in $expected`, ({ count, expected }) => {
expect(userCounts.todos).toBe(10);
document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { count } }));
expect(userCounts.todos).toBe(expected);
});
});
describe('with diff on count', () => {
it.each([
{ delta: 5, expected: 15 },
{ delta: -5, expected: 5 },
{ delta: 0, expected: 10 },
{ delta: -100, expected: 0 },
{ delta: NaN, expected: 10 },
{ delta: '99+', expected: 10 },
])(`with count: $diff results in $expected`, ({ delta, expected }) => {
expect(userCounts.todos).toBe(10);
document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { delta } }));
expect(userCounts.todos).toBe(expected);
});
});
it('updates count over delta if both are defined', () => {
expect(userCounts.todos).toBe(10);
const detail = {
count: 20,
delta: -5,
};
document.dispatchEvent(new CustomEvent('todo:toggle', { detail }));
expect(userCounts.todos).toBe(detail.count);
});
});
describe('destroyUserCountsManager', () => {
it('unregisters event handler', () => {
expect(document.removeEventListener).not.toHaveBeenCalledWith();

View File

@ -5,17 +5,23 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue';
import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql';
import alertQuery from '~/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql';
import waitForPromises from 'jest/__helpers__/wait_for_promises';
import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
const mockAlert = mockAlerts[1];
describe('Alert Details Sidebar To Do', () => {
let wrapper;
let requestHandler;
const defaultHandler = {
createAlertTodo: jest.fn().mockResolvedValue({}),
markAsDone: jest.fn().mockResolvedValue({}),
createAlertTodo: jest
.fn()
.mockResolvedValue({ data: { alertTodoCreate: { errors: [], alert: mockAlert } } }),
markAsDone: jest
.fn()
.mockResolvedValue({ data: { todoMarkDone: { errors: [], todo: { id: 1234 } } } }),
};
const createMockApolloProvider = (handler) => {
@ -30,14 +36,33 @@ describe('Alert Details Sidebar To Do', () => {
};
function mountComponent({ data, sidebarCollapsed = true, handler = defaultHandler } = {}) {
wrapper = mount(SidebarTodo, {
apolloProvider: createMockApolloProvider(handler),
propsData: {
alert: { ...mockAlert },
...data,
sidebarCollapsed,
projectPath: 'projectPath',
const propsData = {
alert: { ...mockAlert },
...data,
sidebarCollapsed,
projectPath: 'projectPath',
};
const fakeApollo = createMockApolloProvider(handler);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: alertQuery,
variables: {
fullPath: propsData.projectPath,
alertId: propsData.alert.iid,
},
data: {
project: {
id: '1',
alertManagementAlerts: {
nodes: [propsData.alert],
},
},
},
});
wrapper = mount(SidebarTodo, {
apolloProvider: fakeApollo,
propsData,
});
}
@ -47,9 +72,7 @@ describe('Alert Details Sidebar To Do', () => {
describe('adding a todo', () => {
beforeEach(() => {
mountComponent({
data: { alert: mockAlert },
sidebarCollapsed: false,
loading: false,
});
});
@ -65,11 +88,23 @@ describe('Alert Details Sidebar To Do', () => {
expect(requestHandler.createAlertTodo).toHaveBeenCalledWith(
expect.objectContaining({
iid: '1527542',
iid: '1527543',
projectPath: 'projectPath',
}),
);
});
it('triggers an update of the todo count', async () => {
const dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
findToDoButton().trigger('click');
await waitForPromises();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchedEvent.detail).toEqual({ delta: 1 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
});
describe('removing a todo', () => {
@ -95,6 +130,18 @@ describe('Alert Details Sidebar To Do', () => {
id: '1234',
});
});
it('triggers an update of the todo count', async () => {
const dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
findToDoButton().trigger('click');
await waitForPromises();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchedEvent.detail).toEqual({ delta: -1 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
});
});
});

View File

@ -8,20 +8,65 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED",
"assignees": { "nodes": [] },
"notes": { "nodes": [] },
"todos": { "nodes": [] }
"assignees": {
"nodes": [
]
},
"notes": {
"nodes": [
]
},
"todos": {
"nodes": [
]
}
},
{
"id": 5,
"__typename": "AlertManagementAlert",
"iid": "1527543",
"title": "Some other alert Some other alert Some other alert Some other alert Some other alert Some other alert",
"description": "So descriptive",
"severity": "MEDIUM",
"eventCount": 1,
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"details": 5,
"monitoringTool": "Eyes",
"service": "1984",
"runbook": "I am running",
"createdAt": "2020-04-17T23:20:14.996Z",
"updatedAt": "2020-04-17T23:23:14.996Z",
"endedAt": "2020-04-18T23:18:14.996Z",
"hosts": [
"127.0.0.1"
],
"environment": {
"id": "gid://gitlab/Environment/29",
"name": "Alerta",
"path": "/path/to/alerta"
},
"status": "ACKNOWLEDGED",
"assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] },
"issue": { "state" : "closed", "iid": "1", "title": "My test issue" },
"assignees": {
"nodes": [
{
"username": "root",
"avatarUrl": "/url",
"name": "root"
}
]
},
"issue": {
"__typename": "Issue",
"id": "gid://gitlab/Issue/1",
"state": "closed",
"iid": "1",
"title": "My test issue",
"createdAt": "2020-04-17T23:18:14.996Z",
"webUrl": "http://192.168.1.4:3000/projectPath/-/issues/1"
},
"notes": {
"nodes": [
{
@ -30,7 +75,7 @@
"id": "gid://gitlab/User/1",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
@ -39,7 +84,11 @@
}
]
},
"todos": { "nodes": [] }
"todos": {
"nodes": [
]
}
},
{
"iid": "1527544",
@ -49,7 +98,15 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "RESOLVED",
"assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] },
"assignees": {
"nodes": [
{
"username": "root",
"avatarUrl": "/url",
"name": "root"
}
]
},
"notes": {
"nodes": [
{
@ -58,7 +115,7 @@
"id": "gid://gitlab/User/2",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
@ -66,6 +123,10 @@
}
]
},
"todos": { "nodes": [] }
"todos": {
"nodes": [
]
}
}
]

View File

@ -16,7 +16,7 @@ RSpec.describe Resolvers::Ci::RunnerProjectsResolver, feature_category: :fleet_v
describe '#resolve' do
context 'with authorized user', :enable_admin_mode do
let(:current_user) { create(:user, :admin) }
let_it_be(:current_user) { create(:user, :admin) }
context 'with search argument' do
let(:args) { { search: 'Project1.' } }
@ -69,15 +69,15 @@ RSpec.describe Resolvers::Ci::RunnerProjectsResolver, feature_category: :fleet_v
end
context 'without arguments' do
it 'returns a lazy value with all projects sorted by :id_asc' do
it 'returns a lazy value with all projects sorted by :id_desc' do
expect(subject).to be_a(GraphQL::Execution::Lazy)
expect(subject.value.items).to eq([project1, project2, project3])
expect(subject.value.items).to eq([project3, project2, project1])
end
end
end
context 'with unauthorized user' do
let(:current_user) { create(:user) }
let_it_be(:current_user) { create(:user) }
it { is_expected.to be_nil }
end

View File

@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRegistryProtectionRuleAccessLevel'], feature_category: :container_registry do
it 'exposes all options' do
expect(described_class.values.keys).to match_array(%w[DEVELOPER MAINTAINER OWNER])
expect(described_class.values.keys).to match_array(%w[MAINTAINER OWNER ADMIN])
end
end

View File

@ -21,14 +21,14 @@ RSpec.describe GitlabSchema.types['ContainerRegistryProtectionRule'], feature_ca
it { is_expected.to have_non_null_graphql_type(GraphQL::Types::String) }
end
describe 'push_protected_up_to_access_level' do
subject { described_class.fields['pushProtectedUpToAccessLevel'] }
describe 'minimum_access_level_for_push' do
subject { described_class.fields['minimumAccessLevelForPush'] }
it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
end
describe 'delete_protected_up_to_access_level' do
subject { described_class.fields['deleteProtectedUpToAccessLevel'] }
describe 'minimum_access_level_for_delete' do
subject { described_class.fields['minimumAccessLevelForDelete'] }
it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
end

View File

@ -102,4 +102,40 @@ RSpec.describe Projects::Ml::ModelRegistryHelper, feature_category: :mlops do
end
end
end
describe '#show_ml_model_version_data' do
let_it_be(:model) do
build_stubbed(:ml_models, :with_latest_version_and_package, project: project, id: 1)
end
let_it_be(:model_version) do
model.latest_version
end
subject(:parsed) { Gitlab::Json.parse(helper.show_ml_model_version_data(model_version, user)) }
it 'generates the correct data' do
is_expected.to eq({
"projectPath" => project.full_path,
"modelId" => model.id,
"modelVersionId" => model_version.id,
"modelName" => model_version.name,
"versionName" => model_version.version,
"canWriteModelRegistry" => true
})
end
context 'when user does not have write access to model registry' do
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :write_model_registry, project)
.and_return(false)
end
it 'canWriteModelRegistry is false' do
expect(parsed['canWriteModelRegistry']).to eq(false)
end
end
end
end

View File

@ -51,20 +51,16 @@ RSpec.describe Gitlab::Checks::ChangedBlobs, feature_category: :source_code_mana
end
end
context 'with quarantine directory' do
let_it_be(:project) { create(:project, :small_repo) }
context 'with quarantine directory', :request_store do
let_it_be_with_refind(:project) { create(:project, :small_repo) }
let(:revisions) { [repository.commit.id] }
let(:git_env) do
{
'GIT_OBJECT_DIRECTORY_RELATIVE' => "objects",
'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['/dir/one', '/dir/two']
}
end
before do
allow(Gitlab::Git::HookEnv).to receive(:all).with(repository.gl_repository).and_return(git_env)
::Gitlab::Git::HookEnv.set(project.repository.gl_repository,
project.repository.raw_repository.relative_path,
'GIT_OBJECT_DIRECTORY_RELATIVE' => 'objects',
'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['/dir/one', '/dir/two'])
end
context 'when the blob does not exist in the repo' do
@ -78,7 +74,7 @@ RSpec.describe Gitlab::Checks::ChangedBlobs, feature_category: :source_code_mana
end
context 'when the same file with different paths is committed' do
let_it_be(:commits) do
before_all do
project.repository.commit_files(
user,
branch_name: project.repository.root_ref,

View File

@ -29,16 +29,12 @@ RSpec.describe Gitlab::Checks::FileSizeCheck::HookEnvironmentAwareAnyOversizedBl
end
context 'with hook env' do
context 'with hook environment' do
let(:git_env) do
{
'GIT_OBJECT_DIRECTORY_RELATIVE' => "objects",
'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['/dir/one', '/dir/two']
}
end
context 'with hook environment', :request_store do
before do
allow(Gitlab::Git::HookEnv).to receive(:all).with(repository.gl_repository).and_return(git_env)
::Gitlab::Git::HookEnv.set(project.repository.gl_repository,
project.repository.raw_repository.relative_path,
'GIT_OBJECT_DIRECTORY_RELATIVE' => 'objects',
'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['/dir/one', '/dir/two'])
end
it 'returns an emtpy array' do

View File

@ -3,14 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Git::HookEnv do
let(:relative_path) { 'snapshot/relative-path.git' }
let(:gl_repository) { 'project-123' }
describe ".set" do
context 'with RequestStore disabled' do
it 'does not store anything' do
described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
described_class.set(gl_repository, relative_path, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
expect(described_class.all(gl_repository)).to be_empty
expect(described_class.get_relative_path).to be_nil
end
end
@ -18,6 +20,7 @@ RSpec.describe Gitlab::Git::HookEnv do
it 'whitelist some `GIT_*` variables and stores them using RequestStore' do
described_class.set(
gl_repository,
relative_path,
GIT_OBJECT_DIRECTORY_RELATIVE: 'foo',
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: 'bar',
GIT_EXEC_PATH: 'baz',
@ -34,15 +37,16 @@ RSpec.describe Gitlab::Git::HookEnv do
end
end
describe ".all" do
context 'with RequestStore enabled', :request_store do
before do
described_class.set(
gl_repository,
GIT_OBJECT_DIRECTORY_RELATIVE: 'foo',
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: ['bar'])
end
context 'with RequestStore enabled', :request_store do
before do
described_class.set(
gl_repository,
relative_path,
GIT_OBJECT_DIRECTORY_RELATIVE: 'foo',
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: ['bar'])
end
describe ".all" do
it 'returns an env hash' do
expect(described_class.all(gl_repository)).to eq({
'GIT_OBJECT_DIRECTORY_RELATIVE' => 'foo',
@ -50,6 +54,12 @@ RSpec.describe Gitlab::Git::HookEnv do
})
end
end
describe ".get_relative_path" do
it 'returns the relative path' do
expect(described_class.get_relative_path).to eq(relative_path)
end
end
end
describe ".to_env_hash" do
@ -70,7 +80,7 @@ RSpec.describe Gitlab::Git::HookEnv do
with_them do
before do
described_class.set(gl_repository, key.to_sym => input)
described_class.set(gl_repository, relative_path, key.to_sym => input)
end
it 'puts the right value in the hash' do
@ -86,26 +96,36 @@ RSpec.describe Gitlab::Git::HookEnv do
describe 'thread-safety' do
context 'with RequestStore enabled', :request_store do
let(:other_relative_path) { 'other_relative_path' }
before do
allow(RequestStore).to receive(:active?).and_return(true)
described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
described_class.set(gl_repository, relative_path, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
end
it 'is thread-safe' do
another_thread = Thread.new do
described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'bar')
described_class.set(gl_repository, other_relative_path, GIT_OBJECT_DIRECTORY_RELATIVE: 'bar')
Thread.stop
described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]
{
relative_path: described_class.get_relative_path,
GIT_OBJECT_DIRECTORY_RELATIVE: described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]
}
end
# Ensure another_thread runs first
sleep 0.1 until another_thread.stop?
expect(described_class.get_relative_path).to eq(relative_path)
expect(described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]).to eq('foo')
another_thread.run
expect(another_thread.value).to eq('bar')
expect(another_thread.value).to eq({
relative_path: other_relative_path,
GIT_OBJECT_DIRECTORY_RELATIVE: 'bar'
})
end
end
end

View File

@ -371,14 +371,13 @@ RSpec.describe Gitlab::GitAccessSnippet do
it_behaves_like 'migration bot does not err'
end
context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is set' do
context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is set', :request_store do
let(:change_size) { 100 }
before do
allow(Gitlab::Git::HookEnv)
.to receive(:all)
.with(repository.gl_repository)
.and_return({ 'GIT_OBJECT_DIRECTORY_RELATIVE' => 'objects' })
::Gitlab::Git::HookEnv.set(repository.gl_repository,
repository.raw_repository.relative_path,
'GIT_OBJECT_DIRECTORY_RELATIVE' => 'objects')
# Stub the object directory size to "simulate" quarantine size
allow(repository).to receive(:object_directory_size).and_return(change_size)

View File

@ -397,6 +397,9 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
stub_env('GITLAB_ANALYTICS_ID', app_id)
stub_env('GITLAB_ANALYTICS_URL', url)
stub_feature_flags(internal_events_batching: true)
allow(GitlabSDK::Client)
.to receive(:new)
.with(app_id: app_id, host: url, buffer_size: described_class::SNOWPLOW_EMITTER_BUFFER_SIZE)
@ -465,5 +468,19 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
it_behaves_like 'does not send a Product Analytics event'
end
context 'with internal_events_batching FF off' do
before do
stub_feature_flags(internal_events_batching: false)
end
it 'passes buffer_size 1 to SDK client' do
expect(GitlabSDK::Client)
.to receive(:new)
.with(app_id: app_id, host: url, buffer_size: described_class::DEFAULT_BUFFER_SIZE)
track_event
end
end
end
end

View File

@ -52,7 +52,26 @@ RSpec.describe Gitlab::SidekiqSharding::ScheduledEnq, feature_category: :scalabi
it_behaves_like 'uses sharding router'
end
context 'with routable classes' do
context 'with ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper classes' do
let(:job_hash) { { class: ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, args: [] } }
let(:store_name) { 'queues_shard_test' }
before do
# simulate routing rules in config/gitlab.yml
allow(ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper)
.to receive(:get_sidekiq_options).and_return({ 'store' => store_name })
# skip label creation to avoid calling .get_shard_instance
allow_next_instance_of(Gitlab::SidekiqMiddleware::ClientMetrics) do |mh|
allow(mh).to receive(:create_labels).and_return({ boundary: "", destination_shard_redis: "",
external_dependencies: "", feature_category: "", queue: "", scheduling: "", urgency: "", worker: "" })
end
end
it_behaves_like 'uses sharding router'
end
context 'with ApplicationWorker classes' do
let(:job_hash) { { class: Chaos::CpuSpinWorker, args: [] } }
let(:store_name) { 'queues_shard_test' }

View File

@ -14,25 +14,25 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
describe 'enums' do
it {
is_expected.to(
define_enum_for(:push_protected_up_to_access_level)
define_enum_for(:minimum_access_level_for_push)
.with_values(
developer: Gitlab::Access::DEVELOPER,
maintainer: Gitlab::Access::MAINTAINER,
owner: Gitlab::Access::OWNER
owner: Gitlab::Access::OWNER,
admin: Gitlab::Access::ADMIN
)
.with_prefix(:push_protected_up_to)
.with_prefix(:minimum_access_level_for_push)
)
}
it {
is_expected.to(
define_enum_for(:delete_protected_up_to_access_level)
.with_values(
developer: Gitlab::Access::DEVELOPER,
maintainer: Gitlab::Access::MAINTAINER,
owner: Gitlab::Access::OWNER
)
.with_prefix(:delete_protected_up_to)
define_enum_for(:minimum_access_level_for_delete)
.with_values(
maintainer: Gitlab::Access::MAINTAINER,
owner: Gitlab::Access::OWNER,
admin: Gitlab::Access::ADMIN
)
.with_prefix(:minimum_access_level_for_delete)
)
}
end
@ -90,12 +90,12 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
end
end
describe '#delete_protected_up_to_access_level' do
it { is_expected.to validate_presence_of(:delete_protected_up_to_access_level) }
describe '#minimum_access_level_for_delete' do
it { is_expected.to validate_presence_of(:minimum_access_level_for_delete) }
end
describe '#push_protected_up_to_access_level' do
it { is_expected.to validate_presence_of(:push_protected_up_to_access_level) }
describe '#minimum_access_level_for_push' do
it { is_expected.to validate_presence_of(:minimum_access_level_for_push) }
end
end
@ -240,7 +240,7 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-stage*",
push_protected_up_to_access_level: :developer
minimum_access_level_for_push: :maintainer
)
end
@ -248,7 +248,7 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-prod*",
push_protected_up_to_access_level: :maintainer
minimum_access_level_for_push: :owner
)
end
@ -256,7 +256,7 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-release*",
push_protected_up_to_access_level: :owner
minimum_access_level_for_push: :admin
)
end
@ -264,7 +264,7 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-*",
push_protected_up_to_access_level: :developer
minimum_access_level_for_push: :maintainer
)
end

View File

@ -2090,15 +2090,12 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
where(:shared_runners_enabled, :allow_descendants_override_disabled_shared_runners, :other_setting, :result) do
true | true | Namespace::SR_ENABLED | false
true | true | Namespace::SR_DISABLED_WITH_OVERRIDE | true
true | true | Namespace::SR_DISABLED_AND_OVERRIDABLE | true
true | true | Namespace::SR_DISABLED_AND_UNOVERRIDABLE | true
false | true | Namespace::SR_ENABLED | false
false | true | Namespace::SR_DISABLED_WITH_OVERRIDE | false
false | true | Namespace::SR_DISABLED_AND_OVERRIDABLE | false
false | true | Namespace::SR_DISABLED_AND_UNOVERRIDABLE | true
false | false | Namespace::SR_ENABLED | false
false | false | Namespace::SR_DISABLED_WITH_OVERRIDE | false
false | false | Namespace::SR_DISABLED_AND_OVERRIDABLE | false
false | false | Namespace::SR_DISABLED_AND_UNOVERRIDABLE | false
end

View File

@ -818,8 +818,8 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :fleet_visibi
'projectCount' => 2,
'projects' => {
'nodes' => [
a_graphql_entity_for(project1),
a_graphql_entity_for(project2)
a_graphql_entity_for(project2),
a_graphql_entity_for(project1)
]
})
expect(runner2_data).to match a_hash_including(

View File

@ -16,8 +16,8 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
{
project_path: project.full_path,
repository_path_pattern: container_registry_protection_rule_attributes.repository_path_pattern,
push_protected_up_to_access_level: 'MAINTAINER',
delete_protected_up_to_access_level: 'MAINTAINER'
minimum_access_level_for_push: 'MAINTAINER',
minimum_access_level_for_delete: 'MAINTAINER'
}
end
@ -67,11 +67,11 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
it_behaves_like 'a successful response'
context 'with invalid input fields `pushProtectedUpToAccessLevel` and `deleteProtectedUpToAccessLevel`' do
context 'with invalid input fields `minimumAccessLevelForPush` and `minimumAccessLevelForDelete`' do
let(:kwargs) do
super().merge(
push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL',
delete_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL'
minimum_access_level_for_push: 'UNKNOWN_ACCESS_LEVEL',
minimum_access_level_for_delete: 'UNKNOWN_ACCESS_LEVEL'
)
end
@ -80,7 +80,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
it {
subject
expect_graphql_errors_to_include([/pushProtectedUpToAccessLevel/, /deleteProtectedUpToAccessLevel/])
expect_graphql_errors_to_include([/minimumAccessLevelForPush/, /minimumAccessLevelForDelete/])
}
end
@ -107,7 +107,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
context 'with existing containers protection rule' do
let_it_be(:existing_container_registry_protection_rule) do
create(:container_registry_protection_rule, project: project,
push_protected_up_to_access_level: Gitlab::Access::DEVELOPER)
minimum_access_level_for_push: Gitlab::Access::MAINTAINER)
end
context 'when container name pattern is slightly different' do
@ -128,7 +128,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
context 'when field `repository_path_pattern` is taken' do
let(:kwargs) do
super().merge(repository_path_pattern: existing_container_registry_protection_rule.repository_path_pattern,
push_protected_up_to_access_level: 'MAINTAINER')
minimum_access_level_for_push: 'OWNER')
end
it_behaves_like 'an erroneous response'
@ -144,7 +144,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
it 'does not create new container protection rules' do
expect(::ContainerRegistry::Protection::Rule.where(project: project,
repository_path_pattern: kwargs[:repository_path_pattern],
push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist
minimum_access_level_for_push: Gitlab::Access::OWNER)).not_to exist
end
end
end

View File

@ -42,8 +42,8 @@ RSpec.describe 'Deleting a container registry protection rule', :aggregate_failu
'containerRegistryProtectionRule' => {
'id' => container_protection_rule.to_global_id.to_s,
'repositoryPathPattern' => container_protection_rule.repository_path_pattern,
'deleteProtectedUpToAccessLevel' => container_protection_rule.delete_protected_up_to_access_level.upcase,
'pushProtectedUpToAccessLevel' => container_protection_rule.push_protected_up_to_access_level.upcase
'minimumAccessLevelForDelete' => container_protection_rule.minimum_access_level_for_delete.upcase,
'minimumAccessLevelForPush' => container_protection_rule.minimum_access_level_for_push.upcase
}
)
end

View File

@ -7,7 +7,7 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:container_registry_protection_rule) do
create(:container_registry_protection_rule, project: project, push_protected_up_to_access_level: :developer)
create(:container_registry_protection_rule, project: project, minimum_access_level_for_push: :maintainer)
end
let_it_be(:current_user) { create(:user, maintainer_of: project) }
@ -21,8 +21,8 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
<<~QUERY
containerRegistryProtectionRule {
repositoryPathPattern
deleteProtectedUpToAccessLevel
pushProtectedUpToAccessLevel
minimumAccessLevelForDelete
minimumAccessLevelForPush
}
clientMutationId
errors
@ -34,8 +34,8 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
{
id: container_registry_protection_rule.to_global_id,
repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-updated",
delete_protected_up_to_access_level: 'OWNER',
push_protected_up_to_access_level: 'MAINTAINER'
minimum_access_level_for_delete: 'OWNER',
minimum_access_level_for_push: 'MAINTAINER'
}
end
@ -52,8 +52,8 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
expect(mutation_response).to include(
'containerRegistryProtectionRule' => {
'repositoryPathPattern' => input[:repository_path_pattern],
'deleteProtectedUpToAccessLevel' => input[:delete_protected_up_to_access_level],
'pushProtectedUpToAccessLevel' => input[:push_protected_up_to_access_level]
'minimumAccessLevelForDelete' => input[:minimum_access_level_for_delete],
'minimumAccessLevelForPush' => input[:minimum_access_level_for_push]
}
)
end
@ -62,7 +62,7 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
subject.tap do
expect(container_registry_protection_rule.reload).to have_attributes(
repository_path_pattern: input[:repository_path_pattern],
push_protected_up_to_access_level: input[:push_protected_up_to_access_level].downcase
minimum_access_level_for_push: input[:minimum_access_level_for_push].downcase
)
end
end
@ -96,12 +96,12 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
end
end
context 'with invalid input param `pushProtectedUpToAccessLevel`' do
let(:input) { super().merge(push_protected_up_to_access_level: nil) }
context 'with invalid input param `minimumAccessLevelForPush`' do
let(:input) { super().merge(minimum_access_level_for_push: nil) }
it_behaves_like 'an erroneous response'
it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } }
it { is_expected.tap { expect_graphql_errors_to_include(/minimumAccessLevelForPush can't be blank/) } }
end
context 'with invalid input param `repositoryPathPattern`' do

View File

@ -65,23 +65,6 @@ RSpec.describe 'GroupUpdate', feature_category: :groups_and_projects do
expect(group.reload.shared_runners_setting).to eq(variables[:shared_runners_setting].downcase)
end
context 'when using DISABLED_WITH_OVERRIDE (deprecated)' do
let(:variables) do
{
full_path: group.full_path,
shared_runners_setting: 'DISABLED_WITH_OVERRIDE'
}
end
it 'updates shared runners settings with disabled_and_overridable' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to be_nil
expect(group.reload.shared_runners_setting).to eq('disabled_and_overridable')
end
end
context 'when bad arguments are provided' do
let(:variables) { { full_path: '', shared_runners_setting: 'INVALID' } }

View File

@ -38,8 +38,8 @@ RSpec.describe 'getting the containers protection rules linked to a project', :a
expect(protection_rules).to include(
hash_including(
'repositoryPathPattern' => container_protection_rule.repository_path_pattern,
'pushProtectedUpToAccessLevel' => 'DEVELOPER',
'deleteProtectedUpToAccessLevel' => 'DEVELOPER'
'minimumAccessLevelForDelete' => 'MAINTAINER',
'minimumAccessLevelForPush' => 'MAINTAINER'
)
)
end

View File

@ -490,6 +490,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
end
context "access granted" do
let(:relative_path) { nil }
let(:env) { {} }
around do |example|
@ -515,7 +516,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
include_context 'with env passed as a JSON'
it 'sets env in RequestStore' do
expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys)
expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, relative_path, env.stringify_keys)
subject
@ -551,7 +552,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
end
it 'sets env in RequestStore and routes gRPC messages to primary', :request_store do
expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys).and_call_original
expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, relative_path, env.stringify_keys).and_call_original
subject
@ -560,21 +561,9 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
end
end
context 'when Gitaly provides a relative_path argument', :request_store do
subject { push(key, project, relative_path: relative_path) }
let(:relative_path) { 'relative_path' }
it 'stores relative_path value in RequestStore' do
allow(Gitlab::SafeRequestStore).to receive(:[]=).and_call_original
expect(Gitlab::SafeRequestStore).to receive(:[]=).with(:gitlab_git_relative_path, relative_path)
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
context "git push with project.wiki" do
let(:relative_path) { project.wiki.repository.relative_path }
subject { push(key, project.wiki, env: env.to_json) }
it 'responds with success' do
@ -625,7 +614,9 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
# relative_path is sent from Gitaly to Rails when invoking internal API. In production it points to the
# transaction's snapshot repository. As Gitaly is stubbed out from the invocation loop, there is no transaction
# and thus no snapshot repository. Pass the original relative path.
subject { push(key, personal_snippet, env: env.to_json, changes: snippet_changes, relative_path: "#{personal_snippet.repository.disk_path}.git") }
let(:relative_path) { personal_snippet.repository.relative_path }
subject { push(key, personal_snippet, env: env.to_json, changes: snippet_changes) }
it 'responds with success' do
subject
@ -664,7 +655,9 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
# relative_path is sent from Gitaly to Rails when invoking internal API. In production it points to the
# transaction's snapshot repository. As Gitaly is stubbed out from the invocation loop, there is no transaction
# and thus no snapshot repository. Pass the original relative path.
subject { push(key, project_snippet, env: env.to_json, changes: snippet_changes, relative_path: "#{project_snippet.repository.disk_path}.git") }
let(:relative_path) { project_snippet.repository.relative_path }
subject { push(key, project_snippet, env: env.to_json, changes: snippet_changes) }
it 'responds with success' do
subject

View File

@ -23,8 +23,8 @@ RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', fea
be_a(ContainerRegistry::Protection::Rule)
.and(have_attributes(
repository_path_pattern: params[:repository_path_pattern],
push_protected_up_to_access_level: params[:push_protected_up_to_access_level].to_s,
delete_protected_up_to_access_level: params[:delete_protected_up_to_access_level].to_s
minimum_access_level_for_push: params[:minimum_access_level_for_push].to_s,
minimum_access_level_for_delete: params[:minimum_access_level_for_delete].to_s
))
}
)
@ -37,7 +37,7 @@ RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', fea
ContainerRegistry::Protection::Rule.where(
project: project,
repository_path_pattern: params[:repository_path_pattern],
push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
minimum_access_level_for_push: params[:minimum_access_level_for_push]
)
).to exist
end
@ -58,7 +58,7 @@ RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', fea
ContainerRegistry::Protection::Rule.where(
project: project,
repository_path_pattern: params[:repository_path_pattern],
push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
minimum_access_level_for_push: params[:minimum_access_level_for_push]
)
).not_to exist
end
@ -75,20 +75,20 @@ RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', fea
it { is_expected.to have_attributes(message: match(/Repository path pattern can't be blank/)) }
end
context 'when delete_protected_up_to_access_level is invalid' do
let(:params) { super().merge(delete_protected_up_to_access_level: 1000) }
context 'when minimum_access_level_for_delete is invalid' do
let(:params) { super().merge(minimum_access_level_for_delete: 1000) }
it_behaves_like 'an erroneous service response'
it { is_expected.to have_attributes(message: match(/is not a valid delete_protected_up_to_access_level/)) }
it { is_expected.to have_attributes(message: match(/is not a valid minimum_access_level_for_delete/)) }
end
context 'when push_protected_up_to_access_level is invalid' do
let(:params) { super().merge(push_protected_up_to_access_level: 1000) }
context 'when minimum_access_level_for_push is invalid' do
let(:params) { super().merge(minimum_access_level_for_push: 1000) }
it_behaves_like 'an erroneous service response'
it { is_expected.to have_attributes(message: match(/is not a valid push_protected_up_to_access_level/)) }
it { is_expected.to have_attributes(message: match(/is not a valid minimum_access_level_for_push/)) }
end
end
@ -102,8 +102,8 @@ RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', fea
super().merge(
# The field `repository_path_pattern` is unique; this is why we change the value in a minimum way
repository_path_pattern: "#{existing_container_registry_protection_rule.repository_path_pattern}-unique",
push_protected_up_to_access_level:
existing_container_registry_protection_rule.push_protected_up_to_access_level
minimum_access_level_for_push:
existing_container_registry_protection_rule.minimum_access_level_for_push
)
end
@ -114,7 +114,7 @@ RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', fea
let(:params) do
super().merge(
repository_path_pattern: existing_container_registry_protection_rule.repository_path_pattern,
push_protected_up_to_access_level: :maintainer
minimum_access_level_for_push: :owner
)
end

View File

@ -15,8 +15,8 @@ RSpec.describe ContainerRegistry::Protection::UpdateRuleService, '#execute', fea
attributes_for(
:container_registry_protection_rule,
repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-updated",
delete_protected_up_to_access_level: 'owner',
push_protected_up_to_access_level: 'owner'
minimum_access_level_for_delete: 'owner',
minimum_access_level_for_push: 'owner'
)
end
@ -77,8 +77,8 @@ RSpec.describe ContainerRegistry::Protection::UpdateRuleService, '#execute', fea
{ repository_path_pattern: '' } | include("Repository path pattern can't be blank")
{ repository_path_pattern: 'wrong-project-scope/repository-path' } | include("Repository path pattern should start with the project's full path")
lazy { { repository_path_pattern: "#{project.full_path}/path-invalid-chars-#@" } } | include("Repository path pattern should be a valid container repository path with optional wildcard characters.")
{ delete_protected_up_to_access_level: 1000 } | /not a valid delete_protected_up_to_access_level/
{ push_protected_up_to_access_level: 1000 } | /not a valid push_protected_up_to_access_level/
{ minimum_access_level_for_delete: 1000 } | /not a valid minimum_access_level_for_delete/
{ minimum_access_level_for_push: 1000 } | /not a valid minimum_access_level_for_push/
end
# rubocop:enable Layout/LineLength

View File

@ -246,12 +246,6 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
include_examples 'allow descendants to override'
end
context "when using SR_DISABLED_WITH_OVERRIDE" do
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_WITH_OVERRIDE } }
include_examples 'allow descendants to override'
end
end
end
end

View File

@ -41,7 +41,7 @@ module APIInternalBaseHelpers
)
end
def push(key, container, protocol = 'ssh', env: nil, changes: nil, relative_path: nil)
def push(key, container, protocol = 'ssh', env: nil, changes: nil)
push_with_path(
key,
full_path: full_path_for(container),
@ -49,7 +49,7 @@ module APIInternalBaseHelpers
protocol: protocol,
env: env,
changes: changes,
relative_path: relative_path
relative_path: container.repository.relative_path
)
end

View File

@ -1454,13 +1454,13 @@ RSpec.shared_examples 'a container registry auth service' do
context 'for different repository_path_patterns and current user roles' do
# rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table layout
where(:repository_path_pattern, :push_protected_up_to_access_level, :current_user, :shared_examples_name) do
ref(:container_repository_path) | :developer | ref(:project_developer) | 'a protected container repository'
ref(:container_repository_path) | :developer | ref(:project_owner) | 'a pushable'
ref(:container_repository_path) | :maintainer | ref(:project_maintainer) | 'a protected container repository'
ref(:container_repository_path) | :owner | ref(:project_owner) | 'a protected container repository'
ref(:container_repository_path_pattern_no_match) | :developer | ref(:project_developer) | 'a pushable'
ref(:container_repository_path_pattern_no_match) | :owner | ref(:project_owner) | 'a pushable'
where(:repository_path_pattern, :minimum_access_level_for_push, :current_user, :shared_examples_name) do
ref(:container_repository_path) | :maintainer | ref(:project_developer) | 'a protected container repository'
ref(:container_repository_path) | :maintainer | ref(:project_owner) | 'a pushable'
ref(:container_repository_path) | :owner | ref(:project_maintainer) | 'a protected container repository'
ref(:container_repository_path) | :admin | ref(:project_owner) | 'a protected container repository'
ref(:container_repository_path_pattern_no_match) | :maintainer | ref(:project_developer) | 'a pushable'
ref(:container_repository_path_pattern_no_match) | :admin | ref(:project_owner) | 'a pushable'
end
# rubocop:enable Layout/LineLength
@ -1468,7 +1468,7 @@ RSpec.shared_examples 'a container registry auth service' do
before do
container_registry_protection_rule.update!(
repository_path_pattern: repository_path_pattern,
push_protected_up_to_access_level: push_protected_up_to_access_level
minimum_access_level_for_push: minimum_access_level_for_push
)
end
@ -1480,7 +1480,7 @@ RSpec.shared_examples 'a container registry auth service' do
let_it_be(:current_user) { project_maintainer }
before do
container_registry_protection_rule.update!(push_protected_up_to_access_level: :maintainer)
container_registry_protection_rule.update!(minimum_access_level_for_push: :owner)
end
where(:current_params_scopes, :shared_examples_name) do
@ -1505,18 +1505,18 @@ RSpec.shared_examples 'a container registry auth service' do
end
context 'with matching package protection rule for all roles' do
where(:repository_path_pattern, :push_protected_up_to_access_level, :shared_examples_name) do
ref(:container_repository_path) | :developer | 'a pushable'
ref(:container_repository_path) | :owner | 'a pushable'
ref(:container_repository_path_pattern_no_match) | :developer | 'a pushable'
ref(:container_repository_path_pattern_no_match) | :owner | 'a pushable'
where(:repository_path_pattern, :minimum_access_level_for_push, :shared_examples_name) do
ref(:container_repository_path) | :maintainer | 'a pushable'
ref(:container_repository_path) | :admin | 'a pushable'
ref(:container_repository_path_pattern_no_match) | :maintainer | 'a pushable'
ref(:container_repository_path_pattern_no_match) | :admin | 'a pushable'
end
with_them do
before do
container_registry_protection_rule.update!(
repository_path_pattern: repository_path_pattern,
push_protected_up_to_access_level: push_protected_up_to_access_level
minimum_access_level_for_push: minimum_access_level_for_push
)
end