kitchen-pulumi 0.1.0.pre.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +4 -0
  4. data/lib/kitchen/driver/pulumi.rb +170 -0
  5. data/lib/kitchen/provisioner/pulumi.rb +24 -0
  6. data/lib/kitchen/pulumi.rb +8 -0
  7. data/lib/kitchen/pulumi/command.rb +11 -0
  8. data/lib/kitchen/pulumi/command/input.rb +33 -0
  9. data/lib/kitchen/pulumi/command/output.rb +33 -0
  10. data/lib/kitchen/pulumi/config_attribute.rb +12 -0
  11. data/lib/kitchen/pulumi/config_attribute/backend.rb +34 -0
  12. data/lib/kitchen/pulumi/config_attribute/color.rb +34 -0
  13. data/lib/kitchen/pulumi/config_attribute/config.rb +35 -0
  14. data/lib/kitchen/pulumi/config_attribute/config_file.rb +34 -0
  15. data/lib/kitchen/pulumi/config_attribute/directory.rb +34 -0
  16. data/lib/kitchen/pulumi/config_attribute/fail_fast.rb +34 -0
  17. data/lib/kitchen/pulumi/config_attribute/plugins.rb +34 -0
  18. data/lib/kitchen/pulumi/config_attribute/refresh_config.rb +34 -0
  19. data/lib/kitchen/pulumi/config_attribute/secrets.rb +35 -0
  20. data/lib/kitchen/pulumi/config_attribute/stack.rb +33 -0
  21. data/lib/kitchen/pulumi/config_attribute/stack_evolution.rb +37 -0
  22. data/lib/kitchen/pulumi/config_attribute/systems.rb +33 -0
  23. data/lib/kitchen/pulumi/config_attribute_cacher.rb +28 -0
  24. data/lib/kitchen/pulumi/config_attribute_definer.rb +39 -0
  25. data/lib/kitchen/pulumi/config_schemas.rb +11 -0
  26. data/lib/kitchen/pulumi/config_schemas/array_of_hashes.rb +12 -0
  27. data/lib/kitchen/pulumi/config_schemas/boolean.rb +12 -0
  28. data/lib/kitchen/pulumi/config_schemas/config_evolution_array.rb +40 -0
  29. data/lib/kitchen/pulumi/config_schemas/error_messages.yml +21 -0
  30. data/lib/kitchen/pulumi/config_schemas/hash.rb +12 -0
  31. data/lib/kitchen/pulumi/config_schemas/stack_settings_hash.rb +20 -0
  32. data/lib/kitchen/pulumi/config_schemas/string.rb +12 -0
  33. data/lib/kitchen/pulumi/config_schemas/system.rb +577 -0
  34. data/lib/kitchen/pulumi/config_schemas/systems.rb +20 -0
  35. data/lib/kitchen/pulumi/configurable.rb +27 -0
  36. data/lib/kitchen/pulumi/error.rb +10 -0
  37. data/lib/kitchen/pulumi/file_path_config_attribute_definer.rb +25 -0
  38. data/lib/kitchen/pulumi/inspec.rb +66 -0
  39. data/lib/kitchen/pulumi/inspec_options_mapper.rb +69 -0
  40. data/lib/kitchen/pulumi/inspec_with_hosts.rb +40 -0
  41. data/lib/kitchen/pulumi/inspec_without_hosts.rb +34 -0
  42. data/lib/kitchen/pulumi/kitchen_instance.rb +12 -0
  43. data/lib/kitchen/pulumi/shell_out.rb +40 -0
  44. data/lib/kitchen/pulumi/system.rb +130 -0
  45. data/lib/kitchen/pulumi/system_attrs_resolver.rb +59 -0
  46. data/lib/kitchen/pulumi/system_hosts_resolver.rb +34 -0
  47. data/lib/kitchen/pulumi/version.rb +5 -0
  48. data/lib/kitchen/verifier/pulumi.rb +177 -0
  49. metadata +323 -0
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation'
4
+ require 'kitchen/pulumi/config_schemas'
5
+ require 'kitchen/pulumi/config_schemas/system'
6
+
7
+ module Kitchen
8
+ module Pulumi
9
+ module ConfigSchemas
10
+ # The value of the +systems+ key must be a sequence of systems.
11
+ #
12
+ # {include:Kitchen::Pulumi::ConfigSchemas::System}
13
+ Systems = ::Dry::Validation.Schema do
14
+ required(:value).each do
15
+ schema ::Kitchen::Pulumi::ConfigSchemas::System
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen'
4
+ require 'kitchen/pulumi'
5
+ require 'kitchen/pulumi/kitchen_instance'
6
+ require 'kitchen/pulumi/version'
7
+
8
+ module Kitchen
9
+ module Pulumi
10
+ # Module for plugins which are configurable via user-provided values
11
+ # in .kitchen.yaml
12
+ module Configurable
13
+ # Alternative implementation of Kitchen::Configurable#finalize_config!
14
+ # which validates the configuration before attempting to expand paths.
15
+ # See https://github.com/test-kitchen/test-kitchen/issues/1229
16
+ def finalize_config!(kitchen_instance)
17
+ kitchen_instance || raise(::Kitchen::ClientError,
18
+ "Instance must be provided to #{self}")
19
+ @instance = KitchenInstance.new(kitchen_instance)
20
+ validate_config!
21
+ expand_paths!
22
+ load_needed_dependencies!
23
+ self
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen'
4
+
5
+ module Kitchen
6
+ module Pulumi
7
+ class Error < ::StandardError
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen/pulumi'
4
+ require 'kitchen/pulumi/config_attribute_definer'
5
+
6
+ module Kitchen
7
+ module Pulumi
8
+ # Class for defining config attributes that are consumed as file paths
9
+ class FilePathConfigAttributeDefiner
10
+ def initialize(attribute:, schema:)
11
+ @attribute = attribute
12
+ @definer = ConfigAttributeDefiner.new(
13
+ attribute: attribute,
14
+ schema: schema,
15
+ )
16
+ end
17
+
18
+ # Defines the config attribute and then expands the file path
19
+ def define(plugin_class: plugin)
20
+ @definer.define(plugin_class: plugin_class)
21
+ plugin_class.expand_path_for(@attribute.to_sym)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'inspec'
4
+ require 'kitchen/pulumi/error'
5
+ require 'train'
6
+
7
+ module Kitchen
8
+ module Pulumi
9
+ # InSpec is the class of objects which act as interfaces to InSpec.
10
+ class InSpec
11
+ class << self
12
+ # .logger= sets the logger for all InSpec processes.
13
+ #
14
+ # The logdev of the logger is extended to conform to interface
15
+ # expected by InSpec.
16
+ #
17
+ # @param logger [::Kitchen::Logger] the logger to use.
18
+ # @return [void]
19
+ def logger=(logger)
20
+ logger.logdev.define_singleton_method :filename do
21
+ false
22
+ end
23
+
24
+ ::Inspec::Log.logger = logger
25
+ end
26
+ end
27
+
28
+ # #exec executes InSpec.
29
+ #
30
+ # @raise [::Kitchen::Pulumi::Error] if executing InSpec fails.
31
+ # @return [self]
32
+ def exec
33
+ @runner.run.tap do |exit_code|
34
+ if exit_code != 0
35
+ raise ::Kitchen::Pulumi::Error, "InSpec exited with #{exit_code}"
36
+ end
37
+ end
38
+
39
+ self
40
+ rescue ::ArgumentError, ::RuntimeError, ::Train::UserError => e
41
+ raise ::Kitchen::Pulumi::Error, "Executing InSpec failed\n#{e.message}"
42
+ end
43
+
44
+ # #info logs an information message using the InSpec logger.
45
+ #
46
+ # @param message [::String] the message to be logged.
47
+ # @return [self]
48
+ def info(message:)
49
+ ::Inspec::Log.info ::String.new message
50
+
51
+ self
52
+ end
53
+
54
+ private
55
+
56
+ def initialize(options:, profile_locations:)
57
+ @runner = ::Inspec::Runner.new options.merge(
58
+ logger: ::Inspec::Log.logger,
59
+ )
60
+ profile_locations.each do |profile_location|
61
+ @runner.add_target profile_location
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen/pulumi'
4
+
5
+ module Kitchen
6
+ module Pulumi
7
+ # Kitchen::Pulumi::InSpecOptionsMapper maps system configuration attributes
8
+ # to an InSpec options hash.
9
+ class InSpecOptionsMapper
10
+ SYSTEM_ATTRIBUTES_TO_OPTIONS = {
11
+ attrs: :input_file,
12
+ backend_cache: :backend_cache,
13
+ backend: :backend,
14
+ bastion_host: :bastion_host,
15
+ bastion_port: :bastion_port,
16
+ bastion_user: :bastion_user,
17
+ controls: :controls,
18
+ enable_password: :enable_password,
19
+ key_files: :key_files,
20
+ password: :password,
21
+ path: :path,
22
+ port: :port,
23
+ proxy_command: :proxy_command,
24
+ reporter: 'reporter',
25
+ self_signed: :self_signed,
26
+ shell_command: :shell_command,
27
+ shell_options: :shell_options,
28
+ shell: :shell,
29
+ show_progress: :show_progress,
30
+ ssl: :ssl,
31
+ sudo_command: :sudo_command,
32
+ sudo_options: :sudo_options,
33
+ sudo_password: :sudo_password,
34
+ sudo: :sudo,
35
+ user: :user,
36
+ vendor_cache: :vendor_cache,
37
+ }.freeze
38
+
39
+ # map populates an InSpec options hash based on the intersection between
40
+ # the system keys and the supported options
41
+ # keys, converting keys from symbols to strings as required by InSpec.
42
+ #
43
+ # @param options [::Hash] the InSpec options hash to be populated.
44
+ # @return [void]
45
+ def map(options:, system:)
46
+ supported = system.lazy.select do |attribute_name, _|
47
+ system_attributes_to_options.key?(attribute_name)
48
+ end
49
+
50
+ supported.each do |attribute_name, attribute_value|
51
+ options.store(
52
+ system_attributes_to_options.fetch(attribute_name),
53
+ attribute_value,
54
+ )
55
+ end
56
+
57
+ options
58
+ end
59
+
60
+ private
61
+
62
+ attr_accessor :system_attributes_to_options
63
+
64
+ def initialize
65
+ self.system_attributes_to_options = ::Kitchen::Pulumi::InSpecOptionsMapper::SYSTEM_ATTRIBUTES_TO_OPTIONS.dup # rubocop:disable Metrics/LineLength
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen'
4
+ require 'kitchen/pulumi/error'
5
+ require 'kitchen/pulumi/inspec'
6
+
7
+ module Kitchen
8
+ module Pulumi
9
+ # InSpec instances act as interfaces to the InSpec gem.
10
+ class InSpecWithHosts
11
+ # exec executes the InSpec controls of an InSpec profile.
12
+ #
13
+ # @raise [::Kitchen::Pulumi::Error] if the execution of the InSpec
14
+ # controls fails.
15
+ # @return [void]
16
+ def exec(system:)
17
+ system.each_host do |host:|
18
+ ::Kitchen::Pulumi::InSpec
19
+ .new(
20
+ options: options.merge(host: host),
21
+ profile_locations: profile_locations,
22
+ )
23
+ .info(message: "#{system}: Verifying host #{host}").exec
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :options, :profile_locations
30
+
31
+ # @param options [::Hash] options for execution.
32
+ # @param profile_locations [::Array<::String>] the locations of the
33
+ # InSpec profiles which contain the controls to be executed.
34
+ def initialize(options:, profile_locations:)
35
+ self.options = options
36
+ self.profile_locations = profile_locations
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen/pulumi/inspec'
4
+
5
+ module Kitchen
6
+ module Pulumi
7
+ # InSpec instances act as interfaces to the InSpec gem.
8
+ class InSpecWithoutHosts
9
+ # exec executes the InSpec controls of an InSpec profile.
10
+ #
11
+ # @raise [::Kitchen::Pulumi::Error] if the execution of the InSpec
12
+ # controls fails.
13
+ # @return [void]
14
+ def exec(system:)
15
+ ::Kitchen::Pulumi::InSpec
16
+ .new(options: options, profile_locations: profile_locations)
17
+ .info(message: "#{system}: Verifying")
18
+ .exec
19
+ end
20
+
21
+ private
22
+
23
+ attr_accessor :options, :profile_locations
24
+
25
+ # @param options [::Hash] options for execution.
26
+ # @param profile_locations [::Array<::String>] the locations of the
27
+ # InSpec profiles which contain the controls to be executed.
28
+ def initialize(options:, profile_locations:)
29
+ self.options = options
30
+ self.profile_locations = profile_locations
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+ require 'kitchen'
5
+ require 'kitchen/pulumi'
6
+
7
+ module Kitchen
8
+ module Pulumi
9
+ class KitchenInstance < DelegateClass ::Kitchen::Instance
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen/pulumi'
4
+ require 'kitchen/pulumi/error'
5
+ require 'mixlib/shellout'
6
+
7
+ module Kitchen
8
+ module Pulumi
9
+ # Module orchestrating calls to the Pulumi CLI
10
+ module ShellOut
11
+ # Shells out to the Pulumi CLI
12
+ def self.run(cmd:, duration: 7200, logger:, &block)
13
+ cmds = Array(cmd)
14
+ block ||= ->(stdout) { stdout }
15
+ shell_out(commands: cmds, duration: duration, logger: logger, &block)
16
+ rescue ::Errno::EACCES, ::Errno::ENOENT,
17
+ ::Mixlib::ShellOut::InvalidCommandOption,
18
+ ::Mixlib::ShellOut::CommandTimeout,
19
+ ::Mixlib::ShellOut::ShellCommandFailed => e
20
+ raise(::Kitchen::Pulumi::Error, "Error: #{e.message}")
21
+ end
22
+
23
+ def self.shell_out(commands:, duration: 7200, logger:)
24
+ commands.each do |command|
25
+ shell_out = ::Mixlib::ShellOut.new(
26
+ "pulumi #{command}",
27
+ live_stream: logger,
28
+ timeout: duration,
29
+ )
30
+
31
+ logger.warn("Running #{shell_out.command}")
32
+
33
+ shell_out.run_command
34
+ shell_out.error!
35
+ yield(stdout: shell_out.stdout)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kitchen/pulumi/error'
4
+ require 'kitchen/pulumi/inspec_with_hosts'
5
+ require 'kitchen/pulumi/inspec_without_hosts'
6
+ require 'kitchen/pulumi/system_attrs_resolver'
7
+ require 'kitchen/pulumi/system_hosts_resolver'
8
+
9
+ module Kitchen
10
+ module Pulumi
11
+ # System is the class of objects which are verified by the Pulumi Verifier.
12
+ class System
13
+ # #add_attrs adds attributes to the system.
14
+ #
15
+ # @param attrs [#to_hash] the attributes to be added.
16
+ # @return [self]
17
+ def add_attrs(attrs:)
18
+ @attributes = @attributes.merge Hash attrs
19
+
20
+ self
21
+ end
22
+
23
+ # #add_hosts adds hosts to the system.
24
+ #
25
+ # @param hosts [#to_arr,#to_a] the hosts to be added.
26
+ # @return [self]
27
+ def add_hosts(hosts:)
28
+ @hosts += Array hosts
29
+
30
+ self
31
+ end
32
+
33
+ # #each_host enumerates each host of the system.
34
+ #
35
+ # @yieldparam host [::String] the next host.
36
+ # @return [self]
37
+ def each_host
38
+ @hosts.each do |host|
39
+ yield host: host
40
+ end
41
+
42
+ self
43
+ end
44
+
45
+ # #to_s returns a string representation of the system.
46
+ #
47
+ # @return [::String] the name of the system.
48
+ def to_s
49
+ @mapping.fetch :name
50
+ end
51
+
52
+ # #verify verifies the system by executing InSpec.
53
+ #
54
+ # @param inputs [::Hash] the Pulumi input values to be utilized as
55
+ # InSpec profile attributes.
56
+ # @param inspec_options [::Hash] the options to be passed to InSpec.
57
+ # @param outputs [::Hash] the Pulumi output values to be utilized as
58
+ # InSpec profile attributes.
59
+ # @return [self]
60
+ def verify(inputs:, inspec_options:, outputs:)
61
+ resolve inputs: inputs, outputs: outputs
62
+ execute_inspec options: inspec_options
63
+
64
+ self
65
+ rescue StandardError => e
66
+ raise ::Kitchen::Pulumi::Error, "#{self}: #{e.message}"
67
+ end
68
+
69
+ private
70
+
71
+ def execute_inspec(options:)
72
+ inspec.new(
73
+ options: options_with_attributes(options: options),
74
+ profile_locations: @mapping.fetch(:profile_locations),
75
+ ).exec(system: self)
76
+ end
77
+
78
+ def initialize(mapping:)
79
+ @attributes = {}
80
+ @attrs_outputs = mapping.fetch :attrs_outputs do
81
+ {}
82
+ end
83
+ @hosts = mapping.fetch :hosts do
84
+ []
85
+ end
86
+ @mapping = mapping
87
+ end
88
+
89
+ def inspec
90
+ if @hosts.empty?
91
+ ::Kitchen::Pulumi::InSpecWithoutHosts
92
+ else
93
+ ::Kitchen::Pulumi::InSpecWithHosts
94
+ end
95
+ end
96
+
97
+ def options_with_attributes(options:)
98
+ options.merge attributes: @attributes
99
+ end
100
+
101
+ def resolve(inputs:, outputs:)
102
+ resolve_attrs inputs: inputs, outputs: outputs
103
+ resolve_hosts outputs: outputs
104
+ end
105
+
106
+ def resolve_attrs(inputs:, outputs:)
107
+ ::Kitchen::Pulumi::SystemAttrsResolver.new(
108
+ inputs: inputs, outputs: outputs,
109
+ ).resolve(
110
+ attrs_outputs_keys: @attrs_outputs.keys,
111
+ attrs_outputs_values: @attrs_outputs.values,
112
+ system: self,
113
+ )
114
+
115
+ self
116
+ end
117
+
118
+ def resolve_hosts(outputs:)
119
+ return self unless @mapping.key? :hosts_output
120
+
121
+ ::Kitchen::Pulumi::SystemHostsResolver.new(outputs: outputs).resolve(
122
+ hosts_output: @mapping.fetch(:hosts_output),
123
+ system: self,
124
+ )
125
+
126
+ self
127
+ end
128
+ end
129
+ end
130
+ end