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.
- checksums.yaml +4 -4
- data/README.md +63 -24
- data/lib/chef-dk/builtin_commands.rb +2 -0
- data/lib/chef-dk/command/diff.rb +312 -0
- data/lib/chef-dk/command/push.rb +1 -1
- data/lib/chef-dk/command/shell_init.rb +21 -3
- data/lib/chef-dk/command/update.rb +28 -5
- data/lib/chef-dk/configurable.rb +1 -1
- data/lib/chef-dk/exceptions.rb +3 -0
- data/lib/chef-dk/pager.rb +106 -0
- data/lib/chef-dk/policyfile/chef_repo_cookbook_source.rb +114 -0
- data/lib/chef-dk/policyfile/comparison_base.rb +124 -0
- data/lib/chef-dk/policyfile/cookbook_sources.rb +1 -0
- data/lib/chef-dk/policyfile/differ.rb +266 -0
- data/lib/chef-dk/policyfile/dsl.rb +26 -3
- data/lib/chef-dk/policyfile/uploader.rb +4 -5
- data/lib/chef-dk/policyfile_compiler.rb +8 -0
- data/lib/chef-dk/policyfile_lock.rb +135 -3
- data/lib/chef-dk/policyfile_services/install.rb +1 -0
- data/lib/chef-dk/policyfile_services/update_attributes.rb +104 -0
- data/lib/chef-dk/service_exceptions.rb +12 -0
- data/lib/chef-dk/ui.rb +8 -0
- data/lib/chef-dk/version.rb +1 -1
- data/spec/spec_helper.rb +6 -0
- data/spec/test_helpers.rb +4 -0
- data/spec/unit/command/diff_spec.rb +283 -0
- data/spec/unit/command/shell_init_spec.rb +19 -2
- data/spec/unit/command/update_spec.rb +96 -0
- data/spec/unit/command/verify_spec.rb +0 -6
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/Berksfile +3 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/README.md +4 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/chefignore +96 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/metadata.rb +9 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/recipes/default.rb +8 -0
- data/spec/unit/pager_spec.rb +119 -0
- data/spec/unit/policyfile/chef_repo_cookbook_source_spec.rb +66 -0
- data/spec/unit/policyfile/comparison_base_spec.rb +343 -0
- data/spec/unit/policyfile/differ_spec.rb +687 -0
- data/spec/unit/policyfile_evaluation_spec.rb +87 -0
- data/spec/unit/policyfile_lock_build_spec.rb +247 -8
- data/spec/unit/policyfile_lock_serialization_spec.rb +47 -0
- data/spec/unit/policyfile_services/export_repo_spec.rb +2 -0
- data/spec/unit/policyfile_services/push_spec.rb +2 -0
- data/spec/unit/policyfile_services/update_attributes_spec.rb +217 -0
- metadata +62 -6
@@ -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,
|
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(
|
75
|
+
set_default_community_source(source_argument)
|
71
76
|
when :chef_server
|
72
|
-
set_default_chef_server_source(
|
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:
|
57
|
-
|
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/
|
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:
|
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::
|
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
|
|