Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-04 21:09:55 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-04 21:09:55 +0300
commitae42530b1be0d25186881ae45c39bdf1122a84b9 (patch)
tree0592eb5b3b23d1dcd3b00bdb3b00f3b28412a291
parente0655935eb32ba057b6ced978940076681d71177 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/issue_templates/actionable_insight.md3
-rw-r--r--.rubocop_manual_todo.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue23
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js2
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js5
-rw-r--r--app/assets/javascripts/filtered_search/constants.js2
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js19
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue7
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_input.vue (renamed from app/assets/javascripts/registry/settings/components/expiration_textarea.vue)14
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_run_text.vue17
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_toggle.vue15
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue18
-rw-r--r--app/assets/javascripts/registry/settings/constants.js10
-rw-r--r--app/assets/javascripts/search_autocomplete.js4
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss4
-rw-r--r--app/graphql/mutations/environments/canary_ingress/update.rb39
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/delete.rb2
-rw-r--r--app/graphql/resolvers/ci/config_resolver.rb60
-rw-r--r--app/graphql/types/ci/config/config_type.rb21
-rw-r--r--app/graphql/types/ci/config/group_type.rb19
-rw-r--r--app/graphql/types/ci/config/job_type.rb21
-rw-r--r--app/graphql/types/ci/config/need_type.rb15
-rw-r--r--app/graphql/types/ci/config/stage_type.rb17
-rw-r--r--app/graphql/types/ci/config/status_enum.rb15
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/models/alert_management/alert.rb5
-rw-r--r--app/services/environments/canary_ingress/update_service.rb70
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml16
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/environments/canary_ingress/update_worker.rb22
-rw-r--r--changelogs/unreleased/212320-move-canary-ingress-to-core.yml5
-rw-r--r--changelogs/unreleased/233994_add_increment_counter_js_tracking.yml5
-rw-r--r--changelogs/unreleased/add-domain-type-to-alerts.yml5
-rw-r--r--changelogs/unreleased/lm-add-ci-config-0.yml5
-rw-r--r--db/migrate/20201203123524_add_domain_enum_to_alerts.rb19
-rw-r--r--db/migrate/20201203171631_add_index_to_domain.rb17
-rw-r--r--db/schema_migrations/202012031235241
-rw-r--r--db/schema_migrations/202012031716311
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/troubleshooting/kubernetes_cheat_sheet.md6
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql107
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json353
-rw-r--r--doc/api/graphql/reference/index.md48
-rw-r--r--doc/development/product_analytics/snowplow.md77
-rw-r--r--doc/development/product_analytics/usage_ping.md41
-rw-r--r--doc/user/group/saml_sso/index.md3
-rw-r--r--doc/user/project/canary_deployments.md5
-rw-r--r--lib/api/entities/project_import_status.rb5
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb21
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb2
-rw-r--r--spec/frontend/api_spec.js40
-rw-r--r--spec/frontend/diffs/components/settings_dropdown_spec.js4
-rw-r--r--spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap8
-rw-r--r--spec/frontend/registry/settings/components/expiration_input_spec.js (renamed from spec/frontend/registry/settings/components/expiration_textarea_spec.js)22
-rw-r--r--spec/frontend/registry/settings/components/expiration_run_text_spec.js34
-rw-r--r--spec/frontend/registry/settings/components/expiration_toggle_spec.js13
-rw-r--r--spec/frontend/registry/settings/components/settings_form_spec.js24
-rw-r--r--spec/graphql/features/authorization_spec.rb22
-rw-r--r--spec/graphql/features/feature_flag_spec.rb2
-rw-r--r--spec/graphql/mutations/environments/canary_ingress/update_spec.rb66
-rw-r--r--spec/graphql/resolvers/ci/config_resolver_spec.rb56
-rw-r--r--spec/graphql/types/ci/config/config_type_spec.rb18
-rw-r--r--spec/graphql/types/ci/config/group_type_spec.rb17
-rw-r--r--spec/graphql/types/ci/config/job_type_spec.rb18
-rw-r--r--spec/graphql/types/ci/config/need_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/config/stage_type_spec.rb16
-rw-r--r--spec/graphql/types/permission_types/base_permission_type_spec.rb6
-rw-r--r--spec/lib/gitlab/graphql/markdown_field_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb91
-rw-r--r--spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb45
-rw-r--r--spec/services/environments/canary_ingress/update_service_spec.rb139
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb3
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_includes.yml19
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb4
-rw-r--r--spec/support/helpers/graphql_helpers.rb8
-rw-r--r--spec/workers/environments/canary_ingress/update_worker_spec.rb33
80 files changed, 1794 insertions, 171 deletions
diff --git a/.gitlab/issue_templates/actionable_insight.md b/.gitlab/issue_templates/actionable_insight.md
index 68b2b153831..ff6a4f12918 100644
--- a/.gitlab/issue_templates/actionable_insight.md
+++ b/.gitlab/issue_templates/actionable_insight.md
@@ -31,5 +31,4 @@ Actionable insights always have a follow-up action that needs to take place as a
-
- /label ~"Actionable Insight"
+/label ~"Actionable Insight"
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 8bd1a345ffe..2103f352768 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -21,7 +21,6 @@ Graphql/IDType:
Exclude:
- 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'app/graphql/mutations/boards/issues/issue_move_list.rb'
- - 'app/graphql/mutations/metrics/dashboard/annotations/delete.rb'
- 'app/graphql/resolvers/design_management/design_at_version_resolver.rb'
- 'app/graphql/resolvers/design_management/design_resolver.rb'
- 'app/graphql/resolvers/design_management/designs_resolver.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a1046f7c062..ecf555d67ff 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-f5c6f6efe69a4c23fb1f10cebb66c47f90f1a70c
+2eb4db13dab06b87382582f5fcddab0c8397463e
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index f469f49ce20..f922d78a74d 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -69,6 +69,7 @@ const Api = {
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
tagsPath: '/api/:version/projects/:id/repository/tags',
freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
+ usageDataIncrementCounterPath: '/api/:version/usage_data/increment_counter',
usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
@@ -751,6 +752,19 @@ const Api = {
return axios.post(url, freezePeriod);
},
+ trackRedisCounterEvent(event) {
+ if (!gon.features?.usageDataApi) {
+ return null;
+ }
+
+ const url = Api.buildUrl(this.usageDataIncrementCounterPath);
+ const headers = {
+ 'Content-Type': 'application/json',
+ };
+
+ return axios.post(url, { event }, { headers });
+ },
+
trackRedisHllUserEvent(event) {
if (!gon.features?.usageDataApi) {
return null;
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
index b8904de8049..2fe2fd6b3d8 100644
--- a/app/assets/javascripts/diffs/components/settings_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -14,26 +14,10 @@ export default {
GlDropdown,
GlFormCheckbox,
},
- data() {
- return {
- checked: false,
- };
- },
computed: {
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapState('diffs', ['renderTreeList', 'showWhitespace', 'viewDiffsFileByFile']),
},
- watch: {
- viewDiffsFileByFile() {
- this.checked = this.viewDiffsFileByFile;
- },
- checked() {
- eventHub.$emit(EVT_VIEW_FILE_BY_FILE, { setting: this.checked });
- },
- },
- created() {
- this.checked = this.viewDiffsFileByFile;
- },
methods: {
...mapActions('diffs', [
'setInlineDiffViewType',
@@ -110,7 +94,12 @@ export default {
</label>
</div>
<div class="gl-mt-3 gl-px-3">
- <gl-form-checkbox v-model="checked" data-testid="file-by-file" class="gl-mb-0">
+ <gl-form-checkbox
+ data-testid="file-by-file"
+ class="gl-mb-0"
+ :checked="viewDiffsFileByFile"
+ @input="toggleFileByFile"
+ >
{{ $options.i18n.fileByFile }}
</gl-form-checkbox>
</div>
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 7d4df25816b..8899870be79 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -91,7 +91,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
],
};
- const tokenPosition = 2;
+ const tokenPosition = 3;
IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvedBy.token]);
IssuableTokenKeys.tokenKeysWithAlternative.splice(tokenPosition, 0, ...[approvedBy.token]);
IssuableTokenKeys.conditions.push(...approvedBy.condition);
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index d7645f96406..77491d1556b 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -71,6 +71,11 @@ export default class AvailableDropdownMappings {
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-assignee'),
},
+ reviewer: {
+ reference: null,
+ gl: DropdownUser,
+ element: this.container.querySelector('#js-dropdown-reviewer'),
+ },
'approved-by': {
reference: null,
gl: DropdownUser,
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
index 6cd6f9c9906..08736b09407 100644
--- a/app/assets/javascripts/filtered_search/constants.js
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -1,4 +1,4 @@
-export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by'];
+export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by', 'reviewer'];
export const DROPDOWN_TYPE = {
hint: 'hint',
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index d2ac80fa190..6e742e4ca02 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -22,6 +22,15 @@ export const tokenKeys = [
tag: '@assignee',
},
{
+ formattedKey: __('Reviewer'),
+ key: 'reviewer',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ icon: 'user',
+ tag: '@reviewer',
+ },
+ {
formattedKey: __('Milestone'),
key: 'milestone',
type: 'string',
@@ -86,6 +95,16 @@ export const conditions = flattenDeep(
value: __('Any'),
},
{
+ url: 'reviewer_id=None',
+ tokenKey: 'reviewer',
+ value: __('None'),
+ },
+ {
+ url: 'reviewer_id=Any',
+ tokenKey: 'reviewer',
+ value: __('Any'),
+ },
+ {
url: 'author_username=support-bot',
tokenKey: 'author',
value: 'support-bot',
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 258633e0125..203d6a12edd 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -47,15 +47,14 @@ export default {
class="dropdown-menu-toggle build-content gl-build-content"
>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
- <span class="gl-display-flex gl-align-items-center gl-w-90">
+ <span class="gl-display-flex gl-align-items-center gl-min-w-0">
<ci-icon :status="group.status" :size="24" />
-
- <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
+ <span class="gl-text-truncate mw-70p gl-pl-3">
{{ group.name }}
</span>
</span>
- <span class="gl-font-weight-100 gl-font-size-lg gl-pr-2"> {{ group.size }} </span>
+ <span class="gl-font-weight-100 gl-font-size-lg"> {{ group.size }} </span>
</div>
</button>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_textarea.vue b/app/assets/javascripts/registry/settings/components/expiration_input.vue
index 3f817e37dca..2dbd9d26f60 100644
--- a/app/assets/javascripts/registry/settings/components/expiration_textarea.vue
+++ b/app/assets/javascripts/registry/settings/components/expiration_input.vue
@@ -1,11 +1,11 @@
<script>
-import { GlFormGroup, GlFormTextarea, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlFormGroup, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { NAME_REGEX_LENGTH, TEXT_AREA_INVALID_FEEDBACK } from '../constants';
export default {
components: {
GlFormGroup,
- GlFormTextarea,
+ GlFormInput,
GlSprintf,
GlLink,
},
@@ -48,7 +48,7 @@ export default {
textAreaLengthErrorMessage() {
return this.isInputValid(this.value) ? '' : TEXT_AREA_INVALID_FEEDBACK;
},
- textAreaValidation() {
+ inputValidation() {
const nameRegexErrors = this.error || this.textAreaLengthErrorMessage;
return {
state: nameRegexErrors === null ? null : !nameRegexErrors,
@@ -77,8 +77,8 @@ export default {
<gl-form-group
:id="`${name}-form-group`"
:label-for="name"
- :state="textAreaValidation.state"
- :invalid-feedback="textAreaValidation.message"
+ :state="inputValidation.state"
+ :invalid-feedback="inputValidation.message"
>
<template #label>
<span data-testid="label">
@@ -89,11 +89,11 @@ export default {
</gl-sprintf>
</span>
</template>
- <gl-form-textarea
+ <gl-form-input
:id="name"
v-model="internalValue"
:placeholder="placeholder"
- :state="textAreaValidation.state"
+ :state="inputValidation.state"
:disabled="disabled"
trim
/>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_run_text.vue b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
index 186ad2f34b9..fd9ca6a54c5 100644
--- a/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
+++ b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
@@ -13,6 +13,16 @@ export default {
required: false,
default: NOT_SCHEDULED_POLICY_TEXT,
},
+ enabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ parsedValue() {
+ return this.enabled ? this.value : NOT_SCHEDULED_POLICY_TEXT;
+ },
},
i18n: {
NEXT_CLEANUP_LABEL,
@@ -26,6 +36,11 @@ export default {
:label="$options.i18n.NEXT_CLEANUP_LABEL"
label-for="expiration-policy-info-text"
>
- <gl-form-input id="expiration-policy-info-text" class="gl-pl-0!" plaintext :value="value" />
+ <gl-form-input
+ id="expiration-policy-info-text"
+ class="gl-pl-0!"
+ plaintext
+ :value="parsedValue"
+ />
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
index 9dabe8ac51a..7f045244926 100644
--- a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
+++ b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
@@ -1,6 +1,6 @@
<script>
import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui';
-import { ENABLED_TEXT, DISABLED_TEXT, ENABLE_TOGGLE_DESCRIPTION } from '../constants';
+import { ENABLED_TOGGLE_DESCRIPTION, DISABLED_TOGGLE_DESCRIPTION } from '../constants';
export default {
components: {
@@ -20,9 +20,6 @@ export default {
default: false,
},
},
- i18n: {
- ENABLE_TOGGLE_DESCRIPTION,
- },
computed: {
enabled: {
get() {
@@ -32,8 +29,8 @@ export default {
this.$emit('input', value);
},
},
- toggleStatusText() {
- return this.enabled ? ENABLED_TEXT : DISABLED_TEXT;
+ toggleText() {
+ return this.enabled ? ENABLED_TOGGLE_DESCRIPTION : DISABLED_TOGGLE_DESCRIPTION;
},
},
};
@@ -44,9 +41,9 @@ export default {
<div class="gl-display-flex">
<gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="disabled" />
<span class="gl-ml-5 gl-line-height-24" data-testid="description">
- <gl-sprintf :message="$options.i18n.ENABLE_TOGGLE_DESCRIPTION">
- <template #toggleStatus>
- <strong>{{ toggleStatusText }}</strong>
+ <gl-sprintf :message="toggleText">
+ <template #strong="{content}">
+ <strong>{{ content }}</strong>
</template>
</gl-sprintf>
</span>
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index d0a8081e455..3eab7e6d038 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -25,7 +25,7 @@ import { formOptionsGenerator } from '~/registry/shared/utils';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
import ExpirationDropdown from './expiration_dropdown.vue';
-import ExpirationTextarea from './expiration_textarea.vue';
+import ExpirationInput from './expiration_input.vue';
import ExpirationToggle from './expiration_toggle.vue';
import ExpirationRunText from './expiration_run_text.vue';
@@ -35,7 +35,7 @@ export default {
GlButton,
GlSprintf,
ExpirationDropdown,
- ExpirationTextarea,
+ ExpirationInput,
ExpirationToggle,
ExpirationRunText,
},
@@ -202,7 +202,11 @@ export default {
data-testid="cadence-dropdown"
@input="onModelChange($event, 'cadence')"
/>
- <expiration-run-text :value="prefilledForm.nextRunAt" class="gl-mb-0!" />
+ <expiration-run-text
+ :value="prefilledForm.nextRunAt"
+ :enabled="prefilledForm.enabled"
+ class="gl-mb-0!"
+ />
</div>
<gl-card class="gl-mt-7">
<template #header>
@@ -229,14 +233,14 @@ export default {
data-testid="keep-n-dropdown"
@input="onModelChange($event, 'keepN')"
/>
- <expiration-textarea
+ <expiration-input
v-model="prefilledForm.nameRegexKeep"
:error="apiErrors.nameRegexKeep"
:disabled="isFieldDisabled"
:label="$options.i18n.NAME_REGEX_KEEP_LABEL"
:description="$options.i18n.NAME_REGEX_KEEP_DESCRIPTION"
name="keep-regex"
- data-testid="keep-regex-textarea"
+ data-testid="keep-regex-input"
@input="onModelChange($event, 'nameRegexKeep')"
@validation="setLocalErrors($event, 'nameRegexKeep')"
/>
@@ -268,7 +272,7 @@ export default {
data-testid="older-than-dropdown"
@input="onModelChange($event, 'olderThan')"
/>
- <expiration-textarea
+ <expiration-input
v-model="prefilledForm.nameRegex"
:error="apiErrors.nameRegex"
:disabled="isFieldDisabled"
@@ -276,7 +280,7 @@ export default {
:placeholder="$options.i18n.NAME_REGEX_PLACEHOLDER"
:description="$options.i18n.NAME_REGEX_DESCRIPTION"
name="remove-regex"
- data-testid="remove-regex-textarea"
+ data-testid="remove-regex-input"
@input="onModelChange($event, 'nameRegex')"
@validation="setLocalErrors($event, 'nameRegex')"
/>
diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/registry/settings/constants.js
index ba5820196ff..1dd533ce665 100644
--- a/app/assets/javascripts/registry/settings/constants.js
+++ b/app/assets/javascripts/registry/settings/constants.js
@@ -37,11 +37,11 @@ export const NAME_REGEX_DESCRIPTION = s__(
'ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}',
);
-export const ENABLED_TEXT = __('Enabled');
-export const DISABLED_TEXT = __('Disabled');
-
-export const ENABLE_TOGGLE_DESCRIPTION = s__(
- 'ContainerRegistry|%{toggleStatus} - Tags that match the rules on this page are automatically scheduled for deletion.',
+export const ENABLED_TOGGLE_DESCRIPTION = s__(
+ 'ContainerRegistry|%{strongStart}Enabled%{strongEnd} - Tags that match the rules on this page are automatically scheduled for deletion.',
+);
+export const DISABLED_TOGGLE_DESCRIPTION = s__(
+ 'ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted.',
);
export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup:');
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 7073b9ca12d..97674348436 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -250,6 +250,10 @@ export class SearchAutocomplete {
url: `${mrPath}/?assignee_username=${userName}`,
},
{
+ text: s__("SearchAutocomplete|Merge requests that I'm a reviewer"),
+ url: `${mrPath}/?reviewer_username=${userName}`,
+ },
+ {
text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_username=${userName}`,
},
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 502f5b08522..7b424882ffa 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -139,10 +139,6 @@
width: 186px;
}
-.gl-w-90 {
- width: 90%;
-}
-
.gl-build-content {
@include build-content();
}
diff --git a/app/graphql/mutations/environments/canary_ingress/update.rb b/app/graphql/mutations/environments/canary_ingress/update.rb
new file mode 100644
index 00000000000..1798143053a
--- /dev/null
+++ b/app/graphql/mutations/environments/canary_ingress/update.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Environments
+ module CanaryIngress
+ class Update < ::Mutations::BaseMutation
+ graphql_name 'EnvironmentsCanaryIngressUpdate'
+
+ authorize :update_environment
+
+ argument :id,
+ ::Types::GlobalIDType[::Environment],
+ required: true,
+ description: 'The global ID of the environment to update'
+
+ argument :weight,
+ GraphQL::INT_TYPE,
+ required: true,
+ description: 'The weight of the Canary Ingress'
+
+ def resolve(id:, **kwargs)
+ environment = authorized_find!(id: id)
+
+ result = ::Environments::CanaryIngress::UpdateService
+ .new(environment.project, current_user, kwargs)
+ .execute_async(environment)
+
+ { errors: Array.wrap(result[:message]) }
+ end
+
+ def find_object(id:)
+ # TODO: remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ::Types::GlobalIDType[::Environment].coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
index d6731dfcafd..5d6763d8711 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
@@ -11,7 +11,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Metrics::Dashboard::Annotation],
required: true,
- description: 'The global ID of the annotation to delete'
+ description: 'Global ID of the annotation to delete'
def resolve(id:)
annotation = authorized_find!(id: id)
diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb
new file mode 100644
index 00000000000..d6e7c206691
--- /dev/null
+++ b/app/graphql/resolvers/ci/config_resolver.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class ConfigResolver < BaseResolver
+ type Types::Ci::Config::ConfigType, null: true
+
+ argument :content, GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Contents of .gitlab-ci.yml'
+
+ def resolve(content:)
+ result = ::Gitlab::Ci::YamlProcessor.new(content).execute
+
+ response = if result.errors.empty?
+ {
+ status: :valid,
+ errors: [],
+ stages: make_stages(result.jobs)
+ }
+ else
+ {
+ status: :invalid,
+ errors: result.errors
+ }
+ end
+
+ response.merge(merged_yaml: result.merged_yaml)
+ end
+
+ private
+
+ def make_jobs(config_jobs)
+ config_jobs.map do |job_name, job|
+ {
+ name: job_name,
+ stage: job[:stage],
+ group_name: CommitStatus.new(name: job_name).group_name,
+ needs: job.dig(:needs, :job) || []
+ }
+ end
+ end
+
+ def make_groups(job_data)
+ jobs = make_jobs(job_data)
+
+ jobs_by_group = jobs.group_by { |job| job[:group_name] }
+ jobs_by_group.map do |name, jobs|
+ { jobs: jobs, name: name, stage: jobs.first[:stage], size: jobs.size }
+ end
+ end
+
+ def make_stages(jobs)
+ make_groups(jobs)
+ .group_by { |group| group[:stage] }
+ .map { |name, groups| { name: name, groups: groups } }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/config_type.rb b/app/graphql/types/ci/config/config_type.rb
new file mode 100644
index 00000000000..e54b345f3d3
--- /dev/null
+++ b/app/graphql/types/ci/config/config_type.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ module Config
+ class ConfigType < BaseObject
+ graphql_name 'CiConfig'
+
+ field :errors, [GraphQL::STRING_TYPE], null: true,
+ description: 'Linting errors'
+ field :merged_yaml, GraphQL::STRING_TYPE, null: true,
+ description: 'Merged CI config YAML'
+ field :stages, [Types::Ci::Config::StageType], null: true,
+ description: 'Stages of the pipeline'
+ field :status, Types::Ci::Config::StatusEnum, null: true,
+ description: 'Status of linting, can be either valid or invalid'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/group_type.rb b/app/graphql/types/ci/config/group_type.rb
new file mode 100644
index 00000000000..8b0db2934a4
--- /dev/null
+++ b/app/graphql/types/ci/config/group_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ module Config
+ class GroupType < BaseObject
+ graphql_name 'CiConfigGroup'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job group'
+ field :jobs, [Types::Ci::Config::JobType], null: true,
+ description: 'Jobs in group'
+ field :size, GraphQL::INT_TYPE, null: true,
+ description: 'Size of the job group'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/job_type.rb b/app/graphql/types/ci/config/job_type.rb
new file mode 100644
index 00000000000..59bcbd9ef49
--- /dev/null
+++ b/app/graphql/types/ci/config/job_type.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ module Config
+ class JobType < BaseObject
+ graphql_name 'CiConfigJob'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job'
+ field :group_name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job group'
+ field :stage, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job stage'
+ field :needs, [Types::Ci::Config::NeedType], null: true,
+ description: 'Builds that must complete before the jobs run'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/need_type.rb b/app/graphql/types/ci/config/need_type.rb
new file mode 100644
index 00000000000..a442450b9ae
--- /dev/null
+++ b/app/graphql/types/ci/config/need_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ module Config
+ class NeedType < BaseObject
+ graphql_name 'CiConfigNeed'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the need'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/stage_type.rb b/app/graphql/types/ci/config/stage_type.rb
new file mode 100644
index 00000000000..20618bc41f8
--- /dev/null
+++ b/app/graphql/types/ci/config/stage_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ module Config
+ class StageType < BaseObject
+ graphql_name 'CiConfigStage'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the stage'
+ field :groups, [Types::Ci::Config::GroupType], null: true,
+ description: 'Groups of jobs for the stage'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/status_enum.rb b/app/graphql/types/ci/config/status_enum.rb
new file mode 100644
index 00000000000..92b04c61679
--- /dev/null
+++ b/app/graphql/types/ci/config/status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Config
+ class StatusEnum < BaseEnum
+ graphql_name 'CiConfigStatus'
+ description 'Values for YAML processor result'
+
+ value 'VALID', 'The configuration file is valid', value: :valid
+ value 'INVALID', 'The configuration file is not valid', value: :invalid
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 93c650c0b97..4c9070e4d5a 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -31,6 +31,7 @@ module Types
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
mount_mutation Mutations::Discussions::ToggleResolve
+ mount_mutation Mutations::Environments::CanaryIngress::Update
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetConfidential
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 75ff6f8deca..05bb371088c 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -91,6 +91,11 @@ module Types
description: 'Get runner setup instructions',
resolver: Resolvers::Ci::RunnerSetupResolver
+ field :ci_config, Types::Ci::Config::ConfigType, null: true,
+ description: 'Get linted and processed contents of a CI config. Should not be requested more than once per request.',
+ resolver: Resolvers::Ci::ConfigResolver,
+ complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1
+
def design_management
DesignManagementObject.new(nil)
end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index f7f7bb99b5a..fa9b9fc9e2d 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -69,6 +69,11 @@ module AlertManagement
unknown: 5
}
+ enum domain: {
+ operations: 0,
+ threat_monitoring: 1
+ }
+
state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered]
diff --git a/app/services/environments/canary_ingress/update_service.rb b/app/services/environments/canary_ingress/update_service.rb
new file mode 100644
index 00000000000..474c3de23d9
--- /dev/null
+++ b/app/services/environments/canary_ingress/update_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Environments
+ module CanaryIngress
+ class UpdateService < ::BaseService
+ def execute_async(environment)
+ result = validate(environment)
+
+ return result unless result[:status] == :success
+
+ Environments::CanaryIngress::UpdateWorker.perform_async(environment.id, params)
+
+ success
+ end
+
+ # This method actually executes the PATCH request to Kubernetes,
+ # that is used by internal processes i.e. sidekiq worker.
+ # You should always use `execute_async` to properly validate user's requests.
+ def execute(environment)
+ canary_ingress = environment.ingresses&.find(&:canary?)
+
+ unless canary_ingress.present?
+ return error(_('Canary Ingress does not exist in the environment.'))
+ end
+
+ if environment.patch_ingress(canary_ingress, patch_data)
+ success
+ else
+ error(_('Failed to update the Canary Ingress.'), :bad_request)
+ end
+ end
+
+ private
+
+ def validate(environment)
+ unless Feature.enabled?(:canary_ingress_weight_control, environment.project, default_enabled: true)
+ return error(_("Feature flag is not enabled on the environment's project."))
+ end
+
+ unless can?(current_user, :update_environment, environment)
+ return error(_('You do not have permission to update the environment.'))
+ end
+
+ unless params[:weight].is_a?(Integer) && (0..100).cover?(params[:weight])
+ return error(_('Canary weight must be specified and valid range (0..100).'))
+ end
+
+ if environment.has_running_deployments?
+ return error(_('There are running deployments on the environment. Please retry later.'))
+ end
+
+ if ::Gitlab::ApplicationRateLimiter.throttled?(:update_environment_canary_ingress, scope: [environment])
+ return error(_("This environment's canary ingress has been updated recently. Please retry later."))
+ end
+
+ success
+ end
+
+ def patch_data
+ {
+ metadata: {
+ annotations: {
+ Gitlab::Kubernetes::Ingress::ANNOTATION_KEY_CANARY_WEIGHT => params[:weight].to_s
+ }
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 5df7ade8609..79d86500bd9 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -75,6 +75,22 @@
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
+ #js-dropdown-reviewer.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
+ - if current_user
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ = render 'shared/issuable/user_dropdown_item',
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
= render_if_exists 'shared/issuable/approver_dropdown'
= render_if_exists 'shared/issuable/approved_by_dropdown'
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 0aa27c0d208..416c10e46fe 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1537,6 +1537,14 @@
:weight: 2
:idempotent:
:tags: []
+- :name: environments_canary_ingress_update
+ :feature_category: :continuous_delivery
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: error_tracking_issue_link
:feature_category: :error_tracking
:has_external_dependencies: true
diff --git a/app/workers/environments/canary_ingress/update_worker.rb b/app/workers/environments/canary_ingress/update_worker.rb
new file mode 100644
index 00000000000..53cc38e9eec
--- /dev/null
+++ b/app/workers/environments/canary_ingress/update_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Environments
+ module CanaryIngress
+ class UpdateWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: false
+ idempotent!
+ worker_has_external_dependencies!
+ feature_category :continuous_delivery
+
+ def perform(environment_id, params)
+ Environment.find_by_id(environment_id).try do |environment|
+ Environments::CanaryIngress::UpdateService
+ .new(environment.project, nil, params.with_indifferent_access)
+ .execute(environment)
+ end
+ end
+ end
+ end
+end
diff --git a/changelogs/unreleased/212320-move-canary-ingress-to-core.yml b/changelogs/unreleased/212320-move-canary-ingress-to-core.yml
new file mode 100644
index 00000000000..d35b927573e
--- /dev/null
+++ b/changelogs/unreleased/212320-move-canary-ingress-to-core.yml
@@ -0,0 +1,5 @@
+---
+title: Move CanaryIngress to core
+merge_request: 48836
+author:
+type: changed
diff --git a/changelogs/unreleased/233994_add_increment_counter_js_tracking.yml b/changelogs/unreleased/233994_add_increment_counter_js_tracking.yml
new file mode 100644
index 00000000000..19ff6938535
--- /dev/null
+++ b/changelogs/unreleased/233994_add_increment_counter_js_tracking.yml
@@ -0,0 +1,5 @@
+---
+title: Frontend client for increment_counter API
+merge_request: 47622
+author:
+type: added
diff --git a/changelogs/unreleased/add-domain-type-to-alerts.yml b/changelogs/unreleased/add-domain-type-to-alerts.yml
new file mode 100644
index 00000000000..eb95628ecad
--- /dev/null
+++ b/changelogs/unreleased/add-domain-type-to-alerts.yml
@@ -0,0 +1,5 @@
+---
+title: Add domain column to alerts table
+merge_request: 49120
+author:
+type: added
diff --git a/changelogs/unreleased/lm-add-ci-config-0.yml b/changelogs/unreleased/lm-add-ci-config-0.yml
new file mode 100644
index 00000000000..7bda30712a6
--- /dev/null
+++ b/changelogs/unreleased/lm-add-ci-config-0.yml
@@ -0,0 +1,5 @@
+---
+title: 'Expose GraphQL resolver for processing CI config'
+merge_request: 46912
+author:
+type: added
diff --git a/db/migrate/20201203123524_add_domain_enum_to_alerts.rb b/db/migrate/20201203123524_add_domain_enum_to_alerts.rb
new file mode 100644
index 00000000000..f1dec91a346
--- /dev/null
+++ b/db/migrate/20201203123524_add_domain_enum_to_alerts.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddDomainEnumToAlerts < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_column :alert_management_alerts, :domain, :integer, limit: 2, default: 0
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :alert_management_alerts, :domain, :integer, limit: 2
+ end
+ end
+end
diff --git a/db/migrate/20201203171631_add_index_to_domain.rb b/db/migrate/20201203171631_add_index_to_domain.rb
new file mode 100644
index 00000000000..dc7b9539e95
--- /dev/null
+++ b/db/migrate/20201203171631_add_index_to_domain.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToDomain < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+ INDEX_NAME = 'index_alert_management_alerts_on_domain'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :alert_management_alerts, :domain, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index :alert_management_alerts, :domain, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20201203123524 b/db/schema_migrations/20201203123524
new file mode 100644
index 00000000000..27f47a237c0
--- /dev/null
+++ b/db/schema_migrations/20201203123524
@@ -0,0 +1 @@
+4bb54293c339e20082a739f7724b02141d8fb3b0b140e21ac2acab6cbd2d2f01 \ No newline at end of file
diff --git a/db/schema_migrations/20201203171631 b/db/schema_migrations/20201203171631
new file mode 100644
index 00000000000..e93633344b9
--- /dev/null
+++ b/db/schema_migrations/20201203171631
@@ -0,0 +1 @@
+3b6d3fb9c279f5e8c76921e654b188a5a5ba0fddd7ff753a03706b41f43240ed \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 385183bb176..76f6f1561f8 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -8811,6 +8811,7 @@ CREATE TABLE alert_management_alerts (
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
prometheus_alert_id integer,
environment_id integer,
+ domain smallint DEFAULT 0,
CONSTRAINT check_2df3e2fdc1 CHECK ((char_length(monitoring_tool) <= 100)),
CONSTRAINT check_5e9e57cadb CHECK ((char_length(description) <= 1000)),
CONSTRAINT check_bac14dddde CHECK ((char_length(service) <= 100)),
@@ -20435,6 +20436,8 @@ CREATE INDEX index_alert_assignees_on_alert_id ON alert_management_alert_assigne
CREATE UNIQUE INDEX index_alert_assignees_on_user_id_and_alert_id ON alert_management_alert_assignees USING btree (user_id, alert_id);
+CREATE INDEX index_alert_management_alerts_on_domain ON alert_management_alerts USING btree (domain);
+
CREATE INDEX index_alert_management_alerts_on_environment_id ON alert_management_alerts USING btree (environment_id) WHERE (environment_id IS NOT NULL);
CREATE INDEX index_alert_management_alerts_on_issue_id ON alert_management_alerts USING btree (issue_id);
diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
index 1cb3afcc3af..aa5e440b8b1 100644
--- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
+++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
@@ -126,7 +126,7 @@ and they will assist you with any issues you are having.
kubectl get pods | grep task-runner
# enter it
- kubectl exec -it <task-runner-pod-name> bash
+ kubectl exec -it <task-runner-pod-name> -- bash
# open rails console
# rails console can be also called from other GitLab pods
@@ -139,10 +139,10 @@ and they will assist you with any issues you are having.
/usr/local/bin/gitlab-rake gitlab:check
# open console without entering pod
- kubectl exec -it <task-runner-pod-name> /srv/gitlab/bin/rails console
+ kubectl exec -it <task-runner-pod-name> -- /srv/gitlab/bin/rails console
# check the status of DB migrations
- kubectl exec -it <task-runner-pod-name> /usr/local/bin/gitlab-rake db:migrate:status
+ kubectl exec -it <task-runner-pod-name> -- /usr/local/bin/gitlab-rake db:migrate:status
```
You can also use `gitlab-rake`, instead of `/usr/local/bin/gitlab-rake`.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index f7a1cff7afd..be9d1d8e765 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2237,6 +2237,101 @@ type BurnupChartDailyTotals {
scopeWeight: Int!
}
+type CiConfig {
+ """
+ Linting errors
+ """
+ errors: [String!]
+
+ """
+ Merged CI config YAML
+ """
+ mergedYaml: String
+
+ """
+ Stages of the pipeline
+ """
+ stages: [CiConfigStage!]
+
+ """
+ Status of linting, can be either valid or invalid
+ """
+ status: CiConfigStatus
+}
+
+type CiConfigGroup {
+ """
+ Jobs in group
+ """
+ jobs: [CiConfigJob!]
+
+ """
+ Name of the job group
+ """
+ name: String
+
+ """
+ Size of the job group
+ """
+ size: Int
+}
+
+type CiConfigJob {
+ """
+ Name of the job group
+ """
+ groupName: String
+
+ """
+ Name of the job
+ """
+ name: String
+
+ """
+ Builds that must complete before the jobs run
+ """
+ needs: [CiConfigNeed!]
+
+ """
+ Name of the job stage
+ """
+ stage: String
+}
+
+type CiConfigNeed {
+ """
+ Name of the need
+ """
+ name: String
+}
+
+type CiConfigStage {
+ """
+ Groups of jobs for the stage
+ """
+ groups: [CiConfigGroup!]
+
+ """
+ Name of the stage
+ """
+ name: String
+}
+
+"""
+Values for YAML processor result
+"""
+enum CiConfigStatus {
+ """
+ The configuration file is not valid
+ """
+ INVALID
+
+ """
+ The configuration file is valid
+ """
+ VALID
+}
+
type CiGroup {
"""
Detailed status of the group
@@ -5415,7 +5510,7 @@ input DeleteAnnotationInput {
clientMutationId: String
"""
- The global ID of the annotation to delete
+ Global ID of the annotation to delete
"""
id: MetricsDashboardAnnotationID!
}
@@ -17906,6 +18001,16 @@ type PromoteToEpicPayload {
type Query {
"""
+ Get linted and processed contents of a CI config. Should not be requested more than once per request.
+ """
+ ciConfig(
+ """
+ Contents of .gitlab-ci.yml
+ """
+ content: String!
+ ): CiConfig
+
+ """
Find a container repository
"""
containerRepository(
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 2a10f750dfb..20398bac168 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -6002,6 +6002,330 @@
},
{
"kind": "OBJECT",
+ "name": "CiConfig",
+ "description": null,
+ "fields": [
+ {
+ "name": "errors",
+ "description": "Linting errors",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergedYaml",
+ "description": "Merged CI config YAML",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "stages",
+ "description": "Stages of the pipeline",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiConfigStage",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "status",
+ "description": "Status of linting, can be either valid or invalid",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "CiConfigStatus",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiConfigGroup",
+ "description": null,
+ "fields": [
+ {
+ "name": "jobs",
+ "description": "Jobs in group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiConfigJob",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the job group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "size",
+ "description": "Size of the job group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiConfigJob",
+ "description": null,
+ "fields": [
+ {
+ "name": "groupName",
+ "description": "Name of the job group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the job",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "needs",
+ "description": "Builds that must complete before the jobs run",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiConfigNeed",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "stage",
+ "description": "Name of the job stage",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiConfigNeed",
+ "description": null,
+ "fields": [
+ {
+ "name": "name",
+ "description": "Name of the need",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiConfigStage",
+ "description": null,
+ "fields": [
+ {
+ "name": "groups",
+ "description": "Groups of jobs for the stage",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiConfigGroup",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the stage",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "CiConfigStatus",
+ "description": "Values for YAML processor result",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "VALID",
+ "description": "The configuration file is valid",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INVALID",
+ "description": "The configuration file is not valid",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "CiGroup",
"description": null,
"fields": [
@@ -14822,7 +15146,7 @@
"inputFields": [
{
"name": "id",
- "description": "The global ID of the annotation to delete",
+ "description": "Global ID of the annotation to delete",
"type": {
"kind": "NON_NULL",
"name": null,
@@ -52315,6 +52639,33 @@
"description": null,
"fields": [
{
+ "name": "ciConfig",
+ "description": "Get linted and processed contents of a CI config. Should not be requested more than once per request.",
+ "args": [
+ {
+ "name": "content",
+ "description": "Contents of .gitlab-ci.yml",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiConfig",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "containerRepository",
"description": "Find a container repository",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index eb3661854d8..fa568cb9452 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -367,6 +367,45 @@ Represents the total number of issues and their weights for a particular day.
| `scopeCount` | Int! | Number of issues as of this day |
| `scopeWeight` | Int! | Total weight of issues as of this day |
+### CiConfig
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `errors` | String! => Array | Linting errors |
+| `mergedYaml` | String | Merged CI config YAML |
+| `stages` | CiConfigStage! => Array | Stages of the pipeline |
+| `status` | CiConfigStatus | Status of linting, can be either valid or invalid |
+
+### CiConfigGroup
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `jobs` | CiConfigJob! => Array | Jobs in group |
+| `name` | String | Name of the job group |
+| `size` | Int | Size of the job group |
+
+### CiConfigJob
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `groupName` | String | Name of the job group |
+| `name` | String | Name of the job |
+| `needs` | CiConfigNeed! => Array | Builds that must complete before the jobs run |
+| `stage` | String | Name of the job stage |
+
+### CiConfigNeed
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `name` | String | Name of the need |
+
+### CiConfigStage
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `groups` | CiConfigGroup! => Array | Groups of jobs for the stage |
+| `name` | String | Name of the stage |
+
### CiGroup
| Field | Type | Description |
@@ -3925,6 +3964,15 @@ Types of blob viewers.
| `rich` | |
| `simple` | |
+### CiConfigStatus
+
+Values for YAML processor result.
+
+| Value | Description |
+| ----- | ----------- |
+| `INVALID` | The configuration file is not valid |
+| `VALID` | The configuration file is valid |
+
### CommitActionMode
Mode of a commit action.
diff --git a/doc/development/product_analytics/snowplow.md b/doc/development/product_analytics/snowplow.md
index b864e48fa5b..43ffaf45098 100644
--- a/doc/development/product_analytics/snowplow.md
+++ b/doc/development/product_analytics/snowplow.md
@@ -370,48 +370,79 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event
- Look at the [Snowplow Micro repository](https://github.com/snowplow-incubator/snowplow-micro)
- Watch our [installation guide recording](https://www.youtube.com/watch?v=OX46fo_A0Ag)
-1. Install [Snowplow Micro](https://github.com/snowplow-incubator/snowplow-micro):
+1. Ensure Docker is installed and running.
- ```shell
- docker run --mount type=bind,source=$(pwd)/example,destination=/config -p 9090:9090 snowplow/snowplow-micro:latest --collector-config /config/micro.conf --iglu /config/iglu.json
- ```
-
-1. Install Snowplow Micro by cloning the settings in [this project](https://gitlab.com/gitlab-org/snowplow-micro-configuration):
+1. Install [Snowplow Micro](https://github.com/snowplow-incubator/snowplow-micro) by cloning the settings in [this project](https://gitlab.com/gitlab-org/snowplow-micro-configuration):
+1. Navigate to the directory with the cloned project, and start the appropriate Docker
+ container with the following script:
```shell
- git clone git@gitlab.com:gitlab-org/snowplow-micro-configuration.git
./snowplow-micro.sh
```
-1. Update port in SQL to set `9090`:
+1. Update your instance's settings to enable Snowplow events and point to the Snowplow Micro collector:
```shell
gdk psql -d gitlabhq_development
update application_settings set snowplow_collector_hostname='localhost:9090', snowplow_enabled=true, snowplow_cookie_domain='.gitlab.com';
```
-1. Update `app/assets/javascripts/tracking.js` to [remove this line](https://gitlab.com/snippets/1918635):
+1. Update `DEFAULT_SNOWPLOW_OPTIONS` in `app/assets/javascripts/tracking.js` to remove `forceSecureTracker: true`:
+
+ ```diff
+ diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
+ index 0a1211d0a76..3b98c8f28f2 100644
+ --- a/app/assets/javascripts/tracking.js
+ +++ b/app/assets/javascripts/tracking.js
+ @@ -7,7 +7,6 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
+ appId: '',
+ userFingerprint: false,
+ respectDoNotTrack: true,
+ - forceSecureTracker: true,
+ eventMethod: 'post',
+ contexts: { webPage: true, performanceTiming: true },
+ formTracking: false,
- ```javascript
- forceSecureTracker: true
```
-1. Update `lib/gitlab/tracking.rb` to [add these lines](https://gitlab.com/snippets/1918635):
-
- ```ruby
- protocol: 'http',
- port: 9090,
+1. Update `snowplow_options` in `lib/gitlab/tracking.rb` to add `protocol` and `port`:
+
+ ```diff
+ diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
+ index 618e359211b..e9084623c43 100644
+ --- a/lib/gitlab/tracking.rb
+ +++ b/lib/gitlab/tracking.rb
+ @@ -41,7 +41,9 @@ def snowplow_options(group)
+ cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain,
+ app_id: Gitlab::CurrentSettings.snowplow_app_id,
+ form_tracking: additional_features,
+ - link_click_tracking: additional_features
+ + link_click_tracking: additional_features,
+ + protocol: 'http',
+ + port: 9090
+ }.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
+ end
```
-1. Update `lib/gitlab/tracking.rb` to [change async emitter from https to http](https://gitlab.com/snippets/1918635):
+1. Update `emitter` in `lib/gitlab/tracking/destinations/snowplow.rb` to change `protocol`:
+
+ ```diff
+ diff --git a/lib/gitlab/tracking/destinations/snowplow.rb b/lib/gitlab/tracking/destinations/snowplow.rb
+ index 4fa844de325..5dd9d0eacfb 100644
+ --- a/lib/gitlab/tracking/destinations/snowplow.rb
+ +++ b/lib/gitlab/tracking/destinations/snowplow.rb
+ @@ -40,7 +40,7 @@ def tracker
+ def emitter
+ SnowplowTracker::AsyncEmitter.new(
+ Gitlab::CurrentSettings.snowplow_collector_hostname,
+ - protocol: 'https'
+ + protocol: 'http'
+ )
+ end
+ end
- ```ruby
- SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'http'),
```
-1. Enable Snowplow in the admin area, Settings::Integrations::Snowplow to point to:
- `http://localhost:3000/admin/application_settings/integrations#js-snowplow-settings`.
-
1. Restart GDK:
```shell
@@ -423,6 +454,8 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event
```ruby
Gitlab::Tracking.self_describing_event('iglu:com.gitlab/pageview_context/jsonschema/1-0-0', data: { page_type: 'MY_TYPE' }, context: nil)
```
+
+1. Navigate to `localhost:9090/micro/good` to see the event.
### Snowplow Mini
diff --git a/doc/development/product_analytics/usage_ping.md b/doc/development/product_analytics/usage_ping.md
index c7e8155a754..3923e046ec2 100644
--- a/doc/development/product_analytics/usage_ping.md
+++ b/doc/development/product_analytics/usage_ping.md
@@ -265,6 +265,45 @@ Examples of implementation:
- Using Redis methods [`INCR`](https://redis.io/commands/incr), [`GET`](https://redis.io/commands/get), and [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb)
- Using Redis methods [`HINCRBY`](https://redis.io/commands/hincrby), [`HGETALL`](https://redis.io/commands/hgetall), and [`Gitlab::UsageCounters::PodLogs`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_counters/pod_logs.rb)
+##### UsageData API Tracking
+
+<!-- There's nearly identical content in `##### Adding new events`. If you fix errors here, you may need to fix the same errors in the other location. -->
+
+1. Track event using `UsageData` API
+
+ Increment event count using ordinary Redis counter, for given event name.
+
+ Tracking events using the `UsageData` API requires the `usage_data_api` feature flag to be enabled, which is enabled by default.
+
+ API requests are protected by checking for a valid CSRF token.
+
+ In order to be able to increment the values the related feature `usage_data_<event_name>` should be enabled.
+
+ ```plaintext
+ POST /usage_data/increment_counter
+ ```
+
+ | Attribute | Type | Required | Description |
+ | :-------- | :--- | :------- | :---------- |
+ | `event` | string | yes | The event name it should be tracked |
+
+ Response
+
+ - `200` if event was tracked
+ - `400 Bad request` if event parameter is missing
+ - `401 Unauthorized` if user is not authenticated
+ - `403 Forbidden` for invalid CSRF token provided
+
+1. Track events using JavaScript/Vue API helper which calls the API above
+
+ Note that `usage_data_api` and `usage_data_#{event_name}` should be enabled in order to be able to track events
+
+ ```javascript
+ import api from '~/api';
+
+ api.trackRedisCounterEvent('my_already_defined_event_name'),
+ ```
+
#### Redis HLL Counters
With `Gitlab::UsageDataCounters::HLLRedisCounter` we have available data structures used to count unique values.
@@ -387,6 +426,8 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
track_usage_event(:incident_management_incident_created, current_user.id)
```
+<!-- There's nearly identical content in `##### UsageData API Tracking`. If you find / fix errors here, you may need to fix errors in that section too. -->
+
1. Track event using `UsageData` API
Increment unique users count using Redis HLL, for given event name.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index df4e5ea091e..cc9145b9e7b 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -250,6 +250,9 @@ For example, to unlink the `MyOrg` account, the following **Disconnect** button
## Group Sync
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a demo of Group Sync using Azure, see [Demo: SAML Group Sync](https://youtu.be/Iqvo2tJfXjg).
+
When the SAML response includes a user and their group memberships from the SAML identity provider,
GitLab uses that information to automatically manage that user's GitLab group memberships.
diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md
index c7985467b5c..f3bb12c8a63 100644
--- a/doc/user/project/canary_deployments.md
+++ b/doc/user/project/canary_deployments.md
@@ -68,9 +68,10 @@ can easily notice them.
![Canary deployments on Deploy Board](img/deploy_boards_canary_deployments.png)
-### Advanced traffic control with Canary Ingress **(PREMIUM)**
+### Advanced traffic control with Canary Ingress
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215501) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215501) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212320) to Core in GitLab 13.7.
Canary deployments can be more strategic with [Canary Ingress](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary),
which is an advanced traffic routing service that controls incoming HTTP
diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb
index f92593da3fa..e79c1cdf1a2 100644
--- a/lib/api/entities/project_import_status.rb
+++ b/lib/api/entities/project_import_status.rb
@@ -12,9 +12,8 @@ module API
project.import_state&.relation_hard_failures(limit: 100) || []
end
- # TODO: Use `expose_nil` once we upgrade the grape-entity gem
- expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project|
- project.import_state.last_error
+ expose :import_error do |project, _options|
+ project.import_state&.last_error
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 98b52d679cd..f79660377fb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7300,13 +7300,16 @@ msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
-msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgid "ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted."
msgstr ""
-msgid "ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion"
+msgid "ContainerRegistry|%{strongStart}Enabled%{strongEnd} - Tags that match the rules on this page are automatically scheduled for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|%{toggleStatus} - Tags that match the rules on this page are automatically scheduled for deletion."
+msgid "ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion"
msgstr ""
msgid "ContainerRegistry|Build an image"
@@ -24067,6 +24070,9 @@ msgstr ""
msgid "SearchAutocomplete|Merge requests assigned to me"
msgstr ""
+msgid "SearchAutocomplete|Merge requests that I'm a reviewer"
+msgstr ""
+
msgid "SearchAutocomplete|in all GitLab"
msgstr ""
@@ -27288,9 +27294,6 @@ msgstr ""
msgid "The issue was successfully promoted to an epic. Redirecting to epic..."
msgstr ""
-msgid "The license for Deploy Board is required to use this feature."
-msgstr ""
-
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
msgstr ""
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 0237bbc793b..0e76b5478a1 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -172,4 +172,25 @@ RSpec.describe 'Dashboard Merge Requests' do
expect(find('.issues-filters')).to have_content('Created date')
end
end
+
+ context 'merge request review', :js do
+ let_it_be(:author_user) { create(:user) }
+ let!(:review_requested_merge_request) do
+ create(:merge_request,
+ reviewers: [current_user],
+ source_branch: 'review',
+ source_project: project,
+ author: author_user)
+ end
+
+ before do
+ visit merge_requests_dashboard_path(reviewer_username: current_user.username)
+ end
+
+ it 'displays review requested merge requests' do
+ expect(page).to have_content(review_requested_merge_request.title)
+
+ expect_tokens([reviewer_token(current_user.name)])
+ end
+ end
end
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 120c5b56e03..2b03ecf5af1 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
within '#js-registry-policies' do
case result
when :available_section
- expect(find('[data-testid="enable-toggle"]')).to have_content('Tags that match the rules on this page are automatically scheduled for deletion.')
+ expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
when :disabled_message
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
end
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 724d33922a1..37630c15b89 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1254,6 +1254,46 @@ describe('Api', () => {
});
});
+ describe('trackRedisCounterEvent', () => {
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/usage_data/increment_counter`;
+
+ const event = 'dummy_event';
+ const postData = { event };
+ const headers = {
+ 'Content-Type': 'application/json',
+ };
+
+ describe('when usage data increment counter is called with feature flag disabled', () => {
+ beforeEach(() => {
+ gon.features = { ...gon.features, usageDataApi: false };
+ });
+
+ it('returns null', () => {
+ jest.spyOn(axios, 'post');
+ mock.onPost(expectedUrl).replyOnce(httpStatus.OK, true);
+
+ expect(axios.post).toHaveBeenCalledTimes(0);
+ expect(Api.trackRedisCounterEvent(event)).toEqual(null);
+ });
+ });
+
+ describe('when usage data increment counter is called', () => {
+ beforeEach(() => {
+ gon.features = { ...gon.features, usageDataApi: true };
+ });
+
+ it('resolves the Promise', () => {
+ jest.spyOn(axios, 'post');
+ mock.onPost(expectedUrl, { event }).replyOnce(httpStatus.OK, true);
+
+ return Api.trackRedisCounterEvent(event).then(({ data }) => {
+ expect(data).toEqual(true);
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl, postData, { headers });
+ });
+ });
+ });
+ });
+
describe('trackRedisHllUserEvent', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/usage_data/increment_unique_users`;
diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js
index a9b1c81d00e..eb9f9b4db73 100644
--- a/spec/frontend/diffs/components/settings_dropdown_spec.js
+++ b/spec/frontend/diffs/components/settings_dropdown_spec.js
@@ -181,7 +181,7 @@ describe('Diff settings dropdown component', () => {
${true} | ${true}
${false} | ${false}
`(
- 'sets { checked: $checked } if the fileByFile setting is $fileByFile',
+ 'sets the checkbox to { checked: $checked } if the fileByFile setting is $fileByFile',
async ({ fileByFile, checked }) => {
createComponent(store => {
Object.assign(store.state.diffs, {
@@ -191,7 +191,7 @@ describe('Diff settings dropdown component', () => {
await vm.$nextTick();
- expect(vm.checked).toBe(checked);
+ expect(getFileByFileCheckbox(wrapper).element.checked).toBe(checked);
},
);
diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
index 86895341f2c..d7f89ce070e 100644
--- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -30,8 +30,8 @@ exports[`Settings Form Keep N matches snapshot 1`] = `
`;
exports[`Settings Form Keep Regex matches snapshot 1`] = `
-<expiration-textarea-stub
- data-testid="keep-regex-textarea"
+<expiration-input-stub
+ data-testid="keep-regex-input"
description="Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}"
error=""
label="Keep tags matching:"
@@ -52,8 +52,8 @@ exports[`Settings Form OlderThan matches snapshot 1`] = `
`;
exports[`Settings Form Remove regex matches snapshot 1`] = `
-<expiration-textarea-stub
- data-testid="remove-regex-textarea"
+<expiration-input-stub
+ data-testid="remove-regex-input"
description="Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}"
error=""
label="Remove tags matching:"
diff --git a/spec/frontend/registry/settings/components/expiration_textarea_spec.js b/spec/frontend/registry/settings/components/expiration_input_spec.js
index 80464c61117..cb5034d2864 100644
--- a/spec/frontend/registry/settings/components/expiration_textarea_spec.js
+++ b/spec/frontend/registry/settings/components/expiration_input_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { GlSprintf, GlFormTextarea, GlLink } from '@gitlab/ui';
+import { GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
import { GlFormGroup } from 'jest/registry/shared/stubs';
-import component from '~/registry/settings/components/expiration_textarea.vue';
+import component from '~/registry/settings/components/expiration_input.vue';
import { NAME_REGEX_LENGTH } from '~/registry/shared/constants';
-describe('ExpirationTextarea', () => {
+describe('ExpirationInput', () => {
let wrapper;
const defaultProps = {
@@ -16,7 +16,7 @@ describe('ExpirationTextarea', () => {
const tagsRegexHelpPagePath = 'fooPath';
- const findTextArea = () => wrapper.find(GlFormTextarea);
+ const findInput = () => wrapper.find(GlFormInput);
const findFormGroup = () => wrapper.find(GlFormGroup);
const findLabel = () => wrapper.find('[data-testid="label"]');
const findDescription = () => wrapper.find('[data-testid="description"]');
@@ -53,7 +53,7 @@ describe('ExpirationTextarea', () => {
it('has a textarea component', () => {
mountComponent();
- expect(findTextArea().exists()).toBe(true);
+ expect(findInput().exists()).toBe(true);
});
it('has a description', () => {
@@ -78,7 +78,7 @@ describe('ExpirationTextarea', () => {
mountComponent({ value, disabled });
- expect(findTextArea().attributes()).toMatchObject({
+ expect(findInput().attributes()).toMatchObject({
id: defaultProps.name,
value,
placeholder: defaultProps.placeholder,
@@ -92,7 +92,7 @@ describe('ExpirationTextarea', () => {
mountComponent();
- findTextArea().vm.$emit('input', emittedValue);
+ findInput().vm.$emit('input', emittedValue);
expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
});
});
@@ -141,12 +141,12 @@ describe('ExpirationTextarea', () => {
// since the component has no state we both emit the event and set the prop
mountComponent({ value: invalidString });
- findTextArea().vm.$emit('input', invalidString);
+ findInput().vm.$emit('input', invalidString);
});
it('textAreaValidation state is false', () => {
expect(findFormGroup().props('state')).toBe(false);
- expect(findTextArea().attributes('state')).toBeUndefined();
+ expect(findInput().attributes('state')).toBeUndefined();
});
it('emits the @validation event with false payload', () => {
@@ -157,10 +157,10 @@ describe('ExpirationTextarea', () => {
it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => {
mountComponent();
- findTextArea().vm.$emit('input', 'foo');
+ findInput().vm.$emit('input', 'foo');
expect(findFormGroup().props('state')).toBe(true);
- expect(findTextArea().attributes('state')).toBe('true');
+ expect(findInput().attributes('state')).toBe('true');
expect(wrapper.emitted('validation')).toEqual([[true]]);
});
});
diff --git a/spec/frontend/registry/settings/components/expiration_run_text_spec.js b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
index d023f1fd05a..c594b1f449d 100644
--- a/spec/frontend/registry/settings/components/expiration_run_text_spec.js
+++ b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
@@ -28,19 +28,12 @@ describe('ExpirationToggle', () => {
describe('structure', () => {
it('has an input component', () => {
mountComponent();
+
expect(findInput().exists()).toBe(true);
});
});
describe('model', () => {
- it('assigns the right props to the input component', () => {
- mountComponent({ value, disabled: true });
-
- expect(findInput().attributes()).toMatchObject({
- value,
- });
- });
-
it('assigns the right props to the form-group component', () => {
mountComponent();
@@ -51,16 +44,19 @@ describe('ExpirationToggle', () => {
});
describe('formattedValue', () => {
- it('displays the values when it exists', () => {
- mountComponent({ value });
-
- expect(findInput().attributes('value')).toBe(value);
- });
-
- it('displays a placeholder when no value is present', () => {
- mountComponent();
-
- expect(findInput().attributes('value')).toBe(NOT_SCHEDULED_POLICY_TEXT);
- });
+ it.each`
+ valueProp | enabled | expected
+ ${value} | ${true} | ${value}
+ ${value} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
+ ${undefined} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
+ ${undefined} | ${true} | ${NOT_SCHEDULED_POLICY_TEXT}
+ `(
+ 'when value is $valueProp and enabled is $enabled the input value is $expected',
+ ({ valueProp, enabled, expected }) => {
+ mountComponent({ value: valueProp, enabled });
+
+ expect(findInput().attributes('value')).toBe(expected);
+ },
+ );
});
});
diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
index 8b670c98dc1..99ff7a7f77a 100644
--- a/spec/frontend/registry/settings/components/expiration_toggle_spec.js
+++ b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
@@ -3,9 +3,8 @@ import { GlToggle, GlSprintf } from '@gitlab/ui';
import { GlFormGroup } from 'jest/registry/shared/stubs';
import component from '~/registry/settings/components/expiration_toggle.vue';
import {
- ENABLE_TOGGLE_DESCRIPTION,
- ENABLED_TEXT,
- DISABLED_TEXT,
+ ENABLED_TOGGLE_DESCRIPTION,
+ DISABLED_TOGGLE_DESCRIPTION,
} from '~/registry/settings/constants';
describe('ExpirationToggle', () => {
@@ -39,9 +38,7 @@ describe('ExpirationToggle', () => {
it('has a description', () => {
mountComponent();
- expect(findDescription().text()).toContain(
- ENABLE_TOGGLE_DESCRIPTION.replace('%{toggleStatus}', ''),
- );
+ expect(findDescription().exists()).toBe(true);
});
});
@@ -68,13 +65,13 @@ describe('ExpirationToggle', () => {
it('says enabled when the toggle is on', () => {
mountComponent({ value: true });
- expect(findDescription().text()).toContain(ENABLED_TEXT);
+ expect(findDescription().text()).toMatchInterpolatedText(ENABLED_TOGGLE_DESCRIPTION);
});
it('says disabled when the toggle is off', () => {
mountComponent({ value: false });
- expect(findDescription().text()).toContain(DISABLED_TEXT);
+ expect(findDescription().text()).toMatchInterpolatedText(DISABLED_TOGGLE_DESCRIPTION);
});
});
});
diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js
index 3744faa0d80..02e57396c42 100644
--- a/spec/frontend/registry/settings/components/settings_form_spec.js
+++ b/spec/frontend/registry/settings/components/settings_form_spec.js
@@ -44,9 +44,9 @@ describe('Settings Form', () => {
const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]');
const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]');
const findKeepNDropdown = () => wrapper.find('[data-testid="keep-n-dropdown"]');
- const findKeepRegexTextarea = () => wrapper.find('[data-testid="keep-regex-textarea"]');
+ const findKeepRegexInput = () => wrapper.find('[data-testid="keep-regex-input"]');
const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]');
- const findRemoveRegexTextarea = () => wrapper.find('[data-testid="remove-regex-textarea"]');
+ const findRemoveRegexInput = () => wrapper.find('[data-testid="remove-regex-input"]');
const mountComponent = ({
props = defaultProps,
@@ -115,13 +115,13 @@ describe('Settings Form', () => {
});
describe.each`
- model | finder | fieldName | type | defaultValue
- ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
- ${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'}
- ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'}
- ${'nameRegexKeep'} | ${findKeepRegexTextarea} | ${'Keep Regex'} | ${'textarea'} | ${''}
- ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
- ${'nameRegex'} | ${findRemoveRegexTextarea} | ${'Remove regex'} | ${'textarea'} | ${''}
+ model | finder | fieldName | type | defaultValue
+ ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
+ ${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'}
+ ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'}
+ ${'nameRegexKeep'} | ${findKeepRegexInput} | ${'Keep Regex'} | ${'textarea'} | ${''}
+ ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
+ ${'nameRegex'} | ${findRemoveRegexInput} | ${'Remove regex'} | ${'textarea'} | ${''}
`('$fieldName', ({ model, finder, type, defaultValue }) => {
it('matches snapshot', () => {
mountComponent();
@@ -240,8 +240,8 @@ describe('Settings Form', () => {
await wrapper.vm.$nextTick();
- expect(findKeepRegexTextarea().props('error')).toBe('');
- expect(findRemoveRegexTextarea().props('error')).toBe('');
+ expect(findKeepRegexInput().props('error')).toBe('');
+ expect(findRemoveRegexInput().props('error')).toBe('');
expect(findSaveButton().props('disabled')).toBe(false);
});
});
@@ -338,7 +338,7 @@ describe('Settings Form', () => {
await waitForPromises();
await wrapper.vm.$nextTick();
- expect(findKeepRegexTextarea().props('error')).toEqual('baz');
+ expect(findKeepRegexInput().props('error')).toEqual('baz');
});
});
});
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index e40c44925e2..ec67ed16fe9 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'with a single permission' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_single
+ query.field :item, type, null: true, resolver: simple_resolver(test_object), authorize: permission_single
end
end
@@ -66,7 +66,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
permissions = permission_collection
query_factory do |qt|
- qt.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object } do
+ qt.field :item, type, null: true, resolver: simple_resolver(test_object) do
authorize permissions
end
end
@@ -79,7 +79,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'Field authorizations when field is a built in type' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }
+ query.field :item, type, null: true, resolver: simple_resolver(test_object)
end
end
@@ -132,7 +132,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'Type authorizations' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }
+ query.field :item, type, null: true, resolver: simple_resolver(test_object)
end
end
@@ -169,7 +169,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_2
+ query.field :item, type, null: true, resolver: simple_resolver(test_object), authorize: permission_2
end
end
@@ -188,7 +188,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :item, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object, second_test_object] }
+ query.field :item, type.connection_type, null: true, resolver: simple_resolver([test_object, second_test_object])
end
end
@@ -208,9 +208,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'limiting connections with multiple objects' do
let(:query_type) do
query_factory do |query|
- query.field :item, type.connection_type, null: true, resolve: ->(obj, args, ctx) do
- [test_object, second_test_object]
- end
+ query.field :item, type.connection_type, null: true, resolver: simple_resolver([test_object, second_test_object])
end
end
@@ -234,7 +232,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :item, [type], null: true, resolve: ->(obj, args, ctx) { [test_object] }
+ query.field :item, [type], null: true, resolver: simple_resolver([test_object])
end
end
@@ -262,13 +260,13 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
type_factory do |type|
type.graphql_name 'FakeProjectType'
type.field :test_issues, issue_type.connection_type, null: false,
- resolve: -> (_, _, _) { Issue.where(project: [visible_project, other_project]).order(id: :asc) }
+ resolver: simple_resolver(Issue.where(project: [visible_project, other_project]).order(id: :asc))
end
end
let(:query_type) do
query_factory do |query|
- query.field :test_project, project_type, null: false, resolve: -> (_, _, _) { visible_project }
+ query.field :test_project, project_type, null: false, resolver: simple_resolver(visible_project)
end
end
diff --git a/spec/graphql/features/feature_flag_spec.rb b/spec/graphql/features/feature_flag_spec.rb
index 9ebc6e595a6..77810f78257 100644
--- a/spec/graphql/features/feature_flag_spec.rb
+++ b/spec/graphql/features/feature_flag_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'Graphql Field feature flags' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, feature_flag: feature_flag, resolve: ->(obj, args, ctx) { test_object }
+ query.field :item, type, null: true, feature_flag: feature_flag, resolver: simple_resolver(test_object)
end
end
diff --git a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
new file mode 100644
index 00000000000..c022828cf09
--- /dev/null
+++ b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Environments::CanaryIngress::Update do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let(:user) { maintainer }
+
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ before_all do
+ project.add_maintainer(maintainer)
+ project.add_reporter(reporter)
+ end
+
+ describe '#resolve' do
+ subject { mutation.resolve(id: environment_id, weight: weight) }
+
+ let(:environment_id) { environment.to_global_id.to_s }
+ let(:weight) { 50 }
+ let(:update_service) { double('update_service') }
+
+ before do
+ allow(Environments::CanaryIngress::UpdateService).to receive(:new) { update_service }
+ end
+
+ context 'when service execution succeeded' do
+ before do
+ allow(update_service).to receive(:execute_async) { { status: :success } }
+ end
+
+ it 'returns no errors' do
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when service encounters a problem' do
+ before do
+ allow(update_service).to receive(:execute_async) { { status: :error, message: 'something went wrong' } }
+ end
+
+ it 'returns an error' do
+ expect(subject[:errors]).to eq(['something went wrong'])
+ end
+ end
+
+ context 'when environment is not found' do
+ let(:environment_id) { non_existing_record_id.to_s }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(GraphQL::CoercionError)
+ end
+ end
+
+ context 'when user is reporter who does not have permission to access the environment' do
+ let(:user) { reporter }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/config_resolver_spec.rb b/spec/graphql/resolvers/ci/config_resolver_spec.rb
new file mode 100644
index 00000000000..6911acdb4ec
--- /dev/null
+++ b/spec/graphql/resolvers/ci/config_resolver_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::ConfigResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ before do
+ yaml_processor_double = instance_double(::Gitlab::Ci::YamlProcessor)
+ allow(yaml_processor_double).to receive(:execute).and_return(fake_result)
+
+ allow(::Gitlab::Ci::YamlProcessor).to receive(:new).and_return(yaml_processor_double)
+ end
+
+ context 'with a valid .gitlab-ci.yml' do
+ let(:fake_result) do
+ ::Gitlab::Ci::YamlProcessor::Result.new(
+ ci_config: ::Gitlab::Ci::Config.new(content),
+ errors: [],
+ warnings: []
+ )
+ end
+
+ let_it_be(:content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_includes.yml'))
+ end
+
+ it 'lints the ci config file' do
+ response = resolve(described_class, args: { content: content }, ctx: {})
+
+ expect(response[:status]).to eq(:valid)
+ expect(response[:errors]).to be_empty
+ end
+ end
+
+ context 'with an invalid .gitlab-ci.yml' do
+ let(:content) { 'invalid' }
+
+ let(:fake_result) do
+ Gitlab::Ci::YamlProcessor::Result.new(
+ ci_config: nil,
+ errors: ['Invalid configuration format'],
+ warnings: []
+ )
+ end
+
+ it 'responds with errors about invalid syntax' do
+ response = resolve(described_class, args: { content: content }, ctx: {})
+
+ expect(response[:status]).to eq(:invalid)
+ expect(response[:errors]).to eq(['Invalid configuration format'])
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/ci/config/config_type_spec.rb b/spec/graphql/types/ci/config/config_type_spec.rb
new file mode 100644
index 00000000000..edd190a4365
--- /dev/null
+++ b/spec/graphql/types/ci/config/config_type_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Config::ConfigType do
+ specify { expect(described_class.graphql_name).to eq('CiConfig') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ errors
+ mergedYaml
+ stages
+ status
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/config/group_type_spec.rb b/spec/graphql/types/ci/config/group_type_spec.rb
new file mode 100644
index 00000000000..7d808e85371
--- /dev/null
+++ b/spec/graphql/types/ci/config/group_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Config::GroupType do
+ specify { expect(described_class.graphql_name).to eq('CiConfigGroup') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ name
+ jobs
+ size
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/config/job_type_spec.rb b/spec/graphql/types/ci/config/job_type_spec.rb
new file mode 100644
index 00000000000..600d665a84b
--- /dev/null
+++ b/spec/graphql/types/ci/config/job_type_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Config::JobType do
+ specify { expect(described_class.graphql_name).to eq('CiConfigJob') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ name
+ group_name
+ stage
+ needs
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/config/need_type_spec.rb b/spec/graphql/types/ci/config/need_type_spec.rb
new file mode 100644
index 00000000000..3387049a81d
--- /dev/null
+++ b/spec/graphql/types/ci/config/need_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Config::NeedType do
+ specify { expect(described_class.graphql_name).to eq('CiConfigNeed') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ name
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/config/stage_type_spec.rb b/spec/graphql/types/ci/config/stage_type_spec.rb
new file mode 100644
index 00000000000..aba97f8c7ed
--- /dev/null
+++ b/spec/graphql/types/ci/config/stage_type_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Config::StageType do
+ specify { expect(described_class.graphql_name).to eq('CiConfigStage') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ name
+ groups
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb
index 2ce02f1520c..68632a509ee 100644
--- a/spec/graphql/types/permission_types/base_permission_type_spec.rb
+++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb
@@ -11,9 +11,13 @@ RSpec.describe Types::PermissionTypes::BasePermissionType do
Class.new(described_class) do
graphql_name 'TestClass'
- permission_field :do_stuff, resolve: -> (_, _, _) { true }
+ permission_field :do_stuff
ability_field(:read_issue)
abilities :admin_issue
+
+ define_method :do_stuff do
+ true
+ end
end
end
diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb
index 82090f992eb..0e36ea14ac3 100644
--- a/spec/lib/gitlab/graphql/markdown_field_spec.rb
+++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe Gitlab::Graphql::MarkdownField do
.to raise_error(expected_error)
end
+ # TODO: remove as part of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536
+ # so that until that time, the developer check is there
it 'raises when passing a resolve block' do
expect { class_with_markdown_field(:test_html, null: true, resolve: -> (_, _, _) { 'not really' } ) }
.to raise_error(expected_error)
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
new file mode 100644
index 00000000000..b682470e0a1
--- /dev/null
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciConfig' do
+ include GraphqlHelpers
+
+ subject(:post_graphql_query) { post_graphql(query, current_user: user) }
+
+ let(:user) { create(:user) }
+
+ let_it_be(:content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_includes.yml'))
+ end
+
+ let(:query) do
+ %(
+ query {
+ ciConfig(content: "#{content}") {
+ status
+ errors
+ stages {
+ name
+ groups {
+ name
+ size
+ jobs {
+ name
+ groupName
+ stage
+ needs {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ post_graphql_query
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the correct structure' do
+ expect(graphql_data['ciConfig']).to eq(
+ "status" => "VALID",
+ "errors" => [],
+ "stages" =>
+ [
+ {
+ "name" => "build",
+ "groups" =>
+ [
+ {
+ "name" => "rspec",
+ "size" => 2,
+ "jobs" =>
+ [
+ { "name" => "rspec 0 1", "groupName" => "rspec", "stage" => "build", "needs" => [] },
+ { "name" => "rspec 0 2", "groupName" => "rspec", "stage" => "build", "needs" => [] }
+ ]
+ },
+ {
+ "name" => "spinach", "size" => 1, "jobs" =>
+ [
+ { "name" => "spinach", "groupName" => "spinach", "stage" => "build", "needs" => [] }
+ ]
+ }
+ ]
+ },
+ {
+ "name" => "test",
+ "groups" =>
+ [
+ {
+ "name" => "docker",
+ "size" => 1,
+ "jobs" => [
+ { "name" => "docker", "groupName" => "docker", "stage" => "test", "needs" => [{ "name" => "spinach" }, { "name" => "rspec 0 1" }] }
+ ]
+ }
+ ]
+ }
+ ]
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
new file mode 100644
index 00000000000..f25a49291a6
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do
+ include GraphqlHelpers
+ include KubernetesHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:cluster) { create(:cluster, :project, projects: [project]) }
+ let_it_be(:service) { create(:cluster_platform_kubernetes, :configured, cluster: cluster) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :success, environment: environment, project: project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let(:environment_id) { environment.to_global_id.to_s }
+ let(:weight) { 25 }
+ let(:actor) { developer }
+
+ let(:mutation) do
+ graphql_mutation(:environments_canary_ingress_update, id: environment_id, weight: weight)
+ end
+
+ before_all do
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ end
+
+ before do
+ stub_kubeclient_ingresses(environment.deployment_namespace, response: kube_ingresses_response(with_canary: true))
+ end
+
+ context 'when kubernetes accepted the patch request' do
+ before do
+ stub_kubeclient_ingresses(environment.deployment_namespace, method: :patch, resource_path: "/production-auto-deploy")
+ end
+
+ it 'updates successfully' do
+ post_graphql_mutation(mutation, current_user: actor)
+
+ expect(graphql_mutation_response(:environments_canary_ingress_update)['errors'])
+ .to be_empty
+ end
+ end
+end
diff --git a/spec/services/environments/canary_ingress/update_service_spec.rb b/spec/services/environments/canary_ingress/update_service_spec.rb
new file mode 100644
index 00000000000..31d6f543817
--- /dev/null
+++ b/spec/services/environments/canary_ingress/update_service_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_cache do
+ include KubernetesHelpers
+
+ let_it_be(:project, refind: true) { create(:project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let(:user) { maintainer }
+ let(:params) { {} }
+ let(:service) { described_class.new(project, user, params) }
+
+ before_all do
+ project.add_maintainer(maintainer)
+ project.add_reporter(reporter)
+ end
+
+ shared_examples_for 'failed request' do
+ it 'returns an error' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq(message)
+ end
+ end
+
+ describe '#execute_async' do
+ subject { service.execute_async(environment) }
+
+ let(:environment) { create(:environment, project: project) }
+ let(:params) { { weight: 50 } }
+ let(:canary_ingress) { ::Gitlab::Kubernetes::Ingress.new(kube_ingress(track: :canary)) }
+
+ context 'when canary_ingress_weight_control feature flag is disabled' do
+ before do
+ stub_feature_flags(canary_ingress_weight_control: false)
+ end
+
+ it_behaves_like 'failed request' do
+ let(:message) { "Feature flag is not enabled on the environment's project." }
+ end
+ end
+
+ context 'when the actor does not have permission to update environment' do
+ let(:user) { reporter }
+
+ it_behaves_like 'failed request' do
+ let(:message) { "You do not have permission to update the environment." }
+ end
+ end
+
+ context 'when weight parameter is invalid' do
+ let(:params) { { weight: 'unknown' } }
+
+ it_behaves_like 'failed request' do
+ let(:message) { 'Canary weight must be specified and valid range (0..100).' }
+ end
+ end
+
+ context 'when no parameters exist' do
+ let(:params) { {} }
+
+ it_behaves_like 'failed request' do
+ let(:message) { 'Canary weight must be specified and valid range (0..100).' }
+ end
+ end
+
+ context 'when environment has a running deployment' do
+ before do
+ allow(environment).to receive(:has_running_deployments?) { true }
+ end
+
+ it_behaves_like 'failed request' do
+ let(:message) { 'There are running deployments on the environment. Please retry later.' }
+ end
+ end
+
+ context 'when canary ingress was updated recently' do
+ before do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?) { true }
+ end
+
+ it_behaves_like 'failed request' do
+ let(:message) { "This environment's canary ingress has been updated recently. Please retry later." }
+ end
+ end
+ end
+
+ describe '#execute' do
+ subject { service.execute(environment) }
+
+ let(:environment) { create(:environment, project: project) }
+ let(:params) { { weight: 50 } }
+ let(:canary_ingress) { ::Gitlab::Kubernetes::Ingress.new(kube_ingress(track: :canary)) }
+
+ context 'when canary ingress is present in the environment' do
+ before do
+ allow(environment).to receive(:ingresses) { [canary_ingress] }
+ end
+
+ context 'when patch request succeeds' do
+ let(:patch_data) do
+ {
+ metadata: {
+ annotations: {
+ Gitlab::Kubernetes::Ingress::ANNOTATION_KEY_CANARY_WEIGHT => params[:weight].to_s
+ }
+ }
+ }
+ end
+
+ before do
+ allow(environment).to receive(:patch_ingress).with(canary_ingress, patch_data) { true }
+ end
+
+ it 'returns success' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:message]).to be_nil
+ end
+ end
+
+ context 'when patch request does not succeed' do
+ before do
+ allow(environment).to receive(:patch_ingress) { false }
+ end
+
+ it_behaves_like 'failed request' do
+ let(:message) { 'Failed to update the Canary Ingress.' }
+ end
+ end
+ end
+
+ context 'when canary ingress is not present in the environment' do
+ it_behaves_like 'failed request' do
+ let(:message) { 'Canary Ingress does not exist in the environment.' }
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index 4674f614cf1..3d7d928d744 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -54,7 +54,6 @@ RSpec.describe Projects::Alerting::NotifyService do
shared_examples 'assigns the alert properties' do
it 'ensure that created alert has all data properly assigned' do
subject
-
expect(last_alert_attributes).to match(
project_id: project.id,
title: payload_raw.fetch(:title),
@@ -62,6 +61,7 @@ RSpec.describe Projects::Alerting::NotifyService do
severity: payload_raw.fetch(:severity),
status: AlertManagement::Alert.status_value(:triggered),
events: 1,
+ domain: 'operations',
hosts: payload_raw.fetch(:hosts),
payload: payload_raw.with_indifferent_access,
issue_id: nil,
@@ -187,6 +187,7 @@ RSpec.describe Projects::Alerting::NotifyService do
status: AlertManagement::Alert.status_value(:triggered),
events: 1,
hosts: [],
+ domain: 'operations',
payload: payload_raw.with_indifferent_access,
issue_id: nil,
description: nil,
diff --git a/spec/support/gitlab_stubs/gitlab_ci_includes.yml b/spec/support/gitlab_stubs/gitlab_ci_includes.yml
new file mode 100644
index 00000000000..e74773ce23e
--- /dev/null
+++ b/spec/support/gitlab_stubs/gitlab_ci_includes.yml
@@ -0,0 +1,19 @@
+rspec 0 1:
+ stage: build
+ script: 'rake spec'
+ needs: []
+
+rspec 0 2:
+ stage: build
+ script: 'rake spec'
+ needs: []
+
+spinach:
+ stage: build
+ script: 'rake spinach'
+ needs: []
+
+docker:
+ stage: test
+ script: 'curl http://dockerhub/URL'
+ needs: [spinach, rspec 0 1]
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index d203ff60cc9..10068b9c508 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -113,6 +113,10 @@ module FilteredSearchHelpers
create_token('Assignee', assignee_name)
end
+ def reviewer_token(reviewer_name = nil)
+ create_token('Reviewer', reviewer_name)
+ end
+
def milestone_token(milestone_name = nil, has_symbol = true, operator = '=')
symbol = has_symbol ? '%' : nil
create_token('Milestone', milestone_name, symbol, operator)
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index d53ca5b36ee..b20801bd3c4 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -150,6 +150,14 @@ module GraphqlHelpers
field.resolve_field(instance, args, context)
end
+ def simple_resolver(resolved_value = 'Resolved value')
+ Class.new(Resolvers::BaseResolver) do
+ define_method :resolve do |**_args|
+ resolved_value
+ end
+ end
+ end
+
# Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
#
# prepare_input_for_mutation({ 'my_key' => 1 })
diff --git a/spec/workers/environments/canary_ingress/update_worker_spec.rb b/spec/workers/environments/canary_ingress/update_worker_spec.rb
new file mode 100644
index 00000000000..7bc5108719c
--- /dev/null
+++ b/spec/workers/environments/canary_ingress/update_worker_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Environments::CanaryIngress::UpdateWorker do
+ let_it_be(:environment) { create(:environment) }
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ subject { worker.perform(environment_id, params) }
+
+ let(:environment_id) { environment.id }
+ let(:params) { { 'weight' => 50 } }
+
+ it 'executes the update service' do
+ expect_next_instance_of(Environments::CanaryIngress::UpdateService, environment.project, nil, params) do |service|
+ expect(service).to receive(:execute).with(environment)
+ end
+
+ subject
+ end
+
+ context 'when an environment does not exist' do
+ let(:environment_id) { non_existing_record_id }
+
+ it 'does not execute the update service' do
+ expect(Environments::CanaryIngress::UpdateService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end