cem_acpt 0.2.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +38 -0
  3. data/Gemfile +4 -3
  4. data/Gemfile.lock +85 -56
  5. data/README.md +144 -83
  6. data/cem_acpt.gemspec +8 -7
  7. data/exe/cem_acpt +41 -7
  8. data/lib/cem_acpt/config.rb +345 -0
  9. data/lib/cem_acpt/core_extensions.rb +17 -61
  10. data/lib/cem_acpt/goss/api/action_response.rb +175 -0
  11. data/lib/cem_acpt/goss/api.rb +83 -0
  12. data/lib/cem_acpt/goss.rb +8 -0
  13. data/lib/cem_acpt/image_name_builder.rb +0 -9
  14. data/lib/cem_acpt/logging/formatter.rb +97 -0
  15. data/lib/cem_acpt/logging.rb +168 -142
  16. data/lib/cem_acpt/platform/base.rb +26 -37
  17. data/lib/cem_acpt/platform/gcp.rb +48 -62
  18. data/lib/cem_acpt/platform.rb +30 -28
  19. data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
  20. data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
  21. data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
  22. data/lib/cem_acpt/provision/terraform.rb +193 -0
  23. data/lib/cem_acpt/provision.rb +20 -0
  24. data/lib/cem_acpt/puppet_helpers.rb +0 -1
  25. data/lib/cem_acpt/test_data.rb +23 -13
  26. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
  27. data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
  28. data/lib/cem_acpt/test_runner.rb +170 -3
  29. data/lib/cem_acpt/utils/puppet.rb +29 -0
  30. data/lib/cem_acpt/utils/ssh.rb +197 -0
  31. data/lib/cem_acpt/utils/terminal.rb +27 -0
  32. data/lib/cem_acpt/utils.rb +4 -138
  33. data/lib/cem_acpt/version.rb +1 -1
  34. data/lib/cem_acpt.rb +70 -20
  35. data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
  36. data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
  37. data/lib/terraform/gcp/linux/main.tf +191 -0
  38. data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
  39. data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
  40. data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
  41. data/lib/terraform/gcp/windows/.keep +0 -0
  42. data/sample_config.yaml +22 -21
  43. metadata +88 -56
  44. data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
  45. data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
  46. data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
  47. data/lib/cem_acpt/bootstrap.rb +0 -12
  48. data/lib/cem_acpt/context.rb +0 -153
  49. data/lib/cem_acpt/platform/base/cmd.rb +0 -71
  50. data/lib/cem_acpt/platform/gcp/cmd.rb +0 -345
  51. data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
  52. data/lib/cem_acpt/platform/vmpooler.rb +0 -24
  53. data/lib/cem_acpt/rspec_utils.rb +0 -242
  54. data/lib/cem_acpt/shared_objects.rb +0 -537
  55. data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
  56. data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
  57. data/lib/cem_acpt/test_runner/runner.rb +0 -210
  58. data/lib/cem_acpt/test_runner/runner_result.rb +0 -103
