chef 11.10.0.alpha.1 → 11.10.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/README.md +57 -36
  2. data/distro/common/html/chef-client.8.html +4 -4
  3. data/distro/common/html/chef-expander.8.html +4 -4
  4. data/distro/common/html/chef-expanderctl.8.html +4 -4
  5. data/distro/common/html/chef-server-webui.8.html +4 -4
  6. data/distro/common/html/chef-server.8.html +4 -4
  7. data/distro/common/html/chef-shell.1.html +4 -4
  8. data/distro/common/html/chef-solo.8.html +4 -4
  9. data/distro/common/html/chef-solr.8.html +5 -5
  10. data/distro/common/html/knife-bootstrap.1.html +4 -4
  11. data/distro/common/html/knife-client.1.html +4 -4
  12. data/distro/common/html/knife-configure.1.html +4 -4
  13. data/distro/common/html/knife-cookbook-site.1.html +4 -4
  14. data/distro/common/html/knife-cookbook.1.html +4 -4
  15. data/distro/common/html/knife-data-bag.1.html +4 -4
  16. data/distro/common/html/knife-environment.1.html +4 -4
  17. data/distro/common/html/knife-exec.1.html +4 -4
  18. data/distro/common/html/knife-index.1.html +4 -4
  19. data/distro/common/html/knife-node.1.html +4 -4
  20. data/distro/common/html/knife-role.1.html +4 -4
  21. data/distro/common/html/knife-search.1.html +4 -4
  22. data/distro/common/html/knife-ssh.1.html +4 -4
  23. data/distro/common/html/knife-status.1.html +4 -4
  24. data/distro/common/html/knife-tag.1.html +4 -4
  25. data/distro/common/html/knife.1.html +4 -4
  26. data/distro/common/man/man1/knife-bootstrap.1 +58 -64
  27. data/distro/common/man/man1/knife-client.1 +19 -22
  28. data/distro/common/man/man1/knife-configure.1 +37 -46
  29. data/distro/common/man/man1/knife-cookbook-site.1 +14 -17
  30. data/distro/common/man/man1/knife-cookbook.1 +15 -18
  31. data/distro/common/man/man1/knife-data-bag.1 +14 -17
  32. data/distro/common/man/man1/knife-delete.1 +38 -47
  33. data/distro/common/man/man1/knife-deps.1 +39 -48
  34. data/distro/common/man/man1/knife-diff.1 +43 -52
  35. data/distro/common/man/man1/knife-download.1 +47 -53
  36. data/distro/common/man/man1/knife-edit.1 +32 -41
  37. data/distro/common/man/man1/knife-environment.1 +14 -17
  38. data/distro/common/man/man1/knife-exec.1 +52 -61
  39. data/distro/common/man/man1/knife-index-rebuild.1 +1 -61
  40. data/distro/common/man/man1/knife-list.1 +47 -59
  41. data/distro/common/man/man1/knife-node.1 +15 -18
  42. data/distro/common/man/man1/knife-raw.1 +28 -46
  43. data/distro/common/man/man1/knife-recipe-list.1 +1 -61
  44. data/distro/common/man/man1/knife-role.1 +19 -25
  45. data/distro/common/man/man1/knife-search.1 +53 -62
  46. data/distro/common/man/man1/knife-show.1 +36 -28
  47. data/distro/common/man/man1/knife-ssh.1 +55 -61
  48. data/distro/common/man/man1/knife-status.1 +34 -43
  49. data/distro/common/man/man1/knife-tag.1 +14 -17
  50. data/distro/common/man/man1/knife-upload.1 +47 -56
  51. data/distro/common/man/man1/knife-user.1 +17 -20
  52. data/distro/common/man/man1/knife-xargs.1 +60 -69
  53. data/lib/chef/application.rb +3 -1
  54. data/lib/chef/application/windows_service.rb +0 -1
  55. data/lib/chef/client.rb +41 -152
  56. data/lib/chef/config.rb +19 -23
  57. data/lib/chef/data_bag.rb +1 -1
  58. data/lib/chef/data_bag_item.rb +1 -1
  59. data/lib/chef/exceptions.rb +8 -0
  60. data/lib/chef/formatters/doc.rb +15 -0
  61. data/lib/chef/formatters/error_inspectors/api_error_formatting.rb +2 -1
  62. data/lib/chef/http.rb +18 -8
  63. data/lib/chef/http/authenticator.rb +4 -0
  64. data/lib/chef/http/cookie_manager.rb +3 -0
  65. data/lib/chef/http/decompressor.rb +4 -0
  66. data/lib/chef/http/json_input.rb +4 -0
  67. data/lib/chef/http/json_output.rb +4 -0
  68. data/lib/chef/http/validate_content_length.rb +94 -0
  69. data/lib/chef/knife.rb +0 -1
  70. data/lib/chef/knife/configure.rb +6 -6
  71. data/lib/chef/knife/cookbook_create.rb +2 -2
  72. data/lib/chef/knife/core/subcommand_loader.rb +49 -3
  73. data/lib/chef/knife/ssh.rb +34 -4
  74. data/lib/chef/mixin/path_sanity.rb +1 -0
  75. data/lib/chef/monologger.rb +1 -2
  76. data/lib/chef/node.rb +7 -0
  77. data/lib/chef/policy_builder.rb +49 -0
  78. data/lib/chef/policy_builder/expand_node_object.rb +230 -0
  79. data/lib/chef/policy_builder/policyfile.rb +338 -0
  80. data/lib/chef/provider/file.rb +15 -5
  81. data/lib/chef/provider/group.rb +6 -2
  82. data/lib/chef/provider/group/windows.rb +12 -2
  83. data/lib/chef/provider/http_request.rb +3 -2
  84. data/lib/chef/provider/package.rb +1 -0
  85. data/lib/chef/provider/package/aix.rb +1 -1
  86. data/lib/chef/provider/service/debian.rb +7 -2
  87. data/lib/chef/resource/file.rb +8 -1
  88. data/lib/chef/resource/package.rb +9 -0
  89. data/lib/chef/resource/service.rb +0 -1
  90. data/lib/chef/rest.rb +2 -0
  91. data/lib/chef/run_context.rb +1 -1
  92. data/lib/chef/util/file_edit.rb +1 -1
  93. data/lib/chef/util/windows/net_group.rb +7 -6
  94. data/lib/chef/version.rb +1 -1
  95. data/lib/chef/win32/version.rb +31 -18
  96. data/spec/data/cookbooks/preseed/templates/default/preseed-template-variables.seed +1 -0
  97. data/spec/functional/resource/file_spec.rb +0 -1
  98. data/spec/functional/resource/group_spec.rb +96 -16
  99. data/spec/functional/resource/package_spec.rb +17 -0
  100. data/spec/functional/resource/user_spec.rb +2 -2
  101. data/spec/functional/win32/versions_spec.rb +39 -0
  102. data/spec/integration/client/client_spec.rb +27 -28
  103. data/spec/spec_helper.rb +2 -0
  104. data/spec/support/platform_helpers.rb +7 -1
  105. data/spec/support/shared/functional/file_resource.rb +83 -43
  106. data/spec/unit/application_spec.rb +7 -5
  107. data/spec/unit/client_spec.rb +10 -3
  108. data/spec/unit/config_spec.rb +0 -30
  109. data/spec/unit/cookbook_spec.rb +1 -0
  110. data/spec/unit/data_bag_item_spec.rb +8 -0
  111. data/spec/unit/data_bag_spec.rb +6 -0
  112. data/spec/unit/http_spec.rb +48 -0
  113. data/spec/unit/knife/core/subcommand_loader_spec.rb +77 -1
  114. data/spec/unit/knife/ssh_spec.rb +107 -0
  115. data/spec/unit/mixin/path_sanity_spec.rb +6 -0
  116. data/spec/unit/mixin/securable_spec.rb +77 -3
  117. data/spec/unit/monologger_spec.rb +45 -0
  118. data/spec/unit/node_spec.rb +16 -0
  119. data/spec/unit/policy_builder/expand_node_object_spec.rb +320 -0
  120. data/spec/unit/policy_builder/policyfile_spec.rb +399 -0
  121. data/spec/unit/policy_builder_spec.rb +26 -0
  122. data/spec/unit/provider/deploy_spec.rb +3 -0
  123. data/spec/unit/provider/group/windows_spec.rb +1 -0
  124. data/spec/unit/provider/http_request_spec.rb +23 -1
  125. data/spec/unit/provider/service/debian_service_spec.rb +50 -19
  126. data/spec/unit/recipe_spec.rb +4 -0
  127. data/spec/unit/resource/package_spec.rb +5 -0
  128. data/spec/unit/rest_spec.rb +375 -278
  129. data/spec/unit/run_context_spec.rb +4 -0
  130. metadata +96 -59
  131. checksums.yaml +0 -7
