Commit b3a3357d authored by Krasimir Angelov's avatar Krasimir Angelov

Implement CI syntax for secrets

Adds `secrets` section in CI config for jobs. There is no support for
default values yet.

Related to https://gitlab.com/gitlab-org/gitlab/-/issues/28321 and
https://gitlab.com/gitlab-org/gitlab/-/issues/218746.
parent 2629dac2
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Config
module Entry
module Job
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
entry :secrets, ::Gitlab::Ci::Config::Entry::Secrets,
description: 'Configured secrets for this job',
inherit: false
end
override :value
def value
super.merge({ secrets: secrets_value }.compact)
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a secret definition.
#
class Secret < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[vault].freeze
attributes ALLOWED_KEYS
entry :vault, Entry::Vault::Secret, description: 'Vault secrets engine configuration'
validations do
validates :config, allowed_keys: ALLOWED_KEYS, required_keys: ALLOWED_KEYS
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a secrets definition.
#
class Secrets < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
def compose!(deps = nil)
super do
@config.each do |name, config|
factory = ::Gitlab::Config::Entry::Factory.new(Entry::Secret)
.value(config || {})
.with(key: name, parent: self, description: "#{name} secret definition") # rubocop:disable CodeReuse/ActiveRecord
.metadata(name: name)
@entries[name] = factory.create!
end
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
module Vault
##
# Entry that represents Vault secret engine.
#
class Engine < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[name path].freeze
attributes ALLOWED_KEYS
validations do
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
validates :name, presence: true, type: String
validates :path, presence: true, type: String
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
module Vault
##
# Entry that represents Vault secret.
#
class Secret < ::Gitlab::Config::Entry::Simplifiable
strategy :StringStrategy, if: -> (config) { config.is_a?(String) }
strategy :HashStrategy, if: -> (config) { config.is_a?(Hash) }
class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors
["#{location} should be a hash or a string"]
end
end
class StringStrategy < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, presence: true
validates :config, type: String
end
def value
{
engine: {
name: 'kv-v2', path: secret[:engine_path]
},
path: secret[:path],
field: secret[:field]
}
end
private
def secret
@secret ||= begin
path_and_field, _, engine_path = config.rpartition('@')
if path_and_field == ""
path_and_field = config
engine_path = 'kv-v2'
end
path, _, field = path_and_field.rpartition('/')
{
engine_path: engine_path,
path: path,
field: field
}
end
end
end
class HashStrategy < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[engine path field].freeze
attributes ALLOWED_KEYS
entry :engine, Entry::Vault::Engine, description: 'Vault secrets engine configuration'
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :path, presence: true, type: String
validates :field, presence: true, type: String
validates :engine, presence: true, type: Hash
end
def value
{
engine: engine_value,
path: path,
field: field
}
end
end
end
end
end
end
end
end
...@@ -10,6 +10,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do ...@@ -10,6 +10,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
entry.compose! entry.compose!
end end
context 'when entry value is correct' do
context 'when has secrets' do
let(:config) { { script: 'echo', secrets: {} } }
it { expect(entry).to be_valid }
end
end
context 'when entry value is not correct' do context 'when entry value is not correct' do
context 'when has needs' do context 'when has needs' do
context 'when needs is bridge type' do context 'when needs is bridge type' do
...@@ -27,6 +35,73 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do ...@@ -27,6 +35,73 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
end end
end end
end end
context 'when has invalid secrets' do
let(:config) { { script: 'echo', secrets: [] } }
it 'reports error' do
expect(entry.errors)
.to include 'secrets config should be a hash'
end
end
end
end
describe '.nodes' do
context 'when filtering all the entry/node names' do
subject(:nodes) { described_class.nodes }
it 'has "secrets" node' do
expect(nodes).to have_key(:secrets)
end
end
end
describe 'secrets' do
let(:config) { { script: 'echo', secrets: secrets } }
let(:secrets) do
{
DATABASE_PASSWORD: { vault: 'production/db/password' },
SSL_PRIVATE_KEY: { vault: 'production/ssl/private-key@ops' },
S3_SECRET_KEY: {
vault: {
engine: { name: 'kv-v2', path: 'aws' },
path: 'production/s3',
field: 'secret-key'
}
}
}
end
before do
entry.compose!
end
it 'includes secrets value' do
expect(entry.errors).to be_empty
expect(entry.value[:secrets]).to eq({
DATABASE_PASSWORD: {
vault: {
engine: { name: 'kv-v2', path: 'kv-v2' },
path: 'production/db',
field: 'password'
}
},
SSL_PRIVATE_KEY: {
vault: {
engine: { name: 'kv-v2', path: 'ops' },
path: 'production/ssl',
field: 'private-key'
}
},
S3_SECRET_KEY: {
vault: {
engine: { name: 'kv-v2', path: 'aws' },
path: 'production/s3',
field: 'secret-key'
}
}
})
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Secret do
let(:entry) { described_class.new(config) }
describe 'validation' do
before do
entry.compose!
end
context 'when entry config value is correct' do
let(:config) do
{
vault: {
engine: { name: 'kv-v2', path: 'kv-v2' },
path: 'production/db',
field: 'password'
}
}
end
describe '#value' do
it 'returns secret configuration' do
expect(entry.value).to eq(config)
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when there is an unknown key present' do
let(:config) { { foo: {} } }
it 'reports error' do
expect(entry.errors)
.to include 'secret config contains unknown keys: foo'
end
end
context 'when there is no vault entry' do
let(:config) { {} }
it 'reports error' do
expect(entry.errors)
.to include 'secret config missing required keys: vault'
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Secrets do
let(:entry) { described_class.new(config) }
describe 'validation' do
before do
entry.compose!
end
context 'when entry config value is correct' do
let(:config) { {} }
describe '#value' do
it 'returns secrets configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when config is of incorrect type' do
let(:config) { [] }
it 'reports error' do
expect(entry.errors)
.to include 'secrets config should be a hash'
end
end
end
describe '#compose!' do
context 'when valid secret entries composed' do
let(:config) do
{
DATABASE_PASSWORD: {
vault: {
engine: { name: 'kv-v2', path: 'kv-v2' },
path: 'production/db',
field: 'password'
}
}
}
end
before do
entry.compose!
end
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq(config)
end
end
describe '#descendants' do
it 'creates valid descendant nodes' do
expect(entry.descendants).to all(be_a(Gitlab::Ci::Config::Entry::Secret))
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Vault::Engine do
let(:entry) { described_class.new(config) }
describe 'validation' do
before do
entry.compose!
end
context 'when entry config value is correct' do
let(:config) { { name: 'kv-v2', path: 'kv-v2' } }
describe '#value' do
it 'returns Vault secret engine configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when there is an unknown key present' do
let(:config) { { foo: :bar } }
it 'reports error' do
expect(entry.errors)
.to include 'engine config contains unknown keys: foo'
end
end
context 'when name is not present' do
let(:config) { {} }
it 'reports error' do
expect(entry.errors)
.to include 'engine name can\'t be blank'
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Vault::Secret do
let(:entry) { described_class.new(config) }
describe 'validation' do
before do
entry.compose!
end
context 'when entry config value is correct' do
let(:hash_config) do
{
engine: {
name: 'kv-v2',
path: 'some/path'
},
path: 'production/db',
field: 'password'
}
end
context 'when config is a hash' do
let(:config) { hash_config }
describe '#value' do
it 'returns Vault secret configuration' do
expect(entry.value).to eq(hash_config)
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when config is a string with engine path' do
let(:config) { 'production/db/password@some/path' }
describe '#value' do
it 'returns Vault secret configuration' do
expect(entry.value).to eq(hash_config)
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when config is a string without engine path' do
let(:config) { 'production/db/password' }
describe '#value' do
it 'returns Vault secret configuration' do
expect(entry.value).to eq(hash_config.deep_merge(engine: { path: 'kv-v2' }))
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when there is an unknown key present' do
let(:config) { { foo: :bar } }
it 'reports error' do
expect(entry.errors)
.to include 'hash strategy config contains unknown keys: foo'
end
end
context 'when path is not present' do
let(:config) { {} }
it 'reports error' do
expect(entry.errors)
.to include 'hash strategy path can\'t be blank'
end
end
context 'when field is not present' do
let(:config) { {} }
it 'reports error' do
expect(entry.errors)
.to include 'hash strategy field can\'t be blank'
end
end
context 'when engine is not a hash' do
let(:config) { { engine: [] } }
it 'reports error' do
expect(entry.errors)
.to include 'hash strategy engine should be a hash'
end
end
end
end
end
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
allow_failure type when start_in artifacts cache allow_failure type when start_in artifacts cache
dependencies before_script needs after_script dependencies before_script needs after_script
environment coverage retry parallel interruptible timeout environment coverage retry parallel interruptible timeout
resource_group release].freeze resource_group release secrets].freeze
REQUIRED_BY_NEEDS = %i[stage].freeze REQUIRED_BY_NEEDS = %i[stage].freeze
...@@ -191,3 +191,5 @@ module Gitlab ...@@ -191,3 +191,5 @@ module Gitlab
end end
end end
end end
::Gitlab::Ci::Config::Entry::Job.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Job')
...@@ -33,7 +33,7 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -33,7 +33,7 @@ describe Gitlab::Ci::Config::Entry::Job do
inherit] inherit]
end end
it { is_expected.to match_array result } it { is_expected.to include(*result) }
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