@@ -1,153 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'platform'
4
- require_relative 'utils'
5
- require_relative 'shared_objects'
6
- require_relative 'test_data'
7
- require_relative 'test_runner/run_handler'
8
- require_relative 'logging'
9
-
10
- module CemAcpt
11
- # Context provides the context in which the RunHandler creates and starts Runners.
12
- module Context
13
- class ContextError < StandardError; end
14
-
15
- class << self
16
- include CemAcpt::Logging
17
-
18
- KEY_PATH = File.join([ENV['HOME'], '.ssh', 'acpt_test_key']).freeze
19
- KH_PATH = File.join([ENV['HOME'], '.ssh', 'acpt_test_known_hosts']).freeze
20
-
21
- def log(msg, level = :info)
22
- real_msg = "CONTEXT: #{msg}"
23
- logger.send(level, real_msg)
24
- end
25
-
26
- # Builds the Puppet module package
27
- # @param opts [Hash] config opts
28
- # @return [String] The path to the Puppet module package
29
- def build_module_package(opts = {})
30
- module_dir = opts[:module_dir] || __dir__
31
- pkg_path = CemAcpt::Utils::Puppet.build_module_package(module_dir)
32
- log("Module package built at #{pkg_path}")
33
- pkg_path
34
- end
35
-
36
- # Creates a SSH key and a SSH known hosts file for the acceptance test suite
37
- def new_test_ssh_key
38
- log('Creating ephemeral SSH key and known hosts file for acceptance test suites...')
39
- @ssh_priv_key, @ssh_pub_key = CemAcpt::Utils::SSH.ephemeral_ssh_key
40
- @ssh_known_hosts = CemAcpt::Utils::SSH.acpt_known_hosts
41
- CemAcpt::Utils::SSH.set_ssh_file_permissions(@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts)
42
- log('Successfully created SSH files...')
43
- log("SSH private key: #{@ssh_priv_key}", :debug)
44
- log("SSH public key: #{@ssh_pub_key}", :debug)
45
- log("SSH known hosts: #{@ssh_known_hosts}", :debug)
46
- end
47
-
48
- # Deletes acceptance test suite SSH files
49
- def clean_test_ssh_key
50
- log('Deleting ephemeral ssh keys and acpt_known_hosts if they exist...')
51
- [@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts].map { |f| File.delete(f) if File.exist?(f) }
52
- end
53
-
54
- # Prints a period to the terminal every 5 seconds in a single line to keep the terminal
55
- # alive. This is used when running in CI mode. Does nothing unless the option `:CI` is
56
- # `true`, or the environment variables CI or GITHUB_ACTION are set to a truthy value.
57
- # @param opts [Hash] config opts
58
- def keep_terminal_alive(opts = {})
59
- @keep_terminal_alive = if opts[:CI] || ENV['CI'] || ENV['GITHUB_ACTION']
60
- CemAcpt::Utils::Terminal.keep_terminal_alive
61
- end
62
- end
63
-
64
- def clean_up_test_suite(opts)
65
- @ctx.node_inventory.clear!
66
- @ctx.node_inventory.clean_local_files
67
- clean_test_ssh_key unless opts[:no_ephemeral_ssh_key]
68
- @run_handler&.destroy_test_nodes
69
- @keep_terminal_alive&.kill
70
- end
71
- end
72
-
73
- # Creates a context (CemAcpt::Context::Ctx) object for the RunHandler to create and start Runners.
74
- # Provides the following objects for the Runners: a config object,
75
- # the test data hash, the node inventory, and the local port allocator.
76
- # Additionally, it creates the platform-specific node objects for each
77
- # test suite in the test data. It then calls the provided block with
78
- # the context objects nodes, config, test_data, and node_inventory.
79
- # @param config_opts [Hash] the config options
80
- def self.with(**opts)
81
- @opts = opts
82
- @start_time = Time.now
83
- raise CemAcpt::Context::ContextError, 'CemAcpt::Context.with requires a block' unless block_given?
84
-
85
- config_file = @opts[:config_file] || File.expand_path('./cem_acpt_config.yaml')
86
- logger.info("Running acceptance test suite at #{@start_time}")
87
- logger.debug("Config opts: #{@opts}")
88
- logger.debug("Config file: #{config_file}")
89
- logger.info("Using module directory: #{@opts[:module_dir]}")
90
- keep_terminal_alive(@opts)
91
- Dir.chdir(opts[:module_dir]) do
92
- new_test_ssh_key unless @opts[:no_ephemeral_ssh_key]
93
- pkg_path = build_module_package(@opts)
94
- @ctx = CemAcpt::Context::Ctx.new(opts: @opts, config_file: config_file, module_package_path: pkg_path)
95
- logger.debug("Created Ctx object #{@ctx}")
96
- @run_handler = CemAcpt::TestRunner::RunHandler.new(@ctx)
97
- logger.debug("Created RunHandler object #{@run_handler}")
98
- yield @run_handler
99
- end
100
- @exit_code = @run_handler.exit_code
101
- rescue StandardError => e
102
- logger.fatal("Acceptance test suite encountered an error: #{e.message}")
103
- logger.fatal(e.backtrace.join("\n"))
104
- @exit_code = 1
105
- ensure
106
- clean_up_test_suite(@opts)
107
- total_minutes = ((Time.now - @start_time) / 60).round
108
- logger.info("Test suite finished in ~#{total_minutes} minutes")
109
- @exit_code || 1
110
- end
111
-
112
- # Ctx holds the context objects for the RunHandler to create and start Runners.
113
- class Ctx
114
- attr_reader :config, :test_data, :module_package_path, :node_inventory, :local_port_allocator
115
-
116
- def initialize(opts: nil, config_file: nil, module_package_path: nil)
117
- @config = CemAcpt::Config.new.load(opts: opts, config_file: config_file)
118
- @test_data = CemAcpt::TestData.acceptance_test_data(@config)
119
- @module_package_path = module_package_path
120
- @node_inventory = CemAcpt::NodeInventory.new
121
- @local_port_allocator = CemAcpt::LocalPortAllocator.new
122
- prep_environment
123
- end
124
-
125
- def nodes
126
- return @nodes if defined?(@nodes)
127
-
128
- raise CemAcpt::Error, 'No platform(s) specified' unless @config.has?('platform') || @config.has?('platforms')
129
-
130
- @nodes = nodes_from_platforms
131
- @nodes
132
- end
133
-
134
- private
135
-
136
- def prep_environment
137
- @node_inventory.clean_local_files
138
- end
139
-
140
- def nodes_from_platforms
141
- nodes = {}
142
- if @config.has?('platform')
143
- nodes[@config.get('platform')] = CemAcpt::Platform.use(@config.get('platform'), @config, @test_data, @local_port_allocator)
144
- elsif @config.has?('platforms')
145
- config.get('platforms').each do |platform|
146
- nodes[platform] = CemAcpt::Platform.use(platform, @config, @test_data, @local_port_allocator)
147
- end
148
- end
149
- nodes
150
- end
151
- end
152
- end
153
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CemAcpt::Platform
4
- require_relative File.join(__dir__, '..', '..', 'logging.rb')
5
-
6
- class CmdError < StandardError; end
7
-
8
- # Base class for command providers. Provides an API for subclasses to implement.
9
- class CmdBase
10
- include CemAcpt::Logging
11
-
12
- attr_reader :env
13
-
14
- def initialize(*_args, env: {}, **_kwargs)
15
- @env = env
16
- end
17
-
18
- def local_exec(*_args, **_kwargs)
19
- raise NotImplementedError, '#local_exec must be implemented by a subclass'
20
- end
21
-
22
- def ssh(_instance_name, _command, _ignore_command_errors: false, _opts: {})
23
- raise NotImplementedError, '#ssh must be implemented by a subclass'
24
- end
25
-
26
- def scp_upload(_instance_name, _local, _remote, _scp_opts: {}, _opts: {})
27
- raise NotImplementedError, '#scp_upload must be implemented by a subclass'
28
- end
29
-
30
- def scp_download(_instance_name, _local, _remote, _scp_opts: {}, _opts: {})
31
- raise NotImplementedError, '#scp_download must be implemented by a subclass'
32
- end
33
-
34
- def ssh_ready?(_instance_name, _timeout = 300, _opts: {})
35
- raise NotImplementedError, '#ssh_ready? must be implemented by a subclass'
36
- end
37
-
38
- def apply_manifest(_instance_name, _manifest, _opts: {})
39
- raise NotImplementedError, '#create_manifest_on_node must be implemented by a subclass'
40
- end
41
-
42
- def run_shell(_instance_name, _command, _opts: {})
43
- raise NotImplementedError, '#run_shell must be implemented by a subclass'
44
- end
45
-
46
- def trim_output(output)
47
- if output.include?("\n")
48
- output.split("\n").map(&:strip).reject(&:empty?).join("\n")
49
- else
50
- output.strip
51
- end
52
- end
53
-
54
- def with_timed_retry(timeout = 300)
55
- return unless block_given?
56
-
57
- last_error = nil
58
- start_time = Time.now
59
- while Time.now - start_time < timeout
60
- begin
61
- output = yield
62
- return output
63
- rescue StandardError => e
64
- last_error = e
65
- sleep(10)
66
- end
67
- end
68
- raise last_error
69
- end
70
- end
71
- end
@@ -1,345 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CemAcpt::Platform::Gcp
4
- require 'json'
5
- require 'open3'
6
- require_relative File.join(__dir__, '..', 'base', 'cmd.rb')
7
-
8
- # This class provides methods to run gcloud commands. It allows for default values to be
9
- # set for the project, zone, and user and can also find these values from the local config.
10
- # Additionally, this class provides a way to run SSH commands against GCP VMs using IAP.
11
- class Cmd < CemAcpt::Platform::CmdBase
12
- def initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil, ssh_key: nil)
13
- super(env: { 'CLOUDSDK_PYTHON_SITEPACKAGES' => '1' })
14
- require 'net/ssh'
15
- require 'net/ssh/proxy/command'
16
-
17
- @project = project unless project.nil?
18
- @zone = zone unless zone.nil?
19
- @default_out_format = out_format
20
- @default_filter = filter
21
- @user_name = user_name
22
- @local_port = local_port
23
- @ssh_key = ssh_key
24
- raise CemAcpt::Platform::CmdError, 'gcloud command not available' unless gcloud?
25
- raise CemAcpt::Platform::CmdError, 'gcloud is not authenticated' unless authenticated?
26
- end
27
-
28
- # Returns either the project name passed in during object initialization
29
- # or the project name from the local config.
30
- def project
31
- @project ||= project_from_config&.chomp
32
- end
33
-
34
- # Returns either the zone name passed in during object initialization
35
- # or the zone name from the local config.
36
- def zone
37
- @zone ||= zone_from_config&.chomp
38
- end
39
-
40
- # Returns either the user name passed in during object initialization
41
- # or queries the current active, authenticated user from gcloud and returns the name.
42
- def user_name
43
- @user_name ||= authenticated_user_name
44
- end
45
-
46
- # Returns the format string passed in during object initialization or
47
- # the default format string.
48
- def format(out_format = nil)
49
- out_format&.chomp || @default_out_format
50
- end
51
-
52
- # Returns the filter string passed in during object initialization or
53
- # the default filter string.
54
- def filter(out_filter = nil)
55
- out_filter&.chomp || @default_filter
56
- end
57
-
58
- def local_port
59
- @local_port ||= rand(49_512..65_535)
60
- end
61
-
62
- def ssh_key
63
- return @ssh_key unless @ssh_key.nil?
64
-
65
- if File.exist?(File.join([ENV['HOME'], '.ssh', 'acpt_test_key']))
66
- @ssh_key = File.join([ENV['HOME'], '.ssh', 'acpt_test_key'])
67
- else
68
- logger.debug("Test SSH key not found at #{File.join([ENV['HOME'], '.ssh', 'acpt_test_key'])}, using default")
69
- @ssh_key = File.join([ENV['HOME'], '.ssh', 'google_compute_engine'])
70
- end
71
- @ssh_key
72
- end
73
-
74
- # Returns a formatted hash of ssh options to be used with Net::SSH.start.
75
- # If you pass in a GCP VM instance name, this method will configure the
76
- # IAP tunnel ProxyCommand to use. If you pass in an opts hash, it will
77
- # merge the options with the default options.
78
- def ssh_opts(instance_name: nil, use_proxy_command: true, opts: {})
79
- base_opts = default_ssh_opts
80
- if use_proxy_command
81
- base_opts[:proxy] = proxy_command(instance_name, port: base_opts[:port])
82
- base_opts[:host_key_alias] = vm_alias(instance_name)
83
- end
84
- base_opts.merge(opts).reject { |_, v| v.nil? }
85
- end
86
-
87
- # Runs `gcloud` commands locally.
88
- def local_exec(command, out_format: 'json', out_filter: nil, project_flag: true)
89
- final_command = format_gcloud_command(command, out_format: out_format, out_filter: out_filter, project_flag: project_flag)
90
- stdout, stderr, status = Open3.capture3(env, final_command)
91
- raise "gcloud command '#{final_command}' failed: #{stderr}" unless status.success?
92
-
93
- if format(out_format) == 'json'
94
- begin
95
- ::JSON.parse(stdout)
96
- rescue ::JSON::ParserError
97
- stdout.chomp
98
- end
99
- else
100
- stdout.chomp
101
- end
102
- end
103
-
104
- # Stops a GCP VM instance.
105
- def stop_instance(instance_name)
106
- local_exec("compute instances stop #{instance_name}")
107
- end
108
-
109
- # Deletes a GCP VM instance.
110
- def delete_instance(instance_name)
111
- local_exec("compute instances delete #{instance_name} --quiet")
112
- rescue StandardError
113
- # Ignore errors when deleting instances.
114
- end
115
-
116
- # Returns a Hash describing a GCP VM instance.
117
- def vm_describe(instance_name, out_format: 'json', out_filter: nil)
118
- local_exec("compute instances describe #{instance_name}", out_format: out_format, out_filter: out_filter)
119
- end
120
-
121
- # This function runs the specified command as the currently authenticated user
122
- # on the given CGP VM via SSH. Using `ssh` does not invoke the gcloud command
123
- # and is dependent on the system's SSH configuration.
124
- def ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {})
125
- Net::SSH.start(instance_name, user_name,
126
- ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |ssh|
127
- logger.debug("Running SSH command '#{cmd}' on instance '#{instance_name}'")
128
- result = ssh.exec!(cmd)
129
- if result.exitstatus != 0 && !ignore_command_errors
130
- abridged_cmd = cmd.length > 100 ? "#{cmd[0..100]}..." : cmd
131
- raise "Failed to run SSH command \"#{abridged_cmd}\" on #{instance_name}: #{result}"
132
- end
133
- result
134
- end
135
- end
136
-
137
- # Uploads a file to the specified VM.
138
- def scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {})
139
- raise "Local file #{local} does not exist" unless File.exist?(local)
140
-
141
- cmd = [
142
- 'compute',
143
- 'scp',
144
- ]
145
- cmd << '--strict-host-key-checking=no'
146
- cmd << "--tunnel-through-iap" if use_proxy_command
147
- cmd << "--recurse" if scp_opts[:recurse]
148
- cmd << "#{local} #{instance_name}:#{remote}"
149
- local_exec(cmd.join(' '), out_format: 'json')
150
- end
151
-
152
- # Downloads a file from the specified VM.
153
- def scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {})
154
- raise "Local file #{local} does not exist" unless File.exist?(local)
155
-
156
- cmd = [
157
- 'compute',
158
- 'scp',
159
- ]
160
- cmd << '--strict-host-key-checking=no'
161
- cmd << '--tunnel-through-iap' if use_proxy_command
162
- cmd << '--recurse' if scp_opts[:recurse]
163
- cmd << "#{instance_name}:#{remote} #{local}"
164
- local_exec(cmd.join(' '), out_format: 'json')
165
- end
166
-
167
- def ssh_ready?(instance_name, opts: {})
168
- ssh_options = ssh_opts(instance_name: instance_name, opts: opts)
169
- logger.debug("Testing SSH connection to #{instance_name} with options #{ssh_options}")
170
- gcloud_ssh(instance_name, 'echo "SSH is ready"')
171
- logger.debug('Removing ecdsa & ed25519 host keys from config due to bug in jruby-openssl')
172
- gcloud_ssh(instance_name, 'sudo sed -E -i "s;HostKey /etc/ssh/ssh_host_(ecdsa|ed25519)_key;;g" /etc/ssh/sshd_config')
173
- logger.debug('Restarting SSH service')
174
- gcloud_ssh(instance_name, 'sudo systemctl restart sshd', ignore_command_errors: true)
175
- logger.info("SSH connection to #{instance_name} is ready")
176
- true
177
- rescue StandardError => e
178
- logger.debug("SSH connection to #{instance_name} failed: #{e}")
179
- false
180
- end
181
-
182
- # This function spawns a background thread to run a GCP IAP tunnel, run the given
183
- # code block, then kill the thread. The code block will be yielded ssh_opts that
184
- # are used to configure SSH connections over the IAP tunnel. The IAP tunnel is
185
- # run in it's own thread and will not block the main thread. The IAP tunnel is
186
- # killed when the code block exits. All SSH connections made in the code block
187
- # must be made to the host '127.0.0.1' using the yielded ssh_opts and not the VM name.
188
- # @param instance_name [String] The name of the GCP VM instance to connect to.
189
- # @param instance_port [Integer] The port to connect to on the GCP VM instance.
190
- # @param disable_connection_check [Boolean] If true, the connection check will be disabled.
191
- def with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false, &block)
192
- cmd = [
193
- 'compute start-iap-tunnel',
194
- "#{instance_name} #{instance_port}",
195
- "--local-host-port=localhost:#{local_port}",
196
- ]
197
- cmd << '--disable-connection-check' if disable_connection_check
198
- tunnel_ssh_opts = {
199
- proxy: nil,
200
- port: local_port,
201
- forward_agent: false,
202
- }
203
- final_cmd = format_gcloud_command(cmd.join(' '), out_format: nil, project_flag: false)
204
- tunnel_pid = Process.spawn(env, final_cmd)
205
- begin
206
- block.call(ssh_opts(use_proxy_command: false, opts: tunnel_ssh_opts))
207
- ensure
208
- Process.kill('TERM', tunnel_pid)
209
- end
210
- end
211
-
212
- def project_from_config
213
- local_exec('config get-value project', out_format: nil, project_flag: false)
214
- end
215
-
216
- def zone_from_config
217
- local_exec('config get-value compute/zone', out_format: nil, project_flag: false)
218
- end
219
-
220
- def run_shell(instance_name, cmd, opts = {})
221
- ssh_opts = opts.key?(:ssh_opts) ? opts[:ssh_opts] : {}
222
- command = [
223
- 'sudo -n -u root -i',
224
- cmd,
225
- ]
226
- ssh(
227
- instance_name,
228
- command.join(' '),
229
- ignore_command_errors: true,
230
- opts: ssh_opts,
231
- )
232
- end
233
-
234
- def apply_manifest(instance_name, manifest, opts = {})
235
- unless opts[:apply][:no_upload]
236
- with_temp_manifest_file(manifest) do |tf|
237
- upload_temp_manifest(instance_name, tf.path, remote_path: '/tmp/acpt_manifest.pp', opts: opts)
238
- end
239
- end
240
- apply_cmd = [opts[:puppet_path], 'apply', '/tmp/acpt_manifest.pp']
241
- apply_cmd << '--trace' if opts[:apply][:trace]
242
- apply_cmd << "--hiera_config=#{opts[:apply][:hiera_config]}" if opts[:apply][:hiera_config]
243
- apply_cmd << '--debug' if opts[:apply][:debug]
244
- apply_cmd << '--noop' if opts[:apply][:noop]
245
- apply_cmd << '--detailed-exitcodes' if opts[:apply][:detailed_exitcodes]
246
-
247
- run_shell(
248
- instance_name,
249
- apply_cmd.join(' '),
250
- opts
251
- )
252
- end
253
-
254
- private
255
-
256
- def format_gcloud_command(command, out_format: 'json', out_filter: nil, project_flag: true)
257
- cmd_parts = ['gcloud', command]
258
- cmd_parts << "--project=#{project.chomp}" unless !project_flag || project.nil?
259
- cmd_parts << "--format=#{format(out_format)}" if format(out_format)
260
- cmd_parts << "--filter=\"#{filter(out_filter)}\"" if filter(out_filter)
261
- cmd_parts.join(' ')
262
- end
263
-
264
- def with_temp_manifest_file(manifest, file_name: 'acpt_manifest')
265
- require 'tempfile'
266
- tf = Tempfile.new(file_name)
267
- tf.write(manifest)
268
- tf.close
269
- begin
270
- yield tf
271
- ensure
272
- tf.unlink
273
- end
274
- end
275
-
276
- def upload_temp_manifest(instance_name, local_path, remote_path: '/tmp/acpt_manifest.pp', opts: {})
277
- scp_upload(instance_name, local_path, remote_path, opts: opts[:ssh_opts]) unless opts[:apply][:no_upload]
278
- end
279
-
280
- def gcloud_ssh(instance_name, command, ignore_command_errors: false)
281
- local_exec("compute ssh --ssh-key-file #{ssh_key} --tunnel-through-iap #{instance_name} --command='#{command}'")
282
- rescue StandardError => e
283
- raise e unless ignore_command_errors
284
- end
285
-
286
- def vm_alias(vm)
287
- "compute.#{vm_describe(vm)['id']}"
288
- end
289
-
290
- # Default options for Net::SSH
291
- def default_ssh_opts
292
- {
293
- auth_methods: ['publickey'],
294
- check_host_ip: false,
295
- compression: true,
296
- config: false,
297
- keys: [ssh_key],
298
- keys_only: true,
299
- kex: ['diffie-hellman-group-exchange-sha256'], # ecdh algos cause jruby to shit the bed
300
- non_interactive: true,
301
- port: 22,
302
- user: user_name,
303
- user_known_hosts_file: File.join(ENV['HOME'], '.ssh', 'acpt_test_known_hosts'),
304
- verify_host_key: :never,
305
- }
306
- end
307
-
308
- # Returns a Proxy::Command object to use for the SSH option ProxyCommand.
309
- # This works in the same way that `gcloud compute ssh --tunnel-through-iap` does.
310
- def proxy_command(instance_name, port: 22, quiet: true, verbosity: 'debug')
311
- proxy_command = [
312
- 'gcloud compute start-iap-tunnel',
313
- "#{instance_name} #{port}",
314
- '--listen-on-stdin',
315
- "--project=#{project}",
316
- "--zone=#{zone}",
317
- ]
318
- #proxy_command << '--no-user-output-enabled --quiet' if quiet
319
- proxy_command << "--verbosity=#{verbosity}" unless quiet || verbosity.nil? || verbosity.empty?
320
- Net::SSH::Proxy::Command.new(proxy_command.join(' '))
321
- end
322
-
323
- # This function checks to ensure that the gcloud tool is available on the system.
324
- def gcloud?
325
- ver = local_exec('--version', out_format: nil, project_flag: false)
326
- !ver.nil? && !ver.empty?
327
- rescue RuntimeError, Errno::ENOENT
328
- false
329
- end
330
-
331
- # This function checks to ensure that a user is authenticated with gcloud.
332
- def authenticated?
333
- local_exec('auth list', project_flag: false).any? { |a| a['status'] == 'ACTIVE' }
334
- rescue RuntimeError, Errno::ENOENT
335
- false
336
- end
337
-
338
- # This function returns the currently authenticated user's name sanitized for use in SSH
339
- def authenticated_user_name
340
- local_exec('compute os-login describe-profile', project_flag: false)['posixAccounts'][0]['username']
341
- rescue StandardError => e
342
- raise "failed to find authenticated user name from os-login profile: #{e.message}"
343
- end
344
- end
345
- end