@@ -0,0 +1,45 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 Opscode, 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/monologger'
19
+ require 'tempfile'
20
+ require 'spec_helper'
21
+
22
+ describe MonoLogger do
23
+ it "should disable buffering when passed an IO stream" do
24
+ STDOUT.sync = false
25
+ MonoLogger.new(STDOUT)
26
+ STDOUT.sync.should == true
27
+ end
28
+
29
+ describe "when given an object that responds to write and close e.g. IO" do
30
+ it "should use the object directly" do
31
+ stream = StringIO.new
32
+ MonoLogger.new(stream).fatal("Houston, we've had a problem.")
33
+ stream.string.should =~ /Houston, we've had a problem./
34
+ end
35
+ end
36
+
37
+ describe "when given an object that is stringable (to_str)" do
38
+ it "should open a File object with the given path" do
39
+ temp_file = Tempfile.new("rspec-monologger-log")
40
+ temp_file.close
41
+ MonoLogger.new(temp_file.path).fatal("Do, or do not. There is no try.")
42
+ File.read(temp_file.path).should =~ /Do, or do not. There is no try./
43
+ end
44
+ end
45
+ end
@@ -461,6 +461,22 @@ describe Chef::Node do
461
461
  end
462
462
  end
463
463
 
464
+ describe "loaded_recipe" do
465
+ it "should not add a recipe that is already in the recipes list" do
466
+ node.automatic_attrs[:recipes] = [ "nginx::module" ]
467
+ node.loaded_recipe(:nginx, "module")
468
+ expect(node.automatic_attrs[:recipes].length).to eq(1)
469
+ end
470
+
471
+ it "should add a recipe that is not already in the recipes list" do
472
+ node.automatic_attrs[:recipes] = [ "nginx::other_module" ]
473
+ node.loaded_recipe(:nginx, "module")
474
+ expect(node.automatic_attrs[:recipes].length).to eq(2)
475
+ expect(node.recipe?("nginx::module")).to be_true
476
+ expect(node.recipe?("nginx::other_module")).to be_true
477
+ end
478
+ end
479
+
464
480
  describe "when querying for recipes in the run list" do
