kitchen-pulumi 0.1.0.pre.beta → 0.1.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.
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