summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2020-03-23 10:17:46 -0700
committerGitHub <noreply@github.com>2020-03-23 10:17:46 -0700
commit5d10babc7abe54cf556c2edfd12c6376894e1350 (patch)
tree53c177e0d47a0b3eeb551b25e2efb82be83a159f
parent85005840999a86453175d2345545bf8e8c22c3bc (diff)
parent97497d968b806727cfeb00fdef2ec3976fbd53d8 (diff)
downloadchef-5d10babc7abe54cf556c2edfd12c6376894e1350.tar.gz
Merge pull request #9507 from chef/btm/yaml
Add support for parsing YAML recipes with chef-apply
-rw-r--r--lib/chef/application/apply.rb13
-rw-r--r--lib/chef/recipe.rb25
-rw-r--r--spec/unit/application/apply_spec.rb3
-rw-r--r--spec/unit/recipe_spec.rb68
4 files changed, 95 insertions, 14 deletions
diff --git a/lib/chef/application/apply.rb b/lib/chef/application/apply.rb
index 4718e8750b..7267f4ecfa 100644
--- a/lib/chef/application/apply.rb
+++ b/lib/chef/application/apply.rb
@@ -105,6 +105,12 @@ class Chef::Application::Apply < Chef::Application
description: "Enable whyrun mode.",
boolean: true
+ option :yaml,
+ long: "--yaml",
+ description: "Parse recipe as YAML",
+ boolean: true,
+ default: false
+
option :profile_ruby,
long: "--[no-]profile-ruby",
description: "Dump complete Ruby call graph stack of entire #{Chef::Dist::PRODUCT} run (expert only).",
@@ -198,7 +204,12 @@ class Chef::Application::Apply < Chef::Application
@recipe_text, @recipe_fh = read_recipe_file @recipe_filename
end
recipe, run_context = get_recipe_and_run_context
- recipe.instance_eval(@recipe_text, @recipe_filename, 1)
+ if config[:yaml] || File.extname(@recipe_filename) == ".yml"
+ logger.info "Parsing recipe as YAML"
+ recipe.from_yaml(@recipe_text)
+ else
+ recipe.instance_eval(@recipe_text, @recipe_filename, 1)
+ end
runner = Chef::Runner.new(run_context)
catch(:end_client_run_early) do
begin
diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb
index ddb45de8e3..890edc6d13 100644
--- a/lib/chef/recipe.rb
+++ b/lib/chef/recipe.rb
@@ -1,7 +1,7 @@
#--
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
-# Copyright:: Copyright 2008-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2020, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,6 +17,7 @@
# limitations under the License.
#
+require "yaml"
require_relative "dsl/recipe"
require_relative "mixin/from_file"
require_relative "mixin/deprecation"
@@ -88,26 +89,24 @@ class Chef
def from_yaml_file(filename)
self.source_file = filename
if File.file?(filename) && File.readable?(filename)
- from_yaml(IO.read(filename))
+ yaml_contents = IO.read(filename)
+ if ::YAML.load_stream(yaml_contents).length > 1
+ raise ArgumentError, "YAML recipe '#{filename}' contains multiple documents, only one is supported"
+ end
+
+ from_yaml(yaml_contents)
else
- raise IOError, "Cannot open or read #{filename}!"
+ raise IOError, "Cannot open or read file '#{filename}'!"
end
end
def from_yaml(string)
res = ::YAML.safe_load(string)
- if res.is_a?(Hash)
- from_hash(res)
- elsif res.is_a?(Array)
- from_array(res)
- else
- raise "boom"
+ unless res.is_a?(Hash) && res.key?("resources")
+ raise ArgumentError, "YAML recipe '#{source_file}' must contain a top-level 'resources' hash (YAML sequence), i.e. 'resources:'"
end
- end
- def from_array(array)
- Chef::Log.warn "array yaml files are super duper experimental behavior"
- array.each { |e| from_hash(e) }
+ from_hash(res)
end
def from_hash(hash)
diff --git a/spec/unit/application/apply_spec.rb b/spec/unit/application/apply_spec.rb
index b659f13fe8..f071dbd981 100644
--- a/spec/unit/application/apply_spec.rb
+++ b/spec/unit/application/apply_spec.rb
@@ -46,6 +46,7 @@ describe Chef::Application::Apply do
it "should read text properly" do
expect(@app.read_recipe_file(@recipe_file_name)[0]).to eq(@recipe_text)
end
+
it "should return a file_handle" do
expect(@app.read_recipe_file(@recipe_file_name)[1]).to be_instance_of(RSpec::Mocks::Double)
end
@@ -57,6 +58,7 @@ describe Chef::Application::Apply do
@app.read_recipe_file(nil)
end
end
+
describe "when recipe doesn't exist" do
before do
allow(File).to receive(:exist?).with(@recipe_path).and_return(false)
@@ -68,6 +70,7 @@ describe Chef::Application::Apply do
end
end
end
+
describe "temp_recipe_file" do
before do
@app.instance_variable_set(:@recipe_text, @recipe_text)
diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb
index 099e6750f6..ef8fb78f90 100644
--- a/spec/unit/recipe_spec.rb
+++ b/spec/unit/recipe_spec.rb
@@ -576,6 +576,74 @@ describe Chef::Recipe do
end
end
+ describe "from_yaml_file" do
+ it "raises ArgumentError if the YAML file contains multiple documents" do
+ filename = "multiple_docs.yaml"
+ yaml = "---\n- resources:\n - type: false\n---\n-resources:\n - type: false\n"
+ allow(File).to receive(:file?).and_call_original
+ allow(File).to receive(:readable?).and_call_original
+ allow(IO).to receive(:read).and_call_original
+ allow(File).to receive(:file?).with(filename).and_return(true)
+ allow(File).to receive(:readable?).with(filename).and_return(true)
+ allow(IO).to receive(:read).with(filename).and_return(yaml)
+ expect { recipe.from_yaml_file(filename) }.to raise_error(ArgumentError, /contains multiple documents/)
+ end
+
+ it "raises IOError if the file does not exist" do
+ filename = "/nonexistent"
+ allow(File).to receive(:file?).and_call_original
+ allow(File).to receive(:file?).with(filename).and_return(false)
+ expect { recipe.from_yaml_file(filename) }.to raise_error(IOError, /Cannot open or read/)
+ end
+ end
+
+ describe "from_yaml" do
+ it "raises ArgumentError if the YAML is not a top-level hash" do
+ yaml = <<~YAML
+ ---
+ - one
+ - resources
+ - three
+ YAML
+ expect { recipe.from_yaml(yaml) }.to raise_error(ArgumentError, /must contain a top-level 'resources' hash/)
+ end
+
+ it "raises ArgumentError if the YAML does not contain a resources hash" do
+ yaml = <<~YAML
+ ---
+ - airplanes:
+ - type: "execute"
+ command: "whoami"
+ YAML
+ expect { recipe.from_yaml(yaml) }.to raise_error(ArgumentError, /must contain a top-level 'resources' hash/)
+ end
+
+ it "does not raise if the YAML contains a resources hash" do
+ yaml = <<~YAML
+ ---
+ resources:
+ - type: "execute"
+ command: "whoami"
+ YAML
+ expect(recipe).to receive(:from_hash).with({ "resources" => [{ "command" => "whoami", "type" => "execute" }] })
+ recipe.from_yaml(yaml)
+ end
+ end
+
+ describe "from_hash" do
+ it "declares resources from a hash" do
+ resources = { "resources" => [
+ { "name" => "running some commands", "type" => "execute", "command" => "whoami" },
+ { "name" => "preparing the bits", "type" => "service", "action" => "start", "service_name" => "bit_launcher" },
+ ] }
+
+ recipe.from_hash(resources)
+ expect(recipe.resources(execute: "running some commands").command).to eql("whoami")
+ expect(recipe.resources(service: "preparing the bits").service_name).to eql("bit_launcher")
+ expect(recipe.resources(service: "preparing the bits").action).to eql([:start])
+ end
+ end
+
describe "included DSL" do
it "should respond to :ps_credential from Chef::DSL::Powershell" do
expect(recipe.respond_to?(:ps_credential)).to be true