chef-apply 0.1.17 → 0.1.18

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -7
  3. data/Gemfile.lock +176 -84
  4. data/chef-apply.gemspec +2 -2
  5. data/lib/chef_apply/version.rb +1 -1
  6. data/spec/fixtures/custom_config.toml +2 -0
  7. data/spec/integration/chef-run_spec.rb +41 -0
  8. data/spec/integration/fixtures/chef_help.out +69 -0
  9. data/spec/integration/fixtures/chef_version.out +1 -0
  10. data/spec/integration/spec_helper.rb +55 -0
  11. data/spec/spec_helper.rb +114 -0
  12. data/spec/support/matchers/output_to_terminal.rb +36 -0
  13. data/spec/unit/action/base_spec.rb +89 -0
  14. data/spec/unit/action/converge_target_spec.rb +292 -0
  15. data/spec/unit/action/generate_local_policy_spec.rb +114 -0
  16. data/spec/unit/action/generate_temp_cookbook_spec.rb +75 -0
  17. data/spec/unit/action/install_chef/base_spec.rb +234 -0
  18. data/spec/unit/action/install_chef_spec.rb +69 -0
  19. data/spec/unit/cli/options_spec.rb +75 -0
  20. data/spec/unit/cli/validation_spec.rb +78 -0
  21. data/spec/unit/cli_spec.rb +440 -0
  22. data/spec/unit/config_spec.rb +70 -0
  23. data/spec/unit/errors/ccr_failure_mapper_spec.rb +103 -0
  24. data/spec/unit/file_fetcher_spec.rb +40 -0
  25. data/spec/unit/fixtures/multi-error.out +2 -0
  26. data/spec/unit/log_spec.rb +37 -0
  27. data/spec/unit/recipe_lookup_spec.rb +122 -0
  28. data/spec/unit/startup_spec.rb +283 -0
  29. data/spec/unit/target_host_spec.rb +231 -0
  30. data/spec/unit/target_resolver_spec.rb +380 -0
  31. data/spec/unit/telemeter/sender_spec.rb +140 -0
  32. data/spec/unit/telemeter_spec.rb +191 -0
  33. data/spec/unit/temp_cookbook_spec.rb +199 -0
  34. data/spec/unit/ui/error_printer_spec.rb +173 -0
  35. data/spec/unit/ui/terminal_spec.rb +109 -0
  36. data/spec/unit/version_spec.rb +31 -0
  37. data/warning.txt +3 -0
  38. metadata +34 -2
