chef-dk 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +2 -2
  3. data/README.md +25 -0
  4. data/lib/chef-dk/builtin_commands.rb +4 -0
  5. data/lib/chef-dk/cli.rb +46 -0
  6. data/lib/chef-dk/command/base.rb +4 -0
  7. data/lib/chef-dk/command/generator_commands/template.rb +2 -1
  8. data/lib/chef-dk/command/install.rb +105 -0
  9. data/lib/chef-dk/command/push.rb +123 -0
  10. data/lib/chef-dk/cookbook_profiler/identifiers.rb +5 -0
  11. data/lib/chef-dk/exceptions.rb +38 -0
  12. data/lib/chef-dk/generator.rb +16 -1
  13. data/lib/chef-dk/helpers.rb +1 -1
  14. data/lib/chef-dk/policyfile/cookbook_location_specification.rb +4 -0
  15. data/lib/chef-dk/policyfile/cookbook_locks.rb +73 -0
  16. data/lib/chef-dk/policyfile/reports/install.rb +70 -0
  17. data/lib/chef-dk/policyfile/reports/table_printer.rb +58 -0
  18. data/lib/chef-dk/policyfile/reports/upload.rb +70 -0
  19. data/lib/chef-dk/policyfile/solution_dependencies.rb +102 -8
  20. data/lib/chef-dk/policyfile/uploader.rb +37 -6
  21. data/lib/chef-dk/policyfile_compiler.rb +19 -5
  22. data/lib/chef-dk/policyfile_lock.rb +122 -9
  23. data/lib/chef-dk/policyfile_services/install.rb +131 -0
  24. data/lib/chef-dk/policyfile_services/push.rb +121 -0
  25. data/lib/chef-dk/skeletons/code_generator/recipes/cookbook.rb +6 -4
  26. data/lib/chef-dk/ui.rb +50 -0
  27. data/lib/chef-dk/version.rb +1 -1
  28. data/spec/shared/a_file_generator.rb +4 -1
  29. data/spec/test_helpers.rb +21 -0
  30. data/spec/unit/cli_spec.rb +100 -1
  31. data/spec/unit/command/base_spec.rb +23 -0
  32. data/spec/unit/command/exec_spec.rb +2 -2
  33. data/spec/unit/command/install_spec.rb +159 -0
  34. data/spec/unit/command/push_spec.rb +203 -0
  35. data/spec/unit/command/shell_init_spec.rb +1 -1
  36. data/spec/unit/policyfile/cookbook_location_specification_spec.rb +7 -0
  37. data/spec/unit/policyfile/cookbook_locks_spec.rb +13 -2
  38. data/spec/unit/policyfile/reports/install_spec.rb +115 -0
  39. data/spec/unit/policyfile/reports/upload_spec.rb +96 -0
  40. data/spec/unit/policyfile/solution_dependencies_spec.rb +1 -1
  41. data/spec/unit/policyfile/uploader_spec.rb +9 -12
  42. data/spec/unit/policyfile_lock_serialization_spec.rb +292 -0
  43. data/spec/unit/policyfile_services/install_spec.rb +170 -0
  44. data/spec/unit/policyfile_services/push_spec.rb +202 -0
  45. metadata +48 -6
