chef-apply 0.1.17 → 0.1.18

Sign up to get free protection for your applications and to get access to all the features.
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