Commit fe6c21d1 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 ec0bc339
......@@ -38,8 +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
Projects::VariablesController.prepend_if_ee('EE::Projects::VariablesController')
......@@ -6,6 +6,7 @@ module Ci
include HasVariable
include Presentable
include Maskable
prepend HasEnvironmentScope
belongs_to :project
......@@ -19,5 +20,3 @@ module Ci
scope :unprotected, -> { where(protected: false) }
end
end
Ci::Variable.prepend(HasEnvironmentScope)
......@@ -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
......
# frozen_string_literal: true
class VariableEntity < Grape::Entity
prepend ::EE::VariableEntity # rubocop: disable Cop/InjectEnterpriseEditionModule
expose :id
expose :key
expose :value
expose :protected?, as: :protected
expose :masked?, as: :masked
expose :environment_scope
end
- form_field = local_assigns.fetch(:form_field, nil)
- variable = local_assigns.fetch(:variable, nil)
- if @project && @project.feature_available?(:variable_environment_scope)
- if @project
- environment_scope = variable&.environment_scope || '*'
- environment_scope_label = environment_scope == '*' ? s_('CiVariable|All environments') : environment_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:
| [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
......
# frozen_string_literal: true
module EE
module Projects
module VariablesController
extend ActiveSupport::Concern
def variable_params_attributes
attrs = super
attrs.unshift(:environment_scope) if
project.feature_available?(:variable_environment_scope)
attrs
end
end
end
end
......@@ -279,13 +279,6 @@ module EE
end
end
def ci_variables_for(ref:, environment: nil)
return super.where(environment_scope: '*') unless
environment && feature_available?(:variable_environment_scope)
super.on_environment(environment)
end
def execute_hooks(data, hooks_scope = :push_hooks)
super
......
......@@ -66,7 +66,6 @@ class License < ApplicationRecord
service_desk
smartcard_auth
unprotection_restrictions
variable_environment_scope
reject_unsigned_commits
commit_committer_check
external_authorization_service_api_management
......@@ -143,7 +142,6 @@ class License < ApplicationRecord
repository_mirrors
scoped_issue_board
service_desk
variable_environment_scope
].freeze
FEATURES_BY_PLAN = {
......
# frozen_string_literal: true
module EE
module VariableEntity
extend ActiveSupport::Concern
prepended do
expose :environment_scope, if: ->(variable, options) { variable.project.feature_available?(:variable_environment_scope) }
end
end
end
......@@ -190,18 +190,6 @@ module EE
end
end
module Variable
extend ActiveSupport::Concern
prepended do
expose :environment_scope, if: ->(variable, options) do
if variable.respond_to?(:environment_scope)
variable.project.feature_available?(:variable_environment_scope)
end
end
end
end
module Todo
extend ActiveSupport::Concern
......
# frozen_string_literal: true
module EE
module API
module Helpers
module VariablesHelpers
extend ActiveSupport::Concern
prepended do
params :optional_params_ee do
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module API
module Variables
extend ActiveSupport::Concern
prepended do
helpers do
extend ::Gitlab::Utils::Override
override :filter_variable_parameters
def filter_variable_parameters(params)
unless user_project.feature_available?(:variable_environment_scope)
params.delete(:environment_scope)
end
params
end
end
end
end
end
end
......@@ -6,18 +6,6 @@ module EE
extend ActiveSupport::Concern
class_methods do
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 package_name_regex
@package_name_regex ||= %r{\A\@?(([\w\-\.]*)/)*([\w\-\.]*)\z}.freeze
end
......
require 'spec_helper'
describe Projects::VariablesController do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
sign_in(user)
project.add_maintainer(user)
allow_any_instance_of(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:variable_environment_scope).and_return(true)
end
describe 'PATCH #update' do
let!(:variable) { create(:ci_variable, project: project, environment_scope: 'custom_scope') }
let(:owner) { project }
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
subject do
patch :update,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
variables_attributes: variables_attributes
},
format: :json
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' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'has all variables in response' do
subject
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
require 'spec_helper'
describe 'Project variables EE', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
let(:page_path) { project_settings_ci_cd_path(project) }
before do
stub_licensed_features(variable_environment_scope: variable_environment_scope)
login_as(user)
project.add_maintainer(user)
project.variables << variable
visit page_path
end
context 'when variable environment scope is available' do
let(:variable_environment_scope) { true }
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
context 'when variable environment scope is not available' do
let(:variable_environment_scope) { false }
it 'does not show variable environment scope element' do
expect(page).not_to have_selector('input[name="variables[variables_attributes][][environment_scope]"]')
expect(page).not_to have_selector('.js-variable-environment-dropdown-wrapper')
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Policy::Variables do
let(:project) { create(:project) }
let(:pipeline) do
build(:ci_empty_pipeline, project: project, ref: 'master')
end
let(:ci_build) do
build(:ci_build, pipeline: pipeline,
project: project,
ref: 'master',
stage: 'review',
environment: 'test/$CI_JOB_STAGE/1')
end
let(:seed) { double('build seed', to_resource: ci_build) }
describe '#satisfied_by?' do
context 'when using project ci variables in environment scope' do
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
context 'when environment scope variables feature is enabled' do
before do
stub_licensed_features(variable_environment_scope: true)
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
context 'when environment scope variables feature is disabled' do
before do
stub_licensed_features(variable_environment_scope: false)
end
it 'is not satisfied by scoped variable match' do
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-2"'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
it 'is satisfied when matching against unscoped variable' do
policy = described_class.new(['$SCOPED_VARIABLE == "my-value-1"'])
expect(policy).to be_satisfied_by(pipeline, seed)
end
end
end
end
end
......@@ -2,14 +2,6 @@
require 'spec_helper'
describe Gitlab::Regex do
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 '.feature_flag_regex' do
subject { described_class.feature_flag_regex }
......
......@@ -93,22 +93,6 @@ describe Ci::Build do
variable.save!
end
context 'when variable environment scope is available' do
before do
stub_licensed_features(variable_environment_scope: true)
end
it { is_expected.to include(environment_variable) }
end
context 'when variable environment scope is not available' do
before do
stub_licensed_features(variable_environment_scope: false)
end
it { is_expected.not_to include(environment_variable) }
end
context 'when there is a plan for the group' do
it 'GITLAB_FEATURES should include the features for that plan' do
is_expected.to include({ key: 'GITLAB_FEATURES', value: anything, public: true, masked: false })
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::Variable do
subject { build(:ci_variable) }
describe 'validations' do
it { is_expected.to include_module(HasEnvironmentScope) }
end
it do
is_expected.to validate_uniqueness_of(:key)
.scoped_to(:project_id, :environment_scope)
.with_message(/\(\w+\) has already been taken/)
end
end
......@@ -777,178 +777,6 @@ describe Project do
end
end
describe '#ci_variables_for' do
let(:project) { create(:project) }
let!(:ci_variable) do
create(:ci_variable, value: 'secret', project: project)
end
let!(:protected_variable) do
create(:ci_variable, :protected, value: 'protected', project: project)
end
subject { project.ci_variables_for(ref: 'ref') }
before do
stub_application_setting(
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
context 'when environment name is specified' do
let(:environment) { 'review/name' }
subject do
project.ci_variables_for(ref: 'ref', environment: environment)
end
shared_examples 'matching environment scope' do
context 'when variable environment scope is available' do
before do
stub_licensed_features(variable_environment_scope: true)
end
it 'contains the ci variable' do
is_expected.to contain_exactly(ci_variable)
end
end
context 'when variable environment scope is unavailable' do
before do
stub_licensed_features(variable_environment_scope: false)
end
it 'does not contain the ci variable' do
is_expected.not_to contain_exactly(ci_variable)
end
end
end
shared_examples 'not matching environment scope' do
context 'when variable environment scope is available' do
before do
stub_licensed_features(variable_environment_scope: true)
end
it 'does not contain the ci variable' do
is_expected.not_to contain_exactly(ci_variable)
end
end
context 'when variable environment scope is unavailable' do
before do
stub_licensed_features(variable_environment_scope: false)
end
it 'does not contain the ci variable' do
is_expected.not_to contain_exactly(ci_variable)
end
end
end
context 'when environment scope is exactly matched' do
before do
ci_variable.update(environment_scope: 'review/name')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope is matched by wildcard' do
before do
ci_variable.update(environment_scope: 'review/*')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope does not match' do
before do
ci_variable.update(environment_scope: 'review/*/special')
end
it_behaves_like 'not matching environment scope'
end
context 'when environment scope has _' do
before do
stub_licensed_features(variable_environment_scope: true)
end
it 'does not treat it as wildcard' do
ci_variable.update(environment_scope: '*_*')
is_expected.not_to contain_exactly(ci_variable)
end
context 'when environment name contains underscore' 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
# 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
before do
stub_licensed_features(variable_environment_scope: true)
end
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
before do
stub_licensed_features(variable_environment_scope: true)
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 '#approvals_before_merge' do
where(:license_value, :db_value, :expected) do
true | 5 | 5
......
require 'spec_helper'
describe API::Variables do
let(:user) { create(:user) }
let(:project) { create(:project) }
describe 'POST /projects/:id/variables' do
context 'with variable environment scope available' do
before do
stub_licensed_features(variable_environment_scope: true)
project.add_maintainer(user)
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
end
require 'spec_helper'
describe VariableEntity do
let(:variable) { create(:ci_variable) }
let(:entity) { described_class.new(variable) }
describe '#as_json' do
subject { entity.as_json }
context 'when project has variable environment scopes available' do
before do
allow(variable.project).to receive(:feature_available?).with(:variable_environment_scope).and_return(true)
end
it 'contains the environment_scope field' do
expect(subject).to include(:environment_scope)
end
end
context 'when project does not have variable environment scopes available' do
before do
allow(variable.project).to receive(:feature_available?).with(:variable_environment_scope).and_return(false)
end
it 'does not contain the environment_scope field' do
expect(subject).not_to include(:environment_scope)
end
end
end
end
......@@ -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
......@@ -1714,7 +1715,6 @@ API::Entities.prepend_entity(::API::Entities::Namespace, with: EE::API::Entities
API::Entities.prepend_entity(::API::Entities::Project, with: EE::API::Entities::Project)
API::Entities.prepend_entity(::API::Entities::ProtectedRefAccess, with: EE::API::Entities::ProtectedRefAccess)
API::Entities.prepend_entity(::API::Entities::UserPublic, with: EE::API::Entities::UserPublic)
API::Entities.prepend_entity(::API::Entities::Variable, with: EE::API::Entities::Variable)
API::Entities.prepend_entity(::API::Entities::Todo, with: EE::API::Entities::Todo)
API::Entities.prepend_entity(::API::Entities::ProtectedBranch, with: EE::API::Entities::ProtectedBranch)
API::Entities.prepend_entity(::API::Entities::Identity, with: EE::API::Entities::Identity)
......
# 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
API::Helpers::VariablesHelpers.prepend_if_ee('EE::API::Helpers::VariablesHelpers')
......@@ -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
......@@ -123,5 +119,3 @@ module API
end
end
end
API::Variables.prepend_if_ee('EE::API::Variables')
......@@ -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
......
......@@ -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
......
# frozen_string_literal: true
require 'spec_helper'
describe HasEnvironmentScope do
......@@ -18,27 +20,27 @@ describe HasEnvironmentScope do
let(:project) { create(:project) }
it 'returns scoped objects' do
cluster1 = create(:cluster, projects: [project], environment_scope: '*')
cluster2 = create(:cluster, projects: [project], environment_scope: 'product/*')
create(:cluster, projects: [project], environment_scope: 'staging/*')
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.clusters.on_environment('product/canary-1')).to eq([cluster1, cluster2])
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(:cluster, projects: [project], environment_scope: '*')
cluster2 = create(:cluster, projects: [project], environment_scope: 'product/*')
create(:cluster, projects: [project], environment_scope: 'staging/*')
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.clusters.on_environment('product/canary-1', relevant_only: true)).to eq([cluster2])
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(:cluster, projects: [project], environment_scope: '*')
create(:cluster, projects: [project], environment_scope: 'production*')
create(:cluster, projects: [project], environment_scope: 'production')
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.clusters.on_environment('production').map(&:environment_scope)
result = project.variables.on_environment('production').map(&:environment_scope)
expect(result).to eq(['*', 'production*', 'production'])
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
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