@@ -0,0 +1,121 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 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-dk/authenticated_http'
19
+ require 'chef-dk/policyfile_compiler'
20
+ require 'chef-dk/policyfile/uploader'
21
+
22
+ module ChefDK
23
+ module PolicyfileServices
24
+ class Push
25
+
26
+ attr_reader :root_dir
27
+ attr_reader :config
28
+ attr_reader :policy_group
29
+ attr_reader :ui
30
+
31
+ def initialize(policyfile: nil, ui: nil, policy_group: nil, config: nil, root_dir: nil)
32
+ @root_dir = root_dir
33
+ @policyfile_relative_path = policyfile
34
+ @ui = ui
35
+ @config = config
36
+ @policy_group = policy_group
37
+
38
+ @http_client = nil
39
+ @storage_config = nil
40
+ @policy_data = nil
41
+ end
42
+
43
+ def policyfile_relative_path
44
+ @policyfile_relative_path || "Policyfile.rb"
45
+ end
46
+
47
+ def policyfile_path
48
+ File.expand_path(policyfile_relative_path, root_dir)
49
+ end
50
+
51
+ def lockfile_relative_path
52
+ policyfile_relative_path.gsub(/\.rb\Z/, '') + ".lock.json"
53
+ end
54
+
55
+ def lockfile_path
56
+ File.expand_path(lockfile_relative_path, root_dir)
57
+ end
58
+
59
+ def http_client
60
+ @http_client ||= ChefDK::AuthenticatedHTTP.new(config.chef_server_url,
61
+ signing_key_filename: config.client_key,
62
+ client_name: config.node_name)
63
+ end
64
+
65
+ def policy_data
66
+ @policy_data ||= FFI_Yajl::Parser.parse(IO.read(lockfile_path))
67
+ rescue => error
68
+ raise PolicyfilePushError.new("Error reading lockfile #{lockfile_path}", error)
69
+ end
70
+
71
+ def storage_config
72
+ @storage_config ||= ChefDK::Policyfile::StorageConfig.new.use_policyfile_lock(lockfile_path)
73
+ end
74
+
75
+ def uploader
76
+ ChefDK::Policyfile::Uploader.new(policyfile_lock, policy_group, ui: ui, http_client: http_client)
77
+ end
78
+
79
+ def run
80
+ unless File.exist?(lockfile_path)
81
+ raise LockfileNotFound, "No lockfile at #{lockfile_path} - you need to run `install` before `push`"
82
+ end
83
+
84
+ validate_lockfile
85
+ write_updated_lockfile
86
+ upload_policy
87
+
88
+ end
89
+
90
+ def policyfile_lock
91
+ @policyfile_lock || validate_lockfile
92
+ end
93
+
94
+ private
95
+
96
+ def upload_policy
97
+ uploader.upload
98
+ rescue => error
99
+ raise PolicyfilePushError.new("Failed to upload policy to policy group #{policy_group}", error)
100
+ end
101
+
102
+ def write_updated_lockfile
103
+ File.open(lockfile_path, "w+") do |f|
104
+ f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
105
+ end
106
+ end
107
+
108
+ def validate_lockfile
109
+ return @policyfile_lock if @policyfile_lock
110
+ @policyfile_lock = ChefDK::PolicyfileLock.new(storage_config).build_from_lock_data(policy_data)
111
+ # TODO: enumerate any cookbook that have been updated
112
+ @policyfile_lock.validate_cookbooks!
113
+ @policyfile_lock
114
+ rescue => error
115
+ raise PolicyfilePushError.new("Invalid lockfile data", error)
116
+ end
117
+
118
+ end
119
+ end
120
+ end
121
+
@@ -43,11 +43,13 @@ template "#{cookbook_dir}/recipes/default.rb" do
43
43
  end
44
44
 
45
45
  # git
46
- if context.have_git && !context.skip_git_init
46
+ if context.have_git
47
+ if !context.skip_git_init
47
48
 
48
- execute("initialize-git") do
49
- command("git init .")
50
- cwd cookbook_dir
49
+ execute("initialize-git") do
50
+ command("git init .")
51
+ cwd cookbook_dir
52
+ end
51
53
  end
52
54
 
53
55
  cookbook_file "#{cookbook_dir}/.gitignore" do
@@ -0,0 +1,50 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 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
+ module ChefDK
19
+ class UI
20
+
21
+ class NullStream
22
+
23
+ def puts(*anything)
24
+ nil
25
+ end
26
+
27
+ end
28
+
29
+ def self.null
30
+ new(out: NullStream.new, err: NullStream.new)
31
+ end
32
+
33
+ attr_reader :out_stream
34
+ attr_reader :err_stream
35
+
36
+ def initialize(out: nil, err: nil)
37
+ @out_stream = out || $stdout
38
+ @err_stream = err || $stderr
39
+ end
40
+
41
+ def err(message)
42
+ @err_stream.puts(message)
43
+ end
44
+
45
+ def msg(message)
46
+ @out_stream.puts(message)
47
+ end
48
+ end
49
+ end
50
+
@@ -16,5 +16,5 @@
16
16
  #
