chef-dk 2.3.4 → 2.4.17

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/Gemfile +22 -18
  3. data/Gemfile.lock +184 -254
  4. data/README.md +1 -1
  5. data/Rakefile +1 -1
  6. data/acceptance/Gemfile.lock +27 -32
  7. data/lib/chef-dk/chef_server_api_multi.rb +73 -0
  8. data/lib/chef-dk/command/update.rb +5 -12
  9. data/lib/chef-dk/configurable.rb +19 -0
  10. data/lib/chef-dk/cookbook_omnifetch.rb +1 -0
  11. data/lib/chef-dk/exceptions.rb +11 -0
  12. data/lib/chef-dk/generator.rb +1 -1
  13. data/lib/chef-dk/policyfile/attribute_merge_checker.rb +110 -0
  14. data/lib/chef-dk/policyfile/chef_server_cookbook_source.rb +5 -4
  15. data/lib/chef-dk/policyfile/chef_server_lock_fetcher.rb +164 -0
  16. data/lib/chef-dk/policyfile/cookbook_location_specification.rb +3 -3
  17. data/lib/chef-dk/policyfile/dsl.rb +16 -0
  18. data/lib/chef-dk/policyfile/included_policies_cookbook_source.rb +156 -0
  19. data/lib/chef-dk/policyfile/local_lock_fetcher.rb +122 -0
  20. data/lib/chef-dk/policyfile/lock_applier.rb +80 -0
  21. data/lib/chef-dk/policyfile/null_cookbook_source.rb +4 -0
  22. data/lib/chef-dk/policyfile/policyfile_location_specification.rb +122 -0
  23. data/lib/chef-dk/policyfile_compiler.rb +129 -16
  24. data/lib/chef-dk/policyfile_lock.rb +30 -0
  25. data/lib/chef-dk/policyfile_services/install.rb +7 -1
  26. data/lib/chef-dk/policyfile_services/update_attributes.rb +10 -2
  27. data/lib/chef-dk/skeletons/code_generator/templates/default/recipe_spec.rb.erb +14 -1
  28. data/lib/chef-dk/version.rb +1 -1
  29. data/omnibus_overrides.rb +6 -6
  30. data/spec/unit/chef_server_api_multi_spec.rb +120 -0
  31. data/spec/unit/command/update_spec.rb +3 -3
  32. data/spec/unit/configurable_spec.rb +27 -0
  33. data/spec/unit/policyfile/attribute_merge_checker_spec.rb +80 -0
  34. data/spec/unit/policyfile/chef_server_lock_fetcher_spec.rb +161 -0
  35. data/spec/unit/policyfile/cookbook_location_specification_spec.rb +48 -0
  36. data/spec/unit/policyfile/included_policies_cookbook_source_spec.rb +242 -0
  37. data/spec/unit/policyfile/local_lock_fetcher_spec.rb +161 -0
  38. data/spec/unit/policyfile/lock_applier_spec.rb +100 -0
  39. data/spec/unit/policyfile_demands_spec.rb +1 -1
  40. data/spec/unit/policyfile_includes_dsl_spec.rb +159 -0
  41. data/spec/unit/policyfile_includes_spec.rb +720 -0
  42. data/spec/unit/policyfile_install_with_includes_spec.rb +232 -0
  43. data/spec/unit/policyfile_lock_build_spec.rb +11 -2
  44. data/spec/unit/policyfile_services/update_attributes_spec.rb +13 -0
  45. metadata +28 -3
@@ -0,0 +1,80 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module ChefDK
19
+ module Policyfile
20
+
21
+ # A class that can apply constraints from a lock to a compiler
22
+ class LockApplier
23
+ attr_reader :unlocked_policies
24
+ attr_reader :policyfile_lock
25
+ attr_reader :policyfile_compiler
26
+
27
+ # Initialize a LockApplier. By default, all locked data this class knows
28
+ # about will be applied to the compiler. Currently, it applies locks only
29
+ # for included policies.
30
+ #
31
+ # @param policyfile_lock [PolicyfileLock] contains the locked data to use
32
+ # @param policyfile_compiler [PolicyfileCompiler] the compiler to apply the locked data
33
+ def initialize(policyfile_lock, policyfile_compiler)
34
+ @policyfile_lock = policyfile_lock
35
+ @policyfile_compiler = policyfile_compiler
36
+ @unlocked_policies = []
37
+ end
38
+
39
+ # Unlocks included policies
40
+ #
41
+ # @param policies [:all] Unconstrain all policies
42
+ # @param policies [Array<String>] Unconstrain a specific policy by name
43
+ def with_unlocked_policies(policies)
44
+ if policies == :all || unlocked_policies == :all
45
+ @unlocked_policies = :all
46
+ else
47
+ policies.each do |policy|
48
+ @unlocked_policies << policy
49
+ end
50
+ end
51
+ self
52
+ end
53
+
54
+ # Apply locks described in policyfile_lock allowing for the deviations asked
55
+ # for.
56
+ #
57
+ # @note No changes are applied until apply! is invoked
58
+ def apply!
59
+ prepare_constraints_for_policies
60
+ end
61
+
62
+ private
63
+
64
+ def prepare_constraints_for_policies
65
+ if unlocked_policies == :all
66
+ return
67
+ end
68
+
69
+ policyfile_compiler.included_policies.each do |policy|
70
+ if !unlocked_policies.include?(policy.name)
71
+ lock = policyfile_lock.included_policy_locks.find do |policy_lock|
72
+ policy_lock["name"] == policy.name
73
+ end
74
+ policy.apply_locked_source_options(lock["source_options"])
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -36,6 +36,10 @@ module ChefDK
36
36
  true
37
37
  end
38
38
 
39
+ def preferred_cookbooks
40
+ []
41
+ end
42
+
39
43
  def desc
40
44
  "null_cookbook_source"
41
45
  end
@@ -0,0 +1,122 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "chef-dk/policyfile_lock"
19
+ require "chef-dk/policyfile/local_lock_fetcher"
20
+ require "chef-dk/policyfile/chef_server_lock_fetcher"
21
+ require "chef-dk/exceptions"
22
+
23
+ module ChefDK
24
+ module Policyfile
25
+ # A PolicyfileLocationSpecification specifies where a policyfile lock is to be fetched from.
26
+ # Using this information, it provides a fetcher that is capable loading the policyfile
27
+ # lock.
28
+ class PolicyfileLocationSpecification
29
+
30
+ # @return [String] the name of the policyfile
31
+ attr_reader :name
32
+
33
+ # @return [Hash] options describing how to get the policyfile lock
34
+ attr_reader :source_options
35
+
36
+ attr_reader :storage_config
37
+ attr_reader :chef_config
38
+ attr_reader :ui
39
+
40
+ LOCATION_TYPES = [:path, :server]
41
+
42
+ # Intialize a location spec
43
+ #
44
+ # @param name [String] the name of the policyfile
45
+ # @param source_options [Hash] options describing where the policyfile lock lives
46
+ # @param storage_config [Poilcyfile::StorageConfig]
47
+ # @param chef_config [Chef::Config] chef config that will be used when communication
48
+ # with a chef server is required
49
+ def initialize(name, source_options, storage_config, chef_config = nil)
50
+ @name = name
51
+ @source_options = source_options
52
+ @storage_config = storage_config
53
+ @ui = nil
54
+ @chef_config = chef_config
55
+ end
56
+
57
+ # @return The revision id from the fetched lock
58
+ def revision_id
59
+ fetcher.lock_data["revision_id"]
60
+ end
61
+
62
+ # @return A policyfile lock fetcher compatible with the given source_options
63
+ def fetcher
64
+ @fetcher ||= begin
65
+ if source_options[:path]
66
+ Policyfile::LocalLockFetcher.new(name, source_options, storage_config)
67
+ elsif source_options[:server]
68
+ Policyfile::ChefServerLockFetcher.new(name, source_options, chef_config)
69
+ else
70
+ raise ChefDK::InvalidPolicyfileLocation.new(
71
+ "Invalid policyfile lock location type. The supported locations are: #{LOCATION_TYPES.join(", ")}")
72
+ end
73
+ end
74
+ end
75
+
76
+ # @return [True] if there were no errors with the provided source_options
77
+ # @return [False] if there were errors with the provided source_options
78
+ def valid?
79
+ errors.empty?
80
+ end
81
+
82
+ # Check the options provided when craeting this class for errors
83
+ #
84
+ # @return [Array<String>] A list of errors found
85
+ def errors
86
+ error_messages = []
87
+
88
+ if LOCATION_TYPES.all? { |l| source_options[l].nil? }
89
+ error_messages << "include_policy must use one of the following sources: #{LOCATION_TYPES.join(', ')}"
90
+ else
91
+ if !fetcher.nil?
92
+ error_messages += fetcher.errors
93
+ end
94
+ end
95
+
96
+ error_messages
97
+ end
98
+
99
+ # Fetches and loads the policyfile lock
100
+ #
101
+ # @return [PolicyfileLock] the loaded policyfile lock
102
+ def policyfile_lock
103
+ @policyfile_lock ||= begin
104
+ PolicyfileLock.new(storage_config, ui: ui).build_from_lock_data(fetcher.lock_data)
105
+ end
106
+ end
107
+
108
+ # @return [Hash] The source_options that describe how to fetch this exact lock again
109
+ def source_options_for_lock
110
+ fetcher.source_options_for_lock
111
+ end
112
+
113
+ # Applies source options from a lock file. This is used to make sure that the same
114
+ # policyfile lock is loaded that was locked
115
+ #
116
+ # @param options_from_lock [Hash] The source options loaded from a policyfile lock
117
+ def apply_locked_source_options(options_from_lock)
118
+ fetcher.apply_locked_source_options(options_from_lock)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -20,8 +20,11 @@ require "forwardable"
20
20
 
