Bring scoped environment variables to core
As decided in https://gitlab.com/gitlab-org/gitlab-ce/issues/53593
This commit is contained in:
parent
455d16d1bf
commit
5f82ff1469
|
|
@ -38,6 +38,6 @@ class Projects::VariablesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def variable_params_attributes
|
||||
%i[id variable_type key secret_value protected masked _destroy]
|
||||
%i[id variable_type key secret_value protected masked environment_scope _destroy]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ module Ci
|
|||
include HasVariable
|
||||
include Presentable
|
||||
include Maskable
|
||||
prepend HasEnvironmentScope
|
||||
|
||||
belongs_to :project
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module HasEnvironmentScope
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
prepended do
|
||||
validates(
|
||||
:environment_scope,
|
||||
presence: true,
|
||||
format: { with: ::Gitlab::Regex.environment_scope_regex,
|
||||
message: ::Gitlab::Regex.environment_scope_regex_message }
|
||||
)
|
||||
|
||||
##
|
||||
# Select rows which have a scope that matches the given environment name.
|
||||
# Rows are ordered by relevance, by default. The most relevant row is
|
||||
# placed at the end of a list.
|
||||
#
|
||||
# options:
|
||||
# - relevant_only: (boolean)
|
||||
# You can get the most relevant row only. Other rows are not be
|
||||
# selected even if its scope matches the environment name.
|
||||
# This is equivalent to using `#last` from SQL standpoint.
|
||||
#
|
||||
scope :on_environment, -> (environment_name, relevant_only: false) do
|
||||
order_direction = relevant_only ? 'DESC' : 'ASC'
|
||||
|
||||
where = <<~SQL
|
||||
environment_scope IN (:wildcard, :environment_name) OR
|
||||
:environment_name LIKE
|
||||
#{::Gitlab::SQL::Glob.to_like('environment_scope')}
|
||||
SQL
|
||||
|
||||
order = <<~SQL
|
||||
CASE environment_scope
|
||||
WHEN :wildcard THEN 0
|
||||
WHEN :environment_name THEN 2
|
||||
ELSE 1
|
||||
END #{order_direction}
|
||||
SQL
|
||||
|
||||
values = {
|
||||
wildcard: '*',
|
||||
environment_name: environment_name
|
||||
}
|
||||
|
||||
sanitized_order_sql = sanitize_sql_array([order, values])
|
||||
|
||||
# The query is trying to find variables with scopes matching the
|
||||
# current environment name. Suppose the environment name is
|
||||
# 'review/app', and we have variables with environment scopes like:
|
||||
# * variable A: review
|
||||
# * variable B: review/app
|
||||
# * variable C: review/*
|
||||
# * variable D: *
|
||||
# And the query should find variable B, C, and D, because it would
|
||||
# try to convert the scope into a LIKE pattern for each variable:
|
||||
# * A: review
|
||||
# * B: review/app
|
||||
# * C: review/%
|
||||
# * D: %
|
||||
# Note that we'll match % and _ literally therefore we'll escape them.
|
||||
# In this case, B, C, and D would match. We also want to prioritize
|
||||
# the exact matched name, and put * last, and everything else in the
|
||||
# middle. So the order should be: D < C < B
|
||||
relation = where(where, values)
|
||||
.order(Arel.sql(sanitized_order_sql)) # `order` cannot escape for us!
|
||||
|
||||
relation = relation.limit(1) if relevant_only
|
||||
|
||||
relation
|
||||
end
|
||||
end
|
||||
|
||||
def environment_scope=(new_environment_scope)
|
||||
super(new_environment_scope.to_s.strip)
|
||||
end
|
||||
end
|
||||
|
|
@ -1828,11 +1828,16 @@ class Project < ApplicationRecord
|
|||
end
|
||||
|
||||
def ci_variables_for(ref:, environment: nil)
|
||||
# EE would use the environment
|
||||
if protected_for?(ref)
|
||||
variables
|
||||
result = if protected_for?(ref)
|
||||
variables
|
||||
else
|
||||
variables.unprotected
|
||||
end
|
||||
|
||||
if environment
|
||||
result.on_environment(environment)
|
||||
else
|
||||
variables.unprotected
|
||||
result.where(environment_scope: '*')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ class VariableEntity < Grape::Entity
|
|||
|
||||
expose :protected?, as: :protected
|
||||
expose :masked?, as: :masked
|
||||
expose :environment_scope
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
- form_field = local_assigns.fetch(:form_field, nil)
|
||||
- variable = local_assigns.fetch(:variable, nil)
|
||||
|
||||
- if @project
|
||||
- environment_scope = variable&.environment_scope || '*'
|
||||
- environment_scope_label = environment_scope == '*' ? s_('CiVariable|All environments') : environment_scope
|
||||
|
||||
%input{ type: "hidden", name: "#{form_field}[variables_attributes][][environment_scope]", value: environment_scope }
|
||||
= dropdown_tag(environment_scope_label,
|
||||
options: { wrapper_class: 'ci-variable-body-item js-variable-environment-dropdown-wrapper',
|
||||
toggle_class: 'js-variable-environment-toggle wide',
|
||||
filter: true,
|
||||
dropdown_class: "dropdown-menu-selectable",
|
||||
placeholder: s_('CiVariable|Search environments'),
|
||||
footer_content: true,
|
||||
data: { selected: environment_scope } }) do
|
||||
%ul.dropdown-footer-list
|
||||
%li
|
||||
%button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: s_('CiVariable|New environment') }
|
||||
= s_('CiVariable|Create wildcard')
|
||||
%code
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
.bold.table-section.section-15.append-right-10
|
||||
= s_('CiVariables|Scope')
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Bring scoped environment variables to core
|
||||
merge_request: 30779
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -279,7 +279,7 @@ The following documentation relates to the DevOps **Release** stage:
|
|||
| [Canary Deployments](user/project/canary_deployments.md) **(PREMIUM)** | Employ a popular CI strategy where a small portion of the fleet is updated to the new version first. |
|
||||
| [Deploy Boards](user/project/deploy_boards.md) **(PREMIUM)** | View the current health and status of each CI environment running on Kubernetes, displaying the status of the pods in the deployment. |
|
||||
| [Environments and deployments](ci/environments.md) | With environments, you can control the continuous deployment of your software within GitLab. |
|
||||
| [Environment-specific variables](ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium) **(PREMIUM)** | Limit scope of variables to specific environments. |
|
||||
| [Environment-specific variables](ci/variables/README.md#limiting-environment-scopes-of-environment-variables) | Limit scope of variables to specific environments. |
|
||||
| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Deployment and Delivery with GitLab. |
|
||||
| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy a static site directly from GitLab. |
|
||||
| [Protected Runners](ci/runners/README.md#protected-runners) | Select Runners to only pick jobs for protected branches and tags. |
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ POST /projects/:id/variables
|
|||
| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
|
||||
| `protected` | boolean | no | Whether the variable is protected |
|
||||
| `masked` | boolean | no | Whether the variable is masked |
|
||||
| `environment_scope` | string | no | The `environment_scope` of the variable **(PREMIUM)** |
|
||||
| `environment_scope` | string | no | The `environment_scope` of the variable |
|
||||
|
||||
```
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
|
||||
|
|
@ -108,7 +108,7 @@ PUT /projects/:id/variables/:key
|
|||
| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
|
||||
| `protected` | boolean | no | Whether the variable is protected |
|
||||
| `masked` | boolean | no | Whether the variable is masked |
|
||||
| `environment_scope` | string | no | The `environment_scope` of the variable **(PREMIUM)** |
|
||||
| `environment_scope` | string | no | The `environment_scope` of the variable |
|
||||
|
||||
```
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
|
||||
|
|
|
|||
|
|
@ -692,7 +692,7 @@ with `review/` would have that particular variable.
|
|||
|
||||
Some GitLab features can behave differently for each environment.
|
||||
For example, you can
|
||||
[create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-environment-variables-premium). **(PREMIUM)**
|
||||
[create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-environment-variables).
|
||||
|
||||
In most cases, these features use the _environment specs_ mechanism, which offers
|
||||
an efficient way to implement scoping within each environment group.
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ Protected variables can be added by going to your project's
|
|||
|
||||
Once you set them, they will be available for all subsequent pipelines.
|
||||
|
||||
### Limiting environment scopes of environment variables **(PREMIUM)**
|
||||
### Limiting environment scopes of environment variables
|
||||
|
||||
You can limit the environment scope of a variable by
|
||||
[defining which environments][envs] it can be available for.
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so
|
|||
except for the environment scope, they would also need to have a different
|
||||
domain they would be deployed to. This is why you need to define a separate
|
||||
`KUBE_INGRESS_BASE_DOMAIN` variable for all the above
|
||||
[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
|
||||
[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables).
|
||||
|
||||
The following table is an example of how the three different clusters would
|
||||
be configured.
|
||||
|
|
@ -662,10 +662,10 @@ repo or by specifying a project variable:
|
|||
You can also make use of the `HELM_UPGRADE_EXTRA_ARGS` environment variable to override the default values in the `values.yaml` file in the [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app).
|
||||
To apply your own `values.yaml` file to all Helm upgrade commands in Auto Deploy set `HELM_UPGRADE_EXTRA_ARGS` to `--values my-values.yaml`.
|
||||
|
||||
### Custom Helm chart per environment **(PREMIUM)**
|
||||
### Custom Helm chart per environment
|
||||
|
||||
You can specify the use of a custom Helm chart per environment by scoping the environment variable
|
||||
to the desired environment. See [Limiting environment scopes of variables](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
|
||||
to the desired environment. See [Limiting environment scopes of variables](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables).
|
||||
|
||||
### Customizing `.gitlab-ci.yml`
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ The domain should have a wildcard DNS configured to the Ingress IP address.
|
|||
When adding more than one Kubernetes cluster to your project, you need to differentiate
|
||||
them with an environment scope. The environment scope associates clusters with
|
||||
[environments](../../../ci/environments.md) similar to how the
|
||||
[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium)
|
||||
[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables)
|
||||
work.
|
||||
|
||||
While evaluating which environment matches the environment scope of a
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ If you don't want to use GitLab Runner in privileged mode, either:
|
|||
|
||||
When adding more than one Kubernetes cluster to your project, you need to differentiate
|
||||
them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments.md) similar to how the
|
||||
[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium) work.
|
||||
[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables) work.
|
||||
|
||||
The default environment scope is `*`, which means all jobs, regardless of their
|
||||
environment, will use that cluster. Each scope can only be used by a single
|
||||
|
|
|
|||
|
|
@ -1346,6 +1346,7 @@ module API
|
|||
expose :variable_type, :key, :value
|
||||
expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
|
||||
expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) }
|
||||
expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) }
|
||||
end
|
||||
|
||||
class Pipeline < PipelineBasic
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Helpers
|
||||
module VariablesHelpers
|
||||
extend ActiveSupport::Concern
|
||||
extend Grape::API::Helpers
|
||||
|
||||
params :optional_params_ee do
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,8 +7,6 @@ module API
|
|||
before { authenticate! }
|
||||
before { authorize! :admin_build, user_project }
|
||||
|
||||
helpers Helpers::VariablesHelpers
|
||||
|
||||
helpers do
|
||||
def filter_variable_parameters(params)
|
||||
# This method exists so that EE can more easily filter out certain
|
||||
|
|
@ -59,8 +57,7 @@ module API
|
|||
optional :protected, type: Boolean, desc: 'Whether the variable is protected'
|
||||
optional :masked, type: Boolean, desc: 'Whether the variable is masked'
|
||||
optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
|
||||
|
||||
use :optional_params_ee
|
||||
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
|
||||
end
|
||||
post ':id/variables' do
|
||||
variable_params = declared_params(include_missing: false)
|
||||
|
|
@ -84,8 +81,7 @@ module API
|
|||
optional :protected, type: Boolean, desc: 'Whether the variable is protected'
|
||||
optional :masked, type: Boolean, desc: 'Whether the variable is masked'
|
||||
optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
|
||||
|
||||
use :optional_params_ee
|
||||
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
put ':id/variables/:key' do
|
||||
|
|
|
|||
|
|
@ -46,6 +46,18 @@ module Gitlab
|
|||
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'"
|
||||
end
|
||||
|
||||
def environment_scope_regex_chars
|
||||
"#{environment_name_regex_chars}\\*"
|
||||
end
|
||||
|
||||
def environment_scope_regex
|
||||
@environment_scope_regex ||= /\A[#{environment_scope_regex_chars}]+\z/.freeze
|
||||
end
|
||||
|
||||
def environment_scope_regex_message
|
||||
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', '*' and spaces"
|
||||
end
|
||||
|
||||
def kubernetes_namespace_regex
|
||||
/\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2263,6 +2263,9 @@ msgstr ""
|
|||
msgid "CiVariables|Remove variable row"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Scope"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2284,15 +2287,24 @@ msgstr ""
|
|||
msgid "CiVariable|All environments"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Create wildcard"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Error occurred while saving variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Masked"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|New environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Protected"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Search environments"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Toggle masked"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -36,5 +36,70 @@ describe Projects::VariablesController do
|
|||
end
|
||||
|
||||
include_examples 'PATCH #update updates variables'
|
||||
|
||||
context 'with environment scope' do
|
||||
let!(:variable) { create(:ci_variable, project: project, environment_scope: 'custom_scope') }
|
||||
|
||||
let(:variable_attributes) do
|
||||
{ id: variable.id,
|
||||
key: variable.key,
|
||||
secret_value: variable.value,
|
||||
protected: variable.protected?.to_s,
|
||||
environment_scope: variable.environment_scope }
|
||||
end
|
||||
let(:new_variable_attributes) do
|
||||
{ key: 'new_key',
|
||||
secret_value: 'dummy_value',
|
||||
protected: 'false',
|
||||
environment_scope: 'new_scope' }
|
||||
end
|
||||
|
||||
context 'with same key and different environment scope' do
|
||||
let(:variables_attributes) do
|
||||
[
|
||||
variable_attributes,
|
||||
new_variable_attributes.merge(key: variable.key)
|
||||
]
|
||||
end
|
||||
|
||||
it 'does not update the existing variable' do
|
||||
expect { subject }.not_to change { variable.reload.value }
|
||||
end
|
||||
|
||||
it 'creates the new variable' do
|
||||
expect { subject }.to change { owner.variables.count }.by(1)
|
||||
end
|
||||
|
||||
it 'returns a successful response including all variables' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('variables')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with same key and same environment scope' do
|
||||
let(:variables_attributes) do
|
||||
[
|
||||
variable_attributes,
|
||||
new_variable_attributes.merge(key: variable.key, environment_scope: variable.environment_scope)
|
||||
]
|
||||
end
|
||||
|
||||
it 'does not update the existing variable' do
|
||||
expect { subject }.not_to change { variable.reload.value }
|
||||
end
|
||||
|
||||
it 'does not create the new variable' do
|
||||
expect { subject }.not_to change { owner.variables.count }
|
||||
end
|
||||
|
||||
it 'returns a bad request response' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,4 +17,27 @@ describe 'Project variables', :js do
|
|||
end
|
||||
|
||||
it_behaves_like 'variable list'
|
||||
|
||||
it 'adds new variable with a special environment scope' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('somekey')
|
||||
find('.js-ci-variable-input-value').set('somevalue')
|
||||
|
||||
find('.js-variable-environment-toggle').click
|
||||
find('.js-variable-environment-dropdown-wrapper .dropdown-input-field').set('review/*')
|
||||
find('.js-variable-environment-dropdown-wrapper .js-dropdown-create-new-item').click
|
||||
|
||||
expect(find('input[name="variables[variables_attributes][][environment_scope]"]', visible: false).value).to eq('review/*')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('somekey')
|
||||
expect(page).to have_content('review/*')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_licensed_features(variable_environment_scope: true)
|
||||
|
||||
project.add_maintainer(admin)
|
||||
sign_in(admin)
|
||||
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
|
||||
|
|
|
|||
|
|
@ -91,5 +91,38 @@ describe Gitlab::Ci::Build::Policy::Variables do
|
|||
expect(policy).to be_satisfied_by(pipeline, seed)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using project ci variables in environment scope' do
|
||||
let(:ci_build) do
|
||||
build(:ci_build, pipeline: pipeline,
|
||||
project: project,
|
||||
ref: 'master',
|
||||
stage: 'review',
|
||||
environment: 'test/$CI_JOB_STAGE/1')
|
||||
end
|
||||
|
||||
before do
|
||||
create(:ci_variable, project: project,
|
||||
key: 'SCOPED_VARIABLE',
|
||||
value: 'my-value-1')
|
||||
|
||||
create(:ci_variable, project: project,
|
||||
key: 'SCOPED_VARIABLE',
|
||||
value: 'my-value-2',
|
||||
environment_scope: 'test/review/*')
|
||||
end
|
||||
|
||||
it 'is satisfied by scoped variable match' do
|
||||
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-2"'])
|
||||
|
||||
expect(policy).to be_satisfied_by(pipeline, seed)
|
||||
end
|
||||
|
||||
it 'is not satisfied when matching against overridden variable' do
|
||||
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-1"'])
|
||||
|
||||
expect(policy).not_to be_satisfied_by(pipeline, seed)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,6 +32,14 @@ describe Gitlab::Regex do
|
|||
it { is_expected.not_to match('/') }
|
||||
end
|
||||
|
||||
describe '.environment_scope_regex' do
|
||||
subject { described_class.environment_scope_regex }
|
||||
|
||||
it { is_expected.to match('foo') }
|
||||
it { is_expected.to match('foo*Z') }
|
||||
it { is_expected.not_to match('!!()()') }
|
||||
end
|
||||
|
||||
describe '.environment_slug_regex' do
|
||||
subject { described_class.environment_slug_regex }
|
||||
|
||||
|
|
|
|||
|
|
@ -2340,6 +2340,32 @@ describe Ci::Build do
|
|||
it_behaves_like 'containing environment variables'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has an environment specific variable' do
|
||||
let(:environment_specific_variable) do
|
||||
{ key: 'MY_STAGING_ONLY_VARIABLE', value: 'environment_specific_variable', public: false, masked: false }
|
||||
end
|
||||
|
||||
before do
|
||||
create(:ci_variable, environment_specific_variable.slice(:key, :value)
|
||||
.merge(project: project, environment_scope: 'stag*'))
|
||||
end
|
||||
|
||||
it_behaves_like 'containing environment variables'
|
||||
|
||||
context 'when environment scope does not match build environment' do
|
||||
it { is_expected.not_to include(environment_specific_variable) }
|
||||
end
|
||||
|
||||
context 'when environment scope matches build environment' do
|
||||
before do
|
||||
create(:environment, name: 'staging', project: project)
|
||||
build.update!(environment: 'staging')
|
||||
end
|
||||
|
||||
it { is_expected.to include(environment_specific_variable) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build started manually' do
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ describe Ci::Variable do
|
|||
describe 'validations' do
|
||||
it { is_expected.to include_module(Presentable) }
|
||||
it { is_expected.to include_module(Maskable) }
|
||||
it { is_expected.to include_module(HasEnvironmentScope) }
|
||||
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe HasEnvironmentScope do
|
||||
subject { build(:ci_variable) }
|
||||
|
||||
it { is_expected.to allow_value('*').for(:environment_scope) }
|
||||
it { is_expected.to allow_value('review/*').for(:environment_scope) }
|
||||
it { is_expected.not_to allow_value('').for(:environment_scope) }
|
||||
it { is_expected.not_to allow_value('!!()()').for(:environment_scope) }
|
||||
|
||||
it do
|
||||
is_expected.to validate_uniqueness_of(:key)
|
||||
.scoped_to(:project_id, :environment_scope)
|
||||
.with_message(/\(\w+\) has already been taken/)
|
||||
end
|
||||
|
||||
describe '.on_environment' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'returns scoped objects' do
|
||||
variable1 = create(:ci_variable, project: project, environment_scope: '*')
|
||||
variable2 = create(:ci_variable, project: project, environment_scope: 'product/*')
|
||||
create(:ci_variable, project: project, environment_scope: 'staging/*')
|
||||
|
||||
expect(project.variables.on_environment('product/canary-1')).to eq([variable1, variable2])
|
||||
end
|
||||
|
||||
it 'returns only the most relevant object if relevant_only is true' do
|
||||
create(:ci_variable, project: project, environment_scope: '*')
|
||||
variable2 = create(:ci_variable, project: project, environment_scope: 'product/*')
|
||||
create(:ci_variable, project: project, environment_scope: 'staging/*')
|
||||
|
||||
expect(project.variables.on_environment('product/canary-1', relevant_only: true)).to eq([variable2])
|
||||
end
|
||||
|
||||
it 'returns scopes ordered by lowest precedence first' do
|
||||
create(:ci_variable, project: project, environment_scope: '*')
|
||||
create(:ci_variable, project: project, environment_scope: 'production*')
|
||||
create(:ci_variable, project: project, environment_scope: 'production')
|
||||
|
||||
result = project.variables.on_environment('production').map(&:environment_scope)
|
||||
|
||||
expect(result).to eq(['*', 'production*', 'production'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#environment_scope=' do
|
||||
context 'when the new environment_scope is nil' do
|
||||
it 'strips leading and trailing whitespaces' do
|
||||
subject.environment_scope = nil
|
||||
|
||||
expect(subject.environment_scope).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the new environment_scope has leadind and trailing whitespaces' do
|
||||
it 'strips leading and trailing whitespaces' do
|
||||
subject.environment_scope = ' * '
|
||||
|
||||
expect(subject.environment_scope).to eq('*')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2648,9 +2648,10 @@ describe Project do
|
|||
|
||||
describe '#ci_variables_for' do
|
||||
let(:project) { create(:project) }
|
||||
let(:environment_scope) { '*' }
|
||||
|
||||
let!(:ci_variable) do
|
||||
create(:ci_variable, value: 'secret', project: project)
|
||||
create(:ci_variable, value: 'secret', project: project, environment_scope: environment_scope)
|
||||
end
|
||||
|
||||
let!(:protected_variable) do
|
||||
|
|
@ -2695,6 +2696,96 @@ describe Project do
|
|||
|
||||
it_behaves_like 'ref is protected'
|
||||
end
|
||||
|
||||
context 'when environment name is specified' do
|
||||
let(:environment) { 'review/name' }
|
||||
|
||||
subject do
|
||||
project.ci_variables_for(ref: 'ref', environment: environment)
|
||||
end
|
||||
|
||||
context 'when environment scope is exactly matched' do
|
||||
let(:environment_scope) { 'review/name' }
|
||||
|
||||
it { is_expected.to contain_exactly(ci_variable) }
|
||||
end
|
||||
|
||||
context 'when environment scope is matched by wildcard' do
|
||||
let(:environment_scope) { 'review/*' }
|
||||
|
||||
it { is_expected.to contain_exactly(ci_variable) }
|
||||
end
|
||||
|
||||
context 'when environment scope does not match' do
|
||||
let(:environment_scope) { 'review/*/special' }
|
||||
|
||||
it { is_expected.not_to contain_exactly(ci_variable) }
|
||||
end
|
||||
|
||||
context 'when environment scope has _' do
|
||||
let(:environment_scope) { '*_*' }
|
||||
|
||||
it 'does not treat it as wildcard' do
|
||||
is_expected.not_to contain_exactly(ci_variable)
|
||||
end
|
||||
|
||||
context 'when environment name contains underscore' do
|
||||
let(:environment) { 'foo_bar/test' }
|
||||
let(:environment_scope) { 'foo_bar/*' }
|
||||
|
||||
it 'matches literally for _' do
|
||||
is_expected.to contain_exactly(ci_variable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The environment name and scope cannot have % at the moment,
|
||||
# but we're considering relaxing it and we should also make sure
|
||||
# it doesn't break in case some data sneaked in somehow as we're
|
||||
# not checking this integrity in database level.
|
||||
context 'when environment scope has %' do
|
||||
it 'does not treat it as wildcard' do
|
||||
ci_variable.update_attribute(:environment_scope, '*%*')
|
||||
|
||||
is_expected.not_to contain_exactly(ci_variable)
|
||||
end
|
||||
|
||||
context 'when environment name contains a percent' do
|
||||
let(:environment) { 'foo%bar/test' }
|
||||
|
||||
it 'matches literally for _' do
|
||||
ci_variable.update(environment_scope: 'foo%bar/*')
|
||||
|
||||
is_expected.to contain_exactly(ci_variable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when variables with the same name have different environment scopes' do
|
||||
let!(:partially_matched_variable) do
|
||||
create(:ci_variable,
|
||||
key: ci_variable.key,
|
||||
value: 'partial',
|
||||
environment_scope: 'review/*',
|
||||
project: project)
|
||||
end
|
||||
|
||||
let!(:perfectly_matched_variable) do
|
||||
create(:ci_variable,
|
||||
key: ci_variable.key,
|
||||
value: 'prefect',
|
||||
environment_scope: 'review/name',
|
||||
project: project)
|
||||
end
|
||||
|
||||
it 'puts variables matching environment scope more in the end' do
|
||||
is_expected.to eq(
|
||||
[ci_variable,
|
||||
partially_matched_variable,
|
||||
perfectly_matched_variable])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#any_lfs_file_locks?', :request_store do
|
||||
|
|
|
|||
|
|
@ -106,6 +106,30 @@ describe API::Variables do
|
|||
|
||||
expect(response).to have_gitlab_http_status(400)
|
||||
end
|
||||
|
||||
it 'creates variable with a specific environment scope' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2', environment_scope: 'review/*' }
|
||||
end.to change { project.variables.reload.count }.by(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(201)
|
||||
expect(json_response['key']).to eq('TEST_VARIABLE_2')
|
||||
expect(json_response['value']).to eq('VALUE_2')
|
||||
expect(json_response['environment_scope']).to eq('review/*')
|
||||
end
|
||||
|
||||
it 'allows duplicated variable key given different environment scopes' do
|
||||
variable = create(:ci_variable, project: project)
|
||||
|
||||
expect do
|
||||
post api("/projects/#{project.id}/variables", user), params: { key: variable.key, value: 'VALUE_2', environment_scope: 'review/*' }
|
||||
end.to change { project.variables.reload.count }.by(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(201)
|
||||
expect(json_response['key']).to eq(variable.key)
|
||||
expect(json_response['value']).to eq('VALUE_2')
|
||||
expect(json_response['environment_scope']).to eq('review/*')
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user with invalid permissions' do
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ describe VariableEntity do
|
|||
subject { entity.as_json }
|
||||
|
||||
it 'contains required fields' do
|
||||
expect(subject).to include(:id, :key, :value, :protected)
|
||||
expect(subject).to include(:id, :key, :value, :protected, :environment_scope)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue