chef-dk 0.5.0.rc.1 → 0.5.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/README.md +63 -24
  3. data/lib/chef-dk/builtin_commands.rb +2 -0
  4. data/lib/chef-dk/command/diff.rb +312 -0
  5. data/lib/chef-dk/command/push.rb +1 -1
  6. data/lib/chef-dk/command/shell_init.rb +21 -3
  7. data/lib/chef-dk/command/update.rb +28 -5
  8. data/lib/chef-dk/configurable.rb +1 -1
  9. data/lib/chef-dk/exceptions.rb +3 -0
  10. data/lib/chef-dk/pager.rb +106 -0
  11. data/lib/chef-dk/policyfile/chef_repo_cookbook_source.rb +114 -0
  12. data/lib/chef-dk/policyfile/comparison_base.rb +124 -0
  13. data/lib/chef-dk/policyfile/cookbook_sources.rb +1 -0
  14. data/lib/chef-dk/policyfile/differ.rb +266 -0
  15. data/lib/chef-dk/policyfile/dsl.rb +26 -3
  16. data/lib/chef-dk/policyfile/uploader.rb +4 -5
  17. data/lib/chef-dk/policyfile_compiler.rb +8 -0
  18. data/lib/chef-dk/policyfile_lock.rb +135 -3
  19. data/lib/chef-dk/policyfile_services/install.rb +1 -0
  20. data/lib/chef-dk/policyfile_services/update_attributes.rb +104 -0
  21. data/lib/chef-dk/service_exceptions.rb +12 -0
  22. data/lib/chef-dk/ui.rb +8 -0
  23. data/lib/chef-dk/version.rb +1 -1
  24. data/spec/spec_helper.rb +6 -0
  25. data/spec/test_helpers.rb +4 -0
  26. data/spec/unit/command/diff_spec.rb +283 -0
  27. data/spec/unit/command/shell_init_spec.rb +19 -2
  28. data/spec/unit/command/update_spec.rb +96 -0
  29. data/spec/unit/command/verify_spec.rb +0 -6
  30. data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/Berksfile +3 -0
  31. data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/README.md +4 -0
  32. data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/chefignore +96 -0
  33. data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/metadata.rb +9 -0
  34. data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/recipes/default.rb +8 -0
  35. data/spec/unit/pager_spec.rb +119 -0
  36. data/spec/unit/policyfile/chef_repo_cookbook_source_spec.rb +66 -0
  37. data/spec/unit/policyfile/comparison_base_spec.rb +343 -0
  38. data/spec/unit/policyfile/differ_spec.rb +687 -0
  39. data/spec/unit/policyfile_evaluation_spec.rb +87 -0
  40. data/spec/unit/policyfile_lock_build_spec.rb +247 -8
  41. data/spec/unit/policyfile_lock_serialization_spec.rb +47 -0
  42. data/spec/unit/policyfile_services/export_repo_spec.rb +2 -0
  43. data/spec/unit/policyfile_services/push_spec.rb +2 -0
  44. data/spec/unit/policyfile_services/update_attributes_spec.rb +217 -0
  45. metadata +62 -6
@@ -18,3 +18,4 @@
18
18
  require 'chef-dk/policyfile/null_cookbook_source'
19
19
  require 'chef-dk/policyfile/community_cookbook_source'
20
20
  require 'chef-dk/policyfile/chef_server_cookbook_source'
21
+ require 'chef-dk/policyfile/chef_repo_cookbook_source'
@@ -0,0 +1,266 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2015 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 'diff/lcs'
19
+ require 'diff/lcs/hunk'
20
+ require 'paint'
21
+ require 'ffi_yajl'
22
+
23
+ module ChefDK
24
+ module Policyfile
25
+ class Differ
26
+
27
+ POLICY_SECTIONS = %w{ revision_id run_list named_run_lists cookbook_locks default_attributes override_attributes }.freeze
28
+ LINES_OF_CONTEXT = 3
29
+ INITIAL_FILE_LENGTH_DIFFERENCE = 0
30
+ FORMAT = :unified
31
+
32
+
33
+ attr_reader :old_lock
34
+ attr_reader :old_name
35
+ attr_reader :new_lock
36
+ attr_reader :new_name
37
+ attr_reader :ui
38
+
39
+ def initialize(old_name: nil, old_lock: nil, new_name: nil, new_lock: nil, ui: nil)
40
+ @old_lock = old_lock
41
+ @new_lock = new_lock
42
+ @old_name = old_name
43
+ @new_name = new_name
44
+ @ui = ui
45
+
46
+ @added_cookbooks = nil
47
+ @removed_cookbooks = nil
48
+ @modified_cookbooks = nil
49
+ end
50
+
51
+ def lock_name
52
+ old_lock["name"]
53
+ end
54
+
55
+ def old_cookbook_locks
56
+ old_lock["cookbook_locks"]
57
+ end
58
+
59
+ def new_cookbook_locks
60
+ new_lock["cookbook_locks"]
61
+ end
62
+
63
+ def updated_sections
64
+ @updated_sections ||= POLICY_SECTIONS.select do |key|
65
+ old_lock[key] != new_lock[key]
66
+ end
67
+ end
68
+
69
+ def different?
70
+ !updated_sections.empty?
71
+ end
72
+
73
+ def run_report
74
+ unless different?
75
+ ui.err("No changes for policy lock '#{lock_name}' between '#{old_name}' and '#{new_name}'")
76
+ return true
77
+ end
78
+
79
+ ui.print("Policy lock '#{lock_name}' differs between '#{old_name}' and '#{new_name}':\n\n")
80
+
81
+ report_rev_id_changes
82
+ report_run_list_changes
83
+ report_added_cookbooks
84
+ report_removed_cookbooks
85
+ report_modified_cookbooks
86
+ report_default_attribute_changes
87
+ report_override_attribute_changes
88
+ end
89
+
90
+ def report_rev_id_changes
91
+ h1('REVISION ID CHANGED')
92
+ old_rev = old_lock["revision_id"]
93
+ new_rev = new_lock["revision_id"]
94
+ diff_lines([ old_rev ], [ new_rev ])
95
+ end
96
+
97
+ def report_run_list_changes
98
+ return nil unless updated_sections.include?("run_list")
99
+ h1("RUN LIST CHANGED")
100
+
101
+ old_run_list = old_lock["run_list"]
102
+ new_run_list = new_lock["run_list"]
103
+
104
+ diff_lines(old_run_list, new_run_list)
105
+ end
106
+
107
+ def report_removed_cookbooks
108
+ return nil if removed_cookbooks.empty?
109
+ h1("REMOVED COOKBOOKS")
110
+ removed_cookbooks.each do |cb_name|
111
+ ui.print("\n")
112
+ old_lock = pretty_json(old_cookbook_locks[cb_name])
113
+ new_lock = []
114
+ h2(cb_name)
115
+ diff_lines(old_lock, new_lock)
116
+ end
117
+ end
118
+
119
+ def report_added_cookbooks
120
+ return nil if added_cookbooks.empty?
121
+ h1("ADDED COOKBOOKS")
122
+ added_cookbooks.each do |cb_name|
123
+ ui.print("\n")
124
+ old_lock = []
125
+ new_lock = pretty_json(new_cookbook_locks[cb_name])
126
+ h2(cb_name)
127
+ diff_lines(old_lock, new_lock)
128
+ end
129
+ end
130
+
131
+ def report_modified_cookbooks
132
+ return nil if modified_cookbooks.empty?
133
+ h1("MODIFIED COOKBOOKS")
134
+ modified_cookbooks.each do |cb_name|
135
+ ui.print("\n")
136
+ old_lock = pretty_json(old_cookbook_locks[cb_name])
137
+ new_lock = pretty_json(new_cookbook_locks[cb_name])
138
+ h2(cb_name)
139
+ diff_lines(old_lock, new_lock)
140
+ end
141
+ end
142
+
143
+ def report_default_attribute_changes
144
+ return nil unless updated_sections.include?("default_attributes")
145
+
146
+ h1("DEFAULT ATTRIBUTES CHANGED")
147
+
148
+ old_default = pretty_json(old_lock["default_attributes"])
149
+ new_default = pretty_json(new_lock["default_attributes"])
150
+ diff_lines(old_default, new_default)
151
+ end
152
+
153
+ def report_override_attribute_changes
154
+ return nil unless updated_sections.include?("override_attributes")
155
+
156
+ h1("OVERRIDE ATTRIBUTES CHANGED")
157
+
158
+ old_override = pretty_json(old_lock["override_attributes"])
159
+ new_override = pretty_json(new_lock["override_attributes"])
160
+ diff_lines(old_override, new_override)
161
+ end
162
+
163
+ def added_cookbooks
164
+ detect_cookbook_changes if @added_cookbooks.nil?
165
+ @added_cookbooks
166
+ end
167
+
168
+ def removed_cookbooks
169
+ detect_cookbook_changes if @removed_cookbooks.nil?
170
+ @removed_cookbooks
171
+ end
172
+
173
+ def modified_cookbooks
174
+ detect_cookbook_changes if @modified_cookbooks.nil?
175
+ @modified_cookbooks
176
+ end
177
+
178
+ private
179
+
180
+ def h1(str)
181
+ ui.msg(str)
182
+ ui.msg('=' * str.size)
183
+ end
184
+
185
+ def h2(str)
186
+ ui.msg(str)
187
+ ui.msg('-' * str.size)
188
+ end
189
+
190
+ def diff_lines(old_lines, new_lines)
191
+ file_length_difference = INITIAL_FILE_LENGTH_DIFFERENCE
192
+
193
+ previous_hunk = nil
194
+
195
+ diffs = Diff::LCS.diff(old_lines, new_lines)
196
+
197
+ ui.print("\n")
198
+
199
+ diffs.each do |piece|
200
+ hunk = Diff::LCS::Hunk.new(old_lines, new_lines, piece, LINES_OF_CONTEXT, file_length_difference)
201
+
202
+ file_length_difference = hunk.file_length_difference
203
+
204
+ maybe_contiguous_hunks = (previous_hunk.nil? || hunk.merge(previous_hunk))
205
+
206
+ if !maybe_contiguous_hunks
207
+ print_color_diff("#{previous_hunk.diff(FORMAT)}\n")
208
+ end
209
+ previous_hunk = hunk
210
+ end
211
+ print_color_diff("#{previous_hunk.diff(FORMAT)}\n") unless previous_hunk.nil?
212
+ ui.print("\n")
213
+ end
214
+
215
+ def print_color_diff(hunk)
216
+ hunk.to_s.each_line do |line|
217
+ ui.print(Paint[line, color_for_line(line)])
218
+ end
219
+ end
220
+
221
+ def color_for_line(line)
222
+ case line[0].chr
223
+ when "+"
224
+ :green
225
+ when "-"
226
+ :red
227
+ when "@"
228
+ line[1].chr == "@" ? :blue : nil
229
+ else
230
+ nil
231
+ end
232
+ end
233
+
234
+ def pretty_json(data)
235
+ FFI_Yajl::Encoder.encode(data, pretty: true).lines.map { |l| l.chomp }
236
+ end
237
+
238
+ def detect_cookbook_changes
239
+ all_locked_cookbooks = old_cookbook_locks.keys | new_cookbook_locks.keys
240
+
241
+ @added_cookbooks = []
242
+ @removed_cookbooks = []
243
+ @modified_cookbooks = []
244
+
245
+ all_locked_cookbooks.each do |cb_name|
246
+ if old_cookbook_locks.key?(cb_name) && new_cookbook_locks.key?(cb_name)
247
+ old_cb_lock = old_cookbook_locks[cb_name]
248
+ new_cb_lock = new_cookbook_locks[cb_name]
249
+ if old_cb_lock != new_cb_lock
250
+ @modified_cookbooks << cb_name
251
+ end
252
+ elsif old_cookbook_locks.key?(cb_name)
253
+ @removed_cookbooks << cb_name
254
+ elsif new_cookbook_locks.key?(cb_name)
255
+ @added_cookbooks << cb_name
256
+ else
257
+ raise "Bug: cookbook lock #{cb_name} cannot be determined as new/removed/modified/unmodified"
258
+ end
259
+ end
260
+ end
261
+
262
+
263
+ end
264
+ end
265
+ end
266
+
@@ -19,6 +19,8 @@ require 'chef-dk/policyfile/cookbook_sources'
19
19
  require 'chef-dk/policyfile/cookbook_location_specification'
20
20
  require 'chef-dk/policyfile/storage_config'
21
21
 
22
+ require 'chef/node/attribute'
23
+
22
24
  module ChefDK
23
25
  module Policyfile
24
26
  class DSL
@@ -33,6 +35,7 @@ module ChefDK
33
35
  attr_reader :cookbook_location_specs
34
36
 
35
37
  attr_reader :named_run_lists
38
+ attr_reader :node_attributes
36
39
 
37
40
  attr_reader :storage_config
38
41
 
@@ -44,6 +47,8 @@ module ChefDK
44
47
  @default_source = NullCookbookSource.new
45
48
  @cookbook_location_specs = {}
46
49
  @storage_config = storage_config
50
+
51
+ @node_attributes = Chef::Node::Attribute.new({}, {}, {}, {})
47
52
  end
48
53
 
49
54
  def name(name = nil)
@@ -63,13 +68,15 @@ module ChefDK
63
68
  @named_run_lists[name] = run_list_items.flatten
64
69
  end
65
70
 
66
- def default_source(source_type = nil, source_uri = nil)
71
+ def default_source(source_type = nil, source_argument = nil)
67
72
  return @default_source if source_type.nil?
68
73
  case source_type
69
74
  when :community
70
- set_default_community_source(source_uri)
75
+ set_default_community_source(source_argument)
71
76
  when :chef_server