21
21
  require "solve"
22
22
  require "chef/run_list"
23
+ require "chef/mixin/deep_merge"
23
24
 
24
25
  require "chef-dk/policyfile/dsl"
26
+ require "chef-dk/policyfile/attribute_merge_checker"
27
+ require "chef-dk/policyfile/included_policies_cookbook_source"
25
28
  require "chef-dk/policyfile_lock"
26
29
  require "chef-dk/ui"
27
30
  require "chef-dk/policyfile/reports/install"
@@ -49,8 +52,8 @@ module ChefDK
49
52
  def_delegator :@dsl, :named_run_list
50
53
  def_delegator :@dsl, :named_run_lists
51
54
  def_delegator :@dsl, :errors
52
- def_delegator :@dsl, :default_source
53
55
  def_delegator :@dsl, :cookbook_location_specs
56
+ def_delegator :@dsl, :included_policies
54
57
 
55
58
  attr_reader :dsl
56
59
  attr_reader :storage_config
@@ -67,6 +70,19 @@ module ChefDK
67
70
  @install_report = Policyfile::Reports::Install.new(ui: @ui, policyfile_compiler: self)
68
71
  end
69
72
 
73
+ def default_source(source_type = nil, source_argument = nil, &block)
74
+ if source_type.nil?
75
+ prepend_array = if included_policies.length > 0
76
+ [included_policies_cookbook_source]
77
+ else
78
+ []
79
+ end
80
+ prepend_array + dsl.default_source
81
+ else
82
+ dsl.default_source(source_type, source_argument, &block)
83
+ end
84
+ end
85
+
70
86
  def error!
71
87
  unless errors.empty?
72
88
  raise PolicyfileError, errors.join("\n")
@@ -79,7 +95,17 @@ module ChefDK
79
95
 
80
96
  def expanded_run_list
81
97
  # doesn't support roles yet...
82
- Chef::RunList.new(*run_list)
98
+ concated_runlist = Chef::RunList.new
99
+ included_policies.each do |policy_spec|
100
+ lock = policy_spec.policyfile_lock
101
+ lock.run_list.each do |run_list_item|
102
+ concated_runlist << run_list_item
103
+ end
104
+ end
105
+ run_list.each do |run_list_item|
106
+ concated_runlist << run_list_item
107
+ end
108
+ concated_runlist
83
109
  end
84
110
 
85
111
  # copy of the expanded_run_list, properly formatted for use in a lockfile
@@ -88,8 +114,23 @@ module ChefDK
88
114
  end
89
115
 
90
116
  def expanded_named_run_lists
91
- named_run_lists.inject({}) do |expanded, (name, run_list_items)|
92
- expanded[name] = Chef::RunList.new(*run_list_items)
117
+ included_policies_named_runlists = included_policies.inject({}) do |acc, policy_spec|
118
+ lock = policy_spec.policyfile_lock
119
+ lock.named_run_lists.inject(acc) do |expanded, (name, run_list_items)|
120
+ expanded[name] ||= Chef::RunList.new
121
+ run_list_items.each do |run_list_item|
122
+ expanded[name] << run_list_item
123
+ end
124
+ expanded
125
+ end
126
+ acc
127
+ end
128
+
129
+ named_run_lists.inject(included_policies_named_runlists) do |expanded, (name, run_list_items)|
130
+ expanded[name] ||= Chef::RunList.new
131
+ run_list_items.each do |run_list_item|
132
+ expanded[name] << run_list_item
133
+ end
93
134
  expanded
