Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5e77663f2e
commit
4212e9c2d4
2
Gemfile
2
Gemfile
|
|
@ -601,7 +601,7 @@ gem 'ed25519', '~> 1.3.0'
|
|||
|
||||
# Error Tracking OpenAPI client
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature
|
||||
gem 'error_tracking_open_api', path: 'vendor/gems/error_tracking_open_api'
|
||||
gem 'error_tracking_open_api', path: 'gems/error_tracking_open_api'
|
||||
|
||||
# Vulnerability advisories
|
||||
gem 'cvss-suite', '~> 3.0.1', require: 'cvss_suite'
|
||||
|
|
|
|||
12
Gemfile.lock
12
Gemfile.lock
|
|
@ -12,6 +12,12 @@ PATH
|
|||
addressable (~> 2.8)
|
||||
json (~> 2.6.3)
|
||||
|
||||
PATH
|
||||
remote: gems/error_tracking_open_api
|
||||
specs:
|
||||
error_tracking_open_api (1.0.0)
|
||||
typhoeus (~> 1.0, >= 1.0.1)
|
||||
|
||||
PATH
|
||||
remote: gems/gitlab-rspec
|
||||
specs:
|
||||
|
|
@ -84,12 +90,6 @@ PATH
|
|||
devise (~> 4.0)
|
||||
devise-two-factor (~> 4.0)
|
||||
|
||||
PATH
|
||||
remote: vendor/gems/error_tracking_open_api
|
||||
specs:
|
||||
error_tracking_open_api (1.0.0)
|
||||
typhoeus (~> 1.0, >= 1.0.1)
|
||||
|
||||
PATH
|
||||
remote: vendor/gems/mail-smtp_pool
|
||||
specs:
|
||||
|
|
|
|||
|
|
@ -1098,6 +1098,73 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"parallel": {
|
||||
"description": "Splits up a single job into multiple that run in parallel. Provides `CI_NODE_INDEX` and `CI_NODE_TOTAL` environment variables to the jobs.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Creates N instances of the job that run in parallel.",
|
||||
"default": 0,
|
||||
"minimum": 2,
|
||||
"maximum": 200
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "Defines different variables for jobs that are running in parallel.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Defines the variables for a specific job.",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"array"
|
||||
]
|
||||
}
|
||||
},
|
||||
"maxItems": 200
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matrix"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"parallel_matrix": {
|
||||
"description": "Use the `needs:parallel:matrix` keyword to specify parallelized jobs needed to be completed for the job to run. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#needsparallelmatrix)",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "Defines different variables for jobs that are running in parallel.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Defines the variables for a specific job.",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"array"
|
||||
]
|
||||
}
|
||||
},
|
||||
"maxItems": 200
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matrix"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"when": {
|
||||
"markdownDescription": "Describes the conditions for when to run the job. Defaults to 'on_success'. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when).",
|
||||
"default": "on_success",
|
||||
|
|
@ -1518,6 +1585,9 @@
|
|||
},
|
||||
"optional": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"parallel": {
|
||||
"$ref": "#/definitions/parallel_matrix"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1536,6 +1606,9 @@
|
|||
},
|
||||
"artifacts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"parallel": {
|
||||
"$ref": "#/definitions/parallel_matrix"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1558,6 +1631,9 @@
|
|||
},
|
||||
"artifacts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"parallel": {
|
||||
"$ref": "#/definitions/parallel_matrix"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1771,41 +1847,7 @@
|
|||
"$ref": "#/definitions/retry"
|
||||
},
|
||||
"parallel": {
|
||||
"description": "Parallel will split up a single job into several, and provide `CI_NODE_INDEX` and `CI_NODE_TOTAL` environment variables for the running jobs.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Creates N instances of the same job that run in parallel.",
|
||||
"default": 0,
|
||||
"minimum": 2,
|
||||
"maximum": 200
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "Defines different variables for jobs that are running in parallel.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Defines environment variables for specific job.",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"array"
|
||||
]
|
||||
}
|
||||
},
|
||||
"maxItems": 200
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matrix"
|
||||
]
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/parallel"
|
||||
},
|
||||
"interruptible": {
|
||||
"$ref": "#/definitions/interruptible"
|
||||
|
|
|
|||
|
|
@ -15,66 +15,6 @@ module Admin
|
|||
def project_missing_pipeline_yaml?(project)
|
||||
project.repository&.gitlab_ci_yml.blank?
|
||||
end
|
||||
|
||||
def code_suggestions_description
|
||||
link_start = code_suggestions_link_start(code_suggestions_docs_url)
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
# rubocop:disable Style/FormatString
|
||||
s_('CodeSuggestionsSM|Enable Code Suggestions for users of this instance. %{link_start}What are Code Suggestions?%{link_end}')
|
||||
.html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||
# rubocop:enable Style/FormatString
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
def code_suggestions_token_explanation
|
||||
link_start = code_suggestions_link_start(code_suggestions_pat_docs_url)
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
# rubocop:disable Style/FormatString
|
||||
s_('CodeSuggestionsSM|On GitLab.com, create a token. This token is required to use Code Suggestions on your self-managed instance. %{link_start}How do I create a token?%{link_end}')
|
||||
.html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||
# rubocop:enable Style/FormatString
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
def code_suggestions_agreement
|
||||
terms_link_start = code_suggestions_link_start(code_suggestions_agreement_url)
|
||||
ai_docs_link_start = code_suggestions_link_start(code_suggestions_ai_docs_url)
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
# rubocop:disable Style/FormatString
|
||||
s_('CodeSuggestionsSM|By enabling this feature, you agree to the %{terms_link_start}GitLab Testing Agreement%{link_end} and acknowledge that GitLab will send data from the instance, including personal data, to our %{ai_docs_link_start}AI providers%{link_end} to provide this feature.')
|
||||
.html_safe % { terms_link_start: terms_link_start, ai_docs_link_start: ai_docs_link_start, link_end: '</a>'.html_safe }
|
||||
# rubocop:enable Style/FormatString
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Gitlab/DocUrl
|
||||
# We want to link SaaS docs for flexibility for every URL related to Code Suggestions on Self Managed.
|
||||
# We expect to update docs often during the Beta and we want to point user to the most up to date information.
|
||||
def code_suggestions_docs_url
|
||||
'https://docs.gitlab.com/ee/user/project/repository/code_suggestions.html'
|
||||
end
|
||||
|
||||
def code_suggestions_agreement_url
|
||||
'https://about.gitlab.com/handbook/legal/testing-agreement/'
|
||||
end
|
||||
|
||||
def code_suggestions_ai_docs_url
|
||||
'https://docs.gitlab.com/ee/user/ai_features.html#third-party-services'
|
||||
end
|
||||
|
||||
def code_suggestions_pat_docs_url
|
||||
'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token'
|
||||
end
|
||||
# rubocop:enable Gitlab/DocUrl
|
||||
|
||||
def code_suggestions_link_start(url)
|
||||
"<a href=\"#{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -218,7 +218,6 @@ module ApplicationSettingsHelper
|
|||
:admin_mode,
|
||||
:after_sign_out_path,
|
||||
:after_sign_up_text,
|
||||
:ai_access_token,
|
||||
:akismet_api_key,
|
||||
:akismet_enabled,
|
||||
:allow_local_requests_from_hooks_and_services,
|
||||
|
|
@ -310,7 +309,6 @@ module ApplicationSettingsHelper
|
|||
:inactive_projects_delete_after_months,
|
||||
:inactive_projects_min_size_mb,
|
||||
:inactive_projects_send_warning_email_after_months,
|
||||
:instance_level_code_suggestions_enabled,
|
||||
:invisible_captcha_enabled,
|
||||
:jira_connect_application_key,
|
||||
:jira_connect_public_key_storage_enabled,
|
||||
|
|
|
|||
|
|
@ -726,10 +726,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
|
|||
allow_nil: false,
|
||||
inclusion: { in: [true, false], message: N_('must be a boolean value') }
|
||||
|
||||
validates :ai_access_token,
|
||||
presence: { message: N_("is required to enable Code Suggestions") },
|
||||
if: :instance_level_code_suggestions_enabled
|
||||
|
||||
validates :package_registry_allow_anyone_to_pull_option,
|
||||
inclusion: { in: [true, false], message: N_('must be a boolean value') }
|
||||
|
||||
|
|
@ -968,5 +964,4 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
ApplicationSetting.prepend(ApplicationSettingMaskedAttrs)
|
||||
ApplicationSetting.prepend_mod_with('ApplicationSetting')
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Similar to MASK_PASSWORD mechanism we do for EE, see:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/blob/463bb1f855d71fadef931bd50f1692ee04f211a8/ee/app/models/ee/application_setting.rb#L15
|
||||
# but for non-EE attributes.
|
||||
module ApplicationSettingMaskedAttrs
|
||||
MASK = '*****'
|
||||
|
||||
def ai_access_token=(value)
|
||||
return if value == MASK
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Searches a projects repository for a metrics dashboard and formats the output.
|
||||
# Expects any custom dashboards will be located in `.gitlab/dashboards`
|
||||
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class CustomDashboardService < ::Metrics::Dashboard::BaseService
|
||||
class << self
|
||||
def valid_params?(params)
|
||||
params[:dashboard_path].present?
|
||||
end
|
||||
|
||||
def all_dashboard_paths(project)
|
||||
project.repository.user_defined_metrics_dashboard_paths
|
||||
.map do |filepath|
|
||||
{
|
||||
path: filepath,
|
||||
display_name: name_for_path(filepath),
|
||||
default: false,
|
||||
system_dashboard: false,
|
||||
out_of_the_box_dashboard: out_of_the_box_dashboard?
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Grabs the filepath after the base directory.
|
||||
def name_for_path(filepath)
|
||||
filepath.delete_prefix("#{Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT}/")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Searches the project repo for a custom-defined dashboard.
|
||||
def get_raw_dashboard
|
||||
yml = Gitlab::Metrics::Dashboard::RepoDashboardFinder.read_dashboard(project, dashboard_path)
|
||||
|
||||
load_yaml(yml)
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"project_#{project.id}_metrics_dashboard_#{dashboard_path}"
|
||||
end
|
||||
|
||||
def sequence
|
||||
[
|
||||
::Gitlab::Metrics::Dashboard::Stages::CustomDashboardMetricsInserter
|
||||
] + super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Updates the content of a specified dashboard in .yml file inside `.gitlab/dashboards`
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class UpdateDashboardService < ::BaseService
|
||||
include Stepable
|
||||
|
||||
ALLOWED_FILE_TYPE = '.yml'
|
||||
USER_DASHBOARDS_DIR = ::Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT
|
||||
|
||||
steps :check_push_authorized,
|
||||
:check_branch_name,
|
||||
:check_file_type,
|
||||
:update_file,
|
||||
:create_merge_request
|
||||
|
||||
def execute
|
||||
execute_steps
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_push_authorized(result)
|
||||
return error(_('You are not allowed to push into this branch. Create another branch or open a merge request.'), :forbidden) unless push_authorized?
|
||||
|
||||
success(result)
|
||||
end
|
||||
|
||||
def check_branch_name(result)
|
||||
return error(_('There was an error updating the dashboard, branch name is invalid.'), :bad_request) unless valid_branch_name?
|
||||
return error(_('There was an error updating the dashboard, branch named: %{branch} already exists.') % { branch: params[:branch] }, :bad_request) unless new_or_default_branch?
|
||||
|
||||
success(result)
|
||||
end
|
||||
|
||||
def check_file_type(result)
|
||||
return error(_('The file name should have a .yml extension'), :bad_request) unless target_file_type_valid?
|
||||
|
||||
success(result)
|
||||
end
|
||||
|
||||
def update_file(result)
|
||||
file_update_response = ::Files::UpdateService.new(project, current_user, dashboard_attrs).execute
|
||||
|
||||
if file_update_response[:status] == :success
|
||||
success(result.merge(file_update_response, http_status: :created, dashboard: dashboard_details))
|
||||
else
|
||||
error(file_update_response[:message], :bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
def create_merge_request(result)
|
||||
return success(result) if project.default_branch == branch
|
||||
|
||||
merge_request_params = {
|
||||
source_branch: branch,
|
||||
target_branch: project.default_branch,
|
||||
title: params[:commit_message]
|
||||
}
|
||||
merge_request = ::MergeRequests::CreateService.new(project: project, current_user: current_user, params: merge_request_params).execute
|
||||
|
||||
if merge_request.persisted?
|
||||
success(result.merge(merge_request: Gitlab::UrlBuilder.build(merge_request)))
|
||||
else
|
||||
error(merge_request.errors.full_messages.join(','), :bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
def push_authorized?
|
||||
Gitlab::UserAccess.new(current_user, container: project).can_push_to_branch?(branch)
|
||||
end
|
||||
|
||||
def valid_branch_name?
|
||||
Gitlab::GitRefValidator.validate(branch)
|
||||
end
|
||||
|
||||
def new_or_default_branch?
|
||||
!repository.branch_exists?(branch) || project.default_branch == branch
|
||||
end
|
||||
|
||||
def target_file_type_valid?
|
||||
File.extname(params[:file_name]) == ALLOWED_FILE_TYPE
|
||||
end
|
||||
|
||||
def dashboard_attrs
|
||||
{
|
||||
commit_message: params[:commit_message],
|
||||
file_path: update_dashboard_path,
|
||||
file_content: update_dashboard_content,
|
||||
encoding: 'text',
|
||||
branch_name: branch,
|
||||
start_branch: repository.branch_exists?(branch) ? branch : project.default_branch
|
||||
}
|
||||
end
|
||||
|
||||
def update_dashboard_path
|
||||
File.join(USER_DASHBOARDS_DIR, file_name)
|
||||
end
|
||||
|
||||
def file_name
|
||||
@file_name ||= File.basename(CGI.unescape(params[:file_name]))
|
||||
end
|
||||
|
||||
def branch
|
||||
@branch ||= params[:branch]
|
||||
end
|
||||
|
||||
def update_dashboard_content
|
||||
::PerformanceMonitoring::PrometheusDashboard.from_json(params[:file_content]).to_yaml
|
||||
end
|
||||
|
||||
def repository
|
||||
@repository ||= project.repository
|
||||
end
|
||||
|
||||
def dashboard_details
|
||||
{
|
||||
path: update_dashboard_path,
|
||||
display_name: ::Metrics::Dashboard::CustomDashboardService.name_for_path(update_dashboard_path),
|
||||
default: false,
|
||||
system_dashboard: false
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
- return if Gitlab.org_or_com?
|
||||
|
||||
- expanded = integration_expanded?('ai_access')
|
||||
- token_is_present = @application_setting.ai_access_token.present?
|
||||
- token_label = token_is_present ? s_('CodeSuggestionsSM|Enter new personal access token') : s_('CodeSuggestionsSM|Personal access token')
|
||||
- token_value = token_is_present ? ApplicationSettingMaskedAttrs::MASK : ''
|
||||
|
||||
%section.settings.no-animate#js-ai-access-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= s_('CodeSuggestionsSM|Code Suggestions')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= code_suggestions_description
|
||||
|
||||
.settings-content
|
||||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-ai-access-settings'), html: { class: 'fieldset-form', id: 'ai-access-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.gitlab_ui_checkbox_component :instance_level_code_suggestions_enabled,
|
||||
s_('CodeSuggestionsSM|Enable Code Suggestions for this instance %{beta}').html_safe % { beta: gl_badge_tag(_('Beta'), variant: :neutral, size: :sm) },
|
||||
help_text: code_suggestions_agreement
|
||||
= f.label :ai_access_token, token_label, class: 'label-bold'
|
||||
= f.password_field :ai_access_token, value: token_value, autocomplete: 'on', class: 'form-control gl-form-input', aria: { describedby: 'code_suggestions_token_explanation' }
|
||||
%p.form-text.text-muted{ id: 'code_suggestions_token_explanation' }
|
||||
= code_suggestions_token_explanation
|
||||
|
||||
= f.submit _('Save changes'), pajamas_button: true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_needs_parallel_matrix
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118839
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416479
|
||||
milestone: '16.3'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
|
|
@ -6973,10 +6973,6 @@ Input type: `UserSetNamespaceCommitEmailInput`
|
|||
|
||||
### `Mutation.vulnerabilitiesDismiss`
|
||||
|
||||
WARNING:
|
||||
**Introduced** in 16.2.
|
||||
This feature is an Experiment. It can be changed or removed at any time.
|
||||
|
||||
Input type: `VulnerabilitiesDismissInput`
|
||||
|
||||
#### Arguments
|
||||
|
|
@ -6994,7 +6990,7 @@ Input type: `VulnerabilitiesDismissInput`
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationvulnerabilitiesdismissclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationvulnerabilitiesdismisserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationvulnerabilitiesdismissvulnerabilities"></a>`vulnerabilities` **{warning-solid}** | [`[Vulnerability!]!`](#vulnerability) | **Deprecated:** This feature is an Experiment. It can be changed or removed at any time. Introduced in 16.2. |
|
||||
| <a id="mutationvulnerabilitiesdismissvulnerabilities"></a>`vulnerabilities` | [`[Vulnerability!]!`](#vulnerability) | Vulnerabilities after state change. |
|
||||
|
||||
### `Mutation.vulnerabilityConfirm`
|
||||
|
||||
|
|
|
|||
|
|
@ -832,6 +832,83 @@ deploy:
|
|||
|
||||
Quotes around the `dependencies` entry are required.
|
||||
|
||||
## Specify a parallelized job using needs with multiple parallelized jobs
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/254821) in GitLab 16.3 [with a flag](../../administration/feature_flags.md) named `ci_needs_parallel_matrix`. Disabled by default.
|
||||
|
||||
You can use variables defined in [`needs:parallel:matrix`](../yaml/index.md#needsparallelmatrix) with multiple parallelized jobs.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
linux:build:
|
||||
stage: build
|
||||
script: echo "Building linux..."
|
||||
parallel:
|
||||
matrix:
|
||||
- PROVIDER: aws
|
||||
STACK:
|
||||
- monitoring
|
||||
- app1
|
||||
- app2
|
||||
|
||||
mac:build:
|
||||
stage: build
|
||||
script: echo "Building mac..."
|
||||
parallel:
|
||||
matrix:
|
||||
- PROVIDER: [gcp, vultr]
|
||||
STACK: [data, processing]
|
||||
|
||||
linux:rspec:
|
||||
stage: test
|
||||
needs:
|
||||
- job: linux:build
|
||||
parallel:
|
||||
matrix:
|
||||
- PROVIDER: aws
|
||||
- STACK: app1
|
||||
script: echo "Running rspec on linux..."
|
||||
|
||||
mac:rspec:
|
||||
stage: test
|
||||
needs:
|
||||
- job: mac:build
|
||||
parallel:
|
||||
matrix:
|
||||
- PROVIDER: [gcp, vultr]
|
||||
- STACK: [data]
|
||||
script: echo "Running rspec on mac..."
|
||||
|
||||
production:
|
||||
stage: deploy
|
||||
script: echo "Running production..."
|
||||
environment: production
|
||||
```
|
||||
|
||||
This example generates several jobs. The parallel jobs each have different values
|
||||
for `PROVIDER` and `STACK`.
|
||||
|
||||
- 3 parallel `linux:build` jobs:
|
||||
- `linux:build: [aws, monitoring]`
|
||||
- `linux:build: [aws, app1]`
|
||||
- `linux:build: [aws, app2]`
|
||||
- 4 parallel `mac:build` jobs:
|
||||
- `mac:build: [gcp, data]`
|
||||
- `mac:build: [gcp, processing]`
|
||||
- `mac:build: [vultr, data]`
|
||||
- `mac:build: [vultr, processing]`
|
||||
- A `linux:rspec` job.
|
||||
- A `production` job.
|
||||
|
||||
The jobs have three paths of execution:
|
||||
|
||||
- Linux path: The `linux:rspec` job runs as soon as the `linux:build: [aws, app1]`
|
||||
job finishes, without waiting for `mac:build` to finish.
|
||||
- macOS path: The `mac:rspec` job runs as soon as the `mac:build: [gcp, data]` and
|
||||
`mac:build: [vultr, data]` jobs finish, without waiting for `linux:build` to finish.
|
||||
- The `production` job runs as soon as all previous jobs finish.
|
||||
|
||||
## Use predefined CI/CD variables to run jobs only in specific pipeline types
|
||||
|
||||
You can use [predefined CI/CD variables](../variables/predefined_variables.md) to choose
|
||||
|
|
|
|||
|
|
@ -2389,6 +2389,8 @@ This example creates four paths of execution:
|
|||
it depends on all jobs created in parallel, not just one job. It also downloads
|
||||
artifacts from all the parallel jobs by default. If the artifacts have the same
|
||||
name, they overwrite each other and only the last one downloaded is saved.
|
||||
- To have `needs` refer to a subset of parallelized jobs (and not all of the parallelized jobs),
|
||||
use the [`needs:parallel:matrix`](#needsparallelmatrix) keyword.
|
||||
- In [GitLab 14.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632) you
|
||||
can refer to jobs in the same stage as the job you are configuring. This feature is
|
||||
enabled on GitLab.com and ready for production use. On self-managed [GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632)
|
||||
|
|
@ -2679,6 +2681,61 @@ upstream_status:
|
|||
- If you add the `job` keyword to `needs:pipeline`, the job no longer mirrors the
|
||||
pipeline status. The behavior changes to [`needs:pipeline:job`](#needspipelinejob).
|
||||
|
||||
#### `needs:parallel:matrix`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/254821) in GitLab 16.3 [with a flag](../../administration/feature_flags.md) named `ci_needs_parallel_matrix`. Disabled by default.
|
||||
|
||||
Jobs can use [`parallel:matrix`](#parallelmatrix) to run a job multiple times in parallel in a single pipeline,
|
||||
but with different variable values for each instance of the job.
|
||||
|
||||
Use `needs:parallel:matrix` to execute jobs out-of-order depending on parallelized jobs.
|
||||
|
||||
**Keyword type**: Job keyword. You can use it only as part of a job. Must be used with `needs:job`.
|
||||
|
||||
**Possible inputs**: An array of hashes of variables:
|
||||
|
||||
- The variables and values must be selected from the variables and values defined in the `parallel:matrix` job.
|
||||
|
||||
**Example of `needs:parallel:matrix`**:
|
||||
|
||||
```yaml
|
||||
linux:build:
|
||||
stage: build
|
||||
script: echo "Building linux..."
|
||||
parallel:
|
||||
matrix:
|
||||
- PROVIDER: aws
|
||||
STACK:
|
||||
- monitoring
|
||||
- app1
|
||||
- app2
|
||||
|
||||
linux:rspec:
|
||||
stage: test
|
||||
needs:
|
||||
- job: linux:build
|
||||
parallel:
|
||||
matrix:
|
||||
- PROVIDER: aws
|
||||
- STACK: app1
|
||||
script: echo "Running rspec on linux..."
|
||||
```
|
||||
|
||||
The above example generates the following jobs:
|
||||
|
||||
```plaintext
|
||||
linux:build: [aws, monitoring]
|
||||
linux:build: [aws, app1]
|
||||
linux:build: [aws, app2]
|
||||
linux:rspec
|
||||
```
|
||||
|
||||
The `linux:rspec` job runs as soon as the `linux:build: [aws, app1]` job finishes.
|
||||
|
||||
**Related topics**:
|
||||
|
||||
- [Specify a parallelized job using needs with multiple parallelized jobs](../jobs/job_control.md#specify-a-parallelized-job-using-needs-with-multiple-parallelized-jobs).
|
||||
|
||||
### `only` / `except`
|
||||
|
||||
NOTE:
|
||||
|
|
|
|||
|
|
@ -526,7 +526,7 @@ NOTE:
|
|||
This Rake task needs `docker` to be installed.
|
||||
|
||||
To update generated code for OpenAPI client located in
|
||||
`vendor/gems/error_tracking_open_api` run the following commands:
|
||||
`gems/error_tracking_open_api` run the following commands:
|
||||
|
||||
```shell
|
||||
# Run rake task
|
||||
|
|
@ -535,7 +535,7 @@ bundle exec rake gems:error_tracking_open_api:generate
|
|||
# Review and test the changes
|
||||
|
||||
# Commit the changes
|
||||
git commit -m 'Update ErrorTrackingOpenAPI from OpenAPI definition' vendor/gems/error_tracking_open_api
|
||||
git commit -m 'Update ErrorTrackingOpenAPI from OpenAPI definition' gems/error_tracking_open_api
|
||||
```
|
||||
|
||||
## Update banned SSH keys
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by `rake gems:error_tracking_open_api:generate` on 2023-06-08
|
||||
# Generated by `rake gems:error_tracking_open_api:generate` on 2023-07-13
|
||||
|
||||
See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ Gem::Specification.new do |s|
|
|||
|
||||
s.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1'
|
||||
|
||||
s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'
|
||||
|
||||
|
||||
s.files = Dir.glob("lib/**/*")
|
||||
s.test_files = []
|
||||
|
|
@ -42,11 +42,15 @@ module Gitlab
|
|||
end
|
||||
|
||||
class JobHash < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
include ::Gitlab::Config::Entry::Attributable
|
||||
include ::Gitlab::Config::Entry::Configurable
|
||||
|
||||
ALLOWED_KEYS = %i[job artifacts optional].freeze
|
||||
attributes :job, :artifacts, :optional
|
||||
ALLOWED_KEYS = %i[job artifacts optional parallel].freeze
|
||||
attributes :job, :artifacts, :optional, :parallel
|
||||
|
||||
entry :parallel, Entry::Product::Parallel,
|
||||
description: 'Parallel needs configuration for this job',
|
||||
inherit: true
|
||||
|
||||
validations do
|
||||
validates :config, presence: true
|
||||
|
|
@ -61,9 +65,15 @@ module Gitlab
|
|||
end
|
||||
|
||||
def value
|
||||
{ name: job,
|
||||
result = {
|
||||
name: job,
|
||||
artifacts: artifacts || artifacts.nil?,
|
||||
optional: !!optional }
|
||||
optional: !!optional
|
||||
}
|
||||
|
||||
result[:parallel] = parallel_value if Feature.enabled?(:ci_needs_parallel_matrix) && has_parallel?
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ module Gitlab
|
|||
if config.is_a?(Hash) && config.empty?
|
||||
errors.add(:config, 'can not be an empty Hash')
|
||||
end
|
||||
|
||||
if Feature.enabled?(:ci_needs_parallel_matrix) && number_parallel_build?
|
||||
errors.add(:config, 'cannot use "parallel: <number>".')
|
||||
end
|
||||
end
|
||||
|
||||
validate on: :composed do
|
||||
|
|
@ -47,6 +51,14 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def number_parallel_build?
|
||||
if config.is_a?(Array)
|
||||
config.any? { |need_values| need_values.is_a?(Hash) && need_values[:parallel].is_a?(Numeric) }
|
||||
elsif config.is_a?(Hash)
|
||||
config[:parallel].is_a?(Numeric)
|
||||
end
|
||||
end
|
||||
|
||||
def composable_class
|
||||
Entry::Need
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ module Gitlab
|
|||
job_need_name = job_need[:name].to_sym
|
||||
|
||||
if all_jobs = parallelized_jobs[job_need_name]
|
||||
if Feature.enabled?(:ci_needs_parallel_matrix) && job_need.key?(:parallel)
|
||||
all_jobs = parallelize_job_config(job_need_name, job_need.delete(:parallel))
|
||||
end
|
||||
|
||||
all_jobs.map { |job| job_need.merge(name: job.name) }
|
||||
else
|
||||
job_need
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ module Gitlab
|
|||
# display_name: String,
|
||||
# default: Boolean }]
|
||||
def find_all_paths(project)
|
||||
dashboards = user_facing_dashboard_services.flat_map do |service|
|
||||
dashboards = PREDEFINED_DASHBOARD_LIST.flat_map do |service|
|
||||
service.all_dashboard_paths(project)
|
||||
end
|
||||
|
||||
|
|
@ -72,18 +72,10 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def user_facing_dashboard_services
|
||||
PREDEFINED_DASHBOARD_LIST + [project_service]
|
||||
end
|
||||
|
||||
def system_service
|
||||
::Metrics::Dashboard::SystemDashboardService
|
||||
end
|
||||
|
||||
def project_service
|
||||
::Metrics::Dashboard::CustomDashboardService
|
||||
end
|
||||
|
||||
def service_for(options)
|
||||
Gitlab::Metrics::Dashboard::ServiceSelector.call(options)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ module Gitlab
|
|||
::Metrics::Dashboard::GitlabAlertEmbedService,
|
||||
::Metrics::Dashboard::CustomMetricEmbedService,
|
||||
::Metrics::Dashboard::DefaultEmbedService,
|
||||
::Metrics::Dashboard::SystemDashboardService,
|
||||
::Metrics::Dashboard::CustomDashboardService
|
||||
::Metrics::Dashboard::SystemDashboardService
|
||||
].freeze
|
||||
|
||||
# Returns a class which inherits from the BaseService
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ namespace :gems do
|
|||
end
|
||||
|
||||
def root_directory
|
||||
File.expand_path('../../vendor/gems', __dir__)
|
||||
File.expand_path('../../gems', __dir__)
|
||||
end
|
||||
|
||||
def generate_gem(vendor_gem_dir:, api_url:, gem_name:, module_name:, docker_image:)
|
||||
|
|
@ -53,14 +53,18 @@ namespace :gems do
|
|||
write_file(gem_dir / 'LICENSE', license)
|
||||
write_file(gem_dir / "#{gem_name}.gemspec") do |content|
|
||||
replace_string(content, 'Unlicense', 'MIT')
|
||||
replace_string(content, /.*add_development_dependency 'rspec'.*/, '')
|
||||
replace_string(content, /(\.files\s*=).*/, '\1 Dir.glob("lib/**/*")')
|
||||
replace_string(content, /(\.test_files\s*=).*/, '\1 []')
|
||||
end
|
||||
|
||||
# This is gem is supposed to be generated. No developer should change code.
|
||||
remove_entry_secure(gem_dir / 'Gemfile')
|
||||
# The generated code doesn't align well with `gitlab-styles` configuration.
|
||||
remove_entry_secure(gem_dir / '.rubocop.yml')
|
||||
remove_entry_secure(gem_dir / '.travis.yml')
|
||||
remove_entry_secure(gem_dir / 'git_push.sh')
|
||||
# The RSpec examples are stubs and have no value.
|
||||
remove_entry_secure(gem_dir / 'spec')
|
||||
remove_entry_secure(gem_dir / '.rspec')
|
||||
end
|
||||
|
|
@ -78,14 +82,16 @@ namespace :gems do
|
|||
end
|
||||
|
||||
def readme_banner(task)
|
||||
# rubocop:disable Rails/TimeZone
|
||||
<<~BANNER
|
||||
# Generated by `rake #{task.name}` on #{Time.now.strftime('%Y-%m-%d')}
|
||||
# #{generated_by(task)}
|
||||
|
||||
See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature
|
||||
|
||||
BANNER
|
||||
# rubocop:enable Rails/TimeZone
|
||||
end
|
||||
|
||||
def generated_by(task)
|
||||
"Generated by `rake #{task.name}` on #{Time.now.strftime('%Y-%m-%d')}" # rubocop:disable Rails/TimeZone
|
||||
end
|
||||
|
||||
def license
|
||||
|
|
|
|||
|
|
@ -1966,6 +1966,9 @@ msgstr ""
|
|||
msgid "AI|Generate issue description"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI|GitLab Duo is %{transition} an answer"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI|Helpful"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2035,6 +2038,18 @@ msgstr ""
|
|||
msgid "AI|You can ask AI for more information."
|
||||
msgstr ""
|
||||
|
||||
msgid "AI|finding"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI|generating"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI|producing"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI|working on"
|
||||
msgstr ""
|
||||
|
||||
msgid "API"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46594,9 +46609,6 @@ msgstr ""
|
|||
msgid "The file has been successfully deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "The file name should have a .yml extension"
|
||||
msgstr ""
|
||||
|
||||
msgid "The finding is not a vulnerability because it is part of a test or is test data."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47294,12 +47306,6 @@ msgstr ""
|
|||
msgid "There was an error updating the Maintenance Mode Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error updating the dashboard, branch name is invalid."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error updating the dashboard, branch named: %{branch} already exists."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error when reseting email token."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53194,9 +53200,6 @@ msgstr ""
|
|||
msgid "You are not allowed to log in using password"
|
||||
msgstr ""
|
||||
|
||||
msgid "You are not allowed to push into this branch. Create another branch or open a merge request."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are not allowed to reject a user"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml';
|
|||
import HooksYaml from './yaml_tests/positive_tests/hooks.yml';
|
||||
import SecretsYaml from './yaml_tests/positive_tests/secrets.yml';
|
||||
import ServicesYaml from './yaml_tests/positive_tests/services.yml';
|
||||
import NeedsParallelMatrixYaml from './yaml_tests/positive_tests/needs_parallel_matrix.yml';
|
||||
|
||||
// YAML NEGATIVE TEST
|
||||
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
|
||||
|
|
@ -56,6 +57,9 @@ import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml';
|
|||
import HooksNegative from './yaml_tests/negative_tests/hooks.yml';
|
||||
import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml';
|
||||
import ServicesNegativeYaml from './yaml_tests/negative_tests/services.yml';
|
||||
import NeedsParallelMatrixNumericYaml from './yaml_tests/negative_tests/needs/parallel_matrix/numeric.yml';
|
||||
import NeedsParallelMatrixWrongParallelValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_parallel_value.yml';
|
||||
import NeedsParallelMatrixWrongMatrixValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_matrix_value.yml';
|
||||
|
||||
const ajv = new Ajv({
|
||||
strictTypes: false,
|
||||
|
|
@ -96,6 +100,7 @@ describe('positive tests', () => {
|
|||
IdTokensYaml,
|
||||
ServicesYaml,
|
||||
SecretsYaml,
|
||||
NeedsParallelMatrixYaml,
|
||||
}),
|
||||
)('schema validates %s', (_, input) => {
|
||||
// We construct a new "JSON" from each main key that is inside a
|
||||
|
|
@ -136,6 +141,9 @@ describe('negative tests', () => {
|
|||
ProjectPathIncludeTailSlashYaml,
|
||||
SecretsNegativeYaml,
|
||||
ServicesNegativeYaml,
|
||||
NeedsParallelMatrixNumericYaml,
|
||||
NeedsParallelMatrixWrongParallelValueYaml,
|
||||
NeedsParallelMatrixWrongMatrixValueYaml,
|
||||
}),
|
||||
)('schema validates %s', (_, input) => {
|
||||
// We construct a new "JSON" from each main key that is inside a
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
# invalid needs:parallel:matrix where parallel has a numeric value
|
||||
job_with_needs_parallel_matrix:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel: 10
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# invalid needs:parallel:matrix where matrix value is incorrect
|
||||
job_with_needs_parallel_matrix:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel:
|
||||
matrix: 10
|
||||
|
||||
job_with_needs_parallel_matrix_2:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel:
|
||||
matrix: "string"
|
||||
|
||||
job_with_needs_parallel_matrix_3:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel:
|
||||
matrix: [a1, a2]
|
||||
|
||||
job_with_needs_parallel_matrix_4:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel:
|
||||
matrix:
|
||||
VAR_1: 1
|
||||
VAR_2: 2
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# invalid needs:parallel:matrix where parallel value is incorrect
|
||||
job_with_needs_parallel_matrix:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel:
|
||||
not_matrix:
|
||||
- VAR_1: [a1, a2]
|
||||
VAR_2: [b1, b2]
|
||||
|
||||
job_with_needs_parallel_matrix_2:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel: [a1, a2]
|
||||
|
||||
job_with_needs_parallel_matrix_3:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel: "matrix"
|
||||
|
||||
job_with_needs_parallel_matrix_4:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: some_job_with_parallel_matrix
|
||||
parallel: object
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# valid needs:parallel:matrix
|
||||
job_with_needs_parallel_matrix:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: job_with_parallel_matrix
|
||||
parallel:
|
||||
matrix:
|
||||
- VAR_1: [a]
|
||||
VAR_2: [d]
|
||||
|
||||
job_with_needs_parallel_matrix_2:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: job_with_parallel_matrix
|
||||
parallel:
|
||||
matrix:
|
||||
- VAR_1: a
|
||||
VAR_2: d
|
||||
|
||||
job_with_needs_parallel_matrix_3:
|
||||
script: exit 0
|
||||
needs:
|
||||
- job: job_with_parallel_matrix
|
||||
parallel:
|
||||
matrix:
|
||||
- VAR_1: ["a", b]
|
||||
VAR_2: d
|
||||
- job: job_with_parallel_matrix_2
|
||||
parallel:
|
||||
matrix:
|
||||
- VAR_1: [a, "b", c]
|
||||
VAR_5: [d, "e"]
|
||||
|
|
@ -31,24 +31,4 @@ RSpec.describe Admin::ApplicationSettings::SettingsHelper do
|
|||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Code Suggestions for Self-Managed instances', feature_category: :code_suggestions do
|
||||
describe '#code_suggestions_description' do
|
||||
subject { helper.code_suggestions_description }
|
||||
|
||||
it { is_expected.to include 'https://docs.gitlab.com/ee/user/project/repository/code_suggestions.html' }
|
||||
end
|
||||
|
||||
describe '#code_suggestions_token_explanation' do
|
||||
subject { helper.code_suggestions_token_explanation }
|
||||
|
||||
it { is_expected.to include 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token' }
|
||||
end
|
||||
|
||||
describe '#code_suggestions_agreement' do
|
||||
subject { helper.code_suggestions_agreement }
|
||||
|
||||
it { is_expected.to include 'https://about.gitlab.com/handbook/legal/testing-agreement/' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Gitlab::Ci::Config::Entry::Need do
|
||||
RSpec.describe ::Gitlab::Ci::Config::Entry::Need, feature_category: :pipeline_composition do
|
||||
subject(:need) { described_class.new(config) }
|
||||
|
||||
shared_examples 'job type' do
|
||||
|
|
@ -219,6 +219,81 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do
|
|||
|
||||
it_behaves_like 'job type'
|
||||
end
|
||||
|
||||
context 'when parallel:matrix has a value' do
|
||||
before do
|
||||
need.compose!
|
||||
end
|
||||
|
||||
context 'and it is a string value' do
|
||||
let(:config) do
|
||||
{ job: 'job_name', parallel: { matrix: [{ platform: 'p1', stack: 's1' }] } }
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
describe '#value' do
|
||||
it 'returns job needs configuration' do
|
||||
expect(need.value).to eq(
|
||||
name: 'job_name',
|
||||
artifacts: true,
|
||||
optional: false,
|
||||
parallel: { matrix: [{ "platform" => ['p1'], "stack" => ['s1'] }] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'job type'
|
||||
end
|
||||
|
||||
context 'and it is an array value' do
|
||||
let(:config) do
|
||||
{ job: 'job_name', parallel: { matrix: [{ platform: %w[p1 p2], stack: %w[s1 s2] }] } }
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
describe '#value' do
|
||||
it 'returns job needs configuration' do
|
||||
expect(need.value).to eq(
|
||||
name: 'job_name',
|
||||
artifacts: true,
|
||||
optional: false,
|
||||
parallel: { matrix: [{ 'platform' => %w[p1 p2], 'stack' => %w[s1 s2] }] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'job type'
|
||||
end
|
||||
|
||||
context 'and it is a both an array and string value' do
|
||||
let(:config) do
|
||||
{ job: 'job_name', parallel: { matrix: [{ platform: %w[p1 p2], stack: 's1' }] } }
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
describe '#value' do
|
||||
it 'returns job needs configuration' do
|
||||
expect(need.value).to eq(
|
||||
name: 'job_name',
|
||||
artifacts: true,
|
||||
optional: false,
|
||||
parallel: { matrix: [{ 'platform' => %w[p1 p2], 'stack' => ['s1'] }] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'job type'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cross pipeline artifacts needs' do
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do
|
||||
RSpec.describe ::Gitlab::Ci::Config::Entry::Needs, feature_category: :pipeline_composition do
|
||||
subject(:needs) { described_class.new(config) }
|
||||
|
||||
before do
|
||||
|
|
@ -67,6 +67,141 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when needs value is a hash' do
|
||||
context 'with a job value' do
|
||||
let(:config) do
|
||||
{ job: 'job_name' }
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a parallel value that is a numeric value' do
|
||||
let(:config) do
|
||||
{ job: 'job_name', parallel: 2 }
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it 'returns errors about number values being invalid for needs:parallel' do
|
||||
expect(needs.errors).to match_array(["needs config cannot use \"parallel: <number>\"."])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when needs:parallel value is incorrect' do
|
||||
context 'with a keyword that is not "matrix"' do
|
||||
let(:config) do
|
||||
[
|
||||
{ job: 'job_name', parallel: { not_matrix: [{ one: 'aaa', two: 'bbb' }] } }
|
||||
]
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it 'returns errors about incorrect matrix keyword' do
|
||||
expect(needs.errors).to match_array([
|
||||
'need:parallel config contains unknown keys: not_matrix',
|
||||
'need:parallel config missing required keys: matrix'
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a number value' do
|
||||
let(:config) { [{ job: 'job_name', parallel: 2 }] }
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it 'returns errors about number values being invalid for needs:parallel' do
|
||||
expect(needs.errors).to match_array(["needs config cannot use \"parallel: <number>\"."])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when needs:parallel:matrix value is empty' do
|
||||
let(:config) { [{ job: 'job_name', parallel: { matrix: {} } }] }
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it 'returns error about incorrect type' do
|
||||
expect(needs.errors).to contain_exactly(
|
||||
'need:parallel:matrix config should be an array of hashes')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when needs:parallel:matrix value is incorrect' do
|
||||
let(:config) { [{ job: 'job_name', parallel: { matrix: 'aaa' } }] }
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it 'returns error about incorrect type' do
|
||||
expect(needs.errors).to contain_exactly(
|
||||
'need:parallel:matrix config should be an array of hashes')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when needs:parallel:matrix value is correct' do
|
||||
context 'with a simple config' do
|
||||
let(:config) do
|
||||
[
|
||||
{ job: 'job_name', parallel: { matrix: [{ A: 'a1', B: 'b1' }] } }
|
||||
]
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a complex config' do
|
||||
let(:config) do
|
||||
[
|
||||
{
|
||||
job: 'job_name1',
|
||||
artifacts: true,
|
||||
parallel: { matrix: [{ A: %w[a1 a2], B: %w[b1 b2 b3], C: %w[c1 c2] }] }
|
||||
},
|
||||
{
|
||||
job: 'job_name2',
|
||||
parallel: {
|
||||
matrix: [
|
||||
{ A: %w[a1 a2], D: %w[d1 d2] },
|
||||
{ E: %w[e1 e2], F: ['f1'] },
|
||||
{ C: %w[c1 c2 c3], G: %w[g1 g2], H: ['h1'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with too many cross pipeline dependencies' do
|
||||
let(:limit) { described_class::NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Normalizer do
|
||||
let(:job_name) { :rspec }
|
||||
|
|
@ -103,6 +103,50 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'needs:parallel:matrix' do
|
||||
let(:expanded_needs_parallel_job_attributes) do
|
||||
expanded_needs_parallel_job_names.map do |job_name|
|
||||
{ name: job_name }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job has needs:parallel:matrix on parallelized jobs' do
|
||||
let(:config) do
|
||||
{
|
||||
job_name => job_config,
|
||||
other_job: {
|
||||
script: 'echo 1',
|
||||
needs: {
|
||||
job: [
|
||||
{ name: job_name.to_s, parallel: needs_parallel_config }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'parallelizes and only keeps needs specified by needs:parallel:matrix' do
|
||||
expect(subject.dig(:other_job, :needs, :job)).to eq(expanded_needs_parallel_job_attributes)
|
||||
end
|
||||
|
||||
context 'when FF `ci_needs_parallel_matrix` is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_needs_parallel_matrix: false)
|
||||
end
|
||||
|
||||
let(:expanded_job_attributes) do
|
||||
expanded_job_names.map do |job_name|
|
||||
{ name: job_name, parallel: needs_parallel_config }
|
||||
end
|
||||
end
|
||||
|
||||
it 'keeps all parallelized jobs' do
|
||||
expect(subject.dig(:other_job, :needs, :job)).to eq(expanded_job_attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with parallel config as integer' do
|
||||
let(:variables_config) { {} }
|
||||
let(:parallel_config) { 5 }
|
||||
|
|
@ -167,7 +211,7 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do
|
|||
it_behaves_like 'parallel needs'
|
||||
end
|
||||
|
||||
context 'with parallel matrix config' do
|
||||
context 'with a simple parallel matrix config' do
|
||||
let(:variables_config) do
|
||||
{
|
||||
USER_VARIABLE: 'user value'
|
||||
|
|
@ -192,6 +236,19 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do
|
|||
]
|
||||
end
|
||||
|
||||
let(:needs_parallel_config) do
|
||||
{
|
||||
matrix: [
|
||||
{
|
||||
VAR_1: ['A'],
|
||||
VAR_2: ['C']
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:expanded_needs_parallel_job_names) { ['rspec: [A, C]'] }
|
||||
|
||||
it 'does not have original job' do
|
||||
is_expected.not_to include(job_name)
|
||||
end
|
||||
|
|
@ -228,6 +285,66 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do
|
|||
|
||||
it_behaves_like 'parallel dependencies'
|
||||
it_behaves_like 'parallel needs'
|
||||
it_behaves_like 'needs:parallel:matrix'
|
||||
end
|
||||
|
||||
context 'with a complex parallel matrix config' do
|
||||
let(:variables_config) { {} }
|
||||
let(:parallel_config) do
|
||||
{
|
||||
matrix: [
|
||||
{
|
||||
PLATFORM: ['centos'],
|
||||
STACK: %w[ruby python java],
|
||||
DB: %w[postgresql mysql]
|
||||
},
|
||||
{
|
||||
PLATFORM: ['ubuntu'],
|
||||
PROVIDER: %w[aws gcp]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:needs_parallel_config) do
|
||||
{
|
||||
matrix: [
|
||||
{
|
||||
PLATFORM: ['centos'],
|
||||
STACK: %w[ruby python],
|
||||
DB: ['postgresql']
|
||||
},
|
||||
{
|
||||
PLATFORM: ['ubuntu'],
|
||||
PROVIDER: ['aws']
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:expanded_needs_parallel_job_names) do
|
||||
[
|
||||
'rspec: [centos, ruby, postgresql]',
|
||||
'rspec: [centos, python, postgresql]',
|
||||
'rspec: [ubuntu, aws]'
|
||||
]
|
||||
end
|
||||
|
||||
let(:expanded_job_names) do
|
||||
[
|
||||
'rspec: [centos, ruby, postgresql]',
|
||||
'rspec: [centos, ruby, mysql]',
|
||||
'rspec: [centos, python, postgresql]',
|
||||
'rspec: [centos, python, mysql]',
|
||||
'rspec: [centos, java, postgresql]',
|
||||
'rspec: [centos, java, mysql]',
|
||||
'rspec: [ubuntu, aws]',
|
||||
'rspec: [ubuntu, gcp]'
|
||||
]
|
||||
end
|
||||
|
||||
it_behaves_like 'parallel needs'
|
||||
it_behaves_like 'needs:parallel:matrix'
|
||||
end
|
||||
|
||||
context 'when parallel config does not matches a factory' do
|
||||
|
|
|
|||
|
|
@ -2675,6 +2675,42 @@ module Gitlab
|
|||
|
||||
it_behaves_like 'returns errors', 'jobs:test1 dependencies should be an array of strings'
|
||||
end
|
||||
|
||||
context 'needs with parallel:matrix' do
|
||||
let(:config) do
|
||||
{
|
||||
build1: {
|
||||
stage: 'build',
|
||||
script: 'build',
|
||||
parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': %w[monitoring app1 app2] }] }
|
||||
},
|
||||
test1: {
|
||||
stage: 'test',
|
||||
script: 'test',
|
||||
needs: [{ job: 'build1', parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': ['app1'] }] } }]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it "does create jobs with valid specification" do
|
||||
expect(subject.builds.size).to eq(4)
|
||||
expect(subject.builds[3]).to eq(
|
||||
stage: "test",
|
||||
stage_idx: 2,
|
||||
name: "test1",
|
||||
only: { refs: %w[branches tags] },
|
||||
options: { script: ["test"] },
|
||||
needs_attributes: [
|
||||
{ name: "build1: [aws, app1]", artifacts: true, optional: false }
|
||||
],
|
||||
when: "on_success",
|
||||
allow_failure: false,
|
||||
job_variables: [],
|
||||
root_variables_inheritance: true,
|
||||
scheduling_type: :dag
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with when/rules' do
|
||||
|
|
|
|||
|
|
@ -17,20 +17,12 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
|
|||
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
|
||||
let(:service_call) { described_class.find(project, user, environment: environment, dashboard_path: dashboard_path) }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
|
||||
context 'when the dashboard exists' do
|
||||
let(:project) { project_with_dashboard(dashboard_path) }
|
||||
|
||||
it_behaves_like 'valid dashboard service response'
|
||||
end
|
||||
|
||||
context 'when the dashboard is configured incorrectly' do
|
||||
let(:project) { project_with_dashboard(dashboard_path, {}) }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
|
||||
end
|
||||
|
||||
context 'when the system dashboard is specified' do
|
||||
let(:dashboard_path) { system_dashboard_path }
|
||||
|
||||
|
|
@ -103,14 +95,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
|
|||
|
||||
it { is_expected.to eq dashboard }
|
||||
end
|
||||
|
||||
context 'when an existing project dashboard is specified' do
|
||||
let(:dashboard) { load_sample_dashboard }
|
||||
let(:params) { { dashboard_path: '.gitlab/dashboards/test.yml' } }
|
||||
let(:project) { project_with_dashboard(params[:dashboard_path]) }
|
||||
|
||||
it { is_expected.to eq dashboard }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find_all_paths' do
|
||||
|
|
@ -120,37 +104,5 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
|
|||
it 'includes OOTB dashboards by default' do
|
||||
expect(all_dashboard_paths).to eq([system_dashboard])
|
||||
end
|
||||
|
||||
context 'when the project contains dashboards' do
|
||||
let(:dashboard_content) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
|
||||
let(:project) { project_with_dashboards(dashboards) }
|
||||
|
||||
let(:dashboards) do
|
||||
{
|
||||
'.gitlab/dashboards/metrics.yml' => dashboard_content,
|
||||
'.gitlab/dashboards/better_metrics.yml' => dashboard_content
|
||||
}
|
||||
end
|
||||
|
||||
it 'includes OOTB and project dashboards' do
|
||||
project_dashboard1 = {
|
||||
path: '.gitlab/dashboards/metrics.yml',
|
||||
display_name: 'metrics.yml',
|
||||
default: false,
|
||||
system_dashboard: false,
|
||||
out_of_the_box_dashboard: false
|
||||
}
|
||||
|
||||
project_dashboard2 = {
|
||||
path: '.gitlab/dashboards/better_metrics.yml',
|
||||
display_name: 'better_metrics.yml',
|
||||
default: false,
|
||||
system_dashboard: false,
|
||||
out_of_the_box_dashboard: false
|
||||
}
|
||||
|
||||
expect(all_dashboard_paths).to eq([project_dashboard2, project_dashboard1, system_dashboard])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::ServiceSelector do
|
|||
it { is_expected.to be Metrics::Dashboard::SystemDashboardService }
|
||||
|
||||
context 'when just the dashboard path is provided' do
|
||||
let(:arguments) { { dashboard_path: '.gitlab/dashboards/test.yml' } }
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::CustomDashboardService }
|
||||
|
||||
context 'when the path is for the system dashboard' do
|
||||
let(:arguments) { { dashboard_path: system_dashboard_path } }
|
||||
|
||||
|
|
|
|||
|
|
@ -1687,29 +1687,4 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
|
|||
expect(setting.personal_access_tokens_disabled?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ai_access_token' do
|
||||
context 'when `instance_level_code_suggestions_enabled` is true' do
|
||||
before do
|
||||
setting.instance_level_code_suggestions_enabled = true
|
||||
end
|
||||
|
||||
it { is_expected.not_to allow_value(nil).for(:ai_access_token) }
|
||||
end
|
||||
|
||||
context 'when `instance_level_code_suggestions_enabled` is false' do
|
||||
before do
|
||||
setting.instance_level_code_suggestions_enabled = false
|
||||
end
|
||||
|
||||
it { is_expected.to allow_value(nil).for(:ai_access_token) }
|
||||
end
|
||||
|
||||
it 'does not modify the token if it is unchanged in the form' do
|
||||
setting.ai_access_token = 'foo'
|
||||
setting.ai_access_token = ApplicationSettingMaskedAttrs::MASK
|
||||
|
||||
expect(setting.ai_access_token).to eq('foo')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -229,16 +229,6 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
|
|||
allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find_raw).with(project, dashboard_path: path).and_call_original
|
||||
end
|
||||
|
||||
context 'when schema is valid' do
|
||||
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
|
||||
|
||||
it 'returns empty array' do
|
||||
expect(described_class).to receive(:from_json).with(dashboard_schema)
|
||||
|
||||
expect(schema_validation_warnings).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context 'when schema is invalid' do
|
||||
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
|
||||
|
||||
|
|
@ -250,28 +240,5 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
|
|||
expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
|
||||
end
|
||||
end
|
||||
|
||||
context 'when YAML has wrong syntax' do
|
||||
let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
|
||||
|
||||
subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
|
||||
|
||||
it 'returns array with errors messages' do
|
||||
expect(described_class).not_to receive(:from_json)
|
||||
|
||||
expect(schema_validation_warnings).to eq ['Invalid yaml']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_yaml' do
|
||||
subject { prometheus_dashboard.to_yaml }
|
||||
|
||||
let(:prometheus_dashboard) { described_class.from_json(json_content) }
|
||||
let(:expected_yaml) do
|
||||
"---\npanel_groups:\n- panels:\n - metrics:\n - id: metric_of_ages\n unit: count\n label: Metric of Ages\n query: \n query_range: http_requests_total\n type: area-chart\n title: Chart Title\n y_label: Y-Axis\n weight: \n group: Group Title\n priority: \ndashboard: Dashboard Title\n"
|
||||
end
|
||||
|
||||
it { is_expected.to eq(expected_yaml) }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,36 +25,6 @@ RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics d
|
|||
end
|
||||
|
||||
context 'with correct permissions' do
|
||||
context 'with valid parameters' do
|
||||
context 'dashboard_path as url param url escaped' do
|
||||
it 'creates a new user starred metrics dashboard', :aggregate_failures do
|
||||
post api(url, user), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['project_id']).to eq(project.id)
|
||||
expect(json_response['user_id']).to eq(user.id)
|
||||
expect(json_response['dashboard_path']).to eq(dashboard)
|
||||
end
|
||||
end
|
||||
|
||||
context 'dashboard_path in request body unescaped' do
|
||||
let(:params) do
|
||||
{
|
||||
dashboard_path: dashboard
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a new user starred metrics dashboard', :aggregate_failures do
|
||||
post api(url, user), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['project_id']).to eq(project.id)
|
||||
expect(json_response['user_id']).to eq(user.id)
|
||||
expect(json_response['dashboard_path']).to eq(dashboard)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
it 'returns error message' do
|
||||
post api(url, user), params: { dashboard_path: '' }
|
||||
|
|
|
|||
|
|
@ -143,19 +143,5 @@ RSpec.describe Metrics::Dashboard::Annotations::CreateService, feature_category:
|
|||
it_behaves_like 'annotation creation failure'
|
||||
end
|
||||
end
|
||||
|
||||
context 'incorrect dashboard_path' do
|
||||
let(:cluster) { create(:cluster, :project) }
|
||||
let(:environment) { nil }
|
||||
let(:dashboard_path) { 'something_incorrect.yml' }
|
||||
|
||||
context 'with maintainer user' do
|
||||
before do
|
||||
cluster.project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'prevented annotation creation', 'Dashboard with requested path can not be found'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,167 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Metrics::Dashboard::CustomDashboardService, :use_clean_rails_memory_store_caching,
|
||||
feature_category: :metrics do
|
||||
include MetricsDashboardHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:environment) { create(:environment, project: project) }
|
||||
|
||||
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
|
||||
let(:service_params) { [project, user, { environment: environment, dashboard_path: dashboard_path }] }
|
||||
|
||||
subject { described_class.new(*service_params) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user) if user
|
||||
end
|
||||
|
||||
describe '#raw_dashboard' do
|
||||
let(:project) { project_with_dashboard(dashboard_path) }
|
||||
|
||||
it_behaves_like '#raw_dashboard raises error if dashboard loading fails'
|
||||
end
|
||||
|
||||
describe '#get_dashboard' do
|
||||
let(:service_call) { subject.get_dashboard }
|
||||
|
||||
context 'when the dashboard does not exist' do
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
|
||||
it 'does not update gitlab_metrics_dashboard_processing_time_ms metric', :prometheus do
|
||||
service_call
|
||||
metric = subject.send(:processing_time_metric)
|
||||
labels = subject.send(:processing_time_metric_labels)
|
||||
|
||||
expect(metric.get(labels)).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'raises error for users with insufficient permissions'
|
||||
|
||||
context 'when the dashboard exists' do
|
||||
let(:project) { project_with_dashboard(dashboard_path) }
|
||||
|
||||
it_behaves_like 'valid dashboard service response'
|
||||
it_behaves_like 'updates gitlab_metrics_dashboard_processing_time_ms metric'
|
||||
|
||||
it 'caches the unprocessed dashboard for subsequent calls' do
|
||||
expect_any_instance_of(described_class)
|
||||
.to receive(:get_raw_dashboard)
|
||||
.once
|
||||
.and_call_original
|
||||
|
||||
described_class.new(*service_params).get_dashboard
|
||||
described_class.new(*service_params).get_dashboard
|
||||
end
|
||||
|
||||
it 'tracks panel type' do
|
||||
allow(::Gitlab::Tracking).to receive(:event).and_call_original
|
||||
|
||||
described_class.new(*service_params).get_dashboard
|
||||
|
||||
expect(::Gitlab::Tracking).to have_received(:event)
|
||||
.with('MetricsDashboard::Chart', 'chart_rendered', { label: 'area-chart' })
|
||||
.at_least(:once)
|
||||
end
|
||||
|
||||
context 'with metric in database' do
|
||||
let!(:prometheus_metric) do
|
||||
create(:prometheus_metric, project: project, identifier: 'metric_a1', group: 'custom')
|
||||
end
|
||||
|
||||
it 'includes metric_id' do
|
||||
dashboard = described_class.new(*service_params).get_dashboard
|
||||
|
||||
metric_id = dashboard[:dashboard][:panel_groups].find { |panel_group| panel_group[:group] == 'Group A' }
|
||||
.fetch(:panels).find { |panel| panel[:title] == 'Super Chart A1' }
|
||||
.fetch(:metrics).find { |metric| metric[:id] == 'metric_a1' }
|
||||
.fetch(:metric_id)
|
||||
|
||||
expect(metric_id).to eq(prometheus_metric.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the dashboard is then deleted' do
|
||||
it 'does not return the previously cached dashboard' do
|
||||
described_class.new(*service_params).get_dashboard
|
||||
|
||||
delete_project_dashboard(project, user, dashboard_path)
|
||||
|
||||
expect_any_instance_of(described_class)
|
||||
.to receive(:get_raw_dashboard)
|
||||
.once
|
||||
.and_call_original
|
||||
|
||||
described_class.new(*service_params).get_dashboard
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dashboard is configured incorrectly' do
|
||||
let(:project) { project_with_dashboard(dashboard_path, {}) }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
describe '.all_dashboard_paths' do
|
||||
let(:all_dashboards) { described_class.all_dashboard_paths(project) }
|
||||
|
||||
context 'when there are no project dashboards' do
|
||||
it 'returns an empty array' do
|
||||
expect(all_dashboards).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are project dashboards available' do
|
||||
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
|
||||
let(:project) { project_with_dashboard(dashboard_path) }
|
||||
|
||||
it 'returns the dashboard attributes' do
|
||||
expect(all_dashboards).to eq(
|
||||
[{
|
||||
path: dashboard_path,
|
||||
display_name: 'test.yml',
|
||||
default: false,
|
||||
system_dashboard: false,
|
||||
out_of_the_box_dashboard: false
|
||||
}]
|
||||
)
|
||||
end
|
||||
|
||||
it 'caches repo file list' do
|
||||
expect(Gitlab::Metrics::Dashboard::RepoDashboardFinder).to receive(:list_dashboards)
|
||||
.with(project)
|
||||
.once
|
||||
.and_call_original
|
||||
|
||||
described_class.all_dashboard_paths(project)
|
||||
described_class.all_dashboard_paths(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.valid_params?' do
|
||||
let(:params) { { dashboard_path: '.gitlab/dashboard/test.yml' } }
|
||||
|
||||
subject { described_class.valid_params?(params) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'missing dashboard_path' do
|
||||
let(:params) { {} }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'empty dashboard_path' do
|
||||
let(:params) { { dashboard_path: '' } }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_store_caching, feature_category: :metrics do
|
||||
include MetricsDashboardHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:environment) { create(:environment, project: project) }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service_call) { described_class.new(project, user, params).execute }
|
||||
|
||||
let(:commit_message) { 'test' }
|
||||
let(:branch) { 'dashboard_new_branch' }
|
||||
let(:dashboard) { 'config/prometheus/common_metrics.yml' }
|
||||
let(:file_name) { 'custom_dashboard.yml' }
|
||||
let(:file_content_hash) { YAML.safe_load(File.read(dashboard)) }
|
||||
let(:params) do
|
||||
{
|
||||
file_name: file_name,
|
||||
file_content: file_content_hash,
|
||||
commit_message: commit_message,
|
||||
branch: branch
|
||||
}
|
||||
end
|
||||
|
||||
context 'user does not have push right to repository' do
|
||||
it_behaves_like 'misconfigured dashboard service response with stepable', :forbidden, 'You are not allowed to push into this branch. Create another branch or open a merge request.'
|
||||
end
|
||||
|
||||
context 'with rights to push to the repository' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'path traversal attack attempt' do
|
||||
context 'with a yml extension' do
|
||||
let(:file_name) { 'config/prometheus/../database.yml' }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, "A file with this name doesn't exist"
|
||||
end
|
||||
|
||||
context 'without a yml extension' do
|
||||
let(:file_name) { '../../..../etc/passwd' }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, 'The file name should have a .yml extension'
|
||||
end
|
||||
end
|
||||
|
||||
context 'valid parameters' do
|
||||
it_behaves_like 'valid dashboard update process'
|
||||
end
|
||||
|
||||
context 'selected branch already exists' do
|
||||
let(:branch) { 'existing_branch' }
|
||||
|
||||
before do
|
||||
project.repository.add_branch(user, branch, 'master')
|
||||
end
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, 'There was an error updating the dashboard, branch named: existing_branch already exists.'
|
||||
end
|
||||
|
||||
context 'Files::UpdateService success' do
|
||||
let(:merge_request) { project.merge_requests.last }
|
||||
|
||||
before do
|
||||
allow(::Files::UpdateService).to receive(:new).and_return(double(execute: { status: :success }))
|
||||
end
|
||||
|
||||
it 'returns success', :aggregate_failures do
|
||||
dashboard_details = {
|
||||
path: '.gitlab/dashboards/custom_dashboard.yml',
|
||||
display_name: 'custom_dashboard.yml',
|
||||
default: false,
|
||||
system_dashboard: false
|
||||
}
|
||||
|
||||
expect(service_call[:status]).to be :success
|
||||
expect(service_call[:http_status]).to be :created
|
||||
expect(service_call[:dashboard]).to match dashboard_details
|
||||
expect(service_call[:merge_request]).to eq(Gitlab::UrlBuilder.build(merge_request))
|
||||
end
|
||||
|
||||
context 'when the merge request does not succeed' do
|
||||
let(:error_message) { 'There was an error' }
|
||||
|
||||
let(:merge_request) do
|
||||
build(:merge_request, target_project: project, source_project: project, author: user)
|
||||
end
|
||||
|
||||
before do
|
||||
merge_request.errors.add(:base, error_message)
|
||||
allow_next_instance_of(::MergeRequests::CreateService) do |mr|
|
||||
allow(mr).to receive(:execute).and_return(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an appropriate message and status code', :aggregate_failures do
|
||||
result = service_call
|
||||
|
||||
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:http_status]).to eq(:bad_request)
|
||||
expect(result[:message]).to eq(error_message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with escaped characters in file name' do
|
||||
let(:file_name) { "custom_dashboard%26copy.yml" }
|
||||
|
||||
it 'escapes the special characters', :aggregate_failures do
|
||||
dashboard_details = {
|
||||
path: '.gitlab/dashboards/custom_dashboard©.yml',
|
||||
display_name: 'custom_dashboard©.yml',
|
||||
default: false,
|
||||
system_dashboard: false
|
||||
}
|
||||
|
||||
expect(service_call[:status]).to be :success
|
||||
expect(service_call[:http_status]).to be :created
|
||||
expect(service_call[:dashboard]).to match dashboard_details
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pushing to the default branch' do
|
||||
let(:branch) { 'master' }
|
||||
|
||||
it 'does not create a merge request', :aggregate_failures do
|
||||
dashboard_details = {
|
||||
path: '.gitlab/dashboards/custom_dashboard.yml',
|
||||
display_name: 'custom_dashboard.yml',
|
||||
default: false,
|
||||
system_dashboard: false
|
||||
}
|
||||
|
||||
expect(::MergeRequests::CreateService).not_to receive(:new)
|
||||
expect(service_call.keys).to contain_exactly(:dashboard, :http_status, :status)
|
||||
expect(service_call[:status]).to be :success
|
||||
expect(service_call[:http_status]).to be :created
|
||||
expect(service_call[:dashboard]).to match dashboard_details
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Files::UpdateService fails' do
|
||||
before do
|
||||
allow(::Files::UpdateService).to receive(:new).and_return(double(execute: { status: :error }))
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
expect(service_call[:status]).to be :error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'admin/application_settings/_ai_access.html.haml', feature_category: :code_suggestions do
|
||||
let_it_be(:admin) { build_stubbed(:admin) }
|
||||
let(:page) { Capybara::Node::Simple.new(rendered) }
|
||||
|
||||
before do
|
||||
allow(::Gitlab).to receive(:org_or_com?).and_return(false) # Will not render partial for .com or .org
|
||||
assign(:application_setting, application_setting)
|
||||
allow(view).to receive(:current_user) { admin }
|
||||
allow(view).to receive(:expanded).and_return(true)
|
||||
end
|
||||
|
||||
context 'when ai_access_token is not set' do
|
||||
let(:application_setting) { build(:application_setting) }
|
||||
|
||||
it 'renders an empty password field' do
|
||||
render
|
||||
expect(rendered).to have_field('Personal access token', type: 'password')
|
||||
expect(page.find_field('Personal access token').value).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ai_access_token is set' do
|
||||
let(:application_setting) do
|
||||
build(:application_setting, ai_access_token: 'ai_access_token',
|
||||
instance_level_code_suggestions_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders masked password field' do
|
||||
render
|
||||
expect(rendered).to have_field('Enter new personal access token', type: 'password')
|
||||
expect(page.find_field('Enter new personal access token').value).to eq(ApplicationSettingMaskedAttrs::MASK)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -114,29 +114,31 @@ RSpec.describe 'admin/application_settings/general.html.haml' do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'instance-level code suggestions settings', feature_category: :code_suggestions do
|
||||
# for the licensed tests, refer to ee/spec/views/admin/application_settings/general.html.haml_spec.rb
|
||||
describe 'instance-level code suggestions settings', :without_license, feature_category: :code_suggestions do
|
||||
before do
|
||||
allow(::Gitlab).to receive(:org_or_com?).and_return(gitlab_org_or_com?)
|
||||
|
||||
render
|
||||
end
|
||||
|
||||
context 'when on .com or .org' do
|
||||
let(:gitlab_org_or_com?) { true }
|
||||
|
||||
shared_examples 'does not render the form' do
|
||||
it 'does not render the form' do
|
||||
expect(rendered).not_to have_field('application_setting_instance_level_code_suggestions_enabled')
|
||||
expect(rendered).not_to have_field('application_setting_ai_access_token')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on .com or .org' do
|
||||
let(:gitlab_org_or_com?) { true }
|
||||
|
||||
it_behaves_like 'does not render the form'
|
||||
end
|
||||
|
||||
context 'when not on .com and not on .org' do
|
||||
let(:gitlab_org_or_com?) { false }
|
||||
|
||||
it 'renders the form' do
|
||||
expect(rendered).to have_field('application_setting_instance_level_code_suggestions_enabled')
|
||||
expect(rendered).to have_field('application_setting_ai_access_token')
|
||||
end
|
||||
it_behaves_like 'does not render the form'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue