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
entry.compose!
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 has needs' do
context 'when needs is bridge type' do
......@@ -27,6 +35,73 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
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
# 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
allow_failure type when start_in artifacts cache
dependencies before_script needs after_script
environment coverage retry parallel interruptible timeout
resource_group release].freeze
resource_group release secrets].freeze
REQUIRED_BY_NEEDS = %i[stage].freeze
......@@ -191,3 +191,5 @@ module Gitlab
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
inherit]
end
it { is_expected.to match_array result }
it { is_expected.to include(*result) }
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