72
- set_default_chef_server_source(source_uri)
77
+ set_default_chef_server_source(source_argument)
78
+ when :chef_repo
79
+ set_default_chef_repo_source(source_argument)
73
80
  else
74
81
  @errors << "Invalid default_source type '#{source_type.inspect}'"
75
82
  end
@@ -98,6 +105,14 @@ module ChefDK
98
105
  end
99
106
  end
100
107
 
108
+ def default
109
+ @node_attributes.default
110
+ end
111
+
112
+ def override
113
+ @node_attributes.override
114
+ end
115
+
101
116
  def eval_policyfile(policyfile_string)
102
117
  @policyfile_filename = policyfile_filename
103
118
  instance_eval(policyfile_string, policyfile_filename)
@@ -136,6 +151,14 @@ module ChefDK
136
151
  end
137
152
  end
138
153
 
154
+ def set_default_chef_repo_source(path)
155
+ if path.nil?
156
+ @errors << "You must specify the path to the chef-repo when using a default_source :chef_repo"
157
+ else
158
+ @default_source = ChefRepoCookbookSource.new(path)
159
+ end
160
+ end
161
+
139
162
  def validate!
140
163
  if @run_list.empty?
141
164
  @errors << "Invalid run_list. run_list cannot be empty"
@@ -51,13 +51,12 @@ module ChefDK
51
51
  def upload
52
52
  ui.msg("Uploading policy to policy group #{policy_group}")
53
53
 
54
- if using_policy_document_native_api?
54
+ if !using_policy_document_native_api?
55
55
  ui.msg(<<-DRAGONS)
56
- WARN: Using native policy API preview mode. You may be required to delete and
57
- re-upload this data when upgrading to the final release version of the feature.
56
+ WARN: Uploading policy to policy group #{policy_group} in compatibility mode.
57
+ Cookbooks will be uploaded with very large version numbers, which may be picked
58
+ up by existing nodes.
58
59
  DRAGONS
59
- else
60
- ui.msg("WARN: Uploading policy to policy group #{policy_group} in compatibility mode")
61
60
  end
62
61
 
63
62
  upload_cookbooks
@@ -97,6 +97,14 @@ module ChefDK
97
97
  end
98
98
  end
99
99
 
100
+ def default_attributes
101
+ dsl.node_attributes.combined_default.to_hash
102
+ end
103
+
104
+ def override_attributes
105
+ dsl.node_attributes.combined_override.to_hash
106
+ end
107
+
100
108
  def lock
101
109
  @policyfile_lock ||= PolicyfileLock.build_from_compiler(self, storage_config)
102
110
  end
@@ -1,3 +1,4 @@
1
+ # -*- coding: UTF-8 -*-
1
2
  #
2
3
  # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
4
  # License:: Apache License, Version 2.0
@@ -15,7 +16,7 @@
15
16
  # limitations under the License.
16
17
  #
17
18
 
18
- require 'digest/sha1'
19
+ require 'digest/sha2'
19
20
 
20
21
  require 'chef-dk/policyfile/storage_config'
21
22
  require 'chef-dk/policyfile/cookbook_locks'
@@ -31,7 +32,7 @@ module ChefDK
31
32
  attr_reader :ui
32
33
  attr_reader :policyfile_lock
33
34
 
34
- def initialize(ui: ui, policyfile_lock: nil)
35
+ def initialize(ui: nil, policyfile_lock: nil)
35
36
  @ui = ui
36
37
  @policyfile_lock = policyfile_lock
37
38
 
@@ -83,6 +84,8 @@ module ChefDK
83
84
  attr_accessor :name
