Commit 249f5cf3 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents e57c58d1 b6bbaee5
...@@ -31,5 +31,4 @@ Actionable insights always have a follow-up action that needs to take place as a ...@@ -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"
...@@ -69,6 +69,7 @@ const Api = { ...@@ -69,6 +69,7 @@ const Api = {
issuePath: '/api/:version/projects/:id/issues/:issue_iid', issuePath: '/api/:version/projects/:id/issues/:issue_iid',
tagsPath: '/api/:version/projects/:id/repository/tags', tagsPath: '/api/:version/projects/:id/repository/tags',
freezePeriodsPath: '/api/:version/projects/:id/freeze_periods', freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
usageDataIncrementCounterPath: '/api/:version/usage_data/increment_counter',
usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users', usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists', featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid', featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
...@@ -751,6 +752,19 @@ const Api = { ...@@ -751,6 +752,19 @@ const Api = {
return axios.post(url, freezePeriod); 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) { trackRedisHllUserEvent(event) {
if (!gon.features?.usageDataApi) { if (!gon.features?.usageDataApi) {
return null; return null;
......
<script> <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'; import { NAME_REGEX_LENGTH, TEXT_AREA_INVALID_FEEDBACK } from '../constants';
export default { export default {
components: { components: {
GlFormGroup, GlFormGroup,
GlFormTextarea, GlFormInput,
GlSprintf, GlSprintf,
GlLink, GlLink,
}, },
...@@ -48,7 +48,7 @@ export default { ...@@ -48,7 +48,7 @@ export default {
textAreaLengthErrorMessage() { textAreaLengthErrorMessage() {
return this.isInputValid(this.value) ? '' : TEXT_AREA_INVALID_FEEDBACK; return this.isInputValid(this.value) ? '' : TEXT_AREA_INVALID_FEEDBACK;
}, },
textAreaValidation() { inputValidation() {
const nameRegexErrors = this.error || this.textAreaLengthErrorMessage; const nameRegexErrors = this.error || this.textAreaLengthErrorMessage;
return { return {
state: nameRegexErrors === null ? null : !nameRegexErrors, state: nameRegexErrors === null ? null : !nameRegexErrors,
...@@ -77,8 +77,8 @@ export default { ...@@ -77,8 +77,8 @@ export default {
<gl-form-group <gl-form-group
:id="`${name}-form-group`" :id="`${name}-form-group`"
:label-for="name" :label-for="name"
:state="textAreaValidation.state" :state="inputValidation.state"
:invalid-feedback="textAreaValidation.message" :invalid-feedback="inputValidation.message"
> >
<template #label> <template #label>
<span data-testid="label"> <span data-testid="label">
...@@ -89,11 +89,11 @@ export default { ...@@ -89,11 +89,11 @@ export default {
</gl-sprintf> </gl-sprintf>
</span> </span>
</template> </template>
<gl-form-textarea <gl-form-input
:id="name" :id="name"
v-model="internalValue" v-model="internalValue"
:placeholder="placeholder" :placeholder="placeholder"
:state="textAreaValidation.state" :state="inputValidation.state"
:disabled="disabled" :disabled="disabled"
trim trim
/> />
......
...@@ -13,6 +13,16 @@ export default { ...@@ -13,6 +13,16 @@ export default {
required: false, required: false,
default: NOT_SCHEDULED_POLICY_TEXT, default: NOT_SCHEDULED_POLICY_TEXT,
}, },
enabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
parsedValue() {
return this.enabled ? this.value : NOT_SCHEDULED_POLICY_TEXT;
},
}, },
i18n: { i18n: {
NEXT_CLEANUP_LABEL, NEXT_CLEANUP_LABEL,
...@@ -26,6 +36,11 @@ export default { ...@@ -26,6 +36,11 @@ export default {
:label="$options.i18n.NEXT_CLEANUP_LABEL" :label="$options.i18n.NEXT_CLEANUP_LABEL"
label-for="expiration-policy-info-text" 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> </gl-form-group>
</template> </template>
<script> <script>
import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui'; 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 { export default {
components: { components: {
...@@ -20,9 +20,6 @@ export default { ...@@ -20,9 +20,6 @@ export default {
default: false, default: false,
}, },
}, },
i18n: {
ENABLE_TOGGLE_DESCRIPTION,
},
computed: { computed: {
enabled: { enabled: {
get() { get() {
...@@ -32,8 +29,8 @@ export default { ...@@ -32,8 +29,8 @@ export default {
this.$emit('input', value); this.$emit('input', value);
}, },
}, },
toggleStatusText() { toggleText() {
return this.enabled ? ENABLED_TEXT : DISABLED_TEXT; return this.enabled ? ENABLED_TOGGLE_DESCRIPTION : DISABLED_TOGGLE_DESCRIPTION;
}, },
}, },
}; };
...@@ -44,9 +41,9 @@ export default { ...@@ -44,9 +41,9 @@ export default {
<div class="gl-display-flex"> <div class="gl-display-flex">
<gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="disabled" /> <gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="disabled" />
<span class="gl-ml-5 gl-line-height-24" data-testid="description"> <span class="gl-ml-5 gl-line-height-24" data-testid="description">
<gl-sprintf :message="$options.i18n.ENABLE_TOGGLE_DESCRIPTION"> <gl-sprintf :message="toggleText">
<template #toggleStatus> <template #strong="{content}">
<strong>{{ toggleStatusText }}</strong> <strong>{{ content }}</strong>
</template> </template>
</gl-sprintf> </gl-sprintf>
</span> </span>
......
...@@ -25,7 +25,7 @@ import { formOptionsGenerator } from '~/registry/shared/utils'; ...@@ -25,7 +25,7 @@ import { formOptionsGenerator } from '~/registry/shared/utils';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql'; import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update'; import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
import ExpirationDropdown from './expiration_dropdown.vue'; 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 ExpirationToggle from './expiration_toggle.vue';
import ExpirationRunText from './expiration_run_text.vue'; import ExpirationRunText from './expiration_run_text.vue';
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
GlButton, GlButton,
GlSprintf, GlSprintf,
ExpirationDropdown, ExpirationDropdown,
ExpirationTextarea, ExpirationInput,
ExpirationToggle, ExpirationToggle,
ExpirationRunText, ExpirationRunText,
}, },
...@@ -202,7 +202,11 @@ export default { ...@@ -202,7 +202,11 @@ export default {
data-testid="cadence-dropdown" data-testid="cadence-dropdown"
@input="onModelChange($event, 'cadence')" @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> </div>
<gl-card class="gl-mt-7"> <gl-card class="gl-mt-7">
<template #header> <template #header>
...@@ -229,14 +233,14 @@ export default { ...@@ -229,14 +233,14 @@ export default {
data-testid="keep-n-dropdown" data-testid="keep-n-dropdown"
@input="onModelChange($event, 'keepN')" @input="onModelChange($event, 'keepN')"
/> />
<expiration-textarea <expiration-input
v-model="prefilledForm.nameRegexKeep" v-model="prefilledForm.nameRegexKeep"
:error="apiErrors.nameRegexKeep" :error="apiErrors.nameRegexKeep"
:disabled="isFieldDisabled" :disabled="isFieldDisabled"
:label="$options.i18n.NAME_REGEX_KEEP_LABEL" :label="$options.i18n.NAME_REGEX_KEEP_LABEL"
:description="$options.i18n.NAME_REGEX_KEEP_DESCRIPTION" :description="$options.i18n.NAME_REGEX_KEEP_DESCRIPTION"
name="keep-regex" name="keep-regex"
data-testid="keep-regex-textarea" data-testid="keep-regex-input"
@input="onModelChange($event, 'nameRegexKeep')" @input="onModelChange($event, 'nameRegexKeep')"
@validation="setLocalErrors($event, 'nameRegexKeep')" @validation="setLocalErrors($event, 'nameRegexKeep')"
/> />
...@@ -268,7 +272,7 @@ export default { ...@@ -268,7 +272,7 @@ export default {
data-testid="older-than-dropdown" data-testid="older-than-dropdown"
@input="onModelChange($event, 'olderThan')" @input="onModelChange($event, 'olderThan')"
/> />
<expiration-textarea <expiration-input
v-model="prefilledForm.nameRegex" v-model="prefilledForm.nameRegex"
:error="apiErrors.nameRegex" :error="apiErrors.nameRegex"
:disabled="isFieldDisabled" :disabled="isFieldDisabled"
...@@ -276,7 +280,7 @@ export default { ...@@ -276,7 +280,7 @@ export default {
:placeholder="$options.i18n.NAME_REGEX_PLACEHOLDER" :placeholder="$options.i18n.NAME_REGEX_PLACEHOLDER"
:description="$options.i18n.NAME_REGEX_DESCRIPTION" :description="$options.i18n.NAME_REGEX_DESCRIPTION"
name="remove-regex" name="remove-regex"
data-testid="remove-regex-textarea" data-testid="remove-regex-input"
@input="onModelChange($event, 'nameRegex')" @input="onModelChange($event, 'nameRegex')"
@validation="setLocalErrors($event, 'nameRegex')" @validation="setLocalErrors($event, 'nameRegex')"
/> />
......
...@@ -37,11 +37,11 @@ export const NAME_REGEX_DESCRIPTION = s__( ...@@ -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}', 'ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}',
); );
export const ENABLED_TEXT = __('Enabled'); export const ENABLED_TOGGLE_DESCRIPTION = s__(
export const DISABLED_TEXT = __('Disabled'); 'ContainerRegistry|%{strongStart}Enabled%{strongEnd} - Tags that match the rules on this page are automatically scheduled for deletion.',
);
export const ENABLE_TOGGLE_DESCRIPTION = s__( export const DISABLED_TOGGLE_DESCRIPTION = s__(
'ContainerRegistry|%{toggleStatus} - Tags that match the rules on this page are automatically scheduled for deletion.', 'ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted.',
); );
export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup:'); export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup:');
......
# 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
# 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
# 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
# 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
# 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
# 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
# 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
...@@ -31,6 +31,7 @@ module Types ...@@ -31,6 +31,7 @@ module Types
mount_mutation Mutations::Commits::Create, calls_gitaly: true mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
mount_mutation Mutations::Discussions::ToggleResolve mount_mutation Mutations::Discussions::ToggleResolve
mount_mutation Mutations::Environments::CanaryIngress::Update
mount_mutation Mutations::Issues::Create mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetConfidential
......
...@@ -91,6 +91,11 @@ module Types ...@@ -91,6 +91,11 @@ module Types
description: 'Get runner setup instructions', description: 'Get runner setup instructions',
resolver: Resolvers::Ci::RunnerSetupResolver 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 def design_management
DesignManagementObject.new(nil) DesignManagementObject.new(nil)
end end
......
...@@ -69,6 +69,11 @@ module AlertManagement ...@@ -69,6 +69,11 @@ module AlertManagement
unknown: 5 unknown: 5
} }
enum domain: {
operations: 0,
threat_monitoring: 1
}
state_machine :status, initial: :triggered do state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered] state :triggered, value: STATUSES[:triggered]
......
...@@ -41,10 +41,6 @@ module Environments ...@@ -41,10 +41,6 @@ module Environments
return error(_('You do not have permission to update the environment.')) return error(_('You do not have permission to update the environment.'))
end end
unless environment.project.feature_available?(:deploy_board)
return error(_('The license for Deploy Board is required to use this feature.'))
end
unless params[:weight].is_a?(Integer) && (0..100).cover?(params[:weight]) unless params[:weight].is_a?(Integer) && (0..100).cover?(params[:weight])
return error(_('Canary weight must be specified and valid range (0..100).')) return error(_('Canary weight must be specified and valid range (0..100).'))
end end
......
...@@ -1537,6 +1537,14 @@ ...@@ -1537,6 +1537,14 @@
:weight: 2 :weight: 2
:idempotent: :idempotent:
:tags: [] :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 - :name: error_tracking_issue_link
:feature_category: :error_tracking :feature_category: :error_tracking
:has_external_dependencies: true :has_external_dependencies: true
......
---
title: Move CanaryIngress to core
merge_request: 48836
author:
type: changed
---
title: Frontend client for increment_counter API
merge_request: 47622
author:
type: added
---
title: Add domain column to alerts table
merge_request: 49120
author:
type: added
---
title: 'Expose GraphQL resolver for processing CI config'
merge_request: 46912
author:
type: added
# 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
# 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
4bb54293c339e20082a739f7724b02141d8fb3b0b140e21ac2acab6cbd2d2f01
\ No newline at end of file
3b6d3fb9c279f5e8c76921e654b188a5a5ba0fddd7ff753a03706b41f43240ed
\ No newline at end of file
...@@ -8811,6 +8811,7 @@ CREATE TABLE alert_management_alerts ( ...@@ -8811,6 +8811,7 @@ CREATE TABLE alert_management_alerts (
payload jsonb DEFAULT '{}'::jsonb NOT NULL, payload jsonb DEFAULT '{}'::jsonb NOT NULL,
prometheus_alert_id integer, prometheus_alert_id integer,
environment_id integer, environment_id integer,
domain smallint DEFAULT 0,
CONSTRAINT check_2df3e2fdc1 CHECK ((char_length(monitoring_tool) <= 100)), CONSTRAINT check_2df3e2fdc1 CHECK ((char_length(monitoring_tool) <= 100)),
CONSTRAINT check_5e9e57cadb CHECK ((char_length(description) <= 1000)), CONSTRAINT check_5e9e57cadb CHECK ((char_length(description) <= 1000)),
CONSTRAINT check_bac14dddde CHECK ((char_length(service) <= 100)), 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 ...@@ -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 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_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); CREATE INDEX index_alert_management_alerts_on_issue_id ON alert_management_alerts USING btree (issue_id);
......
...@@ -2237,6 +2237,101 @@ type BurnupChartDailyTotals { ...@@ -2237,6 +2237,101 @@ type BurnupChartDailyTotals {
scopeWeight: Int! 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 { type CiGroup {
""" """
Detailed status of the group Detailed status of the group
...@@ -17905,6 +18000,16 @@ type PromoteToEpicPayload { ...@@ -17905,6 +18000,16 @@ type PromoteToEpicPayload {
} }
type Query { 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 Find a container repository
""" """
......
...@@ -367,6 +367,45 @@ Represents the total number of issues and their weights for a particular day. ...@@ -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 | | `scopeCount` | Int! | Number of issues as of this day |
| `scopeWeight` | Int! | Total weight 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 ### CiGroup
| Field | Type | Description | | Field | Type | Description |
...@@ -3925,6 +3964,15 @@ Types of blob viewers. ...@@ -3925,6 +3964,15 @@ Types of blob viewers.
| `rich` | | | `rich` | |
| `simple` | | | `simple` | |
### CiConfigStatus
Values for YAML processor result.
| Value | Description |
| ----- | ----------- |
| `INVALID` | The configuration file is not valid |
| `VALID` | The configuration file is valid |
### CommitActionMode ### CommitActionMode
Mode of a commit action. Mode of a commit action.
......
...@@ -370,48 +370,79 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event ...@@ -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) - 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) - 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 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):
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. Navigate to the directory with the cloned project, and start the appropriate Docker
``` container with the following script:
1. Install Snowplow Micro by cloning the settings in [this project](https://gitlab.com/gitlab-org/snowplow-micro-configuration):
```shell ```shell
git clone git@gitlab.com:gitlab-org/snowplow-micro-configuration.git
./snowplow-micro.sh ./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 ```shell
gdk psql -d gitlabhq_development gdk psql -d gitlabhq_development
update application_settings set snowplow_collector_hostname='localhost:9090', snowplow_enabled=true, snowplow_cookie_domain='.gitlab.com'; 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): 1. Update `snowplow_options` in `lib/gitlab/tracking.rb` to add `protocol` and `port`:
```ruby ```diff
protocol: 'http', diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
port: 9090, 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: 1. Restart GDK:
```shell ```shell
...@@ -423,6 +454,8 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event ...@@ -423,6 +454,8 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event
```ruby ```ruby
Gitlab::Tracking.self_describing_event('iglu:com.gitlab/pageview_context/jsonschema/1-0-0', data: { page_type: 'MY_TYPE' }, context: nil) 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 ### Snowplow Mini
......
...@@ -265,6 +265,45 @@ Examples of implementation: ...@@ -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 [`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) - 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 #### Redis HLL Counters
With `Gitlab::UsageDataCounters::HLLRedisCounter` we have available data structures used to count unique values. 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 ...@@ -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) 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 1. Track event using `UsageData` API
Increment unique users count using Redis HLL, for given event name. Increment unique users count using Redis HLL, for given event name.
......
...@@ -68,9 +68,10 @@ can easily notice them. ...@@ -68,9 +68,10 @@ can easily notice them.
![Canary deployments on Deploy Board](img/deploy_boards_canary_deployments.png) ![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), 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 which is an advanced traffic routing service that controls incoming HTTP
......
...@@ -14,7 +14,6 @@ module EE ...@@ -14,7 +14,6 @@ module EE
mount_mutation ::Mutations::Issues::SetWeight mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic mount_mutation ::Mutations::Issues::SetEpic
mount_mutation ::Mutations::Issues::PromoteToEpic mount_mutation ::Mutations::Issues::PromoteToEpic
mount_mutation ::Mutations::Environments::CanaryIngress::Update
mount_mutation ::Mutations::EpicTree::Reorder mount_mutation ::Mutations::EpicTree::Reorder
mount_mutation ::Mutations::Epics::Update mount_mutation ::Mutations::Epics::Update
mount_mutation ::Mutations::Epics::Create mount_mutation ::Mutations::Epics::Create
......
...@@ -709,14 +709,6 @@ ...@@ -709,14 +709,6 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :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: group_saml_group_sync - :name: group_saml_group_sync
:feature_category: :authentication_and_authorization :feature_category: :authentication_and_authorization
:has_external_dependencies: :has_external_dependencies:
......
...@@ -27,33 +27,21 @@ RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do ...@@ -27,33 +27,21 @@ RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do
end end
before do before do
stub_licensed_features(deploy_board: true, protected_environments: true) stub_licensed_features(protected_environments: true)
stub_kubeclient_ingresses(environment.deployment_namespace, response: kube_ingresses_response(with_canary: true)) stub_kubeclient_ingresses(environment.deployment_namespace, response: kube_ingresses_response(with_canary: true))
end end
context 'when kubernetes accepted the patch request' do context 'when environment is protected and allowed to be deployed by only operator' do
before do before do
stub_kubeclient_ingresses(environment.deployment_namespace, method: :patch, resource_path: "/production-auto-deploy") stub_kubeclient_ingresses(environment.deployment_namespace, method: :patch, resource_path: "/production-auto-deploy")
create(:protected_environment, :maintainers_can_deploy, name: environment.name, project: project)
end end
it 'updates successfully' do it 'fails to update' do
post_graphql_mutation(mutation, current_user: actor) post_graphql_mutation(mutation, current_user: actor)
expect(graphql_mutation_response(:environments_canary_ingress_update)['errors']) expect(graphql_errors.first)
.to be_empty .to include('message' => "The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
context 'when environment is protected and allowed to be deployed by only operator' do
before do
create(:protected_environment, :maintainers_can_deploy, name: environment.name, project: project)
end
it 'fails to update' do
post_graphql_mutation(mutation, current_user: actor)
expect(graphql_errors.first)
.to include('message' => "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 end
end end
...@@ -12,9 +12,8 @@ module API ...@@ -12,9 +12,8 @@ module API
project.import_state&.relation_hard_failures(limit: 100) || [] project.import_state&.relation_hard_failures(limit: 100) || []
end end
# TODO: Use `expose_nil` once we upgrade the grape-entity gem expose :import_error do |project, _options|
expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project| project.import_state&.last_error
project.import_state.last_error
end end
end end
end end
......
...@@ -7300,13 +7300,16 @@ msgstr[1] "" ...@@ -7300,13 +7300,16 @@ msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags" msgid "ContainerRegistry|%{imageName} tags"
msgstr "" msgstr ""
msgid "ContainerRegistry|%{title} was successfully scheduled for deletion" msgid "ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted."
msgstr "" 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 "" 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 "" msgstr ""
msgid "ContainerRegistry|Build an image" msgid "ContainerRegistry|Build an image"
...@@ -27291,9 +27294,6 @@ msgstr "" ...@@ -27291,9 +27294,6 @@ msgstr ""
msgid "The issue was successfully promoted to an epic. Redirecting to epic..." msgid "The issue was successfully promoted to an epic. Redirecting to epic..."
msgstr "" 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." msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
msgstr "" msgstr ""
......
...@@ -84,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p ...@@ -84,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
within '#js-registry-policies' do within '#js-registry-policies' do
case result case result
when :available_section 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 when :disabled_message
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled') expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
end end
......
...@@ -1254,6 +1254,46 @@ describe('Api', () => { ...@@ -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', () => { describe('trackRedisHllUserEvent', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/usage_data/increment_unique_users`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/usage_data/increment_unique_users`;
......
...@@ -30,8 +30,8 @@ exports[`Settings Form Keep N matches snapshot 1`] = ` ...@@ -30,8 +30,8 @@ exports[`Settings Form Keep N matches snapshot 1`] = `
`; `;
exports[`Settings Form Keep Regex matches snapshot 1`] = ` exports[`Settings Form Keep Regex matches snapshot 1`] = `
<expiration-textarea-stub <expiration-input-stub
data-testid="keep-regex-textarea" data-testid="keep-regex-input"
description="Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}" description="Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}"
error="" error=""
label="Keep tags matching:" label="Keep tags matching:"
...@@ -52,8 +52,8 @@ exports[`Settings Form OlderThan matches snapshot 1`] = ` ...@@ -52,8 +52,8 @@ exports[`Settings Form OlderThan matches snapshot 1`] = `
`; `;
exports[`Settings Form Remove regex matches snapshot 1`] = ` exports[`Settings Form Remove regex matches snapshot 1`] = `
<expiration-textarea-stub <expiration-input-stub
data-testid="remove-regex-textarea" data-testid="remove-regex-input"
description="Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}" description="Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}"
error="" error=""
label="Remove tags matching:" label="Remove tags matching:"
......
import { shallowMount } from '@vue/test-utils'; 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 { 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'; import { NAME_REGEX_LENGTH } from '~/registry/shared/constants';
describe('ExpirationTextarea', () => { describe('ExpirationInput', () => {
let wrapper; let wrapper;
const defaultProps = { const defaultProps = {
...@@ -16,7 +16,7 @@ describe('ExpirationTextarea', () => { ...@@ -16,7 +16,7 @@ describe('ExpirationTextarea', () => {
const tagsRegexHelpPagePath = 'fooPath'; const tagsRegexHelpPagePath = 'fooPath';
const findTextArea = () => wrapper.find(GlFormTextarea); const findInput = () => wrapper.find(GlFormInput);
const findFormGroup = () => wrapper.find(GlFormGroup); const findFormGroup = () => wrapper.find(GlFormGroup);
const findLabel = () => wrapper.find('[data-testid="label"]'); const findLabel = () => wrapper.find('[data-testid="label"]');
const findDescription = () => wrapper.find('[data-testid="description"]'); const findDescription = () => wrapper.find('[data-testid="description"]');
...@@ -53,7 +53,7 @@ describe('ExpirationTextarea', () => { ...@@ -53,7 +53,7 @@ describe('ExpirationTextarea', () => {
it('has a textarea component', () => { it('has a textarea component', () => {
mountComponent(); mountComponent();
expect(findTextArea().exists()).toBe(true); expect(findInput().exists()).toBe(true);
}); });
it('has a description', () => { it('has a description', () => {
...@@ -78,7 +78,7 @@ describe('ExpirationTextarea', () => { ...@@ -78,7 +78,7 @@ describe('ExpirationTextarea', () => {
mountComponent({ value, disabled }); mountComponent({ value, disabled });
expect(findTextArea().attributes()).toMatchObject({ expect(findInput().attributes()).toMatchObject({
id: defaultProps.name, id: defaultProps.name,
value, value,
placeholder: defaultProps.placeholder, placeholder: defaultProps.placeholder,
...@@ -92,7 +92,7 @@ describe('ExpirationTextarea', () => { ...@@ -92,7 +92,7 @@ describe('ExpirationTextarea', () => {
mountComponent(); mountComponent();
findTextArea().vm.$emit('input', emittedValue); findInput().vm.$emit('input', emittedValue);
expect(wrapper.emitted('input')).toEqual([[emittedValue]]); expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
}); });
}); });
...@@ -141,12 +141,12 @@ describe('ExpirationTextarea', () => { ...@@ -141,12 +141,12 @@ describe('ExpirationTextarea', () => {
// since the component has no state we both emit the event and set the prop // since the component has no state we both emit the event and set the prop
mountComponent({ value: invalidString }); mountComponent({ value: invalidString });
findTextArea().vm.$emit('input', invalidString); findInput().vm.$emit('input', invalidString);
}); });
it('textAreaValidation state is false', () => { it('textAreaValidation state is false', () => {
expect(findFormGroup().props('state')).toBe(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', () => { it('emits the @validation event with false payload', () => {
...@@ -157,10 +157,10 @@ describe('ExpirationTextarea', () => { ...@@ -157,10 +157,10 @@ describe('ExpirationTextarea', () => {
it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => { it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => {
mountComponent(); mountComponent();
findTextArea().vm.$emit('input', 'foo'); findInput().vm.$emit('input', 'foo');
expect(findFormGroup().props('state')).toBe(true); expect(findFormGroup().props('state')).toBe(true);
expect(findTextArea().attributes('state')).toBe('true'); expect(findInput().attributes('state')).toBe('true');
expect(wrapper.emitted('validation')).toEqual([[true]]); expect(wrapper.emitted('validation')).toEqual([[true]]);
}); });
}); });
......
...@@ -28,19 +28,12 @@ describe('ExpirationToggle', () => { ...@@ -28,19 +28,12 @@ describe('ExpirationToggle', () => {
describe('structure', () => { describe('structure', () => {
it('has an input component', () => { it('has an input component', () => {
mountComponent(); mountComponent();
expect(findInput().exists()).toBe(true); expect(findInput().exists()).toBe(true);
}); });
}); });
describe('model', () => { 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', () => { it('assigns the right props to the form-group component', () => {
mountComponent(); mountComponent();
...@@ -51,16 +44,19 @@ describe('ExpirationToggle', () => { ...@@ -51,16 +44,19 @@ describe('ExpirationToggle', () => {
}); });
describe('formattedValue', () => { describe('formattedValue', () => {
it('displays the values when it exists', () => { it.each`
mountComponent({ value }); valueProp | enabled | expected
${value} | ${true} | ${value}
expect(findInput().attributes('value')).toBe(value); ${value} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
}); ${undefined} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
${undefined} | ${true} | ${NOT_SCHEDULED_POLICY_TEXT}
it('displays a placeholder when no value is present', () => { `(
mountComponent(); 'when value is $valueProp and enabled is $enabled the input value is $expected',
({ valueProp, enabled, expected }) => {
expect(findInput().attributes('value')).toBe(NOT_SCHEDULED_POLICY_TEXT); mountComponent({ value: valueProp, enabled });
});
expect(findInput().attributes('value')).toBe(expected);
},
);
}); });
}); });
...@@ -3,9 +3,8 @@ import { GlToggle, GlSprintf } from '@gitlab/ui'; ...@@ -3,9 +3,8 @@ import { GlToggle, GlSprintf } from '@gitlab/ui';
import { GlFormGroup } from 'jest/registry/shared/stubs'; import { GlFormGroup } from 'jest/registry/shared/stubs';
import component from '~/registry/settings/components/expiration_toggle.vue'; import component from '~/registry/settings/components/expiration_toggle.vue';
import { import {
ENABLE_TOGGLE_DESCRIPTION, ENABLED_TOGGLE_DESCRIPTION,
ENABLED_TEXT, DISABLED_TOGGLE_DESCRIPTION,
DISABLED_TEXT,
} from '~/registry/settings/constants'; } from '~/registry/settings/constants';
describe('ExpirationToggle', () => { describe('ExpirationToggle', () => {
...@@ -39,9 +38,7 @@ describe('ExpirationToggle', () => { ...@@ -39,9 +38,7 @@ describe('ExpirationToggle', () => {
it('has a description', () => { it('has a description', () => {
mountComponent(); mountComponent();
expect(findDescription().text()).toContain( expect(findDescription().exists()).toBe(true);
ENABLE_TOGGLE_DESCRIPTION.replace('%{toggleStatus}', ''),
);
}); });
}); });
...@@ -68,13 +65,13 @@ describe('ExpirationToggle', () => { ...@@ -68,13 +65,13 @@ describe('ExpirationToggle', () => {
it('says enabled when the toggle is on', () => { it('says enabled when the toggle is on', () => {
mountComponent({ value: true }); mountComponent({ value: true });
expect(findDescription().text()).toContain(ENABLED_TEXT); expect(findDescription().text()).toMatchInterpolatedText(ENABLED_TOGGLE_DESCRIPTION);
}); });
it('says disabled when the toggle is off', () => { it('says disabled when the toggle is off', () => {
mountComponent({ value: false }); mountComponent({ value: false });
expect(findDescription().text()).toContain(DISABLED_TEXT); expect(findDescription().text()).toMatchInterpolatedText(DISABLED_TOGGLE_DESCRIPTION);
}); });
}); });
}); });
...@@ -44,9 +44,9 @@ describe('Settings Form', () => { ...@@ -44,9 +44,9 @@ describe('Settings Form', () => {
const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]'); const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]');
const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]'); const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]');
const findKeepNDropdown = () => wrapper.find('[data-testid="keep-n-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 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 = ({ const mountComponent = ({
props = defaultProps, props = defaultProps,
...@@ -115,13 +115,13 @@ describe('Settings Form', () => { ...@@ -115,13 +115,13 @@ describe('Settings Form', () => {
}); });
describe.each` describe.each`
model | finder | fieldName | type | defaultValue model | finder | fieldName | type | defaultValue
${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false} ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'} ${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'}
${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'} ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'}
${'nameRegexKeep'} | ${findKeepRegexTextarea} | ${'Keep Regex'} | ${'textarea'} | ${''} ${'nameRegexKeep'} | ${findKeepRegexInput} | ${'Keep Regex'} | ${'textarea'} | ${''}
${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'} ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
${'nameRegex'} | ${findRemoveRegexTextarea} | ${'Remove regex'} | ${'textarea'} | ${''} ${'nameRegex'} | ${findRemoveRegexInput} | ${'Remove regex'} | ${'textarea'} | ${''}
`('$fieldName', ({ model, finder, type, defaultValue }) => { `('$fieldName', ({ model, finder, type, defaultValue }) => {
it('matches snapshot', () => { it('matches snapshot', () => {
mountComponent(); mountComponent();
...@@ -240,8 +240,8 @@ describe('Settings Form', () => { ...@@ -240,8 +240,8 @@ describe('Settings Form', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findKeepRegexTextarea().props('error')).toBe(''); expect(findKeepRegexInput().props('error')).toBe('');
expect(findRemoveRegexTextarea().props('error')).toBe(''); expect(findRemoveRegexInput().props('error')).toBe('');
expect(findSaveButton().props('disabled')).toBe(false); expect(findSaveButton().props('disabled')).toBe(false);
}); });
}); });
...@@ -338,7 +338,7 @@ describe('Settings Form', () => { ...@@ -338,7 +338,7 @@ describe('Settings Form', () => {
await waitForPromises(); await waitForPromises();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findKeepRegexTextarea().props('error')).toEqual('baz'); expect(findKeepRegexInput().props('error')).toEqual('baz');
}); });
}); });
}); });
......
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
...@@ -17,10 +17,6 @@ RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_c ...@@ -17,10 +17,6 @@ RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_c
project.add_reporter(reporter) project.add_reporter(reporter)
end end
before do
stub_licensed_features(deploy_board: true)
end
shared_examples_for 'failed request' do shared_examples_for 'failed request' do
it 'returns an error' do it 'returns an error' do
expect(subject[:status]).to eq(:error) expect(subject[:status]).to eq(:error)
...@@ -53,16 +49,6 @@ RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_c ...@@ -53,16 +49,6 @@ RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_c
end end
end end
context 'when project does not have an sufficient license' do
before do
stub_licensed_features(deploy_board: false)
end
it_behaves_like 'failed request' do
let(:message) { 'The license for Deploy Board is required to use this feature.' }
end
end
context 'when weight parameter is invalid' do context 'when weight parameter is invalid' do
let(:params) { { weight: 'unknown' } } let(:params) { { weight: 'unknown' } }
......
...@@ -54,7 +54,6 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -54,7 +54,6 @@ RSpec.describe Projects::Alerting::NotifyService do
shared_examples 'assigns the alert properties' do shared_examples 'assigns the alert properties' do
it 'ensure that created alert has all data properly assigned' do it 'ensure that created alert has all data properly assigned' do
subject subject
expect(last_alert_attributes).to match( expect(last_alert_attributes).to match(
project_id: project.id, project_id: project.id,
title: payload_raw.fetch(:title), title: payload_raw.fetch(:title),
...@@ -62,6 +61,7 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -62,6 +61,7 @@ RSpec.describe Projects::Alerting::NotifyService do
severity: payload_raw.fetch(:severity), severity: payload_raw.fetch(:severity),
status: AlertManagement::Alert.status_value(:triggered), status: AlertManagement::Alert.status_value(:triggered),
events: 1, events: 1,
domain: 'operations',
hosts: payload_raw.fetch(:hosts), hosts: payload_raw.fetch(:hosts),
payload: payload_raw.with_indifferent_access, payload: payload_raw.with_indifferent_access,
issue_id: nil, issue_id: nil,
...@@ -187,6 +187,7 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -187,6 +187,7 @@ RSpec.describe Projects::Alerting::NotifyService do
status: AlertManagement::Alert.status_value(:triggered), status: AlertManagement::Alert.status_value(:triggered),
events: 1, events: 1,
hosts: [], hosts: [],
domain: 'operations',
payload: payload_raw.with_indifferent_access, payload: payload_raw.with_indifferent_access,
issue_id: nil, issue_id: nil,
description: nil, description: nil,
......
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]
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment