simplygenius-atmos 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/exe/atmos +2 -2
  4. data/lib/{atmos.rb → simplygenius/atmos.rb} +9 -7
  5. data/lib/simplygenius/atmos/cli.rb +116 -0
  6. data/lib/simplygenius/atmos/commands/account.rb +69 -0
  7. data/lib/simplygenius/atmos/commands/apply.rb +24 -0
  8. data/lib/simplygenius/atmos/commands/auth_exec.rb +34 -0
  9. data/lib/simplygenius/atmos/commands/base_command.rb +16 -0
  10. data/lib/simplygenius/atmos/commands/bootstrap.rb +76 -0
  11. data/lib/simplygenius/atmos/commands/container.rb +62 -0
  12. data/lib/simplygenius/atmos/commands/destroy.rb +22 -0
  13. data/lib/simplygenius/atmos/commands/generate.rb +187 -0
  14. data/lib/simplygenius/atmos/commands/init.rb +22 -0
  15. data/lib/simplygenius/atmos/commands/new.rb +22 -0
  16. data/lib/simplygenius/atmos/commands/otp.rb +58 -0
  17. data/lib/simplygenius/atmos/commands/plan.rb +24 -0
  18. data/lib/simplygenius/atmos/commands/secret.rb +91 -0
  19. data/lib/simplygenius/atmos/commands/terraform.rb +56 -0
  20. data/lib/simplygenius/atmos/commands/user.rb +78 -0
  21. data/lib/simplygenius/atmos/config.rb +279 -0
  22. data/lib/simplygenius/atmos/exceptions.rb +13 -0
  23. data/lib/simplygenius/atmos/generator.rb +232 -0
  24. data/lib/simplygenius/atmos/ipc.rb +136 -0
  25. data/lib/simplygenius/atmos/ipc_actions/notify.rb +31 -0
  26. data/lib/simplygenius/atmos/ipc_actions/ping.rb +23 -0
  27. data/lib/simplygenius/atmos/logging.rb +164 -0
  28. data/lib/simplygenius/atmos/otp.rb +62 -0
  29. data/lib/simplygenius/atmos/plugin.rb +27 -0
  30. data/lib/simplygenius/atmos/plugin_manager.rb +120 -0
  31. data/lib/simplygenius/atmos/plugins/output_filter.rb +29 -0
  32. data/lib/simplygenius/atmos/plugins/prompt_notify.rb +21 -0
  33. data/lib/simplygenius/atmos/provider_factory.rb +23 -0
  34. data/lib/simplygenius/atmos/providers/aws/account_manager.rb +83 -0
  35. data/lib/simplygenius/atmos/providers/aws/auth_manager.rb +220 -0
  36. data/lib/simplygenius/atmos/providers/aws/container_manager.rb +118 -0
  37. data/lib/simplygenius/atmos/providers/aws/provider.rb +53 -0
  38. data/lib/simplygenius/atmos/providers/aws/s3_secret_manager.rb +51 -0
  39. data/lib/simplygenius/atmos/providers/aws/user_manager.rb +213 -0
  40. data/lib/simplygenius/atmos/settings_hash.rb +93 -0
  41. data/lib/simplygenius/atmos/source_path.rb +186 -0
  42. data/lib/simplygenius/atmos/template.rb +117 -0
  43. data/lib/simplygenius/atmos/terraform_executor.rb +297 -0
  44. data/lib/simplygenius/atmos/ui.rb +173 -0
  45. data/lib/simplygenius/atmos/utils.rb +54 -0
  46. data/lib/simplygenius/atmos/version.rb +5 -0
  47. data/templates/new/config/atmos.yml +21 -13
  48. data/templates/new/config/atmos/recipes.yml +16 -0
  49. data/templates/new/config/atmos/runtime.yml +9 -0
  50. metadata +46 -40
  51. data/lib/atmos/cli.rb +0 -105
  52. data/lib/atmos/commands/account.rb +0 -65
  53. data/lib/atmos/commands/apply.rb +0 -20
  54. data/lib/atmos/commands/auth_exec.rb +0 -29
  55. data/lib/atmos/commands/base_command.rb +0 -12
  56. data/lib/atmos/commands/bootstrap.rb +0 -72
  57. data/lib/atmos/commands/container.rb +0 -58
  58. data/lib/atmos/commands/destroy.rb +0 -18
  59. data/lib/atmos/commands/generate.rb +0 -90
  60. data/lib/atmos/commands/init.rb +0 -18
  61. data/lib/atmos/commands/new.rb +0 -18
  62. data/lib/atmos/commands/otp.rb +0 -54
  63. data/lib/atmos/commands/plan.rb +0 -20
  64. data/lib/atmos/commands/secret.rb +0 -87
  65. data/lib/atmos/commands/terraform.rb +0 -52
  66. data/lib/atmos/commands/user.rb +0 -74
  67. data/lib/atmos/config.rb +0 -208
  68. data/lib/atmos/exceptions.rb +0 -9
  69. data/lib/atmos/generator.rb +0 -199
  70. data/lib/atmos/generator_factory.rb +0 -93
  71. data/lib/atmos/ipc.rb +0 -132
  72. data/lib/atmos/ipc_actions/notify.rb +0 -27
  73. data/lib/atmos/ipc_actions/ping.rb +0 -19
  74. data/lib/atmos/logging.rb +0 -160
  75. data/lib/atmos/otp.rb +0 -61
  76. data/lib/atmos/provider_factory.rb +0 -19
  77. data/lib/atmos/providers/aws/account_manager.rb +0 -82
  78. data/lib/atmos/providers/aws/auth_manager.rb +0 -208
  79. data/lib/atmos/providers/aws/container_manager.rb +0 -116
  80. data/lib/atmos/providers/aws/provider.rb +0 -51
  81. data/lib/atmos/providers/aws/s3_secret_manager.rb +0 -49
  82. data/lib/atmos/providers/aws/user_manager.rb +0 -211
  83. data/lib/atmos/settings_hash.rb +0 -90
  84. data/lib/atmos/terraform_executor.rb +0 -267
  85. data/lib/atmos/ui.rb +0 -159
  86. data/lib/atmos/utils.rb +0 -50
  87. data/lib/atmos/version.rb +0 -3