84
85
  attr_accessor :run_list
85
86
  attr_accessor :named_run_lists
87
+ attr_accessor :default_attributes
88
+ attr_accessor :override_attributes
86
89
 
87
90
  attr_reader :solution_dependencies
88
91
 
@@ -101,6 +104,9 @@ module ChefDK
101
104
  @storage_config = storage_config
102
105
  @ui = ui || UI.null
103
106
 
107
+ @default_attributes = {}
108
+ @override_attributes = {}
109
+
104
110
  @solution_dependencies = Policyfile::SolutionDependencies.new
105
111
  @install_report = InstallReport.new(ui: @ui, policyfile_lock: self)
106
112
  end
@@ -132,6 +138,8 @@ module ChefDK
132
138
  lock["run_list"] = run_list
133
139
  lock["named_run_lists"] = named_run_lists unless named_run_lists.empty?
134
140
  lock["cookbook_locks"] = cookbook_locks_for_lockfile
141
+ lock["default_attributes"] = default_attributes
142
+ lock["override_attributes"] = override_attributes
135
143
  lock["solution_dependencies"] = solution_dependencies.to_lock
136
144
  end
137
145
  end
@@ -139,7 +147,7 @@ module ChefDK
139
147
  # Returns a fingerprint of the PolicyfileLock by computing the SHA1 hash of
140
148
  # #canonical_revision_string
141
149
  def revision_id
142
- Digest::SHA1.new.hexdigest(canonical_revision_string)
150
+ Digest::SHA256.new.hexdigest(canonical_revision_string)
143
151
  end
144
152
 
145
153
  # Generates a string representation of the lock data in a specialized
@@ -172,6 +180,10 @@ module ChefDK
172
180
  canonical_rev_text << "cookbook:#{name};id:#{lock["identifier"]}\n"
173
181
  end
174
182
 
183
+ canonical_rev_text << "default_attributes:#{canonicalize(default_attributes)}\n"
184
+
185
+ canonical_rev_text << "override_attributes:#{canonicalize(override_attributes)}\n"
186
+
175
187
  canonical_rev_text
176
188
  end
177
189
 
@@ -232,6 +244,9 @@ module ChefDK
232
244
  end
233
245
  end
234
246
 
247
+ @default_attributes = compiler.default_attributes
248
+ @override_attributes = compiler.override_attributes
249
+
235
250
  @solution_dependencies = compiler.solution_dependencies
236
251
 
237
252
  self
@@ -241,6 +256,7 @@ module ChefDK
241
256
  set_name_from_lock_data(lock_data)
242
257
  set_run_list_from_lock_data(lock_data)
243
258
  set_cookbook_locks_from_lock_data(lock_data)
259
+ set_attributes_from_lock_data(lock_data)
244
260
  set_solution_dependencies_from_lock_data(lock_data)
245
261
  self
246
262
  end
@@ -264,6 +280,97 @@ module ChefDK
264
280
 
265
281
  private
266
282
 