17
17
 
18
18
  module ChefDK
19
- VERSION = "0.2.1"
19
+ VERSION = "0.3.0"
20
20
  end
@@ -32,6 +32,10 @@ shared_examples_for "a file generator" do
32
32
  reset_tempdir
33
33
  end
34
34
 
35
+ after(:each) do
36
+ ChefDK::Generator::Context.reset
37
+ end
38
+
35
39
  context "when argv is empty" do
36
40
  let(:argv) { [] }
37
41
 
@@ -118,4 +122,3 @@ shared_examples_for "a file generator" do
118
122
  end
119
123
 
120
124
  end
121
-
@@ -56,4 +56,25 @@ module TestHelpers
56
56
  @tmpdir ||= Dir.mktmpdir("chef-dk")
57
57
  File.realpath(@tmpdir)
58
58
  end
59
+
60
+ class TestUI
61
+
62
+ attr_reader :output_stream
63
+
64
+ def initialize
65
+ @output_stream = StringIO.new
66
+ end
67
+
68
+ def err(message)
69
+ @output_stream.puts(message)
70
+ end
71
+
72
+ def msg(message)
73
+ @output_stream.puts(message)
74
+ end
75
+
76
+ def output
77
+ @output_stream.string
78
+ end
79
+ end
59
80
  end
@@ -1,4 +1,3 @@
1
- #
2
1
  # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
2
  # License:: Apache License, Version 2.0
4
3
  #
@@ -58,6 +57,12 @@ E
58
57
  let(:version_message) { "Chef Development Kit Version: #{ChefDK::VERSION}\n" }
59
58
 
60
59
  def run_cli(expected_exit_code)
60
+ expect(cli).to receive(:exit).with(expected_exit_code)
61
+ expect(cli).to receive(:sanity_check!)
62
+ cli.run
63
+ end
64
+
65
+ def run_cli_with_sanity_check(expected_exit_code)
61
66
  expect(cli).to receive(:exit).with(expected_exit_code)
62
67
  cli.run
63
68
  end
@@ -107,6 +112,18 @@ E
107
112
  end
108
113
  end
109
114
 
115
+ context "given an invalid option" do
116
+
117
+ let(:argv) { %w[-nope] }
118
+
119
+ it "prints an 'invalid option message and the help output, then exits non-zero" do
120
+ run_cli(1)
121
+ expect(stdout).to eq(base_help_message)
122
+ expect(stderr).to eq("invalid option: -nope\n")
123
+ end
124
+
125
+ end
126
+
110
127
  context "given an invalid/unknown subcommand" do
111
128
  let(:argv) { %w[ancient-aliens] }
112
129
 
@@ -148,4 +165,86 @@ E
148
165
  end
149
166
  end
150
167
 
