Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-06 18:09:03 +00:00
parent 67fa8362ae
commit 2b6716fbb2
47 changed files with 10951 additions and 175 deletions

1
.gitignore vendored
View File

@ -53,7 +53,6 @@ eslint-report.html
/db/data.yml
/doc/code/*
/dump.rdb
/glfm_specification/input/github_flavored_markdown/ghfm_spec_v_*.txt
/jsconfig.json
/lefthook-local.yml
/log/*.log*

View File

@ -13,6 +13,8 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@ -154,7 +156,9 @@ export default {
},
apollo: {
issues: {
query: getIssuesQuery,
query() {
return this.hasCrmParameter ? getIssuesQuery : getIssuesWithoutCrmQuery;
},
variables() {
return this.queryVariables;
},
@ -178,7 +182,9 @@ export default {
debounce: 200,
},
issuesCounts: {
query: getIssuesCountsQuery,
query() {
return this.hasCrmParameter ? getIssuesCountsQuery : getIssuesCountsWithoutCrmQuery;
},
variables() {
return this.queryVariables;
},
@ -390,6 +396,12 @@ export default {
...this.urlFilterParams,
};
},
hasCrmParameter() {
return (
window.location.search.includes('crm_contact_id=') ||
window.location.search.includes('crm_organization_id=')
);
},
},
watch: {
$route(newValue, oldValue) {

View File

@ -132,6 +132,8 @@ export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
export const TOKEN_TYPE_ITERATION = 'iteration';
export const TOKEN_TYPE_EPIC = 'epic_id';
export const TOKEN_TYPE_WEIGHT = 'weight';
export const TOKEN_TYPE_CONTACT = 'crm_contact';
export const TOKEN_TYPE_ORGANIZATION = 'crm_organization';
export const filters = {
[TOKEN_TYPE_AUTHOR]: {
@ -294,4 +296,24 @@ export const filters = {
},
},
},
[TOKEN_TYPE_CONTACT]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'crmContactId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'crm_contact_id',
},
},
},
[TOKEN_TYPE_ORGANIZATION]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'crmOrganizationId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'crm_organization_id',
},
},
},
};

View File

@ -20,6 +20,8 @@ query getIssues(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$crmContactId: String
$crmOrganizationId: String
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
@ -43,6 +45,8 @@ query getIssues(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
before: $beforeCursor
after: $afterCursor
@ -76,6 +80,8 @@ query getIssues(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
before: $beforeCursor
after: $afterCursor

View File

@ -14,6 +14,8 @@ query getIssuesCount(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$crmContactId: String
$crmOrganizationId: String
$not: NegatedIssueFilterInput
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
@ -32,6 +34,8 @@ query getIssuesCount(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@ -50,6 +54,8 @@ query getIssuesCount(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@ -68,6 +74,8 @@ query getIssuesCount(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@ -90,6 +98,8 @@ query getIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@ -109,6 +119,8 @@ query getIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@ -128,6 +140,8 @@ query getIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
) {
count

View File

@ -0,0 +1,136 @@
query getIssuesCountWithoutCrm(
$isProject: Boolean = false
$fullPath: ID!
$iid: String
$search: String
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
$confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
$myReactionEmoji: String
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
openedIssues: issues(
includeSubgroups: true
state: opened
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
count
}
closedIssues: issues(
includeSubgroups: true
state: closed
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
count
}
allIssues: issues(
includeSubgroups: true
state: all
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
count
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
id
openedIssues: issues(
state: opened
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
count
}
closedIssues: issues(
state: closed
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
count
}
allIssues: issues(
state: all
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
count
}
}
}

View File

@ -0,0 +1,93 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "./issue.fragment.graphql"
query getIssuesWithoutCrm(
$isProject: Boolean = false
$isSignedIn: Boolean = false
$fullPath: ID!
$iid: String
$search: String
$sort: IssueSort
$state: IssuableState
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
$confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
$myReactionEmoji: String
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
$lastPageSize: Int
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
issues(
includeSubgroups: true
iid: $iid
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
pageInfo {
...PageInfo
}
nodes {
...IssueFragment
reference(full: true)
}
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
id
issues(
iid: $iid
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
pageInfo {
...PageInfo
}
nodes {
...IssueFragment
}
}
}
}

View File

@ -1,7 +1,9 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { GlIcon, GlPopover, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { timeTilRun } from '../../utils';
import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
PARTIAL_CLEANUP_CONTINUE_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
@ -15,9 +17,9 @@ export default {
name: 'CleanupStatus',
components: {
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
GlPopover,
GlLink,
GlSprintf,
},
props: {
status: {
@ -29,12 +31,17 @@ export default {
);
},
},
expirationPolicyNextRunAt: {
type: String,
required: false,
default: () => '',
},
},
i18n: {
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
PARTIAL_CLEANUP_CONTINUE_MESSAGE,
},
computed: {
showStatus() {
@ -46,26 +53,57 @@ export default {
statusText() {
return this.$options.i18n[`CLEANUP_STATUS_${this.status}`];
},
expireIconClass() {
return this.failedDelete ? 'gl-text-orange-500' : '';
calculatedTimeTilNextRun() {
return timeTilRun(this.expirationPolicyNextRunAt?.next_run);
},
},
statusPopoverOptions: {
triggers: 'hover',
placement: 'top',
},
cleanupPolicyHelpPage: helpPagePath(
'user/packages/container_registry/reduce_container_registry_storage.html',
{ anchor: 'how-the-cleanup-policy-works' },
),
};
</script>
<template>
<div v-if="showStatus" class="gl-display-inline-flex gl-align-items-center">
<gl-icon name="expire" data-testid="main-icon" :class="expireIconClass" />
<div
v-if="showStatus"
id="status-popover-container"
class="gl-display-inline-flex gl-align-items-center"
>
<div class="gl-display-inline-flex gl-align-items-center">
<gl-icon name="expire" data-testid="main-icon" />
</div>
<span class="gl-mx-2">
{{ statusText }}
</span>
<gl-icon
v-if="failedDelete"
v-gl-tooltip="{ title: $options.i18n.CLEANUP_TIMED_OUT_ERROR_MESSAGE }"
id="status-info"
:size="14"
class="gl-text-black-normal"
class="gl-text-gray-500"
data-testid="extra-info"
name="information"
name="information-o"
/>
<gl-popover
target="status-info"
container="status-popover-container"
v-bind="$options.statusPopoverOptions"
>
<template #title>
{{ $options.i18n.CLEANUP_STATUS_UNFINISHED }}
</template>
<gl-sprintf :message="$options.i18n.PARTIAL_CLEANUP_CONTINUE_MESSAGE">
<template #time>{{ calculatedTimeTilNextRun }}</template
><template #link="{ content }"
><gl-link :href="$options.cleanupPolicyHelpPage" class="gl-font-sm" target="_blank">{{
content
}}</gl-link></template
>
</gl-sprintf>
</gl-popover>
</div>
</template>

View File

@ -22,6 +22,11 @@ export default {
type: Object,
required: true,
},
expirationPolicy: {
type: Object,
default: () => ({}),
required: false,
},
},
computed: {
showPagination() {
@ -38,6 +43,7 @@ export default {
:key="index"
:item="listItem"
:metadata-loading="metadataLoading"
:expiration-policy="expirationPolicy"
@delete="$emit('delete', $event)"
/>
<div class="gl-display-flex gl-justify-content-center">

View File

@ -6,12 +6,10 @@ import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
IMAGE_MIGRATING_STATE,
@ -45,6 +43,11 @@ export default {
default: false,
required: false,
},
expirationPolicy: {
type: Object,
default: () => ({}),
required: false,
},
},
i18n: {
REMOVE_REPOSITORY_LABEL,
@ -73,15 +76,6 @@ export default {
this.item.tagsCount,
);
},
warningIconText() {
if (this.failedDelete) {
return ASYNC_DELETE_IMAGE_ERROR_MESSAGE;
}
if (this.item.expirationPolicyStartedAt) {
return CLEANUP_TIMED_OUT_ERROR_MESSAGE;
}
return null;
},
imageName() {
return this.item.name ? this.item.path : `${this.item.path}/ ${ROOT_IMAGE_TEXT}`;
},
@ -140,6 +134,7 @@ export default {
v-if="item.expirationPolicyCleanupStatus"
class="ml-2"
:status="item.expirationPolicyCleanupStatus"
:expiration-policy-next-run-at="expirationPolicy.next_run_at"
/>
</template>

View File

@ -87,7 +87,7 @@ export const CLEANUP_DISABLED_TOOLTIP = s__(
export const CLEANUP_STATUS_SCHEDULED = s__('ContainerRegistry|Cleanup will run soon');
export const CLEANUP_STATUS_ONGOING = s__('ContainerRegistry|Cleanup is ongoing');
export const CLEANUP_STATUS_UNFINISHED = s__('ContainerRegistry|Cleanup timed out');
export const CLEANUP_STATUS_UNFINISHED = s__('ContainerRegistry|Partial cleanup complete');
export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling the image for deletion.',

View File

@ -10,7 +10,7 @@ export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not dele
export const DELETE_ALERT_LINK_TEXT = s__(
'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}',
);
export const CLEANUP_TIMED_OUT_ERROR_MESSAGE = s__(
'ContainerRegistry|Cleanup timed out before it could delete all tags',
export const PARTIAL_CLEANUP_CONTINUE_MESSAGE = s__(
'ContainerRegistry|The cleanup will continue within %{time}. %{linkStart}Learn more%{linkEnd}',
);
export const SET_UP_CLEANUP = s__('ContainerRegistry|Set up cleanup');

View File

@ -336,6 +336,7 @@ export default {
:images="images"
:metadata-loading="$apollo.queries.additionalDetails.loading"
:page-info="pageInfo"
:expiration-policy="config.expirationPolicy"
@delete="deleteImage"
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"

View File

@ -0,0 +1,8 @@
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
export const timeTilRun = (time) => {
if (!time) return '';
const difference = calculateRemainingMilliseconds(time);
return approximateDuration(difference / 1000);
};

View File

@ -159,7 +159,7 @@ export default {
</span>
</template>
<template #right-action>
<template v-if="packageEntity.canDestroy" #right-action>
<gl-dropdown
data-testid="delete-dropdown"
icon="ellipsis_v"

View File

@ -5,6 +5,7 @@ fragment PackageData on Package {
packageType
createdAt
status
canDestroy
tags {
nodes {
id

View File

@ -126,6 +126,10 @@ class DeployToken < ApplicationRecord
end
end
def impersonated?
false
end
def expires_at
expires_at = read_attribute(:expires_at)
expires_at != Forever.date ? expires_at : nil

View File

@ -6,7 +6,7 @@ module ContainerRegistry
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
include Gitlab::Utils::StrongMemoize
include ExclusiveLeaseGuard
include Gitlab::ExclusiveLeaseHelpers
DEFAULT_LEASE_TIMEOUT = 30.minutes.to_i.freeze
@ -18,16 +18,34 @@ module ContainerRegistry
def perform
re_enqueue = false
try_obtain_lease do
break unless runnable?
re_enqueue = handle_aborted_migration || handle_next_migration
took_lease = with_a_lease_on_repository do
if runnable?
re_enqueue = handle_aborted_migration || handle_next_migration
end
end
log_extra_metadata_on_done(:lease_already_taken, true) unless took_lease
re_enqueue_if_capacity if re_enqueue
end
private
def with_a_lease_on_repository
repository = next_aborted_repository || next_repository
if repository
in_lock(lease_key_for(repository), retries: 0, ttl: DEFAULT_LEASE_TIMEOUT) { yield }
else
log_extra_metadata_on_done(:no_container_repository_found, true)
end
true
rescue FailedToObtainLockError
false
end
def handle_aborted_migration
return unless next_aborted_repository
@ -160,14 +178,8 @@ module ContainerRegistry
log_extra_metadata_on_done(:container_repository_migration_state, repository.migration_state)
end
# used by ExclusiveLeaseGuard
def lease_key
'container_registry:migration:enqueuer_worker'
end
# used by ExclusiveLeaseGuard
def lease_timeout
DEFAULT_LEASE_TIMEOUT
def lease_key_for(repository)
"container_registry:migration:enqueuer_worker:for:#{repository.id}"
end
end
end

View File

@ -1,8 +0,0 @@
---
name: new_vulnerability_form
introduced_by_url:
rollout_issue_url:
milestone: '14.9'
type: development
group: group::threat insights
default_enabled: true

View File

@ -220,7 +220,7 @@ production: &base
#
# log_path: log/mail_room_json.log
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: false
# For Microsoft Graph support

View File

@ -3,6 +3,6 @@ table_name: verification_codes
classes: []
feature_categories:
- jihu
description: TODO
description: Used by the JiHu edition for user verification
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71139
milestone: '14.4'

View File

@ -302,7 +302,7 @@ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
@ -340,7 +340,7 @@ incoming_email:
# The IDLE command timeout.
idle_timeout: 60
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: true
```
@ -384,7 +384,7 @@ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
@ -422,7 +422,7 @@ incoming_email:
# The IDLE command timeout.
idle_timeout: 60
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: true
```
@ -464,7 +464,7 @@ gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
@ -495,7 +495,7 @@ incoming_email:
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: true
```
@ -528,7 +528,7 @@ gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
@ -555,7 +555,7 @@ incoming_email:
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: true
```
@ -612,7 +612,7 @@ gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
@ -642,7 +642,7 @@ incoming_email:
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: true
```
@ -673,7 +673,7 @@ gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
@ -703,7 +703,7 @@ incoming_email:
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: true
```
@ -733,7 +733,7 @@ gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
@ -758,7 +758,7 @@ incoming_email:
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: true
```

View File

@ -25,9 +25,6 @@ review for the pipeline, focusing on the additional access. You can use the [sof
as a starting point, and for more information about supply chain attacks, see
[How a DevOps Platform helps protect against supply chain attacks](https://about.gitlab.com/blog/2021/04/28/devops-platform-supply-chain-attacks/).
WARNING:
The `CI_JOB_JWT_V2` variable is under development [(alpha)](../../policy/alpha-beta-support.md#alpha-features) and is not yet suitable for production use.
## Use cases
- Removes the need to store secrets in your GitLab group or project. Temporary credentials can be retrieved from your cloud provider through OIDC.

View File

@ -22,14 +22,14 @@ This tutorial assumes you are familiar with GitLab CI/CD and Vault.
To follow along, you must have:
- An account on GitLab.
- Access to a running Vault server (at least v1.2.0) to configure authentication and to create roles and policies. For HashiCorp Vaults, this can be the Open Source or Enterprise version.
- Access to a running Vault server (at least v1.2.0) to configure authentication and to create roles and policies. You can use Open Source or Enterprise version of HashiCorp Vault.
NOTE:
You must replace the `vault.example.com` URL below with the URL of your Vault server, and `gitlab.example.com` with the URL of your GitLab instance.
## How it works
Each job has JSON Web Token (JWT) provided as CI/CD variable named `CI_JOB_JWT`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication) method.
Each job has JSON Web Token (JWT) provided as CI/CD variable named `CI_JOB_JWT` or `CI_JOB_JWT_V2`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication) method.
The following fields are included in the JWT:
@ -40,7 +40,8 @@ The following fields are included in the JWT:
| `iat` | Always | Issued at |
| `nbf` | Always | Not valid before |
| `exp` | Always | Expires at |
| `sub` | Always | Subject (job ID) |
| `sub` | Always | `project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}` |
| `aud` | Always | Issuer, the domain of your GitLab instance |
| `namespace_id` | Always | Use this to scope to group or user level namespace by ID |
| `namespace_path` | Always | Use this to scope to group or user level namespace by path |
| `project_id` | Always | Use this to scope to project by ID |
@ -62,11 +63,12 @@ Example JWT payload:
```json
{
"jti": "c82eeb0c-5c6f-4a33-abf5-4c474b92b558",
"iss": "gitlab.example.com",
"iss": "https://gitlab.example.com",
"aud": "https://gitlab.example.com",
"iat": 1585710286,
"nbf": 1585798372,
"exp": 1585713886,
"sub": "job_1212",
"sub": "project_path:mygroup/myproject:ref_type:branch:ref:main",
"namespace_id": "1",
"namespace_path": "mygroup",
"project_id": "22",
@ -91,6 +93,8 @@ You can use this JWT and your instance's JWKS endpoint (`https://gitlab.example.
When configuring roles in Vault, you can use [bound_claims](https://www.vaultproject.io/docs/auth/jwt#bound-claims) to match against the JWT's claims and restrict which secrets each CI job has access to.
You must use `bound_audiences` to match against the JWT's audience claim.
To communicate with Vault, you can use either its CLI client or perform API requests (using `curl` or another client).
## Example
@ -154,7 +158,8 @@ $ vault write auth/jwt/role/myproject-staging - <<EOF
"project_id": "22",
"ref": "master",
"ref_type": "branch"
}
},
"bound_audiences": "https://gitlab.example.com"
}
EOF
```
@ -174,7 +179,8 @@ $ vault write auth/jwt/role/myproject-production - <<EOF
"ref_protected": "true",
"ref_type": "branch",
"ref": "auto-deploy-*"
}
},
"bound_audiences": "https://gitlab.example.com"
}
EOF
```
@ -211,7 +217,7 @@ Role example to support the templated policy above, mapping the claim field `pro
}
```
For the full list of options, see Vault's [Create Role documentation](https://www.vaultproject.io/api/auth/jwt#create-role).
For the full list of options, see the Vault [Create Role documentation](https://www.vaultproject.io/api/auth/jwt#create-role).
WARNING:
Always restrict your roles to project or namespace by using one of the provided claims (for example, `project_id` or `namespace_id`). Otherwise any JWT generated by this instance may be allowed to authenticate using this role.
@ -221,12 +227,12 @@ Now, configure the JWT Authentication method:
```shell
$ vault write auth/jwt/config \
jwks_url="https://gitlab.example.com/-/jwks" \
bound_issuer="gitlab.example.com"
bound_issuer="https://gitlab.example.com"
```
[bound_issuer](https://www.vaultproject.io/api/auth/jwt#inlinecode-bound_issuer) specifies that only a JWT with the issuer (that is, the `iss` claim) set to `gitlab.example.com` can use this method to authenticate, and that the JWKS endpoint (`https://gitlab.example.com/-/jwks`) should be used to validate the token.
[`bound_issuer`](https://www.vaultproject.io/api/auth/jwt#inlinecode-bound_issuer) specifies that only a JWT with the issuer (that is, the `iss` claim) set to `https://gitlab.example.com` can use this method to authenticate, and that the JWKS endpoint (`https://gitlab.example.com/-/jwks`) should be used to validate the token.
For the full list of available configuration options, see Vault's [API documentation](https://www.vaultproject.io/api/auth/jwt#configure).
For the full list of available configuration options, see the Vault [API documentation](https://www.vaultproject.io/api/auth/jwt#configure).
The following job, when run for the default branch, is able to read secrets under `secret/myproject/staging/`, but not the secrets under `secret/myproject/production/`:
@ -241,7 +247,7 @@ read_secrets:
- export VAULT_ADDR=http://vault.example.com:8200
# Authenticate and get token. Token expiry time and other properties can be configured
# when configuring JWT Auth - https://www.vaultproject.io/api/auth/jwt#parameters-1
- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-staging jwt=$CI_JOB_JWT)"
- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-staging jwt=$CI_JOB_JWT_V2)"
# Now use the VAULT_TOKEN to read the secret and store it in an environment variable
- export PASSWORD="$(vault kv get -field=password secret/myproject/staging/db)"
# Use the secret
@ -269,7 +275,7 @@ read_secrets:
- export VAULT_ADDR=http://vault.example.com:8200
# Authenticate and get token. Token expiry time and other properties can be configured
# when configuring JWT Auth - https://www.vaultproject.io/api/auth/jwt#parameters-1
- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-production jwt=$CI_JOB_JWT)"
- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-production jwt=$CI_JOB_JWT_V2)"
# Now use the VAULT_TOKEN to read the secret and store it in environment variable
- export PASSWORD="$(vault kv get -field=password secret/myproject/production/db)"
# Use the secret
@ -280,7 +286,7 @@ read_secrets:
### Limit token access to Vault secrets
You can control `CI_JOB_JWT` access to Vault secrets by using Vault protections
You can control `CI_JOB_JWT_V2` access to Vault secrets by using Vault protections
and GitLab features. For example, restrict the token by:
- Using Vault [bound_claims](https://www.vaultproject.io/docs/auth/jwt#bound-claims)
@ -293,3 +299,14 @@ and GitLab features. For example, restrict the token by:
that are restricted to a subset of project users.
- Scoping the JWT to [GitLab projected tags](../../../user/project/protected_tags.md),
that are restricted to a subset of project users.
## Troubleshooting
### `Code: 400. Errors: audience claim found in JWT but no audiences bound to the role`
This issue occurs because you are using a JWT V2 token and your role on Vault is missing a value for `bound_audiences`.
### `Code: 400. Errors: error validating claims: validation failed, invalid issuer claim (iss)`
The issuer in Vault must match the issuer defined in a JWT V2 token.
In Vault, ensure you're using `https` in your issuer.

View File

@ -25,8 +25,7 @@ as the first supported secrets engine.
GitLab authenticates using Vault's
[JSON Web Token (JWT) authentication method](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication), using
the [JSON Web Token](https://gitlab.com/gitlab-org/gitlab/-/issues/207125) (`CI_JOB_JWT`)
introduced in GitLab 12.10.
the [JSON Web Token](https://gitlab.com/gitlab-org/gitlab/-/issues/207125) (`CI_JOB_JWT_V2`).
You must [configure your Vault server](#configure-your-vault-server) before you
can use [use Vault secrets in a CI job](#use-vault-secrets-in-a-ci-job).
@ -34,7 +33,7 @@ can use [use Vault secrets in a CI job](#use-vault-secrets-in-a-ci-job).
The flow for using GitLab with HashiCorp Vault
is summarized by this diagram:
![Flow between GitLab and HashiCorp](../img/gitlab_vault_workflow_v13_4.png "How GitLab CI_JOB_JWT works with HashiCorp Vault")
![Flow between GitLab and HashiCorp](../img/gitlab_vault_workflow_v13_4.png "How GitLab CI_JOB_JWT_V2 works with HashiCorp Vault")
1. Configure your vault and secrets.
1. Generate your JWT and provide it to your CI job.
@ -64,7 +63,7 @@ To configure your Vault server:
$ vault write auth/jwt/config \
jwks_url="https://gitlab.example.com/-/jwks" \
bound_issuer="gitlab.example.com"
bound_issuer="https://gitlab.example.com"
```
1. Configure policies on your Vault server to grant or forbid access to certain
@ -167,7 +166,8 @@ $ vault write auth/jwt/role/myproject-production - <<EOF
"ref_protected": "true",
"ref_type": "tag",
"ref": "auto-deploy-*"
}
},
"bound_audiences": "https://gitlab.example.com"
}
EOF
```
@ -177,7 +177,7 @@ Always restrict your roles to a project or namespace by using one of the provide
claims like `project_id` or `namespace_id`. Without these restrictions, any JWT
generated by this GitLab instance may be allowed to authenticate using this role.
For a full list of `CI_JOB_JWT` claims, read the
For a full list of `CI_JOB_JWT_V2` claims, read the
[How it works](../examples/authenticating-with-hashicorp-vault/index.md#how-it-works) section of the
[Authenticating and Reading Secrets With HashiCorp Vault](../examples/authenticating-with-hashicorp-vault/index.md) tutorial.

View File

@ -64,7 +64,7 @@ There are also a number of [variables you can use to configure runner behavior](
| `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the Docker image running the job. |
| `CI_JOB_JWT` | 12.10 | all | A RS256 JSON web token to authenticate with third party systems that support JWT authentication, for example [HashiCorp's Vault](../secrets/index.md). |
| `CI_JOB_JWT_V1` | 14.6 | all | The same value as `CI_JOB_JWT`. |
| `CI_JOB_JWT_V2` | 14.6 | all | [**alpha:**](../../policy/alpha-beta-support.md#alpha-features) A newly formatted RS256 JSON web token to increase compatibility. Similar to `CI_JOB_JWT`, except the issuer (`iss`) claim is changed from `gitlab.com` to `https://gitlab.com`, `sub` has changed from `job_id` to a string that contains the project path, and an `aud` claim is added. Format is subject to change. Be aware, the `aud` field is a constant value. Trusting JWTs in multiple relying parties can lead to [one RP sending a JWT to another one and acting maliciously as a job](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72555#note_769112331). |
| `CI_JOB_JWT_V2` | 14.6 | all | A newly formatted RS256 JSON web token to increase compatibility. Similar to `CI_JOB_JWT`, except the issuer (`iss`) claim is changed from `gitlab.com` to `https://gitlab.com`, `sub` has changed from `job_id` to a string that contains the project path, and an `aud` claim is added. Format is subject to change. Be aware, the `aud` field is a constant value. Trusting JWTs in multiple relying parties can lead to [one RP sending a JWT to another one and acting maliciously as a job](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72555#note_769112331). |
| `CI_JOB_MANUAL` | 8.12 | all | `true` if a job was started manually. |
| `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job. |
| `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the job's stage. |

View File

@ -86,7 +86,7 @@ See the [Rails guides](https://guides.rubyonrails.org/action_mailer_basics.html#
# The IDLE command timeout.
idle_timeout: 60
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
# Whether to expunge (permanently remove) messages from the mailbox when they are marked as deleted after delivery
expunge_deleted: false
```

View File

@ -64,7 +64,7 @@ serve as input to automated conformance tests. It is
[explained in the CommonMark specification](https://spec.commonmark.org/0.30/#about-this-document):
> This document attempts to specify Markdown syntax unambiguously. It contains many
> examples with side-by-side Markdown and HTML. These are intended to double as conformance tests.
> examples with side-by-side Markdown and HTML. These examples are intended to double as conformance tests.
The HTML-rendered versions of the specifications:
@ -508,21 +508,76 @@ They are either downloaded, as in the case of the
GFM `spec.txt` file, or manually
updated, as in the case of all GFM files.
- `glfm_specification/input/github_flavored_markdown/gfm_spec_v_0.29.txt` -
official latest [GFM spec.txt](https://github.com/github/cmark-gfm/blob/master/test/spec.txt),
automatically downloaded and updated by `update-specification.rb` script.
- `glfm_specification/input/gitlab_flavored_markdown/glfm_intro.txt` -
Manually updated text of intro section for generated GLFM `spec.txt`.
- Replaces GFM version of introductory
section in `spec.txt`.
- `glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt` -
Manually updated canonical Markdown+HTML examples for GLFM extensions.
- Standard backtick-delimited `spec.txt` examples format with Markdown + canonical HTML.
- Inserted as a new section before the appendix of generated `spec.txt`.
- `glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml` -
Manually updated status of automatic generation of files based on Markdown
examples.
- Allows example snapshot generation, Markdown conformance tests, or
##### GitHub Flavored Markdown specification
[`glfm_specification/input/github_flavored_markdown/gfm_spec_v_0.29.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/github_flavored_markdown/gfm_spec_v_0.29.txt)
is the official latest [GFM `spec.txt`](https://github.com/github/cmark-gfm/blob/master/test/spec.txt).
- It is automatically downloaded and updated by `update-specification.rb` script.
- When it is downloaded, the version number is added to the filename.
##### `glfm_intro.txt`
[`glfm_specification/input/gitlab_flavored_markdown/glfm_intro.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/gitlab_flavored_markdown/glfm_intro.txt)
is the GitLab-specific version of the prose in the introduction section of the GLFM specification.
- It is manually updated.
- The `update-specification.rb` script inserts it into the generated GLFM `spec.txt` to replace
the GitHub-specific GFM version of the introductory section.
##### `glfm_canonical_examples.txt`
[`glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt)
is the manually updated canonical Markdown+HTML examples for GLFM extensions.
- It contains examples in the [standard backtick-delimited `spec.txt` format](#various-markdown-specifications),
each of which contain a Markdown example and the corresponding canonical HTML.
- The `update-specification.rb` script inserts it as new sections before the appendix
of generated `spec.txt`.
- It should consist of `H1` header sections, with a examples nested exactly 2 levels deep within `H2`
header sections.
`glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt` sample entries:
NOTE:
All lines in this example are prefixed with a `|` character. This prefix helps avoid false
errors when this file is checked by `markdownlint`, and possible errors in other Markdown editors.
The actual file should not have these prefixed `|` characters.
```plaintext
|# First GitLab-Specific Section with Examples
|
|## Strong but with two asterisks
|
|```````````````````````````````` example
|**bold**
|.
|<p><strong>bold</strong></p>
|````````````````````````````````
|
|# Second GitLab-Specific Section with Examples
|
|## Strong but with HTML
|
|```````````````````````````````` example
|<strong>
|bold
|</strong>
|.
|<p><strong>
|bold
|</strong></p>
|````````````````````````````````
```
##### `glfm_example_status.yml`
[`glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml)
controls the behavior of the [scripts](#scripts) and [tests](#types-of-markdown-tests-driven-by-the-glfm-specification).
- It is manually updated.
- It controls the status of automatic generation of files based on Markdown examples.
- It allows example snapshot generation, Markdown conformance tests, or
Markdown snapshot tests to be skipped for individual examples. For example, if
they are unimplemented, broken, or cannot be tested for some reason.
@ -545,28 +600,39 @@ updated, as in the case of all GFM files.
The `glfm_specification/output` directory contains the CommonMark standard format
`spec.txt` file which represents the canonical GLFM specification which is generated
by the `update-specification.rb` script. It also contains the rendered `spec.html`
and `spec.pdf` which are generated from with the `spec.txt` as input.
which is generated based on the `spec.txt` as input.
- `glfm_specification/output/spec.txt` - A Markdown file, in the standard format
with prose and Markdown + canonical HTML examples, generated (or updated) by the
`update-specification.rb` script.
- `glfm_specification/output/spec.html` - An HTML file, rendered based on `spec.txt`,
also generated (or updated) by the `update-specification.rb` script at the same time as
`spec.txt`. It corresponds to the HTML-rendered versions of the
"GitHub Flavored Markdown" (<abbr title="GitHub Flavored Markdown">GFM</abbr>)
[specification](https://github.github.com/gfm/)
and the [CommonMark specification](https://spec.commonmark.org/0.30/).
These output `spec.**` files, which represent the official, canonical GLFM specification
These output `spec.*` files, which represent the official, canonical GLFM specification,
are colocated under the same parent folder `glfm_specification` with the other
`input` specification files. They're located here both for convenience, and because they are all
a mix of manually edited and generated files. In GFM,
`spec.txt` is [located in the test dir](https://github.com/github/cmark-gfm/blob/master/test/spec.txt),
and in CommonMark it's located
[in the project root](https://github.com/github/cmark-gfm/blob/master/test/spec.txt).
No precedent exists for a standard location. In the future, we may decide to
a mix of manually edited and generated files.
In GFM, `spec.txt` is [located in the test dir](https://github.com/github/cmark-gfm/blob/master/test/spec.txt),
and in CommonMark it's located [in the project root](https://github.com/github/cmark-gfm/blob/master/test/spec.txt). No precedent exists for a standard location. In the future, we may decide to
move or copy a hosted version of the rendered HTML `spec.html` version to another location or site.
##### spec.txt
[`glfm_specification/output/spec.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/output/spec.txt)
is a Markdown specification file, in the standard format
with prose and Markdown + canonical HTML examples. It is generated or updated by the
`update-specification.rb` script.
It also serves as input for other scripts such as `update-example-snapshots.rb`
and `run-spec-tests.sh`.
##### spec.html
[`glfm_specification/output/spec.html`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/output/spec.html)
is an HTML file, rendered based on `spec.txt`. It is
also generated (or updated) by the `update-specification.rb` script at the same time as
`spec.txt`.
It corresponds to the HTML-rendered versions of the
"GitHub Flavored Markdown" (<abbr title="GitHub Flavored Markdown">GFM</abbr>)
[specification](https://github.github.com/gfm/)
and the [CommonMark specification](https://spec.commonmark.org/0.30/).
### Example snapshot files
The `example_snapshots` directory contains files which are generated by the
@ -578,12 +644,13 @@ After the entire GLFM implementation is complete for both backend (Ruby) and
frontend (JavaScript), all of these YAML files can be automatically generated.
However, while the implementations are still in progress, the `skip_update_example_snapshots`
key in `glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml`
can be used to disable automatic generation of some examples, and they can instead
can be used to disable automatic generation of some examples. They can instead
be manually edited as necessary to help drive the implementations.
#### `spec/fixtures/glfm/example_snapshots/examples_index.yml`
`spec/fixtures/glfm/example_snapshots/examples_index.yml` is the main list of all
[`spec/fixtures/glfm/example_snapshots/examples_index.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/fixtures/glfm/example_snapshots/examples_index.yml)
is the main list of all
CommonMark, GFM, and GLFM example names, each with a unique canonical name.
- It is generated from the hierarchical sections and examples in the
@ -594,10 +661,15 @@ CommonMark, GFM, and GLFM example names, each with a unique canonical name.
the additional Section 7 in the GLFM `spec.txt`.
- It also contains extra metadata about each example, such as:
1. `spec_txt_example_position` - The position of the example in the generated GLFM `spec.txt` file.
- This value is the index order of each individual Markdown + HTML5 example in the file. It is _not_
the line number in the file.
- This value can be used to locate the example in the rendered `spec.html` file, because the standard
CommonMark tooling includes the index number for each example in the rendered HTML file.
For example: [https://spec.commonmark.org/0.30/#example-42](https://spec.commonmark.org/0.30/#example-42)
1. `source_specification` - Which specification the example originally came from:
`commonmark`, `github`, or `gitlab`.
- The naming convention for example entry names is based on nested header section
names and example index within the header.
names and example index in the header.
- This naming convention should result in fairly stable names and example positions.
The CommonMark / GLFM specification rarely changes, and most GLFM
examples where multiple examples exist for the same Section 7 subsection are
@ -625,7 +697,7 @@ CommonMark, GFM, and GLFM example names, each with a unique canonical name.
#### `spec/fixtures/glfm/example_snapshots/markdown.yml`
`spec/fixtures/glfm/example_snapshots/markdown.yml` contains the original Markdown
[`spec/fixtures/glfm/example_snapshots/markdown.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/fixtures/glfm/example_snapshots/markdown.yml) contains the original Markdown
for each entry in `spec/fixtures/glfm/example_snapshots/examples_index.yml`
- For CommonMark and GFM Markdown,
@ -644,8 +716,8 @@ for each entry in `spec/fixtures/glfm/example_snapshots/examples_index.yml`
#### `spec/fixtures/glfm/example_snapshots/html.yml`
`spec/fixtures/glfm/example_snapshots/html.yml` contains the HTML for each entry in
`spec/fixtures/glfm/example_snapshots/examples_index.yml`
[`spec/fixtures/glfm/example_snapshots/html.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/fixtures/glfm/example_snapshots/html.yml)
contains the HTML for each entry in `spec/fixtures/glfm/example_snapshots/examples_index.yml`
Three types of entries exist, with different HTML for each:
@ -688,8 +760,8 @@ depending on how the implementations evolve.
#### `spec/fixtures/glfm/example_snapshots/prosemirror_json.yml`
`spec/fixtures/glfm/example_snapshots/prosemirror_json.yml` contains the ProseMirror
JSON for each entry in `spec/fixtures/glfm/example_snapshots/examples_index.yml`
[`spec/fixtures/glfm/example_snapshots/prosemirror_json.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/fixtures/glfm/example_snapshots/prosemirror_json.yml)
contains the ProseMirror JSON for each entry in `spec/fixtures/glfm/example_snapshots/examples_index.yml`
- It is generated (or updated) from the frontend code via the `update-example-snapshots.rb`
script, but can be manually updated for examples with incomplete implementations.

View File

@ -7,10 +7,6 @@ comments: false
# Upgrading Community Edition and Enterprise Edition from source **(FREE SELF)**
NOTE:
Users wishing to upgrade to 12.0.0 must take some extra steps. See the
version specific upgrade instructions for 12.0.0 for more details.
Make sure you view this update guide from the branch (version) of GitLab you
would like to install (for example, `11.8`). You can select the required version of documentation in the dropdown at the top right corner of GitLab documentation page.
@ -421,6 +417,39 @@ Example:
Additional instructions here.
-->
### 15.0.0
Support for more than one database has been added to GitLab. [As part of this](https://gitlab.com/gitlab-org/gitlab/-/issues/338182),
`config/database.yml` needs to include a database name in the database configuration.
The `main: database` must be first. If an invalid or deprecated syntax is used, an error is generated
during application start:
```plaintext
ERROR: This installation of GitLab uses unsupported 'config/database.yml'.
The main: database needs to be defined as a first configuration item instead of primary. (RuntimeError)
```
Previously, the `config/database.yml` file looked like the following:
```yaml
production:
adapter: postgresql
encoding: unicode
database: gitlabhq_production
...
```
Starting with GitLab 15.0, it needs to define a `main` database first:
```yaml
production:
main:
adapter: postgresql
encoding: unicode
database: gitlabhq_production
...
```
### 14.5.0
As part of [enabling real-time issue assignees](https://gitlab.com/gitlab-org/gitlab/-/issues/330117), Action Cable is now enabled by default, and requires `config/cable.yml` to be present.

View File

@ -230,6 +230,7 @@ To undo this action, select a different status from the same menu.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/301003) in GitLab 14.9. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/353796) in GitLab 14.10.
> - [Feature flag `new_vulnerability_form`](https://gitlab.com/gitlab-org/gitlab/-/issues/359049) removed in GitLab 15.0.
To add a new vulnerability finding from your project level Vulnerability Report page:

View File

@ -14,11 +14,10 @@ To connect your Kubernetes cluster with GitLab, you can use:
The certificate-based integration is
[**deprecated**](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/)
in GitLab 14.5. The removal dates are:
in GitLab 14.5. The sunsetting plans are described:
- [GitLab.com customers](../../../update/deprecations.md#saas-certificate-based-integration-with-kubernetes): GitLab 15.0.
- [Self-managed customers](../../../update/deprecations.md#self-managed-certificate-based-integration-with-kubernetes):
Placed behind a disabled feature flag in GitLab 15.0, and removed entirely in GitLab 15.6.
- for [GitLab.com customers](../../../update/deprecations.md#saas-certificate-based-integration-with-kubernetes).
- for [Self-managed customers](../../../update/deprecations.md#self-managed-certificate-based-integration-with-kubernetes).
If you are using the certificate-based integration, you should move to another workflow as soon as possible.

View File

@ -196,7 +196,9 @@ coverage-jdk11:
needs: ["test-jdk11"]
artifacts:
reports:
cobertura: target/site/cobertura.xml
coverage_report:
coverage_format: cobertura
path: target/site/cobertura.xml
```
#### Gradle example
@ -232,7 +234,9 @@ coverage-jdk11:
needs: ["test-jdk11"]
artifacts:
reports:
cobertura: build/cobertura.xml
coverage_report:
coverage_format: cobertura
path: build/cobertura.xml
```
### Python example
@ -254,7 +258,9 @@ run tests:
coverage: '/TOTAL.*\s([.\d]+)%/'
artifacts:
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
```
### PHP example
@ -284,7 +290,9 @@ run tests:
- php ./vendor/bin/phpunit --coverage-text --coverage-cobertura=coverage.cobertura.xml
artifacts:
reports:
cobertura: coverage.cobertura.xml
coverage_report:
coverage_format: cobertura
path: coverage.cobertura.xml
```
[Codeception](https://codeception.com/), through PHPUnit, also supports generating Cobertura report with
@ -319,7 +327,9 @@ run tests:
name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA}
expire_in: 2 days
reports:
cobertura: build/coverage.xml
coverage_report:
coverage_format: cobertura
path: build/coverage.xml
```
### Go example
@ -346,7 +356,9 @@ run tests:
- go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml
artifacts:
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
```
### Ruby example
@ -373,5 +385,7 @@ run tests:
- bundle exec rspec
artifacts:
reports:
cobertura: coverage/coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage/coverage.xml
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
PLACEHOLDER FILE. Actual contents will be added by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84347

View File

@ -0,0 +1 @@
PLACEHOLDER FILE. Actual contents will be added in a future MR.

View File

@ -9543,12 +9543,6 @@ msgstr ""
msgid "ContainerRegistry|Cleanup ran but some tags were not removed"
msgstr ""
msgid "ContainerRegistry|Cleanup timed out"
msgstr ""
msgid "ContainerRegistry|Cleanup timed out before it could delete all tags"
msgstr ""
msgid "ContainerRegistry|Cleanup will run %{time}"
msgstr ""
@ -9651,6 +9645,9 @@ msgstr ""
msgid "ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time"
msgstr ""
msgid "ContainerRegistry|Partial cleanup complete"
msgstr ""
msgid "ContainerRegistry|Please try different search criteria"
msgstr ""
@ -9749,6 +9746,9 @@ msgstr ""
msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|The cleanup will continue within %{time}. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|The filter returned no results"
msgstr ""

View File

@ -0,0 +1 @@
PLACEHOLDER FILE. Actual contents will be added by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84347

View File

@ -0,0 +1 @@
PLACEHOLDER FILE. Actual contents will be added by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84347

View File

@ -0,0 +1 @@
PLACEHOLDER FILE. Actual contents will be added by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84347

View File

@ -0,0 +1 @@
PLACEHOLDER FILE. Actual contents will be added by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84347

View File

@ -8,6 +8,8 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@ -118,6 +120,8 @@ describe('CE IssuesListApp component', () => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
[getIssuesWithoutCrmQuery, issuesQueryResponse],
[getIssuesCountsWithoutCrmQuery, issuesCountsQueryResponse],
[setSortPreferenceMutation, sortPreferenceMutationResponse],
];

View File

@ -146,6 +146,8 @@ export const locationSearch = [
'not[epic_id]=34',
'weight=1',
'not[weight]=3',
'crm_contact_id=123',
'crm_organization_id=456',
].join('&');
export const locationSearchWithSpecialValues = [
@ -194,6 +196,8 @@ export const filteredTokens = [
{ type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
{ type: 'crm_contact', value: { data: '123', operator: OPERATOR_IS } },
{ type: 'crm_organization', value: { data: '456', operator: OPERATOR_IS } },
{ type: 'filtered-search-term', value: { data: 'find' } },
{ type: 'filtered-search-term', value: { data: 'issues' } },
];
@ -222,6 +226,8 @@ export const apiParams = {
iterationId: ['4', '12'],
epicId: '12',
weight: '1',
crmContactId: '123',
crmOrganizationId: '456',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
@ -270,6 +276,8 @@ export const urlParams = {
'not[epic_id]': '34',
weight: '1',
'not[weight]': '3',
crm_contact_id: '123',
crm_organization_id: '456',
};
export const urlParamsWithSpecialValues = {

View File

@ -1,8 +1,8 @@
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
@ -17,12 +17,20 @@ describe('cleanup_status', () => {
const findMainIcon = () => wrapper.findByTestId('main-icon');
const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
const findPopover = () => wrapper.findComponent(GlPopover);
const cleanupPolicyHelpPage = helpPagePath(
'user/packages/container_registry/reduce_container_registry_storage.html',
{ anchor: 'how-the-cleanup-policy-works' },
);
const mountComponent = (propsData = { status: SCHEDULED_STATUS }) => {
wrapper = shallowMountExtended(CleanupStatus, {
propsData,
directives: {
GlTooltip: createMockDirective(),
stubs: {
GlLink,
GlPopover,
GlSprintf,
},
});
};
@ -43,7 +51,7 @@ describe('cleanup_status', () => {
mountComponent({ status });
expect(findMainIcon().exists()).toBe(visible);
expect(wrapper.text()).toBe(text);
expect(wrapper.text()).toContain(text);
},
);
@ -53,12 +61,6 @@ describe('cleanup_status', () => {
expect(findMainIcon().exists()).toBe(true);
});
it(`has the orange class when the status is ${UNFINISHED_STATUS}`, () => {
mountComponent({ status: UNFINISHED_STATUS });
expect(findMainIcon().classes('gl-text-orange-500')).toBe(true);
});
});
describe('extra info icon', () => {
@ -76,12 +78,12 @@ describe('cleanup_status', () => {
},
);
it(`has a tooltip`, () => {
it(`has a popover with a learn more link`, () => {
mountComponent({ status: UNFINISHED_STATUS });
const tooltip = getBinding(findExtraInfoIcon().element, 'gl-tooltip');
expect(tooltip.value.title).toBe(CLEANUP_TIMED_OUT_ERROR_MESSAGE);
expect(findPopover().exists()).toBe(true);
expect(findPopover().findComponent(GlLink).exists()).toBe(true);
expect(findPopover().findComponent(GlLink).attributes('href')).toBe(cleanupPolicyHelpPage);
});
});
});

View File

@ -0,0 +1,21 @@
import { timeTilRun } from '~/packages_and_registries/container_registry/explorer/utils';
describe('Container registry utilities', () => {
describe('timeTilRun', () => {
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
});
it('should return a human readable time', () => {
const result = timeTilRun('2063-04-08T01:44:03Z');
expect(result).toBe('4 days');
});
it('should return an empty string with null times', () => {
const result = timeTilRun(null);
expect(result).toBe('');
});
});
});

View File

@ -28,6 +28,7 @@ describe('packages_list_row', () => {
const packageWithoutTags = { ...packageData(), project: packageProject() };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
const packageCannotDestroy = { ...packageData(), canDestroy: false };
const findPackageTags = () => wrapper.find(PackageTags);
const findPackagePath = () => wrapper.find(PackagePath);
@ -101,6 +102,12 @@ describe('packages_list_row', () => {
});
describe('delete button', () => {
it('does not exist when package cannot be destroyed', () => {
mountComponent({ packageEntity: packageCannotDestroy });
expect(findDeleteDropdown().exists()).toBe(false);
});
it('exists and has the correct props', () => {
mountComponent({ packageEntity: packageWithoutTags });

View File

@ -469,4 +469,10 @@ RSpec.describe DeployToken do
end
end
end
describe '.impersonated?' do
it 'returns false' do
expect(subject.impersonated?).to be(false)
end
end
end

View File

@ -236,12 +236,22 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
end
context 'when no repository qualifies' do
include_examples 'an idempotent worker' do
before do
allow(ContainerRepository).to receive(:ready_for_import).and_return(ContainerRepository.none)
end
before do
allow(ContainerRepository).to receive(:ready_for_import).and_return(ContainerRepository.none)
end
it_behaves_like 'no action'
context 'idempotency' do
include_examples 'an idempotent worker' do
it_behaves_like 'no action'
end
end
context 'logging' do
it 'logs no_container_repository_found' do
expect_log_extra_metadata(no_container_repository_found: true)
subject
end
end
end
@ -307,7 +317,7 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
end
context 'with the exclusive lease taken' do
let(:lease_key) { worker.send(:lease_key) }
let(:lease_key) { "container_registry:migration:enqueuer_worker:for:#{container_repository.id}" }
before do
stub_exclusive_lease_taken(lease_key, timeout: 30.minutes)
@ -316,6 +326,23 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
it 'does not perform' do
expect(worker).not_to receive(:runnable?)
expect(worker).not_to receive(:re_enqueue_if_capacity)
expect_log_extra_metadata(lease_already_taken: true)
subject
end
end
context 'with exclusive lease taken for a different repository' do
let(:lease_key) { "container_registry:migration:enqueuer_worker:for:test" }
before do
Gitlab::ExclusiveLease.new(lease_key, timeout: 30.minutes).try_obtain
end
it 'does perform' do
expect(worker).to receive(:runnable?).and_return(true)
expect(worker).to receive(:handle_aborted_migration).and_return(container_repository)
expect(worker).to receive(:re_enqueue_if_capacity)
subject
end