283
+ # Generates a canonical JSON representation of the attributes. Based on
284
+ # http://wiki.laptop.org/go/Canonical_JSON but not quite as strict, yet.
285
+ #
286
+ # In particular:
287
+ # - String encoding stuff isn't normalized
288
+ # - We allow floats that fit within the range/precision requirements of
289
+ # IEEE 754-2008 binary64 (double precision) numbers.
290
+ # - +/- Infinity and NaN are banned, but float/numeric size aren't checked.
291
+ # numerics should be in range [-(2**53)+1, (2**53)-1] to comply with
292
+ # IEEE 754-2008
293
+ #
294
+ # Recursive, so absurd nesting levels could cause a SystemError. Invalid
295
+ # input will cause an InvalidPolicyfileAttribute exception.
296
+ def canonicalize(attributes)
297
+ unless attributes.kind_of?(Hash)
298
+ raise "Top level attributes must be a Hash (you gave: #{attributes})"
299
+ end
300
+ canonicalize_elements(attributes)
301
+ end
302
+
303
+ def canonicalize_elements(item)
304
+ case item
305
+ when Hash
306
+ # Hash keys will sort differently based on the encoding, but after a
307
+ # JSON round trip everything will be UTF-8, so we have to normalize the
308
+ # keys to UTF-8 first so that the sort order uses the UTF-8 strings.
309
+ item_with_normalized_keys = item.inject({}) do |normalized_item, (key, value)|
310
+ validate_attr_key(key)
311
+ normalized_item[key.encode('utf-8')] = value
312
+ normalized_item
313
+ end
314
+ elements = item_with_normalized_keys.keys.sort.map do |key|
315
+ k = '"' << key << '":'
316
+ v = canonicalize_elements(item_with_normalized_keys[key])
317
+ k << v
318
+ end
319
+ "{" << elements.join(',') << "}"
320
+ when String
321
+ '"' << item.encode('utf-8') << '"'
322
+ when Array
323
+ elements = item.map { |i| canonicalize_elements(i) }
324
+ '[' << elements.join(',') << ']'
325
+ when Integer
326
+ item.to_s
327
+ when Float
328
+ unless item.finite?
329
+ raise InvalidPolicyfileAttribute, "Floating point numbers cannot be infinite or NaN. You gave #{item.inspect}"
330
+ end
331
+ # Support for floats assumes that any implementation of our JSON
332
+ # canonicalization routine will use IEEE-754 doubles. In decimal terms,
333
+ # doubles give 15-17 digits of precision, so we err on the safe side
334
+ # and only use 15 digits in the string conversion. We use the `g`
335
+ # format, which is a documented-enough "do what I mean" where floats
336
+ # >= 0.1 and < precsion are represented as floating point literals, and
337
+ # other numbers use the exponent notation with a lowercase 'e'. Note
338
+ # that both Ruby and Erlang document what their `g` does but have some
339
+ # differences both subtle and non-subtle:
340
+ #
341
+ # ```ruby
342
+ # format("%.15g", 0.1) #=> "0.1"
343
+ # format("%.15g", 1_000_000_000.0) #=> "1000000000"
344
+ # ```
345
+ #
346
+ # Whereas:
347
+ #
348
+ # ```erlang
349
+ # lists:flatten(io_lib:format("~.15g", [0.1])). %=> "0.100000000000000"
350
+ # lists:flatten(io_lib:format("~.15e", [1000000000.0])). %=> "1.00000000000000e+9"
351
+ # ```
352
+ #
353
+ # Other implementations should normalize to ruby's %.15g behavior.
354
+ Kernel.format("%.15g", item)
355
+ when NilClass
356
+ "null"
357
+ when TrueClass
358
+ "true"
359
+ when FalseClass
360
+ "false"
361
+ else
362
+ raise InvalidPolicyfileAttribute,
363
+ "Invalid type in attributes. Only Hash, Array, String, Integer, Float, true, false, and nil are accepted. You gave #{item.inspect} (#{item.class})"
364
+ end
365
+ end
366
+
367
+ def validate_attr_key(key)
368
+ unless key.kind_of?(String)
369
+ raise InvalidPolicyfileAttribute,
370
+ "Attribute keys must be Strings (other types are not allowed in JSON). You gave: #{key.inspect} (#{key.class})"
371
+ end
372
+ end
373
+
267
374
  def set_name_from_lock_data(lock_data)
268
375
  name_attribute = lock_data["name"]
269
376
 
@@ -316,6 +423,31 @@ module ChefDK
316
423
  end
317
424
  end
318
425
 
426
+ def set_attributes_from_lock_data(lock_data)
427
+ default_attr_data = lock_data["default_attributes"]
428
+
429
+ if default_attr_data.nil?
430
+ raise InvalidLockfile, "lockfile does not have a `default_attributes` attribute"
431
+ end
432
+
433
+ unless default_attr_data.kind_of?(Hash)
434
+ raise InvalidLockfile, "lockfile's `default_attributes` attribute must be a Hash (JSON object). (got: #{default_attr_data.inspect})"
435
+ end
436
+
437
+ override_attr_data = lock_data["override_attributes"]
438
+
439
+ if override_attr_data.nil?
440
+ raise InvalidLockfile, "lockfile does not have a `override_attributes` attribute"
441
+ end
442
+
443
+ unless override_attr_data.kind_of?(Hash)
444
+ raise InvalidLockfile, "lockfile's `override_attributes` attribute must be a Hash (JSON object). (got: #{override_attr_data.inspect})"
445
+ end
446
+
447
+ @default_attributes = default_attr_data
448
+ @override_attributes = override_attr_data
449
+ end
450
+
319
451
  def set_solution_dependencies_from_lock_data(lock_data)
320
452
  soln_deps = lock_data["solution_dependencies"]
321
453