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,140 @@
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/telemeter/sender"
20
+ require "chef_apply/config"
21
+
22
+ RSpec.describe ChefApply::Telemeter::Sender do
23
+ let(:session_files) { %w{file1 file2} }
24
+ let(:enabled_flag) { true }
25
+ let(:dev_mode) { false }
26
+ let(:config) { double("config") }
27
+
28
+ let(:subject) { ChefApply::Telemeter::Sender.new(session_files) }
29
+
30
+ before do
31
+ allow(config).to receive(:dev).and_return dev_mode
32
+ allow(ChefApply::Config).to receive(:telemetry).and_return config
33
+ allow(ChefApply::Telemeter).to receive(:enabled?).and_return enabled_flag
34
+ # Ensure this is not set for each test:
35
+ ENV.delete("CHEF_TELEMETRY_ENDPOINT")
36
+ end
37
+
38
+ describe "::start_upload_thread" do
39
+ let(:sender_mock) { instance_double(ChefApply::Telemeter::Sender) }
40
+ it "spawns a thread to run the send" do
41
+ expect(ChefApply::Telemeter::Sender).to receive(:find_session_files).and_return session_files
42
+ expect(ChefApply::Telemeter::Sender).to receive(:new).with(session_files).and_return sender_mock
43
+ expect(sender_mock).to receive(:run)
44
+ expect(::Thread).to receive(:new).and_yield
45
+ ChefApply::Telemeter::Sender.start_upload_thread
46
+ end
47
+ end
48
+
49
+ describe "#run" do
50
+ before do
51
+ expect(subject).to receive(:session_files).and_return session_files
52
+ end
53
+
54
+ context "when telemetry is disabled" do
55
+ let(:enabled_flag) { false }
56
+ it "deletes session files without sending" do
57
+ expect(FileUtils).to receive(:rm_rf).with("file1")
58
+ expect(FileUtils).to receive(:rm_rf).with("file2")
59
+ expect(FileUtils).to receive(:rm_rf).with(ChefApply::Config.telemetry_session_file)
60
+ expect(subject).to_not receive(:process_session)
61
+ subject.run
62
+ end
63
+ end
64
+
65
+ context "when telemetry is enabled" do
66
+ context "and telemetry dev mode is true" do
67
+ let(:dev_mode) { true }
68
+ let(:session_files) { [] } # Ensure we don't send anything without mocking :allthecalls:
69
+ context "and a custom telemetry endpoint is not set" do
70
+ it "configures the environment to submit to the Acceptance telemetry endpoint" do
71
+ subject.run
72
+ expect(ENV["CHEF_TELEMETRY_ENDPOINT"]).to eq "https://telemetry-acceptance.chef.io"
73
+ end
74
+ end
75
+
76
+ context "and a custom telemetry endpoint is already set" do
77
+ before do
78
+ ENV["CHEF_TELEMETRY_ENDPOINT"] = "https://localhost"
79
+ end
80
+ it "should not overwrite the custom value" do
81
+ subject.run
82
+ expect(ENV["CHEF_TELEMETRY_ENDPOINT"]).to eq "https://localhost"
83
+ end
84
+ end
85
+ end
86
+
87
+ it "submits the session capture for each session file found" do
88
+ expect(subject).to receive(:process_session).with("file1")
89
+ expect(subject).to receive(:process_session).with("file2")
90
+ expect(FileUtils).to receive(:rm_rf).with(ChefApply::Config.telemetry_session_file)
91
+ subject.run
92
+ end
93
+ end
94
+
95
+ context "when an error occurrs" do
96
+ it "logs it" do
97
+ allow(config).to receive(:enabled?).and_raise("Failed")
98
+ expect(ChefApply::Log).to receive(:fatal)
99
+ subject.run
100
+ end
101
+ end
102
+ end
103
+
104
+ describe "::find_session_files" do
105
+ it "finds all telemetry-payload-*.yml files in the telemetry directory" do
106
+ expect(ChefApply::Config).to receive(:telemetry_path).and_return("/tmp")
107
+ expect(Dir).to receive(:glob).with("/tmp/telemetry-payload-*.yml").and_return []
108
+ ChefApply::Telemeter::Sender.find_session_files
109
+ end
110
+ end
111
+
112
+ describe "process_session" do
113
+ it "loads the sesion and submits it" do
114
+ expect(subject).to receive(:load_and_clear_session).with("file1").and_return({ "version" => "1.0.0", "entries" => [] })
115
+ expect(subject).to receive(:submit_session).with({ "version" => "1.0.0", "entries" => [] })
116
+ subject.process_session("file1")
117
+ end
118
+ end
119
+
120
+ describe "submit_session" do
121
+ let(:telemetry) { instance_double("telemetry") }
122
+ it "removes the telemetry session file and starts a new session, then submits each entry in the session" do
123
+ expect(ChefApply::Config).to receive(:telemetry_session_file).and_return("/tmp/SESSION_ID")
124
+ expect(FileUtils).to receive(:rm_rf).with("/tmp/SESSION_ID")
125
+ expect(Telemetry).to receive(:new).and_return telemetry
126
+ expect(subject).to receive(:submit_entry).with(telemetry, { "event" => "action1" }, 1, 2)
127
+ expect(subject).to receive(:submit_entry).with(telemetry, { "event" => "action2" }, 2, 2)
128
+ subject.submit_session( { "version" => "1.0.0",
129
+ "entries" => [ { "event" => "action1" }, { "event" => "action2" } ] } )
130
+ end
131
+ end
132
+
133
+ describe "submit_entry" do
134
+ let(:telemetry) { instance_double("telemetry") }
135
+ it "submits the entry to telemetry" do
136
+ expect(telemetry).to receive(:deliver).with("test" => "this")
137
+ subject.submit_entry(telemetry, { "test" => "this" }, 1, 1)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,191 @@
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/telemeter"
20
+
21
+ RSpec.describe ChefApply::Telemeter do
22
+ subject { ChefApply::Telemeter.instance }
23
+ let(:host_platform) { "linux" }
24
+
25
+ before do
26
+ allow(subject).to receive(:host_platform).and_return host_platform
27
+ end
28
+
29
+ context "#commit" do
30
+ context "when telemetry is enabled" do
31
+ before do
32
+ allow(subject).to receive(:enabled?).and_return true
33
+ end
34
+
35
+ it "writes events out and clears the queue" do
36
+ subject.capture(:test)
37
+ expect(subject.pending_event_count).to eq 1
38
+ expect(subject).to receive(:convert_events_to_session)
39
+ expect(subject).to receive(:write_session)
40
+
41
+ subject.commit
42
+ expect(subject.pending_event_count).to eq 0
43
+ end
44
+ end
45
+
46
+ context "when telemetry is disabled" do
47
+ before do
48
+ allow(subject).to receive(:enabled?).and_return false
49
+ end
50
+ it "does not write any events and clears the queue" do
51
+ subject.capture(:test)
52
+ expect(subject.pending_event_count).to eq 1
53
+ expect(subject).to_not receive(:convert_events_to_session)
54
+
55
+ subject.commit
56
+ expect(subject.pending_event_count).to eq 0
57
+ end
58
+ end
59
+ end
60
+
61
+ context "#timed_action_capture" do
62
+ context "when a valid target_host is present" do
63
+ it "invokes timed_capture with action and valid target data" do
64
+ target = instance_double("TargetHost",
65
+ base_os: "windows",
66
+ version: "10.0.0",
67
+ architecture: "x86_64",
68
+ hostname: "My_Host",
69
+ transport_type: "winrm")
70
+ action = instance_double("Action::Base", name: "test_action",
71
+ target_host: target)
72
+ expected_data = {
73
+ action: "test_action",
74
+ target: {
75
+ platform: {
76
+ name: "windows",
77
+ version: "10.0.0",
78
+ architecture: "x86_64"
79
+ },
80
+ hostname_sha1: Digest::SHA1.hexdigest("my_host"),
81
+ transport_type: "winrm"
82
+ }
83
+ }
84
+ expect(subject).to receive(:timed_capture).with(:action, expected_data)
85
+ subject.timed_action_capture(action) { :ok }
86
+ end
87
+
88
+ context "when a valid target_host is not present" do
89
+ it "invokes timed_capture with empty target values" do
90
+ expected_data = { action: "Base", target: { platform: {},
91
+ hostname_sha1: nil,
92
+ transport_type: nil } }
93
+ expect(subject).to receive(:timed_capture).
94
+ with(:action, expected_data)
95
+ subject.timed_action_capture(
96
+ ChefApply::Action::Base.new(target_host: nil)
97
+ ) { :ok }
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ context "::enabled?" do
104
+ let(:enabled_flag) { false }
105
+ let(:config) { double("config") }
106
+ before do
107
+ allow(ChefApply::Config).to receive(:telemetry).and_return(config)
108
+ allow(config).to receive(:enable).and_return(enabled_flag)
109
+ end
110
+
111
+ context "when config value is enabled" do
112
+ let(:enabled_flag) { true }
113
+ context "and CHEF_TELEMETRY_OPT_OUT is not present in env vars" do
114
+ it "returns false" do
115
+ ENV.delete("CHEF_TELEMETRY_OPT_OUT")
116
+ expect(subject.enabled?).to eq true
117
+ end
118
+ end
119
+ context "and CHEF_TELEMETRY_OPT_OUT is present in env vars" do
120
+ it "returns false" do
121
+ ENV["CHEF_TELEMETRY_OPT_OUT"] = ""
122
+ expect(subject.enabled?).to eq false
123
+ end
124
+ end
125
+ end
126
+
127
+ context "when config value is disabled" do
128
+ let(:enabled_flag) { false }
129
+ it "returns false" do
130
+ expect(subject.enabled?).to eq false
131
+ end
132
+ end
133
+ end
134
+
135
+ context "#timed_run_capture" do
136
+ it "invokes timed_capture with run data" do
137
+ expected_data = { arguments: [ "arg1" ] }
138
+ expect(subject).to receive(:timed_capture).
139
+ with(:run, expected_data)
140
+ subject.timed_run_capture(["arg1"])
141
+ end
142
+ end
143
+
144
+ context "#timed_capture" do
145
+ let(:runner) { double("capture_test") }
146
+ before do
147
+ expect(subject.pending_event_count).to eq 0
148
+ end
149
+
150
+ it "runs the requested thing and invokes #capture with duration" do
151
+ expect(runner).to receive(:do_it)
152
+ expect(subject).to receive(:capture) do |name, data|
153
+ expect(name).to eq(:do_it_test)
154
+ expect(data[:duration]).to be > 0.0
155
+ end
156
+ subject.timed_capture(:do_it_test) do
157
+ runner.do_it
158
+ end
159
+ end
160
+ end
161
+
162
+ context "#capture" do
163
+ before do
164
+ expect(subject.pending_event_count).to eq 0
165
+ end
166
+ it "adds the captured event to the session" do
167
+ subject.capture(:test, {})
168
+ expect(subject.pending_event_count) == 1
169
+ end
170
+ end
171
+
172
+ context "#make_event_payload" do
173
+ before do
174
+ allow(subject).to receive(:installation_id).and_return "0000"
175
+ end
176
+
177
+ it "adds expected properties" do
178
+ payload = subject.make_event_payload(:run, { hello: "world" })
179
+ expected_payload = {
180
+ event: :run,
181
+ properties: {
182
+ installation_id: "0000",
183
+ run_timestamp: subject.run_timestamp,
184
+ host_platform: host_platform,
185
+ event_data: { hello: "world" }
186
+ }
187
+ }
188
+ expect(payload).to eq expected_payload
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,199 @@
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/temp_cookbook"
20
+ require "tempfile"
21
+ require "securerandom"
22
+
23
+ RSpec.describe ChefApply::TempCookbook do
24
+ subject(:tc) { ChefApply::TempCookbook.new }
25
+ let(:uuid) { SecureRandom.uuid }
26
+
27
+ before do
28
+ @repo_paths = ChefApply::Config.chef.cookbook_repo_paths
29
+ ChefApply::Config.chef.cookbook_repo_paths = []
30
+ end
31
+
32
+ after do
33
+ ChefApply::Config.chef.cookbook_repo_paths = @repo_paths
34
+ tc.delete
35
+ end
36
+
37
+ describe "#from_existing_recipe" do
38
+ it "raises an error if the recipe does not have a .rb extension" do
39
+ err = ChefApply::TempCookbook::UnsupportedExtension
40
+ expect { tc.from_existing_recipe("/some/file.chef") }.to raise_error(err)
41
+ end
42
+
43
+ context "when there is an existing cookbook" do
44
+ let(:cb) do
45
+ d = Dir.mktmpdir
46
+ File.open(File.join(d, "metadata.rb"), "w+") do |f|
47
+ f << "name \"foo\""
48
+ end
49
+ FileUtils.mkdir(File.join(d, "recipes"))
50
+ d
51
+ end
52
+
53
+ let(:existing_recipe) do
54
+ File.open(File.join(cb, "recipes/default.rb"), "w+") do |f|
55
+ f.write(uuid)
56
+ f
57
+ end
58
+ end
59
+
60
+ after do
61
+ FileUtils.remove_entry cb
62
+ end
63
+
64
+ it "copies the whole cookbook" do
65
+ tc.from_existing_recipe(existing_recipe.path)
66
+ expect(File.read(File.join(tc.path, "recipes/default.rb"))).to eq(uuid)
67
+ expect(File.read(File.join(tc.path, "Policyfile.rb"))).to eq <<~EXPECTED_POLICYFILE
68
+ name "foo_policy"
69
+ default_source :supermarket
70
+ run_list "foo::default"
71
+ cookbook "foo", path: "."
72
+ EXPECTED_POLICYFILE
73
+ expect(File.read(File.join(tc.path, "metadata.rb"))).to eq("name \"foo\"")
74
+ end
75
+ end
76
+
77
+ context "when there is only a single recipe not in a cookbook" do
78
+ let(:existing_recipe) do
79
+ t = Tempfile.new(["recipe", ".rb"])
80
+ t.write(uuid)
81
+ t.close
82
+ t
83
+ end
84
+
85
+ after do
86
+ existing_recipe.unlink
87
+ end
88
+
89
+ it "copies the existing recipe into a new cookbook" do
90
+ tc.from_existing_recipe(existing_recipe.path)
91
+ recipe_filename = File.basename(existing_recipe.path)
92
+ recipe_name = File.basename(recipe_filename, File.extname(recipe_filename))
93
+ expect(File.read(File.join(tc.path, "recipes/", recipe_filename))).to eq(uuid)
94
+ expect(File.read(File.join(tc.path, "Policyfile.rb"))).to eq <<~EXPECTED_POLICYFILE
95
+ name "cw_recipe_policy"
96
+ default_source :supermarket
97
+ run_list "cw_recipe::#{recipe_name}"
98
+ cookbook "cw_recipe", path: "."
99
+ EXPECTED_POLICYFILE
100
+ expect(File.read(File.join(tc.path, "metadata.rb"))).to eq("name \"cw_recipe\"\n")
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "#from_resource" do
106
+ it "creates a recipe containing the supplied recipe" do
107
+ tc.from_resource("directory", "/tmp/foo", [])
108
+ expect(File.read(File.join(tc.path, "recipes/default.rb"))).to eq("directory '/tmp/foo'\n")
109
+ end
110
+ end
111
+
112
+ describe "#generate_metadata" do
113
+ it "generates metadata in the temp cookbook" do
114
+ f = tc.generate_metadata("foo")
115
+ expect(File.read(f)).to eq("name \"foo\"\n")
116
+ end
117
+ end
118
+
119
+ describe "#generate_policyfile" do
120
+ context "when there is no existing policyfile" do
121
+ it "generates a policyfile in the temp cookbook" do
122
+ f = tc.generate_policyfile("foo", "bar")
123
+ expect(File.read(f)).to eq <<~EXPECTED_POLICYFILE
124
+ name "foo_policy"
125
+ default_source :supermarket
126
+ run_list "foo::bar"
127
+ cookbook "foo", path: "."
128
+ EXPECTED_POLICYFILE
129
+ end
130
+
131
+ context "when there are configured cookbook_repo_paths" do
132
+ it "generates a policyfile in the temp cookbook" do
133
+ ChefApply::Config.chef.cookbook_repo_paths = %w{one two}
134
+ f = tc.generate_policyfile("foo", "bar")
135
+ expect(File.read(f)).to eq <<~EXPECTED_POLICYFILE
136
+ name "foo_policy"
137
+ default_source :chef_repo, "one"
138
+ default_source :chef_repo, "two"
139
+ default_source :supermarket
140
+ run_list "foo::bar"
141
+ cookbook "foo", path: "."
142
+ EXPECTED_POLICYFILE
143
+ end
144
+ end
145
+ end
146
+
147
+ context "when there is an existing policyfile" do
148
+ before do
149
+ File.open(File.join(tc.path, "Policyfile.rb"), "a") do |f|
150
+ f << "this is a policyfile"
151
+ end
152
+ end
153
+ it "only overrides the existing run_list in the policyfile" do
154
+ f = tc.generate_policyfile("foo", "bar")
155
+ expect(File.read(f)).to eq <<~EXPECTED_POLICYFILE
156
+ this is a policyfile
157
+ # Overriding run_list with command line specified value
158
+ run_list "foo::bar"
159
+ EXPECTED_POLICYFILE
160
+ end
161
+ end
162
+ end
163
+
164
+ describe "#create_resource_definition" do
165
+ let(:r1) { "directory" }
166
+ let(:r2) { "/tmp" }
167
+ let(:props) { nil }
168
+ context "when no properties are provided" do
169
+ it "it creates a simple resource" do
170
+ expect(tc.create_resource_definition(r1, r2, [])).to eq("directory '/tmp'\n")
171
+ end
172
+ end
173
+
174
+ context "when properties are provided" do
175
+ let(:props) do
176
+ {
177
+ "key1" => "value",
178
+ "key2" => 0.1,
179
+ "key3" => 100,
180
+ "key4" => true,
181
+ "key_with_underscore" => "value",
182
+ }
183
+ end
184
+
185
+ it "converts the properties to chef-client args" do
186
+ expected = <<~EXPECTED_RESOURCE
187
+ directory '/tmp' do
188
+ key1 'value'
189
+ key2 0.1
190
+ key3 100
191
+ key4 true
192
+ key_with_underscore 'value'
193
+ end
194
+ EXPECTED_RESOURCE
195
+ expect(tc.create_resource_definition(r1, r2, props)).to eq(expected)
196
+ end
197
+ end
198
+ end
199
+ end