@@ -0,0 +1,69 @@
1
+ Chef Run is a tool to execute ad-hoc tasks using Chef.
2
+
3
+ chef-run <TARGET[S]> <RESOURCE> <RESOURCE_NAME> [PROPERTIES] [FLAGS]
4
+
5
+ Runs a single <RESOURCE> on the specified <TARGET[S]>.
6
+ [PROPERTIES] should be specified as key=value.
7
+
8
+ For example:
9
+
10
+ chef-run web01 service nginx action=restart
11
+ chef-run web01,web02 service nginx action=restart
12
+ chef-run web0[1:2] service nginx action=restart
13
+
14
+ chef-run <TARGET[S]> <RECIPE> [FLAGS]
15
+
16
+ Runs a single recipe located at <RECIPE> on the specified <TARGET[S]>.
17
+
18
+ For example:
19
+
20
+ chef-run web01 path/to/cookbook/recipe.rb
21
+ chef-run web01,web02 path/to/cookbook
22
+ chef-run web0[1:2] cookbook_name
23
+ chef-run web01 cookbook_name::recipe_name
24
+
25
+ ARGUMENTS:
26
+ <TARGET[S]> The hosts or IPs to target. Can also be an SSH or WinRM URLs
27
+ in the form:
28
+
29
+ ssh://[USERNAME]@example.com[:PORT]
30
+ <RESOURCE> A Chef resource, such as 'user' or 'package'
31
+ <RESOURCE_NAME> The name, usually used to specify what 'thing' to set up with
32
+ the resource. For example, given resource 'user', 'name' would be
33
+ the name of the user you wanted to create.
34
+ <RECIPE> The recipe to converge. This can be provided as one of:
35
+ 1. Full path to a recipe file
36
+ 2. Cookbook name. First we check the working directory for this
37
+ cookbook, then we check in the chef repository path. If a
38
+ cookbook is found we run the default recipe.
39
+ 3. This behaves similarly to 'cookbook name' above, but it also allows
40
+ you to specify which recipe to use from the cookbook.
41
+
42
+ FLAGS:
43
+ -c, --config PATH Location of config file. Default: $HOME/.chef-workstation/config.toml
44
+ --cookbook-repo-paths PATH Comma separated list of cookbook repository paths.
45
+ -h, --help Show help and usage for `chef-run`
46
+ -i, --identity-file PATH SSH identity file to use when connecting. Keys loaded into ssh-agent will also be used.
47
+ --[no-]install Install Chef client on the target host(s) if it is not installed.
48
+ This defaults to enabled - the installation will be performed
49
+ if there is no Chef client on the target(s).
50
+ --password <PASSWORD> Password to use for authentication to the target(s). The same
51
+ password will be used for all targets.
52
+ -p, --protocol <PROTOCOL> The protocol to use for connecting to targets.
53
+ The default is 'ssh', and it can be changed in config.toml by
54
+ setting 'connection.default_protocol' to a supported option.
55
+ --[no-]ssl Use SSL for WinRM. Current default: false
56
+ --[no-]ssl-verify Verify peer certificate when using SSL for WinRM
57
+ Use --ssl-no-verify when using SSL for WinRM and
58
+ the remote host is using a self-signed certificate.
59
+ Current default: true
60
+ --[no-]sudo Whether to use root permissions on the target. Default: true
61
+ --sudo-command <COMMAND> Command to use for administrative/root access. Defaults to 'sudo'.
62
+ --sudo-options 'OPTIONS...' Options to use with the sudo command. If there are multiple flags,
63
+ quote them. For example: --sudo-options '-H -P -s'
64
+ --sudo-password <PASSWORD> Password to use with the sudo command. This must be provided if
65
+ password is required for sudo on the target(s). The same sudo password
66
+ will be used for all targets.
67
+ --user <USER> Username to use for authentication to the target(s). The same
68
+ username will be used for all targets.
69
+ -v, --version Show the current version of Chef Run.
@@ -0,0 +1 @@
1
+ chef-run: $VERSION
@@ -0,0 +1,55 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "chef_apply/startup"
19
+ require "chef_apply/version"
20
+
21
+ # Create the chef configuration directory and touch the config
22
+ # file.
23
+ # this makes sure our output doesn't include
24
+ # an extra line telling us that it's created,
25
+ # causing the first integration test to execute to fail on
26
+ # CI.
27
+ # TODO this is not ideal... let's look at
28
+ # testing the output correctly in both cases,
29
+ # possible forcing a specific test that will also create
30
+ # the directory to run first.
31
+ dir = File.join(Dir.home, ".chef-workstation")
32
+ conf = File.join(dir, "config.toml")
33
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
34
+ FileUtils.touch(conf) unless File.exist?(conf)
35
+
36
+ # Simple wrapper that runs the CLI and prevents it
37
+ # from aborting all tests with a SystemExit.
38
+ # We could shell out, but this will run a little faster as we
39
+ # accumulate more things to test - and will work better to get
40
+ # accurate simplecov coverage reporting.
41
+ # usage:
42
+ # expect {run_with_cli("blah")}.to output("blah").to_stdout
43
+ def run_cli_with(args)
44
+ ChefApply::Startup.new(args.split(" ")).run
45
+ rescue SystemExit
46
+ end
47
+
48
+ def fixture_content(name)
49
+ content = File.read(File.join("spec/integration/fixtures", "#{name}.out"))
50
+ # Replace $VERSION if present - this is updated automatically, so we can't include
51
+ # the literal version value in the fixture without
52
+ # having expeditor update it there too...
53
+ content.gsub("$VERSION", ChefApply::VERSION).
54
+ gsub("$HOME", Dir.home)
55
+ end
@@ -0,0 +1,114 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "bundler/setup"
19
+ require "simplecov"
20
+ require "rspec/expectations"
21
+ require "support/matchers/output_to_terminal"
22
+
23
+ RemoteExecResult = Struct.new(:exit_status, :stdout, :stderr)
24
+
25
+ class ChefApply::MockReporter
26
+ def update(msg); ChefApply::UI::Terminal.output msg; end
27
+
28
+ def success(msg); ChefApply::UI::Terminal.output "SUCCESS: #{msg}"; end
29
+
30
+ def error(msg); ChefApply::UI::Terminal.output "FAILURE: #{msg}"; end
31
+ end
32
+
33
+ RSpec::Matchers.define :exit_with_code do |expected_code|
34
+ actual_code = nil
35
+ match do |block|
36
+ begin
37
+ block.call
38
+ rescue SystemExit => e
39
+ actual_code = e.status
40
+ end
41
+ actual_code && actual_code == expected_code
42
+ end
43
+
44
+ failure_message do |block|
45
+ result = actual.nil? ? " did not call exit" : " called exit(#{actual_code})"
46
+ "expected exit(#{expected_code}) but it #{result}."
47
+ end
48
+
49
+ failure_message_when_negated do |block|
50
+ "expected exit(#{expected_code}) but it did."
51
+ end
52
+
53
+ description do
54
+ "expect exit(#{expected_code})"
55
+ end
56
+
57
+ supports_block_expectations do
58
+ true
59
+ end
60
+ end
61
+ # TODO would read better to make this a custom matcher.
62
+ # Simulates a recursive string lookup on the Text object
63
+ #
64
+ # assert_string_lookup("tree.tree.tree.leaf", "a returned string")
65
+ # TODO this can be more cleanly expressed as a custom matcher...
66
+ def assert_string_lookup(key, retval = "testvalue")
67
+ it "should look up string #{key}" do
68
+ top_level_method, *call_seq = key.split(".")
69
+ terminal_method = call_seq.pop
70
+ tmock = double()
71
+ # Because ordering is important
72
+ # (eg calling errors.hello is different from hello.errors),
73
+ # we need to add this individually instead of using
74
+ # `receive_messages`, which doesn't appear to give a way to
75
+ # guarantee ordering
76
+ expect(ChefApply::Text).to receive(top_level_method).
77
+ and_return(tmock)
78
+ call_seq.each do |m|
79
+ expect(tmock).to receive(m).ordered.and_return(tmock)
80
+ end
81
+ expect(tmock).to receive(terminal_method).
82
+ ordered.and_return(retval)
83
+ subject.call
84
+ end
85
+ end
86
+
87
+ RSpec.configure do |config|
88
+ # Enable flags like --only-failures and --next-failure
89
+ config.example_status_persistence_file_path = ".rspec_status"
90
+ config.run_all_when_everything_filtered = true
91
+ config.filter_run :focus
92
+
93
+ # Disable RSpec exposing methods globally on `Module` and `main`
94
+ config.disable_monkey_patching!
95
+
96
+ config.expect_with :rspec do |c|
97
+ c.syntax = :expect
98
+ end
99
+
100
+ config.mock_with :rspec do |mocks|
101
+ mocks.verify_partial_doubles = true
102
+ end
103
+
104
+ config.before(:all) do
105
+ ChefApply::Log.setup "/dev/null", :error
106
+ ChefApply::UI::Terminal.init(File.open("/dev/null", "w"))
107
+ end
108
+ end
109
+
110
+ if ENV["CIRCLE_ARTIFACTS"]
111
+ dir = File.join(ENV["CIRCLE_ARTIFACTS"], "coverage")
112
+ SimpleCov.coverage_dir(dir)
113
+ end
114
+ SimpleCov.start
@@ -0,0 +1,36 @@
1
+ require "rspec/matchers/built_in/output"
2
+ require "chef_apply/ui/terminal"
3
+
4
+ # Custom behavior for the builtin output matcher
5
+ # to allow it to handle to_terminal, which integrates
6
+ # with our UI::Terminal interface.
7
+ module RSpec
8
+ module Matchers
9
+ module BuiltIn
10
+ class Output < BaseMatcher
11
+ # @api private
12
+ # Provides the implementation for `output`.
13
+ # Not intended to be instantiated directly.
14
+ def to_terminal
15
+ @stream_capturer = CaptureTerminal
16
+ self
17
+ end
18
+ module CaptureTerminal
19
+ def self.name
20
+ "terminal"
21
+ end
22
+
23
+ def self.capture(block)
24
+ captured_stream = StringIO.new
25
+ original_stream = ::ChefApply::UI::Terminal.location
26
+ ::ChefApply::UI::Terminal.location = captured_stream
27
+ block.call
28
+ captured_stream.string
29
+ ensure
30
+ ::ChefApply::UI::Terminal.location = original_stream
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "spec_helper"
19
+ require "chef_apply/action/base"
20
+ require "chef_apply/telemeter"
21
+ require "chef_apply/target_host"
22
+
23
+ RSpec.describe ChefApply::Action::Base do
24
+ let(:family) { "windows" }
25
+ let(:target_host) do
26
+ p = double("platform", family: family)
27
+ instance_double(ChefApply::TargetHost, platform: p)
28
+ end
29
+ let(:opts) do
30
+ { target_host: target_host,
31
+ other: "something-else" } end
32
+ subject(:action) { ChefApply::Action::Base.new(opts) }
33
+
34
+ context "#initialize" do
35
+ it "properly initializes exposed attr readers" do
36
+ expect(action.target_host).to eq target_host
37
+ expect(action.config).to eq({ other: "something-else" })
38
+ end
39
+ end
40
+
41
+ context "#run" do
42
+ it "runs the underlying action, capturing timing via telemetry" do
43
+ expect(ChefApply::Telemeter).to receive(:timed_action_capture).with(subject).and_yield
44
+ expect(action).to receive(:perform_action)
45
+ action.run
46
+ end
47
+
48
+ it "invokes an action handler when actions occur and a handler is provided" do
49
+ @run_action = nil
50
+ @args = nil
51
+ expect(ChefApply::Telemeter).to receive(:timed_action_capture).with(subject).and_yield
52
+ expect(action).to receive(:perform_action) { action.notify(:test_success, "some arg", "some other arg") }
53
+ action.run { |action, args| @run_action = action; @args = args }
54
+ expect(@run_action).to eq :test_success
55
+ expect(@args).to eq ["some arg", "some other arg"]
56
+ end
57
+ end
58
+
59
+ shared_examples "check path fetching" do
60
+ [:chef_client, :cache_path, :read_chef_report, :delete_chef_report, :tempdir, :mktemp, :delete_folder].each do |path|
61
+ it "correctly returns path #{path}" do
62
+ expect(action.send(path)).to be_a(String)
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "when connecting to a windows target" do
68
+ include_examples "check path fetching"
69
+
70
+ it "correctly returns chef run string" do
71
+ expect(action.run_chef("a", "b", "c")).to eq(
72
+ "Set-Location -Path a; " \
73
+ "chef-client -z --config b --recipe-url c | Out-Null; " \
74
+ "Set-Location C:/; " \
75
+ "exit $LASTEXITCODE"
76
+ )
77
+ end
78
+ end
79
+
80
+ describe "when connecting to a non-windows target" do
81
+ let(:family) { "linux" }
82
+ include_examples "check path fetching"
83
+
84
+ it "correctly returns chef run string" do
85
+ expect(action.run_chef("a", "b", "c")).to eq("bash -c 'cd a; chef-client -z --config a/b --recipe-url a/c'")
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,292 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "spec_helper"
19
+ require "chef_apply/action/converge_target"
20
+ require "chef_apply/target_host"
21
+ require "chef_apply/errors/ccr_failure_mapper"
22
+ require "chef_apply/temp_cookbook"
23
+
24
+ RSpec.describe ChefApply::Action::ConvergeTarget do
25
+ let(:archive) { "archive.tgz" }
26
+ let(:target_host) do
27
+ p = double("platform", family: "windows")
28
+ instance_double(ChefApply::TargetHost, platform: p)
29
+ end
30
+ let(:local_policy_path) { "/local/policy/path/archive.tgz" }
31
+ let(:opts) { { target_host: target_host, local_policy_path: local_policy_path } }
32
+ subject(:action) { ChefApply::Action::ConvergeTarget.new(opts) }
33
+
34
+ describe "#create_remote_policy" do
35
+ let(:remote_folder) { "/tmp/foo" }
36
+ let(:remote_archive) { File.join(remote_folder, File.basename(archive)) }
37
+
38
+ before do
39
+ end
40
+
41
+ it "pushes it to the remote machine" do
42
+ expect(target_host).to receive(:upload_file).with(local_policy_path, remote_archive)
43
+ expect(subject.create_remote_policy(local_policy_path, remote_folder)).to eq(remote_archive)
44
+ end
45
+
46
+ it "raises an error if the upload fails" do
47
+ expect(target_host).to receive(:upload_file).with(local_policy_path, remote_archive).and_raise("foo")
48
+ err = ChefApply::Action::ConvergeTarget::PolicyUploadFailed
49
+ expect { subject.create_remote_policy(local_policy_path, remote_folder) }.to raise_error(err)
50
+ end
51
+ end
52
+
53
+ describe "#create_remote_config" do
54
+
55
+ @closed = false # tempfile close indicator
56
+ let(:remote_folder) { "/tmp/foo" }
57
+ let(:remote_config) { "#{remote_folder}/workstation.rb" }
58
+ # TODO - mock this, I think we're leaving things behind in /tmp in test runs.
59
+ let!(:local_tempfile) { Tempfile.new }
60
+
61
+ it "pushes it to the remote machine" do
62
+ expect(Tempfile).to receive(:new).and_return(local_tempfile)
63
+ expect(target_host).to receive(:upload_file).with(local_tempfile.path, remote_config)
64
+ expect(subject.create_remote_config(remote_folder)).to eq(remote_config)
65
+ # ensure the tempfile is deleted locally
66
+ expect(local_tempfile.closed?).to eq(true)
67
+ end
68
+
69
+ it "raises an error if the upload fails" do
70
+ expect(Tempfile).to receive(:new).and_return(local_tempfile)
71
+ expect(target_host).to receive(:upload_file).with(local_tempfile.path, remote_config).and_raise("foo")
72
+ err = ChefApply::Action::ConvergeTarget::ConfigUploadFailed
73
+ expect { subject.create_remote_config(remote_folder) }.to raise_error(err)
74
+ # ensure the tempfile is deleted locally
75
+ expect(local_tempfile.closed?).to eq(true)
76
+ end
77
+
78
+ describe "when data_collector is set in config" do
79
+ before do
80
+ ChefApply::Config.data_collector.url = "dc.url"
81
+ ChefApply::Config.data_collector.token = "dc.token"
82
+ end
83
+
84
+ after do
85
+ ChefApply::Config.reset
86
+ end
87
+
88
+ it "creates a config file with data collector config values" do
89
+ expect(Tempfile).to receive(:new).and_return(local_tempfile)
90
+ expect(local_tempfile).to receive(:write).with(<<~EOM
91
+ local_mode true
92
+ color false
93
+ cache_path "\#{ENV['APPDATA']}/chef-workstation"
94
+ chef_repo_path "\#{ENV['APPDATA']}/chef-workstation"
95
+ require_relative "reporter"
96
+ reporter = ChefApply::Reporter.new
97
+ report_handlers << reporter
98
+ exception_handlers << reporter
99
+ data_collector.server_url "dc.url"
100
+ data_collector.token "dc.token"
101
+ data_collector.mode :solo
102
+ data_collector.organization "Chef Workstation"
103
+ EOM
104
+ )
105
+ expect(target_host).to receive(:upload_file).with(local_tempfile.path, remote_config)
106
+ expect(subject.create_remote_config(remote_folder)).to eq(remote_config)
107
+ # ensure the tempfile is deleted locally
108
+ expect(local_tempfile.closed?).to eq(true)
109
+ end
110
+ end
111
+
112
+ describe "when data_collector is not set" do
113
+ before do
114
+ ChefApply::Config.data_collector.url = nil
115
+ ChefApply::Config.data_collector.token = nil
116
+ end
117
+
118
+ it "creates a config file without data collector config values" do
119
+ expect(Tempfile).to receive(:new).and_return(local_tempfile)
120
+ expect(local_tempfile).to receive(:write).with(<<~EOM
121
+ local_mode true
122
+ color false
123
+ cache_path "\#{ENV['APPDATA']}/chef-workstation"
124
+ chef_repo_path "\#{ENV['APPDATA']}/chef-workstation"
125
+ require_relative "reporter"
126
+ reporter = ChefApply::Reporter.new
127
+ report_handlers << reporter
128
+ exception_handlers << reporter
129
+ EOM
130
+ )
131
+ expect(target_host).to receive(:upload_file).with(local_tempfile.path, remote_config)
132
+ expect(subject.create_remote_config(remote_folder)).to eq(remote_config)
133
+ # ensure the tempfile is deleted locally
134
+ expect(local_tempfile.closed?).to eq(true)
135
+ end
136
+ end
137
+ end
138
+
139
+ describe "#create_remote_handler" do
140
+ let(:remote_folder) { "/tmp/foo" }
141
+ let(:remote_reporter) { "#{remote_folder}/reporter.rb" }
142
+ let!(:local_tempfile) { Tempfile.new }
143
+
144
+ it "pushes it to the remote machine" do
145
+ expect(Tempfile).to receive(:new).and_return(local_tempfile)
146
+ expect(target_host).to receive(:upload_file).with(local_tempfile.path, remote_reporter)
147
+ expect(subject.create_remote_handler(remote_folder)).to eq(remote_reporter)
148
+ # ensure the tempfile is deleted locally
149
+ expect(local_tempfile.closed?).to eq(true)
150
+ end
151
+
152
+ it "raises an error if the upload fails" do
153
+ expect(Tempfile).to receive(:new).and_return(local_tempfile)
154
+ expect(target_host).to receive(:upload_file).with(local_tempfile.path, remote_reporter).and_raise("foo")
155
+ err = ChefApply::Action::ConvergeTarget::HandlerUploadFailed
156
+ expect { subject.create_remote_handler(remote_folder) }.to raise_error(err)
157
+ # ensure the tempfile is deleted locally
158
+ expect(local_tempfile.closed?).to eq(true)
159
+ end
160
+ end
161
+
162
+ describe "#upload_trusted_certs" do
163
+ let(:remote_folder) { "/tmp/foo" }
164
+ let(:remote_tcd) { File.join(remote_folder, "trusted_certs") }
165
+ let(:tmpdir) { Dir.mktmpdir }
166
+ let(:certs_dir) { File.join(tmpdir, "weird/glob/chars[/") }
167
+
168
+ before do
169
+ ChefApply::Config.chef.trusted_certs_dir = certs_dir
170
+ FileUtils.mkdir_p(certs_dir)
171
+ end
172
+
173
+ after do
174
+ ChefApply::Config.reset
175
+ FileUtils.remove_entry tmpdir
176
+ end
177
+
178
+ context "when there are local certificates" do
179
+ let!(:cert1) { FileUtils.touch(File.join(certs_dir, "1.crt"))[0] }
180
+ let!(:cert2) { FileUtils.touch(File.join(certs_dir, "2.pem"))[0] }
181
+
182
+ it "uploads the local certs" do
183
+ expect(target_host).to receive(:run_command).with("#{subject.mkdir} #{remote_tcd}", true)
184
+ expect(target_host).to receive(:upload_file).with(cert1, File.join(remote_tcd, File.basename(cert1)))
185
+ expect(target_host).to receive(:upload_file).with(cert2, File.join(remote_tcd, File.basename(cert2)))
186
+ subject.upload_trusted_certs(remote_folder)
187
+ end
188
+ end
189
+
190
+ context "when there are no local certificates" do
191
+ it "does not upload any certs" do
192
+ expect(target_host).to_not receive(:run_command)
193
+ expect(target_host).to_not receive(:upload_file)
194
+ subject.upload_trusted_certs(remote_folder)
195
+ end
196
+ end
197
+
198
+ end
199
+
200
+ describe "#perform_action" do
201
+ let(:remote_folder) { "/tmp/foo" }
202
+ let(:remote_archive) { File.join(remote_folder, File.basename(archive)) }
203
+ let(:remote_config) { "#{remote_folder}/workstation.rb" }
204
+ let(:remote_handler) { "#{remote_folder}/reporter.rb" }
205
+ let(:tmpdir) { double("tmpdir", exit_status: 0, stdout: remote_folder) }
206
+ before do
207
+ expect(target_host).to receive(:run_command!).with(subject.mktemp, true).and_return(tmpdir)
208
+ end
209
+ let(:result) { double("command result", exit_status: 0, stdout: "") }
210
+
211
+ it "runs the converge and reports back success" do
212
+ expect(action).to receive(:create_remote_policy).with(local_policy_path, remote_folder).and_return(remote_archive)
213
+ expect(action).to receive(:create_remote_config).with(remote_folder).and_return(remote_config)
214
+ expect(action).to receive(:create_remote_handler).with(remote_folder).and_return(remote_handler)
215
+ expect(action).to receive(:upload_trusted_certs).with(remote_folder)
216
+ expect(target_host).to receive(:run_command).with(/chef-client.+#{archive}/).and_return(result)
217
+ expect(target_host).to receive(:run_command!)
218
+ .with("#{subject.delete_folder} #{remote_folder}")
219
+ .and_return(result)
220
+ [:running_chef, :success].each do |n|
221
+ expect(action).to receive(:notify).with(n)
222
+ end
223
+ subject.perform_action
224
+ end
225
+
226
+ context "when chef schedules restart" do
227
+ let(:result) { double("command result", exit_status: 35) }
228
+
229
+ it "runs the converge and reports back reboot" do
230
+ expect(action).to receive(:create_remote_policy).with(local_policy_path, remote_folder).and_return(remote_archive)
231
+ expect(action).to receive(:create_remote_config).with(remote_folder).and_return(remote_config)
232
+ expect(action).to receive(:create_remote_handler).with(remote_folder).and_return(remote_handler)
233
+ expect(action).to receive(:upload_trusted_certs).with(remote_folder)
234
+ expect(target_host).to receive(:run_command).with(/chef-client.+#{archive}/).and_return(result)
235
+ expect(target_host).to receive(:run_command!)
236
+ .with("#{subject.delete_folder} #{remote_folder}")
237
+ .and_return(result)
238
+ [:running_chef, :reboot].each do |n|
239
+ expect(action).to receive(:notify).with(n)
240
+ end
241
+ subject.perform_action
242
+ end
243
+ end
244
+
245
+ context "when command fails" do
246
+ let(:result) { double("command result", exit_status: 1, stdout: "", stderr: "") }
247
+ let(:report_result) { double("report result", exit_status: 0, stdout: '{ "exception": "thing" }') }
248
+ let(:exception_mapper) { double("mapper") }
249
+ before do
250
+ expect(ChefApply::Errors::CCRFailureMapper).to receive(:new).
251
+ and_return exception_mapper
252
+ end
253
+
254
+ it "reports back failure and reads the remote report" do
255
+ expect(action).to receive(:create_remote_policy).with(local_policy_path, remote_folder).and_return(remote_archive)
256
+ expect(action).to receive(:create_remote_config).with(remote_folder).and_return(remote_config)
257
+ expect(action).to receive(:create_remote_handler).with(remote_folder).and_return(remote_handler)
258
+ expect(action).to receive(:upload_trusted_certs).with(remote_folder)
259
+ expect(target_host).to receive(:run_command).with(/chef-client.+#{archive}/).and_return(result)
260
+ expect(target_host).to receive(:run_command!)
261
+ .with("#{subject.delete_folder} #{remote_folder}")
262
+ [:running_chef, :converge_error].each do |n|
263
+ expect(action).to receive(:notify).with(n)
264
+ end
265
+ expect(target_host).to receive(:run_command).with(subject.read_chef_report).and_return(report_result)
266
+ expect(target_host).to receive(:run_command!).with(subject.delete_chef_report)
267
+ expect(exception_mapper).to receive(:raise_mapped_exception!)
268
+ subject.perform_action
269
+ end
270
+
271
+ context "when remote report cannot be read" do
272
+ let(:report_result) { double("report result", exit_status: 1, stdout: "", stderr: "") }
273
+ it "reports back failure" do
274
+ expect(action).to receive(:create_remote_policy).with(local_policy_path, remote_folder).and_return(remote_archive)
275
+ expect(action).to receive(:create_remote_config).with(remote_folder).and_return(remote_config)
276
+ expect(action).to receive(:create_remote_handler).with(remote_folder).and_return(remote_handler)
277
+ expect(action).to receive(:upload_trusted_certs).with(remote_folder)
278
+ expect(target_host).to receive(:run_command).with(/chef-client.+#{archive}/).and_return(result)
279
+ expect(target_host).to receive(:run_command!)
280
+ .with("#{subject.delete_folder} #{remote_folder}")
281
+ [:running_chef, :converge_error].each do |n|
282
+ expect(action).to receive(:notify).with(n)
283
+ end
284
+ expect(target_host).to receive(:run_command).with(subject.read_chef_report).and_return(report_result)
285
+ expect(exception_mapper).to receive(:raise_mapped_exception!)
286
+ subject.perform_action
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ end