Skip to content
This repository was archived by the owner on Jun 29, 2023. It is now read-only.

Commit 5b2f01c

Browse files
authored
Support libraries parameter to extend a deploy.yaml with other ones (#45)
## Use case Ideally, using KDT at LiveRamp should be as simple as including a file which contains all LiveRamp specific defaults, such as the default destination of Artifactory for all published files, a default gcp artifact, the use of the gcp image registry, and any authentication hookups which help use KDT with Jenkins. ## Solution Define a `libraries` section of a deploy.yaml which permits downloading an arbitrary number of local or gs:// files to extend this config with. This way, we can define a gs://liveramp-kdt-configs/v1.yaml (or osmething) which contains all of the above defaults. Most people would use it this way: ``` version: 2 libraries: - gs://liveramp-kdt-configs/v1.yaml ``` Assuming v1 contained all the expected values, this should be a valid KDT deploy.yaml and we would expect many apps to just use this verbatim. ### A note on precedence The definition of extension is particular. When one deploy file references a library, the in-memory model is extended with the contents of the library file. Extension means different things for different fields, but in general: * Items in the library but not in the original file are merged into the original file. * Items in the original file which conflict with the library will not be updated from the library. * Library hooks will be executed first, in order, and your own hooks will be run at the end. This is because a library might be expected to prepare some stuff in the background to make execution of the default hooks possible or easier. ### Other cleanups * This change removes a bunch of v1 compatibility. In v3 release of KDT, we intend to completely remove that support.
1 parent 4b64081 commit 5b2f01c

File tree

13 files changed

+289
-231
lines changed

13 files changed

+289
-231
lines changed

.ruby-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.3.1
1+
2.6.6

lib/kube_deploy_tools/deploy_config_file.rb

Lines changed: 84 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
require 'yaml'
44

55
require 'kube_deploy_tools/deploy_config_file/util'
6+
require 'kube_deploy_tools/deploy_config_file/deep_merge'
67
require 'kube_deploy_tools/formatted_logger'
78
require 'kube_deploy_tools/image_registry'
9+
require 'kube_deploy_tools/shellrunner'
810

911
DEPLOY_YAML = 'deploy.yaml'
1012
DEPLOY_YML_V1 = 'deploy.yml'
@@ -16,6 +18,10 @@ class DeployConfigFile
1618

1719
include DeployConfigFileUtil
1820

21+
# TODO(joshk): Refactor into initialize(fp) which takes a file-like object;
22+
# after this, auto discovery should go into DeployConfigFile.locate
23+
# classmethod. This would require erasing auto-upgrade capability, which
24+
# should be possible if we major version bump.
1925
def initialize(filename)
2026
config = nil
2127
if !filename.nil? && Pathname.new(filename).absolute?
@@ -69,93 +75,36 @@ def initialize(filename)
6975
case version
7076
when 2
7177
fetch_and_parse_version2_config!
72-
when 1
73-
fetch_and_parse_version1_config!
7478
end
7579
end
7680

7781
def fetch_and_parse_version2_config!
82+
# The literal contents of your deploy.yaml are now populated into |self|.
7883
config = @original_config
79-
@image_registries = parse_image_registries(config.fetch('image_registries', []))
80-
@default_flags = parse_default_flags(config.fetch('default_flags', {}))
81-
@artifacts = parse_artifacts(config.fetch('artifacts', []), @default_flags, @image_registries)
82-
@flavors = parse_flavors(config.fetch('flavors', {}))
83-
@hooks = parse_hooks(config.fetch('hooks', ['default']))
84-
@expiration = parse_expiration(config.fetch('expiration', []))
85-
end
86-
87-
# Fetches and parse a version 1 config as a version 2 config, with the
88-
# defaults set as previously with KDT 1.x behavior
89-
def fetch_and_parse_version1_config!
90-
config = @original_config
91-
@image_registries = parse_image_registries([
92-
{
93-
'name' => 'aws',
94-
'driver' => 'aws',
95-
'prefix' => '***REMOVED***',
96-
'config' => {
97-
'region' => 'us-west-2'
98-
}
99-
},
100-
{
101-
'name' => 'gcp',
102-
'driver' => 'gcp',
103-
'prefix' => '***REMOVED***'
104-
},
105-
{
106-
'name' => 'local',
107-
'driver' => 'noop',
108-
'prefix' => 'local-registry'
109-
}
110-
])
111-
@default_flags = parse_default_flags({
112-
'pull_policy' => 'IfNotPresent',
113-
})
114-
@artifacts = parse_artifacts(config.fetch('deploy').fetch('clusters', [])
115-
.map.with_index { |c, i|
116-
target = c.fetch('target')
117-
environment = c.fetch('environment')
118-
case target
119-
when 'local'
120-
cloud = 'local'
121-
image_registry = 'local'
122-
when 'colo-service'
123-
cloud = 'colo'
124-
image_registry = 'aws'
125-
when 'us-east-1', 'us-west-2', 'eu-west-1'
126-
cloud = 'aws'
127-
image_registry = 'aws'
128-
when 'gcp'
129-
cloud = 'gcp'
130-
image_registry = 'gcp'
131-
else
132-
raise ArgumentError, "Expected a valid KDT 1.x .target for .deploy.clusters[#{i}].target, but got '#{target}'"
133-
end
134-
135-
flags = c.fetch('extra_flags', {})
136-
.merge({
137-
'target' => target,
138-
'environment' => environment,
139-
'cloud' => cloud
140-
})
141-
142-
if flags.key?('pull_policy') && flags.fetch('pull_policy') == @default_flags.fetch('pull_policy')
143-
flags.delete('pull_policy')
144-
end
14584

146-
artifact = {
147-
'name' => target + '-' + environment,
148-
'image_registry' => image_registry,
149-
'flags' => flags,
150-
}
85+
@image_registries = parse_image_registries(config.fetch('image_registries', []))
86+
@default_flags = config.fetch('default_flags', {})
87+
@artifacts = config.fetch('artifacts', [])
88+
@flavors = config.fetch('flavors', {})
89+
@hooks = config.fetch('hooks', ['default'])
90+
@expiration = config.fetch('expiration', [])
91+
92+
validate_default_flags
93+
validate_flavors
94+
validate_hooks
95+
validate_expiration
96+
97+
# Augment these literal contents by resolving all libraries.
98+
# extend! typically gives the current file precedence when merge conflicts occur,
99+
# but the expected precedence of library inclusion is the reverse (library 2 should
100+
# overwrite what library 1 specifies), so reverse the libraries list first.
101+
config.fetch('libraries', []).reverse.each do |libfn|
102+
extend!(load_library(libfn))
103+
end
151104

152-
artifact
153-
},
154-
@default_flags,
155-
@image_registries
156-
)
157-
@flavors = parse_flavors(config.fetch('deploy', {}).fetch('flavors', {}))
158-
@hooks = parse_hooks(config.fetch('deploy', {}).fetch('hooks', ['default']))
105+
# Now that we have a complete list of image registries, validation is now possible.
106+
# Note that this also populates @valid_image_registries.
107+
validate_artifacts!
159108
end
160109

161110
def parse_image_registries(image_registries)
@@ -182,8 +131,8 @@ def map_image_registry(image_registries)
182131
valid_image_registries
183132
end
184133

185-
# .artifacts depends on .default_flags
186-
def parse_artifacts(artifacts, default_flags, image_registries)
134+
# .artifacts depends on .default_flags and .image_registries
135+
def validate_artifacts!
187136
check_and_err(artifacts.is_a?(Array), '.artifacts is not an Array')
188137

189138
duplicates = select_duplicates(artifacts.map { |i| i.fetch('name') })
@@ -192,7 +141,7 @@ def parse_artifacts(artifacts, default_flags, image_registries)
192141
"Expected .artifacts names to be unique, but found duplicates: #{duplicates}"
193142
)
194143