@@ -0,0 +1,78 @@
1
+ require_relative 'base_command'
2
+ require 'climate_control'
3
+
4
+ module SimplyGenius
5
+ module Atmos
6
+ module Commands
7
+
8
+ class User < BaseCommand
9
+
10
+ def self.description
11
+ "Manages users in the cloud provider"
12
+ end
13
+
14
+ subcommand "create", "Create a new user" do
15
+
16
+ option ["-f", "--force"],
17
+ :flag, "forces deletion/updates for pre-existing resources\n",
18
+ default: false
19
+
20
+ option ["-l", "--login"],
21
+ :flag, "generate a login password\n",
22
+ default: false
23
+
24
+ option ["-m", "--mfa"],
25
+ :flag, "setup a mfa device\n",
26
+ default: false
27
+
28
+ option ["-k", "--key"],
29
+ :flag, "create access keys\n",
30
+ default: false
31
+
32
+ option ["-p", "--public-key"],
33
+ "PUBLIC_KEY", "add ssh public key\n"
34
+
35
+ option ["-g", "--group"],
36
+ "GROUP",
37
+ "associate the given groups to new user\n",
38
+ multivalued: true
39
+
40
+ parameter "USERNAME",
41
+ "The username of the user to add\nShould be an email address" do |u|
42
+ raise ArgumentError.new("Not an email") if u !~ URI::MailTo::EMAIL_REGEXP
43
+ u
44
+ end
45
+
46
+ def execute
47
+
48
+ Atmos.config.provider.auth_manager.authenticate(ENV) do |auth_env|
49
+ ClimateControl.modify(auth_env) do
50
+ manager = Atmos.config.provider.user_manager
51
+ user = manager.create_user(username)
52
+ user.merge!(manager.set_groups(username, group_list, force: force?)) if group_list.present?
53
+ user.merge!(manager.enable_login(username, force: force?)) if login?
54
+ user.merge!(manager.enable_mfa(username, force: force?)) if mfa?
55
+ user.merge!(manager.enable_access_keys(username, force: force?)) if key?
56
+ user.merge!(manager.set_public_key(username, public_key, force: force?)) if public_key.present?
57
+
58
+ logger.info "\nUser created:\n#{display user}\n"
59
+
60
+ if mfa? && user[:mfa_secret]
61
+ save_mfa = agree("Save the MFA secret for runtime integration with auth? ") {|q|
62
+ q.default = 'y'
63
+ }
64
+ Atmos::Otp.instance.save if save_mfa
65
+ end
66
+
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,279 @@
1
+ require_relative '../atmos'
2
+ require_relative '../atmos/settings_hash'
3
+ require_relative '../atmos/provider_factory'
4
+ require_relative '../atmos/plugin_manager'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+ require 'find'
8
+
9
+ module SimplyGenius
10
+ module Atmos
11
+
12
+ class Config
13
+ include GemLogger::LoggerSupport
14
+ include FileUtils
15
+
16
+ attr_accessor :atmos_env, :root_dir,
17
+ :config_file,
18
+ :tmp_root
19
+
20
+ def initialize(atmos_env)
21
+ @atmos_env = atmos_env
22
+ @root_dir = File.expand_path(Dir.pwd)
23
+ @config_file = File.join(root_dir, "config", "atmos.yml")
24
+ @tmp_root = File.join(root_dir, "tmp")
25
+ @included_configs = []
26
+ end
27
+
28
+ def is_atmos_repo?
29
+ File.exist?(config_file)
30
+ end
31
+
32
+ def [](key)
33
+ load
34
+ result = @config.notation_get(key)
35
+ return result
36
+ end
37
+
38
+ def to_h
39
+ load
40
+ @config.to_hash
41
+ end
42
+
43
+ def provider
44
+ @provider ||= ProviderFactory.get(self[:provider])
45
+ end
46
+
47
+ def plugin_manager
48
+ @plugin_manager ||= PluginManager.new(self[:plugins])
49
+ end
50
+
51
+ def all_env_names
52
+ load
53
+ @full_config[:environments].keys
54
+ end
55
+
56
+ def account_hash
57
+ load
58
+ environments = @full_config[:environments] || {}
59
+ environments.inject(Hash.new) do |accum, entry|
60
+ accum[entry.first] = entry.last[:account_id]
61
+ accum
62
+ end
63
+ end
64
+
65
+ def tmp_dir
66
+ @tmp_dir ||= begin
67
+ dir = File.join(tmp_root, atmos_env)
68
+ logger.debug("Tmp dir: #{dir}")
69
+ mkdir_p(dir)
70
+ dir
71
+ end
72
+ end
73
+
74
+ def auth_cache_dir
75
+ @auth_cache_dir ||= begin
76
+ dir = File.join(tmp_dir, 'auth')
77
+ logger.debug("Auth cache dir: #{dir}")
78
+ mkdir_p(dir)
79
+ dir
80
+ end
81
+ end
82
+
83
+ def tf_working_dir(group='default')
84
+ @tf_working_dir ||= {}
85
+ @tf_working_dir[group] ||= begin
86
+ dir = File.join(tmp_dir, 'tf', group)
87
+ logger.debug("Terraform working dir: #{dir}")
88
+ mkdir_p(dir)
89
+ dir
90
+ end
91
+ end
92
+
93
+ def add_user_load_path(*paths)
94
+ load_path = paths + Array(self[:load_path])
95
+ if load_path.present?
96
+ load_path = load_path.collect { |path| File.expand_path(path) }
97
+ logger.debug("Adding to load path: #{load_path.inspect}")
98
+ $LOAD_PATH.insert(0, *load_path)
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ INTERP_PATTERN = /(\#\{([^\}]+)\})/
105
+
106
+ def config_merge(lhs, rhs)
107
+ result = nil
108
+
109
+ return rhs if lhs.nil?
110
+ return lhs if rhs.nil?
111
+
112
+ # Warn if user fat fingered config
113
+ unless lhs.is_a?(rhs.class) || rhs.is_a?(lhs.class)
114
+ logger.warn("Different types in deep merge: #{lhs.class}, #{rhs.class}")
115
+ end
116
+
117
+ case rhs
118
+ when Hash
119
+ result = lhs.deep_dup
120
+
121
+ lhs.each do |k, v|
122
+ if k =~ /^\^(.*)/
123
+ key = k.is_a?(Symbol) ? $1.to_sym : $1
124
+ result[key] = result.delete(k)
125
+ end
126
+ end
127
+
128
+ rhs.each do |k, v|
129
+ if k =~ /^\^(.*)/
130
+ key = k.is_a?(Symbol) ? $1.to_sym : $1
131
+ result[key] = v
132
+ else
133
+ result[k] = config_merge(result[k], v)
134
+ end
135
+ end
136
+ when Enumerable
137
+ result = lhs + rhs
138
+ else
139
+ result = rhs
140
+ end
141
+
142
+ return result
143
+ end
144
+
145
+ def load_config_sources(relative_root, config, *patterns)
146
+ patterns.each do |pattern|
147
+ logger.debug("Loading atmos config files using pattern: #{pattern}")
148
+
149
+ # relative to main atmos config file unless qualified
150
+ if pattern !~ /^[\/~]/
151
+ pattern = File.join(relative_root, pattern)
152
+ end
153
+ # expand to handle tilde/etc
154
+ pattern = File.expand_path(pattern)
155
+ logger.debug("Expanded pattern: #{pattern}")
156
+
157
+ Dir[pattern].each do |f|
158
+ logger.debug("Loading atmos config file: #{f}")
159
+ h = SettingsHash.new(YAML.load_file(f))
160
+ config = config_merge(config, h)
161
+ @included_configs << f
162
+ end
163
+ end
164
+
165
+ config
166
+ end
167
+
168
+ def load_submap(relative_root, group, name, config)
169
+ submap_dir = File.join(relative_root, 'atmos', group)
170
+ submap_file = File.join(submap_dir, "#{name}.yml")
171
+ if File.exist?(submap_file)
172
+ logger.debug("Loading atmos #{group} config file: #{submap_file}")
173
+ h = SettingsHash.new({group => {name => YAML.load_file(submap_file)}})
174
+ config = config_merge(config, h)
175
+ @included_configs << submap_file
176
+ end
177
+
178
+ begin
179
+ submap = config.deep_fetch(group, name)
180
+ config = config_merge(config, submap)
181
+ rescue
182
+ logger.debug("No #{group} config found for '#{name}'")
183
+ end
184
+
185
+ config
186
+ end
187
+
188
+ def load
189
+ @config ||= begin
190
+
191
+ logger.debug("Atmos env: #{atmos_env}")
192
+
193
+ if ! File.exist?(config_file)
194
+ logger.warn "Could not find an atmos config file at: #{config_file}"
195
+ @full_config = SettingsHash.new
196
+ else
197
+ logger.debug("Loading atmos config file #{config_file}")
198
+ @full_config = SettingsHash.new(YAML.load_file(config_file))
199
+ @included_configs << config_file
200
+ end
201
+
202
+ @full_config = load_config_sources(File.dirname(config_file), @full_config, *Array(@full_config[:config_sources]))
203
+
204
+ @full_config['provider'] = provider_name = @full_config['provider'] || 'aws'
205
+
206
+ @full_config = load_submap(File.dirname(config_file), 'providers', provider_name, @full_config)
207
+ @full_config = load_submap(File.dirname(config_file), 'environments', atmos_env, @full_config)
208
+
209
+ global = SettingsHash.new(@full_config.reject {|k, v| ['providers', 'environments'].include?(k) })
210
+ conf = config_merge(global, {
211
+ atmos_env: atmos_env,
212
+ atmos_version: VERSION
213
+ })
214
+ expand(conf, conf)
215
+ end
216
+ end
217
+
218
+ def expand(config, obj)
219
+ case obj
220
+ when Hash
221
+ SettingsHash.new(Hash[obj.collect {|k, v| [k, expand(config, v)] }])
222
+ when Array
223
+ result = obj.collect {|i| expand(config, i) }
224
+ # HACK: accounting for the case when someone wants to force an override using '^' as the first list item, when
225
+ # there is no upstream to override (i.e. merge proc doesn't get triggered as key is unique, so just added verbatim)
226
+ result.delete_at(0) if result[0] == "^"
227
+ result
228
+ when String
229
+ result = obj
230
+ result.scan(INTERP_PATTERN).each do |substr, statement|
231
+ # TODO: check for cycles
232
+ if statement =~ /^[\w\.\[\]]$/
233
+ val = config.notation_get(statement)
234
+ else
235
+ # TODO: be consistent with dot notation between eval and
236
+ # notation_get. eval ends up calling Hashie method_missing,
237
+ # which returns nil if a key doesn't exist, causing a nil
238
+ # exception for next item in chain, while notation_get returns
239
+ # nil gracefully for the entire chain (preferred)
240
+ begin
241
+ val = eval(statement, config.instance_eval("binding"))
242
+ rescue => e
243
+ file, line = find_config_error(substr)
244
+ file_msg = file.nil? ? "" : " in #{File.basename(file)}:#{line}"
245
+ raise RuntimeError.new("Failing config statement '#{substr}'#{file_msg} => #{e.class} #{e.message}")
246
+ end
247
+ end
248
+ result = result.sub(substr, expand(config, val).to_s)
249
+ end
250
+ result = true if result == 'true'
251
+ result = false if result == 'false'
252
+ result
253
+ else
254
+ obj
255
+ end
256
+ end
257
+
258
+ def find_config_error(statement)
259
+ filename = nil
260
+ line = 0
261
+
262
+ @included_configs.each do |c|
263
+ current_line = 0
264
+ File.foreach(c) do |f|
265
+ current_line += 1
266
+ if f.include?(statement)
267
+ filename = c
268
+ line = current_line
269
+ break
270
+ end
271
+ end
272
+ end
273
+
274
+ return filename, line
275
+ end
276
+ end
277
+
278
+ end
279
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../atmos'
2
+
3
+ module SimplyGenius
4
+ module Atmos
5
+
6
+ module Exceptions
7
+ class UsageError < Clamp::UsageError
8
+
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,232 @@
1
+ require_relative '../atmos'
2
+ require_relative '../atmos/ui'
3
+ require 'thor'
4
+ require 'find'
5
+
6
+ module SimplyGenius
7
+ module Atmos
8
+
9
+ # From https://github.com/rubber/rubber/blob/master/lib/rubber/commands/vulcanize.rb
10
+ class Generator
11
+
12
+ include GemLogger::LoggerSupport
13
+
14
+ attr_reader :visited_templates
15
+
16
+ def initialize(**opts)
17
+ if opts.has_key?(:dependencies)
18
+ @dependencies = opts.delete(:dependencies)
19
+ else
20
+ @dependencies = true
21
+ end
22
+ @thor_opts = opts
23
+ @thor_generators = {}
24
+ @resolved_templates = {}
25
+ @visited_templates = []
26
+ end
27
+
28
+ def generate(*template_names, context: {})
29
+ seen = Set.new
30
+
31
+ template_names.each do |template_name|
32
+
33
+ # clone since we are mutating context and can be called from within a
34
+ # template, walk_deps also clones
35
+ tmpl = SourcePath.find_template(template_name)
36
+ tmpl.clone.context.merge!(context)
37
+
38
+ if @dependencies
39
+ deps = tmpl.walk_dependencies.to_a
40
+ else
41
+ deps = [tmpl]
42
+ end
43
+
44
+ deps.each do |dep_tmpl|
45
+ seen_tmpl = [dep_tmpl.name, dep_tmpl.scoped_context]
46
+ unless seen.include?(seen_tmpl)
47
+ apply_template(dep_tmpl)
48
+ end
49
+ seen << seen_tmpl
50
+ end
51
+
52
+ end
53
+
54
+ return visited_templates
55
+ end
56
+
57
+ def apply_template(tmpl)
58
+ @thor_generators[tmpl.source] ||= Class.new(ThorGenerator) do
59
+ source_root tmpl.source.directory
60
+ end
61
+
62
+ gen = @thor_generators[tmpl.source].new(tmpl, self, **@thor_opts)
63
+ gen.apply
64
+ visited_templates << tmpl
65
+
66
+ gen # makes testing easier by giving a handle to thor generator instance
67
+ end
68
+
69
+ protected
70
+
71
+ class ThorGenerator < Thor
72
+
73
+ include Thor::Actions
74
+ attr_reader :tmpl, :parent
75
+
76
+ def initialize(tmpl, parent, **opts)
77
+ @tmpl = tmpl
78
+ @parent = parent
79
+ super([], **opts)
80
+ end
81
+
82
+ no_commands do
83
+
84
+ include GemLogger::LoggerSupport
85
+ include UI
86
+
87
+ def apply
88
+ template_dir = tmpl.directory
89
+ path = tmpl.source.directory
90
+
91
+ logger.debug("Applying template '#{tmpl.name}' from '#{template_dir}' in sourcepath '#{path}'")
92
+
93
+ Find.find(template_dir) do |f|
94
+ next if f == template_dir # don't create a directory for the template dir itself, but don't prune so we recurse
95
+ Find.prune if f == tmpl.config_path # don't copy over templates.yml
96
+ Find.prune if f == tmpl.actions_path # don't copy over templates.rb
97
+
98
+ # Using File.join(x, '') to ensure trailing slash to make sure we end
99
+ # up with a relative path
100
+ template_rel = f.gsub(/#{File.join(template_dir, '')}/, '')
101
+ source_rel = f.gsub(/#{File.join(path, '')}/, '')
102
+ dest_rel = source_rel.gsub(/^#{File.join(tmpl.name, '')}/, '')
103
+
104
+ # Only include optional files when their conditions eval to true
105
+ optional = tmpl.optional[template_rel]
106
+ if optional
107
+ exclude = ! eval(optional)
108
+ logger.debug("Optional template '#{template_rel}' with condition: '#{optional}', excluding=#{exclude}")
109
+ Find.prune if exclude
110
+ end
111
+
112
+ logger.debug("Template '#{source_rel}' => '#{dest_rel}'")
113
+ if File.directory?(f)
114
+ empty_directory(dest_rel)
115
+ else
116
+ copy_file(source_rel, dest_rel, mode: :preserve)
117
+ end
118
+ end
119
+
120
+ eval tmpl.actions, binding, tmpl.actions_path
121
+ end
122
+
123
+ def context
124
+ tmpl.context
125
+ end
126
+
127
+ def scoped_context
128
+ tmpl.scoped_context
129
+ end
130
+
131
+ def lookup_context(varname)
132
+ varname.blank? ? nil: tmpl.scoped_context[varname]
133
+ end
134
+
135
+ def track_context(varname, value)
136
+ varname.blank? || value.nil? ? nil: tmpl.scoped_context[varname] = value
137
+ end
138
+
139
+ def respond_to_missing?(method_name, *args)
140
+ scoped_context.respond_to_missing?(method_name, *args)
141
+ end
142
+
143
+ def method_missing(method_name, *args, &blk)
144
+ scoped_context.method_missing(method_name, *args, &blk)
145
+ end
146
+
147
+ end
148
+
149
+ desc "ask <question string> [varname: name]", "Asks a question, allowing context to provide answer using varname"
150
+ def ask(question, answer_type = nil, varname: nil, &details)
151
+ result = lookup_context(varname)
152
+ if result.nil?
153
+ result = super(question, answer_type, &details)
154
+ end
155
+ track_context(varname, result)
156
+ result
157
+ end
158
+
159
+ desc "agree <question string> [varname: name]", "Asks a Y/N question, allowing context to provide answer using varname"
160
+ def agree(question, character = nil, varname: nil, &details)
161
+ result = lookup_context(varname)
162
+ if result.nil?
163
+ result = super(question, character, &details)
164
+ end
165
+ result = !!result
166
+ track_context(varname, result)
167
+ result
168
+ end
169
+
170
+ desc "choose menu_block [varname: name]", "Provides a menu with choices, allowing context to provide answer using varname"
171
+ def choose(*items, varname: nil, &details)
172
+ result = lookup_context(varname)
173
+ if result.nil?
174
+ result = super(*items, &details)
175
+ end
176
+ track_context(varname, result)
177
+ result
178
+ end
179
+
180
+ desc "generate <tmpl_name> [context_hash]", "Generates the given template with optional context"
181
+ def generate(name, ctx: context)
182
+ parent.generate(name, context: ctx.clone)
183
+ end
184
+
185
+ desc "raw_config <yml_filename>", "Loads yml file"
186
+ def raw_config(yml_file)
187
+ @raw_configs ||= {}
188
+ @raw_configs[yml_file] ||= SettingsHash.new((YAML.load_file(yml_file) rescue {}))
189
+ end
190
+
191
+ desc "add_config <yml_filename> <key> <value> [additive: true]", "Adds config to yml file if not there (additive=true)"
192
+ def add_config(yml_file, key, value, additive: true)
193
+ new_yml = SettingsHash.add_config(yml_file, key, value, additive: additive)
194
+ create_file yml_file, new_yml
195
+ @raw_configs.delete(yml_file) if @raw_configs
196
+ end
197
+
198
+ desc "get_config <yml_filename> <key>", "Gets value of key (dot-notation for nesting) from yml file"
199
+ def get_config(yml_file, key)
200
+ config = raw_config(yml_file)
201
+ config.notation_get(key)
202
+ end
203
+
204
+ desc "config_present? <yml_filename> <key> <value>", "Tests if key is in yml file and equal to value if supplied"
205
+ def config_present?(yml_file, key, value=nil)
206
+ val = get_config(yml_file, key)
207
+
208
+ result = val.present?
209
+ if value && result
210
+ if val.is_a?(Array)
211
+ result = Array(value).all? {|v| val.include?(v) }
212
+ else
213
+ result = (val == value)
214
+ end
215
+ end
216
+
217
+ return result
218
+ end
219
+
220
+ desc "new_keys? <src_yml_filename> <dest_yml_filename>", "Tests if src yml has top level keys not present in dest yml"
221
+ def new_keys?(src_yml_file, dest_yml_file)
222
+ src = raw_config(src_yml_file).keys.sort
223
+ dest = raw_config(dest_yml_file).keys.sort
224
+ (src - dest).size > 0
225
+ end
226
+
227
+ end
228
+
229
+ end
230
+
231
+ end
232
+ end