From 59eb0ad2e8b24b5e70dc982445e536442ec046af Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 30 Oct 2014 15:32:15 -0400 Subject: [PATCH] Add Chef Apply provisioner --- .../provisioners/chef/config/chef_apply.rb | 68 +++++++++++++ plugins/provisioners/chef/plugin.rb | 24 +++-- .../chef/provisioner/chef_apply.rb | 56 +++++++++++ templates/locales/en.yml | 11 +++ .../chef/config/chef_apply_test.rb | 98 +++++++++++++++++++ 5 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 plugins/provisioners/chef/config/chef_apply.rb create mode 100644 plugins/provisioners/chef/provisioner/chef_apply.rb create mode 100644 test/unit/plugins/provisioners/chef/config/chef_apply_test.rb diff --git a/plugins/provisioners/chef/config/chef_apply.rb b/plugins/provisioners/chef/config/chef_apply.rb new file mode 100644 index 000000000..abcd8ea6e --- /dev/null +++ b/plugins/provisioners/chef/config/chef_apply.rb @@ -0,0 +1,68 @@ +module VagrantPlugins + module Chef + module Config + class ChefApply < Vagrant.plugin("2", :config) + extend Vagrant::Util::Counter + + # The raw recipe text (as a string) to execute via chef-apply. + # @return [String] + attr_accessor :recipe + + # The path (on the guest) where the uploaded apply recipe should be + # written (/tmp/vagrant-chef-apply-#.rb). + # @return [String] + attr_accessor :upload_path + + # The Chef log level. + # @return [String] + attr_accessor :log_level + + def initialize + @recipe = UNSET_VALUE + + @log_level = UNSET_VALUE + @upload_path = UNSET_VALUE + end + + def finalize! + @recipe = nil if @recipe == UNSET_VALUE + + if @log_level == UNSET_VALUE + @log_level = :info + else + @log_level = @log_level.to_sym + end + + if @upload_path == UNSET_VALUE + counter = self.class.get_and_update_counter(:chef_apply) + @upload_path = "/tmp/vagrant-chef-apply-#{counter}" + end + end + + def validate(machine) + errors = _detected_errors + + if missing(recipe) + errors << I18n.t("vagrant.provisioners.chef.recipe_empty") + end + + if missing(log_level) + errors << I18n.t("vagrant.provisioners.chef.log_level_empty") + end + + if missing(upload_path) + errors << I18n.t("vagrant.provisioners.chef.upload_path_empty") + end + + { "chef apply provisioner" => errors } + end + + # Determine if the given string is "missing" (blank) + # @return [true, false] + def missing(obj) + obj.to_s.strip.empty? + end + end + end + end +end diff --git a/plugins/provisioners/chef/plugin.rb b/plugins/provisioners/chef/plugin.rb index 817de36a3..343a82018 100644 --- a/plugins/provisioners/chef/plugin.rb +++ b/plugins/provisioners/chef/plugin.rb @@ -10,12 +10,12 @@ module VagrantPlugins name "chef" description <<-DESC Provides support for provisioning your virtual machines with - Chef via `chef-solo` or `chef-client`. + Chef via `chef-solo`, `chef-client`, or `chef-apply`. DESC - config(:chef_solo, :provisioner) do - require_relative "config/chef_solo" - Config::ChefSolo + config(:chef_apply, :provisioner) do + require_relative "config/chef_apply" + Config::ChefApply end config(:chef_client, :provisioner) do @@ -23,14 +23,19 @@ module VagrantPlugins Config::ChefClient end + config(:chef_solo, :provisioner) do + require_relative "config/chef_solo" + Config::ChefSolo + end + config(:chef_zero, :provisioner) do require_relative "config/chef_zero" Config::ChefZero end - provisioner(:chef_solo) do - require_relative "provisioner/chef_solo" - Provisioner::ChefSolo + provisioner(:chef_apply) do + require_relative "provisioner/chef_apply" + Provisioner::ChefApply end provisioner(:chef_client) do @@ -38,6 +43,11 @@ module VagrantPlugins Provisioner::ChefClient end + provisioner(:chef_solo) do + require_relative "provisioner/chef_solo" + Provisioner::ChefSolo + end + provisioner(:chef_zero) do require_relative "provisioner/chef_zero" Provisioner::ChefZero diff --git a/plugins/provisioners/chef/provisioner/chef_apply.rb b/plugins/provisioners/chef/provisioner/chef_apply.rb new file mode 100644 index 000000000..04029f1a7 --- /dev/null +++ b/plugins/provisioners/chef/provisioner/chef_apply.rb @@ -0,0 +1,56 @@ +require "tempfile" + +module VagrantPlugins + module Chef + module Provisioner + class ChefApply < Vagrant.plugin("2", :provisioner) + def provision + command = "chef-apply" + command << " --log-level #{config.log_level}" + command << " #{config.upload_path}" + + user = @machine.ssh_info[:username] + + # Reset upload path permissions for the current ssh user + @machine.communicate.sudo("mkdir -p #{config.upload_path}") + @machine.communicate.sudo("chown -R #{user} #{config.upload_path}") + + # Upload the recipe + upload_recipe + + @machine.ui.info(I18n.t("vagrant.provisioners.chef.running_chef_apply", + script: config.path) + ) + + # Execute it with sudo + @machine.communicate.sudo(command) do |type, data| + if [:stderr, :stdout].include?(type) + # Output the data with the proper color based on the stream. + color = (type == :stdout) ? :green : :red + + # Chomp the data to avoid the newlines that the Chef outputs + @machine.env.ui.info(data.chomp, color: color, prefix: false) + end + end + end + + # Write the raw recipe contents to a tempfile and upload that to the + # machine. + def upload_recipe + # Write the raw recipe contents to a tempfile + file = Tempfile.new(["vagrant-chef-apply", ".rb"]) + file.write(config.recipe) + file.rewind + + # Upload the tempfile to the guest + destination = File.join(config.upload_path, "recipe.rb") + @machine.communicate.upload(file.path, destination) + ensure + # Delete our template + file.close + file.unlink + end + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 929a5c338..617cc3b43 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1754,10 +1754,18 @@ en: "The cookbook path '%{path}' doesn't exist. Ignoring..." json: "Generating chef JSON and uploading..." client_key_folder: "Creating folder to hold client key..." + log_level_empty: |- + The Chef provisioner requires a log level. If you did not set a + log level, this is probably a bug and should be reported. upload_validation_key: "Uploading chef client validation key..." upload_encrypted_data_bag_secret_key: "Uploading chef encrypted data bag secret key..." + recipe_empty: |- + Chef Apply provisioning requires that the `config.chef.recipe` be set + to a string containing the recipe contents you want to execute on the + guest. running_client: "Running chef-client..." running_client_again: "Running chef-client again (failed to converge)..." + running_client_apply: "Running chef-apply..." running_solo: "Running chef-solo..." running_solo_again: "Running chef-solo again (failed to converge)..." missing_shared_folders: |- @@ -1784,6 +1792,9 @@ en: server_validation_key_doesnt_exist: |- The validation key set for `config.chef.validation_key_path` does not exist! This file needs to exist so it can be uploaded to the virtual machine. + upload_path_empty: |- + The Chef Apply provisioner requires that the `config.chef.upload_path` + be set to a non-nil, non-empty value. deleting_from_server: "Deleting %{deletable} \"%{name}\" from Chef server..." file: diff --git a/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb b/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb new file mode 100644 index 000000000..4ea316443 --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb @@ -0,0 +1,98 @@ +require_relative "../../../../base" + +require Vagrant.source_root.join("plugins/provisioners/chef/config/chef_apply") + +describe VagrantPlugins::Chef::Config::ChefApply do + include_context "unit" + + subject { described_class.new } + + let(:machine) { double("machine") } + + def chef_error(key, options = {}) + I18n.t("vagrant.provisioners.chef.#{key}", options) + end + + describe "#recipe" do + it "defaults to nil" do + subject.finalize! + expect(subject.recipe).to be(nil) + end + end + + describe "#log_level" do + it "defaults to :info" do + subject.finalize! + expect(subject.log_level).to be(:info) + end + + it "is converted to a symbol" do + subject.log_level = "foo" + subject.finalize! + expect(subject.log_level).to eq(:foo) + end + end + + describe "#upload_path" do + it "defaults to /tmp/vagrant-chef-apply.rb" do + subject.finalize! + expect(subject.upload_path).to match(%r{/tmp/vagrant-chef-apply-\d+}) + end + end + + describe "#validate" do + before do + allow(machine).to receive(:env) + .and_return(double("env", + root_path: "", + )) + + subject.recipe = <<-EOH + package "foo" + EOH + end + + let(:result) { subject.validate(machine) } + let(:errors) { result["chef apply provisioner"] } + + context "when the recipe is nil" do + it "returns an error" do + subject.recipe = nil + subject.finalize! + expect(errors).to include chef_error("recipe_empty") + end + end + + context "when the recipe is empty" do + it "returns an error" do + subject.recipe = " " + subject.finalize! + expect(errors).to include chef_error("recipe_empty") + end + end + + context "when the log_level is an empty array" do + it "returns an error" do + subject.log_level = " " + subject.finalize! + expect(errors).to include chef_error("log_level_empty") + end + end + + context "when the upload_path is nil" do + it "returns an error" do + subject.upload_path = nil + subject.finalize! + expect(errors).to include chef_error("upload_path_empty") + end + end + + context "when the upload_path is an empty array" do + it "returns an error" do + subject.upload_path = " " + subject.finalize! + expect(errors).to include chef_error("upload_path_empty") + end + end + end +end