chef-dk 0.5.0.rc.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|