94
135
  end
95
136
  end
@@ -102,11 +143,19 @@ module ChefDK
102
143
  end
103
144
 
104
145
  def default_attributes
105
- dsl.node_attributes.combined_default.to_hash
146
+ check_for_default_attribute_conflicts!
147
+ included_policies.map { |p| p.policyfile_lock }.inject(
148
+ dsl.node_attributes.combined_default.to_hash) do |acc, lock|
149
+ Chef::Mixin::DeepMerge.merge(acc, lock.default_attributes)
150
+ end
106
151
  end
107
152
 
108
153
  def override_attributes
109
- dsl.node_attributes.combined_override.to_hash
154
+ check_for_override_attribute_conflicts!
155
+ included_policies.map { |p| p.policyfile_lock }.inject(
156
+ dsl.node_attributes.combined_override.to_hash) do |acc, lock|
157
+ Chef::Mixin::DeepMerge.merge(acc, lock.override_attributes)
158
+ end
110
159
  end
111
160
 
112
161
  def lock
@@ -208,16 +257,9 @@ module ChefDK
208
257
  end
209
258
 
210
259
  def graph_demands
211
- cookbooks_for_demands.map do |cookbook_name|
212
- spec = cookbook_location_spec_for(cookbook_name)
213
- if spec.nil?
214
- [ cookbook_name, DEFAULT_DEMAND_CONSTRAINT ]
215
- elsif spec.version_fixed?
216
- [ cookbook_name, "= #{spec.version}" ]
217
- else
218
- [ cookbook_name, spec.version_constraint.to_s ]
219
- end
220
- end
260
+ ## TODO: By merging cookbooks from the current policyfile and included policies,
261
+ # we lose the ability to know where a conflict came from
262
+ (cookbook_demands_from_current + cookbook_demands_from_policies)
221
263
  end
222
264
 
223
265
  def artifacts_graph
@@ -411,5 +453,76 @@ module ChefDK
411
453
  dependency_set
412
454
  end
413
455
 
456
+ def check_for_default_attribute_conflicts!
457
+ checker = Policyfile::AttributeMergeChecker.new
458
+ checker.with_attributes("user-specified", dsl.node_attributes.combined_default)
459
+ included_policies.map do |policy_spec|
460
+ lock = policy_spec.policyfile_lock
461
+ checker.with_attributes(policy_spec.name, lock.default_attributes)
462
+ end
463
+ checker.check!
464
+ end
465
+
466
+ def check_for_override_attribute_conflicts!
467
+ checker = Policyfile::AttributeMergeChecker.new
468
+ checker.with_attributes("user-specified", dsl.node_attributes.combined_override)
469
+ included_policies.map do |policy_spec|
470
+ lock = policy_spec.policyfile_lock
471
+ checker.with_attributes(policy_spec.name, lock.override_attributes)
472
+ end
473
+ checker.check!
474
+ end
475
+
476
+ def cookbook_demands_from_policies
477
+ included_policies.flat_map do |policy_spec|
478
+ lock = policy_spec.policyfile_lock
479
+ lock.solution_dependencies.to_lock["Policyfile"]
480
+ end
481
+ end
482
+
483
+ def cookbook_demands_from_current
484
+ cookbooks_for_demands.map do |cookbook_name|
485
+ spec = cookbook_location_spec_for(cookbook_name)
486
+ if spec.nil?
487
+ [ cookbook_name, DEFAULT_DEMAND_CONSTRAINT ]
488
+ elsif spec.version_fixed?
489
+ [ cookbook_name, "= #{spec.version}" ]
490
+ else
491
+ [ cookbook_name, spec.version_constraint.to_s ]
492
+ end
493
+ end
494
+ end
495
+
496
+ def included_policies_cookbook_source
497
+ @included_policies_cookbook_source ||= begin
498
+ source = Policyfile::IncludedPoliciesCookbookSource.new(included_policies)
499
+ handle_included_policies_preferred_cookbook_conflicts(source)
500
+ source
501
+ end
502
+ end
503
+
504
+ def handle_included_policies_preferred_cookbook_conflicts(included_policies_source)
505
+ # All cookbooks in the included policies are preferred.
506
+ conflicting_source_messages = []
507
+ dsl.default_source.reject { |s| s.null? }.each do |source_b|
508
+ conflicting_preferences = included_policies_source.preferred_cookbooks & source_b.preferred_cookbooks
509
+ next if conflicting_preferences.empty?
510
+ next if conflicting_preferences.all? do |cookbook_name|
511
+ version = included_policies_source.universe_graph[cookbook_name].keys.first
512
+ if included_policies_source.source_options_for(cookbook_name, version) == source_b.source_options_for(cookbook_name, version)
513
+ true
514
+ else
515
+ false
516
+ end
517
+ end
518
+ conflicting_source_messages << "#{source_b.desc} sets a preferred for cookbook(s) #{conflicting_preferences.join(', ')}. This conflicts with an included policy."
519
+ end
520
+ unless conflicting_source_messages.empty?
521
+ msg = "You may not override the cookbook sources for any cookbooks required by included policies.\n"
522
+ msg << conflicting_source_messages.join("\n") << "\n"
523
+ raise IncludePolicyCookbookSourceConflict.new(msg)
524
+ end
525
+ end
526
+
414
527
  end
