summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2015-02-09 20:31:30 -0800
committerLamont Granquist <lamont@scriptkiddie.org>2015-02-11 19:12:07 -0800
commit1aa9b128d22a21a39e6d7b7a833538ae3e15929d (patch)
tree0ff07fef992db67a7a1e899a35484fa0c6f292a7
parentb5243fb3a5151d65f3caa689d01a573809c441f2 (diff)
downloadchef-1aa9b128d22a21a39e6d7b7a833538ae3e15929d.tar.gz
validatorless bootstraps and chef-vault integration
-rw-r--r--lib/chef/knife/bootstrap.rb96
-rw-r--r--lib/chef/knife/bootstrap/chef_vault_handler.rb165
-rw-r--r--lib/chef/knife/bootstrap/client_builder.rb190
-rw-r--r--lib/chef/knife/bootstrap/templates/archlinux-gems.erb9
-rw-r--r--lib/chef/knife/bootstrap/templates/chef-aix.erb9
-rw-r--r--lib/chef/knife/bootstrap/templates/chef-full.erb9
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb8
-rw-r--r--spec/unit/knife/bootstrap/chef_vault_handler_spec.rb153
-rw-r--r--spec/unit/knife/bootstrap/client_builder_spec.rb178
-rw-r--r--spec/unit/knife/bootstrap_spec.rb76
-rw-r--r--spec/unit/knife/core/bootstrap_context_spec.rb1
11 files changed, 864 insertions, 30 deletions
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
index dc41e67c8d..79d9db0e97 100644
--- a/lib/chef/knife/bootstrap.rb
+++ b/lib/chef/knife/bootstrap.rb
@@ -19,12 +19,17 @@
require 'chef/knife'
require 'chef/knife/data_bag_secret_options'
require 'erubis'
+require 'chef/knife/bootstrap/chef_vault_handler'
+require 'chef/knife/bootstrap/client_builder'
class Chef
class Knife
class Bootstrap < Knife
include DataBagSecretOptions
+ attr_accessor :client_builder
+ attr_accessor :chef_vault_handler
+
deps do
require 'chef/knife/core/bootstrap_context'
require 'chef/json_compat'
@@ -194,10 +199,54 @@ class Chef
:description => "Verify the SSL cert for HTTPS requests to the Chef server API.",
:boolean => true
+ option :vault_file,
+ :long => '--vault-file VAULT_FILE',
+ :description => 'A JSON file with a list of vault(s) and item(s) to be updated'
+
+ option :vault_list,
+ :long => '--vault-list VAULT_LIST',
+ :description => 'A JSON string with the vault(s) and item(s) to be updated'
+
+ option :vault_item,
+ :long => '--vault-item VAULT_ITEM',
+ :description => 'A single vault and item to update as "vault:item"',
+ :proc => Proc.new { |i|
+ (vault, item) = i.split(/:/)
+ vault_item ||= {}
+ vault_item[vault] ||= []
+ vault_item[vault].push(item)
+ }
+
+ def initialize(argv=[])
+ super
+ @client_builder = Chef::Knife::Bootstrap::ClientBuilder.new(
+ chef_config: Chef::Config,
+ knife_config: config,
+ ui: ui,
+ )
+ @chef_vault_handler = Chef::Knife::Bootstrap::ChefVaultHandler.new(
+ knife_config: config,
+ ui: ui
+ )
+ end
+
+ # The default bootstrap template to use to bootstrap a server This is a public API hook
+ # which knife plugins use or inherit and override.
+ #
+ # @return [String] Default bootstrap template
def default_bootstrap_template
"chef-full"
end
+ # The server_name is the DNS or IP we are going to connect to, it is not necessarily
+ # the node name, the fqdn, or the hostname of the server. This is a public API hook
+ # which knife plugins use or inherit and override.
+ #
+ # @return [String] The DNS or IP that bootstrap will connect to
+ def server_name
+ Array(@name_args).first
+ end
+
def bootstrap_template
# The order here is important. We want to check if we have the new Chef 12 option is set first.
# Knife cloud plugins unfortunately all set a default option for the :distro so it should be at
@@ -237,20 +286,45 @@ class Chef
template_file
end
+ def secret
+ @secret ||= encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil
+ end
+
+ def bootstrap_context
+ @bootstrap_context ||= Knife::Core::BootstrapContext.new(
+ config,
+ config[:run_list],
+ Chef::Config,
+ secret
+ )
+ end
+
def render_template
template_file = find_template
template = IO.read(template_file).chomp
- secret = encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil
- context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config, secret)
- Erubis::Eruby.new(template).evaluate(context)
+ Erubis::Eruby.new(template).evaluate(bootstrap_context)
end
def run
validate_name_args!
- @node_name = Array(@name_args).first
$stdout.sync = true
- ui.info("Connecting to #{ui.color(@node_name, :bold)}")
+
+ # chef-vault integration must use the new client-side hawtness, otherwise to use the
+ # new client-side hawtness, just delete your validation key.
+ if chef_vault_handler.doing_chef_vault? || !File.exist?(File.expand_path(Chef::Config[:validation_key]))
+ client_builder.run
+
+ chef_vault_handler.run(node_name: config[:chef_node_name])
+
+ bootstrap_context.client_pem = client_builder.client_path
+ else
+ ui.info("Doing old-style registration with a validation key...")
+ ui.info("Delete your validation key in order to use your user credentials instead")
+ ui.info("")
+ end
+
+ ui.info("Connecting to #{ui.color(server_name, :bold)}")
begin
knife_ssh.run
@@ -265,24 +339,19 @@ class Chef
end
def validate_name_args!
- if Array(@name_args).first.nil?
+ if server_name.nil?
ui.error("Must pass an FQDN or ip to bootstrap")
exit 1
- elsif Array(@name_args).first == "windows"
+ elsif server_name == "windows"
+ # catches "knife bootstrap windows" when that command is not installed
ui.warn("Hostname containing 'windows' specified. Please install 'knife-windows' if you are attempting to bootstrap a Windows node via WinRM.")
end
end
- def server_name
- Array(@name_args).first
- end
-
def knife_ssh
ssh = Chef::Knife::Ssh.new
ssh.ui = ui
ssh.name_args = [ server_name, ssh_command ]
-
- # command line arguments and config file values are now merged into config in Chef::Knife#merge_configs
ssh.config[:ssh_user] = config[:ssh_user]
ssh.config[:ssh_password] = config[:ssh_password]
ssh.config[:ssh_port] = config[:ssh_port]
@@ -311,7 +380,6 @@ class Chef
command
end
-
end
end
end
diff --git a/lib/chef/knife/bootstrap/chef_vault_handler.rb b/lib/chef/knife/bootstrap/chef_vault_handler.rb
new file mode 100644
index 0000000000..c421d2cb15
--- /dev/null
+++ b/lib/chef/knife/bootstrap/chef_vault_handler.rb
@@ -0,0 +1,165 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) 2015 Opscode, 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.
+#
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+ class ChefVaultHandler
+
+ # @return [Hash] knife merged config, typically @config
+ attr_accessor :knife_config
+
+ # @return [Chef::Knife::UI] ui object for output
+ attr_accessor :ui
+
+ # @return [String] name of the node (technically name of the client)
+ attr_reader :node_name
+
+ # @param knife_config [Hash] knife merged config, typically @config
+ # @param ui [Chef::Knife::UI] ui object for output
+ def initialize(knife_config: {}, ui: nil)
+ @knife_config = knife_config
+ @ui = ui
+ end
+
+ # Updates the chef vault items for the newly created node.
+ #
+ # @param node_name [String] name of the node (technically name of the client)
+ # @todo: node_name should be mandatory (ruby 2.0 compat)
+ def run(node_name: nil)
+ return unless doing_chef_vault?
+
+ sanity_check
+
+ @node_name = node_name
+
+ ui.info("Updating Chef Vault, waiting for client to be searchable..") while wait_for_client
+
+ update_vault_list!
+ end
+
+ # Iterate through all the vault items to update. Items may be either a String
+ # or an Array of Strings:
+ #
+ # {
+ # "vault1": "item",
+ # "vault2": [ "item1", "item2", "item2" ]
+ # }
+ #
+ def update_vault_list!
+ vault_json.each do |vault, items|
+ [ items ].flatten.each do |item|
+ update_vault(vault, item)
+ end
+ end
+ end
+
+ # @return [Boolean] if we've got chef vault options to act on or not
+ def doing_chef_vault?
+ !!(vault_list || vault_file || vault_item)
+ end
+
+ private
+
+ # warn if the user has given mutual conflicting options
+ def sanity_check
+ if vault_item && (vault_list || vault_file)
+ ui.warn "--vault-item given with --vault-list or --vault-file, ignoring the latter"
+ end
+
+ if vault_list && vault_file
+ ui.warn "--vault-list given with --vault-file, ignoring the latter"
+ end
+ end
+
+ # @return [String] string with serialized JSON representing the chef vault items
+ def vault_list
+ knife_config[:vault_list]
+ end
+
+ # @return [String] JSON text in a file representing the chef vault items
+ def vault_file
+ knife_config[:vault_file]
+ end
+
+ # @return [Hash] Ruby object representing the chef vault items to create
+ def vault_item
+ knife_config[:vault_item]
+ end
+
+ # Helper to return a ruby object represeting all the data bags and items
+ # to update via chef-vault.
+ #
+ # @return [Hash] deserialized ruby hash with all the vault items
+ def vault_json
+ @vault_json ||=
+ begin
+ if vault_item
+ vault_item
+ else
+ json = vault_list ? vault_list : File.read(vault_file)
+ Chef::JSONCompat.from_json(json)
+ end
+ end
+ end
+
+ # Update an individual vault item and save it
+ #
+ # @param vault [String] name of the chef-vault encrypted data bag
+ # @param item [String] name of the chef-vault encrypted item
+ def update_vault(vault, item)
+ require_chef_vault!
+ vault_item = load_chef_vault_item(vault, item)
+ vault_item.clients("name:#{node_name}")
+ vault_item.save
+ end
+
+ # Hook to stub out ChefVault
+ #
+ # @param vault [String] name of the chef-vault encrypted data bag
+ # @param item [String] name of the chef-vault encrypted item
+ # @returns [ChefVault::Item] ChefVault::Item object
+ def load_chef_vault_item(vault, item)
+ ChefVault::Item.load(vault, item)
+ end
+
+ public :load_chef_vault_item # for stubbing
+
+ # Helper used to spin waiting for the client to appear in search.
+ #
+ # @return [Boolean] true if the client is searchable
+ def wait_for_client
+ sleep 1
+ !Chef::Search::Query.new.search(:client, "name:#{node_name}")[0]
+ end
+
+ # Helper to very lazily require the chef-vault gem
+ def require_chef_vault!
+ @require_chef_vault ||=
+ begin
+ require 'chef-vault'
+ true
+ rescue LoadError
+ raise "Knife bootstrap cannot configure chef vault items when the chef-vault gem is not installed"
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/bootstrap/client_builder.rb b/lib/chef/knife/bootstrap/client_builder.rb
new file mode 100644
index 0000000000..b9c1d98bec
--- /dev/null
+++ b/lib/chef/knife/bootstrap/client_builder.rb
@@ -0,0 +1,190 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) 2015 Opscode, 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 'chef/node'
+require 'chef/rest'
+require 'chef/api_client/registration'
+require 'chef/api_client'
+require 'tmpdir'
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+ class ClientBuilder
+
+ # @return [Hash] knife merged config, typically @config
+ attr_accessor :knife_config
+ # @return [Hash] chef config object
+ attr_accessor :chef_config
+ # @return [Chef::Knife::UI] ui object for output
+ attr_accessor :ui
+
+ # @param knife_config [Hash] Hash of knife config settings
+ # @param chef_config [Hash] Hash of chef config settings
+ # @param ui [Chef::Knife::UI] UI object for output
+ def initialize(knife_config: {}, chef_config: {}, ui: nil)
+ @knife_config = knife_config
+ @chef_config = chef_config
+ @ui = ui
+ end
+
+ # Main entry. Prompt the user to clean up any old client or node objects. Then create
+ # the new client, then create the new node.
+ def run
+ sanity_check
+
+ ui.info("Creating new client for #{node_name}")
+
+ create_client!
+
+ ui.info("Creating new node for #{node_name}")
+
+ create_node!
+ end
+
+ # Tempfile to use to write newly created client credentials to.
+ #
+ # This method is public so that the knife bootstrapper can read then and pass the value into
+ # the handler for chef vault which needs the client cert we create here.
+ #
+ # We hang onto the tmpdir as an ivar as well so that it will not get GC'd and removed
+ #
+ # @return [String] path to the generated client.pem
+ def client_path
+ @client_path ||=
+ begin
+ @tmpdir = Dir.mktmpdir
+ File.join(@tmpdir, "#{node_name}.pem")
+ end
+ end
+
+ private
+
+ # @return [String] node name from the knife_config
+ def node_name
+ knife_config[:chef_node_name]
+ end
+
+ # @return [String] enviroment from the knife_config
+ def environment
+ knife_config[:environment]
+ end
+
+ # @return [String] run_list from the knife_config
+ def run_list
+ knife_config[:run_list]
+ end
+
+ # @return [Hash,Array] Object representation of json first-boot attributes from the knife_config
+ def first_boot_attributes
+ knife_config[:first_boot_attributes]
+ end
+
+ # @return [String] chef server url from the Chef::Config
+ def chef_server_url
+ chef_config[:chef_server_url]
+ end
+
+ # Accesses the run_list and coerces it into an Array, changing nils into
+ # the empty Array, and splitting strings representations of run_lists into
+ # Arrays.
+ #
+ # @return [Array] run_list coerced into an array
+ def normalized_run_list
+ case run_list
+ when nil
+ []
+ when String
+ run_list.split(/\s*,\s*/)
+ when Array
+ run_list
+ end
+ end
+
+ # Create the client object and save it to the Chef API
+ def create_client!
+ Chef::ApiClient::Registration.new(node_name, client_path, http_api: rest).run
+ end
+
+ # Create the node object (via the lazy accessor) and save it to the Chef API
+ def create_node!
+ node.save
+ end
+
+ # Create a new Chef::Node. Supports creating the node with its name, run_list, attributes
+ # and environment. This injects a rest object into the Chef::Node which uses the client key
+ # for authentication so that the client creates the node and therefore we get the acls setup
+ # correctly.
+ #
+ # @return [Chef::Node] new chef node to create
+ def node
+ @node ||=
+ begin
+ node = Chef::Node.new(chef_server_rest: client_rest)
+ node.name(node_name)
+ node.run_list(normalized_run_list)
+ node.normal_attrs = first_boot_attributes if first_boot_attributes
+ node.environment(environment) if environment
+ node
+ end
+ end
+
+ # Check for the existence of a node and/or client already on the server. If the node
+ # already exists, we must delete it in order to proceed so that we can create a new node
+ # object with the permissions of the new client. There is a use case for creating a new
+ # client and wiring it up to a precreated node object, but we do currently support that.
+ #
+ # We prompt the user about what to do and will fail hard if we do not get confirmation to
+ # delete any prior node/client objects.
+ def sanity_check
+ if resource_exists?("nodes/#{node_name}")
+ ui.confirm("Node #{node_name} exists, overwrite it")
+ rest.delete("nodes/#{node_name}")
+ end
+ if resource_exists?("clients/#{node_name}")
+ ui.confirm("Client #{node_name} exists, overwrite it")
+ rest.delete("clients/#{node_name}")
+ end
+ end
+
+ # Check if an relative path exists on the chef server
+ #
+ # @param relative_path [String] URI path relative to the chef organization
+ # @return [Boolean] if the relative path exists or returns a 404
+ def resource_exists?(relative_path)
+ rest.get_rest(relative_path)
+ true
+ rescue Net::HTTPServerException => e
+ raise unless e.response.code == "404"
+ false
+ end
+
+ # @return [Chef::REST] REST client using the client credentials
+ def client_rest
+ @client_rest ||= Chef::REST.new(chef_server_url, node_name, client_path)
+ end
+
+ # @return [Chef::REST] REST client using the cli user's knife credentials
+ # this uses the users's credentials
+ def rest
+ @rest ||= Chef::REST.new(chef_server_url)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/bootstrap/templates/archlinux-gems.erb b/lib/chef/knife/bootstrap/templates/archlinux-gems.erb
index 581293daa3..55d2c0cc12 100644
--- a/lib/chef/knife/bootstrap/templates/archlinux-gems.erb
+++ b/lib/chef/knife/bootstrap/templates/archlinux-gems.erb
@@ -11,10 +11,12 @@ fi
mkdir -p /etc/chef
+<% if validation_key -%>
cat > /etc/chef/validation.pem <<'EOP'
<%= validation_key %>
EOP
chmod 0600 /etc/chef/validation.pem
+<% end -%>
<% if encrypted_data_bag_secret -%>
cat > /etc/chef/encrypted_data_bag_secret <<'EOP'
@@ -39,6 +41,13 @@ EOP
<% end -%>
<% end -%>
+<% if client_pem -%>
+cat > /etc/chef/client.pem <<'EOP'
+<%= ::File.read(::File.expand_path(client_pem)) %>
+EOP
+chmod 0600 /etc/chef/client.pem
+<% end -%>
+
cat > /etc/chef/client.rb <<'EOP'
log_level :info
log_location STDOUT
diff --git a/lib/chef/knife/bootstrap/templates/chef-aix.erb b/lib/chef/knife/bootstrap/templates/chef-aix.erb
index 013ad1decb..45fbba7b48 100644
--- a/lib/chef/knife/bootstrap/templates/chef-aix.erb
+++ b/lib/chef/knife/bootstrap/templates/chef-aix.erb
@@ -24,10 +24,19 @@ fi
mkdir -p /etc/chef
+<% if client_pem -%>
+cat > /etc/chef/client.pem <<'EOP'
+<%= ::File.read(::File.expand_path(client_pem)) %>
+EOP
+chmod 0600 /etc/chef/client.pem
+<% end -%>
+
+<% if validation_key -%>
cat > /etc/chef/validation.pem <<'EOP'
<%= validation_key %>
EOP
chmod 0600 /etc/chef/validation.pem
+<% end -%>
<% if encrypted_data_bag_secret -%>
cat > /etc/chef/encrypted_data_bag_secret <<'EOP'
diff --git a/lib/chef/knife/bootstrap/templates/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb
index f49fafa98b..17d7a9e3b5 100644
--- a/lib/chef/knife/bootstrap/templates/chef-full.erb
+++ b/lib/chef/knife/bootstrap/templates/chef-full.erb
@@ -38,10 +38,19 @@ exists() {
mkdir -p /etc/chef
+<% if client_pem -%>
+cat > /etc/chef/client.pem <<'EOP'
+<%= ::File.read(::File.expand_path(client_pem)) %>
+EOP
+chmod 0600 /etc/chef/client.pem
+<% end -%>
+
+<% if validation_key -%>
cat > /etc/chef/validation.pem <<'EOP'
<%= validation_key %>
EOP
chmod 0600 /etc/chef/validation.pem
+<% end -%>
<% if encrypted_data_bag_secret -%>
cat > /etc/chef/encrypted_data_bag_secret <<'EOP'
diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb
index 60db34c8d0..7197653489 100644
--- a/lib/chef/knife/core/bootstrap_context.rb
+++ b/lib/chef/knife/core/bootstrap_context.rb
@@ -30,6 +30,8 @@ class Chef
#
class BootstrapContext
+ attr_accessor :client_pem
+
def initialize(config, run_list, chef_config, secret = nil)
@config = config
@run_list = run_list
@@ -42,7 +44,11 @@ class Chef
end
def validation_key
- IO.read(File.expand_path(@chef_config[:validation_key]))
+ if File.exist?(File.expand_path(@chef_config[:validation_key]))
+ IO.read(File.expand_path(@chef_config[:validation_key]))
+ else
+ false
+ end
end
def encrypted_data_bag_secret
diff --git a/spec/unit/knife/bootstrap/chef_vault_handler_spec.rb b/spec/unit/knife/bootstrap/chef_vault_handler_spec.rb
new file mode 100644
index 0000000000..5d0c1c900c
--- /dev/null
+++ b/spec/unit/knife/bootstrap/chef_vault_handler_spec.rb
@@ -0,0 +1,153 @@
+#
+# Author:: Lamont Granquist <lamont@chef.io>)
+# 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'
+
+describe Chef::Knife::Bootstrap::ChefVaultHandler do
+
+ let(:stdout) { StringIO.new }
+ let(:stderr) { StringIO.new }
+ let(:stdin) { StringIO.new }
+ let(:ui) { Chef::Knife::UI.new(stdout, stderr, stdin, {}) }
+
+ let(:knife_config) { {} }
+
+ let(:node_name) { "bevell.wat" }
+
+ let(:chef_vault_handler) {
+ chef_vault_handler = Chef::Knife::Bootstrap::ChefVaultHandler.new(knife_config: knife_config, ui: ui)
+ chef_vault_handler
+ }
+
+ context "when there's no vault option" do
+ it "should report its not doing anything" do
+ expect(chef_vault_handler.doing_chef_vault?).to be false
+ end
+
+ it "shouldn't do anything" do
+ expect(chef_vault_handler).to_not receive(:sanity_check)
+ expect(chef_vault_handler).to_not receive(:update_vault_list!)
+ chef_vault_handler
+ end
+ end
+
+ context "when setting chef vault items" do
+ let(:vault_item) { double("ChefVault::Item") }
+
+ before do
+ expect(chef_vault_handler).to receive(:wait_for_client).and_return(false)
+ expect(chef_vault_handler).to receive(:require_chef_vault!).at_least(:once)
+ expect(vault_item).to receive(:clients).with("name:#{node_name}").at_least(:once)
+ expect(vault_item).to receive(:save).at_least(:once)
+ end
+
+ context "from knife_config[:vault_item]" do
+ it "sets a single item as a scalar" do
+ knife_config[:vault_item] = { 'vault' => 'item1' }
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets a single item as an array" do
+ knife_config[:vault_item] = { 'vault' => [ 'item1' ] }
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two items as an array" do
+ knife_config[:vault_item] = { 'vault' => [ 'item1', 'item2' ] }
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item2').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two vaults from different hash keys" do
+ knife_config[:vault_item] = { 'vault' => [ 'item1', 'item2' ], 'vault2' => [ 'item3' ] }
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item2').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault2', 'item3').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+ end
+
+ context "from knife_config[:vault_list]" do
+ it "sets a single item as a scalar" do
+ knife_config[:vault_list] = '{ "vault": "item1" }'
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets a single item as an array" do
+ knife_config[:vault_list] = '{ "vault": [ "item1" ] }'
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two items as an array" do
+ knife_config[:vault_list] = '{ "vault": [ "item1", "item2" ] }'
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item2').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two vaults from different hash keys" do
+ knife_config[:vault_list] = '{ "vault": [ "item1", "item2" ], "vault2": [ "item3" ] }'
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item2').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault2', 'item3').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+ end
+
+ context "from knife_config[:vault_file]" do
+
+ def setup_file_contents(json)
+ stringio = StringIO.new(json)
+ knife_config[:vault_file] = "/foo/bar/baz"
+ expect(File).to receive(:read).with(knife_config[:vault_file]).and_return(stringio)
+ end
+
+ it "sets a single item as a scalar" do
+ setup_file_contents('{ "vault": "item1" }')
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets a single item as an array" do
+ setup_file_contents('{ "vault": [ "item1" ] }')
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two items as an array" do
+ setup_file_contents('{ "vault": [ "item1", "item2" ] }')
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item2').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two vaults from different hash keys" do
+ setup_file_contents('{ "vault": [ "item1", "item2" ], "vault2": [ "item3" ] }')
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item1').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault', 'item2').and_return(vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_vault_item).with('vault2', 'item3').and_return(vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+ end
+ end
+end
diff --git a/spec/unit/knife/bootstrap/client_builder_spec.rb b/spec/unit/knife/bootstrap/client_builder_spec.rb
new file mode 100644
index 0000000000..e6aa307c7e
--- /dev/null
+++ b/spec/unit/knife/bootstrap/client_builder_spec.rb
@@ -0,0 +1,178 @@
+#
+# Author:: Lamont Granquist <lamont@chef.io>)
+# 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'
+
+
+describe Chef::Knife::Bootstrap::ClientBuilder do
+
+ let(:stdout) { StringIO.new }
+ let(:stderr) { StringIO.new }
+ let(:stdin) { StringIO.new }
+ let(:ui) { Chef::Knife::UI.new(stdout, stderr, stdin, {}) }
+
+ let(:knife_config) { {} }
+
+ let(:chef_config) { {} }
+
+ let(:node_name) { "bevell.wat" }
+
+ let(:rest) { double("Chef::REST") }
+
+ let(:client_builder) {
+ client_builder = Chef::Knife::Bootstrap::ClientBuilder.new(knife_config: knife_config, chef_config: chef_config, ui: ui)
+ allow(client_builder).to receive(:rest).and_return(rest)
+ allow(client_builder).to receive(:node_name).and_return(node_name)
+ client_builder
+ }
+
+ context "#sanity_check!" do
+ let(:response_404) { OpenStruct.new(:code => '404') }
+ let(:exception_404) { Net::HTTPServerException.new("404 not found", response_404) }
+
+ context "in cases where the prompting fails" do
+ before do
+ # should fail early in #run
+ expect(client_builder).to_not receive(:create_client!)
+ expect(client_builder).to_not receive(:create_node!)
+ end
+
+ it "exits when the node exists and the user does not want to delete" do
+ expect(rest).to receive(:get_rest).with("nodes/#{node_name}")
+ expect(ui.stdin).to receive(:readline).and_return('n')
+ expect { client_builder.run }.to raise_error(SystemExit)
+ end
+
+ it "exits when the client exists and the user does not want to delete" do
+ expect(rest).to receive(:get_rest).with("nodes/#{node_name}").and_raise(exception_404)
+ expect(rest).to receive(:get_rest).with("clients/#{node_name}")
+ expect(ui.stdin).to receive(:readline).and_return('n')
+ expect { client_builder.run }.to raise_error(SystemExit)
+ end
+ end
+
+ context "in cases where the prompting succeeds" do
+ before do
+ # mock out the rest of #run
+ expect(client_builder).to receive(:create_client!)
+ expect(client_builder).to receive(:create_node!)
+ end
+
+ it "when both the client and node do not exist it succeeds" do
+ expect(rest).to receive(:get_rest).with("nodes/#{node_name}").and_raise(exception_404)
+ expect(rest).to receive(:get_rest).with("clients/#{node_name}").and_raise(exception_404)
+ expect { client_builder.run }.not_to raise_error
+ end
+
+ it "when we are allowed to delete an old node" do
+ expect(rest).to receive(:get_rest).with("nodes/#{node_name}")
+ expect(ui.stdin).to receive(:readline).and_return('y')
+ expect(rest).to receive(:get_rest).with("clients/#{node_name}").and_raise(exception_404)
+ expect(rest).to receive(:delete).with("nodes/#{node_name}")
+ expect { client_builder.run }.not_to raise_error
+ end
+
+ it "when we are allowed to delete an old client" do
+ expect(rest).to receive(:get_rest).with("nodes/#{node_name}").and_raise(exception_404)
+ expect(rest).to receive(:get_rest).with("clients/#{node_name}")
+ expect(ui.stdin).to receive(:readline).and_return('y')
+ expect(rest).to receive(:delete).with("clients/#{node_name}")
+ expect { client_builder.run }.not_to raise_error
+ end
+
+ it "when we are are allowed to delete both an old client and node" do
+ expect(rest).to receive(:get_rest).with("nodes/#{node_name}")
+ expect(rest).to receive(:get_rest).with("clients/#{node_name}")
+ expect(ui.stdin).to receive(:readline).twice.and_return('y')
+ expect(rest).to receive(:delete).with("nodes/#{node_name}")
+ expect(rest).to receive(:delete).with("clients/#{node_name}")
+ expect { client_builder.run }.not_to raise_error
+ end
+ end
+ end
+
+ context "#create_client!" do
+ before do
+ # mock out the rest of #run
+ expect(client_builder).to receive(:sanity_check)
+ expect(client_builder).to receive(:create_node!)
+ end
+
+ it "delegates everything to Chef::ApiClient::Registration" do
+ reg_double = double("Chef::ApiClient::Registration")
+ expect(Chef::ApiClient::Registration).to receive(:new).with(node_name, client_builder.client_path, http_api: rest).and_return(reg_double)
+ expect(reg_double).to receive(:run)
+ client_builder.run
+ end
+
+ end
+
+ context "#client_path" do
+ it "has a public API for the temporary client.pem file" do
+ expect(client_builder.client_path).to match(/#{node_name}.pem/)
+ end
+ end
+
+ context "#create_node!" do
+ before do
+ # mock out the rest of #run
+ expect(client_builder).to receive(:sanity_check)
+ expect(client_builder).to receive(:create_client!)
+ # mock out default node building steps
+ expect(client_builder).to receive(:client_rest).and_return(client_rest)
+ expect(Chef::Node).to receive(:new).with(chef_server_rest: client_rest).and_return(node)
+ expect(node).to receive(:name).with(node_name)
+ expect(node).to receive(:save)
+ end
+
+ let(:client_rest) { double("Chef::REST (client)") }
+
+ let(:node) { double("Chef::Node") }
+
+ it "builds a node with a default run_list of []" do
+ expect(node).to receive(:run_list).with([])
+ client_builder.run
+ end
+
+ it "builds a node when the run_list is a string" do
+ knife_config[:run_list] = "role[base],role[app]"
+ expect(node).to receive(:run_list).with(["role[base]", "role[app]"])
+ client_builder.run
+ end
+
+ it "builds a node when the run_list is an Array" do
+ knife_config[:run_list] = ["role[base]", "role[app]"]
+ expect(node).to receive(:run_list).with(["role[base]", "role[app]"])
+ client_builder.run
+ end
+
+ it "builds a node with first_boot_attributes if they're given" do
+ knife_config[:first_boot_attributes] = {:baz => :quux}
+ expect(node).to receive(:normal_attrs=).with({:baz=>:quux})
+ expect(node).to receive(:run_list).with([])
+ client_builder.run
+ end
+
+ it "builds a node with an environment if its given" do
+ knife_config[:environment] = "production"
+ expect(node).to receive(:environment).with("production")
+ expect(node).to receive(:run_list).with([])
+ client_builder.run
+ end
+ end
+end
diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb
index 4cb681fdbe..4743ea315e 100644
--- a/spec/unit/knife/bootstrap_spec.rb
+++ b/spec/unit/knife/bootstrap_spec.rb
@@ -520,25 +520,71 @@ describe Chef::Knife::Bootstrap do
knife_ssh
end
- it "configures the underlying ssh command and then runs it" do
- expect(knife_ssh).to receive(:run)
- knife.run
- end
+ context "when running with a configured and present validation key" do
+ before do
+ # this tests runs the old code path where we have a validation key, so we need to pass that check
+ allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
+ end
+
+
+ it "configures the underlying ssh command and then runs it" do
+ expect(knife_ssh).to receive(:run)
+ knife.run
+ end
+
+ it "falls back to password based auth when auth fails the first time" do
+ allow(knife).to receive(:puts)
+
+ fallback_knife_ssh = knife_ssh.dup
+ expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed.new("no ssh for you"))
+ allow(knife).to receive(:knife_ssh_with_password_auth).and_return(fallback_knife_ssh)
+ expect(fallback_knife_ssh).to receive(:run)
+ knife.run
+ end
+
+ it "raises the exception if config[:ssh_password] is set and an authentication exception is raised" do
+ knife.config[:ssh_password] = "password"
+ expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed)
+ expect { knife.run }.to raise_error(Net::SSH::AuthenticationFailed)
+ end
- it "falls back to password based auth when auth fails the first time" do
- allow(knife).to receive(:puts)
+ it "creates the client and adds chef-vault items if vault_list is set" do
+ knife.config[:vault_file] = "/not/our/responsibility/to/check/if/this/exists"
+ expect(knife_ssh).to receive(:run)
+ expect(knife.client_builder).to receive(:run)
+ expect(knife.chef_vault_handler).to receive(:run).with(node_name: knife.config[:chef_node_name])
+ knife.run
+ end
+
+ it "creates the client and adds chef-vault items if vault_items is set" do
+ knife.config[:vault_list] = '{ "vault" => "item" }'
+ expect(knife_ssh).to receive(:run)
+ expect(knife.client_builder).to receive(:run)
+ expect(knife.chef_vault_handler).to receive(:run).with(node_name: knife.config[:chef_node_name])
+ knife.run
+ end
- fallback_knife_ssh = knife_ssh.dup
- expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed.new("no ssh for you"))
- allow(knife).to receive(:knife_ssh_with_password_auth).and_return(fallback_knife_ssh)
- expect(fallback_knife_ssh).to receive(:run)
- knife.run
+ it "does old-style validation without creating a client key if vault_list+vault_items is not set" do
+ expect(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
+ expect(knife_ssh).to receive(:run)
+ expect(knife.client_builder).not_to receive(:run)
+ expect(knife.chef_vault_handler).not_to receive(:run).with(node_name: knife.config[:chef_node_name])
+ knife.run
+ end
end
- it "raises the exception if config[:ssh_password] is set and an authentication exception is raised" do
- knife.config[:ssh_password] = "password"
- expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed)
- expect { knife.run }.to raise_error(Net::SSH::AuthenticationFailed)
+ context "when the validation key is not present" do
+ before do
+ # this tests runs the old code path where we have a validation key, so we need to pass that check
+ allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(false)
+ end
+
+ it "creates the client (and possibly adds chef-vault items)" do
+ expect(knife_ssh).to receive(:run)
+ expect(knife.client_builder).to receive(:run)
+ expect(knife.chef_vault_handler).to receive(:run).with(node_name: knife.config[:chef_node_name])
+ knife.run
+ end
end
end
diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb
index e7d2464fa1..3718cb228c 100644
--- a/spec/unit/knife/core/bootstrap_context_spec.rb
+++ b/spec/unit/knife/core/bootstrap_context_spec.rb
@@ -77,6 +77,7 @@ EXPECTED
describe "validation key path that contains a ~" do
let(:chef_config){ {:validation_key => '~/my.key'} }
it "reads the validation key when it contains a ~" do
+ expect(File).to receive(:exist?).with(File.expand_path("my.key", ENV['HOME'])).and_return(true)
expect(IO).to receive(:read).with(File.expand_path("my.key", ENV['HOME']))
bootstrap_context.validation_key
end