Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-29 18:15:31 +00:00
parent 7f0fd430c2
commit ed899a6a1e
135 changed files with 2792 additions and 1184 deletions

View File

@ -220,7 +220,7 @@ variables:
DOCS_REVIEW_APPS_DOMAIN: "docs.gitlab-review.app"
DOCS_GITLAB_REPO_SUFFIX: "ee"
REVIEW_APPS_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bookworm-ruby-3.0:gcloud-383-kubectl-1.26-helm-3.9"
REVIEW_APPS_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bookworm-ruby-3.0:gcloud-383-kubectl-1.27-helm-3.9"
REVIEW_APPS_DOMAIN: "gitlab-review.app"
REVIEW_APPS_GCP_PROJECT: "gitlab-review-apps"
REVIEW_APPS_GCP_REGION: "us-central1"

View File

@ -7,7 +7,7 @@
.gitlab/CODEOWNERS @gitlab-org/development-leaders @gitlab-org/tw-leadership
## Allows release tooling and Gitaly team members to update the Gitaly Version
GITALY_SERVER_VERSION @project_278964_bot6 @gitlab-org/maintainers/rails-backend @gitlab-org/delivery @gl-gitaly
/GITALY_SERVER_VERSION @project_278964_bot6 @gitlab-org/maintainers/rails-backend @gitlab-org/delivery @gl-gitaly
## Files that are excluded from required approval
## These rules override the * rule above, so that changes to docs and templates

View File

@ -50,12 +50,8 @@ workflow:
- export QA_GITLAB_URL="http://gitlab.${GITLAB_DOMAIN}"
- source scripts/utils.sh
- source scripts/rspec_helpers.sh
- source scripts/qa/cng_deploy/cng-kind.sh
- cd qa && bundle install
- bundle exec cng create cluster --ci
# Currently this only performs pre-deploy setup
- bundle exec cng create deployment --ci
- deploy ${GITLAB_DOMAIN}
- bundle exec cng create deployment --gitlab-domain "${GITLAB_DOMAIN}" --ci --with-cluster ${EXTRA_DEPLOY_VALUES}
script:
- export QA_COMMAND="bundle exec bin/qa ${QA_SCENARIO:=Test::Instance::All} $QA_GITLAB_URL -- --force-color --order random --format documentation"
- echo "Running - '$QA_COMMAND'"
@ -120,7 +116,11 @@ cng-qa-min-redis-version:
extends: .cng-base
variables:
QA_SCENARIO: Test::Instance::Smoke
REDIS_VERSION_TYPE: MIN_REDIS_VERSION
before_script:
- |
redis_version=$(awk -F "=" "/MIN_REDIS_VERSION =/ {print \$2}" $CI_PROJECT_DIR/lib/system_check/app/redis_version_check.rb | sed "s/['\" ]//g")
export EXTRA_DEPLOY_VALUES="--set redis.image.tag=${redis_version%.*}"
- !reference [.cng-base, before_script]
after_script:
- !reference [.set-suite-status, after_script]
- !reference [.cng-base, after_script]

View File

@ -2,17 +2,6 @@
# Cop supports --autocorrect.
Layout/FirstHashElementIndentation:
Exclude:
- 'ee/app/helpers/ee/geo_helper.rb'
- 'ee/app/helpers/ee/groups/group_members_helper.rb'
- 'ee/app/models/ee/list.rb'
- 'ee/app/services/app_sec/dast/profiles/update_service.rb'
- 'ee/app/services/elastic/cluster_reindexing_service.rb'
- 'ee/app/services/iterations/create_service.rb'
- 'ee/app/services/resource_events/change_iteration_service.rb'
- 'ee/app/services/security/token_revocation_service.rb'
- 'ee/app/services/timebox_report_service.rb'
- 'ee/lib/ee/gitlab/ci/parsers.rb'
- 'ee/lib/ee/gitlab/usage_data.rb'
- 'ee/spec/factories/dependencies.rb'
- 'ee/spec/finders/epics_finder_spec.rb'
- 'ee/spec/finders/namespaces/free_user_cap/users_finder_spec.rb'

View File

@ -2,20 +2,6 @@
# Cop supports --autocorrect.
Layout/SpaceInLambdaLiteral:
Exclude:
- 'app/serializers/deploy_keys/basic_deploy_key_entity.rb'
- 'app/serializers/deployment_cluster_entity.rb'
- 'app/serializers/deployment_entity.rb'
- 'app/serializers/detailed_status_entity.rb'
- 'app/serializers/diff_file_base_entity.rb'
- 'app/serializers/diff_file_entity.rb'
- 'app/serializers/diffs_entity.rb'
- 'app/serializers/discussion_entity.rb'
- 'app/serializers/draft_note_entity.rb'
- 'app/serializers/environment_entity.rb'
- 'app/serializers/feature_flag_entity.rb'
- 'app/serializers/issue_board_entity.rb'
- 'app/serializers/issue_entity.rb'
- 'app/serializers/issue_sidebar_basic_entity.rb'
- 'ee/app/serializers/blocking_merge_request_entity.rb'
- 'ee/app/serializers/clusters/environment_entity.rb'
- 'ee/app/serializers/dashboard_operations_project_entity.rb'

View File

@ -40,7 +40,9 @@ export default {
<gl-empty-state v-if="abuseReports.length == 0" :title="s__('AbuseReports|No reports found')" />
<ul v-else class="gl-pl-0">
<abuse-report-row v-for="(report, index) in abuseReports" :key="index" :report="report" />
<li v-for="(report, index) in abuseReports" :key="index" class="gl-list-style-none">
<abuse-report-row :report="report" />
</li>
</ul>
<gl-pagination

View File

@ -32,7 +32,7 @@ export default {
};
</script>
<template>
<div class="d-inline-block gl-float-right mr-3">
<div class="gl-inline-block gl-float-right mr-3">
<gl-button v-gl-modal="$options.modalId" variant="danger" category="primary">
{{ __('Delete') }}
</gl-button>

View File

@ -82,12 +82,12 @@ export default {
<div class="file-holder">
<div ref="header" class="file-title file-title-flex-parent">
<div class="file-header-content d-flex align-content-center gl-flex-wrap overflow-hidden">
<div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
<div v-if="hasCode" class="gl-inline-block cursor-pointer" @click="toggle()">
<gl-icon :name="collapseIcon" :size="16" class="gl-mr-2" />
</div>
<template v-if="filePath">
<file-icon :file-name="filePath" :size="16" aria-hidden="true" css-classes="gl-mr-2" />
<strong class="file-title-name d-inline-block overflow-hidden limited-width">
<strong class="file-title-name gl-inline-block overflow-hidden limited-width">
<gl-truncate with-tooltip :text="filePath" position="middle" />
</strong>
<clipboard-button

View File

@ -180,7 +180,7 @@ export default {
v-if="itemPath"
v-gl-tooltip
:title="itemPath"
class="path-id-text d-inline-block"
class="path-id-text gl-inline-block"
>{{ itemPath }}</span
>
<span>{{ pathIdSeparator }}{{ itemId }}</span>
@ -232,7 +232,7 @@ export default {
<span
v-if="isLocked"
v-gl-tooltip
class="gl-display-inline-block gl-cursor-not-allowed"
class="gl-inline-block gl-cursor-not-allowed"
:title="lockedMessage"
data-testid="lockIcon"
>

View File

@ -66,7 +66,7 @@ export default {
<template>
<div class="suggestion-item">
<div class="d-flex gl-align-items-center">
<div class="gl-flex gl-align-items-center">
<gl-icon
v-if="suggestion.confidential"
v-gl-tooltip.bottom
@ -85,7 +85,7 @@ export default {
<div class="text-secondary suggestion-footer">
<gl-icon ref="state" :name="stateIconName" :class="stateIconClass" class="gl-cursor-help" />
<gl-tooltip :target="() => $refs.state" placement="bottom">
<span class="d-block">
<span class="gl-block">
<span class="bold"> {{ stateTitle }} </span> {{ timeFormatted(closedOrCreatedDate) }}
</span>
<span class="text-tertiary">{{ tooltipTitle(closedOrCreatedDate) }}</span>
@ -103,9 +103,9 @@ export default {
:size="16"
css-classes="mr-0 float-none"
tooltip-placement="bottom"
class="d-inline-block"
class="gl-inline-block"
>
<span class="bold d-block">{{ __('Author') }}</span> {{ suggestion.author.name }}
<span class="bold gl-block">{{ __('Author') }}</span> {{ suggestion.author.name }}
<span class="text-tertiary">@{{ suggestion.author.username }}</span>
</user-avatar-image>
</gl-link>

View File

@ -217,9 +217,7 @@ function handleTracingPeriodFilter(rawValue, filterName, filterParams) {
* @param {Object} filterObj : An Object representing filters
* @returns URLSearchParams
*/
function tracingFilterObjToQueryParams(filterObj) {
const filterParams = new URLSearchParams();
function addTracingAttributesFiltersToQueryParams(filterObj, filterParams) {
Object.keys(SUPPORTED_TRACING_FILTERS).forEach((filterName) => {
const filterValues = Array.isArray(filterObj[filterName]) ? filterObj[filterName] : [];
const validFilters = filterValues.filter((f) =>
@ -266,7 +264,13 @@ async function fetchTraces(
tracingUrl,
{ filters = {}, pageToken, pageSize, sortBy, abortController } = {},
) {
const params = tracingFilterObjToQueryParams(filters);
const params = new URLSearchParams();
const { attributes } = filters;
if (attributes) {
addTracingAttributesFiltersToQueryParams(attributes, params);
}
if (pageToken) {
params.append('page_token', pageToken);
}
@ -294,7 +298,12 @@ async function fetchTraces(
}
async function fetchTracesAnalytics(tracingAnalyticsUrl, { filters = {}, abortController } = {}) {
const params = tracingFilterObjToQueryParams(filters);
const params = new URLSearchParams();
const { attributes } = filters;
if (attributes) {
addTracingAttributesFiltersToQueryParams(attributes, params);
}
try {
const { data } = await axios.get(tracingAnalyticsUrl, {

View File

@ -27,13 +27,13 @@ export default {
<template>
<ul class="gl-pl-0">
<image-list-row
v-for="(listItem, index) in images"
:key="index"
:item="listItem"
:metadata-loading="metadataLoading"
:expiration-policy="expirationPolicy"
@delete="$emit('delete', $event)"
/>
<li v-for="(listItem, index) in images" :key="index" class="gl-list-style-none">
<image-list-row
:item="listItem"
:metadata-loading="metadataLoading"
:expiration-policy="expirationPolicy"
@delete="$emit('delete', $event)"
/>
</li>
</ul>
</template>

View File

@ -54,12 +54,12 @@ export default {
<div v-else data-testid="main-area">
<ul class="gl-pl-0">
<manifest-row
v-for="(manifest, index) in manifests"
:key="index"
:dependency-proxy-image-prefix="dependencyProxyImagePrefix"
:manifest="manifest"
/>
<li v-for="(manifest, index) in manifests" :key="index">
<manifest-row
:dependency-proxy-image-prefix="dependencyProxyImagePrefix"
:manifest="manifest"
/>
</li>
</ul>
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination

View File

@ -189,17 +189,17 @@ export default {
<template v-else-if="hasVersions">
<ul class="gl-pl-0">
<package-list-row
v-for="v in packageEntity.versions"
:key="v.id"
:package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
name: packageEntity.name,
...v,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:package-link="v.id.toString()"
:disable-delete="true"
:show-package-type="false"
/>
<li v-for="v in packageEntity.versions" :key="v.id" class="gl-list-style-none">
<package-list-row
:package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
name: packageEntity.name,
...v,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:package-link="v.id.toString()"
:disable-delete="true"
:show-package-type="false"
/>
</li>
</ul>
</template>

View File

@ -76,14 +76,14 @@ export default {
<template v-else>
<ul data-testid="packages-table" class="gl-pl-0">
<packages-list-row
v-for="packageEntity in list"
:key="packageEntity.id"
:package-entity="packageEntity"
:package-link="packageEntity._links.web_path"
:is-group="isGroupPage"
@packageToDelete="setItemToBeDeleted"
/>
<li v-for="packageEntity in list" :key="packageEntity.id" class="gl-list-style-none">
<packages-list-row
:package-entity="packageEntity"
:package-link="packageEntity._links.web_path"
:is-group="isGroupPage"
@packageToDelete="setItemToBeDeleted"
/>
</li>
</ul>
<gl-pagination

View File

@ -117,13 +117,15 @@ export default {
</gl-button>
</div>
<ul v-for="(item, index) in items" :key="index" class="gl-pl-0">
<slot
:select-item="selectItem"
:is-selected="isSelected"
:item="item"
:first="!hiddenDelete && index === 0"
></slot>
<ul class="gl-pl-0">
<li v-for="(item, index) in items" :key="index" class="gl-list-style-none">
<slot
:select-item="selectItem"
:is-selected="isSelected"
:item="item"
:first="!hiddenDelete && index === 0"
></slot>
</li>
</ul>
<div class="gl-display-flex gl-justify-content-center">

View File

@ -195,7 +195,7 @@ export default {
<gl-sprintf :message="$options.i18n.issueTrackerEnableMessage">
<template #link="{ content }">
<gl-link
class="gl-display-inline-block"
class="gl-inline-block"
data-testid="issue-help-page"
:href="issuesHelpPagePath"
target="_blank"
@ -209,7 +209,7 @@ export default {
id="service-desk-checkbox"
:value="isEnabled"
:disabled="!isIssueTrackerEnabled"
class="d-inline-block align-middle mr-1"
class="!gl-inline-block align-middle mr-1"
:label="$options.i18n.toggleLabel"
label-position="hidden"
@change="onCheckboxToggle"
@ -241,7 +241,7 @@ export default {
</template>
</gl-form-input-group>
<template v-if="email && hasServiceDeskEmail" #description>
<span class="gl-mt-2 gl-display-inline-block">
<span class="gl-mt-2 gl-inline-block">
<gl-sprintf :message="__('Emails sent to %{email} are also supported.')">
<template #email>
<code>{{ incomingEmail }}</code>

View File

@ -59,7 +59,7 @@ export default {
v-if="parentPath === loadingPath"
size="sm"
inline
class="d-inline-block align-text-bottom"
class="gl-inline-block align-text-bottom"
/>
<router-link v-else :to="parentRoute" :aria-label="__('Go to parent')"> .. </router-link>
</td>

View File

@ -42,17 +42,17 @@ export default {
</script>
<template>
<!-- must be `d-inline-block` or parent flex-basis causes width issues -->
<!-- must be `gl-inline-block` or parent flex-basis causes width issues -->
<gl-link
:href="assigneeUrl"
:data-user-id="assigneeId"
:data-username="user.username"
:data-cannot-merge="cannotMerge"
data-placement="left"
class="gl-display-inline-block js-user-link"
class="gl-inline-block js-user-link"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="gl-display-flex">
<!-- use gl-flex so that slot can be appropriately styled -->
<span class="gl-flex">
<assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot></slot>
</span>

View File

@ -46,17 +46,17 @@ export default {
</script>
<template>
<!-- must be `d-inline-block` or parent flex-basis causes width issues -->
<!-- must be `gl-inline-block` or parent flex-basis causes width issues -->
<gl-link
:href="reviewerUrl"
:data-user-id="reviewerId"
:data-username="user.username"
:data-cannot-merge="cannotMerge"
data-placement="left"
class="gl-display-inline-block js-user-link gl-reset-color! gl-hover-text-blue-800!"
class="gl-inline-block js-user-link gl-reset-color! gl-hover-text-blue-800!"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="gl-display-flex">
<span class="gl-flex">
<reviewer-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>

View File

@ -81,7 +81,7 @@ export default {
v-gl-tooltip.right
:title="tooltipTitle"
:class="{ 'ml-auto': isCentered }"
class="file-changed-icon d-inline-block"
class="file-changed-icon gl-inline-block"
>
<gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span>

View File

@ -58,7 +58,7 @@ export default {
</script>
<template>
<li
<div
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
@ -159,5 +159,5 @@ export default {
</div>
<div class="gl-w-9"></div>
</div>
</li>
</div>
</template>

View File

@ -34,9 +34,7 @@ export default {
class="issues-bulk-update right-sidebar"
aria-live="polite"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-gray-100"
>
<div class="gl-display-flex gl-justify-content-space-between gl-p-4 gl-border-b">
<slot name="bulk-edit-actions"></slot>
</div>
<slot name="sidebar-items"></slot>

View File

@ -255,7 +255,6 @@ export default {
>
<gl-form-checkbox
v-if="showCheckbox"
class="issue-check gl-mr-0"
:checked="checked"
:data-id="issuableId"
:data-iid="issuableIid"

View File

@ -1,9 +1,7 @@
<script>
import { GlAlert, GlBadge, GlKeysetPagination, GlSkeletonLoader, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { DRAG_DELAY } from '~/sortable/constants';
@ -221,7 +219,7 @@ export default {
},
data() {
return {
checkedIssuables: {},
checkedIssuableIds: [],
};
},
computed: {
@ -237,18 +235,10 @@ export default {
return DEFAULT_SKELETON_COUNT;
},
allIssuablesChecked() {
return this.bulkEditIssuables.length === this.issuables.length;
return this.checkedIssuables.length === this.issuables.length;
},
/**
* Returns all the checked issuables from `checkedIssuables` map.
*/
bulkEditIssuables() {
return Object.keys(this.checkedIssuables).reduce((acc, issuableId) => {
if (this.checkedIssuables[issuableId].checked) {
acc.push(this.checkedIssuables[issuableId].issuable);
}
return acc;
}, []);
checkedIssuables() {
return this.issuables.filter((issuable) => this.checkedIssuableIds.includes(issuable.id));
},
issuablesWrapper() {
return this.isManualOrdering ? VueDraggable : 'ul';
@ -258,24 +248,6 @@ export default {
},
},
watch: {
issuables(list) {
this.checkedIssuables = list.reduce((acc, issuable) => {
const id = this.issuableId(issuable);
acc[id] = {
// By default, an issuable is not checked,
// But if `checkedIssuables` is already
// populated, use existing value.
checked:
typeof this.checkedIssuables[id] !== 'boolean'
? false
: this.checkedIssuables[id].checked,
// We're caching issuable reference here
// for ease of populating in `bulkEditIssuables`.
issuable,
};
return acc;
}, {});
},
urlParams: {
deep: true,
immediate: true,
@ -291,21 +263,28 @@ export default {
},
},
methods: {
issuableId(issuable) {
return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId();
isIssuableChecked(issuable) {
return this.checkedIssuableIds.includes(issuable.id);
},
issuableChecked(issuable) {
return this.checkedIssuables[this.issuableId(issuable)]?.checked;
updateCheckedIssuableIds(issuable, toCheck) {
const isIdChecked = this.checkedIssuableIds.includes(issuable.id);
if (toCheck && !isIdChecked) {
this.checkedIssuableIds.push(issuable.id);
}
if (!toCheck && isIdChecked) {
const indexToDelete = this.checkedIssuableIds.findIndex((id) => id === issuable.id);
this.checkedIssuableIds.splice(indexToDelete, 1);
}
},
handleIssuableCheckedInput(issuable, value) {
this.checkedIssuables[this.issuableId(issuable)].checked = value;
this.updateCheckedIssuableIds(issuable, value);
this.$emit('update-legacy-bulk-edit');
issuableEventHub.$emit('issuables:issuableChecked', issuable, value);
},
handleAllIssuablesCheckedInput(value) {
Object.keys(this.checkedIssuables).forEach((issuableId) => {
this.checkedIssuables[issuableId].checked = value;
});
this.issuables.forEach((issuable) => this.updateCheckedIssuableIds(issuable, value));
this.$emit('update-legacy-bulk-edit');
},
handleVueDraggableUpdate({ newIndex, oldIndex }) {
@ -357,10 +336,10 @@ export default {
<gl-alert v-if="error" variant="danger" @dismiss="$emit('dismiss-alert')">{{ error }}</gl-alert>
<issuable-bulk-edit-sidebar :expanded="showBulkEditSidebar">
<template #bulk-edit-actions>
<slot name="bulk-edit-actions" :checked-issuables="bulkEditIssuables"></slot>
<slot name="bulk-edit-actions" :checked-issuables="checkedIssuables"></slot>
</template>
<template #sidebar-items>
<slot name="sidebar-items" :checked-issuables="bulkEditIssuables"></slot>
<slot name="sidebar-items" :checked-issuables="checkedIssuables"></slot>
</template>
</issuable-bulk-edit-sidebar>
<slot name="list-body"></slot>
@ -380,7 +359,7 @@ export default {
>
<issuable-item
v-for="issuable in issuables"
:key="issuableId(issuable)"
:key="issuable.id"
:class="{ 'gl-cursor-grab': isManualOrdering }"
data-testid="issuable-container"
:data-qa-issuable-title="issuable.title"
@ -389,7 +368,7 @@ export default {
:issuable="issuable"
:label-filter-param="labelFilterParam"
:show-checkbox="showBulkEditSidebar"
:checked="issuableChecked(issuable)"
:checked="isIssuableChecked(issuable)"
:show-work-item-type-icon="showWorkItemTypeIcon"
:prevent-redirect="preventRedirect"
:is-active="isIssuableActive(issuable)"

View File

@ -29,7 +29,7 @@ module Types
field :icon, GraphQL::Types::String, null: true, description: 'Icon for the catalog resource.',
method: :avatar_path, alpha: { milestone: '15.11' }
field :full_path, GraphQL::Types::String, null: true, description: 'Full project path of the catalog resource.',
field :full_path, GraphQL::Types::ID, null: true, description: 'Full project path of the catalog resource.',
alpha: { milestone: '16.11' }
field :web_path, GraphQL::Types::String, null: true, description: 'Web path of the catalog resource.',

View File

@ -11,7 +11,7 @@ module Types
field :project, Types::ProjectType, null: false, description: 'Project the design belongs to.'
field :issue, Types::IssueType, null: false, description: 'Issue the design belongs to.'
field :filename, GraphQL::Types::String, null: false, description: 'Filename of the design.'
field :full_path, GraphQL::Types::String, null: false, description: 'Full path to the design file.'
field :full_path, GraphQL::Types::ID, null: false, description: 'Full path to the design file.'
field :image, GraphQL::Types::String, null: false, extras: [:parent], description: 'URL of the full-sized image.'
field :image_v432x230,
GraphQL::Types::String,

View File

@ -138,7 +138,7 @@ class ContainerRepository < ApplicationRecord
# does a search of tags containing the name and we filter them
# to find the exact match. Otherwise, we instantiate a tag.
def tag(tag)
if can_access_the_gitlab_api?
if migrated_and_can_access_the_gitlab_api?
page = tags_page(name: tag)
return if page[:tags].blank?
@ -158,7 +158,7 @@ class ContainerRepository < ApplicationRecord
def tags
strong_memoize(:tags) do
if can_access_the_gitlab_api?
if migrated_and_can_access_the_gitlab_api?
result = []
each_tags_page do |array_of_tags|
result << array_of_tags
@ -268,7 +268,7 @@ class ContainerRepository < ApplicationRecord
end
def last_published_at
return unless can_access_the_gitlab_api?
return unless migrated_and_can_access_the_gitlab_api?
timestamp_string = gitlab_api_client_repository_details['last_published_at']
DateTime.iso8601(timestamp_string)
@ -321,8 +321,8 @@ class ContainerRepository < ApplicationRecord
private
def can_access_the_gitlab_api?
migrated? && ContainerRegistry::GitlabApiClient.supports_gitlab_api?
def migrated_and_can_access_the_gitlab_api?
migrated? && gitlab_api_client.supports_gitlab_api?
end
def transform_tags_page(tags_response_body)

View File

@ -542,6 +542,7 @@ class Project < ApplicationRecord
delegate :job_token_scope_enabled, :job_token_scope_enabled=, prefix: :ci_outbound
with_options prefix: :ci do
delegate :pipeline_variables_minimum_override_role, :pipeline_variables_minimum_override_role=
delegate :default_git_depth, :default_git_depth=
delegate :forward_deployment_enabled, :forward_deployment_enabled=
delegate :forward_deployment_rollback_allowed, :forward_deployment_rollback_allowed=
@ -3101,6 +3102,12 @@ class Project < ApplicationRecord
ci_cd_settings.restrict_user_defined_variables?
end
def override_pipeline_variables_allowed?(access_level)
return false unless ci_cd_settings
ci_cd_settings.override_pipeline_variables_allowed?(access_level)
end
def keep_latest_artifacts_available?
return false unless ci_cd_settings

View File

@ -6,6 +6,17 @@ class ProjectCiCdSetting < ApplicationRecord
belongs_to :project, inverse_of: :ci_cd_settings
DEFAULT_GIT_DEPTH = 20
NO_ONE_ALLOWED_ROLE = 1
DEVELOPER_ROLE = 2
MAINTAINER_ROLE = 3
OWNER_ROLE = 4
enum pipeline_variables_minimum_override_role: {
no_one_allowed: NO_ONE_ALLOWED_ROLE,
developer: DEVELOPER_ROLE,
maintainer: MAINTAINER_ROLE,
owner: OWNER_ROLE
}, _prefix: true
before_create :set_default_git_depth
@ -28,8 +39,28 @@ class ProjectCiCdSetting < ApplicationRecord
Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? && keep_latest_artifact?
end
def override_pipeline_variables_allowed?(role_access_level)
return true unless restrict_user_defined_variables?
project_minimum_access_level = pipeline_variables_minimum_override_role_for_database
return false if project_minimum_access_level == NO_ONE_ALLOWED_ROLE
role_project_minimum_access_level = role_map_pipeline_variables_minimum_override_role[project_minimum_access_level]
role_access_level >= role_project_minimum_access_level
end
private
def role_map_pipeline_variables_minimum_override_role
{
DEVELOPER_ROLE => Gitlab::Access::DEVELOPER,
MAINTAINER_ROLE => Gitlab::Access::MAINTAINER,
OWNER_ROLE => Gitlab::Access::OWNER
}
end
def set_default_git_depth
self.default_git_depth ||= DEFAULT_GIT_DEPTH
end

View File

@ -68,6 +68,9 @@ class ProjectPolicy < BasePolicy
desc "Project is archived"
condition(:archived, scope: :subject, score: 0) { project.archived? }
desc "Project user pipeline variables minimum override role"
condition(:project_pipeline_override_role_owner) { project.ci_pipeline_variables_minimum_override_role == 'owner' }
desc "Project is in the process of being deleted"
condition(:pending_delete) { project.pending_delete? }
@ -240,7 +243,11 @@ class ProjectPolicy < BasePolicy
end
condition(:user_defined_variables_allowed) do
!@subject.restrict_user_defined_variables?
if ::Feature.enabled?(:allow_user_variables_by_minimum_role, @subject)
@subject.override_pipeline_variables_allowed?(team_access_level)
else
!@subject.restrict_user_defined_variables? || can?(:maintainer_access)
end
end
condition(:packages_disabled, scope: :subject) { !@subject.packages_enabled }
@ -309,6 +316,8 @@ class ProjectPolicy < BasePolicy
rule { maintainer }.enable :maintainer_access
rule { owner | admin | organization_owner }.enable :owner_access
rule { project_pipeline_override_role_owner & ~can?(:owner_access) }.prevent :change_restrict_user_defined_variables
rule { can?(:owner_access) }.policy do
enable :guest_access
enable :reporter_access
@ -607,6 +616,7 @@ class ProjectPolicy < BasePolicy
enable :admin_push_rules
enable :manage_deploy_tokens
enable :manage_merge_request_settings
enable :change_restrict_user_defined_variables
end
rule { can?(:admin_build) }.enable :manage_trigger
@ -943,7 +953,7 @@ class ProjectPolicy < BasePolicy
prevent :manage_resource_access_tokens
end
rule { user_defined_variables_allowed | can?(:maintainer_access) }.policy do
rule { user_defined_variables_allowed }.policy do
enable :set_pipeline_variables
end

View File

@ -15,16 +15,16 @@ module DeployKeys
expose :expires_at
expose :updated_at
expose :can_edit
expose :user, as: :owner, using: ::API::Entities::UserBasic, if: -> (_, opts) { can_read_owner?(opts) }
expose :edit_path, if: -> (_, opts) { opts[:project] } do |deploy_key|
expose :user, as: :owner, using: ::API::Entities::UserBasic, if: ->(_, opts) { can_read_owner?(opts) }
expose :edit_path, if: ->(_, opts) { opts[:project] } do |deploy_key|
edit_project_deploy_key_path(options[:project], deploy_key)
end
expose :enable_path, if: -> (_, opts) { opts[:project] } do |deploy_key|
expose :enable_path, if: ->(_, opts) { opts[:project] } do |deploy_key|
enable_project_deploy_key_path(options[:project], deploy_key)
end
expose :disable_path, if: -> (_, opts) { opts[:project] } do |deploy_key|
expose :disable_path, if: ->(_, opts) { opts[:project] } do |deploy_key|
disable_project_deploy_key_path(options[:project], deploy_key)
end

View File

@ -10,11 +10,11 @@ class DeploymentClusterEntity < Grape::Entity
deployment.cluster.name
end
expose :path, if: -> (deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment|
expose :path, if: ->(deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment|
deployment.cluster.present(current_user: request.current_user).show_path
end
expose :kubernetes_namespace, if: -> (deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment|
expose :kubernetes_namespace, if: ->(deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment|
deployment.kubernetes_namespace
end
end

View File

@ -27,7 +27,7 @@ class DeploymentEntity < Grape::Entity
expose :deployed_by, as: :user, using: UserEntity
expose :deployable, if: -> (deployment) { deployment.deployable.present? } do |deployment, opts|
expose :deployable, if: ->(deployment) { deployment.deployable.present? } do |deployment, opts|
deployment.deployable.then do |deployable|
if include_details?
Ci::JobEntity.represent(deployable, opts)
@ -38,10 +38,10 @@ class DeploymentEntity < Grape::Entity
end
end
expose :commit, using: CommitEntity, if: -> (*) { include_details? }
expose :manual_actions, using: Ci::JobEntity, if: -> (*) { include_details? && can_create_deployment? }
expose :scheduled_actions, using: Ci::JobEntity, if: -> (*) { include_details? && can_create_deployment? }
expose :playable_job, as: :playable_build, if: -> (deployment) { include_details? && can_create_deployment? && deployment.playable_job } do |deployment, options|
expose :commit, using: CommitEntity, if: ->(*) { include_details? }
expose :manual_actions, using: Ci::JobEntity, if: ->(*) { include_details? && can_create_deployment? }
expose :scheduled_actions, using: Ci::JobEntity, if: ->(*) { include_details? && can_create_deployment? }
expose :playable_job, as: :playable_build, if: ->(deployment) { include_details? && can_create_deployment? && deployment.playable_job } do |deployment, options|
Ci::JobEntity.represent(deployment.playable_job, options.merge(only: [:play_path, :retry_path]))
end

View File

@ -38,7 +38,7 @@ class DetailedStatusEntity < Grape::Entity
Gitlab::Favicon.ci_status_overlay(status.favicon)
end
expose :action, if: -> (status, _) { status.has_action? } do
expose :action, if: ->(status, _) { status.has_action? } do
expose :action_icon, as: :icon, documentation: { type: 'string', example: 'cancel' }
expose :action_title, as: :title, documentation: { type: 'string', example: 'Cancel' }
expose :action_path, as: :path, documentation: { type: 'string', example: '/namespace1/project1/-/jobs/2/cancel' }

View File

@ -31,7 +31,7 @@ class DiffFileBaseEntity < Grape::Entity
}
end
expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
expose :edit_path, if: ->(_, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
next unless has_edit_path?(merge_request)
@ -43,7 +43,7 @@ class DiffFileBaseEntity < Grape::Entity
project_edit_blob_path(target_project, tree_join(target_branch, diff_file.new_path), options)
end
expose :ide_edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
expose :ide_edit_path, if: ->(_, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
next unless has_edit_path?(merge_request)
@ -61,11 +61,11 @@ class DiffFileBaseEntity < Grape::Entity
new_path
end
expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
expose :formatted_external_url, if: ->(_, options) { options[:environment] } do |diff_file|
options[:environment].formatted_external_url
end
expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
expose :external_url, if: ->(_, options) { options[:environment] } do |diff_file|
options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
end

View File

@ -9,7 +9,7 @@ class DiffFileEntity < DiffFileBaseEntity
expose :added_lines
expose :removed_lines
expose :load_collapsed_diff_url, if: -> (diff_file, options) { options[:merge_request] } do |diff_file|
expose :load_collapsed_diff_url, if: ->(diff_file, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
project = merge_request.target_project
@ -25,7 +25,7 @@ class DiffFileEntity < DiffFileBaseEntity
)
end
expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
expose :view_path, if: ->(_, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
project = merge_request.target_project
@ -36,7 +36,7 @@ class DiffFileEntity < DiffFileBaseEntity
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
end
expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
expose :replaced_view_path, if: ->(_, options) { options[:merge_request] } do |diff_file|
image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
@ -48,14 +48,14 @@ class DiffFileEntity < DiffFileBaseEntity
project_blob_path(project, tree_join(diff_file.old_content_sha, diff_file.old_path)) if image_diff && image_replaced
end
expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
expose :context_lines_path, if: ->(diff_file, _) { diff_file.text? } do |diff_file|
next unless diff_file.content_sha
project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
end
# Used for inline diffs
expose :diff_lines_for_serializer, as: :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, options) { display_highlighted_diffs?(diff_file, options) }
expose :diff_lines_for_serializer, as: :highlighted_diff_lines, using: DiffLineEntity, if: ->(diff_file, options) { display_highlighted_diffs?(diff_file, options) }
expose :viewer do |diff_file, options|
whitespace_only = if !display_highlighted_diffs?(diff_file, options)
@ -72,9 +72,9 @@ class DiffFileEntity < DiffFileBaseEntity
expose :fully_expanded?, as: :is_fully_expanded
# Used for parallel diffs
expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, options) { parallel_diff_view?(options) && diff_file.text? }
expose :parallel_diff_lines, using: DiffLineParallelEntity, if: ->(diff_file, options) { parallel_diff_view?(options) && diff_file.text? }
expose :code_navigation_path, if: -> (diff_file) { options[:code_navigation_path] } do |diff_file|
expose :code_navigation_path, if: ->(diff_file) { options[:code_navigation_path] } do |diff_file|
options[:code_navigation_path].full_json_path_for(diff_file.new_path)
end

View File

@ -39,7 +39,7 @@ class DiffsEntity < Grape::Entity
options[:latest_diff]
end
expose :latest_version_path, if: -> (*) { merge_request } do |diffs|
expose :latest_version_path, if: ->(*) { merge_request } do |diffs|
diffs_project_merge_request_path(merge_request&.project, merge_request)
end
@ -59,11 +59,11 @@ class DiffsEntity < Grape::Entity
render_overflow_warning?(diffs)
end
expose :email_patch_path, if: -> (*) { merge_request } do |diffs|
expose :email_patch_path, if: ->(*) { merge_request } do |diffs|
merge_request_path(merge_request, format: :patch)
end
expose :plain_diff_path, if: -> (*) { merge_request } do |diffs|
expose :plain_diff_path, if: ->(*) { merge_request } do |diffs|
merge_request_path(merge_request, format: :diff)
end
@ -79,7 +79,7 @@ class DiffsEntity < Grape::Entity
)
end
expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
expose :merge_request_diffs, using: MergeRequestDiffEntity, if: ->(_, options) { options[:merge_request_diffs]&.any? } do |diffs|
options[:merge_request_diffs]
end

View File

@ -11,11 +11,11 @@ class DiscussionEntity < BaseDiscussionEntity
)
end
expose :positions, if: -> (d, _) { display_merge_ref_discussions?(d) } do |discussion|
expose :positions, if: ->(d, _) { display_merge_ref_discussions?(d) } do |discussion|
discussion.diff_note_positions.map(&:position)
end
expose :line_codes, if: -> (d, _) { display_merge_ref_discussions?(d) } do |discussion|
expose :line_codes, if: ->(d, _) { display_merge_ref_discussions?(d) } do |discussion|
discussion.diff_note_positions.map(&:line_code)
end

View File

@ -5,7 +5,7 @@ class DraftNoteEntity < Grape::Entity
expose :id
expose :author, using: NoteUserEntity
expose :merge_request_id
expose :position, if: -> (note, _) { note.on_diff? }
expose :position, if: ->(note, _) { note.on_diff? }
expose :line_code
expose :file_identifier_hash
expose :file_hash

View File

@ -19,10 +19,10 @@ class EnvironmentEntity < Grape::Entity
expose :name_without_type
expose :last_deployment, using: DeploymentEntity
expose :stop_actions_available?, as: :has_stop_action
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
expose :rollout_status, if: ->(*) { can_read_deploy_board? }, using: RolloutStatusEntity
expose :tier
expose :upcoming_deployment, if: -> (environment) { environment.upcoming_deployment } do |environment, ops|
expose :upcoming_deployment, if: ->(environment) { environment.upcoming_deployment } do |environment, ops|
DeploymentEntity.represent(environment.upcoming_deployment,
ops.merge(except: UNNECESSARY_ENTRIES_FOR_UPCOMING_DEPLOYMENT))
end
@ -35,7 +35,7 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment)
end
expose :cancel_auto_stop_path, if: -> (*) { can_update_environment? } do |environment|
expose :cancel_auto_stop_path, if: ->(*) { can_update_environment? } do |environment|
cancel_auto_stop_project_environment_path(environment.project, environment)
end

View File

@ -12,15 +12,15 @@ class FeatureFlagEntity < Grape::Entity
expose :description
expose :version
expose :edit_path, if: -> (feature_flag, _) { can_update?(feature_flag) } do |feature_flag|
expose :edit_path, if: ->(feature_flag, _) { can_update?(feature_flag) } do |feature_flag|
edit_project_feature_flag_path(feature_flag.project, feature_flag)
end
expose :update_path, if: -> (feature_flag, _) { can_update?(feature_flag) } do |feature_flag|
expose :update_path, if: ->(feature_flag, _) { can_update?(feature_flag) } do |feature_flag|
project_feature_flag_path(feature_flag.project, feature_flag)
end
expose :destroy_path, if: -> (feature_flag, _) { can_destroy?(feature_flag) } do |feature_flag|
expose :destroy_path, if: ->(feature_flag, _) { can_destroy?(feature_flag) } do |feature_flag|
project_feature_flag_path(feature_flag.project, feature_flag)
end

View File

@ -24,7 +24,7 @@ class IssueBoardEntity < Grape::Entity
API::Entities::Project.represent issue.project, only: [:id, :path, :path_with_namespace]
end
expose :milestone, if: -> (issue) { issue.milestone } do |issue|
expose :milestone, if: ->(issue) { issue.milestone } do |issue|
API::Entities::Milestone.represent issue.milestone, only: [:id, :title]
end
@ -36,23 +36,23 @@ class IssueBoardEntity < Grape::Entity
LabelEntity.represent issue.labels, project: issue.project, only: [:id, :title, :description, :color, :priority, :text_color]
end
expose :reference_path, if: -> (issue) { issue.project } do |issue, options|
expose :reference_path, if: ->(issue) { issue.project } do |issue, options|
options[:include_full_project_path] ? issue.to_reference(full: true) : issue.to_reference
end
expose :real_path, if: -> (issue) { issue.project } do |issue|
expose :real_path, if: ->(issue) { issue.project } do |issue|
Gitlab::UrlBuilder.build(issue, only_path: true)
end
expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|
expose :issue_sidebar_endpoint, if: ->(issue) { issue.project } do |issue|
project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar_extras')
end
expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue|
expose :toggle_subscription_endpoint, if: ->(issue) { issue.project } do |issue|
toggle_subscription_project_issue_path(issue.project, issue)
end
expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue|
expose :assignable_labels_endpoint, if: ->(issue) { issue.project } do |issue|
project_labels_path(issue.project, format: :json, include_ancestor_groups: true)
end

View File

@ -72,11 +72,11 @@ class IssueEntity < IssuableEntity
preview_markdown_path(issue.project, target_type: 'Issue', target_id: issue.iid)
end
expose :confidential_issues_docs_path, if: -> (issue) { issue.confidential? } do |issue|
expose :confidential_issues_docs_path, if: ->(issue) { issue.confidential? } do |issue|
help_page_path('user/project/issues/confidential_issues')
end
expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue|
expose :locked_discussion_docs_path, if: ->(issue) { issue.discussion_locked? } do |issue|
help_page_path('user/discussions/index', anchor: 'prevent-comments-by-locking-an-issue')
end
@ -84,7 +84,7 @@ class IssueEntity < IssuableEntity
issue.project.archived?
end
expose :archived_project_docs_path, if: -> (issue) { issue.project.archived? } do |issue|
expose :archived_project_docs_path, if: ->(issue) { issue.project.archived? } do |issue|
help_page_path('user/project/settings/index', anchor: 'archive-a-project')
end

View File

@ -6,7 +6,7 @@ class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
expose :severity
expose :current_user, merge: true do
expose :can_update_escalation_status, if: -> (issue, _) { issue.supports_escalation? } do |issue|
expose :can_update_escalation_status, if: ->(issue, _) { issue.supports_escalation? } do |issue|
can?(current_user, :update_escalation_status, issue.project)
end
end

View File

@ -52,10 +52,10 @@ module BaseServiceUtility
# message - Error message to include in the Hash
# http_status - Optional HTTP status code override (default: nil)
# pass_back - Additional attributes to be included in the resulting Hash
def error(message, http_status = nil, pass_back: {})
def error(message, http_status = nil, status: :error, pass_back: {})
result = {
message: message,
status: :error
status: status
}.reverse_merge(pass_back)
result[:http_status] = http_status if http_status

View File

@ -6,6 +6,7 @@ module Projects
include ValidatesClassificationLabel
ValidationError = Class.new(StandardError)
ApiError = Class.new(StandardError)
def execute
build_topics
@ -40,6 +41,8 @@ module Projects
end
rescue ValidationError => e
error(e.message)
rescue ApiError => e
error(e.message, status: :api_error)
end
def run_auto_devops_pipeline?
@ -63,6 +66,22 @@ module Projects
validate_default_branch_change
validate_renaming_project_with_tags
validate_restrict_user_defined_variables_change
end
def validate_restrict_user_defined_variables_change
return if ::Feature.disabled?(:allow_user_variables_by_minimum_role, project)
return unless changing_restrict_user_defined_variables? || changing_pipeline_variables_minimum_override_role?
if changing_pipeline_variables_minimum_override_role? &&
params[:ci_pipeline_variables_minimum_override_role] == 'owner' &&
!can?(current_user, :owner_access, project)
raise_api_error(s_("UpdateProject|Changing the ci_pipeline_variables_minimum_override_role to the owner role is not allowed"))
end
return if can?(current_user, :change_restrict_user_defined_variables, project)
raise_api_error(s_("UpdateProject|Changing the restrict_user_defined_variables or ci_pipeline_variables_minimum_override_role is not allowed"))
end
def validate_default_branch_change
@ -167,6 +186,10 @@ module Projects
raise ValidationError, message
end
def raise_api_error(message)
raise ApiError, message
end
def update_failed!
model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || s_('UpdateProject|Project could not be updated!')
@ -188,6 +211,20 @@ module Projects
new_branch != project.default_branch
end
def changing_restrict_user_defined_variables?
new_restrict_user_defined_variables = params[:restrict_user_defined_variables]
return false if new_restrict_user_defined_variables.nil?
project.restrict_user_defined_variables != new_restrict_user_defined_variables
end
def changing_pipeline_variables_minimum_override_role?
new_pipeline_variables_minimum_override_role = params[:ci_pipeline_variables_minimum_override_role]
return false if new_pipeline_variables_minimum_override_role.nil?
project.ci_pipeline_variables_minimum_override_role != new_pipeline_variables_minimum_override_role
end
def enabling_wiki?
return false if project.wiki_enabled?

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module RemoteMirrors # rubocop:disable Gitlab/BoundedContexts -- https://gitlab.com/gitlab-org/gitlab/-/issues/462816
class DestroyService < BaseService
def execute(remote_mirror)
return ServiceResponse.error(message: _('Access Denied')) unless allowed?
return ServiceResponse.error(message: _('Remote mirror is missing')) unless remote_mirror
return ServiceResponse.error(message: _('Project mismatch')) unless remote_mirror.project == project
if remote_mirror.destroy
ServiceResponse.success
else
ServiceResponse.error(message: remote_mirror.errors)
end
end
private
def allowed?
Ability.allowed?(current_user, :admin_remote_mirror, project)
end
end
end

View File

@ -9,7 +9,14 @@ module WorkItems
def relate_issuables(work_item)
link = set_parent(issuable, work_item)
link.move_to_end
# It's possible to force the relative_position. This is for example used when importing parent links from
# legacy epics.
if params[:relative_position]
link.relative_position = params[:relative_position]
else
link.move_to_end
end
create_notes_and_resource_event(work_item, link) if link.changed? && link.save
link

View File

@ -12,7 +12,7 @@
- tracking_data = create_mr_tracking_data(can_create_merge_request, can_create_confidential_merge_request?)
- default_create_mr_path = create_mr_path(from: @issue.to_branch_name, source_project: @project, to: default_project.default_branch, mr_params: { issue_iid: @issue.iid })
.create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: default_create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
.create-mr-dropdown-wrap.gl-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: default_create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
.btn-group.unavailable
= render Pajamas::ButtonComponent.new(button_options: { disabled: 'disabled' }) do
= gl_loading_icon(inline: true, css_class: 'js-create-mr-spinner gl-button-icon gl-hidden')

View File

@ -15,24 +15,24 @@
= hidden_merge_request_icon(merge_request)
= link_to merge_request.title, merge_request_path(merge_request), class: 'js-prefetch-document'
- if merge_request.tasks?
%span.task-status.gl-display-inline-block.gl-font-sm
%span.task-status.gl-inline-block.gl-font-sm
&nbsp;
= merge_request.task_status
.issuable-info
%span.issuable-reference.gl-display-inline-block
%span.issuable-reference.gl-inline-block
#{issuable_reference(merge_request)}
%span.issuable-authored.gl-display-inline-block.gl-text-gray-500!
%span.issuable-authored.gl-inline-block.gl-text-gray-500!
&middot;
#{s_('IssueList|created %{timeAgoString} by %{user}').html_safe % { timeAgoString: time_ago_with_tooltip(merge_request.created_at, placement: 'bottom'), user: link_to_member(@project, merge_request.author, avatar: false, extra_class: 'gl-text-gray-500!') }}
- if merge_request.milestone
%span.issuable-milestone.gl-display-inline-block.gl-text-truncate.gl-max-w-26.gl-align-bottom
%span.issuable-milestone.gl-inline-block.gl-text-truncate.gl-max-w-26.gl-align-bottom
&nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), class: 'gl-text-gray-500!', data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= sprite_icon('milestone', size: 12, css_class: 'gl-vertical-align-text-bottom')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
%span.project-ref-path.has-tooltip.d-inline-block.gl-text-truncate.gl-max-w-26.gl-align-bottom{ title: _('Target branch: %{target_branch}') % {target_branch: merge_request.target_branch} }
%span.project-ref-path.has-tooltip.gl-inline-block.gl-text-truncate.gl-max-w-26.gl-align-bottom{ title: _('Target branch: %{target_branch}') % {target_branch: merge_request.target_branch} }
&nbsp;
= link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name gl-text-gray-500!' do
= sprite_icon('branch', size: 12, css_class: 'fork-sprite')

View File

@ -10,7 +10,7 @@
&middot;
= sprintf(s_('created %{issuable_created} by %{author}'), { issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
- if (target_branch = issuable_visible_target_branch(issuable))
%span.project-ref-path.has-tooltip.d-inline-block.gl-text-truncate.gl-max-w-26.gl-align-bottom{ title: _('Target branch: %{target_branch}') % {target_branch: target_branch} }
%span.project-ref-path.has-tooltip.gl-inline-block.gl-text-truncate.gl-max-w-26.gl-align-bottom{ title: _('Target branch: %{target_branch}') % {target_branch: target_branch} }
&nbsp;
= link_to project_ref_path(issuable.project, target_branch), class: 'ref-name gl-text-secondary!' do
= sprite_icon('branch', size: 12, css_class: 'fork-sprite')

View File

@ -0,0 +1,6 @@
- form = local_assigns.fetch(:form)
- private_profile_help_link = link_to _("Learn more"), help_page_path('user/profile/index', anchor: 'make-your-user-profile-page-private')
- private_profile_label = safe_format(s_("Profiles|Don't display activity-related personal information on your profile. %{private_profile_help_link_start}Learn more%{private_profile_help_link_end}."), tag_pair(private_profile_help_link, :private_profile_help_link_start, :private_profile_help_link_end))
= form.gitlab_ui_checkbox_component :private_profile, private_profile_label

View File

@ -158,9 +158,7 @@
%fieldset.form-group.gl-form-group
%legend.col-form-label
= _('Private profile')
- private_profile_help_link = link_to _("Learn more"), help_page_path('user/profile/index', anchor: 'make-your-user-profile-page-private')
- private_profile_label = safe_format(s_("Profiles|Don't display activity-related personal information on your profile. %{private_profile_help_link_start}Learn more%{private_profile_help_link_end}."), tag_pair(private_profile_help_link, :private_profile_help_link_start, :private_profile_help_link_end))
= f.gitlab_ui_checkbox_component :private_profile, private_profile_label
= render_if_exists 'user_settings/profiles/private_profile', form: f, user: @user
%fieldset.form-group.gl-form-group
%legend.col-form-label
= s_("Profiles|Private contributions")

View File

@ -0,0 +1,8 @@
---
name: allow_user_variables_by_minimum_role
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149343
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/456284
milestone: '17.1'
type: development
group: group::pipeline security
default_enabled: false

View File

@ -0,0 +1,9 @@
---
name: use_remote_mirror_destroy_service
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455518
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/153845
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/463022
milestone: '17.1'
group: group::source code
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,9 @@
---
migration_job_name: BackfillEpicIssuesIntoWorkItemParentLinks
description: Creates records in the work_item_parent_links table for each record in the epic_issues table
feature_category: team_planning
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147509
milestone: '17.1'
queued_migration_version: 20240522183910
finalize_after: '2024-06-20'
finalized_by:

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddCiPipelineVariablesMinimumRoleEnum < Gitlab::Database::Migration[2.2]
milestone '17.1'
def change
add_column :project_ci_cd_settings, :pipeline_variables_minimum_override_role,
:integer, default: ProjectCiCdSetting::MAINTAINER_ROLE, null: false, limit: 2
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class DeleteInvalidPathLocksRecords < Gitlab::Database::Migration[2.2]
milestone '17.1'
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
BATCH_SIZE = 1000
def up
return if Gitlab.com?
relation = define_batchable_model('path_locks').where(project_id: nil)
loop do
batch = relation.limit(BATCH_SIZE)
delete_count = relation.where(id: batch.select(:id)).delete_all
break if delete_count == 0
end
end
def down
# no-op
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddNotNullConstraintToPathLocks < Gitlab::Database::Migration[2.2]
milestone '17.1'
disable_ddl_transaction!
def up
add_not_null_constraint :path_locks, :project_id
end
def down
remove_not_null_constraint :path_locks, :project_id
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class QueueBackfillEpicIssuesIntoWorkItemParentLinks < Gitlab::Database::Migration[2.2]
milestone '17.1'
restrict_gitlab_migration gitlab_schema: :gitlab_main
MIGRATION = "BackfillEpicIssuesIntoWorkItemParentLinks"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 10_000
SUB_BATCH_SIZE = 100
# not passing any group id, means we'd backfill everything. We still have the option to pass in a group id if we
# need to reschedule the backfilling for a single group
GROUP_ID = nil
def up
queue_batched_background_migration(
MIGRATION,
:epic_issues,
:id,
GROUP_ID,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :epic_issues, :id, [GROUP_ID])
end
end

View File

@ -0,0 +1 @@
24d4e0a054fa3c52b1ce41c7a28cb759e711d1b7860caf934ddb93c7540ee8c5

View File

@ -0,0 +1 @@
176ca5cc2642c0873366e46848a8040845cfbacb0080ea697a4dba82267d0ea0

View File

@ -0,0 +1 @@
cd33ac1a0bf63f127323f3e84913ddb9315aff5fc828d6bd5999d52857f1647e

View File

@ -0,0 +1 @@
d3f5bc2c43d871d32e6726e4b3f001a08a4de338dc5c17b7d1b0cf2d6747ee5a

View File

@ -13708,7 +13708,8 @@ CREATE TABLE path_locks (
project_id integer,
user_id integer,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
updated_at timestamp without time zone NOT NULL,
CONSTRAINT check_e1de2eb0f1 CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE path_locks_id_seq
@ -14507,7 +14508,8 @@ CREATE TABLE project_ci_cd_settings (
inbound_job_token_scope_enabled boolean DEFAULT true NOT NULL,
forward_deployment_rollback_allowed boolean DEFAULT true NOT NULL,
merge_trains_skip_train_allowed boolean DEFAULT false NOT NULL,
restrict_pipeline_cancellation_role smallint DEFAULT 0 NOT NULL
restrict_pipeline_cancellation_role smallint DEFAULT 0 NOT NULL,
pipeline_variables_minimum_override_role smallint DEFAULT 3 NOT NULL
);
CREATE SEQUENCE project_ci_cd_settings_id_seq

View File

@ -21,7 +21,7 @@ If the GitLab instance uses Admin Mode, you must [enable Admin Mode for your ses
the **Admin Area** button is visible.
NOTE:
Only administrators can access the Admin Area.
Only administrators on GitLab self-managed can access the Admin Area. On GitLab.com the Admin Area feature is not available.
## Administering organizations

View File

@ -17409,7 +17409,7 @@ Represents the total number of issues and their weights for a particular day.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cicatalogresourcedescription"></a>`description` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 15.11. **Status**: Experiment. Description of the catalog resource. |
| <a id="cicatalogresourcefullpath"></a>`fullPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Full project path of the catalog resource. |
| <a id="cicatalogresourcefullpath"></a>`fullPath` **{warning-solid}** | [`ID`](#id) | **Introduced** in GitLab 16.11. **Status**: Experiment. Full project path of the catalog resource. |
| <a id="cicatalogresourceicon"></a>`icon` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 15.11. **Status**: Experiment. Icon for the catalog resource. |
| <a id="cicatalogresourceid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in GitLab 15.11. **Status**: Experiment. ID of the catalog resource. |
| <a id="cicatalogresourcelast30dayusagecount"></a>`last30DayUsageCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in GitLab 17.0. **Status**: Experiment. Number of projects that used a component from this catalog resource in a pipeline, by using `include:component`, in the last 30 days. |
@ -19708,7 +19708,7 @@ A single design.
| <a id="designdiscussions"></a>`discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) |
| <a id="designevent"></a>`event` | [`DesignVersionEvent!`](#designversionevent) | How this design was changed in the current version. |
| <a id="designfilename"></a>`filename` | [`String!`](#string) | Filename of the design. |
| <a id="designfullpath"></a>`fullPath` | [`String!`](#string) | Full path to the design file. |
| <a id="designfullpath"></a>`fullPath` | [`ID!`](#id) | Full path to the design file. |
| <a id="designid"></a>`id` | [`ID!`](#id) | ID of this design. |
| <a id="designimage"></a>`image` | [`String!`](#string) | URL of the full-sized image. |
| <a id="designimagev432x230"></a>`imageV432x230` | [`String`](#string) | The URL of the design resized to fit within the bounds of 432x230. This will be `null` if the image has not been generated. |
@ -19782,7 +19782,7 @@ A design pinned to a specific version. The image field reflects the design as of
| <a id="designatversiondiffrefs"></a>`diffRefs` | [`DiffRefs!`](#diffrefs) | Diff refs for this design. |
| <a id="designatversionevent"></a>`event` | [`DesignVersionEvent!`](#designversionevent) | How this design was changed in the current version. |
| <a id="designatversionfilename"></a>`filename` | [`String!`](#string) | Filename of the design. |
| <a id="designatversionfullpath"></a>`fullPath` | [`String!`](#string) | Full path to the design file. |
| <a id="designatversionfullpath"></a>`fullPath` | [`ID!`](#id) | Full path to the design file. |
| <a id="designatversionid"></a>`id` | [`ID!`](#id) | ID of this design. |
| <a id="designatversionimage"></a>`image` | [`String!`](#string) | URL of the full-sized image. |
| <a id="designatversionimagev432x230"></a>`imageV432x230` | [`String`](#string) | The URL of the design resized to fit within the bounds of 432x230. This will be `null` if the image has not been generated. |
@ -37048,7 +37048,7 @@ Implementations:
| <a id="designfieldsdiffrefs"></a>`diffRefs` | [`DiffRefs!`](#diffrefs) | Diff refs for this design. |
| <a id="designfieldsevent"></a>`event` | [`DesignVersionEvent!`](#designversionevent) | How this design was changed in the current version. |
| <a id="designfieldsfilename"></a>`filename` | [`String!`](#string) | Filename of the design. |
| <a id="designfieldsfullpath"></a>`fullPath` | [`String!`](#string) | Full path to the design file. |
| <a id="designfieldsfullpath"></a>`fullPath` | [`ID!`](#id) | Full path to the design file. |
| <a id="designfieldsid"></a>`id` | [`ID!`](#id) | ID of this design. |
| <a id="designfieldsimage"></a>`image` | [`String!`](#string) | URL of the full-sized image. |
| <a id="designfieldsimagev432x230"></a>`imageV432x230` | [`String`](#string) | The URL of the design resized to fit within the bounds of 432x230. This will be `null` if the image has not been generated. |

View File

@ -40826,6 +40826,8 @@ definitions:
type: boolean
restrict_user_defined_variables:
type: boolean
ci_pipeline_variables_minimum_override_role:
type: string
runners_token:
type: string
example: b8547b1dc37721d05889db52fa2f02
@ -52502,6 +52504,8 @@ definitions:
type: boolean
restrict_user_defined_variables:
type: boolean
ci_pipeline_variables_minimum_override_role:
type: string
runners_token:
type: string
example: b8547b1dc37721d05889db52fa2f02
@ -53075,7 +53079,16 @@ definitions:
description: Enable or disable separated caches based on branch protection.
restrict_user_defined_variables:
type: boolean
description: Restrict use of user-defined variables when triggering a pipeline
description: Restrict ability to override variables when triggering a pipeline
ci_pipeline_variables_minimum_override_role:
type: string
description: Limit ability to override CI/CD variables when triggering a pipeline
to only users with at least the set minimum role
enum:
- no_one_allowed
- developer
- maintainer
- owner
allow_pipeline_trigger_approve_deployment:
type: boolean
description: Allow pipeline triggerer to approve deployments

View File

@ -749,7 +749,7 @@ You should avoid overriding predefined variables in most cases, as it can cause
### Restrict who can override variables
You can limit the ability to override variables to only users with the Maintainer role.
You can limit the ability to override variables to only users with at least the Maintainer role.
When other users try to run a pipeline with overridden variables, they receive the
`Insufficient permissions to set pipeline variables` error message.
@ -759,6 +759,28 @@ to enable the `restrict_user_defined_variables` setting. The setting is `disable
If you [store your CI/CD configurations in a different repository](../../ci/pipelines/settings.md#specify-a-custom-cicd-configuration-file),
use this setting for control over the environment the pipeline runs in.
#### By minimum role
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/440338) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `allow_user_variables_by_minimum_role`. Disabled by default.
When the `restrict_user_defined_variables` option is enabled, you can specify which
[roles](../../user/permissions.md#roles) can override variables with the
`ci_pipeline_variables_minimum_override_role` setting.
To change the setting, use [the projects API](../../api/projects.md#edit-project)
to modify `ci_pipeline_variables_minimum_override_role` to one of:
- `owner`: Only users with the Owner role can override variables. You must have the Owner
role in the project to change the setting to this value.
- `maintainer`: Only users with at least the Maintainer role can override variables.
Default when not specified.
- `developer`: Only users with at least the Developer role can override variables.
- `no_one_allowed`: Users cannot override variables.
If you set the minimum role to `owner`, only users with at least the `owner` role
can update the `ci_pipeline_variables_minimum_override_role` and `restrict_user_defined_variables`
settings.
## Exporting variables
Scripts executed in separate shell contexts do not share exports, aliases,

View File

@ -0,0 +1,35 @@
---
stage: SaaS Platforms
group: GitLab Dedicated
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# Enabling features for GitLab Dedicated
## Versioning
GitLab Dedicated is running the n-1 GitLab version to provide sufficient run-up time to make changes across many GitLab instances, and reduce the number of releases necessary to maintain GitLab in accordance with the security maintenance policy.
GitLab Dedicated instances are automatically upgraded during scheduled maintenance windows throughout the week.
The [release rollout schedule](../administration/dedicated/create_instance.md#gitlab-release-rollout-schedule) for GitLab Dedicated outlines when instances are expected to be upgraded to a new release.
## Feature flags
[Feature flags support the development and rollout of new or experimental features](https://handbook.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags) on GitLab.com. Feature flags are not tools for managing configuration.
Due to the high risk of enabling experimental features on GitLab Dedicated, and the additional workload needed to manage these on a per-instance basis, feature flags are not supported on GitLab Dedicated.
Instead, all per-instance configurations must be made using the application (UI or API) settings to allow customers to control them.
## Enabling features
All features need to be Generally Available before they can be deployed to GitLab Dedicated. In most cases, this means any feature flags are defaulted to on, and the feature is being used on GitLab.com and by Self-Managed users.
New versions of GitLab and any other changes, are deployed using automation during scheduled maintenance windows. Because of the required automation and the timing of deployments, features must be safe for auto-rollout. This means that new features don't require any immediate manual adjustment from operators or customers.
Features that require additional configuration after they have been deployed, must have API or UI settings to allow the customer to make the necessary changes.
GitLab Dedicated is a single-tenant SaaS product. This means that one-off, customer-specific tasks cannot be supported.
Features that may not be suitable or useful for every customer must be controlled using application settings to avoid creating unsustainable workloads.

View File

@ -2,6 +2,7 @@ PATH
remote: .
specs:
gitlab-cng (0.0.1)
activesupport (>= 7)
rainbow (~> 3.1)
require_all (~> 3.0)
thor (~> 1.3)

View File

@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
spec.executables = "cng"
spec.require_paths = ["lib"]
spec.add_dependency "activesupport", ">= 7"
spec.add_dependency "rainbow", "~> 3.1"
spec.add_dependency "require_all", "~> 3.0"
spec.add_dependency "thor", "~> 1.3"

View File

@ -6,6 +6,9 @@ module Gitlab
# Create command composed of subcommands that create various resources needed for CNG deployment
#
class Create < Command
# @return [Array] configurations that are used for kind cluster deployments
KIND_CLUSTER_CONFIGURATIONS = %w[kind].freeze
desc "cluster", "Create kind cluster for local deployments"
option :name,
desc: "Cluster name",
@ -29,6 +32,8 @@ module Gitlab
long_desc <<~LONGDESC
This command installs a GitLab chart archive and performs all additional pre-install and post-install setup.
Argument NAME is helm install name and defaults to "gitlab".
Deployment has several optional environment variables it can read before performing chart install:
QA_EE_LICENSE|EE_LICENSE - gitlab test license, if present, will be added to deployment,
LONGDESC
option :configuration,
desc: "Deployment configuration",
@ -49,8 +54,21 @@ module Gitlab
desc: "Use CI specific configuration",
default: false,
type: :boolean
option :gitlab_domain,
desc: "Domain for deployed app. Defaults to (your host IP).nip.io",
type: :string
option :with_cluster,
desc: "Create kind cluster for local deployments. \
Only valid for configurations designed to run against local kind cluster",
type: :boolean
def deployment(name = "gitlab")
Deployment::Installation.new(name, **symbolized_options).create
if options[:with_cluster] && KIND_CLUSTER_CONFIGURATIONS.include?(options[:configuration])
invoke :cluster, [], ci: options[:ci]
end
Deployment::Installation
.new(name, **symbolized_options.slice(:configuration, :namespace, :set, :ci, :gitlab_domain))
.create
end
end
end

View File

@ -20,9 +20,11 @@ module Gitlab
class Base
include Helpers::Output
def initialize(namespace, kubeclient)
def initialize(namespace, kubeclient, ci, gitlab_domain)
@namespace = namespace
@kubeclient = kubeclient
@ci = ci
@gitlab_domain = gitlab_domain
end
class << self
@ -41,7 +43,7 @@ module Gitlab
#
# @return [void]
def skip_post_deployment_setup!
@skip_pre_deployment_setup = true
@skip_post_deployment_setup = true
end
end
@ -51,7 +53,7 @@ module Gitlab
def run_pre_deployment_setup
return if self.class.skip_pre_deployment_setup
raise(NoMethodError, 'run_pre_deployment_setup not implemented')
raise(NoMethodError, "run_pre_deployment_setup not implemented")
end
# Steps to be executed after helm deployment has been performed
@ -60,19 +62,26 @@ module Gitlab
def run_post_deployment_setup
return if self.class.skip_post_deployment_setup
raise(NoMethodError, 'run_post_deployment_setup not implemented')
raise(NoMethodError, "run_post_deployment_setup not implemented")
end
# Values hash containing the values to be passed to helm chart install
#
# @return [Hash]
def values
raise(NoMethodError, 'values not implemented')
{}
end
# Deployed app url
#
# @return [String]
def gitlab_url
"http://gitlab.#{gitlab_domain}"
end
private
attr_reader :namespace, :kubeclient
attr_reader :namespace, :kubeclient, :ci, :gitlab_domain
end
end
end

View File

@ -7,41 +7,118 @@ module Gitlab
# Configuration for performing deployment setup on local kind cluster
#
class Kind < Base
# @return [String] secret name for initial admin password
ADMIN_PASSWORD_SECRET = "gitlab-initial-root-password"
# @return [String] configmap name for pre-receive hook
PRE_RECEIVE_HOOK_CONFIGMAP_NAME = "pre-receive-hook"
# @return [String] pre-receive hook script used by e2e tests
PRE_RECEIVE_HOOK = <<~'SH'
#!/usr/bin/env bash
skip_post_deployment_setup!
if [[ $GL_PROJECT_PATH =~ 'reject-prereceive' ]]; then
echo 'GL-HOOK-ERR: Custom error message rejecting prereceive hook for projects with GL_PROJECT_PATH matching pattern reject-prereceive'
exit 1
fi
SH
# Run pre-deployment setup
#
# @return [void]
def run_pre_deployment_setup
create_initial_root_password
create_pre_receive_hook
end
private
# Run post-deployment setup
#
# @return [void]
def run_post_deployment_setup
create_root_token
end
# Pre-receive hook script used by e2e tests to test global git hooks
# Helm chart values specific to kind deployment
#
# @return [Hash]
def values
{
global: {
initialRootPassword: {
secret: ADMIN_PASSWORD_SECRET
},
gitaly: {
hooks: {
preReceive: {
configmap: PRE_RECEIVE_HOOK_CONFIGMAP_NAME
}
}
}
},
"nginx-ingress": {
controller: {
replicaCount: 1,
minAavailable: 1,
service: {
type: "NodePort",
nodePorts: {
"gitlab-shell": 32022,
http: 32080
}
}
}
}
}
end
# Gitlab url
#
# @return [String]
def pre_receive_hook
<<~SH
#!/usr/bin/env bash
def gitlab_url
"http://gitlab.#{gitlab_domain}#{ci ? '' : ':32080'}"
end
if [[ $GL_PROJECT_PATH =~ 'reject-prereceive' ]]; then
echo 'GL-HOOK-ERR: Custom error message rejecting prereceive hook for projects with GL_PROJECT_PATH matching pattern reject-prereceive'
exit 1
fi
SH
private
# Gitlab initial admin password, defaults to commonly used password across development environments
#
# @return [String]
def admin_password
@admin_password ||= ENV["GITLAB_ADMIN_PASSWORD"] || "5iveL!fe"
end
# Gitlab admin user personal access token, defaults to value used in development seed data
#
# @return [String]
def admin_token
@admin_token ||= ENV["GITLAB_ADMIN_ACCESS_TOKEN"] || "ypCa3Dzb23o5nvsixwPA"
end
# Token seed script for root user
#
# @return [String]
def admin_pat_seed
<<~RUBY
Gitlab::Seeder.quiet do
User.find_by(username: 'root').tap do |user|
params = {
scopes: Gitlab::Auth.all_available_scopes.map(&:to_s),
name: 'seeded-api-token'
}
user.personal_access_tokens.build(params).tap do |pat|
pat.expires_at = 365.days.from_now
pat.set_token("#{admin_token}")
pat.save!
end
end
end
RUBY
end
# Create initial root password
#
# @return [void]
def create_initial_root_password
admin_password = ENV["GITLAB_ADMIN_PASSWORD"]
log("Creating initial root password secret", :info)
return log("`GITLAB_ADMIN_PASSWORD` variable is not set, skipping", :warn) unless admin_password
log("Creating admin user initial password secret", :info)
secret = Kubectl::Resources::Secret.new(ADMIN_PASSWORD_SECRET, "password", admin_password)
puts mask_secrets(kubeclient.create_resource(secret), [admin_password, Base64.encode64(admin_password)])
end
@ -51,9 +128,20 @@ module Gitlab
# @return [void]
def create_pre_receive_hook
log("Creating pre-receive hook", :info)
configmap = Kubectl::Resources::Configmap.new(PRE_RECEIVE_HOOK_CONFIGMAP_NAME, "hook.sh", pre_receive_hook)
configmap = Kubectl::Resources::Configmap.new(PRE_RECEIVE_HOOK_CONFIGMAP_NAME, "hook.sh", PRE_RECEIVE_HOOK)
puts kubeclient.create_resource(configmap)
end
# Create admin user personal access token
#
# @return [void]
def create_root_token
log("Creating admin user personal access token", :info)
puts mask_secrets(
kubeclient.execute("toolbox", ["gitlab-rails", "runner", admin_pat_seed], container: "toolbox"),
[admin_token]
).strip
end
end
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
module Gitlab
module Cng
module Deployment
# Helpers for common chart values
#
class DefaultValues
extend Helpers::CI
IMAGE_REPOSITORY = "registry.gitlab.com/gitlab-org/build/cng-mirror"
class << self
# Main common chart values
#
# @param [String] domain
# @return [Hash]
def common_values(domain)
{
global: {
hosts: {
domain: domain,
https: false
},
ingress: {
configureCertmanager: false,
tls: {
enabled: false
}
},
appConfig: {
applicationSettingsCacheSeconds: 0
}
},
gitlab: { "gitlab-exporter": { enabled: false } },
redis: { metrics: { enabled: false } },
prometheus: { install: false },
certmanager: { install: false },
"gitlab-runner": { install: false }
}
end
# Key value pairs for ci specific component version values
#
# This is defined as key value pairs to allow constructing example cli args for easier reproducability
#
# @return [Hash]
def component_ci_versions
{
"gitaly.image.repository" => "#{IMAGE_REPOSITORY}/gitaly",
"gitaly.image.tag" => gitaly_version,
"gitlab-shell.image.repository" => "#{IMAGE_REPOSITORY}/gitlab-shell",
"gitlab-shell.image.tag" => "v#{gitlab_shell_version}",
"migrations.image.repository" => "#{IMAGE_REPOSITORY}/gitlab-toolbox-ee",
"migrations.image.tag" => commit_sha,
"toolbox.image.repository" => "#{IMAGE_REPOSITORY}/gitlab-toolbox-ee",
"toolbox.image.tag" => commit_sha,
"sidekiq.annotations.commit" => commit_short_sha,
"sidekiq.image.repository" => "#{IMAGE_REPOSITORY}/gitlab-sidekiq-ee",
"sidekiq.image.tag" => commit_sha,
"webservice.annotations.commit" => commit_short_sha,
"webservice.image.repository" => "#{IMAGE_REPOSITORY}/gitlab-webservice-ee",
"webservice.image.tag" => commit_sha,
"webservice.workhorse.image" => "#{IMAGE_REPOSITORY}/gitlab-workhorse-ee",
"webservice.workhorse.tag" => commit_sha
}
end
end
end
end
end
end

View File

@ -1,21 +1,27 @@
# frozen_string_literal: true
require "socket"
require "yaml"
require "active_support/core_ext/hash"
module Gitlab
module Cng
module Deployment
# Class handling all the pre and post deployment setup steps and gitlab helm chart installation
#
class Installation
include Helpers::Output
include Helpers::Shell
LICENSE_SECRET = "gitlab-license"
def initialize(name, configuration:, namespace:, ci:, set: [])
def initialize(name, configuration:, namespace:, ci:, gitlab_domain: nil, set: [])
@name = name
@configuration = configuration
@namespace = namespace
@ci = ci
@gitlab_domain = gitlab_domain
@set = set
@kubeclient = Kubectl::Client.new(namespace)
end
# Perform deployment with all the additional setup
@ -24,20 +30,69 @@ module Gitlab
def create
log("Creating CNG deployment '#{name}' using '#{configuration}' configuration", :info, bright: true)
run_pre_deploy_setup
run_deploy
run_post_deploy_setup
rescue Helpers::Shell::CommandFailure
exit(1)
end
private
attr_reader :name, :configuration, :namespace, :ci, :set, :kubeclient
attr_reader :name, :configuration, :namespace, :ci, :set
alias_method :cli_values, :set
# Kubectl client instance
#
# @return [Kubectl::Client]
def kubeclient
@kubeclient ||= Kubectl::Client.new(namespace)
end
# Gitlab app domain
#
# @return [String]
def gitlab_domain
@gitlab_domain ||= "#{Socket.ip_address_list.detect(&:ipv4_private?).ip_address}.nip.io"
end
# Configuration class instance
#
# @return [Configuration::Base]
def config_instance
@config_instance ||= Configurations.const_get(configuration.capitalize, false).new(namespace, kubeclient)
@config_instance ||= Configurations.const_get(configuration.capitalize, false).new(
namespace,
kubeclient,
ci,
gitlab_domain
)
end
# Gitlab license
#
# @return [String]
def license
@license ||= ENV["QA_EE_LICENSE"] || ENV["EE_LICENSE"]
end
# Helm values for license secret
#
# @return [Hash]
def license_values
return {} unless license
{
global: {
extraEnv: {
GITLAB_LICENSE_MODE: "test",
CUSTOMER_PORTAL_URL: "https://customers.staging.gitlab.com"
}
},
gitlab: {
license: {
secret: LICENSE_SECRET
}
}
}
end
# Execute pre-deployment setup
@ -54,13 +109,34 @@ module Gitlab
end
end
# Run helm deployment
#
# @return [void]
def run_deploy
cmd = [
"upgrade",
"--install", name, "gitlab/gitlab",
"--namespace", namespace,
"--timeout", "5m",
"--wait"
]
cmd.push(*DefaultValues.component_ci_versions.flat_map { |k, v| ["--set", "gitlab.#{k}=#{v}"] }) if ci
cmd.push(*cli_values.flat_map { |v| ["--set", v] })
cmd.push("--values", "-")
values = DefaultValues.common_values(gitlab_domain)
.deep_merge(license_values)
.deep_merge(config_instance.values)
.deep_stringify_keys
Helpers::Spinner.spin("running helm deployment") { puts run_helm_cmd(cmd, values.to_yaml) }
log("Deployment successfull and app is available via: #{config_instance.gitlab_url}", :success, bright: true)
end
# Execute post-deployment setup
#
# @return [void]
def run_post_deploy_setup
Helpers::Spinner.spin("running post-deployment setup") do
config_instance.run_pre_deployment_setup
end
Helpers::Spinner.spin("running post-deployment setup") { config_instance.run_post_deployment_setup }
end
# Add helm chart repo
@ -99,12 +175,10 @@ module Gitlab
#
# @return [void]
def create_license
license = ENV["QA_EE_LICENSE"]
log("Creating gitlab license secret", :info)
return log("`QA_EE_LICENSE` variable is not set, skipping", :warn) unless license
return log("`QA_EE_LICENSE|EE_LICENSE` variable is not set, skipping", :warn) unless license
secret = Kubectl::Resources::Secret.new(LICENSE_SECRET, "license", ENV["QA_EE_LICENSE"])
secret = Kubectl::Resources::Secret.new(LICENSE_SECRET, "license", license)
puts mask_secrets(kubeclient.create_resource(secret), [license, Base64.encode64(license)])
end
@ -112,8 +186,8 @@ module Gitlab
#
# @param [Array] cmd
# @return [String]
def run_helm_cmd(cmd)
execute_shell(["helm", *cmd])
def run_helm_cmd(cmd, stdin = nil)
execute_shell(["helm", *cmd], stdin_data: stdin)
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Gitlab
module Cng
module Helpers
# Helper functions for fetching CI related information
#
module CI
extend self
def commit_sha
@commit_sha ||= ENV["CI_COMMIT_SHA"] || raise("CI_COMMIT_SHA is not set")
end
def commit_short_sha
@commit_short_sha ||= ENV["CI_COMMIT_SHORT_SHA"] || raise("CI_COMMIT_SHORT_SHA is not set")
end
def gitaly_version
@gitaly_version ||= File.read(File.join(ci_project_dir, "GITALY_SERVER_VERSION")).strip
end
def gitlab_shell_version
@gitlab_shell_version ||= File.read(File.join(ci_project_dir, "GITLAB_SHELL_VERSION")).strip
end
def ci_project_dir
@ci_project_dir ||= ENV["CI_PROJECT_DIR"] || raise("CI_PROJECT_DIR is not set")
end
end
end
end
end

View File

@ -21,12 +21,12 @@ module Gitlab
end
def create
log "Creating cluster '#{name}'", :info
return log " cluster '#{name}' already exists, skipping!" if cluster_exists?
log("Creating cluster '#{name}'", :info, bright: true)
return log(" cluster '#{name}' already exists, skipping!", :warn) if cluster_exists?
create_cluster
update_server_url
log "Cluster '#{name}' created", :success
log("Cluster '#{name}' created", :success)
rescue Helpers::Shell::CommandFailure
# Exit cleanly without stacktrace if shell command fails
exit(1)

View File

@ -8,6 +8,9 @@ module Gitlab
class Client
include Helpers::Shell
# Error raised by kubectl client class
Error = Class.new(StandardError)
def initialize(namespace)
@namespace = namespace
end
@ -24,12 +27,48 @@ module Gitlab
# @param [Resources::Base] resource
# @return [String] command output
def create_resource(resource)
execute_shell(["kubectl", "apply", "-n", namespace, "-f", "-"], stdin_data: resource.json)
run_in_namespace("apply", args: ["-f", "-"], stdin_data: resource.json)
end
# Execute command in a pod
#
# @param [String] pod full or part of pod name
# @param [Array] command
# @param [String] container
# @return [String]
def execute(pod, command, container: nil)
args = ["--", *command]
args.unshift("-c", container) if container
run_in_namespace("exec", get_pod_name(pod), args: args)
end
private
attr_reader :namespace
# Get full pod name
#
# @param [String] name
# @return [String]
def get_pod_name(name)
pod = run_in_namespace("get", "pods", args: ["--output", "jsonpath={.items[*].metadata.name}"])
.split(" ")
.find { |pod| pod.include?(name) }
raise Error, "Pod '#{name}' not found" unless pod
pod
end
# Run kubectl command in namespace
#
# @param [Array] *action
# @param [Array] args
# @param [String] stdin_data
# @return [String]
def run_in_namespace(*action, args:, stdin_data: nil)
execute_shell(["kubectl", *action, "-n", namespace, *args], stdin_data: stdin_data)
end
end
end
end

View File

@ -0,0 +1 @@
7aa06a578d76bdc294ee8e9acb4f063e7d9f1d5f

View File

@ -0,0 +1 @@
14.35.0

View File

@ -3,13 +3,14 @@
RSpec.describe Gitlab::Cng::Commands::Create do
include_context "with command testing helper"
let(:kind_cluster) { instance_double(Gitlab::Cng::Kind::Cluster, create: nil) }
before do
allow(Gitlab::Cng::Kind::Cluster).to receive(:new).and_return(kind_cluster)
end
describe "cluster command" do
let(:command_name) { "cluster" }
let(:kind_cluster) { instance_double(Gitlab::Cng::Kind::Cluster, create: nil) }
before do
allow(Gitlab::Cng::Kind::Cluster).to receive(:new).and_return(kind_cluster)
end
it "defines cluster command" do
expect_command_to_include_attributes(command_name, {
@ -38,7 +39,7 @@ RSpec.describe Gitlab::Cng::Commands::Create do
allow(Gitlab::Cng::Deployment::Installation).to receive(:new).and_return(deployment_install)
end
it "defines cluster command" do
it "defines deployment command" do
expect_command_to_include_attributes(command_name, {
description: "Create CNG deployment from official GitLab Helm chart",
name: command_name,
@ -47,15 +48,36 @@ RSpec.describe Gitlab::Cng::Commands::Create do
end
it "invokes kind cluster creation with correct arguments" do
invoke_command(command_name, [], { configuration: "kind", ci: true, namespace: "gitlab" })
invoke_command(command_name, [], {
configuration: "kind",
ci: true,
namespace: "gitlab",
gitlab_domain: "127.0.0.1.nip.io"
})
expect(deployment_install).to have_received(:create)
expect(Gitlab::Cng::Deployment::Installation).to have_received(:new).with(
"gitlab",
configuration: "kind",
ci: true,
namespace: "gitlab"
namespace: "gitlab",
gitlab_domain: "127.0.0.1.nip.io"
)
end
it "invokes kind cluster creation when --with-cluster argument is passed" do
invoke_command(command_name, [], {
configuration: "kind",
ci: true,
with_cluster: true
})
expect(kind_cluster).to have_received(:create)
expect(Gitlab::Cng::Kind::Cluster).to have_received(:new).with({
ci: true,
name: "gitlab"
})
expect(deployment_install).to have_received(:create)
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Cng::Deployment::Configurations::Base do
subject(:configuration) { Class.new(described_class) }
let(:config) do
configuration.new("gitlab", Gitlab::Cng::Kubectl::Client.new("gitlab"), false, "domain")
end
it "returns empty values by default" do
expect(config.values).to eq({})
end
it "returns correct default gitlab_url" do
expect(config.gitlab_url).to eq("http://gitlab.domain")
end
it "has setup hooks enabled by default", :aggregate_failures do
expect { config.run_pre_deployment_setup }.to raise_error(
NoMethodError,
"run_pre_deployment_setup not implemented"
)
expect { config.run_post_deployment_setup }.to raise_error(
NoMethodError,
"run_post_deployment_setup not implemented"
)
end
context "with disabled setup hooks" do
subject(:configuration) do
Class.new(described_class) do
skip_pre_deployment_setup!
skip_post_deployment_setup!
end
end
it "does not run setup hooks" do
expect(config.run_pre_deployment_setup).to be_nil
expect(config.run_post_deployment_setup).to be_nil
end
end
end

View File

@ -0,0 +1,102 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Cng::Deployment::Configurations::Kind do
subject(:configuration) { described_class.new("gitlab", kubeclient, true, "127.0.0.1.nip.io") }
let(:kubeclient) { instance_double(Gitlab::Cng::Kubectl::Client, create_resource: "", execute: "") }
let(:env) do
{
"GITLAB_ADMIN_PASSWORD" => "password",
"GITLAB_ADMIN_ACCESS_TOKEN" => "token"
}
end
around do |example|
ClimateControl.modify(env) { example.run }
end
it "runs pre-deployment setup", :aggregate_failures do
expect { configuration.run_pre_deployment_setup }.to output(/Creating admin user initial password secret/).to_stdout
expect(kubeclient).to have_received(:create_resource).with(
Gitlab::Cng::Kubectl::Resources::Secret.new("gitlab-initial-root-password", "password", "password")
)
expect(kubeclient).to have_received(:create_resource).with(
Gitlab::Cng::Kubectl::Resources::Configmap.new(
"pre-receive-hook",
"hook.sh",
<<~SH
#!/usr/bin/env bash
if [[ $GL_PROJECT_PATH =~ 'reject-prereceive' ]]; then
echo 'GL-HOOK-ERR: Custom error message rejecting prereceive hook for projects with GL_PROJECT_PATH matching pattern reject-prereceive'
exit 1
fi
SH
))
end
it "runs post-deployment setup", :aggregate_failures do
expect { configuration.run_post_deployment_setup }.to output(/Creating admin user personal access token/).to_stdout
expect(kubeclient).to have_received(:execute).with(
"toolbox",
[
"gitlab-rails",
"runner",
<<~RUBY
Gitlab::Seeder.quiet do
User.find_by(username: 'root').tap do |user|
params = {
scopes: Gitlab::Auth.all_available_scopes.map(&:to_s),
name: 'seeded-api-token'
}
user.personal_access_tokens.build(params).tap do |pat|
pat.expires_at = 365.days.from_now
pat.set_token("token")
pat.save!
end
end
end
RUBY
],
container: "toolbox"
)
end
it "returns configuration specific values" do
expect(configuration.values).to eq({
global: {
initialRootPassword: {
secret: "gitlab-initial-root-password"
},
gitaly: {
hooks: {
preReceive: {
configmap: "pre-receive-hook"
}
}
}
},
"nginx-ingress": {
controller: {
replicaCount: 1,
minAavailable: 1,
service: {
type: "NodePort",
nodePorts: {
"gitlab-shell": 32022,
http: 32080
}
}
}
}
})
end
it "returns correct gitlab url" do
expect(configuration.gitlab_url).to eq("http://gitlab.127.0.0.1.nip.io")
end
end

View File

@ -1,58 +1,167 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Cng::Deployment::Installation do
RSpec.describe Gitlab::Cng::Deployment::Installation, :aggregate_failures do
subject(:installation) do
described_class.new(
"gitlab",
configuration: "kind",
namespace: "gitlab",
ci: false
ci: ci
)
end
let(:command_status) { instance_double(Process::Status, success?: true) }
let(:kubeclient) { instance_double(Gitlab::Cng::Kubectl::Client, create_namespace: "", create_resource: "") }
let(:license_secret) { Gitlab::Cng::Kubectl::Resources::Secret.new("gitlab-license", "license", "test") }
let(:stdin) { StringIO.new }
let(:config_values) { { configuration_specific: true } }
let(:password_secret) do
Gitlab::Cng::Kubectl::Resources::Secret.new("gitlab-initial-root-password", "password", "test")
let(:ip) { instance_double(Addrinfo, ipv4_private?: true, ip_address: "127.0.0.1") }
let(:kubeclient) do
instance_double(Gitlab::Cng::Kubectl::Client, create_namespace: "", create_resource: "", execute: "")
end
let(:hook_configmap) do
Gitlab::Cng::Kubectl::Resources::Configmap.new(
"pre-receive-hook",
"hook.sh",
<<~SH
#!/usr/bin/env bash
if [[ $GL_PROJECT_PATH =~ 'reject-prereceive' ]]; then
echo 'GL-HOOK-ERR: Custom error message rejecting prereceive hook for projects with GL_PROJECT_PATH matching pattern reject-prereceive'
exit 1
fi
SH
let(:configuration) do
instance_double(
Gitlab::Cng::Deployment::Configurations::Kind,
run_pre_deployment_setup: nil,
run_post_deployment_setup: nil,
values: config_values,
gitlab_url: "http://gitlab.#{ip.ip_address}.nip.io"
)
end
let(:env) do
{
"QA_EE_LICENSE" => "license",
"CI_PROJECT_DIR" => File.expand_path("../../../../fixture", __dir__),
"CI_COMMIT_SHA" => "0acb5ee6db0860436fafc2c31a2cd87849c51aa3",
"CI_COMMIT_SHORT_SHA" => "0acb5ee6db08"
}
end
let(:values_yml) do
{
global: {
hosts: {
domain: "#{ip.ip_address}.nip.io",
https: false
},
ingress: {
configureCertmanager: false,
tls: {
enabled: false
}
},
appConfig: {
applicationSettingsCacheSeconds: 0
},
extraEnv: {
GITLAB_LICENSE_MODE: "test",
CUSTOMER_PORTAL_URL: "https://customers.staging.gitlab.com"
}
},
gitlab: {
"gitlab-exporter": { enabled: false },
license: { secret: "gitlab-license" }
},
redis: { metrics: { enabled: false } },
prometheus: { install: false },
certmanager: { install: false },
"gitlab-runner": { install: false },
**config_values
}.deep_stringify_keys.to_yaml
end
before do
allow(Gitlab::Cng::Helpers::Spinner).to receive(:spin).and_yield
allow(Gitlab::Cng::Kubectl::Client).to receive(:new).with("gitlab").and_return(kubeclient)
allow(Gitlab::Cng::Deployment::Configurations::Kind).to receive(:new).and_return(configuration)
allow(Open3).to receive(:popen2e).and_return(["", command_status])
allow(installation).to receive(:execute_shell)
allow(Socket).to receive(:ip_address_list).and_return([ip])
end
around do |example|
ClimateControl.modify({ "QA_EE_LICENSE" => "test", "GITLAB_ADMIN_PASSWORD" => "test" }) { example.run }
ClimateControl.modify(env) { example.run }
end
it "runs setup and helm deployment", :aggregate_failures do
expect { installation.create }.to output(/Creating CNG deployment 'gitlab' using 'kind' configuration/).to_stdout
context "without ci" do
let(:ci) { false }
expect(Open3).to have_received(:popen2e).with({}, *%w[helm repo add gitlab https://charts.gitlab.io])
expect(Open3).to have_received(:popen2e).with({}, *%w[helm repo update gitlab])
it "runs setup and helm deployment" do
expect { installation.create }.to output(/Creating CNG deployment 'gitlab' using 'kind' configuration/).to_stdout
expect(kubeclient).to have_received(:create_namespace)
expect(kubeclient).to have_received(:create_resource).with(license_secret)
expect(kubeclient).to have_received(:create_resource).with(password_secret)
expect(kubeclient).to have_received(:create_resource).with(hook_configmap)
expect(Gitlab::Cng::Deployment::Configurations::Kind).to have_received(:new).with(
"gitlab",
kubeclient,
ci,
"#{ip.ip_address}.nip.io"
)
expect(installation).to have_received(:execute_shell).with(
%w[helm repo add gitlab https://charts.gitlab.io],
stdin_data: nil
)
expect(installation).to have_received(:execute_shell).with(
%w[helm repo add gitlab https://charts.gitlab.io],
stdin_data: nil
)
expect(installation).to have_received(:execute_shell).with(
%w[helm repo update gitlab],
stdin_data: nil
)
expect(installation).to have_received(:execute_shell).with(
%w[
helm upgrade
--install gitlab gitlab/gitlab
--namespace gitlab
--timeout 5m
--wait
--values -
],
stdin_data: values_yml
)
expect(kubeclient).to have_received(:create_namespace)
expect(kubeclient).to have_received(:create_resource).with(
Gitlab::Cng::Kubectl::Resources::Secret.new("gitlab-license", "license", "license")
)
expect(configuration).to have_received(:run_pre_deployment_setup)
expect(configuration).to have_received(:run_post_deployment_setup)
end
end
context "with ci" do
let(:ci) { true }
it "runs helm install with correctly merged values and component versions" do
expect { installation.create }.to output(/Creating CNG deployment 'gitlab' using 'kind' configuration/).to_stdout
expect(installation).to have_received(:execute_shell).with(
%W[
helm upgrade
--install gitlab gitlab/gitlab
--namespace gitlab
--timeout 5m
--wait
--set gitlab.gitaly.image.repository=registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly
--set gitlab.gitaly.image.tag=7aa06a578d76bdc294ee8e9acb4f063e7d9f1d5f
--set gitlab.gitlab-shell.image.repository=registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell
--set gitlab.gitlab-shell.image.tag=v14.35.0
--set gitlab.migrations.image.repository=registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-toolbox-ee
--set gitlab.migrations.image.tag=#{env['CI_COMMIT_SHA']}
--set gitlab.toolbox.image.repository=registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-toolbox-ee
--set gitlab.toolbox.image.tag=#{env['CI_COMMIT_SHA']}
--set gitlab.sidekiq.annotations.commit=#{env['CI_COMMIT_SHORT_SHA']}
--set gitlab.sidekiq.image.repository=registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-sidekiq-ee
--set gitlab.sidekiq.image.tag=#{env['CI_COMMIT_SHA']}
--set gitlab.webservice.annotations.commit=#{env['CI_COMMIT_SHORT_SHA']}
--set gitlab.webservice.image.repository=registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-webservice-ee
--set gitlab.webservice.image.tag=#{env['CI_COMMIT_SHA']}
--set gitlab.webservice.workhorse.image=registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-workhorse-ee
--set gitlab.webservice.workhorse.tag=#{env['CI_COMMIT_SHA']}
--values -
],
stdin_data: values_yml
)
end
end
end

View File

@ -19,4 +19,15 @@ RSpec.describe Gitlab::Cng::Kubectl::Client do
expect(client.create_resource(resource)).to eq("cmd-output")
expect(Open3).to have_received(:popen2e).with({}, *%w[kubectl apply -n gitlab -f -])
end
it "executes custom command in pod" do
allow(Open3).to receive(:popen2e).with({}, *%w[
kubectl get pods -n gitlab --output jsonpath={.items[*].metadata.name}
]).and_return(["some-pod-123 test-pod-123", command_status])
expect(client.execute("test-pod", ["ls"], container: "toolbox")).to eq("cmd-output")
expect(Open3).to have_received(:popen2e).with({}, *%w[
kubectl exec test-pod-123 -n gitlab -c toolbox -- ls
])
end
end

View File

@ -252,9 +252,23 @@ RSpec.describe Gitlab::SecretDetection::Scan, feature_category: :secret_detectio
it "whole secret detection scan operation times out" do
scan_timeout_secs = 0.000_001 # 1 micro-sec to intentionally timeout large blob
response = Gitlab::SecretDetection::Response.new(Gitlab::SecretDetection::Status::SCAN_TIMEOUT)
expected_response = Gitlab::SecretDetection::Response.new(Gitlab::SecretDetection::Status::SCAN_TIMEOUT)
expect(scan.secrets_scan(blobs, timeout: scan_timeout_secs)).to eq(response)
begin
response = scan.secrets_scan(blobs, timeout: scan_timeout_secs)
expect(response).to eq(expected_response)
rescue ArgumentError
# When RSpec's main process terminates and attempts to clean up child processes upon completion, it terminates
# subprocesses where the scans might be still ongoing. This behavior is not recognized by the
# upstream library (parallel), which manages all forked subprocesses it created for running scans. When the
# upstream library attempts to close its forked subprocesses which already terminated, it raises an
# 'ArgumentError' with the message 'bad signal type NilClass,' resulting in flaky failures in the test
# expectations.
#
# Example: https://gitlab.com/gitlab-org/gitlab/-/jobs/6935051992
#
puts "skipping the test since the subprocesses forked for SD scanning are terminated by main process"
end
end
it "one of the blobs times out while others continue to get scanned" do

View File

@ -124,6 +124,7 @@ module API
end
expose :keep_latest_artifacts_available?, as: :keep_latest_artifact, documentation: { type: 'boolean' }
expose :restrict_user_defined_variables, documentation: { type: 'boolean' }
expose :ci_pipeline_variables_minimum_override_role, documentation: { type: 'string' }
expose :runners_token, documentation: { type: 'string', example: 'b8547b1dc37721d05889db52fa2f02' }
expose :runner_token_expiration_interval, documentation: { type: 'integer', example: 3600 }
expose :group_runners_enabled, documentation: { type: 'boolean' }

View File

@ -116,6 +116,7 @@ module API
optional :ci_allow_fork_pipelines_to_run_in_parent_project, type: Boolean, desc: 'Allow fork merge request pipelines to run in parent project'
optional :ci_separated_caches, type: Boolean, desc: 'Enable or disable separated caches based on branch protection.'
optional :restrict_user_defined_variables, type: Boolean, desc: 'Restrict use of user-defined variables when triggering a pipeline'
optional :ci_pipeline_variables_minimum_override_role, values: %w[no_one_allowed developer maintainer owner], type: String, desc: 'Limit ability to override CI/CD variables when triggering a pipeline to only users with at least the set minimum role'
end
params :optional_update_params_ee do
@ -208,6 +209,7 @@ module API
:model_experiments_access_level,
:model_registry_access_level,
:warn_about_potentially_unwanted_characters,
:ci_pipeline_variables_minimum_override_role,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,

View File

@ -596,6 +596,8 @@ module API
present_project user_project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project),
current_user: current_user
elsif result[:status] == :api_error
render_api_error!(result[:message], 400)
else
render_validation_error!(user_project)
end

View File

@ -178,16 +178,24 @@ module API
delete ':id/remote_mirrors/:mirror_id' do
mirror = find_remote_mirror
destroy_conditionally!(mirror) do
mirror_params = declared_params(include_missing: false).merge(_destroy: 1)
mirror_params[:id] = mirror_params.delete(:mirror_id)
update_params = { remote_mirrors_attributes: mirror_params }
if Feature.enabled?(:use_remote_mirror_destroy_service, user_project)
destroy_conditionally!(mirror) do
result = ::RemoteMirrors::DestroyService.new(user_project, current_user).execute(mirror)
# Note: We are using the update service to be consistent with how the controller handles deletion
result = ::Projects::UpdateService.new(user_project, current_user, update_params).execute
render_api_error!(result.message, 400) if result.error?
end
else
destroy_conditionally!(mirror) do
mirror_params = declared_params(include_missing: false).merge(_destroy: 1)
mirror_params[:id] = mirror_params.delete(:mirror_id)
update_params = { remote_mirrors_attributes: mirror_params }
if result[:status] != :success
render_api_error!(result[:message], 400)
# Note: We are using the update service to be consistent with how the controller handles deletion
result = ::Projects::UpdateService.new(user_project, current_user, update_params).execute
if result[:status] != :success
render_api_error!(result[:message], 400)
end
end
end
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Backfill epic issues into work item parent links
# There are two ways to use this background migration
#
# 1. Backfill every epic_issues record by providing a nil `group_id` argument. batch_table must be
# `epic_issues` and batch_column must be `id`.
# 2. Backfill epic_issues only for a specific group by providing a `group_id` argument. batch_table must be
# `epics` and batch_column must be `iid`.
class BackfillEpicIssuesIntoWorkItemParentLinks < BatchedMigrationJob
operation_name :backfill_epic_issues_into_work_item_parent_links
feature_category :team_planning
job_arguments :group_id
scope_to ->(relation) { scope_by_arguments(relation) }
class ParentLink < ApplicationRecord
self.table_name = :work_item_parent_links
self.inheritance_column = :_type_disabled
end
def perform
if group_id.present? && (batch_table.to_sym != :epics || batch_column.to_sym != :iid)
raise 'when group_id is provided, use `epics` as batch_table and `iid` as batch_column'
end
if group_id.blank? && batch_table.to_sym == :epics
raise 'use `epic_issues` as batch_table when no group_id is provided'
end
each_sub_batch do |sub_batch|
ParentLink.transaction do
if batch_table.to_sym == :epic_issues
upsert_by_epic_issues(sub_batch)
else
upsert_by_epics(sub_batch)
end
end
end
end
private
def scope_by_arguments(relation)
return relation if group_id.blank?
relation.where(group_id: group_id)
end
def upsert_records(records)
ParentLink.upsert_all(
records,
on_duplicate: :update,
unique_by: :index_work_item_parent_links_on_work_item_id
)
end
def batch_attributes(sub_batch)
locked_batch = sub_batch.joins('INNER JOIN epics ON epics.id = epic_issues.epic_id')
.select(:id, :epic_id, :issue_id, :relative_position)
.select('epics.issue_id AS parent_issue_id')
.lock!
locked_batch.map do |epic_issue|
{
work_item_parent_id: epic_issue.parent_issue_id,
work_item_id: epic_issue.issue_id,
relative_position: epic_issue.relative_position
}
end
end
def upsert_by_epics(sub_batch)
epic_issues.where(epic_id: sub_batch.select(:id)).each_batch(of: 100) do |batch|
upsert_records(
batch_attributes(batch)
)
end
end
def upsert_by_epic_issues(sub_batch)
upsert_records(
batch_attributes(sub_batch)
)
end
def epic_issues
@epic_issues ||= define_batchable_model(:epic_issues, connection: ApplicationRecord.connection)
end
end
end
end

View File

@ -25,6 +25,8 @@ module Gitlab
complexity, depth, field_usages =
GraphQL::Analysis::AST.analyze_query(@subject, ALL_ANALYZERS, multiplex_analyzers: [])
field_usages ||= {} # in various edge cases, #analyze_query returns []
results[:depth] = depth
results[:complexity] = complexity
# This duration is not the execution time of the

View File

@ -4703,6 +4703,9 @@ msgstr ""
msgid "AiImpactAnalytics|Monthly user engagement with AI Code Suggestions. Percentage ratio calculated as monthly unique Code Suggestions users / total monthly unique code contributors."
msgstr ""
msgid "AiImpactAnalytics|Usage rate for Code Suggestions is calculated with data starting on %{startDate}"
msgstr ""
msgid "Akismet"
msgstr ""
@ -39990,6 +39993,9 @@ msgstr ""
msgid "Profiles|Do not show on profile"
msgstr ""
msgid "Profiles|Don't display activity-related personal information on your profile."
msgstr ""
msgid "Profiles|Don't display activity-related personal information on your profile. %{private_profile_help_link_start}Learn more%{private_profile_help_link_end}."
msgstr ""
@ -49109,6 +49115,9 @@ msgstr ""
msgid "Setting enforced"
msgstr ""
msgid "Setting locked. Profiles are required to be public in this instance."
msgstr ""
msgid "Settings"
msgstr ""
@ -56056,6 +56065,12 @@ msgstr ""
msgid "UpdateProject|Cannot rename project, the container registry path rename validation failed: %{error}"
msgstr ""
msgid "UpdateProject|Changing the ci_pipeline_variables_minimum_override_role to the owner role is not allowed"
msgstr ""
msgid "UpdateProject|Changing the restrict_user_defined_variables or ci_pipeline_variables_minimum_override_role is not allowed"
msgstr ""
msgid "UpdateProject|Could not set the default branch"
msgstr ""

View File

@ -2,6 +2,7 @@ PATH
remote: ../gems/gitlab-cng
specs:
gitlab-cng (0.0.1)
activesupport (>= 7)
rainbow (~> 3.1)
require_all (~> 3.0)
thor (~> 1.3)

View File

@ -12,11 +12,13 @@ module QA
def initialize(sdk_host, sdk_app_id)
# Below is an image of a sample app that uses Product Analytics Browser SDK.
# The image is created in https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-browser
# It's buit on every merge to main branch in the repository.
# It's built on every merge to main branch in the repository.
# @name should not contain _ (underscores) as it is used to generate host_name
# and _ are not allowed for domain names.
# Note: set @host_name = 'localhost' here when running locally against GDK.
@image = 'registry.gitlab.com/gitlab-org/analytics-section/product-analytics/' \
'gl-application-sdk-browser/example-app:main'
@name = 'browser_sdk'
@name = 'browser-sdk'
@sdk_host = URI(sdk_host)
@sdk_app_id = sdk_app_id
@port = '8081'

View File

@ -10,11 +10,13 @@ module QA
def initialize(sdk_host, sdk_app_id)
# Below is an image of a sample app that uses Product Analytics .NET SDK.
# The image is created in https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-dotnet
# It's buit on every merge to main branch in the repository.
# It's built on every merge to main branch in the repository.
# @name should not contain _ (underscores) as it is used to generate host_name
# and _ are not allowed for domain names.
# Note: set @host_name = 'localhost' here when running locally against GDK.
@image = 'registry.gitlab.com/gitlab-org/analytics-section/product-analytics/' \
'gl-application-sdk-dotnet/example-app:main'
@name = 'dotnet_sdk'
@name = 'dotnet-sdk'
@sdk_host = URI(sdk_host)
@sdk_app_id = sdk_app_id
@port = '5171'

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
module QA
module Service
module DockerRun
module ProductAnalytics
class RubySdkApp < Base
include Support::API
def initialize(sdk_host)
# Below is an image of a sample app that uses Product Analytics ruby SDK.
# The image is created in https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-rb
# It's built on every merge to main branch in the repository.
# @name should not contain _ (underscores) as it is used to generate host_name
# and _ are not allowed for domain names.
# Note: set @host_name = 'localhost' here when running locally against GDK.
@image = 'registry.gitlab.com/gitlab-org/analytics-section/product-analytics/' \
'gl-application-sdk-rb/example-app:main'
@name = 'ruby-sdk'
@sdk_host = URI(sdk_host)
@port = '5172'
super()
end
def register!(sdk_app_id)
shell <<~CMD.tr("\n", ' ')
docker run -d --rm
--name #{@name}
--network #{network}
--hostname #{host_name}
-p #{@port}:#{@port}
-e PA_COLLECTOR_URL=#{@sdk_host}
-e PA_APPLICATION_ID=#{sdk_app_id}
#{@image}
-p #{@port}
CMD
wait_for_app_available
end
def trigger_event
get "http://#{host_name}:#{@port}/api/v1/send_event"
Runtime::Logger.info('Ruby SDK event is triggered!')
end
private
def wait_for_app_available
Runtime::Logger.info("Waiting for Ruby SDK sample app to become available at http://#{host_name}:#{@port}...")
Support::Waiter.wait_until(sleep_interval: 1,
message: "Wait for Ruby SDK sample app to become available at http://#{host_name}:#{@port}") { app_available? }
Runtime::Logger.info('Ruby SDK sample app is up!')
end
def app_available?
response = get "http://#{host_name}:#{@port}"
response.code == 200
rescue Errno::ECONNRESET, Errno::ECONNREFUSED, RestClient::ServerBrokeConnection => e
Runtime::Logger.debug("Ruby SDK sample app is not yet available: #{e.inspect}")
false
end
end
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More