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
/label ~"Actionable Insight"
/label ~"Actionable Insight"
......@@ -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;
......
<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
/>
......
......@@ -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>
<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>
......
......@@ -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')"
/>
......
......@@ -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:');
......
# 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
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
......
......@@ -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
......
......@@ -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]
......
......@@ -41,10 +41,6 @@ module Environments
return error(_('You do not have permission to update the environment.'))
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])
return error(_('Canary weight must be specified and valid range (0..100).'))
end
......
......@@ -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
......
---
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 (
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);
......
......@@ -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
......@@ -17905,6 +18000,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
"""
......
......@@ -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.
......
......@@ -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
......
......@@ -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.
......
......@@ -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
......
......@@ -14,7 +14,6 @@ module EE
mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic
mount_mutation ::Mutations::Issues::PromoteToEpic
mount_mutation ::Mutations::Environments::CanaryIngress::Update
mount_mutation ::Mutations::EpicTree::Reorder
mount_mutation ::Mutations::Epics::Update
mount_mutation ::Mutations::Epics::Create
......
......@@ -709,14 +709,6 @@
:weight: 1
: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: group_saml_group_sync
:feature_category: :authentication_and_authorization
:has_external_dependencies:
......
......@@ -27,33 +27,21 @@ RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do
end
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))
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
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
it 'updates successfully' do
it 'fails to update' do
post_graphql_mutation(mutation, current_user: actor)
expect(graphql_mutation_response(:environments_canary_ingress_update)['errors'])
.to be_empty
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
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
......@@ -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
......
......@@ -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"
......@@ -27291,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 ""
......
......@@ -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
......
......@@ -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`;
......
......@@ -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:"
......
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]]);
});
});
......
......@@ -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);
},
);
});
});
......@@ -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);
});
});
});
......@@ -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');
});
});
});
......
# 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
project.add_reporter(reporter)
end
before do
stub_licensed_features(deploy_board: true)
end
shared_examples_for 'failed request' do
it 'returns an error' do
expect(subject[:status]).to eq(:error)
......@@ -53,16 +49,6 @@ RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_c
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
let(:params) { { weight: 'unknown' } }
......
......@@ -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,
......
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