Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7f0fd430c2
commit
ed899a6a1e
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
= 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!
|
||||
·
|
||||
#{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
|
||||
|
||||
= 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} }
|
||||
|
||||
= 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')
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
·
|
||||
= 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} }
|
||||
|
||||
= 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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
24d4e0a054fa3c52b1ce41c7a28cb759e711d1b7860caf934ddb93c7540ee8c5
|
||||
|
|
@ -0,0 +1 @@
|
|||
176ca5cc2642c0873366e46848a8040845cfbacb0080ea697a4dba82267d0ea0
|
||||
|
|
@ -0,0 +1 @@
|
|||
cd33ac1a0bf63f127323f3e84913ddb9315aff5fc828d6bd5999d52857f1647e
|
||||
|
|
@ -0,0 +1 @@
|
|||
d3f5bc2c43d871d32e6726e4b3f001a08a4de338dc5c17b7d1b0cf2d6747ee5a
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -2,6 +2,7 @@ PATH
|
|||
remote: .
|
||||
specs:
|
||||
gitlab-cng (0.0.1)
|
||||
activesupport (>= 7)
|
||||
rainbow (~> 3.1)
|
||||
require_all (~> 3.0)
|
||||
thor (~> 1.3)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
7aa06a578d76bdc294ee8e9acb4f063e7d9f1d5f
|
||||
|
|
@ -0,0 +1 @@
|
|||
14.35.0
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue