Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
824dc86379
commit
2b4a1d3772
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'GoogleCloudField',
|
||||
components: {
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ value: '', state: null }),
|
||||
validator: ({ value }) => typeof value === 'string',
|
||||
},
|
||||
invalidFeedbackIfEmpty: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('This field is required.'),
|
||||
},
|
||||
invalidFeedbackIfMalformed: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('This value is not valid.'),
|
||||
},
|
||||
regexp: {
|
||||
type: RegExp,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
invalidFeedback: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onChange(value) {
|
||||
if (!value) {
|
||||
this.state = false;
|
||||
this.invalidFeedback = this.invalidFeedbackIfEmpty;
|
||||
} else if (this.regexp && !value.match(this.regexp)) {
|
||||
this.state = false;
|
||||
this.invalidFeedback = this.invalidFeedbackIfMalformed;
|
||||
} else {
|
||||
this.state = true;
|
||||
this.invalidFeedback = '';
|
||||
}
|
||||
this.$emit('change', { state: this.state, value });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-form-group
|
||||
:label-for="name"
|
||||
:state="state"
|
||||
:invalid-feedback="invalidFeedback"
|
||||
:label="label"
|
||||
>
|
||||
<template v-for="slot in Object.keys($scopedSlots)" #[slot]>
|
||||
<slot :name="slot"></slot>
|
||||
</template>
|
||||
<gl-form-input
|
||||
:id="name"
|
||||
:name="name"
|
||||
:state="state"
|
||||
:value="value ? value.value : ''"
|
||||
type="text"
|
||||
@change="onChange"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
|
|
@ -1,16 +1,7 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
GlLink,
|
||||
GlIcon,
|
||||
GlModal,
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import { GlAlert, GlButton, GlLink, GlIcon, GlModal, GlPopover, GlSprintf } from '@gitlab/ui';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import GoogleCloudFieldGroup from '~/ci/runner/components/registration/google_cloud_field_group.vue';
|
||||
import { createAlert } from '~/alert';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
|
|
@ -19,6 +10,7 @@ import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
|
|||
import runnerForRegistrationQuery from '../../graphql/register/runner_for_registration.query.graphql';
|
||||
import provisionGoogleCloudRunnerGroup from '../../graphql/register/provision_google_cloud_runner_group.query.graphql';
|
||||
import provisionGoogleCloudRunnerProject from '../../graphql/register/provision_google_cloud_runner_project.query.graphql';
|
||||
|
||||
import {
|
||||
I18N_FETCH_ERROR,
|
||||
STATUS_ONLINE,
|
||||
|
|
@ -26,8 +18,17 @@ import {
|
|||
} from '../../constants';
|
||||
import { captureException } from '../../sentry_utils';
|
||||
|
||||
const GC_PROJECT_PATTERN = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/; // https://cloud.google.com/resource-manager/reference/rest/v1/projects
|
||||
const GC_REGION_PATTERN = /^[a-z]+-[a-z]+\d+$/;
|
||||
const GC_ZONE_PATTERN = /^[a-z]+-[a-z]+\d+-[a-z]$/;
|
||||
const GC_MACHINE_TYPE_PATTERN = /^[a-z]([-a-z0-9]*[a-z0-9])?$/;
|
||||
|
||||
export default {
|
||||
name: 'GoogleCloudRegistrationInstructions',
|
||||
GC_PROJECT_PATTERN,
|
||||
GC_REGION_PATTERN,
|
||||
GC_ZONE_PATTERN,
|
||||
GC_MACHINE_TYPE_PATTERN,
|
||||
i18n: {
|
||||
heading: s__('Runners|Register runner'),
|
||||
headingDescription: s__(
|
||||
|
|
@ -122,10 +123,9 @@ export default {
|
|||
},
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GoogleCloudFieldGroup,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlModal,
|
||||
|
|
@ -150,19 +150,16 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
projectId: '',
|
||||
region: '',
|
||||
zone: '',
|
||||
machineType: 'n2d-standard-2',
|
||||
token: '',
|
||||
runner: null,
|
||||
showInstructionsModal: false,
|
||||
showInstructionsButtonVariant: 'default',
|
||||
validations: {
|
||||
projectId: false,
|
||||
region: false,
|
||||
zone: false,
|
||||
},
|
||||
|
||||
cloudProjectId: null,
|
||||
region: null,
|
||||
zone: null,
|
||||
machineType: { state: true, value: 'n2d-standard-2' },
|
||||
|
||||
provisioningSteps: [],
|
||||
setupBashScript: '',
|
||||
showAlert: false,
|
||||
|
|
@ -204,14 +201,12 @@ export default {
|
|||
project: {
|
||||
query: provisionGoogleCloudRunnerProject,
|
||||
fetchPolicy: fetchPolicies.NETWORK_ONLY,
|
||||
skip: true,
|
||||
manual: true,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectPath,
|
||||
cloudProjectId: this.projectId,
|
||||
region: this.region,
|
||||
zone: this.zone,
|
||||
machineType: this.machineType,
|
||||
runnerToken: this.token,
|
||||
...this.variables,
|
||||
};
|
||||
},
|
||||
result({ data, error }) {
|
||||
|
|
@ -224,20 +219,16 @@ export default {
|
|||
error(error) {
|
||||
this.handleError(error);
|
||||
},
|
||||
skip() {
|
||||
return !this.projectPath || this.invalidFields.length > 0;
|
||||
},
|
||||
},
|
||||
group: {
|
||||
query: provisionGoogleCloudRunnerGroup,
|
||||
fetchPolicy: fetchPolicies.NETWORK_ONLY,
|
||||
skip: true,
|
||||
manual: true,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.groupPath,
|
||||
cloudProjectId: this.projectId,
|
||||
region: this.region,
|
||||
zone: this.zone,
|
||||
machineType: this.machineType,
|
||||
runnerToken: this.token,
|
||||
...this.variables,
|
||||
};
|
||||
},
|
||||
result({ data, error }) {
|
||||
|
|
@ -250,12 +241,18 @@ export default {
|
|||
error(error) {
|
||||
this.handleError(error);
|
||||
},
|
||||
skip() {
|
||||
return !this.groupPath || this.invalidFields.length > 0;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
variables() {
|
||||
return {
|
||||
runnerToken: this.token,
|
||||
cloudProjectId: this.cloudProjectId?.value,
|
||||
region: this.region?.value,
|
||||
zone: this.zone?.value,
|
||||
machineType: this.machineType?.value,
|
||||
};
|
||||
},
|
||||
isRunnerOnline() {
|
||||
return this.runner?.status === STATUS_ONLINE;
|
||||
},
|
||||
|
|
@ -270,8 +267,8 @@ export default {
|
|||
);
|
||||
},
|
||||
invalidFields() {
|
||||
return Object.keys(this.validations).filter((field) => {
|
||||
return this.validations[field] === false;
|
||||
return ['cloudProjectId', 'region', 'zone', 'machineType'].filter((field) => {
|
||||
return !this[field]?.state;
|
||||
});
|
||||
},
|
||||
bashInstructions() {
|
||||
|
|
@ -304,34 +301,19 @@ export default {
|
|||
if (this.invalidFields.length > 0) {
|
||||
this.showAlert = true;
|
||||
} else {
|
||||
if (this.projectPath) {
|
||||
this.$apollo.queries.project.start();
|
||||
} else {
|
||||
this.$apollo.queries.group.start();
|
||||
}
|
||||
this.showAlert = false;
|
||||
this.showInstructionsModal = true;
|
||||
}
|
||||
},
|
||||
validateZone() {
|
||||
if (this.zone.length > 0) {
|
||||
this.validations.zone = true;
|
||||
} else {
|
||||
this.validations.zone = false;
|
||||
}
|
||||
},
|
||||
validateRegion() {
|
||||
if (this.region.length > 0) {
|
||||
this.validations.region = true;
|
||||
} else {
|
||||
this.validations.region = false;
|
||||
}
|
||||
},
|
||||
validateProjectId() {
|
||||
if (this.projectId.length > 0) {
|
||||
this.validations.projectId = true;
|
||||
} else {
|
||||
this.validations.projectId = false;
|
||||
}
|
||||
},
|
||||
|
||||
goToFirstInvalidField() {
|
||||
if (this.invalidFields.length > 0) {
|
||||
this.$refs[this.invalidFields[0]].$el.focus();
|
||||
this.$refs[this.invalidFields[0]].$el.querySelector('input').focus();
|
||||
}
|
||||
},
|
||||
handleError(error) {
|
||||
|
|
@ -427,7 +409,20 @@ export default {
|
|||
<h2 class="gl-heading-2">{{ $options.i18n.stepOneHeading }}</h2>
|
||||
<p>{{ $options.i18n.stepOneDescription }}</p>
|
||||
|
||||
<gl-form-group :label="$options.i18n.projectIdLabel" label-for="project-id">
|
||||
<google-cloud-field-group
|
||||
ref="cloudProjectId"
|
||||
v-model="cloudProjectId"
|
||||
name="cloudProjectId"
|
||||
:label="$options.i18n.projectIdLabel"
|
||||
:invalid-feedback-if-empty="s__('Runners|Project ID is required.')"
|
||||
:invalid-feedback-if-malformed="
|
||||
s__(
|
||||
'Runners|Project ID must be 6 to 30 lowercase letters, digits, or hyphens. It needs to start with a lowercase letter and end with a letter or number.',
|
||||
)
|
||||
"
|
||||
:regexp="$options.GC_PROJECT_PATTERN"
|
||||
data-testid="project-id-input"
|
||||
>
|
||||
<template #description>
|
||||
<gl-sprintf :message="$options.i18n.projectIdDescription">
|
||||
<template #link="{ content }">
|
||||
|
|
@ -442,16 +437,19 @@ export default {
|
|||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
<gl-form-input
|
||||
id="project-id"
|
||||
ref="projectId"
|
||||
v-model="projectId"
|
||||
type="text"
|
||||
data-testid="project-id-input"
|
||||
@input="validateProjectId"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group label-for="region-id">
|
||||
</google-cloud-field-group>
|
||||
|
||||
<google-cloud-field-group
|
||||
ref="region"
|
||||
v-model="region"
|
||||
name="region"
|
||||
:invalid-feedback-if-empty="s__('Runners|Region is required.')"
|
||||
:invalid-feedback-if-malformed="
|
||||
s__('Runners|Region must have the correct format. Example: us-central1')
|
||||
"
|
||||
:regexp="$options.GC_REGION_PATTERN"
|
||||
data-testid="region-input"
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
{{ $options.i18n.regionLabel }}
|
||||
|
|
@ -471,15 +469,19 @@ export default {
|
|||
</gl-popover>
|
||||
</div>
|
||||
</template>
|
||||
<gl-form-input
|
||||
id="region-id"
|
||||
ref="region"
|
||||
v-model="region"
|
||||
data-testid="region-input"
|
||||
@input="validateRegion"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group label-for="zone-id">
|
||||
</google-cloud-field-group>
|
||||
|
||||
<google-cloud-field-group
|
||||
ref="zone"
|
||||
v-model="zone"
|
||||
name="zone"
|
||||
:invalid-feedback-if-empty="s__('Runners|Zone is required.')"
|
||||
:invalid-feedback-if-malformed="
|
||||
s__('Runners|Zone must have the correct format. Example: us-central1-a')
|
||||
"
|
||||
:regexp="$options.GC_ZONE_PATTERN"
|
||||
data-testid="zone-input"
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
{{ $options.i18n.zoneLabel }}
|
||||
|
|
@ -505,15 +507,21 @@ export default {
|
|||
<gl-icon name="external-link" :aria-label="$options.i18n.externalLink" />
|
||||
</gl-link>
|
||||
</template>
|
||||
<gl-form-input
|
||||
id="zone-id"
|
||||
ref="zone"
|
||||
v-model="zone"
|
||||
data-testid="zone-input"
|
||||
@input="validateZone"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group label-for="machine-type-id">
|
||||
</google-cloud-field-group>
|
||||
|
||||
<google-cloud-field-group
|
||||
ref="machineType"
|
||||
v-model="machineType"
|
||||
name="machineType"
|
||||
:invalid-feedback-if-empty="s__('Runners|Machine type is required.')"
|
||||
:invalid-feedback-if-malformed="
|
||||
s__(
|
||||
'Runners|Machine type must have the format `family-series-size`. Example: n2d-standard-2',
|
||||
)
|
||||
"
|
||||
:regexp="$options.GC_MACHINE_TYPE_PATTERN"
|
||||
data-testid="machine-type-input"
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
{{ $options.i18n.machineTypeLabel }}
|
||||
|
|
@ -547,8 +555,7 @@ export default {
|
|||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
<gl-form-input id="machine-type-id" v-model="machineType" data-testid="machine-type-input" />
|
||||
</gl-form-group>
|
||||
</google-cloud-field-group>
|
||||
|
||||
<hr />
|
||||
<!-- end: step one -->
|
||||
|
|
@ -627,5 +634,11 @@ export default {
|
|||
</gl-modal>
|
||||
<hr />
|
||||
<!-- end: step two -->
|
||||
<section v-if="isRunnerOnline">
|
||||
<h2 class="gl-heading-2">🎉 {{ s__("Runners|You've registered a new runner!") }}</h2>
|
||||
<p>
|
||||
{{ s__('Runners|Your runner is online and ready to run jobs.') }}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -980,68 +980,76 @@ export default {
|
|||
@select-issuable="handleSelectIssuable"
|
||||
>
|
||||
<template #nav-actions>
|
||||
<local-storage-sync
|
||||
v-if="gridViewFeatureEnabled"
|
||||
:value="viewType"
|
||||
:storage-key="$options.ISSUES_VIEW_TYPE_KEY"
|
||||
@input="switchViewType"
|
||||
>
|
||||
<gl-button-group>
|
||||
<gl-button
|
||||
:variant="isGridView ? 'default' : 'confirm'"
|
||||
data-testid="list-view-type"
|
||||
@click="switchViewType($options.ISSUES_LIST_VIEW_KEY)"
|
||||
>
|
||||
{{ $options.i18n.listLabel }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
:variant="isGridView ? 'confirm' : 'default'"
|
||||
data-testid="grid-view-type"
|
||||
@click="switchViewType($options.ISSUES_GRID_VIEW_KEY)"
|
||||
>
|
||||
{{ $options.i18n.gridLabel }}
|
||||
</gl-button>
|
||||
</gl-button-group>
|
||||
</local-storage-sync>
|
||||
<div class="gl-display-flex gl-gap-3">
|
||||
<local-storage-sync
|
||||
v-if="gridViewFeatureEnabled"
|
||||
:value="viewType"
|
||||
:storage-key="$options.ISSUES_VIEW_TYPE_KEY"
|
||||
@input="switchViewType"
|
||||
>
|
||||
<gl-button-group>
|
||||
<gl-button
|
||||
:variant="isGridView ? 'default' : 'confirm'"
|
||||
data-testid="list-view-type"
|
||||
@click="switchViewType($options.ISSUES_LIST_VIEW_KEY)"
|
||||
>
|
||||
{{ $options.i18n.listLabel }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
:variant="isGridView ? 'confirm' : 'default'"
|
||||
data-testid="grid-view-type"
|
||||
@click="switchViewType($options.ISSUES_GRID_VIEW_KEY)"
|
||||
>
|
||||
{{ $options.i18n.gridLabel }}
|
||||
</gl-button>
|
||||
</gl-button-group>
|
||||
</local-storage-sync>
|
||||
|
||||
<gl-button
|
||||
v-if="canBulkUpdate"
|
||||
:disabled="isBulkEditButtonDisabled"
|
||||
@click="handleBulkUpdateClick"
|
||||
>
|
||||
{{ $options.i18n.editIssues }}
|
||||
</gl-button>
|
||||
<slot name="new-issuable-button">
|
||||
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
|
||||
{{ $options.i18n.newIssueLabel }}
|
||||
<gl-button
|
||||
v-if="canBulkUpdate"
|
||||
:disabled="isBulkEditButtonDisabled"
|
||||
class="gl-flex-grow-1"
|
||||
@click="handleBulkUpdateClick"
|
||||
>
|
||||
{{ $options.i18n.editIssues }}
|
||||
</gl-button>
|
||||
</slot>
|
||||
<new-resource-dropdown
|
||||
v-if="showNewIssueDropdown"
|
||||
:query="$options.searchProjectsQuery"
|
||||
:query-variables="newIssueDropdownQueryVariables"
|
||||
:extract-projects="extractProjects"
|
||||
:group-id="groupId"
|
||||
/>
|
||||
<gl-disclosure-dropdown
|
||||
v-gl-tooltip.hover="$options.i18n.actionsLabel"
|
||||
category="tertiary"
|
||||
icon="ellipsis_v"
|
||||
no-caret
|
||||
:toggle-text="$options.i18n.actionsLabel"
|
||||
text-sr-only
|
||||
data-testid="issues-list-more-actions-dropdown"
|
||||
>
|
||||
<csv-import-export-buttons
|
||||
v-if="showCsvButtons"
|
||||
:export-csv-path="exportCsvPathWithQuery"
|
||||
:issuable-count="currentTabCount"
|
||||
<slot name="new-issuable-button">
|
||||
<gl-button
|
||||
v-if="showNewIssueLink"
|
||||
:href="newIssuePath"
|
||||
variant="confirm"
|
||||
class="gl-flex-grow-1"
|
||||
>
|
||||
{{ $options.i18n.newIssueLabel }}
|
||||
</gl-button>
|
||||
</slot>
|
||||
<new-resource-dropdown
|
||||
v-if="showNewIssueDropdown"
|
||||
:query="$options.searchProjectsQuery"
|
||||
:query-variables="newIssueDropdownQueryVariables"
|
||||
:extract-projects="extractProjects"
|
||||
:group-id="groupId"
|
||||
/>
|
||||
<gl-disclosure-dropdown-group
|
||||
:bordered="showCsvButtons"
|
||||
:group="subscribeDropdownOptions"
|
||||
/>
|
||||
</gl-disclosure-dropdown>
|
||||
<gl-disclosure-dropdown
|
||||
v-gl-tooltip.hover="$options.i18n.actionsLabel"
|
||||
category="tertiary"
|
||||
icon="ellipsis_v"
|
||||
no-caret
|
||||
:toggle-text="$options.i18n.actionsLabel"
|
||||
text-sr-only
|
||||
data-testid="issues-list-more-actions-dropdown"
|
||||
>
|
||||
<csv-import-export-buttons
|
||||
v-if="showCsvButtons"
|
||||
:export-csv-path="exportCsvPathWithQuery"
|
||||
:issuable-count="currentTabCount"
|
||||
/>
|
||||
<gl-disclosure-dropdown-group
|
||||
:bordered="showCsvButtons"
|
||||
:group="subscribeDropdownOptions"
|
||||
/>
|
||||
</gl-disclosure-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #timeframe="{ issuable = {} }">
|
||||
|
|
|
|||
|
|
@ -63,28 +63,30 @@ new Vue({
|
|||
},
|
||||
});
|
||||
|
||||
const {
|
||||
graphEndpoint,
|
||||
graphEndDate,
|
||||
graphStartDate,
|
||||
graphRef,
|
||||
graphCsvPath,
|
||||
} = codeCoverageContainer.dataset;
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: codeCoverageContainer,
|
||||
render(h) {
|
||||
return h(CodeCoverage, {
|
||||
props: {
|
||||
graphEndpoint,
|
||||
graphEndDate,
|
||||
graphStartDate,
|
||||
graphRef,
|
||||
graphCsvPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
if (codeCoverageContainer?.dataset) {
|
||||
const {
|
||||
graphEndpoint,
|
||||
graphEndDate,
|
||||
graphStartDate,
|
||||
graphRef,
|
||||
graphCsvPath,
|
||||
} = codeCoverageContainer.dataset;
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: codeCoverageContainer,
|
||||
render(h) {
|
||||
return h(CodeCoverage, {
|
||||
props: {
|
||||
graphEndpoint,
|
||||
graphEndDate,
|
||||
graphStartDate,
|
||||
graphRef,
|
||||
graphCsvPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
|
|
|
|||
|
|
@ -186,6 +186,8 @@ export default {
|
|||
:toggle-text="$options.i18n.toggleButtonLabel"
|
||||
variant="confirm"
|
||||
data-testid="new-resource-dropdown"
|
||||
class="gl-display-flex! gl-w-auto!"
|
||||
toggle-class="gl-m-0! gl-w-auto! gl-flex-grow-0!"
|
||||
@click="handleDropdownClick"
|
||||
@shown="handleDropdownShown"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
migration_job_name: BackfillCatalogResourceVersionSemVer
|
||||
description: Backfills the semver columns of existing catalog resource versions.
|
||||
feature_category: pipeline_composition
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146688
|
||||
milestone: '16.10'
|
||||
queued_migration_version: 20240305182005
|
||||
finalize_after: '2024-03-15'
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueBackfillCatalogResourceVersionSemVer < Gitlab::Database::Migration[2.2]
|
||||
milestone '16.10'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = 'BackfillCatalogResourceVersionSemVer'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 500
|
||||
SUB_BATCH_SIZE = 100
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:catalog_resource_versions,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :catalog_resource_versions, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
a195ad9f1d08c0c54c2576c0e00c7f0877fa819594a2fb210bcccea67d3b1ea6
|
||||
|
|
@ -238,7 +238,7 @@ To get started, sign in to the [Azure Portal](https://portal.azure.com). For you
|
|||
you need the following information:
|
||||
|
||||
- A tenant ID. You may already have one. For more information, see the
|
||||
[Microsoft Azure Tenant](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant) documentation.
|
||||
[Microsoft Azure Tenant](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-create-new-tenant) documentation.
|
||||
- A client ID and a client secret. Follow the instructions in the
|
||||
[Microsoft Quickstart Register an Application](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) documentation
|
||||
to obtain the tenant ID, client ID, and client secret for your app.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Configure AWS Backup to back up S3 data. This can be done at the same time when
|
|||
1. [Create Storage Transfer Service jobs](https://cloud.google.com/storage-transfer/docs/create-transfers) which copy each GitLab object storage bucket to a backup bucket. You can create these jobs once, and [schedule them to run daily](https://cloud.google.com/storage-transfer/docs/schedule-transfer-jobs). However this mixes new and old object storage data, so files that were deleted in GitLab will still exist in the backup. This wastes storage after restore, but it is otherwise not a problem. These files would be inaccessible to GitLab users since they do not exist in the GitLab database. You can delete [some of these orphaned files](../../raketasks/cleanup.md#clean-up-project-upload-files-from-object-storage) after restore, but this clean up Rake task only operates on a subset of files.
|
||||
1. For `When to overwrite`, choose `Never`. GitLab object stored files are intended to be immutable. This selection could be helpful if a malicious actor succeeded at mutating GitLab files.
|
||||
1. For `When to delete`, choose `Never`. If you sync the backup bucket to source, then you cannot recover if files are accidentally or maliciously deleted from source.
|
||||
1. Alternatively, it is possible to backup object storage into buckets or subdirectories segregated by day. This avoids the problem of orphaned files after restore, and supports backup of file versions if needed. But it greatly increases backup storage costs. This can be done with [a Cloud Function triggered by Cloud Scheduler](https://cloud.google.com/scheduler/docs/tut-pub-sub), or with a script run by a cronjob. A partial example:
|
||||
1. Alternatively, it is possible to backup object storage into buckets or subdirectories segregated by day. This avoids the problem of orphaned files after restore, and supports backup of file versions if needed. But it greatly increases backup storage costs. This can be done with [a Cloud Function triggered by Cloud Scheduler](https://cloud.google.com/scheduler/docs/tut-gcf-pub-sub), or with a script run by a cronjob. A partial example:
|
||||
|
||||
```shell
|
||||
# Set GCP project so you don't have to specify it in every command
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ You can learn more about how to administer GitLab.
|
|||
### Free GitLab training
|
||||
|
||||
- GitLab basics: Discover self-service guides on [Git and GitLab basics](../tutorials/index.md).
|
||||
- GitLab Learn: Learn new GitLab skills in a structured course at [GitLab Learn](https://about.gitlab.com/learn/).
|
||||
- GitLab University: Learn new GitLab skills in a structured course at [GitLab University](https://university.gitlab.com/learn/dashboard).
|
||||
|
||||
### Third-party training
|
||||
|
||||
|
|
|
|||
|
|
@ -1401,7 +1401,7 @@ Make sure everything continues to work as expected before replicating it in prod
|
|||
### Docker Distribution Registry
|
||||
|
||||
The [Docker Distribution Registry](https://docs.docker.com/registry/) was donated to the CNCF
|
||||
and is now known as the [Distribution Registry](https://distribution.github.io/distribution).
|
||||
and is now known as the [Distribution Registry](https://distribution.github.io/distribution/).
|
||||
This registry is the open source implementation that the GitLab container registry is based on.
|
||||
The GitLab container registry is compatible with the basic functionality provided by the Distribution Registry,
|
||||
including all the supported storage backends. To migrate to the GitLab container registry
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ a useless shell layer. However, that does not work for all Docker versions.
|
|||
- For Docker 17.03 and earlier, the `entrypoint` can be set to
|
||||
`/bin/sh -c`, `/bin/bash -c`, or an equivalent shell available in the image.
|
||||
|
||||
The syntax of `image:entrypoint` is similar to [Dockerfile `ENTRYPOINT`](https://docs.docker.com/engine/reference/builder/#entrypoint).
|
||||
The syntax of `image:entrypoint` is similar to [Dockerfile `ENTRYPOINT`](https://docs.docker.com/reference/dockerfile/#entrypoint).
|
||||
|
||||
Let's assume you have a `super/sql:experimental` image with a SQL database
|
||||
in it. You want to use it as a base image for your job because you
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ Automatic class path correction also works for a Java project with:
|
|||
|
||||
- A full path of `test-org/test-java-project`.
|
||||
- The following files relative to the project root:
|
||||
|
||||
|
||||
```shell
|
||||
src/main/java/com/gitlab/security_products/tests/App.java
|
||||
```
|
||||
|
|
@ -300,7 +300,7 @@ run tests:
|
|||
The following `.gitlab-ci.yml` example for PHP uses [PHPUnit](https://phpunit.readthedocs.io/)
|
||||
to collect test coverage data and generate the report.
|
||||
|
||||
With a minimal [`phpunit.xml`](https://docs.phpunit.de/en/10.2/configuration.html) file (you may reference
|
||||
With a minimal [`phpunit.xml`](https://docs.phpunit.de/en/11.0/configuration.html) file (you may reference
|
||||
[this example repository](https://gitlab.com/yookoala/code-coverage-visualization-with-php/)), you can run the test and
|
||||
generate the `coverage.xml`:
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ You can also integrate GitLab with the following security partners:
|
|||
|
||||
<!-- vale gitlab.Spelling = NO -->
|
||||
|
||||
- [Anchore](https://docs.anchore.com/current/docs/configuration/integration/ci_cd/gitlab/)
|
||||
- [Anchore](https://docs.anchore.com/current/docs/integration/ci_cd/gitlab/)
|
||||
- [Bridgecrew](https://docs.bridgecrew.io/docs/integrate-with-gitlab-self-managed)
|
||||
- [Checkmarx](https://checkmarx.atlassian.net/wiki/spaces/SD/pages/1929937052/GitLab+Integration)
|
||||
- [CodeSecure](https://codesecure.com/our-integrations/codesonar-sast-gitlab-ci-pipeline/)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ Prerequisites:
|
|||
|
||||
- If your remote repository is on GitHub and you have
|
||||
[two-factor authentication (2FA) configured](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa),
|
||||
create a [personal access token for GitHub](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
|
||||
create a [personal access token for GitHub](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
|
||||
with the `repo` scope. If 2FA is enabled, this personal access
|
||||
token serves as your GitHub password.
|
||||
- [GitLab Silent Mode](../../../../administration/silent_mode/index.md) is not enabled.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class BackfillCatalogResourceVersionSemVer < BatchedMigrationJob
|
||||
feature_category :pipeline_composition
|
||||
|
||||
scope_to ->(relation) { relation.where(semver_major: nil) }
|
||||
operation_name :backfill_catalog_resource_versions_sem_var
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
with_release_tag(sub_batch).each do |version|
|
||||
match_data = ::Gitlab::Regex.semver_regex.match(normalized_version_name(version.tag))
|
||||
|
||||
next unless match_data
|
||||
|
||||
version.update!(
|
||||
semver_major: match_data[1].to_i,
|
||||
semver_minor: match_data[2].to_i,
|
||||
semver_patch: match_data[3].to_i,
|
||||
semver_prerelease: match_data[4]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_release_tag(sub_batch)
|
||||
sub_batch
|
||||
.joins('INNER JOIN releases ON releases.id = catalog_resource_versions.release_id')
|
||||
.select('catalog_resource_versions.*, releases.tag')
|
||||
end
|
||||
|
||||
# Removes `v` prefix and converts partial or extended semver
|
||||
# numbers into a normalized format. Examples:
|
||||
#
|
||||
# 1 => 1.0.0
|
||||
# v1.2 => 1.2.0
|
||||
# 1.2-alpha => 1.2.0-alpha
|
||||
# 1.0+123 => 1.0.0+123
|
||||
# 1.2.3.4 => 1.2.3
|
||||
#
|
||||
def normalized_version_name(name)
|
||||
name = name.capitalize.delete_prefix('V')
|
||||
|
||||
first_part, *other_parts = name.split(/([-,+])/)
|
||||
dot_count = first_part.count('.')
|
||||
|
||||
return name unless dot_count != 2 && /^[\d.]+$/.match?(first_part)
|
||||
|
||||
if dot_count < 2
|
||||
add_zeroes = '.0' * (2 - dot_count)
|
||||
|
||||
[first_part, add_zeroes, *other_parts].join
|
||||
else
|
||||
# Assuming the format is 1.2.3.4, we only want to keep the first 3 digits.
|
||||
truncated_first_part = first_part.split(/([.])/)[0..4]
|
||||
|
||||
[truncated_first_part, *other_parts].join
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -43271,6 +43271,12 @@ msgstr ""
|
|||
msgid "Runners|Machine type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Machine type is required."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Machine type must have the format `family-series-size`. Example: n2d-standard-2"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Machine type with preset amounts of virtual machines processors (vCPUs) and memory"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43404,6 +43410,12 @@ msgstr ""
|
|||
msgid "Runners|Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Project ID is required."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Project ID must be 6 to 30 lowercase letters, digits, or hyphens. It needs to start with a lowercase letter and end with a letter or number."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Project runners"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43422,6 +43434,12 @@ msgstr ""
|
|||
msgid "Runners|Region"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Region is required."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Region must have the correct format. Example: us-central1"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Register"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43870,9 +43888,21 @@ msgstr ""
|
|||
msgid "Runners|You've created a new runner!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|You've registered a new runner!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Your runner is online and ready to run jobs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Zone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Zone is required."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Zone must have the correct format. Example: us-central1-a"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|active"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -51986,6 +52016,9 @@ msgstr ""
|
|||
msgid "This user is the author of this %{workItemType}."
|
||||
msgstr ""
|
||||
|
||||
msgid "This value is not valid."
|
||||
msgstr ""
|
||||
|
||||
msgid "This vulnerability was automatically resolved because its vulnerability type was disabled in this project or removed from GitLab's default ruleset. For details about SAST rule changes, see https://docs.gitlab.com/ee/user/application_security/sast/rules#important-rule-changes."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,9 @@ module QA
|
|||
# @param [Hash] args the existing args passed to method
|
||||
# @return [Hash] args or args with merged canary cookie if it exists
|
||||
def with_canary(args)
|
||||
canary_cookie = QA::Runtime::Env.canary_cookie
|
||||
return args if canary_cookie.empty?
|
||||
|
||||
args.deep_merge(cookies: QA::Runtime::Env.canary_cookie)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ module QA
|
|||
DEFAULT_TEST_PATTERN = "qa/specs/features/**/*_spec.rb"
|
||||
|
||||
class << self
|
||||
delegate :configure!, :move_regenerated_report, :download_report, :upload_report, to: :new
|
||||
delegate :configure!, :move_regenerated_report, :download_report, :upload_report, :merged_report, to: :new
|
||||
end
|
||||
|
||||
def initialize(report_name = nil)
|
||||
|
|
@ -106,6 +106,16 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
# Single merged knapsack report
|
||||
#
|
||||
# @return [Hash]
|
||||
def merged_report
|
||||
logger.info("Fetching all knapsack reports from GCS '#{BUCKET}' bucket")
|
||||
client.list_objects(BUCKET).items.each_with_object({}) do |report, hash|
|
||||
hash.merge!(JSON.parse(client.get_object(BUCKET, report.name)[:body]))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Setup knapsack logger
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Tools
|
||||
class KnapsackReportUpdater
|
||||
include Support::API
|
||||
include Ci::Helpers
|
||||
|
||||
GITLAB_PROJECT_ID = 278964
|
||||
UPDATE_BRANCH_NAME = "qa-knapsack-master-report-update"
|
||||
|
||||
def self.run
|
||||
new.update_master_report
|
||||
end
|
||||
|
||||
# Create master_report.json merge request
|
||||
#
|
||||
# @return [void]
|
||||
def update_master_report
|
||||
create_branch
|
||||
create_commit
|
||||
create_mr
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Gitlab access token
|
||||
#
|
||||
# @return [String]
|
||||
def gitlab_access_token
|
||||
@gitlab_access_token ||= ENV["GITLAB_ACCESS_TOKEN"] || raise("Missing GITLAB_ACCESS_TOKEN env variable")
|
||||
end
|
||||
|
||||
# Gitlab api url
|
||||
#
|
||||
# @return [String]
|
||||
def gitlab_api_url
|
||||
@gitlab_api_url ||= ENV["CI_API_V4_URL"] || raise("Missing CI_API_V4_URL env variable")
|
||||
end
|
||||
|
||||
# Create branch for knapsack report update
|
||||
#
|
||||
# @return [void]
|
||||
def create_branch
|
||||
logger.info("Creating branch '#{UPDATE_BRANCH_NAME}' branch")
|
||||
api_post("repository/branches", {
|
||||
branch: UPDATE_BRANCH_NAME,
|
||||
ref: "master"
|
||||
})
|
||||
end
|
||||
|
||||
# Create update commit for knapsack report
|
||||
#
|
||||
# @return [void]
|
||||
def create_commit
|
||||
logger.info("Creating master_report.json update commit")
|
||||
api_post("repository/commits", {
|
||||
branch: UPDATE_BRANCH_NAME,
|
||||
commit_message: "Update master_report.json for E2E tests",
|
||||
actions: [
|
||||
{
|
||||
action: "update",
|
||||
file_path: "qa/knapsack/master_report.json",
|
||||
content: JSON.pretty_generate(Support::KnapsackReport.merged_report.sort.to_h)
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
# Create merge request with updated knapsack master report
|
||||
#
|
||||
# @return [void]
|
||||
def create_mr
|
||||
logger.info("Creating merge request")
|
||||
resp = api_post("merge_requests", {
|
||||
source_branch: UPDATE_BRANCH_NAME,
|
||||
target_branch: "master",
|
||||
title: "Update master_report.json for E2E tests",
|
||||
remove_source_branch: true,
|
||||
squash: true,
|
||||
labels: "Quality,team::Test and Tools Infrastructure,type::maintenance,maintenance::pipelines",
|
||||
description: <<~DESCRIPTION
|
||||
Update fallback knapsack report with latest spec runtime data.
|
||||
|
||||
@gl-quality/qe-maintainers please review and merge.
|
||||
DESCRIPTION
|
||||
})
|
||||
|
||||
logger.info("Merge request created: #{resp[:web_url]}")
|
||||
end
|
||||
|
||||
# Api update request
|
||||
#
|
||||
# @param [String] path
|
||||
# @param [Hash] payload
|
||||
# @return [Hash, Array]
|
||||
def api_post(path, payload)
|
||||
response = post("#{gitlab_api_url}/projects/#{GITLAB_PROJECT_ID}/#{path}", payload, {
|
||||
headers: { "PRIVATE-TOKEN" => gitlab_access_token }
|
||||
})
|
||||
raise "Api request to #{path} failed! Body: #{response.body}" unless success?(response.code)
|
||||
|
||||
parse_body(response)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -25,7 +25,6 @@ RSpec.describe QA::Specs::Helpers::FastQuarantine do
|
|||
expect(RSpec).to have_received(:configure)
|
||||
expect(File).to have_received(:write).with(fq_path, fq_contents)
|
||||
expect(RestClient::Request).to have_received(:execute).with(
|
||||
cookies: {},
|
||||
method: :get,
|
||||
url: "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/rspec/fast_quarantine-gitlab.txt",
|
||||
verify_ssl: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe QA::Tools::KnapsackReportUpdater do
|
||||
include QA::Support::Helpers::StubEnv
|
||||
|
||||
let(:http_response) { instance_double("HTTPResponse", code: 200, body: {}.to_json) }
|
||||
let(:logger) { instance_double("Logger", info: nil) }
|
||||
let(:merged_report) { { spec_file: 0.0 } }
|
||||
let(:branch) { "qa-knapsack-master-report-update" }
|
||||
|
||||
def request_args(path, payload)
|
||||
{
|
||||
method: :post,
|
||||
url: "https://gitlab.com/api/v4/projects/278964/#{path}",
|
||||
payload: payload,
|
||||
verify_ssl: false,
|
||||
headers: { "PRIVATE-TOKEN" => "token" }
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(RestClient::Request).to receive(:execute).and_return(http_response)
|
||||
allow(QA::Support::KnapsackReport).to receive(:merged_report).and_return(merged_report)
|
||||
allow(Gitlab::QA::TestLogger).to receive(:logger).and_return(logger)
|
||||
|
||||
stub_env("CI_API_V4_URL", "https://gitlab.com/api/v4")
|
||||
stub_env("GITLAB_ACCESS_TOKEN", "token")
|
||||
end
|
||||
|
||||
it "creates master report merge request", :aggregate_failures do
|
||||
described_class.run
|
||||
|
||||
expect(RestClient::Request).to have_received(:execute).with(request_args("repository/branches", {
|
||||
branch: branch,
|
||||
ref: "master"
|
||||
}))
|
||||
expect(RestClient::Request).to have_received(:execute).with(request_args("repository/commits", {
|
||||
branch: branch,
|
||||
commit_message: "Update master_report.json for E2E tests",
|
||||
actions: [
|
||||
{
|
||||
action: "update",
|
||||
file_path: "qa/knapsack/master_report.json",
|
||||
content: JSON.pretty_generate(merged_report)
|
||||
}
|
||||
]
|
||||
}))
|
||||
expect(RestClient::Request).to have_received(:execute).with(request_args("merge_requests", {
|
||||
source_branch: branch,
|
||||
target_branch: "master",
|
||||
title: "Update master_report.json for E2E tests",
|
||||
remove_source_branch: true,
|
||||
squash: true,
|
||||
labels: "Quality,team::Test and Tools Infrastructure,type::maintenance,maintenance::pipelines",
|
||||
description: <<~DESCRIPTION
|
||||
Update fallback knapsack report with latest spec runtime data.
|
||||
|
||||
@gl-quality/qe-maintainers please review and merge.
|
||||
DESCRIPTION
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
|
@ -217,8 +217,7 @@ describe QA::Tools::ReliableReport do
|
|||
let(:common_api_args) do
|
||||
{
|
||||
verify_ssl: false,
|
||||
headers: { "PRIVATE-TOKEN" => "gitlab_token" },
|
||||
cookies: {}
|
||||
headers: { "PRIVATE-TOKEN" => "gitlab_token" }
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -69,4 +69,9 @@ namespace :knapsack do
|
|||
task :notify_long_running_specs do
|
||||
QA::Tools::LongRunningSpecReporter.execute
|
||||
end
|
||||
|
||||
desc "Update fallback knapsack report"
|
||||
task :update_fallback_report do
|
||||
QA::Tools::KnapsackReportUpdater.run
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import GoogleCloudFieldGroup from '~/ci/runner/components/registration/google_cloud_field_group.vue';
|
||||
|
||||
describe('GoogleCloudRegistrationInstructions', () => {
|
||||
let wrapper;
|
||||
|
||||
const findLabel = () => wrapper.find('label');
|
||||
const findInput = () => wrapper.find('input');
|
||||
|
||||
const createComponent = ({ props = {}, ...options } = {}) => {
|
||||
wrapper = mountExtended(GoogleCloudFieldGroup, {
|
||||
propsData: {
|
||||
name: 'field',
|
||||
...props,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const changeValue = (textInput, value) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
textInput.element.value = value;
|
||||
return textInput.trigger('change');
|
||||
};
|
||||
|
||||
it('shows a form label and field', () => {
|
||||
createComponent({
|
||||
props: { label: 'My field', name: 'myField' },
|
||||
});
|
||||
|
||||
expect(findLabel().attributes('for')).toBe('myField');
|
||||
expect(findLabel().text()).toBe('My field');
|
||||
|
||||
expect(findInput().attributes()).toMatchObject({
|
||||
id: 'myField',
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a value without updating it', () => {
|
||||
createComponent({
|
||||
props: { value: { state: true, value: 'Prefilled' } },
|
||||
});
|
||||
|
||||
expect(findInput().element.value).toEqual('Prefilled');
|
||||
expect(wrapper.emitted('change')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts arbitrary slots', () => {
|
||||
createComponent({
|
||||
slots: {
|
||||
label: '<strong>Label</strong>',
|
||||
description: '<div>Description</div>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(findLabel().html()).toContain('<strong>Label</strong>');
|
||||
expect(wrapper.html()).toContain('<div>Description</div>');
|
||||
});
|
||||
|
||||
describe('field validation', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
regexp: /^[a-z][0-9]$/,
|
||||
invalidFeedbackIfEmpty: 'Field is required.',
|
||||
invalidFeedbackIfMalformed: 'Field is incorrect.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('validates a missing value', async () => {
|
||||
await changeValue(findInput(), '');
|
||||
|
||||
expect(wrapper.emitted('change')).toEqual([[{ state: false, value: '' }]]);
|
||||
expect(wrapper.text()).toBe('Field is required.');
|
||||
expect(findInput().attributes('aria-invalid')).toBe('true');
|
||||
});
|
||||
|
||||
it('validates a wrong value', async () => {
|
||||
await changeValue(findInput(), '11');
|
||||
|
||||
expect(wrapper.emitted('change')).toEqual([[{ state: false, value: '11' }]]);
|
||||
expect(wrapper.text()).toBe('Field is incorrect.');
|
||||
expect(findInput().attributes('aria-invalid')).toBe('true');
|
||||
});
|
||||
|
||||
it('validates a correct value', async () => {
|
||||
await changeValue(findInput(), 'a1');
|
||||
|
||||
expect(wrapper.emitted('change')).toEqual([[{ state: true, value: 'a1' }]]);
|
||||
expect(wrapper.text()).toBe('');
|
||||
expect(findInput().attributes('aria-invalid')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { GlAlert } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { createWrapper } from '@vue/test-utils';
|
||||
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
|
@ -10,6 +11,7 @@ import GoogleCloudRegistrationInstructions from '~/ci/runner/components/registra
|
|||
import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
|
||||
import provisionGoogleCloudRunnerQueryProject from '~/ci/runner/graphql/register/provision_google_cloud_runner_project.query.graphql';
|
||||
import provisionGoogleCloudRunnerQueryGroup from '~/ci/runner/graphql/register/provision_google_cloud_runner_group.query.graphql';
|
||||
import { STATUS_ONLINE } from '~/ci/runner/constants';
|
||||
import {
|
||||
runnerForRegistration,
|
||||
mockAuthenticationToken,
|
||||
|
|
@ -35,6 +37,14 @@ const mockRunnerWithoutTokenResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
const mockRunnerOnlineResponse = {
|
||||
data: {
|
||||
runner: {
|
||||
...runnerForRegistration.data.runner,
|
||||
status: STATUS_ONLINE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockProjectRunnerCloudSteps = {
|
||||
data: {
|
||||
|
|
@ -61,23 +71,33 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
const findRegionInput = () => wrapper.findByTestId('region-input');
|
||||
const findZoneInput = () => wrapper.findByTestId('zone-input');
|
||||
const findMachineTypeInput = () => wrapper.findByTestId('machine-type-input');
|
||||
|
||||
const findProjectIdLink = () => wrapper.findByTestId('project-id-link');
|
||||
const findZoneLink = () => wrapper.findByTestId('zone-link');
|
||||
const findMachineTypeLink = () => wrapper.findByTestId('machine-types-link');
|
||||
const findToken = () => wrapper.findByTestId('runner-token');
|
||||
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
|
||||
const findModalTerrarformInstructions = () =>
|
||||
wrapper.findByTestId('terraform-script-instructions');
|
||||
const findModalTerrarformApplyInstructions = () =>
|
||||
wrapper.findByTestId('terraform-apply-instructions');
|
||||
const findModalBashInstructions = () => wrapper.findByTestId('bash-instructions');
|
||||
const findInstructionsButton = () => wrapper.findByTestId('show-instructions-button');
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findInstructionsButton = () => wrapper.findByTestId('show-instructions-button');
|
||||
|
||||
const getModal = () => extendedWrapper(createWrapper(document.querySelector('.gl-modal')));
|
||||
const findModalBashInstructions = () => getModal().findByTestId('bash-instructions');
|
||||
const findModalTerraformApplyInstructions = () =>
|
||||
getModal().findByTestId('terraform-apply-instructions');
|
||||
const findModalTerraformInstructions = () =>
|
||||
getModal().findByTestId('terraform-script-instructions');
|
||||
|
||||
const fillInTextField = (formGroup, value) => {
|
||||
const input = formGroup.find('input');
|
||||
input.element.value = value;
|
||||
return input.trigger('change');
|
||||
};
|
||||
|
||||
const fillInGoogleForm = () => {
|
||||
findProjectIdInput().vm.$emit('input', 'dev-gcp-xxx-integrati-xxxxxxxx');
|
||||
findRegionInput().vm.$emit('input', 'us-central1');
|
||||
findZoneInput().vm.$emit('input', 'us-central1');
|
||||
fillInTextField(findProjectIdInput(), 'dev-gcp-xxx-integrati-xxxxxxxx');
|
||||
fillInTextField(findRegionInput(), 'us-central1');
|
||||
fillInTextField(findZoneInput(), 'us-central1-a');
|
||||
fillInTextField(findMachineTypeInput(), 'n2d-standard-4');
|
||||
|
||||
findInstructionsButton().vm.$emit('click');
|
||||
|
||||
|
|
@ -86,6 +106,7 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
|
||||
const runnerWithTokenResolver = jest.fn().mockResolvedValue(mockRunnerResponse);
|
||||
const runnerWithoutTokenResolver = jest.fn().mockResolvedValue(mockRunnerWithoutTokenResponse);
|
||||
const runnerOnlineResolver = jest.fn().mockResolvedValue(mockRunnerOnlineResponse);
|
||||
const projectInstructionsResolver = jest.fn().mockResolvedValue(mockProjectRunnerCloudSteps);
|
||||
const groupInstructionsResolver = jest.fn().mockResolvedValue(mockGroupRunnerCloudSteps);
|
||||
|
||||
|
|
@ -98,16 +119,13 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
projectPath: 'test/project',
|
||||
};
|
||||
|
||||
const createComponent = (
|
||||
mountFn = shallowMountExtended,
|
||||
handlers = defaultHandlers,
|
||||
props = defaultProps,
|
||||
) => {
|
||||
wrapper = mountFn(GoogleCloudRegistrationInstructions, {
|
||||
const createComponent = (handlers = defaultHandlers, props = defaultProps) => {
|
||||
wrapper = mountExtended(GoogleCloudRegistrationInstructions, {
|
||||
apolloProvider: createMockApollo(handlers),
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -123,11 +141,11 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
it('machine type input has a default value', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findMachineTypeInput().attributes('value')).toBe('n2d-standard-2');
|
||||
expect(findMachineTypeInput().find('input').element.value).toEqual('n2d-standard-2');
|
||||
});
|
||||
|
||||
it('contains external docs links', () => {
|
||||
createComponent(mountExtended);
|
||||
createComponent();
|
||||
|
||||
expect(findProjectIdLink().attributes('href')).toBe(
|
||||
'https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects',
|
||||
|
|
@ -147,7 +165,7 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
});
|
||||
|
||||
it('displays runner token', async () => {
|
||||
createComponent(mountExtended);
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -158,7 +176,7 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
});
|
||||
|
||||
it('does not display runner token', async () => {
|
||||
createComponent(mountExtended, [[runnerForRegistrationQuery, runnerWithoutTokenResolver]]);
|
||||
createComponent([[runnerForRegistrationQuery, runnerWithoutTokenResolver]]);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -174,30 +192,22 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
await waitForPromises();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().text()).toBe('To view the setup instructions, complete the previous form.');
|
||||
|
||||
expect(findAlert().text()).toContain(
|
||||
'To view the setup instructions, complete the previous form.',
|
||||
);
|
||||
});
|
||||
|
||||
it('Hides an alert when the form is valid', async () => {
|
||||
createComponent(mountExtended, [
|
||||
[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver],
|
||||
]);
|
||||
createComponent([[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver]]);
|
||||
|
||||
findProjectIdInput().vm.$emit('input', 'dev-gcp-xxx-integrati-xxxxxxxx');
|
||||
findRegionInput().vm.$emit('input', 'us-central1');
|
||||
findZoneInput().vm.$emit('input', 'us-central1');
|
||||
findMachineTypeInput().vm.$emit('input', 'n2d-standard-2');
|
||||
|
||||
findInstructionsButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
await fillInGoogleForm();
|
||||
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('Shows a modal with the correspondent scripts for a project', async () => {
|
||||
createComponent(shallowMountExtended, [
|
||||
[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver],
|
||||
]);
|
||||
createComponent([[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver]]);
|
||||
|
||||
await fillInGoogleForm();
|
||||
|
||||
|
|
@ -205,16 +215,15 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
expect(groupInstructionsResolver).not.toHaveBeenCalled();
|
||||
|
||||
expect(findModalBashInstructions().text()).not.toBeNull();
|
||||
expect(findModalTerrarformInstructions().text()).not.toBeNull();
|
||||
expect(findModalTerrarformApplyInstructions().text).not.toBeNull();
|
||||
expect(findModalTerraformInstructions().text()).not.toBeNull();
|
||||
expect(findModalTerraformApplyInstructions().text()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('Shows a modal with the correspondent scripts for a group', async () => {
|
||||
createComponent(
|
||||
shallowMountExtended,
|
||||
[[provisionGoogleCloudRunnerQueryGroup, groupInstructionsResolver]],
|
||||
{ runnerId: mockRunnerId, groupPath: 'groups/test' },
|
||||
);
|
||||
createComponent([[provisionGoogleCloudRunnerQueryGroup, groupInstructionsResolver]], {
|
||||
runnerId: mockRunnerId,
|
||||
groupPath: 'groups/test',
|
||||
});
|
||||
|
||||
await fillInGoogleForm();
|
||||
|
||||
|
|
@ -222,27 +231,130 @@ describe('GoogleCloudRegistrationInstructions', () => {
|
|||
expect(projectInstructionsResolver).not.toHaveBeenCalled();
|
||||
|
||||
expect(findModalBashInstructions().text()).not.toBeNull();
|
||||
expect(findModalTerrarformInstructions().text()).not.toBeNull();
|
||||
expect(findModalTerrarformApplyInstructions().text).not.toBeNull();
|
||||
expect(findModalTerraformInstructions().text()).not.toBeNull();
|
||||
expect(findModalTerraformApplyInstructions().text()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('Does not display a modal with text when validation errors occur', async () => {
|
||||
createComponent(mountExtended, [[provisionGoogleCloudRunnerQueryProject, errorResolver]]);
|
||||
|
||||
findProjectIdInput().vm.$emit('input', 'dev-gcp-xxx-integrati-xxxxxxxx');
|
||||
findRegionInput().vm.$emit('input', 'us-central1xxx');
|
||||
findZoneInput().vm.$emit('input', 'us-central181263');
|
||||
it('Shows feedback when runner is online', async () => {
|
||||
createComponent([[runnerForRegistrationQuery, runnerOnlineResolver]]);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(errorResolver).toHaveBeenCalled();
|
||||
expect(runnerOnlineResolver).toHaveBeenCalledTimes(1);
|
||||
expect(runnerOnlineResolver).toHaveBeenCalledWith({
|
||||
id: expect.stringContaining(mockRunnerId),
|
||||
});
|
||||
|
||||
expect(findAlert().text()).toContain(
|
||||
'To view the setup instructions, make sure all form fields are completed and correct.',
|
||||
);
|
||||
expect(wrapper.text()).toContain('Your runner is online');
|
||||
});
|
||||
|
||||
expect(findModalBashInstructions().exists()).toBe(false);
|
||||
expect(findModalTerrarformInstructions().exists()).toBe(false);
|
||||
expect(findModalTerrarformApplyInstructions().exists()).toBe(false);
|
||||
describe('Field validation', () => {
|
||||
const expectValidation = (fieldGroup, { ariaInvalid, feedback }) => {
|
||||
expect(fieldGroup.attributes('aria-invalid')).toBe(ariaInvalid);
|
||||
expect(fieldGroup.find('input').attributes('aria-invalid')).toBe(ariaInvalid);
|
||||
expect(fieldGroup.text()).toContain(feedback);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('cloud project id validates', () => {
|
||||
it.each`
|
||||
case | input | ariaInvalid | feedback
|
||||
${'correct'} | ${'correct-project-name'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'correct-project-name-1'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'project'} | ${undefined} | ${''}
|
||||
${'invalid (too short)'} | ${'short'} | ${'true'} | ${'Project ID must be'}
|
||||
${'invalid (starts with a number)'} | ${'1number'} | ${'true'} | ${'Project ID must be'}
|
||||
${'invalid (starts with uppercase)'} | ${'Project'} | ${'true'} | ${'Project ID must be'}
|
||||
${'invalid (contains uppercase)'} | ${'pRoject'} | ${'true'} | ${'Project ID must be'}
|
||||
${'invalid (contains symbol)'} | ${'pro!ect'} | ${'true'} | ${'Project ID must be'}
|
||||
${'invalid (too long)'} | ${'a-project-name-that-is-too-long'} | ${'true'} | ${'Project ID must be'}
|
||||
${'invalid (ends with hyphen)'} | ${'a-project-'} | ${'true'} | ${'Project ID must be'}
|
||||
${'invalid (missing)'} | ${''} | ${'true'} | ${'Project ID is required'}
|
||||
`('"$input" as $case', async ({ input, ariaInvalid, feedback }) => {
|
||||
await fillInTextField(findProjectIdInput(), input);
|
||||
|
||||
expectValidation(findProjectIdInput(), { ariaInvalid, feedback });
|
||||
});
|
||||
});
|
||||
|
||||
describe('region validates', () => {
|
||||
it.each`
|
||||
case | input | ariaInvalid | feedback
|
||||
${'correct'} | ${'us-central1'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'europe-west8'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'moon-up99'} | ${undefined} | ${''}
|
||||
${'invalid (is zone)'} | ${'us-central1-a'} | ${'true'} | ${'Region must have'}
|
||||
${'invalid (one part)'} | ${'one2'} | ${'true'} | ${'Region must have'}
|
||||
${'invalid (three parts)'} | ${'one-two-three4'} | ${'true'} | ${'Region must have'}
|
||||
${'invalid (contains symbol)'} | ${'one!-two-three4'} | ${'true'} | ${'Region must have'}
|
||||
${'invalid (typo)'} | ${'one--two3'} | ${'true'} | ${'Region must have'}
|
||||
${'invalid (too short)'} | ${'wrong'} | ${'true'} | ${'Region must have'}
|
||||
${'invalid (missing)'} | ${''} | ${'true'} | ${'Region is required'}
|
||||
`('"$input" as $case', async ({ input, ariaInvalid, feedback }) => {
|
||||
await fillInTextField(findRegionInput(), input);
|
||||
|
||||
expectValidation(findRegionInput(), { ariaInvalid, feedback });
|
||||
});
|
||||
});
|
||||
|
||||
describe('zone validates', () => {
|
||||
it.each`
|
||||
case | input | ariaInvalid | feedback
|
||||
${'correct'} | ${'us-central1-a'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'europe-west8-b'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'moon-up99-z'} | ${undefined} | ${''}
|
||||
${'invalid (one part)'} | ${'one2-a'} | ${'true'} | ${'Zone must have'}
|
||||
${'invalid (three parts)'} | ${'one-two-three4-b'} | ${'true'} | ${'Zone must have'}
|
||||
${'invalid (contains symbol)'} | ${'one!-two-three4-c'} | ${'true'} | ${'Zone must have'}
|
||||
${'invalid (typo)'} | ${'one--two3-d'} | ${'true'} | ${'Zone must have'}
|
||||
${'invalid (too short)'} | ${'wrong'} | ${'true'} | ${'Zone must have'}
|
||||
${'invalid (missing)'} | ${''} | ${'true'} | ${'Zone is required'}
|
||||
`('"$input" as $case', async ({ input, ariaInvalid, feedback }) => {
|
||||
await fillInTextField(findZoneInput(), input);
|
||||
|
||||
expectValidation(findZoneInput(), { ariaInvalid, feedback });
|
||||
});
|
||||
});
|
||||
|
||||
describe('machine type validates', () => {
|
||||
it.each`
|
||||
case | input | ariaInvalid | feedback
|
||||
${'correct'} | ${'n2-standard-2'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'t2d-standard-1'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'t2a-standard-48'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'t2d-standard-1'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'c3-standard-4-lssd'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'f1-micro'} | ${undefined} | ${''}
|
||||
${'correct'} | ${'f1'} | ${undefined} | ${''}
|
||||
${'invalid (uppercase letter)'} | ${'N2-standard-2'} | ${'true'} | ${'Machine type must have'}
|
||||
${'invalid (number)'} | ${'22-standard-2'} | ${'true'} | ${'Machine type must have'}
|
||||
${'invalid (ends in dash)'} | ${'22-standard-2-'} | ${'true'} | ${'Machine type must have'}
|
||||
${'invalid (contains space)'} | ${'n2-standard-2 '} | ${'true'} | ${'Machine type must have'}
|
||||
${'invalid (missing)'} | ${''} | ${'true'} | ${'Machine type is required'}
|
||||
`('"$input" as $case', async ({ input, ariaInvalid, feedback }) => {
|
||||
await fillInTextField(findMachineTypeInput(), input);
|
||||
|
||||
expectValidation(findMachineTypeInput(), { ariaInvalid, feedback });
|
||||
});
|
||||
});
|
||||
|
||||
it('Does not display a modal with text when validation errors occur', async () => {
|
||||
createComponent([[provisionGoogleCloudRunnerQueryProject, errorResolver]]);
|
||||
|
||||
await fillInGoogleForm();
|
||||
|
||||
expect(errorResolver).toHaveBeenCalled();
|
||||
|
||||
expect(findAlert().text()).toContain(
|
||||
'To view the setup instructions, make sure all form fields are completed and correct.',
|
||||
);
|
||||
|
||||
expect(findModalBashInstructions().text()).toBe('');
|
||||
expect(findModalTerraformInstructions().text()).toBe('');
|
||||
expect(findModalTerraformApplyInstructions().text()).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawe
|
|||
import { runnerForRegistration } from '../mock_data';
|
||||
|
||||
const mockRunnerId = runnerForRegistration.data.runner.id;
|
||||
const mockGroupPath = '/groups/group1';
|
||||
const mockRunnersPath = '/groups/group1/-/runners';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
|
|
@ -40,6 +41,7 @@ describe('GroupRegisterRunnerApp', () => {
|
|||
propsData: {
|
||||
runnerId: mockRunnerId,
|
||||
runnersPath: mockRunnersPath,
|
||||
groupPath: mockGroupPath,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawe
|
|||
import { runnerForRegistration } from '../mock_data';
|
||||
|
||||
const mockRunnerId = runnerForRegistration.data.runner.id;
|
||||
const mockProjectPath = '/group1/project1';
|
||||
const mockRunnersPath = '/group1/project1/-/settings/ci_cd';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
|
|
@ -40,6 +41,7 @@ describe('ProjectRegisterRunnerApp', () => {
|
|||
propsData: {
|
||||
runnerId: mockRunnerId,
|
||||
runnersPath: mockRunnersPath,
|
||||
projectPath: mockProjectPath,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillCatalogResourceVersionSemVer, feature_category: :pipeline_composition do
|
||||
let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') }
|
||||
let(:project) { table(:projects).create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
|
||||
let(:resource) { table(:catalog_resources).create!(project_id: project.id) }
|
||||
|
||||
let(:releases_table) { table(:releases) }
|
||||
let(:versions_table) { table(:catalog_resource_versions) }
|
||||
|
||||
let(:tag_to_expected_semver) do
|
||||
{
|
||||
'2.0.5' => [2, 0, 5, nil],
|
||||
'1.2.3-alpha' => [1, 2, 3, 'alpha'],
|
||||
'4.5.6-beta+12345' => [4, 5, 6, 'beta'],
|
||||
'1' => [1, 0, 0, nil],
|
||||
'v1.2' => [1, 2, 0, nil],
|
||||
'1.3-alpha' => [1, 3, 0, 'alpha'],
|
||||
'4.0+123' => [4, 0, 0, nil],
|
||||
'0.5-beta+123' => [0, 5, 0, 'beta'],
|
||||
'1.2.3.4' => [1, 2, 3, nil],
|
||||
'v123.34.5.6-beta' => [123, 34, 5, 'beta'],
|
||||
'test-name' => [nil, nil, nil, nil],
|
||||
'semver_already_exists' => [2, 1, 0, nil]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
tag_to_expected_semver.each_key do |tag|
|
||||
releases_table.create!(tag: tag, released_at: Time.zone.now)
|
||||
end
|
||||
|
||||
releases_table.find_each do |release|
|
||||
versions_table.create!(release_id: release.id, catalog_resource_id: resource.id, project_id: project.id)
|
||||
end
|
||||
|
||||
# Pre-set the semver values for one version to ensure they're not modified by the backfill.
|
||||
release = releases_table.find_by(tag: 'semver_already_exists')
|
||||
versions_table.find_by(release_id: release.id).update!(semver_major: 2, semver_minor: 1, semver_patch: 0)
|
||||
end
|
||||
|
||||
subject(:perform_migration) do
|
||||
described_class.new(
|
||||
start_id: versions_table.minimum(:id),
|
||||
end_id: versions_table.maximum(:id),
|
||||
batch_table: :catalog_resource_versions,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 3,
|
||||
pause_ms: 0,
|
||||
connection: ActiveRecord::Base.connection
|
||||
).perform
|
||||
end
|
||||
|
||||
it 'updates the semver columns with the expected values' do
|
||||
perform_migration
|
||||
|
||||
query = versions_table
|
||||
.joins('INNER JOIN releases ON releases.id = catalog_resource_versions.release_id')
|
||||
.select('catalog_resource_versions.*, releases.tag')
|
||||
|
||||
results = query.each_with_object({}) do |row, obj|
|
||||
obj[row.tag] = [row.semver_major, row.semver_minor, row.semver_patch, row.semver_prerelease]
|
||||
end
|
||||
|
||||
expect(results).to eq(tag_to_expected_semver)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueBackfillCatalogResourceVersionSemVer, feature_category: :pipeline_composition do
|
||||
let!(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :catalog_resource_versions,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue