Commit 5f82ff14 authored by Hordur Freyr Yngvason's avatar Hordur Freyr Yngvason Committed by Robert Speicher

Bring scoped environment variables to core

As decided in https://gitlab.com/gitlab-org/gitlab-ce/issues/53593
parent 455d16d1
...@@ -38,6 +38,6 @@ class Projects::VariablesController < Projects::ApplicationController ...@@ -38,6 +38,6 @@ class Projects::VariablesController < Projects::ApplicationController
end end
def variable_params_attributes 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
end end
...@@ -6,6 +6,7 @@ module Ci ...@@ -6,6 +6,7 @@ module Ci
include HasVariable include HasVariable
include Presentable include Presentable
include Maskable include Maskable
prepend HasEnvironmentScope
belongs_to :project belongs_to :project
......
# 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,12 +1828,17 @@ class Project < ApplicationRecord ...@@ -1828,12 +1828,17 @@ class Project < ApplicationRecord
end end
def ci_variables_for(ref:, environment: nil) def ci_variables_for(ref:, environment: nil)
# EE would use the environment result = if protected_for?(ref)
if protected_for?(ref)
variables variables
else else
variables.unprotected variables.unprotected
end end
if environment
result.on_environment(environment)
else
result.where(environment_scope: '*')
end
end end
def protected_for?(ref) def protected_for?(ref)
......
...@@ -7,4 +7,5 @@ class VariableEntity < Grape::Entity ...@@ -7,4 +7,5 @@ class VariableEntity < Grape::Entity
expose :protected?, as: :protected expose :protected?, as: :protected
expose :masked?, as: :masked expose :masked?, as: :masked
expose :environment_scope
end end
- 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
.bold.table-section.section-15.append-right-10
= s_('CiVariables|Scope')
---
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: ...@@ -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. | | [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. | | [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. | | [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 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. | | [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. | | [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 ...@@ -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` | | `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 | | `protected` | boolean | no | Whether the variable is protected |
| `masked` | boolean | no | Whether the variable is masked | | `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" 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 ...@@ -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` | | `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 | | `protected` | boolean | no | Whether the variable is protected |
| `masked` | boolean | no | Whether the variable is masked | | `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" 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. ...@@ -692,7 +692,7 @@ with `review/` would have that particular variable.
Some GitLab features can behave differently for each environment. Some GitLab features can behave differently for each environment.
For example, you can 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 In most cases, these features use the _environment specs_ mechanism, which offers
an efficient way to implement scoping within each environment group. 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 ...@@ -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. 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 You can limit the environment scope of a variable by
[defining which environments][envs] it can be available for. [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 ...@@ -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 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 domain they would be deployed to. This is why you need to define a separate
`KUBE_INGRESS_BASE_DOMAIN` variable for all the above `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 The following table is an example of how the three different clusters would
be configured. be configured.
...@@ -662,10 +662,10 @@ repo or by specifying a project variable: ...@@ -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). 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`. 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 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` ### Customizing `.gitlab-ci.yml`
......
...@@ -86,7 +86,7 @@ The domain should have a wildcard DNS configured to the Ingress IP address. ...@@ -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 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 them with an environment scope. The environment scope associates clusters with
[environments](../../../ci/environments.md) similar to how the [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. work.
While evaluating which environment matches the environment scope of a 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: ...@@ -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 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 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 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 environment, will use that cluster. Each scope can only be used by a single
......
...@@ -1346,6 +1346,7 @@ module API ...@@ -1346,6 +1346,7 @@ module API
expose :variable_type, :key, :value expose :variable_type, :key, :value
expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) } expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) } expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) }
expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) }
end end
class Pipeline < PipelineBasic class Pipeline < PipelineBasic
......
# 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 ...@@ -7,8 +7,6 @@ module API
before { authenticate! } before { authenticate! }
before { authorize! :admin_build, user_project } before { authorize! :admin_build, user_project }
helpers Helpers::VariablesHelpers
helpers do helpers do
def filter_variable_parameters(params) def filter_variable_parameters(params)
# This method exists so that EE can more easily filter out certain # This method exists so that EE can more easily filter out certain
...@@ -59,8 +57,7 @@ module API ...@@ -59,8 +57,7 @@ module API
optional :protected, type: Boolean, desc: 'Whether the variable is protected' optional :protected, type: Boolean, desc: 'Whether the variable is protected'
optional :masked, type: Boolean, desc: 'Whether the variable is masked' 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' 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'
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
use :optional_params_ee
end end
post ':id/variables' do post ':id/variables' do
variable_params = declared_params(include_missing: false) variable_params = declared_params(include_missing: false)
...@@ -84,8 +81,7 @@ module API ...@@ -84,8 +81,7 @@ module API
optional :protected, type: Boolean, desc: 'Whether the variable is protected' optional :protected, type: Boolean, desc: 'Whether the variable is protected'
optional :masked, type: Boolean, desc: 'Whether the variable is masked' 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' optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
use :optional_params_ee
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
put ':id/variables/:key' do put ':id/variables/:key' do
......
...@@ -46,6 +46,18 @@ module Gitlab ...@@ -46,6 +46,18 @@ module Gitlab
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'" "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'"
end 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 def kubernetes_namespace_regex
/\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/ /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/
end end
......
...@@ -2263,6 +2263,9 @@ msgstr "" ...@@ -2263,6 +2263,9 @@ msgstr ""
msgid "CiVariables|Remove variable row" msgid "CiVariables|Remove variable row"
msgstr "" 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" 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 "" msgstr ""
...@@ -2284,15 +2287,24 @@ msgstr "" ...@@ -2284,15 +2287,24 @@ msgstr ""
msgid "CiVariable|All environments" msgid "CiVariable|All environments"
msgstr "" msgstr ""
msgid "CiVariable|Create wildcard"
msgstr ""
msgid "CiVariable|Error occurred while saving variables" msgid "CiVariable|Error occurred while saving variables"
msgstr "" msgstr ""
msgid "CiVariable|Masked" msgid "CiVariable|Masked"
msgstr "" msgstr ""
msgid "CiVariable|New environment"
msgstr ""
msgid "CiVariable|Protected" msgid "CiVariable|Protected"
msgstr "" msgstr ""
msgid "CiVariable|Search environments"
msgstr ""
msgid "CiVariable|Toggle masked" msgid "CiVariable|Toggle masked"
msgstr "" msgstr ""
......
...@@ -36,5 +36,70 @@ describe Projects::VariablesController do ...@@ -36,5 +36,70 @@ describe Projects::VariablesController do
end end
include_examples 'PATCH #update updates variables' 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
end end
...@@ -17,4 +17,27 @@ describe 'Project variables', :js do ...@@ -17,4 +17,27 @@ describe 'Project variables', :js do
end end
it_behaves_like 'variable list' 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 end
...@@ -18,8 +18,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do ...@@ -18,8 +18,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
end end
before do before do
stub_licensed_features(variable_environment_scope: true)
project.add_maintainer(admin) project.add_maintainer(admin)
sign_in(admin) sign_in(admin)
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
......
...@@ -91,5 +91,38 @@ describe Gitlab::Ci::Build::Policy::Variables do ...@@ -91,5 +91,38 @@ describe Gitlab::Ci::Build::Policy::Variables do
expect(policy).to be_satisfied_by(pipeline, seed) expect(policy).to be_satisfied_by(pipeline, seed)
end end
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
end end
...@@ -32,6 +32,14 @@ describe Gitlab::Regex do ...@@ -32,6 +32,14 @@ describe Gitlab::Regex do
it { is_expected.not_to match('/') } it { is_expected.not_to match('/') }
end 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 describe '.environment_slug_regex' do
subject { described_class.environment_slug_regex } subject { described_class.environment_slug_regex }
......
...@@ -2340,6 +2340,32 @@ describe Ci::Build do ...@@ -2340,6 +2340,32 @@ describe Ci::Build do
it_behaves_like 'containing environment variables' it_behaves_like 'containing environment variables'
end end
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 end
context 'when build started manually' do context 'when build started manually' do
......
...@@ -10,6 +10,7 @@ describe Ci::Variable do ...@@ -10,6 +10,7 @@ describe Ci::Variable do
describe 'validations' do describe 'validations' do
it { is_expected.to include_module(Presentable) } it { is_expected.to include_module(Presentable) }
it { is_expected.to include_module(Maskable) } 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/) } it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
end end
......
# 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 ...@@ -2648,9 +2648,10 @@ describe Project do
describe '#ci_variables_for' do describe '#ci_variables_for' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:environment_scope) { '*' }
let!(:ci_variable) do let!(:ci_variable) do
create(:ci_variable, value: 'secret', project: project) create(:ci_variable, value: 'secret', project: project, environment_scope: environment_scope)
end end
let!(:protected_variable) do let!(:protected_variable) do
...@@ -2695,6 +2696,96 @@ describe Project do ...@@ -2695,6 +2696,96 @@ describe Project do
it_behaves_like 'ref is protected' it_behaves_like 'ref is protected'
end 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 end
describe '#any_lfs_file_locks?', :request_store do describe '#any_lfs_file_locks?', :request_store do
......
...@@ -106,6 +106,30 @@ describe API::Variables do ...@@ -106,6 +106,30 @@ describe API::Variables do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end 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 end
context 'authorized user with invalid permissions' do context 'authorized user with invalid permissions' do
......
...@@ -8,7 +8,7 @@ describe VariableEntity do ...@@ -8,7 +8,7 @@ describe VariableEntity do
subject { entity.as_json } subject { entity.as_json }
it 'contains required fields' do 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 end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment