spec/unit/policyfile_includes_spec.rb in chef-dk-3.0.36 vs spec/unit/policyfile_includes_spec.rb in chef-dk-3.1.0
- old
+ new
@@ -1,720 +1,720 @@
-# -*- coding: UTF-8 -*-
-#
-# Copyright:: Copyright (c) 2014-2018, Chef Software Inc.
-# License:: Apache License, Version 2.0
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-require "spec_helper"
-require "chef-dk/policyfile_compiler"
-require "chef-dk/exceptions"
-
-describe ChefDK::PolicyfileCompiler, "including upstream policy locks" do
-
- def expand_run_list(r)
- r.map do |item|
- "recipe[#{item}]"
- end
- end
-
- let(:run_list) { ["local::default"] }
- let(:run_list_expanded) { expand_run_list(run_list) }
- let(:named_run_list) { {} }
- let(:named_run_list_expanded) do
- named_run_list.inject({}) do |acc, (key, val)|
- acc[key] = expand_run_list(val)
- acc
- end
- end
- let(:default_attributes) { {} }
- let(:override_attributes) { {} }
-
- let(:default_source) { nil }
-
- let(:external_cookbook_universe) do
- {
- "cookbookA" => {
- "1.0.0" => [ ],
- "2.0.0" => [ ],
- },
- "cookbookB" => {
- "1.0.0" => [ ],
- "2.0.0" => [ ],
- },
- "cookbookC" => {
- "1.0.0" => [ ],
- "2.0.0" => [ ],
- },
- "local" => {
- "1.0.0" => [ ["cookbookC", "= 1.0.0" ] ],
- },
- "local_easy" => {
- "1.0.0" => [ ["cookbookC", "= 2.0.0" ] ],
- },
- }
- end
-
- let(:included_policy_default_attributes) { {} }
- let(:included_policy_override_attributes) { {} }
- let(:included_policy_expanded_named_runlist) { nil }
- let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] }
- let(:included_policy_cookbooks) do
- [
- {
- name: "cookbookA",
- version: "2.0.0",
- },
- ]
- end
-
- let(:included_policy_lock_data) do
- cookbook_locks = included_policy_cookbooks.inject({}) do |acc, cookbook_info|
- acc[cookbook_info[:name]] = {
- "version" => cookbook_info[:version],
- "identifier" => "identifier",
- "dotted_decimal_identifier" => "dotted_decimal_identifier",
- "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}",
- "origin" => "uri",
- "source_options" => {},
- }
- acc
- end
-
- solution_dependencies_lock = included_policy_cookbooks.map do |cookbook_info|
- [cookbook_info[:name], cookbook_info[:version]]
- end
-
- solution_dependencies_cookbooks = included_policy_cookbooks.inject({}) do |acc, cookbook_info|
- acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]]
- acc
- end
-
- {
- "name" => "included_policyfile",
- "revision_id" => "myrevisionid",
- "run_list" => included_policy_expanded_runlist,
- "cookbook_locks" => cookbook_locks,
- "default_attributes" => included_policy_default_attributes,
- "override_attributes" => included_policy_override_attributes,
- "solution_dependencies" => {
- "Policyfile" => solution_dependencies_lock,
- "dependencies" => solution_dependencies_cookbooks,
- },
- }.tap do |core|
- core["named_run_lists"] = included_policy_expanded_named_runlist if included_policy_expanded_named_runlist
- end
- end
-
- let(:included_policy_lock_name) { "included" }
- let(:included_policy_fetcher) do
- instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double|
- allow(double).to receive(:lock_data).and_return(included_policy_lock_data)
- allow(double).to receive(:valid?).and_return(true)
- allow(double).to receive(:errors).and_return([])
- end
- end
-
- let(:lock_source_options) { { :path => "somelocation" } }
- let(:included_policy_lock_spec) do
- ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_lock_name, lock_source_options, nil).tap do |spec|
- allow(spec).to receive(:valid?).and_return(true)
- allow(spec).to receive(:fetcher).and_return(included_policy_fetcher)
- allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options)
- end
- end
-
- let(:included_policies) { [] }
-
- let(:policyfile) do
- policyfile = ChefDK::PolicyfileCompiler.new.build do |p|
- if default_source
- p.default_source.replace([default_source])
- else
- allow(p.default_source.first).to receive(:universe_graph).and_return(external_cookbook_universe)
- allow(p.default_source.first).to receive(:null?).and_return(false)
- end
- p.run_list(*run_list)
-
- named_run_list.each do |name, run_list|
- p.named_run_list(name, *run_list)
- end
-
- default_attributes.each do |(name, value)|
- p.default[name] = value
- end
-
- override_attributes.each do |(name, value)|
- p.override[name] = value
- end
-
- allow(p).to receive(:included_policies).and_return(included_policies)
- end
-
- policyfile
- end
-
- let(:policyfile_lock) do
- policyfile.lock
- end
-
- context "when no policies are included" do
-
- it "does not emit included policies information in the lockfile" do
- expect(policyfile_lock.to_lock["included_policies"]).to eq(nil)
- end
-
- end
-
- context "when one policy is included" do
-
- let(:included_policies) { [included_policy_lock_spec] }
-
- # currently you must have a run list in a policyfile, but it should now
- # become possible to make a combo-policy just by combining other policies
- context "when the including policy does not have a run list" do
- let(:run_list) { [] }
-
- it "emits a lockfile with an identical run list as the included policy" do
- expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist)
- end
-
- end
-
- context "when the including policy has a run list" do
-
- it "appends run list items from the including policy to the included policy's run list, removing duplicates" do
- expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist + run_list_expanded)
- end
-
- end
-
- context "when the policies have named run lists" do
-
- let(:included_policy_expanded_named_runlist) do
- {
- "shared" => ["recipe[cookbookA::included]"],
- }
- end
-
- context "and no named run lists are shared between the including and included policy" do
-
- let(:named_run_list) do
- {
- "local" => ["local::foo"],
- }
- end
-
- it "preserves the named run lists as given in both policies" do
- expect(policyfile_lock.to_lock["named_run_lists"]).to include(included_policy_expanded_named_runlist, named_run_list_expanded)
- end
-
- end
-
- context "and some named run lists are shared between the including and included policy" do
-
- let(:named_run_list) do
- {
- "shared" => ["local::foo"],
- }
- end
-
- it "appends run lists items from the including policy's run lists to the included policy's run lists" do
- expect(policyfile_lock.to_lock["named_run_lists"]["shared"]).to eq(included_policy_expanded_named_runlist["shared"] + named_run_list_expanded["shared"])
- end
-
- end
-
- end
-
- context "when no cookbooks are shared as dependencies or transitive dependencies" do
- let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] }
- let(:run_list) { ["cookbookA::default"] }
-
- it "does not raise a have conflicting dependency requirements error" do
- expect { policyfile_lock.to_lock }.not_to raise_error
- end
-
- it "emits a lockfile where cookbooks pulled from the upstream are at identical versions" do
- expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to(
- have_key("cookbookC (2.0.0)"))
- end
- end
-
- context "when some cookbooks are shared as dependencies or transitive dependencies" do
- let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] }
- let(:included_policy_cookbooks) do
- [
- {
- name: "cookbookC",
- version: "2.0.0",
- },
- ]
- end
-
- context "and the including policy does not specify any sources" do
- let(:run_list) { [] }
- it "it defaults to those provided in the included policy lock" do
- expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to(
- have_key("cookbookC (2.0.0)"))
- end
- end
-
- context "and the including policy specifies a source that is equivalent to the included policy" do
- let(:run_list) { [] }
- let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") }
-
- before do
- allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"])
- allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({})
- allow(default_source).to receive(:null?).and_return(false)
- allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe)
- allow(default_source).to receive(:desc).and_return("source double")
- end
-
- it "it defaults to those provided in the included policy lock" do
- expect { policyfile_lock.to_lock }.not_to raise_error
- end
- end
-
- context "and the including policy specifies a source that is not equivalent to the included policy" do
- let(:run_list) { [] }
- let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") }
-
- before do
- allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"])
- allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({ "foo" => "bar" })
- allow(default_source).to receive(:null?).and_return(false)
- allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe)
- allow(default_source).to receive(:desc).and_return("source double")
- end
-
- it "it raises an error" do
- expect { policyfile_lock.to_lock }.to raise_error(ChefDK::IncludePolicyCookbookSourceConflict)
- end
- end
-
- context "and the including policy's dependencies can be solved with the included policy's locks" do
- let(:run_list) { ["local_easy::default"] }
-
- it "solves the dependencies added by the top-level policyfile and emits them in the lockfile" do
- expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to(
- have_key("cookbookC (2.0.0)"))
- end
-
- end
-
- context "and the including policy's dependencies cannot be solved with the included policy's locks" do
- let(:run_list) { ["local::default"] }
-
- it "raises an error describing the conflict" do
- expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError)
- end
-
- it "includes the source of the conflicting dependency constraint from the including policy" do
- expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError) do |e|
- expect(e.to_s).to match(/`cookbookC \(= 2.0.0\)`/) # This one comes from the included policy
- expect(e.to_s).to match(/`cookbookC \(= 1.0.0\)` required by `local-1.0.0`/) # This one comes from the included policy
- end
- end
- end
- end
-
- context "when default attributes are specified" do
- let(:default_attributes) do
- {
- "shared" => {
- "foo" => "bar",
- },
- }
- end
-
- context "when the included policy does not have attributes that conflict with the including policy" do
- let(:included_policy_default_attributes) do
- {
- "not_shared" => {
- "foo" => "bar",
- },
- }
- end
-
- it "emits a lockfile with the attributes from both merged" do
- expect(policyfile_lock.to_lock["default_attributes"]).to include(included_policy_default_attributes, default_attributes)
- end
-
- end
-
- context "when the included policy has attributes that conflict with the including policy, but provide the same value" do
- let(:included_policy_default_attributes) { default_attributes }
-
- it "emits a lockfile with the attributes from both merged" do
- expect(policyfile_lock.to_lock["default_attributes"]).to eq(default_attributes)
- end
-
- end
-
- context "when the included policy has attributes that conflict with the including policy's attributes" do
- let(:included_policy_default_attributes) do
- {
- "shared" => {
- "foo" => "not_bar",
- },
- }
- end
-
- it "raises an error describing all attribute conflicts" do
- expect { policyfile_lock.to_lock }.to raise_error(
- ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
- "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]")
- end
- end
- end
-
- context "when override attributes are specified" do
- let(:override_attributes) do
- {
- "shared" => {
- "foo" => "bar",
- },
- }
- end
-
- context "when the included policy does not have attributes that conflict with the including policy" do
- let(:included_policy_override_attributes) do
- {
- "not_shared" => {
- "foo" => "bar",
- },
- }
- end
-
- it "emits a lockfile with the attributes from both merged" do
- expect(policyfile_lock.to_lock["override_attributes"]).to include(included_policy_override_attributes, override_attributes)
- end
-
- end
-
- context "when the included policy has attributes that conflict with the including policy, but provide the same value" do
- let(:included_policy_override_attributes) { override_attributes }
-
- it "emits a lockfile with the attributes from both merged" do
- expect(policyfile_lock.to_lock["override_attributes"]).to eq(override_attributes)
- end
-
- end
-
- context "when the included policy has attributes that conflict with the including policy's attributes" do
- let(:included_policy_override_attributes) do
- {
- "shared" => {
- "foo" => "not_bar",
- },
- }
- end
-
- it "raises an error describing all attribute conflicts" do
- expect { policyfile_lock.to_lock }.to raise_error(
- ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
- "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]")
- end
- end
- end
- end
-
- context "when several policies are included" do
- let(:included_policy_2_default_attributes) { {} }
- let(:included_policy_2_override_attributes) { {} }
- let(:included_policy_2_expanded_named_runlist) { nil }
- let(:included_policy_2_expanded_runlist) { ["recipe[cookbookA::default]"] }
- let(:included_policy_2_cookbooks) do
- [
- {
- name: "cookbookA",
- version: "2.0.0",
- },
- ]
- end
-
- let(:included_policy_2_lock_data) do
- cookbook_locks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info|
- acc[cookbook_info[:name]] = {
- "version" => cookbook_info[:version],
- "identifier" => "identifier",
- "dotted_decimal_identifier" => "dotted_decimal_identifier",
- "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}",
- "origin" => "uri",
- "source_options" => {},
- }
- acc
- end
-
- solution_dependencies_lock = included_policy_2_cookbooks.map do |cookbook_info|
- [cookbook_info[:name], cookbook_info[:version]]
- end
-
- solution_dependencies_cookbooks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info|
- acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]]
- acc
- end
-
- {
- "name" => "included_policy_2file",
- "revision_id" => "myrevisionid",
- "run_list" => included_policy_2_expanded_runlist,
- "cookbook_locks" => cookbook_locks,
- "default_attributes" => included_policy_2_default_attributes,
- "override_attributes" => included_policy_2_override_attributes,
- "solution_dependencies" => {
- "Policyfile" => solution_dependencies_lock,
- "dependencies" => solution_dependencies_cookbooks,
- },
- }.tap do |core|
- core["named_run_lists"] = included_policy_2_expanded_named_runlist if included_policy_2_expanded_named_runlist
- end
- end
-
- let(:included_policy_2_lock_name) { "included2" }
- let(:included_policy_2_fetcher) do
- instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double|
- allow(double).to receive(:lock_data).and_return(included_policy_2_lock_data)
- allow(double).to receive(:valid?).and_return(true)
- allow(double).to receive(:errors).and_return([])
- end
- end
-
- let(:included_policy_2_lock_spec) do
- ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_2_lock_name, lock_source_options, nil).tap do |spec|
- allow(spec).to receive(:valid?).and_return(true)
- allow(spec).to receive(:fetcher).and_return(included_policy_2_fetcher)
- allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options)
- end
- end
-
- let(:included_policies) { [included_policy_lock_spec, included_policy_2_lock_spec] }
-
- let(:run_list) { ["local::default"] }
-
- context "when no cookbooks are shared as dependencies or transitive dependencies by included policies" do
- let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] }
- let(:included_policy_cookbooks) do
- [
- {
- name: "cookbookA",
- version: "2.0.0",
- },
- ]
- end
-
- let(:included_policy_2_expanded_runlist) { ["recipe[cookbookB::default]"] }
- let(:included_policy_2_cookbooks) do
- [
- {
- name: "cookbookB",
- version: "2.0.0",
- },
- ]
- end
-
- it "does not raise a have conflicting dependency requirements error" do
- expect { policyfile_lock.to_lock }.not_to raise_error
- end
-
- it "emits a lockfile with the correct dependencies" do
- expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({
- "cookbookA (2.0.0)" => [],
- "cookbookB (2.0.0)" => [],
- "cookbookC (1.0.0)" => [],
- "local (1.0.0)" => [["cookbookC", "= 1.0.0"]],
- })
- end
- end
-
- context "when some cookbooks appear as dependencies or transitive dependencies of some included policies" do
- let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] }
- let(:included_policy_2_expanded_runlist) { ["recipe[cookbookC::default]"] }
-
- context "and the locked versions of the cookbooks match" do
- let(:included_policy_cookbooks) do
- [
- {
- name: "cookbookC",
- version: "1.0.0",
- },
- ]
- end
-
- let(:included_policy_2_cookbooks) do
- [
- {
- name: "cookbookC",
- version: "1.0.0",
- },
- ]
- end
-
- it "solves the dependencies with the matching versions" do
- expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({
- "cookbookC (1.0.0)" => [],
- "local (1.0.0)" => [["cookbookC", "= 1.0.0"]],
- })
- end
- end
-
- context "and the locked versions of the cookbooks do not match" do
- let(:included_policy_cookbooks) do
- [
- {
- name: "cookbookC",
- version: "1.0.0",
- },
- ]
- end
-
- let(:included_policy_2_cookbooks) do
- [
- {
- name: "cookbookC",
- version: "2.0.0",
- },
- ]
- end
-
- it "raises an error describing the conflict" do
- expect { policyfile_lock }.to raise_error(
- ChefDK::Policyfile::IncludedPoliciesCookbookSource::ConflictingCookbookVersions,
- /Multiple versions provided for cookbook cookbookC/
- )
- end
- end
- end
-
- context "when default attributes are specified" do
- context "when the included policies do not have conflicting attributes" do
- let(:included_policy_default_attributes) do
- {
- "not_conflict" => {
- "foo" => "bar",
- },
- }
- end
- let(:included_policy_2_default_attributes) do
- {
- "not_conflict" => {
- "foo" => "bar",
- },
- }
- end
- let(:default_attributes) do
- {
- "not_conflict" => {
- "bar" => "baz",
- },
- }
- end
-
- it "emits a lockfile with the included policies' attributes merged" do
- expect(policyfile_lock.to_lock["default_attributes"]).to eq({
- "not_conflict" => {
- "foo" => "bar",
- "bar" => "baz",
- },
- })
- end
- end
-
- context "when the included policies have conflicting attributes" do
- let(:included_policy_default_attributes) do
- {
- "conflict" => {
- "foo" => "bar",
- },
- }
- end
-
- let(:included_policy_2_default_attributes) do
- {
- "conflict" => {
- "foo" => "baz",
- },
- }
- end
-
- it "raises an error describing the conflict" do
- expect { policyfile_lock }.to raise_error(
- ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
- "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]")
- end
- end
- end
-
- context "when override attributes are specified" do
- context "when the included policies do not have conflicting attributes" do
- let(:included_policy_override_attributes) do
- {
- "not_conflict" => {
- "foo" => "bar",
- },
- }
- end
- let(:included_policy_2_override_attributes) do
- {
- "not_conflict" => {
- "foo" => "bar",
- },
- }
- end
- let(:override_attributes) do
- {
- "not_conflict" => {
- "bar" => "baz",
- },
- }
- end
-
- it "emits a lockfile with the included policies' attributes merged" do
- expect(policyfile_lock.to_lock["override_attributes"]).to eq({
- "not_conflict" => {
- "foo" => "bar",
- "bar" => "baz",
- },
- })
- end
- end
-
- context "when the included policies have conflicting attributes" do
- let(:included_policy_override_attributes) do
- {
- "conflict" => {
- "foo" => "bar",
- },
- }
- end
-
- let(:included_policy_2_override_attributes) do
- {
- "conflict" => {
- "foo" => "baz",
- },
- }
- end
-
- it "raises an error describing the conflict" do
- expect { policyfile_lock }.to raise_error(
- ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
- "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]")
- end
- end
- end
-
- end
-end
+# -*- coding: UTF-8 -*-
+#
+# Copyright:: Copyright (c) 2014-2018, Chef Software Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "spec_helper"
+require "chef-dk/policyfile_compiler"
+require "chef-dk/exceptions"
+
+describe ChefDK::PolicyfileCompiler, "including upstream policy locks" do
+
+ def expand_run_list(r)
+ r.map do |item|
+ "recipe[#{item}]"
+ end
+ end
+
+ let(:run_list) { ["local::default"] }
+ let(:run_list_expanded) { expand_run_list(run_list) }
+ let(:named_run_list) { {} }
+ let(:named_run_list_expanded) do
+ named_run_list.inject({}) do |acc, (key, val)|
+ acc[key] = expand_run_list(val)
+ acc
+ end
+ end
+ let(:default_attributes) { {} }
+ let(:override_attributes) { {} }
+
+ let(:default_source) { nil }
+
+ let(:external_cookbook_universe) do
+ {
+ "cookbookA" => {
+ "1.0.0" => [ ],
+ "2.0.0" => [ ],
+ },
+ "cookbookB" => {
+ "1.0.0" => [ ],
+ "2.0.0" => [ ],
+ },
+ "cookbookC" => {
+ "1.0.0" => [ ],
+ "2.0.0" => [ ],
+ },
+ "local" => {
+ "1.0.0" => [ ["cookbookC", "= 1.0.0" ] ],
+ },
+ "local_easy" => {
+ "1.0.0" => [ ["cookbookC", "= 2.0.0" ] ],
+ },
+ }
+ end
+
+ let(:included_policy_default_attributes) { {} }
+ let(:included_policy_override_attributes) { {} }
+ let(:included_policy_expanded_named_runlist) { nil }
+ let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] }
+ let(:included_policy_cookbooks) do
+ [
+ {
+ name: "cookbookA",
+ version: "2.0.0",
+ },
+ ]
+ end
+
+ let(:included_policy_lock_data) do
+ cookbook_locks = included_policy_cookbooks.inject({}) do |acc, cookbook_info|
+ acc[cookbook_info[:name]] = {
+ "version" => cookbook_info[:version],
+ "identifier" => "identifier",
+ "dotted_decimal_identifier" => "dotted_decimal_identifier",
+ "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}",
+ "origin" => "uri",
+ "source_options" => {},
+ }
+ acc
+ end
+
+ solution_dependencies_lock = included_policy_cookbooks.map do |cookbook_info|
+ [cookbook_info[:name], cookbook_info[:version]]
+ end
+
+ solution_dependencies_cookbooks = included_policy_cookbooks.inject({}) do |acc, cookbook_info|
+ acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]]
+ acc
+ end
+
+ {
+ "name" => "included_policyfile",
+ "revision_id" => "myrevisionid",
+ "run_list" => included_policy_expanded_runlist,
+ "cookbook_locks" => cookbook_locks,
+ "default_attributes" => included_policy_default_attributes,
+ "override_attributes" => included_policy_override_attributes,
+ "solution_dependencies" => {
+ "Policyfile" => solution_dependencies_lock,
+ "dependencies" => solution_dependencies_cookbooks,
+ },
+ }.tap do |core|
+ core["named_run_lists"] = included_policy_expanded_named_runlist if included_policy_expanded_named_runlist
+ end
+ end
+
+ let(:included_policy_lock_name) { "included" }
+ let(:included_policy_fetcher) do
+ instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double|
+ allow(double).to receive(:lock_data).and_return(included_policy_lock_data)
+ allow(double).to receive(:valid?).and_return(true)
+ allow(double).to receive(:errors).and_return([])
+ end
+ end
+
+ let(:lock_source_options) { { :path => "somelocation" } }
+ let(:included_policy_lock_spec) do
+ ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_lock_name, lock_source_options, nil).tap do |spec|
+ allow(spec).to receive(:valid?).and_return(true)
+ allow(spec).to receive(:fetcher).and_return(included_policy_fetcher)
+ allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options)
+ end
+ end
+
+ let(:included_policies) { [] }
+
+ let(:policyfile) do
+ policyfile = ChefDK::PolicyfileCompiler.new.build do |p|
+ if default_source
+ p.default_source.replace([default_source])
+ else
+ allow(p.default_source.first).to receive(:universe_graph).and_return(external_cookbook_universe)
+ allow(p.default_source.first).to receive(:null?).and_return(false)
+ end
+ p.run_list(*run_list)
+
+ named_run_list.each do |name, run_list|
+ p.named_run_list(name, *run_list)
+ end
+
+ default_attributes.each do |(name, value)|
+ p.default[name] = value
+ end
+
+ override_attributes.each do |(name, value)|
+ p.override[name] = value
+ end
+
+ allow(p).to receive(:included_policies).and_return(included_policies)
+ end
+
+ policyfile
+ end
+
+ let(:policyfile_lock) do
+ policyfile.lock
+ end
+
+ context "when no policies are included" do
+
+ it "does not emit included policies information in the lockfile" do
+ expect(policyfile_lock.to_lock["included_policies"]).to eq(nil)
+ end
+
+ end
+
+ context "when one policy is included" do
+
+ let(:included_policies) { [included_policy_lock_spec] }
+
+ # currently you must have a run list in a policyfile, but it should now
+ # become possible to make a combo-policy just by combining other policies
+ context "when the including policy does not have a run list" do
+ let(:run_list) { [] }
+
+ it "emits a lockfile with an identical run list as the included policy" do
+ expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist)
+ end
+
+ end
+
+ context "when the including policy has a run list" do
+
+ it "appends run list items from the including policy to the included policy's run list, removing duplicates" do
+ expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist + run_list_expanded)
+ end
+
+ end
+
+ context "when the policies have named run lists" do
+
+ let(:included_policy_expanded_named_runlist) do
+ {
+ "shared" => ["recipe[cookbookA::included]"],
+ }
+ end
+
+ context "and no named run lists are shared between the including and included policy" do
+
+ let(:named_run_list) do
+ {
+ "local" => ["local::foo"],
+ }
+ end
+
+ it "preserves the named run lists as given in both policies" do
+ expect(policyfile_lock.to_lock["named_run_lists"]).to include(included_policy_expanded_named_runlist, named_run_list_expanded)
+ end
+
+ end
+
+ context "and some named run lists are shared between the including and included policy" do
+
+ let(:named_run_list) do
+ {
+ "shared" => ["local::foo"],
+ }
+ end
+
+ it "appends run lists items from the including policy's run lists to the included policy's run lists" do
+ expect(policyfile_lock.to_lock["named_run_lists"]["shared"]).to eq(included_policy_expanded_named_runlist["shared"] + named_run_list_expanded["shared"])
+ end
+
+ end
+
+ end
+
+ context "when no cookbooks are shared as dependencies or transitive dependencies" do
+ let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] }
+ let(:run_list) { ["cookbookA::default"] }
+
+ it "does not raise a have conflicting dependency requirements error" do
+ expect { policyfile_lock.to_lock }.not_to raise_error
+ end
+
+ it "emits a lockfile where cookbooks pulled from the upstream are at identical versions" do
+ expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to(
+ have_key("cookbookC (2.0.0)"))
+ end
+ end
+
+ context "when some cookbooks are shared as dependencies or transitive dependencies" do
+ let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] }
+ let(:included_policy_cookbooks) do
+ [
+ {
+ name: "cookbookC",
+ version: "2.0.0",
+ },
+ ]
+ end
+
+ context "and the including policy does not specify any sources" do
+ let(:run_list) { [] }
+ it "it defaults to those provided in the included policy lock" do
+ expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to(
+ have_key("cookbookC (2.0.0)"))
+ end
+ end
+
+ context "and the including policy specifies a source that is equivalent to the included policy" do
+ let(:run_list) { [] }
+ let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") }
+
+ before do
+ allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"])
+ allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({})
+ allow(default_source).to receive(:null?).and_return(false)
+ allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe)
+ allow(default_source).to receive(:desc).and_return("source double")
+ end
+
+ it "it defaults to those provided in the included policy lock" do
+ expect { policyfile_lock.to_lock }.not_to raise_error
+ end
+ end
+
+ context "and the including policy specifies a source that is not equivalent to the included policy" do
+ let(:run_list) { [] }
+ let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") }
+
+ before do
+ allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"])
+ allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({ "foo" => "bar" })
+ allow(default_source).to receive(:null?).and_return(false)
+ allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe)
+ allow(default_source).to receive(:desc).and_return("source double")
+ end
+
+ it "it raises an error" do
+ expect { policyfile_lock.to_lock }.to raise_error(ChefDK::IncludePolicyCookbookSourceConflict)
+ end
+ end
+
+ context "and the including policy's dependencies can be solved with the included policy's locks" do
+ let(:run_list) { ["local_easy::default"] }
+
+ it "solves the dependencies added by the top-level policyfile and emits them in the lockfile" do
+ expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to(
+ have_key("cookbookC (2.0.0)"))
+ end
+
+ end
+
+ context "and the including policy's dependencies cannot be solved with the included policy's locks" do
+ let(:run_list) { ["local::default"] }
+
+ it "raises an error describing the conflict" do
+ expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError)
+ end
+
+ it "includes the source of the conflicting dependency constraint from the including policy" do
+ expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError) do |e|
+ expect(e.to_s).to match(/`cookbookC \(= 2.0.0\)`/) # This one comes from the included policy
+ expect(e.to_s).to match(/`cookbookC \(= 1.0.0\)` required by `local-1.0.0`/) # This one comes from the included policy
+ end
+ end
+ end
+ end
+
+ context "when default attributes are specified" do
+ let(:default_attributes) do
+ {
+ "shared" => {
+ "foo" => "bar",
+ },
+ }
+ end
+
+ context "when the included policy does not have attributes that conflict with the including policy" do
+ let(:included_policy_default_attributes) do
+ {
+ "not_shared" => {
+ "foo" => "bar",
+ },
+ }
+ end
+
+ it "emits a lockfile with the attributes from both merged" do
+ expect(policyfile_lock.to_lock["default_attributes"]).to include(included_policy_default_attributes, default_attributes)
+ end
+
+ end
+
+ context "when the included policy has attributes that conflict with the including policy, but provide the same value" do
+ let(:included_policy_default_attributes) { default_attributes }
+
+ it "emits a lockfile with the attributes from both merged" do
+ expect(policyfile_lock.to_lock["default_attributes"]).to eq(default_attributes)
+ end
+
+ end
+
+ context "when the included policy has attributes that conflict with the including policy's attributes" do
+ let(:included_policy_default_attributes) do
+ {
+ "shared" => {
+ "foo" => "not_bar",
+ },
+ }
+ end
+
+ it "raises an error describing all attribute conflicts" do
+ expect { policyfile_lock.to_lock }.to raise_error(
+ ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
+ "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]")
+ end
+ end
+ end
+
+ context "when override attributes are specified" do
+ let(:override_attributes) do
+ {
+ "shared" => {
+ "foo" => "bar",
+ },
+ }
+ end
+
+ context "when the included policy does not have attributes that conflict with the including policy" do
+ let(:included_policy_override_attributes) do
+ {
+ "not_shared" => {
+ "foo" => "bar",
+ },
+ }
+ end
+
+ it "emits a lockfile with the attributes from both merged" do
+ expect(policyfile_lock.to_lock["override_attributes"]).to include(included_policy_override_attributes, override_attributes)
+ end
+
+ end
+
+ context "when the included policy has attributes that conflict with the including policy, but provide the same value" do
+ let(:included_policy_override_attributes) { override_attributes }
+
+ it "emits a lockfile with the attributes from both merged" do
+ expect(policyfile_lock.to_lock["override_attributes"]).to eq(override_attributes)
+ end
+
+ end
+
+ context "when the included policy has attributes that conflict with the including policy's attributes" do
+ let(:included_policy_override_attributes) do
+ {
+ "shared" => {
+ "foo" => "not_bar",
+ },
+ }
+ end
+
+ it "raises an error describing all attribute conflicts" do
+ expect { policyfile_lock.to_lock }.to raise_error(
+ ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
+ "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]")
+ end
+ end
+ end
+ end
+
+ context "when several policies are included" do
+ let(:included_policy_2_default_attributes) { {} }
+ let(:included_policy_2_override_attributes) { {} }
+ let(:included_policy_2_expanded_named_runlist) { nil }
+ let(:included_policy_2_expanded_runlist) { ["recipe[cookbookA::default]"] }
+ let(:included_policy_2_cookbooks) do
+ [
+ {
+ name: "cookbookA",
+ version: "2.0.0",
+ },
+ ]
+ end
+
+ let(:included_policy_2_lock_data) do
+ cookbook_locks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info|
+ acc[cookbook_info[:name]] = {
+ "version" => cookbook_info[:version],
+ "identifier" => "identifier",
+ "dotted_decimal_identifier" => "dotted_decimal_identifier",
+ "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}",
+ "origin" => "uri",
+ "source_options" => {},
+ }
+ acc
+ end
+
+ solution_dependencies_lock = included_policy_2_cookbooks.map do |cookbook_info|
+ [cookbook_info[:name], cookbook_info[:version]]
+ end
+
+ solution_dependencies_cookbooks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info|
+ acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]]
+ acc
+ end
+
+ {
+ "name" => "included_policy_2file",
+ "revision_id" => "myrevisionid",
+ "run_list" => included_policy_2_expanded_runlist,
+ "cookbook_locks" => cookbook_locks,
+ "default_attributes" => included_policy_2_default_attributes,
+ "override_attributes" => included_policy_2_override_attributes,
+ "solution_dependencies" => {
+ "Policyfile" => solution_dependencies_lock,
+ "dependencies" => solution_dependencies_cookbooks,
+ },
+ }.tap do |core|
+ core["named_run_lists"] = included_policy_2_expanded_named_runlist if included_policy_2_expanded_named_runlist
+ end
+ end
+
+ let(:included_policy_2_lock_name) { "included2" }
+ let(:included_policy_2_fetcher) do
+ instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double|
+ allow(double).to receive(:lock_data).and_return(included_policy_2_lock_data)
+ allow(double).to receive(:valid?).and_return(true)
+ allow(double).to receive(:errors).and_return([])
+ end
+ end
+
+ let(:included_policy_2_lock_spec) do
+ ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_2_lock_name, lock_source_options, nil).tap do |spec|
+ allow(spec).to receive(:valid?).and_return(true)
+ allow(spec).to receive(:fetcher).and_return(included_policy_2_fetcher)
+ allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options)
+ end
+ end
+
+ let(:included_policies) { [included_policy_lock_spec, included_policy_2_lock_spec] }
+
+ let(:run_list) { ["local::default"] }
+
+ context "when no cookbooks are shared as dependencies or transitive dependencies by included policies" do
+ let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] }
+ let(:included_policy_cookbooks) do
+ [
+ {
+ name: "cookbookA",
+ version: "2.0.0",
+ },
+ ]
+ end
+
+ let(:included_policy_2_expanded_runlist) { ["recipe[cookbookB::default]"] }
+ let(:included_policy_2_cookbooks) do
+ [
+ {
+ name: "cookbookB",
+ version: "2.0.0",
+ },
+ ]
+ end
+
+ it "does not raise a have conflicting dependency requirements error" do
+ expect { policyfile_lock.to_lock }.not_to raise_error
+ end
+
+ it "emits a lockfile with the correct dependencies" do
+ expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({
+ "cookbookA (2.0.0)" => [],
+ "cookbookB (2.0.0)" => [],
+ "cookbookC (1.0.0)" => [],
+ "local (1.0.0)" => [["cookbookC", "= 1.0.0"]],
+ })
+ end
+ end
+
+ context "when some cookbooks appear as dependencies or transitive dependencies of some included policies" do
+ let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] }
+ let(:included_policy_2_expanded_runlist) { ["recipe[cookbookC::default]"] }
+
+ context "and the locked versions of the cookbooks match" do
+ let(:included_policy_cookbooks) do
+ [
+ {
+ name: "cookbookC",
+ version: "1.0.0",
+ },
+ ]
+ end
+
+ let(:included_policy_2_cookbooks) do
+ [
+ {
+ name: "cookbookC",
+ version: "1.0.0",
+ },
+ ]
+ end
+
+ it "solves the dependencies with the matching versions" do
+ expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({
+ "cookbookC (1.0.0)" => [],
+ "local (1.0.0)" => [["cookbookC", "= 1.0.0"]],
+ })
+ end
+ end
+
+ context "and the locked versions of the cookbooks do not match" do
+ let(:included_policy_cookbooks) do
+ [
+ {
+ name: "cookbookC",
+ version: "1.0.0",
+ },
+ ]
+ end
+
+ let(:included_policy_2_cookbooks) do
+ [
+ {
+ name: "cookbookC",
+ version: "2.0.0",
+ },
+ ]
+ end
+
+ it "raises an error describing the conflict" do
+ expect { policyfile_lock }.to raise_error(
+ ChefDK::Policyfile::IncludedPoliciesCookbookSource::ConflictingCookbookVersions,
+ /Multiple versions provided for cookbook cookbookC/
+ )
+ end
+ end
+ end
+
+ context "when default attributes are specified" do
+ context "when the included policies do not have conflicting attributes" do
+ let(:included_policy_default_attributes) do
+ {
+ "not_conflict" => {
+ "foo" => "bar",
+ },
+ }
+ end
+ let(:included_policy_2_default_attributes) do
+ {
+ "not_conflict" => {
+ "foo" => "bar",
+ },
+ }
+ end
+ let(:default_attributes) do
+ {
+ "not_conflict" => {
+ "bar" => "baz",
+ },
+ }
+ end
+
+ it "emits a lockfile with the included policies' attributes merged" do
+ expect(policyfile_lock.to_lock["default_attributes"]).to eq({
+ "not_conflict" => {
+ "foo" => "bar",
+ "bar" => "baz",
+ },
+ })
+ end
+ end
+
+ context "when the included policies have conflicting attributes" do
+ let(:included_policy_default_attributes) do
+ {
+ "conflict" => {
+ "foo" => "bar",
+ },
+ }
+ end
+
+ let(:included_policy_2_default_attributes) do
+ {
+ "conflict" => {
+ "foo" => "baz",
+ },
+ }
+ end
+
+ it "raises an error describing the conflict" do
+ expect { policyfile_lock }.to raise_error(
+ ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
+ "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]")
+ end
+ end
+ end
+
+ context "when override attributes are specified" do
+ context "when the included policies do not have conflicting attributes" do
+ let(:included_policy_override_attributes) do
+ {
+ "not_conflict" => {
+ "foo" => "bar",
+ },
+ }
+ end
+ let(:included_policy_2_override_attributes) do
+ {
+ "not_conflict" => {
+ "foo" => "bar",
+ },
+ }
+ end
+ let(:override_attributes) do
+ {
+ "not_conflict" => {
+ "bar" => "baz",
+ },
+ }
+ end
+
+ it "emits a lockfile with the included policies' attributes merged" do
+ expect(policyfile_lock.to_lock["override_attributes"]).to eq({
+ "not_conflict" => {
+ "foo" => "bar",
+ "bar" => "baz",
+ },
+ })
+ end
+ end
+
+ context "when the included policies have conflicting attributes" do
+ let(:included_policy_override_attributes) do
+ {
+ "conflict" => {
+ "foo" => "bar",
+ },
+ }
+ end
+
+ let(:included_policy_2_override_attributes) do
+ {
+ "conflict" => {
+ "foo" => "baz",
+ },
+ }
+ end
+
+ it "raises an error describing the conflict" do
+ expect { policyfile_lock }.to raise_error(
+ ChefDK::Policyfile::AttributeMergeChecker::ConflictError,
+ "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]")
+ end
+ end
+ end
+
+ end
+end