465
481
  context "when a recipe is in the top level run list" do
466
482
  before do
@@ -0,0 +1,320 @@
1
+ #
2
+ # Author:: Daniel DeLeo (<dan@getchef.com>)
3
+ # Copyright:: Copyright 2014 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'spec_helper'
20
+ require 'chef/policy_builder'
21
+
22
+ describe Chef::PolicyBuilder::ExpandNodeObject do
23
+
24
+ let(:node_name) { "joe_node" }
25
+ let(:ohai_data) { {"platform" => "ubuntu", "platform_version" => "13.04", "fqdn" => "joenode.example.com"} }
26
+ let(:json_attribs) { {"run_list" => []} }
27
+ let(:override_runlist) { "recipe[foo::default]" }
28
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
29
+ let(:policy_builder) { Chef::PolicyBuilder::ExpandNodeObject.new(node_name, ohai_data, json_attribs, override_runlist, events) }
30
+
31
+ # All methods that Chef::Client calls on this class.
32
+ describe "Public API" do
33
+ it "implements a node method" do
34
+ expect(policy_builder).to respond_to(:node)
35
+ end
36
+
37
+ it "implements a load_node method" do
38
+ expect(policy_builder).to respond_to(:load_node)
39
+ end
40
+
41
+ it "implements a build_node method" do
42
+ expect(policy_builder).to respond_to(:build_node)
43
+ end
44
+
45
+ it "implements a setup_run_context method that accepts a list of recipe files to run" do
46
+ expect(policy_builder).to respond_to(:setup_run_context)
47
+ expect(policy_builder.method(:setup_run_context).arity).to eq(-1) #optional argument
48
+ end
49
+
50
+ it "implements a run_context method" do
51
+ expect(policy_builder).to respond_to(:run_context)
52
+ end
53
+
54
+ it "implements an expand_run_list method" do
55
+ expect(policy_builder).to respond_to(:expand_run_list)
56
+ end
57
+
58
+ describe "loading the node" do
59
+
60
+ context "on chef-solo" do
61
+
62
+ before do
63
+ Chef::Config[:solo] = true
64
+ end
65
+
66
+ it "creates a new in-memory node object with the given name" do
67
+ policy_builder.load_node
68
+ policy_builder.node.name.should == node_name
69
+ end
70
+
71
+ end
72
+
73
+ context "on chef-client" do
74
+
75
+ let(:node) { Chef::Node.new.tap { |n| n.name(node_name) } }
76
+
77
+ it "loads or creates a node on the server" do
78
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
79
+ policy_builder.load_node
80
+ policy_builder.node.should == node
81
+ end
82
+
83
+ end
84
+ end
85
+
86
+ describe "building the node" do
87
+
88
+ # XXX: Chef::Client just needs to be able to call this, it doesn't depend on the return value.
89
+ it "builds the node and returns the updated node object" do
90
+ pending
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+
97
+ # Implementation specific tests
98
+
99
+ describe "when first created" do
100
+
101
+ it "has a node_name" do
102
+ expect(policy_builder.node_name).to eq(node_name)
103
+ end
104
+
105
+ it "has ohai data" do
106
+ expect(policy_builder.ohai_data).to eq(ohai_data)
107
+ end
108
+
109
+ it "has a set of attributes from command line option" do
110
+ expect(policy_builder.json_attribs).to eq(json_attribs)
111
+ end
112
+
113
+ it "has an override_runlist" do
114
+ expect(policy_builder.override_runlist).to eq(override_runlist)
115
+ end
116
+
117
+ end
118
+
119
+ context "once the node has been loaded" do
120
+ let(:node) do
121
+ node = Chef::Node.new
122
+ node.name(node_name)
123
+ node.run_list(["recipe[a::default]", "recipe[b::server]"])
124
+ node
125
+ end
126
+
127
+ before do
128
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
129
+ policy_builder.load_node
130
+ end
131
+
132
+ it "expands the run_list" do
133
+ expect(policy_builder.expand_run_list).to be_a(Chef::RunList::RunListExpansion)
134
+ expect(policy_builder.run_list_expansion).to be_a(Chef::RunList::RunListExpansion)
135
+ expect(policy_builder.run_list_expansion.recipes).to eq(["a::default", "b::server"])
136
+ end
137
+
138
+ end
139
+
140
+ describe "building the node" do
141
+
142
+ let(:configured_environment) { nil }
143
+ let(:json_attribs) { nil }
144
+
145
+ let(:override_runlist) { nil }
146
+ let(:primary_runlist) { ["recipe[primary::default]"] }
147
+
148
+ let(:original_default_attrs) { {"default_key" => "default_value"} }
149
+ let(:original_override_attrs) { {"override_key" => "override_value"} }
150
+
151
+ let(:node) do
152
+ node = Chef::Node.new
153
+ node.name(node_name)
154
+ node.default_attrs = original_default_attrs
155
+ node.override_attrs = original_override_attrs
156
+ node.run_list(primary_runlist)
157
+ node
158
+ end
159
+
160
+ before do
161
+ Chef::Config[:environment] = configured_environment
162
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
163
+ policy_builder.load_node
164
+ policy_builder.build_node
165
+ end
166
+
167
+ it "sanity checks test setup" do
168
+ expect(node.run_list).to eq(primary_runlist)
169
+ end
170
+
171
+ it "clears existing default and override attributes from the node" do
172
+ expect(node["default_key"]).to be_nil
173
+ expect(node["override_key"]).to be_nil
174
+ end
175
+
176
+ it "applies ohai data to the node" do
177
+ expect(node["fqdn"]).to eq(ohai_data["fqdn"])
178
+ end
179
+
180
+ describe "when the given run list is not in expanded form" do
181
+
182
+ # NOTE: for chef-client, the behavior is always to expand the run list,
183
+ # but this operation is a no-op when none of the run list items are
184
+ # roles. Because of the amount of mocking required to make this work in
185
+ # tests, this test is isolated from the others.
186
+
187
+ let(:primary_runlist) { ["role[some_role]"] }
188
+ let(:expansion) do
189
+ recipe_list = Chef::RunList::VersionedRecipeList.new
190
+ recipe_list.add_recipe("recipe[from_role::default", "1.0.2")
191
+ double("RunListExpansion", :recipes => recipe_list)
192
+ end
193
+
194
+ let(:node) do
195
+ node = Chef::Node.new
196
+ node.name(node_name)
197
+ node.default_attrs = original_default_attrs
198
+ node.override_attrs = original_override_attrs
199
+ node.run_list(primary_runlist)
200
+
201
+ node.should_receive(:expand!).with("server") do
202
+ node.run_list("recipe[from_role::default]")
203
+ expansion
204
+ end
205
+
206
+ node
207
+ end
208
+
209
+ it "expands run list items via the server API" do
210
+ expect(node.run_list).to eq(["recipe[from_role::default]"])
211
+ end
212
+
213
+ end
214
+
215
+ context "when JSON attributes are given on the command line" do
216
+
217
+ let(:json_attribs) { {"run_list" => ["recipe[json_attribs::default]"], "json_attribs_key" => "json_attribs_value" } }
218
+
219
+ it "sets the run list according to the given JSON" do
220
+ expect(node.run_list).to eq(["recipe[json_attribs::default]"])
221
+ end
222
+
223
+ it "sets node attributes according to the given JSON" do
224
+ expect(node["json_attribs_key"]).to eq("json_attribs_value")
225
+ end
226
+
227
+ end
228
+
229
+ context "when an override_runlist is given" do
230
+
231
+ let(:override_runlist) { "recipe[foo::default]" }
232
+
233
+ it "sets the override run_list on the node" do
234
+ expect(node.run_list).to eq([override_runlist])
235
+ expect(policy_builder.original_runlist).to eq(primary_runlist)
236
+ end
237
+
238
+ end
239
+
240
+ context "when no environment is specified" do
241
+
242
+ it "does not set the environment" do
243
+ expect(node.chef_environment).to eq("_default")
244
+ end
245
+
246
+ end
247
+
248
+ context "when a custom environment is configured" do
249
+
250
+ let(:configured_environment) { environment.name }
251
+
252
+ let(:environment) do
253
+ environment = Chef::Environment.new.tap {|e| e.name("prod") }
254
+ Chef::Environment.should_receive(:load).with("prod").and_return(environment)
255
+ environment
256
+ end
257
+
258
+ it "sets the environment as configured" do
259
+ expect(node.chef_environment).to eq(environment.name)
260
+ end
261
+ end
262
+
263
+ end
264
+
265
+ describe "configuring the run_context" do
266
+ let(:json_attribs) { nil }
267
+ let(:override_runlist) { nil }
268
+
269
+ let(:node) do
270
+ node = Chef::Node.new
271
+ node.name(node_name)
272
+ node.run_list("recipe[first::default]", "recipe[second::default]")
273
+ node
274
+ end
275
+
276
+ let(:chef_http) { double("Chef::REST") }
277
+
278
+ let(:cookbook_resolve_url) { "environments/#{node.chef_environment}/cookbook_versions" }
279
+ let(:cookbook_resolve_post_data) { {:run_list=>["first::default", "second::default"]} }
280
+
281
+ # cookbook_hash is just a hash, but since we're passing it between mock
282
+ # objects, we get a little better test strictness by using a double (which
283
+ # will have object equality rather than semantic equality #== semantics).
284
+ let(:cookbook_hash) { double("cookbook hash", :each => nil) }
285
+
286
+ let(:cookbook_synchronizer) { double("CookbookSynchronizer") }
287
+
288
+ before do
289
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
290
+
291
+ policy_builder.stub(:api_service).and_return(chef_http)
292
+
293
+ policy_builder.load_node
294
+ policy_builder.build_node
295
+
296
+ run_list_expansion = policy_builder.run_list_expansion
297
+
298
+ chef_http.should_receive(:post).with(cookbook_resolve_url, cookbook_resolve_post_data).and_return(cookbook_hash)
299
+ Chef::CookbookSynchronizer.should_receive(:new).with(cookbook_hash, events).and_return(cookbook_synchronizer)
300
+ cookbook_synchronizer.should_receive(:sync_cookbooks)
301
+
302
+ Chef::RunContext.any_instance.should_receive(:load).with(run_list_expansion)
303
+
304
+ policy_builder.setup_run_context
305
+ end
306
+
307
+ it "configures FileVendor to fetch files remotely" do
308
+ manifest = double("cookbook manifest")
309
+ Chef::Cookbook::RemoteFileVendor.should_receive(:new).with(manifest, chef_http)
310
+ Chef::Cookbook::FileVendor.create_from_manifest(manifest)
311
+ end
312
+
313
+ it "triggers cookbook compilation in the run_context" do
314
+ # Test condition already covered by `Chef::RunContext.any_instance.should_receive(:load).with(run_list_expansion)`
315
+ end
316
+
317
+ end
318
+
319
+ end
320
+
@@ -0,0 +1,399 @@
1
+ #
2
+ # Author:: Daniel DeLeo (<dan@getchef.com>)
3
+ # Copyright:: Copyright 2014 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'spec_helper'
20
+ require 'chef/policy_builder'
21
+
22
+ describe Chef::PolicyBuilder::Policyfile do
23
+
24
+ let(:node_name) { "joe_node" }
25
+ let(:ohai_data) { {"platform" => "ubuntu", "platform_version" => "13.04", "fqdn" => "joenode.example.com"} }
26
+ let(:json_attribs) { {"custom_attr" => "custom_attr_value"} }
27
+ let(:override_runlist) { nil }
28
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
29
+ let(:policy_builder) { Chef::PolicyBuilder::Policyfile.new(node_name, ohai_data, json_attribs, override_runlist, events) }
30
+
31
+ # Convert a SHA1 (160 bit) hex string into an x.y.z version number where the
32
+ # maximum value is smaller than a postgres BIGINT (signed 64bit, so 63 usable
33
+ # bits). This requires enterprise Chef or open source server 11.1.0+ (currently not released)
34
+ #
35
+ # The SHA1 is devided as follows:
36
+ # * "major": first 14 chars (56 bits)
37
+ # * "minor": next 14 chars (56 bits)
38
+ # * "patch": last 12 chars (48 bits)
39
+ def id_to_dotted(sha1_id)
40
+ major = sha1_id[0...14]
41
+ minor = sha1_id[14...28]
42
+ patch = sha1_id[28..40]
43
+ decimal_integers =[major, minor, patch].map {|hex| hex.to_i(16) }
44
+ decimal_integers.join(".")
45
+ end
46
+
47
+
48
+ let(:example1_lock_data) do
49
+ # based on https://github.com/danielsdeleo/chef-workflow2-prototype/blob/master/skeletons/basic_policy/Policyfile.lock.json
50
+ {
51
+ "identifier" => "168d2102fb11c9617cd8a981166c8adc30a6e915",
52
+ "version" => "2.3.5",
53
+ # NOTE: for compatibility mode we include the dotted id in the policyfile to enhance discoverability.
54
+ "dotted_decimal_identifier" => id_to_dotted("168d2102fb11c9617cd8a981166c8adc30a6e915"),
55
+ "source" => { "path" => "./cookbooks/demo" },
56
+ "scm_identifier"=> {
57
+ "vcs"=> "git",
58
+ "rev_id"=> "9d5b09026470c322c3cb5ca8a4157c4d2f16cef3",
59
+ "remote"=> nil
60
+ }
61
+ }
62
+ end
63
+
64
+ let(:example2_lock_data) do
65
+ {
66
+ "identifier" => "feab40e1fca77c7360ccca1481bb8ba5f919ce3a",
67
+ "version" => "4.2.0",
68
+ # NOTE: for compatibility mode we include the dotted id in the policyfile to enhance discoverability.
69
+ "dotted_decimal_identifier" => id_to_dotted("feab40e1fca77c7360ccca1481bb8ba5f919ce3a"),
70
+ "source" => { "api" => "https://community.getchef.com/api/v1/cookbooks/example2" }
71
+ }
72
+ end
73
+
74
+ let(:policyfile_default_attributes) { {"policyfile_default_attr" => "policyfile_default_value"} }
75
+ let(:policyfile_override_attributes) { {"policyfile_override_attr" => "policyfile_override_value"} }
76
+
77
+ let(:policyfile_run_list) { ["recipe[example1::default]", "recipe[example2::server]"] }
78
+
79
+ let(:parsed_policyfile_json) do
80
+ {
81
+ "run_list" => policyfile_run_list,
82
+
83
+ "cookbook_locks" => {
84
+ "example1" => example1_lock_data,
85
+ "example2" => example2_lock_data
86
+ },
87
+
88
+ "default_attributes" => policyfile_default_attributes,
89
+ "override_attributes" => policyfile_override_attributes
90
+ }
91
+ end
92
+
93
+ let(:err_namespace) { Chef::PolicyBuilder::Policyfile }
94
+
95
+ it "configures a Chef HTTP API client" do
96
+ http = double("Chef::REST")
97
+ server_url = "https://api.opscode.com/organizations/example"
98
+ Chef::Config[:chef_server_url] = server_url
99
+ Chef::REST.should_receive(:new).with(server_url).and_return(http)
100
+ expect(policy_builder.http_api).to eq(http)
101
+ end
102
+
103
+ describe "reporting unsupported features" do
104
+
105
+ def initialize_pb
106
+ Chef::PolicyBuilder::Policyfile.new(node_name, ohai_data, json_attribs, override_runlist, events)
107
+ end
108
+
109
+ context "chef-solo" do
110
+ before { Chef::Config[:solo] = true }
111
+
112
+ it "errors on create" do
113
+ expect { initialize_pb }.to raise_error(err_namespace::UnsupportedFeature)
114
+ end
115
+ end
116
+
117
+ context "when given an override run_list" do
118
+ let(:override_runlist) { "recipe[foo],recipe[bar]" }
119
+
120
+ it "errors on create" do
121
+ expect { initialize_pb }.to raise_error(err_namespace::UnsupportedFeature)
122
+ end
123
+ end
124
+
125
+ context "when json_attribs contains a run_list" do
126
+ let(:json_attribs) { {"run_list" => []} }
127
+
128
+ it "errors on create" do
129
+ expect { initialize_pb }.to raise_error(err_namespace::UnsupportedFeature)
130
+ end
131
+ end
132
+
133
+ context "when an environment is configured" do
134
+ before { Chef::Config[:environment] = "blurch" }
135
+
136
+ it "errors when an environment is configured" do
137
+ expect { initialize_pb }.to raise_error(err_namespace::UnsupportedFeature)
138
+ end
139
+ end
140
+
141
+ end
142
+
143
+ describe "when using compatibility mode" do
144
+
145
+ let(:http_api) { double("Chef::REST") }
146
+
147
+ let(:configured_environment) { nil }
148
+
149
+ let(:override_runlist) { nil }
150
+ let(:primary_runlist) { nil }
151
+
152
+ let(:original_default_attrs) { {"default_key" => "default_value"} }
153
+ let(:original_override_attrs) { {"override_key" => "override_value"} }
154
+
155
+ let(:node) do
156
+ node = Chef::Node.new
157
+ node.name(node_name)
158
+ node.default_attrs = original_default_attrs
159
+ node.override_attrs = original_override_attrs
160
+ node.run_list(primary_runlist) if primary_runlist
161
+ node
162
+ end
163
+
164
+ before do
165
+ # TODO: agree on this name and logic.
166
+ Chef::Config[:deployment_group] = "example-policy-stage"
167
+ policy_builder.stub(:http_api).and_return(http_api)
168
+ end
169
+
170
+ context "when the deployment group cannot be loaded" do
171
+ let(:error404) { Net::HTTPServerException.new("404 message", :body) }
172
+
173
+ before do
174
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
175
+ http_api.should_receive(:get).
176
+ with("data/policyfiles/example-policy-stage").
177
+ and_raise(error404)
178
+ end
179
+
180
+ it "raises an error" do
181
+ expect { policy_builder.load_node }.to raise_error(err_namespace::ConfigurationError)
182
+ end
183
+
184
+ it "sends error message to the event system" do
185
+ events.should_receive(:node_load_failed).with(node_name, an_instance_of(err_namespace::ConfigurationError), Chef::Config)
186
+ expect { policy_builder.load_node }.to raise_error(err_namespace::ConfigurationError)
187
+ end
188
+
189
+ end
190
+
191
+ describe "when the deployment_group is not configured" do
192
+ before do
193
+ Chef::Config[:deployment_group] = nil
194
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
195
+ end
196
+
197
+ it "errors while loading the node" do
198
+ expect { policy_builder.load_node }.to raise_error(err_namespace::ConfigurationError)
199
+ end
200
+
201
+
202
+ it "passes error information to the event system" do
203
+ # TODO: also make sure something acceptable happens with the error formatters
204
+ err_class = err_namespace::ConfigurationError
205
+ events.should_receive(:node_load_failed).with(node_name, an_instance_of(err_class), Chef::Config)
206
+ expect { policy_builder.load_node }.to raise_error(err_class)
207
+ end
208
+ end
209
+
210
+ context "and a deployment_group is configured" do
211
+ before do
212
+ http_api.should_receive(:get).with("data/policyfiles/example-policy-stage").and_return(parsed_policyfile_json)
213
+ end
214
+
215
+ it "fetches the policy file from a data bag item" do
216
+ expect(policy_builder.policy).to eq(parsed_policyfile_json)
217
+ end
218
+
219
+ it "extracts the run_list from the policyfile" do
220
+ expect(policy_builder.run_list).to eq(policyfile_run_list)
221
+ end
222
+
223
+ it "extracts the cookbooks and versions for display from the policyfile" do
224
+ expected = [
225
+ "example1::default@2.3.5 (168d210)",
226
+ "example2::server@4.2.0 (feab40e)"
227
+ ]
228
+
229
+ expect(policy_builder.run_list_with_versions_for_display).to eq(expected)
230
+ end
231
+
232
+ it "generates a RunListExpansion-alike object for feeding to the CookbookCompiler" do
233
+ expect(policy_builder.run_list_expansion_ish).to respond_to(:recipes)
234
+ expect(policy_builder.run_list_expansion_ish.recipes).to eq(["example1::default", "example2::server"])
235
+ end
236
+
237
+ it "implements #expand_run_list in a manner compatible with ExpandNodeObject" do
238
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
239
+ policy_builder.load_node
240
+ expect(policy_builder.expand_run_list).to respond_to(:recipes)
241
+ expect(policy_builder.expand_run_list.recipes).to eq(["example1::default", "example2::server"])
242
+ expect(policy_builder.expand_run_list.roles).to eq([])
243
+ end
244
+
245
+
246
+ describe "validating the Policyfile.lock" do
247
+
248
+ it "errors if the policyfile json contains any non-recipe items" do
249
+ parsed_policyfile_json["run_list"] = ["role[foo]"]
250
+ expect { policy_builder.validate_policyfile }.to raise_error(err_namespace::PolicyfileError)
251
+ end
252
+
253
+ it "errors if the policyfile json contains non-fully qualified recipe items" do
254
+ parsed_policyfile_json["run_list"] = ["recipe[foo]"]
255
+ expect { policy_builder.validate_policyfile }.to raise_error(err_namespace::PolicyfileError)
256
+ end
257
+
258
+ it "errors if the policyfile doesn't have a run_list key" do
259
+ parsed_policyfile_json.delete("run_list")
260
+ expect { policy_builder.validate_policyfile }.to raise_error(err_namespace::PolicyfileError)
261
+ end
262
+
263
+ it "error if the policyfile doesn't have a cookbook_locks key" do
264
+ parsed_policyfile_json.delete("cookbook_locks")
265
+ expect { policy_builder.validate_policyfile }.to raise_error(err_namespace::PolicyfileError)
266
+ end
267
+
268
+ it "accepts a valid policyfile" do
269
+ policy_builder.validate_policyfile
270
+ end
271
+
272
+ end
273
+
274
+ describe "building the node object" do
275
+
276
+ before do
277
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
278
+
279
+ policy_builder.load_node
280
+ policy_builder.build_node
281
+ end
282
+
283
+ it "resets default and override data" do
284
+ expect(node["default_key"]).to be_nil
285
+ expect(node["override_key"]).to be_nil
286
+ end
287
+
288
+ it "applies ohai data" do
289
+ expect(ohai_data).to_not be_empty # ensure test is testing something
290
+ ohai_data.each do |key, value|
291
+ expect(node.automatic_attrs[key]).to eq(value)
292
+ end
293
+ end
294
+
295
+ it "applies attributes from json file" do
296
+ expect(node["custom_attr"]).to eq("custom_attr_value")
297
+ end
298
+
299
+ it "applies attributes from the policyfile" do
300
+ expect(node["policyfile_default_attr"]).to eq("policyfile_default_value")
301
+ expect(node["policyfile_override_attr"]).to eq("policyfile_override_value")
302
+ end
303
+
304
+ it "sets the policyfile's run_list on the node object" do
305
+ expect(node.run_list).to eq(policyfile_run_list)
306
+ end
307
+
308
+ it "creates node.automatic_attrs[:roles]" do
309
+ expect(node.automatic_attrs[:roles]).to eq([])
310
+ end
311
+
312
+ it "create node.automatic_attrs[:recipes]" do
313
+ expect(node.automatic_attrs[:recipes]).to eq(["example1::default", "example2::server"])
314
+ end
315
+
316
+ end
317
+
318
+
319
+ describe "fetching the desired cookbook set" do
320
+
321
+ let(:example1_cookbook_object) { double("Chef::CookbookVersion for example1 cookbook") }
322
+ let(:example2_cookbook_object) { double("Chef::CookbookVersion for example2 cookbook") }
323
+
324
+ let(:expected_cookbook_hash) do
325
+ { "example1" => example1_cookbook_object, "example2" => example2_cookbook_object }
326
+ end
327
+
328
+ let(:example1_xyz_version) { example1_lock_data["dotted_decimal_identifier"] }
329
+ let(:example2_xyz_version) { example2_lock_data["dotted_decimal_identifier"] }
330
+
331
+ let(:cookbook_synchronizer) { double("Chef::CookbookSynchronizer") }
332
+
333
+ context "and a cookbook is missing" do
334
+
335
+ let(:error404) { Net::HTTPServerException.new("404 message", :body) }
336
+
337
+ before do
338
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
339
+
340
+ # Remove references to example2 cookbook because we're iterating
341
+ # over a Hash data structure and on ruby 1.8.7 iteration order will
342
+ # not be stable.
343
+ parsed_policyfile_json["cookbook_locks"].delete("example2")
344
+ parsed_policyfile_json["run_list"].delete("recipe[example2::server]")
345
+
346
+ policy_builder.load_node
347
+ policy_builder.build_node
348
+
349
+ http_api.should_receive(:get).with("cookbooks/example1/#{example1_xyz_version}").
350
+ and_raise(error404)
351
+ end
352
+
353
+ it "raises an error indicating which cookbook is missing" do
354
+ expect { policy_builder.cookbooks_to_sync }.to raise_error(Chef::Exceptions::CookbookNotFound)
355
+ end
356
+
357
+ end
358
+
359
+ context "and the cookbooks can be fetched" do
360
+ before do
361
+ Chef::Node.should_receive(:find_or_create).with(node_name).and_return(node)
362
+
363
+ policy_builder.load_node
364
+ policy_builder.build_node
365
+
366
+ http_api.should_receive(:get).with("cookbooks/example1/#{example1_xyz_version}").
367
+ and_return(example1_cookbook_object)
368
+ http_api.should_receive(:get).with("cookbooks/example2/#{example2_xyz_version}").
369
+ and_return(example2_cookbook_object)
370
+
371
+ Chef::CookbookSynchronizer.stub(:new).
372
+ with(expected_cookbook_hash, events).
373
+ and_return(cookbook_synchronizer)
374
+ end
375
+
376
+ it "builds a Hash of the form 'cookbook_name' => Chef::CookbookVersion" do
377
+ expect(policy_builder.cookbooks_to_sync).to eq(expected_cookbook_hash)
378
+ end
379
+
380
+ it "syncs the desired cookbooks via CookbookSynchronizer" do
381
+ cookbook_synchronizer.should_receive(:sync_cookbooks)
382
+ policy_builder.sync_cookbooks
383
+ end
384
+
385
+ it "builds a run context" do
386
+ cookbook_synchronizer.should_receive(:sync_cookbooks)
387
+ Chef::RunContext.any_instance.should_receive(:load).with(policy_builder.run_list_expansion_ish)
388
+ run_context = policy_builder.setup_run_context
389
+ expect(run_context.node).to eq(node)
390
+ expect(run_context.cookbook_collection.keys).to match_array(["example1", "example2"])
391
+ end
392
+
393
+ end
394
+ end
395
+ end
396
+
397
+ end
398
+
399
+ end