168
+ context "sanity_check!" do
169
+ context "on unix" do
170
+ it "complains if embedded is first" do
171
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/embedded/bin:/opt/chefdk/bin' })
172
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
173
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
174
+ run_cli_with_sanity_check(0)
175
+ expect(stdout).not_to eq(base_help_message)
176
+ expect(stdout).to include("please reverse that order")
177
+ expect(stdout).to include("chef shell-init")
178
+ end
179
+
180
+ it "complains if only embedded is present" do
181
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/embedded/bin' })
182
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
183
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
184
+ run_cli_with_sanity_check(0)
185
+ expect(stdout).not_to eq(base_help_message)
186
+ expect(stdout).to include("you must add")
187
+ expect(stdout).to include("chef shell-init")
188
+ end
189
+
190
+ it "passes when both are present in the correct order" do
191
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/bin:/opt/chefdk/embedded/bin' })
192
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
193
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
194
+ run_cli_with_sanity_check(0)
195
+ expect(stdout).to eq(base_help_message)
196
+ end
197
+
198
+ it "passes when only the omnibus bin dir is present" do
199
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/bin' })
200
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
201
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
202
+ run_cli_with_sanity_check(0)
203
+ expect(stdout).to eq(base_help_message)
204
+ end
205
+ end
206
+
207
+ context "on windows" do
208
+ before do
209
+ allow(Chef::Platform).to receive(:windows?).and_return(true)
210
+ stub_const("File::PATH_SEPARATOR", ';')
211
+ end
212
+
213
+ it "complains if embedded is first" do
214
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\embedded\bin;C:\opscode\chefdk\bin' })
215
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
216
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
217
+ run_cli_with_sanity_check(0)
218
+ expect(stdout).not_to eq(base_help_message)
219
+ expect(stdout).to include("please reverse that order")
220
+ expect(stdout).to include("chef shell-init")
221
+ end
222
+
223
+ it "complains if only embedded is present" do
224
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\embedded\bin' })
225
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
226
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
227
+ run_cli_with_sanity_check(0)
228
+ expect(stdout).not_to eq(base_help_message)
229
+ expect(stdout).to include("you must add")
230
+ expect(stdout).to include("chef shell-init")
231
+ end
232
+
233
+ it "passes when both are present in the correct order" do
234
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\bin;C:\opscode\chefdk\embedded\bin' })
235
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
236
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
237
+ run_cli_with_sanity_check(0)
238
+ expect(stdout).to eq(base_help_message)
239
+ end
240
+
241
+ it "passes when only the omnibus bin dir is present" do
242
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\bin' })
243
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
244
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
245
+ run_cli_with_sanity_check(0)
246
+ expect(stdout).to eq(base_help_message)
247
+ end
248
+ end
249
+ end
151
250
  end
@@ -34,6 +34,7 @@ describe ChefDK::Command::Base do
34
34
  end
35
35
  end
36
36
 
37
+ let(:stderr_io) { StringIO.new }
37
38
  let(:stdout_io) { StringIO.new }
38
39
  let(:command_instance) { TestCommand.new() }
39
40
 
@@ -41,8 +42,13 @@ describe ChefDK::Command::Base do
41
42
  stdout_io.string
42
43
  end
43
44
 
45
+ def stderr
46
+ stderr_io.string
47
+ end
48
+
44
49
  before do
45
50
  allow(command_instance).to receive(:stdout).and_return(stdout_io)
51
+ allow(command_instance).to receive(:stderr).and_return(stderr_io)
46
52
  end
47
53
 
48
54
 
@@ -85,4 +91,21 @@ describe ChefDK::Command::Base do
85
91
  expect(stdout).to eq("thanks for passing me true\n")
86
92
  end
87
93
 
94
+ describe "when given invalid options" do
95
+
96
+ it "prints the help banner and exits gracefully" do
97
+ expect(run_command(%w[-foo])).to eq(1)
98
+
99
+ expect(stderr).to eq("invalid option: -foo\n")
100
+
101
+ expected = <<-E
102
+ use me please
103
+ -u, --user If the user exists
104
+
105
+ E
106
+ expect(stdout).to eq(expected)
107
+ end
108
+
109
+ end
110
+
88
111
  end
@@ -73,7 +73,7 @@ describe ChefDK::Command::Exec do
73
73
 
74
74
  let(:expected_GEM_ROOT) { Gem.default_dir.inspect }
75
75
 
76
- let(:expected_GEM_HOME) { ENV['GEM_HOME'] }
76
+ let(:expected_GEM_HOME) { Gem.user_dir }
77
77
 
78
78
  let(:expected_GEM_PATH) { Gem.path.join(':') }
79
79
 
@@ -107,7 +107,7 @@ describe ChefDK::Command::Exec do
107
107
 
108
108
  let(:expected_GEM_ROOT) { Gem.default_dir.inspect }
109
109
 
110
- let(:expected_GEM_HOME) { ENV['GEM_HOME'] }
110
+ let(:expected_GEM_HOME) { Gem.user_dir }
111
111
 
112
112
  let(:expected_GEM_PATH) { Gem.path.join(':') }
113
113
 
