kitchen-pulumi 0.1.0.pre.beta → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 364ba19dd3c354faa5da8dced15d5a512ddd5632049a7b833ae9be70988986cc
4
- data.tar.gz: 38b0bde440532a316ffeaf1da6fba1556a071d5b3021ed3e10adc90447325862
3
+ metadata.gz: d6c0ce9de8379c66a261cbc65cf916a72cc46f44b1dd1fa75351807a9a38d828
4
+ data.tar.gz: 7e346277daebaa03ca622ca61a694004d412d6a0b6f2fc1badea8d8f752b33f9
5
5
  SHA512:
6
- metadata.gz: 1ddea6c299dfe2537cf3b356af371415bebd01b134abf5f5d7a5ae053fea66f7975fa400914f53d8d8074a571a394829ff3524bc259feed9d9ea0722d3aafa0b
7
- data.tar.gz: 362722b88d15305c290f580c3f5363e3b61d1b84bd084ec88dbe59d345111d50151d64e295b585bfc5916fcb33307f8e3033cb0c12f290490644601edca09e56
6
+ metadata.gz: 88631c7633e26c60539e22deefb778bf44e5f438d2af53101ea5645a02d3633997358ef04747f85f3f12a405ad797e76a96b469eceb262850bd0b4e275eec0d5
7
+ data.tar.gz: b1ec83259b24dad3c038c77f471655ee34ead9b3478cbf5db9942f5c8db4c264d801fae99e14bc889e9f13a4a0b1d9eaeaebb820a1de779d4b13d1b7f8719fd9
data/README.md CHANGED
@@ -1,4 +1,73 @@
1
+ # Kitchen-Pulumi
2
+
3
+ [![Gem](https://img.shields.io/gem/v/kitchen-pulumi.svg)](https://rubygems.org/gems/kitchen-pulumi/)
4
+ [![Gem](https://img.shields.io/gem/dt/kitchen-pulumi.svg)](https://rubygems.org/gems/kitchen-pulumi/)
5
+ [![Gem](https://img.shields.io/gem/dtv/kitchen-pulumi.svg)](https://rubygems.org/gems/kitchen-pulumi/)
1
6
  [![CircleCI](https://circleci.com/gh/jacoblearned/kitchen-pulumi/tree/master.svg?style=shield)](https://circleci.com/gh/jacoblearned/kitchen-pulumi/tree/master)
7
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/35afd25bac772504e2a0/test_coverage)](https://codeclimate.com/github/jacoblearned/kitchen-pulumi/test_coverage)
8
+ [![Maintainability](https://api.codeclimate.com/v1/badges/35afd25bac772504e2a0/maintainability)](https://codeclimate.com/github/jacoblearned/kitchen-pulumi/maintainability)
9
+
10
+ Kitchen-Pulumi is a collection of [Test-Kitchen](https://kitchen.ci/) plugins for developing test-driven [Pulumi](https://www.pulumi.com/)-based cloud infrastructure projects.
11
+ With Kitchen-Pulumi you can provision ephemeral test stacks, verify they are in a desired state using [InSpec](https://www.inspec.io/), and tear them down to gain
12
+ confidence in your infrastructure code before it hits production.
13
+
14
+ ## Features
15
+
16
+ Kitchen-Pulumi provides a Kitchen [driver](https://kitchen.ci/docs/drivers/), [provisioner](https://kitchen.ci/docs/provisioners/),
17
+ and [verifier](https://kitchen.ci/docs/verifiers/) which collectively support the following features:
18
+
19
+ * **Language-agnostic**: Create and test Pulumi stacks in any of their [supported languages](https://www.pulumi.com/docs/reference/languages/).
20
+ * **Backend-agnostic**: Use the Pulumi SaaS backend, a local backend, or your organization's internal backend.
21
+ * **Configurable**: Easily define/override stack config values in your `.kitchen.yml` file for flexible testing across environments or scenarios.
22
+ * **Test changes over time**: Simulate changes in stack config values over time to test how your infrastructure reacts to ever-shifting user-provided values.
23
+ * **Custom state verification**: Code any validation logic you desire, provided it can be ran within an [InSpec profile](https://www.inspec.io/docs/reference/profiles/).
24
+
25
+ If there's a feature you would like to see in Kitchen-Pulumi, please create an issue with the suggested feature and its intended use case.
26
+
27
+ ## Installation
28
+
29
+ Kitchen-Pulumi is compatible with Ruby 2.6 and above. Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ # Gemfile
33
+
34
+ gem 'kitchen-pulumi', require: false
35
+ ```
36
+
37
+ And then execute:
38
+
39
+ ```
40
+ $ bundle
41
+ ```
42
+
43
+ Or install it yourself as:
44
+
45
+ ```
46
+ $ gem install kitchen-pulumi
47
+ ```
48
+
49
+ ## Quick Start / Tutorial
50
+
51
+ Check out the [serverless-rest-api-lambda example](examples/aws/serverless-rest-api-lambda) and follow the instructions in its `README`.
52
+
53
+ ## Usage
54
+
55
+ TODO: Write usage instructions here
56
+
57
+ ## Development
58
+
59
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
60
+ To install this gem onto your local machine, run `bundle exec rake install`.
61
+
62
+ ## Contributing
63
+
64
+ Bug reports and pull requests are always welcome on GitHub at https://github.com/jacoblearned/kitchen-pulumi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
65
+
66
+ ## License
67
+
68
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
69
+
70
+ ## Code of Conduct
71
+
72
+ Everyone interacting in the Kitchen-Pulumi’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jacoblearned/kitchen-pulumi/blob/master/CODE_OF_CONDUCT.md).
2
73
 
3
- # kitchen-pulumi
4
- Test-Kitchen plugins for Pulumi projects
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
3
4
  require 'kitchen'
4
5
  require 'kitchen/driver/base'
5
6
  require 'kitchen/pulumi/error'
6
7
  require 'kitchen/pulumi/shell_out'
8
+ require 'kitchen/pulumi/deep_merge'
7
9
  require 'kitchen/pulumi/configurable'
8
10
  require 'kitchen/pulumi/command/input'
9
11
  require 'kitchen/pulumi/command/output'
@@ -13,17 +15,25 @@ require 'kitchen/pulumi/config_attribute/directory'
13
15
  require 'kitchen/pulumi/config_attribute/plugins'
14
16
  require 'kitchen/pulumi/config_attribute/backend'
15
17
  require 'kitchen/pulumi/config_attribute/secrets'
16
- require 'kitchen/pulumi/config_attribute/stack'
18
+ require 'kitchen/pulumi/config_attribute/test_stack_name'
17
19
  require 'kitchen/pulumi/config_attribute/stack_evolution'
18
20
  require 'kitchen/pulumi/config_attribute/refresh_config'
21
+ require 'kitchen/pulumi/config_attribute/secrets_provider'
22
+ require 'kitchen/pulumi/config_attribute/preserve_config'
19
23
 
20
24
  module Kitchen
25
+ # This namespace is defined by Kitchen.
26
+ #
27
+ # @see https://www.rubydoc.info/gems/test-kitchen/Kitchen/Driver
21
28
  module Driver
22
29
  # Driver class implementing the CLI equivalency between Kitchen and Pulumi
30
+ #
31
+ # @author Jacob Learned
23
32
  class Pulumi < ::Kitchen::Driver::Base
24
33
  kitchen_driver_api_version 2
25
34
 
26
35
  include ::Kitchen::Pulumi::Configurable
36
+ include ::Kitchen::Logging
27
37
 
28
38
  # Include config attributes consumable via .kitchen.yml
29
39
  include ::Kitchen::Pulumi::ConfigAttribute::Config
@@ -32,47 +42,107 @@ module Kitchen
32
42
  include ::Kitchen::Pulumi::ConfigAttribute::Plugins
33
43
  include ::Kitchen::Pulumi::ConfigAttribute::Backend
34
44
  include ::Kitchen::Pulumi::ConfigAttribute::Secrets
35
- include ::Kitchen::Pulumi::ConfigAttribute::Stack
45
+ include ::Kitchen::Pulumi::ConfigAttribute::TestStackName
36
46
  include ::Kitchen::Pulumi::ConfigAttribute::StackEvolution
37
47
  include ::Kitchen::Pulumi::ConfigAttribute::RefreshConfig
48
+ include ::Kitchen::Pulumi::ConfigAttribute::SecretsProvider
49
+ include ::Kitchen::Pulumi::ConfigAttribute::PreserveConfig
38
50
 
51
+ # Initializes a stack via `pulumi stack init`
52
+ #
53
+ # @param _state [::Hash] the current kitchen state
54
+ # @return [void]
39
55
  def create(_state)
40
56
  dir = "-C #{config_directory}"
41
- stack = config_stack.empty? ? instance.suite.name : config_stack
42
- conf_file = config_file
43
-
44
57
  login
45
58
  initialize_stack(stack, dir)
46
- configure(config_config, stack, dir, conf_file)
47
- configure(config_secrets, stack, dir, conf_file, is_secret: true)
48
59
  end
49
60
 
50
- def update(_state)
61
+ # Sets stack config values via `pulumi config` and updates the stack via `pulumi up`
62
+ #
63
+ # @param _state [::Hash] the current kitchen state
64
+ # @param config_only [Boolean] specify true to update the stack config without
65
+ # applying changes to the stack via `pulumi up`
66
+ #
67
+ # for block {|temp_conf_file| ...}
68
+ # @yield [temp_conf_file] provides the path to the temporary config file used
69
+ #
70
+ # @return [void]
71
+ def update(_state, config_only: false)
51
72
  dir = "-C #{config_directory}"
52
- stack = config_stack.empty? ? instance.suite.name : config_stack
53
- conf_file = config_file
54
73
 
55
- login
56
- refresh_config(stack, dir, conf_file) if config_refresh_config
57
- update_stack(stack, dir, conf_file)
58
- evolve_stack(stack, dir) unless config_stack_evolution.empty?
74
+ ::Kitchen::Pulumi.with_temp_conf(config_file) do |temp_conf_file|
75
+ login
76
+ refresh_config(stack, temp_conf_file, dir) if config_refresh_config
77
+ configure(config_config, stack, temp_conf_file, dir)
78
+ configure(config_secrets, stack, temp_conf_file, dir, is_secret: true)
79
+ update_stack(stack, temp_conf_file, dir) unless config_only
80
+
81
+ unless config_stack_evolution.empty?
82
+ evolve_stack(stack, temp_conf_file, dir, config_only: config_only)
83
+ end
84
+
85
+ yield temp_conf_file if block_given?
86
+ end
59
87
  end
60
88
 
89
+ # Destroys a stack via `pulumi destroy`
90
+ #
91
+ # @param _state [::Hash] the current kitchen state
92
+ # @return [void]
61
93
  def destroy(_state)
62
94
  dir = "-C #{config_directory}"
63
- stack = config_stack.empty? ? instance.suite.name : config_stack
64
95
 
65
96
  cmds = [
66
97
  "destroy -y -r --show-config -s #{stack} #{dir}",
67
- "stack rm --preserve-config -y -s #{stack} #{dir}",
98
+ "stack rm #{preserve_config} -y -s #{stack} #{dir}",
68
99
  ]
69
100
 
70
101
  login
71
102
  ::Kitchen::Pulumi::ShellOut.run(cmd: cmds, logger: logger)
72
103
  rescue ::Kitchen::Pulumi::Error => e
73
- puts 'Continuing...' if e.message.match?(/no stack named '#{stack}' found/)
104
+ if e.message.match?(/no stack named '#{stack}' found/) || (
105
+ e.message.match?(/failed to load checkpoint/) && config_backend == 'local'
106
+ )
107
+ puts "Stack '#{stack}' does not exist, continuing..."
108
+ end
109
+ end
110
+
111
+ # Returns `--preserve-config` if the `preserve_config` instance attribute is set
112
+ #
113
+ # @return [String] either `''` or `--preserve-config`
114
+ def preserve_config
115
+ return '' unless config_preserve_config
116
+
117
+ '--preserve-config'
118
+ end
119
+
120
+ # Returns the name of the current stack to use. If the `test_stack_name` driver
121
+ # attribute is set, then it uses that one, otherwise it will be
122
+ # `<suite name>-<platform name>`
123
+ #
124
+ # @return [String] either the empty string or '--preserve-config'
125
+ def stack
126
+ return config_test_stack_name unless config_test_stack_name.empty?
127
+
128
+ "#{instance.suite.name}-#{instance.platform.name}"
129
+ end
130
+
131
+ # Returns the name of the secrets provider, if set, optionally as a Pulumi CLI flag
132
+ #
133
+ # @param flag [Boolean] specify true to prepend `--secrets-provider=`` to the name
134
+ # @return [String] value to use for the secrets provider
135
+ def secrets_provider(flag: false)
136
+ return '' if config_secrets_provider.empty?
137
+
138
+ return "--secrets-provider=\"#{config_secrets_provider}\"" if flag
139
+
140
+ config_secrets_provider
74
141
  end
75
142
 
143
+ # Logs in to the Pulumi backend set for the instance via `pulumi login`
144
+ #
145
+ # @return [void]
76
146
  def login
77
147
  backend = config_backend == 'local' ? '--local' : config_backend
78
148
  ::Kitchen::Pulumi::ShellOut.run(
@@ -81,18 +151,31 @@ module Kitchen
81
151
  )
82
152
  end
83
153
 
154
+ # Initializes a stack in the current directory unless another is provided
155
+ #
156
+ # @param stack [String] name of the stack to initialize
157
+ # @param dir [String] path to the directory to run Pulumi commands in
84
158
  def initialize_stack(stack, dir = '')
85
159
  ::Kitchen::Pulumi::ShellOut.run(
86
- cmd: "stack init #{stack} #{dir}",
160
+ cmd: "stack init #{stack} #{dir} #{secrets_provider(flag: true)}",
87
161
  logger: logger,
88
162
  )
89
163
  rescue ::Kitchen::Pulumi::Error => e
90
164
  puts 'Continuing...' if e.message.match?(/stack '#{stack}' already exists/)
91
165
  end
92
166
 
167
+ # Configures a stack in the current directory unless another is provided
168
+ #
169
+ # @param stack_confs [::Hash] hash specifying the stack config for the instance
170
+ # @param stack [String] name of the stack to configure
171
+ # @param conf_file [String] path to a stack config file to use for configuration
172
+ # @param dir [String] path to the directory to run Pulumi commands in
173
+ # @param is_secret [Boolean] specify true to set the given stack config as secrets
174
+ # @return [void]
93
175
  def configure(stack_confs, stack, conf_file, dir = '', is_secret: false)
94
176
  secret = is_secret ? '--secret' : ''
95
- base_cmd = "config set #{secret} -s #{stack} #{dir} #{conf_file}"
177
+ config_flag = config_file(conf_file, flag: true)
178
+ base_cmd = "config set #{secret} -s #{stack} #{dir} #{config_flag}"
96
179
 
97
180
  stack_confs.each do |namespace, stack_settings|
98
181
  stack_settings.each do |key, val|
@@ -104,59 +187,127 @@ module Kitchen
104
187
  end
105
188
  end
106
189
 
190
+ # Refreshes a stack's config on the specified config file
191
+ #
192
+ # @param stack [String] name of the stack being refreshed
193
+ # @param conf_file [String] path to a stack config file to use for configuration
194
+ # @param dir [String] path to the directory to run Pulumi commands in
195
+ # @return [void]
107
196
  def refresh_config(stack, conf_file, dir = '')
108
197
  ::Kitchen::Pulumi::ShellOut.run(
109
- cmd: "config refresh -s #{stack} #{dir} #{conf_file}",
198
+ cmd: "config refresh -s #{stack} #{dir} #{config_file(conf_file, flag: true)}",
110
199
  logger: logger,
111
200
  )
112
201
  rescue ::Kitchen::Pulumi::Error => e
113
202
  puts 'Continuing...' if e.message.match?(/no previous deployment/)
114
203
  end
115
204
 
116
- def config_file(conf_file = '')
205
+ # Get the value of the config file to use, if set on instance or provided as param,
206
+ # optionally as a command line flag `--config-file`
207
+ #
208
+ # @param conf_file [String] path to a stack config file to use for configuration
209
+ # @param flag [Boolean] specify true to prepend '--config-file ' to the config file
210
+ # @return [String] the path to the config file or its corresponding CLI flag
211
+ def config_file(conf_file = '', flag: false)
117
212
  file = conf_file.empty? ? config_config_file : conf_file
118
213
  return '' if File.directory?(file) || file.empty?
119
214
 
120
- "--config-file #{file}"
215
+ return "--config-file #{file}" if flag
216
+
217
+ file
121
218
  end
122
219
 
220
+ # Updates a stack via `pulumi up` according to instance attributes
221
+ #
222
+ # @param (see #refresh_config)
223
+ # @return [void]
123
224
  def update_stack(stack, conf_file, dir = '')
225
+ base_cmd = "up -y -r --show-config -s #{stack} #{dir}"
124
226
  ::Kitchen::Pulumi::ShellOut.run(
125
- cmd: "up -y -r --show-config -s #{stack} #{dir} #{conf_file}",
227
+ cmd: "#{base_cmd} #{config_file(conf_file, flag: true)}",
126
228
  logger: logger,
127
229
  )
128
230
  end
129
231
 
130
- def evolve_stack(stack, dir = '')
232
+ # Evolves a stack via successive calls to `pulumi config set` and `pulumi up`
233
+ # according to the `stack_evolution` instance attribute, if set. This permits
234
+ # testing stack config changes over time.
235
+ #
236
+ # @param (see #refresh_config)
237
+ # @param config_only [Boolean] specify true to prevent running `pulumi up`
238
+ # @return [void]
239
+ def evolve_stack(stack, conf_file, dir = '', config_only: false)
131
240
  config_stack_evolution.each do |evolution|
132
- conf_file = config_file(evolution.fetch(:config_file, ''))
241
+ new_conf_file = config_file(evolution.fetch(:config_file, ''))
133
242
  new_stack_confs = evolution.fetch(:config, {})
134
243
  new_stack_secrets = evolution.fetch(:secrets, {})
135
244
 
245
+ rewrite_config_file(new_conf_file, conf_file)
246
+
136
247
  configure(new_stack_confs, stack, conf_file, dir)
137
248
  configure(new_stack_secrets, stack, conf_file, dir, is_secret: true)
138
- update_stack(stack, conf_file, dir)
249
+ update_stack(stack, conf_file, dir) unless config_only
139
250
  end
140
251
  end
141
252
 
253
+ # Rewrites a temporary config file by merging the contents of the new config file
254
+ # into the old config file. This is used during stack evolution to ensure that
255
+ # stack config changes for each evolution step are implemented correctly if the
256
+ # user has provided a new config file to use for a step.
257
+ #
258
+ # @param new_conf_file [String] the path to the new config file to use
259
+ # @param old_conf_file [String] the path to the config file to overwrite
260
+ # @return [void]
261
+ def rewrite_config_file(new_conf_file, old_conf_file)
262
+ return if new_conf_file.empty?
263
+
264
+ old_conf = YAML.load_file(old_conf_file)
265
+ new_conf_file = File.join(config_directory, new_conf_file)
266
+ return unless File.exist?(new_conf_file)
267
+
268
+ new_conf = old_conf.deep_merge(YAML.load_file(new_conf_file))
269
+ File.write(old_conf_file, new_conf.to_yaml)
270
+ end
271
+
272
+ # Retrieves the fully resolved stack inputs based on the current configuration
273
+ # of the stack via `pulumi config`
274
+ #
275
+ # @param block [Block] block to run with stack inputs yielded to it
276
+ #
277
+ # for block {|stack_inputs| ... }
278
+ # @yield [stack_inputs] yields a hash of stack inputs
279
+ #
280
+ # @raise [Kitchen::ActionFailed] if an error occurs retrieving stack inputs
281
+ # @return [self]
142
282
  def stack_inputs(&block)
143
- ::Kitchen::Pulumi::Command::Input.run(
144
- directory: config_directory,
145
- stack: config_stack,
146
- conf_file: config_file,
147
- logger: logger,
148
- &block
149
- )
283
+ update({}, config_only: true) do |temp_conf_file|
284
+ ::Kitchen::Pulumi::Command::Input.run(
285
+ directory: config_directory,
286
+ stack: stack,
287
+ conf_file: config_file(temp_conf_file, flag: true),
288
+ logger: logger,
289
+ &block
290
+ )
291
+ end
150
292
 
151
293
  self
152
294
  rescue ::Kitchen::Pulumi::Error => e
153
295
  raise ::Kitchen::ActionFailed, e.message
154
296
  end
155
297
 
298
+ # Retrieves stack outputs via `pulumi stack output`
299
+ #
300
+ # @param block [Block] block to run with stack outputs yielded to it
301
+ #
302
+ # for block {|stack_outputs| ... }
303
+ # @yield [stack_outputs] yields a hash of stack outputs
304
+ #
305
+ # @raise [Kitchen::ActionFailed] if an error occurs retrieving stack outputs
306
+ # @return [self]
156
307
  def stack_outputs(&block)
157
308
  ::Kitchen::Pulumi::Command::Output.run(
158
309
  directory: config_directory,
159
- stack: config_stack,
310
+ stack: stack,
160
311
  logger: logger,
161
312
  &block
162
313
  )
@@ -165,6 +316,14 @@ module Kitchen
165
316
  rescue ::Kitchen::Pulumi::Error => e
166
317
  raise ::Kitchen::ActionFailed, e.message
167
318
  end
319
+
320
+ private
321
+
322
+ # @return [Logger] the common logger
323
+ # @api private
324
+ def logger
325
+ Kitchen.logger
326
+ end
168
327
  end
169
328
  end
170
329
  end
@@ -5,15 +5,25 @@ require 'kitchen/provisioner/base'
5
5
  require 'kitchen/pulumi/configurable'
6
6
 
7
7
  module Kitchen
8
+ # This namespace is defined by Kitchen.
9
+ #
10
+ # @see https://www.rubydoc.info/gems/test-kitchen/Kitchen/Provisioner
8
11
  module Provisioner
9
- # Driver class whose call method is invoked when users
10
- # run kitchen converge
12
+ # Provisioner class whose call method is invoked when users
13
+ # run `kitchen converge`
14
+ #
15
+ # @author Jacob Learned
11
16
  class Pulumi < ::Kitchen::Provisioner::Base
12
17
  kitchen_provisioner_api_version 2
13
18
 
14
19
  include ::Kitchen::Pulumi::Configurable
15
20
 
16
- # Runs stack updates via the instance driver
21
+ # Runs stack updates via the instance driver which shells out
22
+ # to `pulumi up`
23
+ #
24
+ # @param state [::Hash] the current kitchen state
25
+ # @raise [Kitchen::ActionFailed] if an error occurs during update
26
+ # @return void
17
27
  def call(state)
18
28
  instance.driver.update(state)
19
29
  rescue ::Kitchen::Pulumi::Error => e
@@ -1,8 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+
3
5
  # Global kitchen module
4
6
  module Kitchen
5
7
  # Namespace for Kitchen-Pulumi logic
8
+ # @author Jacob Learned
6
9
  module Pulumi
10
+ # Copies the contents of the given config file to a temporary file and yields the
11
+ # path of the temporary file to the block given
12
+ #
13
+ # @param config_file [String] the path to the config file to copy into the temp file
14
+ #
15
+ # for block {|temp_conf| ... }
16
+ # @yield [temp_conf] gives the path to the temporary config file
17
+ def self.with_temp_conf(config_file = '')
18
+ temp_conf = Tempfile.new(['kitchen-pulumi', '.yaml'])
19
+
20
+ if config_file.empty?
21
+ yield('') if block_given?
22
+ else
23
+ begin
24
+ IO.copy_stream(config_file, temp_conf.path)
25
+ yield(temp_conf.path) if block_given?
26
+ ensure
27
+ temp_conf.close
28
+ temp_conf.unlink
29
+ end
30
+ end
31
+ end
7
32
  end
8
33
  end
@@ -26,7 +26,7 @@ module Kitchen
26
26
  extend ConfigAttributeCacher
27
27
 
28
28
  def config_backend_default_value
29
- ''
29
+ 'https://api.pulumi.com'
30
30
  end
31
31
  end
32
32
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen/pulumi'
4
+ require 'kitchen/pulumi/config_schemas/boolean'
5
+ require 'kitchen/pulumi/config_attribute_cacher'
6
+ require 'kitchen/pulumi/config_attribute_definer'
7
+
8
+ module Kitchen
9
+ module Pulumi
10
+ module ConfigAttribute
11
+ # Attribute used to determine if the stack config should be
12
+ # preserved after stack is removed with `pulumi stack rm`
13
+ # during destroy.
14
+ module PreserveConfig
15
+ def self.included(plugin_class)
16
+ definer = ConfigAttributeDefiner.new(
17
+ attribute: self,
18
+ schema: ConfigSchemas::Boolean,
19
+ )
20
+ definer.define(plugin_class: plugin_class)
21
+ end
22
+
23
+ def self.to_sym
24
+ :preserve_config
25
+ end
26
+
27
+ extend ConfigAttributeCacher
28
+
29
+ def config_preserve_config_default_value
30
+ false
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -8,8 +8,8 @@ require 'kitchen/pulumi/config_attribute_definer'
8
8
  module Kitchen
9
9
  module Pulumi
10
10
  module ConfigAttribute
11
- # Attribute used to specify the stack to use for a specific instance
12
- module Stack
11
+ # Attribute used to specify the secrets provider a stack should use
12
+ module SecretsProvider
13
13
  def self.included(plugin_class)
14
14
  definer = ConfigAttributeDefiner.new(
15
15
  attribute: self,
@@ -19,12 +19,12 @@ module Kitchen
19
19
  end
20
20
 
21
21
  def self.to_sym
22
- :stack
22
+ :secrets_provider
23
23
  end
24
24
 
25
25
  extend ConfigAttributeCacher
26
26
 
27
- def config_stack_default_value
27
+ def config_secrets_provider_default_value
28
28
  ''
29
29
  end
30
30
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen/pulumi'
4
+ require 'kitchen/pulumi/config_schemas/string'
5
+ require 'kitchen/pulumi/config_attribute_cacher'
6
+ require 'kitchen/pulumi/config_attribute_definer'
7
+
8
+ module Kitchen
9
+ module Pulumi
10
+ module ConfigAttribute
11
+ # Attribute used to override the stack name to use for the stack
12
+ # created for an instance.
13
+ module TestStackName
14
+ def self.included(plugin_class)
15
+ definer = ConfigAttributeDefiner.new(
16
+ attribute: self,
17
+ schema: ConfigSchemas::String,
18
+ )
19
+ definer.define(plugin_class: plugin_class)
20
+ end
21
+
22
+ def self.to_sym
23
+ :test_stack_name
24
+ end
25
+
26
+ extend ConfigAttributeCacher
27
+
28
+ def config_test_stack_name_default_value
29
+ ''
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -3,7 +3,7 @@
3
3
  require 'dry/validation'
4
4
  require 'kitchen/pulumi/config_schemas'
5
5
 
6
- # rubocop:disable Metrics/LineLength
6
+ # rubocop:disable Layout/LineLength
7
7
  # rubocop:disable Metrics/BlockLength
8
8
  module Kitchen
9
9
  module Pulumi
@@ -574,4 +574,4 @@ module Kitchen
574
574
  end
575
575
  end
576
576
  # rubocop:enable Metrics/BlockLength
577
- # rubocop:enable Metrics/LineLength
577
+ # rubocop:enable Layout/LineLength
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ::Hash
4
+ def deep_merge(second)
5
+ merger = proc { |_key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
6
+ merge(second.to_h, &merger)
7
+ end
8
+ end
@@ -62,7 +62,7 @@ module Kitchen
62
62
  attr_accessor :system_attributes_to_options
63
63
 
64
64
  def initialize
65
- self.system_attributes_to_options = ::Kitchen::Pulumi::InSpecOptionsMapper::SYSTEM_ATTRIBUTES_TO_OPTIONS.dup # rubocop:disable Metrics/LineLength
65
+ self.system_attributes_to_options = ::Kitchen::Pulumi::InSpecOptionsMapper::SYSTEM_ATTRIBUTES_TO_OPTIONS.dup # rubocop:disable Layout/LineLength
66
66
  end
67
67
  end
68
68
  end
@@ -9,7 +9,7 @@ module Kitchen
9
9
  # Module orchestrating calls to the Pulumi CLI
10
10
  module ShellOut
11
11
  # Shells out to the Pulumi CLI
12
- def self.run(cmd:, duration: 7200, logger:, &block)
12
+ def self.run(cmd:, logger:, duration: 7200, &block)
13
13
  cmds = Array(cmd)
14
14
  block ||= ->(stdout) { stdout }
15
15
  shell_out(commands: cmds, duration: duration, logger: logger, &block)
@@ -20,7 +20,7 @@ module Kitchen
20
20
  raise(::Kitchen::Pulumi::Error, "Error: #{e.message}")
21
21
  end
22
22
 
23
- def self.shell_out(commands:, duration: 7200, logger:)
23
+ def self.shell_out(commands:, logger:, duration: 7200)
24
24
  commands.each do |command|
25
25
  shell_out = ::Mixlib::ShellOut.new(
26
26
  "pulumi #{command}",
@@ -38,19 +38,15 @@ module Kitchen
38
38
  # @param inputs [#to_hash] the config inputs provided to a stack
39
39
  # @param outputs [#to_hash] the outputs of the Pulumi stack under test.
40
40
  def initialize(inputs:, outputs:)
41
- @inputs = inputs.map do |key, value|
42
- [key, value.fetch('value', nil)]
43
- end.to_h
44
- @inputs.merge!(@inputs.map do |key, value|
45
- ["input_#{key}", value]
46
- end.to_h)
41
+ @inputs = inputs.transform_values do |value|
42
+ value.fetch('value', nil)
43
+ end
44
+ @inputs.merge!(@inputs.transform_keys { |key| "input_#{key}" })
47
45
 
48
46
  @outputs = Hash[outputs].map do |key, value|
49
47
  [key, value]
50
48
  end.to_h
51
- @outputs.merge!(@outputs.map do |key, value|
52
- ["output_#{key}", value]
53
- end.to_h)
49
+ @outputs.merge!(@outputs.transform_keys { |key| "output_#{key}" })
54
50
  rescue ::KeyError => e
55
51
  raise ::Kitchen::Pulumi::Error, "System attrs resolution failed\n#{e}"
56
52
  end
@@ -2,4 +2,4 @@
2
2
 
3
3
  require 'kitchen/pulumi'
4
4
 
5
- ::Kitchen::Pulumi::VERSION = '0.1.0.pre.beta'
5
+ ::Kitchen::Pulumi::VERSION = '0.1.0'
@@ -13,7 +13,7 @@ module Kitchen
13
13
  #
14
14
  # @see https://www.rubydoc.info/gems/test-kitchen/Kitchen/Verifier
15
15
  module Verifier
16
- # The verifier utilizes the {https://www.inspec.io/ InSpec infrastructure
16
+ # The verifier utilizes the {https://www.inspec.io/ InSpec} infrastructure
17
17
  # testing framework to verify the behaviour and
18
18
  # state of resources in the Pulumi state.
19
19
  #
@@ -63,7 +63,7 @@ module Kitchen
63
63
  # :reek:MissingSafeMethod {
64
64
  # exclude: [ finalize_config!, load_needed_dependencies! ]
65
65
  # }
66
- class Pulumi
66
+ class Pulumi < ::Kitchen::Verifier::Base
67
67
  include ::Kitchen::Configurable
68
68
  include ::Kitchen::Logging
69
69
  include ::Kitchen::Pulumi::ConfigAttribute::Color
@@ -75,7 +75,7 @@ module Kitchen
75
75
  attr_reader :inputs, :outputs
76
76
 
77
77
  def initialize(configuration = {})
78
- init_config configuration
78
+ super(configuration)
79
79
  self.inspec_options_mapper = ::Kitchen::Pulumi::InSpecOptionsMapper.new
80
80
  self.error_messages = []
81
81
  self.inputs = {}
@@ -100,7 +100,7 @@ module Kitchen
100
100
  raise ::Kitchen::ActionFailed, e.message
101
101
  end
102
102
 
103
- # doctor checks the system and configuration for common errors.
103
+ # Checks the system and configuration for common errors.
104
104
  #
105
105
  # @param _kitchen_state [::Hash] the mutable Kitchen instance state.
106
106
  # @return [Boolean] false
@@ -114,6 +114,10 @@ module Kitchen
114
114
  attr_accessor :inspec_options_mapper, :error_messages
115
115
  attr_writer :inputs, :outputs
116
116
 
117
+ # Raises an error immediately if the `fail_fast` config attribute is set on the
118
+ # or collects all errors until execution has ended verifier
119
+ #
120
+ # @return [void]
117
121
  def handle_error(message:)
118
122
  raise ::Kitchen::Pulumi::Error, message if config_fail_fast
119
123
 
@@ -121,6 +125,10 @@ module Kitchen
121
125
  error_messages.push message
122
126
  end
123
127
 
128
+ # Populates the `stack_inputs` and `stack_outputs` with the fully resolved stack
129
+ # inputs and outputs produced by the appropriate Pulumi commands
130
+ #
131
+ # @return [void]
124
132
  def load_variables
125
133
  instance.driver.stack_outputs do |outputs:|
126
134
  self.outputs.replace(outputs)
@@ -151,6 +159,10 @@ module Kitchen
151
159
  )
152
160
  end
153
161
 
162
+ # Runs verification logic of the given system
163
+ #
164
+ # @param system [::Hash] the system to verify
165
+ # @return [void]
154
166
  def verify(system:)
155
167
  ::Kitchen::Pulumi::System.new(
156
168
  mapping: {
@@ -167,6 +179,10 @@ module Kitchen
167
179
  handle_error message: e.message
168
180
  end
169
181
 
182
+ # Runs verification logic for each system defined on the verifier's `systems` config
183
+ # attribute
184
+ #
185
+ # @return [void]
170
186
  def verify_systems
171
187
  config_systems.each do |system|
172
188
  verify system: system
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-pulumi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.beta
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacob Learned
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-21 00:00:00.000000000 Z
11
+ date: 2021-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: guard
@@ -44,42 +44,42 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.11'
47
+ version: '0.13'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.11'
54
+ version: '0.13'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: pry-byebug
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '3.6'
61
+ version: '3.8'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '3.6'
68
+ version: '3.8'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '12.3'
75
+ version: '13.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '12.3'
82
+ version: '13.0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -128,14 +128,70 @@ dependencies:
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0.67'
131
+ version: '1.9'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.9'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-rake
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.5'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.5'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '2.1'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '2.1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.21'
132
174
  type: :development
133
175
  prerelease: false
134
176
  version_requirements: !ruby/object:Gem::Requirement
135
177
  requirements:
136
178
  - - "~>"
137
179
  - !ruby/object:Gem::Version
138
- version: '0.67'
180
+ version: '0.21'
181
+ - !ruby/object:Gem::Dependency
182
+ name: yard
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '0.9'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '0.9'
139
195
  - !ruby/object:Gem::Dependency
140
196
  name: logger
141
197
  requirement: !ruby/object:Gem::Requirement
@@ -182,44 +238,62 @@ dependencies:
182
238
  name: inspec
183
239
  requirement: !ruby/object:Gem::Requirement
184
240
  requirements:
185
- - - "~>"
241
+ - - ">="
186
242
  - !ruby/object:Gem::Version
187
- version: '3.0'
243
+ version: '3'
244
+ - - "<"
245
+ - !ruby/object:Gem::Version
246
+ version: '5'
188
247
  type: :runtime
189
248
  prerelease: false
190
249
  version_requirements: !ruby/object:Gem::Requirement
191
250
  requirements:
192
- - - "~>"
251
+ - - ">="
193
252
  - !ruby/object:Gem::Version
194
- version: '3.0'
253
+ version: '3'
254
+ - - "<"
255
+ - !ruby/object:Gem::Version
256
+ version: '5'
195
257
  - !ruby/object:Gem::Dependency
196
258
  name: json
197
259
  requirement: !ruby/object:Gem::Requirement
198
260
  requirements:
199
- - - "~>"
261
+ - - ">="
200
262
  - !ruby/object:Gem::Version
201
- version: 2.1.0
263
+ version: '2.1'
264
+ - - "<"
265
+ - !ruby/object:Gem::Version
266
+ version: '2.4'
202
267
  type: :runtime
203
268
  prerelease: false
204
269
  version_requirements: !ruby/object:Gem::Requirement
205
270
  requirements:
206
- - - "~>"
271
+ - - ">="
207
272
  - !ruby/object:Gem::Version
208
- version: 2.1.0
273
+ version: '2.1'
274
+ - - "<"
275
+ - !ruby/object:Gem::Version
276
+ version: '2.4'
209
277
  - !ruby/object:Gem::Dependency
210
278
  name: mixlib-shellout
211
279
  requirement: !ruby/object:Gem::Requirement
212
280
  requirements:
213
- - - "~>"
281
+ - - ">="
214
282
  - !ruby/object:Gem::Version
215
283
  version: '2.3'
284
+ - - "<"
285
+ - !ruby/object:Gem::Version
286
+ version: '4.0'
216
287
  type: :runtime
217
288
  prerelease: false
218
289
  version_requirements: !ruby/object:Gem::Requirement
219
290
  requirements:
220
- - - "~>"
291
+ - - ">="
221
292
  - !ruby/object:Gem::Version
222
293
  version: '2.3'
294
+ - - "<"
295
+ - !ruby/object:Gem::Version
296
+ version: '4.0'
223
297
  - !ruby/object:Gem::Dependency
224
298
  name: test-kitchen
225
299
  requirement: !ruby/object:Gem::Requirement
@@ -243,7 +317,7 @@ dependencies:
243
317
  description: 'Kitchen-Pulumi makes it easy to add integration tests to Pulumi-based
244
318
  infrastructure projects.
245
319
 
246
- '
320
+ '
247
321
  email:
248
322
  - jacobmlearned@gmail.com
249
323
  executables: []
@@ -266,11 +340,13 @@ files:
266
340
  - lib/kitchen/pulumi/config_attribute/directory.rb
267
341
  - lib/kitchen/pulumi/config_attribute/fail_fast.rb
268
342
  - lib/kitchen/pulumi/config_attribute/plugins.rb
343
+ - lib/kitchen/pulumi/config_attribute/preserve_config.rb
269
344
  - lib/kitchen/pulumi/config_attribute/refresh_config.rb
270
345
  - lib/kitchen/pulumi/config_attribute/secrets.rb
271
- - lib/kitchen/pulumi/config_attribute/stack.rb
346
+ - lib/kitchen/pulumi/config_attribute/secrets_provider.rb
272
347
  - lib/kitchen/pulumi/config_attribute/stack_evolution.rb
273
348
  - lib/kitchen/pulumi/config_attribute/systems.rb
349
+ - lib/kitchen/pulumi/config_attribute/test_stack_name.rb
274
350
  - lib/kitchen/pulumi/config_attribute_cacher.rb
275
351
  - lib/kitchen/pulumi/config_attribute_definer.rb
276
352
  - lib/kitchen/pulumi/config_schemas.rb
@@ -284,6 +360,7 @@ files:
284
360
  - lib/kitchen/pulumi/config_schemas/system.rb
285
361
  - lib/kitchen/pulumi/config_schemas/systems.rb
286
362
  - lib/kitchen/pulumi/configurable.rb
363
+ - lib/kitchen/pulumi/deep_merge.rb
287
364
  - lib/kitchen/pulumi/error.rb
288
365
  - lib/kitchen/pulumi/file_path_config_attribute_definer.rb
289
366
  - lib/kitchen/pulumi/inspec.rb
@@ -300,7 +377,8 @@ files:
300
377
  homepage: https://github.com/jacoblearned/kitchen-pulumi
301
378
  licenses:
302
379
  - MIT
303
- metadata: {}
380
+ metadata:
381
+ yard.run: yri
304
382
  post_install_message:
305
383
  rdoc_options: []
306
384
  require_paths:
@@ -309,14 +387,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
309
387
  requirements:
310
388
  - - ">="
311
389
  - !ruby/object:Gem::Version
312
- version: '0'
390
+ version: '2.6'
313
391
  required_rubygems_version: !ruby/object:Gem::Requirement
314
392
  requirements:
315
- - - ">"
393
+ - - ">="
316
394
  - !ruby/object:Gem::Version
317
- version: 1.3.1
395
+ version: '0'
318
396
  requirements: []
319
- rubygems_version: 3.0.1
397
+ rubygems_version: 3.1.4
320
398
  signing_key:
321
399
  specification_version: 4
322
400
  summary: Test-Kitchen plugins for Pulumi projects