Commit 56ccff6b authored by allison.browne's avatar allison.browne

Add a ComposableHash class to DRY entries

Use a composable hash to dry up some boiler plate entry code
used for ci configurations where the keys can be anything
the user specifies
parent 0d85c54b
......@@ -12,9 +12,10 @@ module EE
prepended do
attributes :secrets
entry :secrets, ::Gitlab::Ci::Config::Entry::Secrets,
entry :secrets, ::Gitlab::Config::Entry::ComposableHash,
description: 'Configured secrets for this job',
inherit: false
inherit: false,
metadata: { composable_class: ::Gitlab::Ci::Config::Entry::Secret }
end
override :value
......
# 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::Validatable
validations do
validates :config, type: Hash
end
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
require 'spec_helper'
RSpec.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
......@@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a set of jobs.
#
class Jobs < ::Gitlab::Config::Entry::Node
class Jobs < ::Gitlab::Config::Entry::ComposableHash
include ::Gitlab::Config::Entry::Validatable
validations do
......@@ -36,6 +36,10 @@ module Gitlab
end
end
def composable_class(name, config)
self.class.find_type(name, config)
end
TYPES = [Entry::Hidden, Entry::Job, Entry::Bridge].freeze
private_constant :TYPES
......@@ -49,29 +53,6 @@ module Gitlab
type.matching?(name, config)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def compose!(deps = nil)
super do
@config.each do |name, config|
node = self.class.find_type(name, config)
next unless node
factory = ::Gitlab::Config::Entry::Factory.new(node)
.value(config || {})
.metadata(name: name)
.with(key: name, parent: self,
description: "#{name} job definition.")
@entries[name] = factory.create!
end
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
......
# frozen_string_literal: true
module Gitlab
module Config
module Entry
##
# Entry that represents a composable hash definition
# Where each hash key can be any value written by the user
#
class ComposableHash < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, type: Hash
end
def compose!(deps = nil)
super do
@config.each do |name, config|
entry_class = composable_class(name, config)
raise ArgumentError, 'Missing Composable class' unless entry_class
entry_class_name = entry_class.name.split('::').last.downcase
factory = ::Gitlab::Config::Entry::Factory.new(entry_class)
.value(config || {})
.with(key: name, parent: self, description: "#{name} #{entry_class_name} definition") # rubocop:disable CodeReuse/ActiveRecord
.metadata(name: name)
@entries[name] = factory.create!
end
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
def composable_class(name = nil, config = nil)
opt(:composable_class)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do
let(:valid_config) do
{
DATABASE_SECRET: 'passw0rd',
API_TOKEN: 'passw0rd2',
}
end
let(:config) { valid_config }
shared_examples 'composes a hash' do
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
context 'is invalid' do
let(:config) { %w[one two] }
it { expect(entry).not_to be_valid }
end
end
describe '#value' do
context 'when config is a hash' do
it 'returns key value' do
expect(entry.value).to eq config
end
end
end
describe '#compose!' do
before do
entry.compose!
end
it 'composes child entry with configured value' do
expect(entry.value).to eq(config)
end
it 'composes child entries with configured values' do
expect(entry[:DATABASE_SECRET]).to be_a(Gitlab::Config::Entry::Node)
expect(entry[:DATABASE_SECRET].description).to eq('DATABASE_SECRET node definition')
expect(entry[:DATABASE_SECRET].key).to eq(:DATABASE_SECRET)
expect(entry[:DATABASE_SECRET].metadata).to eq(name: :DATABASE_SECRET)
expect(entry[:DATABASE_SECRET].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
expect(entry[:DATABASE_SECRET].value).to eq('passw0rd')
expect(entry[:API_TOKEN]).to be_a(Gitlab::Config::Entry::Node)
expect(entry[:API_TOKEN].description).to eq('API_TOKEN node definition')
expect(entry[:API_TOKEN].key).to eq(:API_TOKEN)
expect(entry[:API_TOKEN].metadata).to eq(name: :API_TOKEN)
expect(entry[:API_TOKEN].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
expect(entry[:API_TOKEN].value).to eq('passw0rd2')
end
describe '#descendants' do
it 'creates descendant nodes' do
expect(entry.descendants.first).to be_a(Gitlab::Config::Entry::Node)
expect(entry.descendants.first.value).to eq('passw0rd')
expect(entry.descendants.second).to be_a(Gitlab::Config::Entry::Node)
expect(entry.descendants.second.value).to eq('passw0rd2')
end
end
end
end
context 'when ComposableHash is instantiated' do
let(:entry) { described_class.new(config) }
before do
allow(entry).to receive(:composable_class).and_return(Gitlab::Config::Entry::Node)
end
it_behaves_like 'composes a hash'
end
context 'when ComposableHash entry is configured in the parent class' do
let(:composable_hash_parent_class) do
Class.new(Gitlab::Config::Entry::Node) do
include ::Gitlab::Config::Entry::Configurable
entry :secrets, ::Gitlab::Config::Entry::ComposableHash,
description: 'Configured secrets for this job',
inherit: false,
default: { hello: :world },
metadata: { composable_class: Gitlab::Config::Entry::Node }
end
end
let(:entry) do
parent_entry = composable_hash_parent_class.new(secrets: config)
parent_entry.compose!
parent_entry[:secrets]
end
it_behaves_like 'composes a hash'
it 'creates entry with configuration from parent class' do
expect(entry.default).to eq({ hello: :world })
expect(entry.metadata).to eq(composable_class: Gitlab::Config::Entry::Node)
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