@@ -0,0 +1,159 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 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-dk/command/install'
20
+
21
+ describe ChefDK::Command::Install do
22
+
23
+ let(:params) { [] }
24
+
25
+ let(:command) do
26
+ c = described_class.new
27
+ c.apply_params!(params)
28
+ c
29
+ end
30
+
31
+ let(:install_service) { instance_double(ChefDK::PolicyfileServices::Install) }
32
+
33
+ it "disables debug by default" do
34
+ expect(command.debug?).to be(false)
35
+ end
36
+
37
+ it "configures a default UI component" do
38
+ ui = command.ui
39
+ expect(ui.out_stream).to eq($stdout)
40
+ expect(ui.err_stream).to eq($stderr)
41
+ end
42
+
43
+ context "when debug mode is set" do
44
+
45
+ let(:params) { [ "-D" ] }
46
+
47
+ it "enables debug" do
48
+ expect(command.debug?).to be(true)
49
+ end
50
+ end
51
+
52
+ context "with no arguments" do
53
+
54
+ it "does not specify a policyfile relative path" do
55
+ expect(command.policyfile_relative_path).to be(nil)
56
+ end
57
+
58
+ it "creates the installer service with a `nil` policyfile path" do
59
+ expect(ChefDK::PolicyfileServices::Install).to receive(:new).
60
+ with(policyfile: nil, ui: command.ui, root_dir: Dir.pwd).
61
+ and_return(install_service)
62
+ expect(command.installer).to eq(install_service)
63
+ end
64
+
65
+ end
66
+
67
+ context "with an explicit policyfile relative path" do
68
+
69
+ let(:params) { [ "MyPolicy.rb" ] }
70
+
71
+ it "respects the user-supplied path" do
72
+ expect(command.policyfile_relative_path).to eq("MyPolicy.rb")
73
+ end
74
+
75
+ it "creates the installer service with the specified policyfile path" do
76
+ expect(ChefDK::PolicyfileServices::Install).to receive(:new).
77
+ with(policyfile: "MyPolicy.rb", ui: command.ui, root_dir: Dir.pwd).
78
+ and_return(install_service)
79
+ expect(command.installer).to eq(install_service)
80
+ end
81
+
82
+ end
83
+
84
+ describe "running the install" do
85
+
86
+ let(:ui) { TestHelpers::TestUI.new }
87
+
88
+ before do
89
+ command.ui = ui
90
+ allow(command).to receive(:installer).and_return(install_service)
91
+ end
92
+
93
+ context "when the command is successful" do
94
+ before do
95
+ expect(install_service).to receive(:run)
96
+ end
97
+
98
+ it "returns 0" do
99
+ expect(command.run).to eq(0)
100
+ end
101
+ end
102
+
103
+ context "when the command is unsuccessful" do
104
+
105
+ let(:backtrace) { caller[0...3] }
106
+
107
+ let(:cause) do
108
+ e = StandardError.new("some operation failed")
109
+ e.set_backtrace(backtrace)
110
+ e
111
+ end
112
+
113
+ let(:exception) do
114
+ ChefDK::PolicyfileInstallError.new("install failed", cause)
115
+ end
116
+
117
+ before do
118
+ expect(install_service).to receive(:run).and_raise(exception)
119
+ end
120
+
121
+ it "returns 1" do
122
+ expect(command.run).to eq(1)
123
+ end
124
+
125
+ it "displays the exception and cause" do
126
+ expected_error_text=<<-E
127
+ Error: install failed
128
+ Reason: StandardError
129
+
130
+ some operation failed
131
+ E
132
+
133
+ command.run
134
+ expect(ui.output).to eq(expected_error_text)
135
+ end
136
+
137
+ context "and debug is enabled" do
138
+
139
+ let(:params) { ["-D"] }
140
+
141
+ it "displays the exception and cause with backtrace" do
142
+ expected_error_text=<<-E
143
+ Error: install failed
144
+ Reason: StandardError
145
+
146
+ some operation failed
147
+ E
148
+
149
+ expected_error_text << backtrace.join("\n") << "\n"
150
+
151
+ command.run
152
+ expect(ui.output).to eq(expected_error_text)
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+ end