# # Copyright:: Copyright (c) 2015 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 "shared/command_with_ui_object" require "chef-dk/command/provision" describe ChefDK::Command::Provision do it_behaves_like "a command with a UI object" let(:command) do described_class.new end let(:push_service) { instance_double(ChefDK::PolicyfileServices::Push) } let(:chef_config_loader) { instance_double("Chef::WorkstationConfigLoader") } let(:chef_config) { double("Chef::Config") } let(:config_arg) { nil } before do ChefDK::ProvisioningData.reset stub_const("Chef::Config", chef_config) allow(Chef::WorkstationConfigLoader).to receive(:new).with(config_arg).and_return(chef_config_loader) end describe "evaluating CLI options and arguments" do let(:ui) { TestHelpers::TestUI.new } before do command.ui = ui end describe "when input is invalid" do context "when not enough arguments are given" do let(:params) { [] } it "prints usage and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("You must specify a POLICY_GROUP or disable policyfiles with --no-policy") end end context "when --no-policy is combined with policy arguments" do let(:params) { %w{ --no-policy some-policy-group } } it "prints usage and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("The --no-policy flag cannot be combined with policyfile arguments") end end context "when a POLICY_GROUP is given but neither of --sync or --policy-name are given" do let(:params) { %w{ some-policy-group } } it "prints usage and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("You must pass either --sync or --policy-name to provision machines in policyfile mode") end end context "when both --sync and --policy-name are given" do let(:params) { %w{ some-policy-group --policy-name foo --sync} } it "prints usage and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("The --policy-name and --sync arguments cannot be combined") end end context "when too many arguments are given" do let(:params) { %w{ policygroup extraneous-argument --sync } } it "prints usage and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("Too many arguments") end end end describe "when input is valid" do let(:context) { ChefDK::ProvisioningData.context } shared_examples "common_optional_options" do context "with default option values" do it "node name is not specified" do expect(command.node_name).to eq(nil) expect(context.node_name).to eq(nil) end it "sets the cookbook path to CWD" do # this is cookbook_path in the chef sense, a directory with cookbooks in it. expect(command.provisioning_cookbook_path).to eq(Dir.pwd) end it "sets the cookbook name to 'provision'" do expect(command.provisioning_cookbook_name).to eq("provision") end it "sets the recipe to 'default'" do expect(command.recipe).to eq("default") expect(command.chef_runner.run_list).to eq(["recipe[provision::default]"]) end it "sets the default action to converge" do expect(command.default_action).to eq(:converge) expect(context.action).to eq(:converge) end end context "with -n NODE_NAME" do let(:extra_params) { %w{ -n example-node } } it "sets the default requested node name" do expect(command.node_name).to eq("example-node") expect(context.node_name).to eq("example-node") end end context "with --cookbook COOKBOOK_PATH" do let(:extra_params) { %w{ --cookbook ~/mystuff/my-provision-cookbook } } let(:expected_cookbook_path) { File.expand_path("~/mystuff") } let(:expected_cookbook_name) { "my-provision-cookbook" } it "sets the cookbook path" do # this is cookbook_path in the chef sense, a directory with cookbooks in it. expect(command.provisioning_cookbook_path).to eq(expected_cookbook_path) end it "sets the cookbook name" do expect(command.provisioning_cookbook_name).to eq(expected_cookbook_name) end end context "with -c CONFIG_FILE" do let(:config_arg) { "~/somewhere_else/knife.rb" } let(:extra_params) { [ "-c", config_arg ] } it "loads config from the specified location" do # The configurable module uses config[:config_file] expect(command.config[:config_file]).to eq("~/somewhere_else/knife.rb") end end context "with -r MACHINE_RECIPE" do let(:extra_params) { %w{ -r ec2cluster } } it "sets the recipe to run as specified" do expect(command.recipe).to eq("ec2cluster") expect(command.chef_runner.run_list).to eq(["recipe[provision::ec2cluster]"]) end end context "with --target" do let(:extra_params) { %w{ -t 192.168.255.123 } } it "sets the target host to the given value" do expect(context.target).to eq("192.168.255.123") end end context "with --opt" do context "with one user-specified option" do let(:extra_params) { %w{ --opt color=ebfg } } it "sets the given option name to the given value" do expect(context.opts.color).to eq("ebfg") end end context "with an option given as a quoted arg with spaces" do let(:extra_params) { [ "--opt", "color = ebfg" ] } it "sets the given option name to the given value" do expect(context.opts.color).to eq("ebfg") end end context "with an option with an '=' in it" do let(:extra_params) { [ "--opt", "api_key=abcdef==" ] } it "sets the given option name to the given value" do expect(context.opts.api_key).to eq("abcdef==") end end context "with an option with a space in it" do let(:extra_params) { [ "--opt", "full_name=Bobo T. Clown" ] } it "sets the given option name to the given value" do expect(context.opts.full_name).to eq("Bobo T. Clown") end end context "with multiple options given" do let(:extra_params) { %w{ --opt color=ebfg --opt nope=seppb } } it "sets the given option name to the given value" do expect(context.opts.color).to eq("ebfg") expect(context.opts.nope).to eq("seppb") end end end context "with -d" do let(:extra_params) { %w{ -d } } it "sets the default action to destroy" do expect(command.default_action).to eq(:destroy) expect(context.action).to eq(:destroy) end end end # shared examples context "when --no-policy is given" do before do allow(chef_config_loader).to receive(:load) allow(command).to receive(:push).and_return(push_service) allow(chef_config).to receive(:ssl_verify_mode).and_return(:verify_peer) command.apply_params!(params) command.setup_context end let(:extra_params) { [] } let(:params) { %w{ --no-policy } + extra_params } it "disables policyfile integration" do expect(command.enable_policyfile?).to be(false) end it "generates chef config with no policyfile options" do expected_config = <<-CONFIG # SSL Settings: ssl_verify_mode :verify_peer CONFIG expect(context.chef_config).to eq(expected_config) end include_examples "common_optional_options" end # when --no-policy is given context "when --sync POLICYFILE argument is given" do let(:policy_data) { { "name" => "myapp" } } before do allow(chef_config_loader).to receive(:load) allow(ChefDK::PolicyfileServices::Push).to receive(:new). with(policyfile: given_policyfile_path, ui: ui, policy_group: given_policy_group, config: chef_config, root_dir: Dir.pwd). and_return(push_service) allow(push_service).to receive(:policy_data).and_return(policy_data) command.apply_params!(params) command.setup_context end context "with explicit policyfile relative path" do let(:given_policyfile_path) { "policies/OtherPolicy.rb" } let(:given_policy_group) { "some-policy-group" } let(:params) { [ given_policy_group, "--sync", given_policyfile_path ] } it "sets policy group" do expect(command.policy_group).to eq(given_policy_group) expect(context.policy_group).to eq(given_policy_group) end it "sets policy name" do expect(command.policy_name).to eq("myapp") expect(context.policy_name).to eq("myapp") end end context "with implicit policyfile relative path" do let(:given_policyfile_path) { nil } let(:given_policy_group) { "some-policy-group" } let(:extra_params) { [] } let(:params) { [ given_policy_group, "--sync" ] + extra_params } before do allow(chef_config).to receive(:ssl_verify_mode).and_return(:verify_peer) end it "sets policy group" do expect(command.policy_group).to eq(given_policy_group) expect(context.policy_group).to eq(given_policy_group) end it "sets policy name" do expect(command.policy_name).to eq("myapp") expect(context.policy_name).to eq("myapp") end it "generates chef config with policyfile options" do expected_config = <<-CONFIG # SSL Settings: ssl_verify_mode :verify_peer # Policyfile Settings: use_policyfile true policy_document_native_api true policy_group "some-policy-group" policy_name "myapp" CONFIG expect(context.chef_config).to eq(expected_config) end include_examples "common_optional_options" end end # when --sync POLICYFILE argument is given context "when a --policy-name is given" do let(:given_policy_group) { "some-policy-group" } let(:extra_params) { [] } let(:params) { [ given_policy_group, "--policy-name", "myapp" ] + extra_params } before do command.apply_params!(params) command.setup_context allow(chef_config).to receive(:ssl_verify_mode).and_return(:verify_peer) end it "sets policy group" do expect(command.policy_group).to eq(given_policy_group) expect(context.policy_group).to eq(given_policy_group) end it "sets policy name" do expect(command.policy_name).to eq("myapp") expect(context.policy_name).to eq("myapp") end it "generates chef config with policyfile options" do expected_config = <<-CONFIG # SSL Settings: ssl_verify_mode :verify_peer # Policyfile Settings: use_policyfile true policy_document_native_api true policy_group "some-policy-group" policy_name "myapp" CONFIG expect(context.chef_config).to eq(expected_config) end include_examples "common_optional_options" end end end describe "running the provision cookbook" do let(:ui) { TestHelpers::TestUI.new } before do allow(chef_config_loader).to receive(:load) allow(command).to receive(:push).and_return(push_service) command.ui = ui end let(:provision_cookbook_path) { File.expand_path("provision", Dir.pwd) } let(:provision_recipe_path) { File.join(provision_cookbook_path, "recipes", "default.rb") } let(:chef_runner) { instance_double("ChefDK::ChefRunner") } let(:params) { %w{ policygroup --sync } } context "when the provision cookbook doesn't exist" do before do allow(File).to receive(:exist?).with(provision_cookbook_path).and_return(false) end it "prints an error and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("Provisioning cookbook not found at path #{provision_cookbook_path}") end end context "when the provision cookbook doesn't have the requested recipe" do before do allow(File).to receive(:exist?).with(provision_cookbook_path).and_return(true) allow(File).to receive(:exist?).with(provision_recipe_path).and_return(false) end it "prints an error and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("Provisioning recipe not found at path #{provision_recipe_path}") end end context "when the policyfile upload fails" do let(:backtrace) { caller[0...3] } let(:cause) do e = StandardError.new("some operation failed") e.set_backtrace(backtrace) e end let(:exception) do ChefDK::PolicyfilePushError.new("push failed", cause) end before do allow(File).to receive(:exist?).with(provision_cookbook_path).and_return(true) allow(File).to receive(:exist?).with(provision_recipe_path).and_return(true) expect(push_service).to receive(:run).and_raise(exception) end it "prints an error and exits non-zero" do expected_output = <<-E Error: push failed Reason: (StandardError) some operation failed E expect(command.run(params)).to eq(1) expect(ui.output).to include(expected_output) end end context "when the chef run fails" do let(:base_exception) { StandardError.new("Something went wrong") } let(:exception) { ChefDK::ChefConvergeError.new("Chef failed to converge: #{base_exception}", base_exception) } let(:policy_data) { { "name" => "myapp" } } before do allow(File).to receive(:exist?).with(provision_cookbook_path).and_return(true) allow(File).to receive(:exist?).with(provision_recipe_path).and_return(true) allow(push_service).to receive(:policy_data).and_return(policy_data) expect(push_service).to receive(:run) allow(command).to receive(:chef_runner).and_return(chef_runner) allow(chef_runner).to receive(:cookbook_path).and_return(Dir.pwd) expect(chef_runner).to receive(:converge).and_raise(exception) end it "prints an error and exits non-zero" do expect(command.run(params)).to eq(1) expect(ui.output).to include("Error: Chef failed to converge") expect(ui.output).to include("Reason: (StandardError) Something went wrong") end end context "when the chef run is successful" do before do allow(File).to receive(:exist?).with(provision_cookbook_path).and_return(true) allow(File).to receive(:exist?).with(provision_recipe_path).and_return(true) allow(command).to receive(:chef_runner).and_return(chef_runner) allow(chef_runner).to receive(:cookbook_path).and_return(Dir.pwd) expect(chef_runner).to receive(:converge) end context "when using --no-policy" do let(:params) { %w{ --no-policy } } it "exits 0" do return_value = command.run(params) expect(ui.output).to eq("") expect(return_value).to eq(0) end end context "with --policy-name" do let(:params) { %w{ policygroup --policy-name otherapp } } it "exits 0" do return_value = command.run(params) expect(ui.output).to eq("") expect(return_value).to eq(0) end end context "with --sync" do let(:policy_data) { { "name" => "myapp" } } before do allow(push_service).to receive(:policy_data).and_return(policy_data) expect(push_service).to receive(:run) end it "exits 0" do return_value = command.run(params) expect(ui.output).to eq("") expect(return_value).to eq(0) end end end end end