simplygenius-atmos 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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