195-
@valid_image_registries = map_image_registry(image_registries)
144+
@valid_image_registries = map_image_registry(@image_registries)
196145

197146
artifacts.each_with_index { |artifact, index|
198147
check_and_err(
@@ -218,28 +167,20 @@ def parse_artifacts(artifacts, default_flags, image_registries)
218167
}
219168
end
220169

221-
def parse_default_flags(default_flags)
222-
check_and_err(default_flags.is_a?(Hash), '.default_flags is not a Hash')
223-
224-
default_flags
170+
def validate_default_flags
171+
check_and_err(@default_flags.is_a?(Hash), '.default_flags is not a Hash')
225172
end
226173

227-
def parse_flavors(flavors)
228-
check_and_err(flavors.is_a?(Hash), '.flavors is not a Hash')
229-
230-
flavors
174+
def validate_flavors
175+
check_and_err(@flavors.is_a?(Hash), '.flavors is not a Hash')
231176
end
232177

233-
def parse_hooks(hooks)
234-
check_and_err(hooks.is_a?(Array), '.hooks is not an Array')
235-
236-
hooks
178+
def validate_hooks
179+
check_and_err(@hooks.is_a?(Array), '.hooks is not an Array')
237180
end
238181

239-
def parse_expiration(expiration)
240-
check_and_err(expiration.is_a?(Array), '.expiration is not an Array')
241-
242-
expiration
182+
def validate_expiration
183+
check_and_err(@expiration.is_a?(Array), '.expiration is not an Array')
243184
end
244185

245186
# upgrade! converts the config to a YAML string in the format
@@ -250,48 +191,55 @@ def upgrade!
250191
version = @original_config.fetch('version', 1)
251192
case version
252193
when 2
253-
config = @original_config
254-
when 1
255-
Logger.info('Upgrading v1 deploy.yml to v2 deploy.yaml')
256-
config = {
257-
'version' => 2,
258-
'artifacts' => @artifacts.map { |a|
259-
{
260-
'name' => a.fetch('name'),
261-
'image_registry' => a.fetch('image_registry'),
262-
'flags' => a.fetch('flags', {})
263-
}
264-
},
265-
'flavors' => @flavors,
266-
'default_flags' => @default_flags,
267-
'hooks' => @hooks,
268-
'image_registries' => @image_registries.map { |_, i|
269-
image_registry = {
270-
'name' => i.name,
271-
'driver' => i.driver,
272-
'prefix' => i.prefix,
273-
}
274-
275-
image_registry['config'] = i.config if !i.config.nil?
276-
277-
image_registry
278-
}
279-
}
280-
end
281-
282-
File.open(@filename, 'w+') { |file| file.write(config.to_yaml) }
283-
284-
# Rename deploy.yml to deploy.yaml, if necessary
285-
dirname = File.dirname(@filename)
286-
basename = File.basename(@filename)
287-
if basename == DEPLOY_YML_V1
288-
Logger.info('Renaming deploy.yml => deploy.yaml')
289-
File.rename(@filename, "#{dirname}/#{DEPLOY_YAML}")
194+
# TODO(joshk): Any required updates to v3 or remove this entire method
195+
true
290196
end
291197
end
292198

293199
def select_duplicates(array)
294200
array.select { |n| array.count(n) > 1 }.uniq
295201
end
202+
203+
# Extend this DeployConfigFile with another instance.
204+
def extend!(other)
205+
# Any image_registries entry in |self| should take precedence
206+
# over any identical key in |other|. The behavior of merge is that
207+
# the 'other' hash wins.
208+
@image_registries = other.image_registries.merge(@image_registries)
209+
210+
# Same behavior as above for #default_flags.
211+
@default_flags = other.default_flags.merge(@default_flags)
212+
213+
# artifacts should be merged by 'name'. In other words, if |self| and |other|
214+
# specify the same 'name' of a registry, self's config for that registry
215+
# should win wholesale (no merging of flags.)
216+
@artifacts = (@artifacts + other.artifacts).uniq { |h| h.fetch('name') }
217+
218+
# Same behavior as for flags and registries, but the flags within the flavor
219+
# are in a Hash, so we need a deep merge.
220+
@flavors = other.flavors.deep_merge(@flavors)
221+
222+
# A break from the preceding merging logic - Dependent hooks have to come
223+
# first and a given named hook can only be run once. But seriously, you
224+
# probably don't want to make a library that specifies hooks.
225+
@hooks = (other.hooks + @hooks).uniq
226+
227+
@expiration = (@expiration + other.expiration).uniq { |h| h.fetch('repository') }
228+
end
229+
230+
def to_h
231+
{
232+
'image_registries' => @image_registries.values.map(&:to_h),
233+
'default_flags' => @default_flags,
234+
'artifacts' => @artifacts,
235+
'flavors' => @flavors,
236+
'hooks' => @hooks,
237+
'expiration' => @expiration,
238+
}
239+
end
240+
241+
def self.deep_merge(h, other)
242+
243+
end
296244
end
297245
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Downloaded from
2+
# https://raw.githubusercontent.com/rails/rails/v6.0.2.2/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
3+
# to avoid a dependency on activesupport.
4+
5+
# frozen_string_literal: true
6+
7+
class Hash
8+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
9+
#
10+
# h1 = { a: true, b: { c: [1, 2, 3] } }
11+
# h2 = { a: false, b: { x: [3, 4, 5] } }
12+
#
13+
# h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
14+
#
15+
# Like with Hash#merge in the standard library, a block can be provided
16+
# to merge values:
17+
#
18+
# h1 = { a: 100, b: 200, c: { c1: 100 } }
19+
# h2 = { b: 250, c: { c1: 200 } }
20+
# h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
21+
# # => { a: 100, b: 450, c: { c1: 300 } }
22+
def deep_merge(other_hash, &block)
23+
dup.deep_merge!(other_hash, &block)
24+
end
25+
26+
# Same as +deep_merge+, but modifies +self+.
27+
def deep_merge!(other_hash, &block)
28+
merge!(other_hash) do |key, this_val, other_val|
29+
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
30+
this_val.deep_merge(other_val, &block)
31+
elsif block_given?
32+
block.call(key, this_val, other_val)
33+
else
34+
other_val
35+
end
36+
end
37+
end
38+
end

lib/kube_deploy_tools/deploy_config_file/util.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,26 @@ def check_and_warn(condition, warning)
1414
Logger.warn(warning)
1515
end
1616
end
17+
18+
def load_library(lib)
19+
# All paths must be valid accessible gcs paths for the current user.
20+
# To modify gcloud identity being used by this process, set
21+
# GOOGLE_APPLICATION_CREDENTIALS or sign in with `gcloud auth login`
22+
lib_config = nil
23+
if lib.start_with?('gs://')
24+
Tempfile.open(['gs-kdt-library', '.yaml']) do |t|
25+
out = Shellrunner.check_call('gsutil', 'cat', lib)
26+
t.write(out)
27+
t.flush
28+
lib_config = DeployConfigFile.new(t.path)
29+
end
30+
elsif File.exist?(lib)
31+
lib_config = DeployConfigFile.new(lib)
32+
else
33+
raise "Unsupported or non-existent library: #{lib}"
34+
end
35+
36+
lib_config
37+
end
1738
end
1839
end

lib/kube_deploy_tools/image_registry.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,14 @@ def ==(o)
1717
@prefix == o.prefix
1818
@config == o.config
1919
end
20+
21+
def to_h
22+
{
23+
'name' => @name,
24+
'driver' => @driver,
25+
'prefix' => @prefix,
26+
'config' => @config,
27+
}
28+
end
2029
end
2130
end

spec/resources/deploy_v1.yml

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)