415
528
  end
@@ -93,6 +93,8 @@ module ChefDK
93
93
 
94
94
  attr_reader :cookbook_locks
95
95
 
96
+ attr_reader :included_policy_locks
97
+
96
98
  attr_reader :install_report
97
99
 
98
100
  def initialize(storage_config, ui: nil)
@@ -108,6 +110,9 @@ module ChefDK
108
110
  @override_attributes = {}
109
111
 
110
112
  @solution_dependencies = Policyfile::SolutionDependencies.new
113
+
114
+ @included_policy_locks = []
115
+
111
116
  @install_report = InstallReport.new(ui: @ui, policyfile_lock: self)
112
117
  end
113
118
 
@@ -137,6 +142,7 @@ module ChefDK
137
142
  lock["name"] = name
138
143
  lock["run_list"] = run_list
139
144
  lock["named_run_lists"] = named_run_lists unless named_run_lists.empty?
145
+ lock["included_policy_locks"] = included_policy_locks
140
146
  lock["cookbook_locks"] = cookbook_locks_for_lockfile
141
147
  lock["default_attributes"] = default_attributes
142
148
  lock["override_attributes"] = override_attributes
@@ -249,6 +255,14 @@ module ChefDK
249
255
 
250
256
  @solution_dependencies = compiler.solution_dependencies
251
257
 
258
+ @included_policy_locks = compiler.included_policies.map do |policy|
259
+ {
260
+ "name" => policy.name,
261
+ "revision_id" => policy.revision_id,
262
+ "source_options" => policy.source_options_for_lock,
263
+ }
264
+ end
265
+
252
266
  self
253
267
  end
254
268
 
@@ -259,6 +273,7 @@ module ChefDK
259
273
  set_cookbook_locks_from_lock_data(lock_data)
260
274
  set_attributes_from_lock_data(lock_data)
261
275
  set_solution_dependencies_from_lock_data(lock_data)
276
+ set_included_policy_locks_from_lock_data(lock_data)
262
277
  self
263
278
  end
264
279
 
@@ -269,6 +284,7 @@ module ChefDK
269
284
  set_cookbook_locks_as_archives_from_lock_data(lock_data)
270
285
  set_attributes_from_lock_data(lock_data)
271
286
  set_solution_dependencies_from_lock_data(lock_data)
287
+ set_included_policy_locks_from_lock_data(lock_data)
272
288
  self
273
289
  end
274
290
 
@@ -517,6 +533,20 @@ module ChefDK
517
533
  @solution_dependencies = s
518
534
  end
519
535
 
536
+ def set_included_policy_locks_from_lock_data(lock_data)
537
+ locks = lock_data["included_policy_locks"]
538
+ if locks.nil?
539
+ @included_policy_locks = []
540
+ else
541
+ locks.each do |lock_info|
542
+ if !(%w{revision_id name source_options}.all? { |key| !lock_info[key].nil? })
543
+ raise InvalidLockfile, "lockfile included policy missing one of the required keys"
544
+ end
545
+ end
546
+ @included_policy_locks = locks
547
+ end
548
+ end
549
+
520
550
  def build_cookbook_lock_from_lock_data(name, lock_info)
521
551
  unless lock_info.kind_of?(Hash)
522
552
  raise InvalidLockfile, "lockfile cookbook_locks entries must be a Hash (JSON object). (got: #{lock_info.inspect})"