summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2015-02-13 16:19:23 -0800
committerLamont Granquist <lamont@scriptkiddie.org>2015-02-13 16:19:23 -0800
commit36477867cac9eb1db37730ecfd4d0181d42fa660 (patch)
treeab7648d1de1870b079e2bc03a4ec15ce8b4afbc9
parentda1adbb2979b7096594968ad0fbbadc8c5f480d3 (diff)
parenteb3efcdcdaab6956ea61ef9c8096130b53d9cc09 (diff)
downloadchef-36477867cac9eb1db37730ecfd4d0181d42fa660.tar.gz
Merge pull request #2030 from chef/lcg/1923
Lcg/1923
-rw-r--r--lib/chef/api_client.rb2
-rw-r--r--lib/chef/api_client/registration.rb16
-rw-r--r--lib/chef/cookbook_version.rb13
-rw-r--r--lib/chef/data_bag.rb7
-rw-r--r--lib/chef/data_bag_item.rb7
-rw-r--r--lib/chef/environment.rb7
-rw-r--r--lib/chef/knife/bootstrap.rb99
-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/README.md (renamed from lib/chef/knife/bootstrap/README.md)0
-rw-r--r--lib/chef/knife/bootstrap/templates/archlinux-gems.erb (renamed from lib/chef/knife/bootstrap/archlinux-gems.erb)9
-rw-r--r--lib/chef/knife/bootstrap/templates/chef-aix.erb (renamed from lib/chef/knife/bootstrap/chef-aix.erb)9
-rw-r--r--lib/chef/knife/bootstrap/templates/chef-full.erb (renamed from lib/chef/knife/bootstrap/chef-full.erb)9
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb8
-rw-r--r--lib/chef/node.rb7
-rw-r--r--lib/chef/role.rb7
-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.rb85
-rw-r--r--spec/unit/knife/core/bootstrap_context_spec.rb1
20 files changed, 915 insertions, 57 deletions
diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb
index a3346b5b68..ce9ceb312c 100644
--- a/lib/chef/api_client.rb
+++ b/lib/chef/api_client.rb
@@ -96,7 +96,7 @@ class Chef
set_or_return(
:private_key,
arg,
- :kind_of => String
+ :kind_of => [String, FalseClass]
)
end
diff --git a/lib/chef/api_client/registration.rb b/lib/chef/api_client/registration.rb
index 8a5885eff3..de5fc7ac3d 100644
--- a/lib/chef/api_client/registration.rb
+++ b/lib/chef/api_client/registration.rb
@@ -33,9 +33,10 @@ class Chef
attr_reader :destination
attr_reader :name
- def initialize(name, destination)
- @name = name
- @destination = destination
+ def initialize(name, destination, http_api: nil)
+ @name = name
+ @destination = destination
+ @http_api = http_api
@server_generated_private_key = nil
end
@@ -120,11 +121,10 @@ class Chef
post_data
end
-
def http_api
- @http_api_as_validator ||= Chef::REST.new(Chef::Config[:chef_server_url],
- Chef::Config[:validation_client_name],
- Chef::Config[:validation_key])
+ @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url],
+ Chef::Config[:validation_client_name],
+ Chef::Config[:validation_key])
end
# Whether or not to generate keys locally and post the public key to the
@@ -161,5 +161,3 @@ class Chef
end
end
end
-
-
diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb
index c51d5798c5..b8f32a61bb 100644
--- a/lib/chef/cookbook_version.rb
+++ b/lib/chef/cookbook_version.rb
@@ -76,6 +76,8 @@ class Chef
attr_reader :recipe_filenames_by_name
attr_reader :attribute_filenames_by_short_filename
+ attr_accessor :chef_server_rest
+
# The first root path is the primary cookbook dir, from which metadata is loaded
def root_dir
root_paths[0]
@@ -98,7 +100,7 @@ class Chef
#
# === Returns
# object<Chef::CookbookVersion>:: Duh. :)
- def initialize(name, *root_paths)
+ def initialize(name, *root_paths, chef_server_rest: nil)
@name = name
@root_paths = root_paths
@frozen = false
@@ -119,6 +121,7 @@ class Chef
@status = :ready
@file_vendor = nil
@metadata = Chef::Cookbook::Metadata.new
+ @chef_server_rest = chef_server_rest
end
def version
@@ -512,12 +515,12 @@ class Chef
cookbook_manifest.force_save_url
end
- def self.chef_server_rest
- Chef::REST.new(Chef::Config[:chef_server_url])
+ def chef_server_rest
+ @chef_server_rest ||= self.chef_server_rest
end
- def chef_server_rest
- self.class.chef_server_rest
+ def self.chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
end
def destroy
diff --git a/lib/chef/data_bag.rb b/lib/chef/data_bag.rb
index 528be3f2c4..8475774fa1 100644
--- a/lib/chef/data_bag.rb
+++ b/lib/chef/data_bag.rb
@@ -33,6 +33,8 @@ class Chef
VALID_NAME = /^[\.\-[:alnum:]_]+$/
+ attr_accessor :chef_server_rest
+
def self.validate_name!(name)
unless name =~ VALID_NAME
raise Exceptions::InvalidDataBagName, "DataBags must have a name matching #{VALID_NAME.inspect}, you gave #{name.inspect}"
@@ -40,8 +42,9 @@ class Chef
end
# Create a new Chef::DataBag
- def initialize
+ def initialize(chef_server_rest: nil)
@name = ''
+ @chef_server_rest = chef_server_rest
end
def name(arg=nil)
@@ -67,7 +70,7 @@ class Chef
end
def chef_server_rest
- Chef::REST.new(Chef::Config[:chef_server_url])
+ @chef_server_rest ||= Chef::REST.new(Chef::Config[:chef_server_url])
end
def self.chef_server_rest
diff --git a/lib/chef/data_bag_item.rb b/lib/chef/data_bag_item.rb
index fc0ee74c0c..9f92e26c50 100644
--- a/lib/chef/data_bag_item.rb
+++ b/lib/chef/data_bag_item.rb
@@ -37,6 +37,8 @@ class Chef
VALID_ID = /^[\.\-[:alnum:]_]+$/
+ attr_accessor :chef_server_rest
+
def self.validate_id!(id_str)
if id_str.nil? || ( id_str !~ VALID_ID )
raise Exceptions::InvalidDataBagItemID, "Data Bag items must have an id matching #{VALID_ID.inspect}, you gave: #{id_str.inspect}"
@@ -49,13 +51,14 @@ class Chef
attr_reader :raw_data
# Create a new Chef::DataBagItem
- def initialize
+ def initialize(chef_server_rest: nil)
@data_bag = nil
@raw_data = Mash.new
+ @chef_server_rest = chef_server_rest
end
def chef_server_rest
- Chef::REST.new(Chef::Config[:chef_server_url])
+ @chef_server_rest ||= Chef::REST.new(Chef::Config[:chef_server_url])
end
def self.chef_server_rest
diff --git a/lib/chef/environment.rb b/lib/chef/environment.rb
index 33dfb52403..7d4b410639 100644
--- a/lib/chef/environment.rb
+++ b/lib/chef/environment.rb
@@ -33,18 +33,21 @@ class Chef
include Chef::Mixin::ParamsValidate
include Chef::Mixin::FromFile
+ attr_accessor :chef_server_rest
+
COMBINED_COOKBOOK_CONSTRAINT = /(.+)(?:[\s]+)((?:#{Chef::VersionConstraint::OPS.join('|')})(?:[\s]+).+)$/.freeze
- def initialize
+ def initialize(chef_server_rest: nil)
@name = ''
@description = ''
@default_attributes = Mash.new
@override_attributes = Mash.new
@cookbook_versions = Hash.new
+ @chef_server_rest = chef_server_rest
end
def chef_server_rest
- Chef::REST.new(Chef::Config[:chef_server_url])
+ @chef_server_rest ||= Chef::REST.new(Chef::Config[:chef_server_url])
end
def self.chef_server_rest
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
index 19a329199d..f23c15fa70 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,55 @@ class Chef
:description => "Verify the SSL cert for HTTPS requests to the Chef server API.",
:boolean => true
+ option :bootstrap_vault_file,
+ :long => '--bootstrap-vault-file VAULT_FILE',
+ :description => 'A JSON file with a list of vault(s) and item(s) to be updated'
+
+ option :bootstrap_vault_json,
+ :long => '--bootstrap-vault-json VAULT_JSON',
+ :description => 'A JSON string with the vault(s) and item(s) to be updated'
+
+ option :bootstrap_vault_item,
+ :long => '--bootstrap-vault-item VAULT_ITEM',
+ :description => 'A single vault and item to update as "vault:item"',
+ :proc => Proc.new { |i|
+ (vault, item) = i.split(/:/)
+ Chef::Config[:knife][:bootstrap_vault_item] ||= {}
+ Chef::Config[:knife][:bootstrap_vault_item][vault] ||= []
+ Chef::Config[:knife][:bootstrap_vault_item][vault].push(item)
+ Chef::Config[:knife][:bootstrap_vault_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
@@ -216,7 +266,7 @@ class Chef
# Otherwise search the template directories until we find the right one
bootstrap_files = []
- bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{template}.erb")
+ bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap/templates', "#{template}.erb")
bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir
bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{template}.erb") if ENV['HOME']
bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb"))
@@ -237,20 +287,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 the validation key at #{Chef::Config[: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 +340,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 +381,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..749f61e6da
--- /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_bootstrap_vault_json!
+ 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_bootstrap_vault_json!
+ 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?
+ !!(bootstrap_vault_json || bootstrap_vault_file || bootstrap_vault_item)
+ end
+
+ private
+
+ # warn if the user has given mutual conflicting options
+ def sanity_check
+ if bootstrap_vault_item && (bootstrap_vault_json || bootstrap_vault_file)
+ ui.warn "--vault-item given with --vault-list or --vault-file, ignoring the latter"
+ end
+
+ if bootstrap_vault_json && bootstrap_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 bootstrap_vault_json
+ knife_config[:bootstrap_vault_json]
+ end
+
+ # @return [String] JSON text in a file representing the chef vault items
+ def bootstrap_vault_file
+ knife_config[:bootstrap_vault_file]
+ end
+
+ # @return [Hash] Ruby object representing the chef vault items to create
+ def bootstrap_vault_item
+ knife_config[:bootstrap_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 bootstrap_vault_item
+ bootstrap_vault_item
+ else
+ json = bootstrap_vault_json ? bootstrap_vault_json : File.read(bootstrap_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!
+ bootstrap_vault_item = load_chef_bootstrap_vault_item(vault, item)
+ bootstrap_vault_item.clients("name:#{node_name}")
+ bootstrap_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_bootstrap_vault_item(vault, item)
+ ChefVault::Item.load(vault, item)
+ end
+
+ public :load_chef_bootstrap_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/README.md b/lib/chef/knife/bootstrap/templates/README.md
index 13a0fe7ada..13a0fe7ada 100644
--- a/lib/chef/knife/bootstrap/README.md
+++ b/lib/chef/knife/bootstrap/templates/README.md
diff --git a/lib/chef/knife/bootstrap/archlinux-gems.erb b/lib/chef/knife/bootstrap/templates/archlinux-gems.erb
index 581293daa3..55d2c0cc12 100644
--- a/lib/chef/knife/bootstrap/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/chef-aix.erb b/lib/chef/knife/bootstrap/templates/chef-aix.erb
index 013ad1decb..45fbba7b48 100644
--- a/lib/chef/knife/bootstrap/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/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb
index f49fafa98b..17d7a9e3b5 100644
--- a/lib/chef/knife/bootstrap/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/lib/chef/node.rb b/lib/chef/node.rb
index 6055f0e1e9..f9f6416f14 100644
--- a/lib/chef/node.rb
+++ b/lib/chef/node.rb
@@ -47,6 +47,8 @@ class Chef
attr_accessor :recipe_list, :run_state, :override_runlist
+ attr_accessor :chef_server_rest
+
# RunContext will set itself as run_context via this setter when
# initialized. This is needed so DSL::IncludeAttribute (in particular,
# #include_recipe) can access the run_context to determine if an attributes
@@ -62,7 +64,8 @@ class Chef
include Chef::Mixin::ParamsValidate
# Create a new Chef::Node object.
- def initialize
+ def initialize(chef_server_rest: nil)
+ @chef_server_rest = chef_server_rest
@name = nil
@chef_environment = '_default'
@@ -80,7 +83,7 @@ class Chef
end
def chef_server_rest
- Chef::REST.new(Chef::Config[:chef_server_url])
+ @chef_server_rest ||= Chef::REST.new(Chef::Config[:chef_server_url])
end
# Set the name of this Node, or return the current name.
diff --git a/lib/chef/role.rb b/lib/chef/role.rb
index 2f174116cf..2e42788f06 100644
--- a/lib/chef/role.rb
+++ b/lib/chef/role.rb
@@ -32,17 +32,20 @@ class Chef
include Chef::Mixin::FromFile
include Chef::Mixin::ParamsValidate
+ attr_accessor :chef_server_rest
+
# Create a new Chef::Role object.
- def initialize
+ def initialize(chef_server_rest: nil)
@name = ''
@description = ''
@default_attributes = Mash.new
@override_attributes = Mash.new
@env_run_lists = {"_default" => Chef::RunList.new}
+ @chef_server_rest = chef_server_rest
end
def chef_server_rest
- Chef::REST.new(Chef::Config[:chef_server_url])
+ @chef_server_rest ||= Chef::REST.new(Chef::Config[:chef_server_url])
end
def self.chef_server_rest
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..d8f84265b7
--- /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_bootstrap_vault_json!)
+ chef_vault_handler
+ end
+ end
+
+ context "when setting chef vault items" do
+ let(:bootstrap_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(bootstrap_vault_item).to receive(:clients).with("name:#{node_name}").at_least(:once)
+ expect(bootstrap_vault_item).to receive(:save).at_least(:once)
+ end
+
+ context "from knife_config[:bootstrap_vault_item]" do
+ it "sets a single item as a scalar" do
+ knife_config[:bootstrap_vault_item] = { 'vault' => 'item1' }
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets a single item as an array" do
+ knife_config[:bootstrap_vault_item] = { 'vault' => [ 'item1' ] }
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two items as an array" do
+ knife_config[:bootstrap_vault_item] = { 'vault' => [ 'item1', 'item2' ] }
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item2').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two vaults from different hash keys" do
+ knife_config[:bootstrap_vault_item] = { 'vault' => [ 'item1', 'item2' ], 'vault2' => [ 'item3' ] }
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item2').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault2', 'item3').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+ end
+
+ context "from knife_config[:bootstrap_vault_json]" do
+ it "sets a single item as a scalar" do
+ knife_config[:bootstrap_vault_json] = '{ "vault": "item1" }'
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets a single item as an array" do
+ knife_config[:bootstrap_vault_json] = '{ "vault": [ "item1" ] }'
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two items as an array" do
+ knife_config[:bootstrap_vault_json] = '{ "vault": [ "item1", "item2" ] }'
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item2').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+
+ it "sets two vaults from different hash keys" do
+ knife_config[:bootstrap_vault_json] = '{ "vault": [ "item1", "item2" ], "vault2": [ "item3" ] }'
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item2').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault2', 'item3').and_return(bootstrap_vault_item)
+ chef_vault_handler.run(node_name: node_name)
+ end
+ end
+
+ context "from knife_config[:bootstrap_vault_file]" do
+
+ def setup_file_contents(json)
+ stringio = StringIO.new(json)
+ knife_config[:bootstrap_vault_file] = "/foo/bar/baz"
+ expect(File).to receive(:read).with(knife_config[:bootstrap_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_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_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_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_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_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item2').and_return(bootstrap_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_bootstrap_vault_item).with('vault', 'item1').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault', 'item2').and_return(bootstrap_vault_item)
+ expect(chef_vault_handler).to receive(:load_chef_bootstrap_vault_item).with('vault2', 'item3').and_return(bootstrap_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 27731ea9f6..5a2e1f1c96 100644
--- a/spec/unit/knife/bootstrap_spec.rb
+++ b/spec/unit/knife/bootstrap_spec.rb
@@ -48,6 +48,13 @@ describe Chef::Knife::Bootstrap do
expect(File.basename(knife.bootstrap_template)).to eq("chef-full")
end
+ context "with --bootstrap-vault-item" do
+ let(:bootstrap_cli_options) { [ "--bootstrap-vault-item", "vault1:item1", "--bootstrap-vault-item", "vault1:item2", "--bootstrap-vault-item", "vault2:item1" ] }
+ it "sets the knife config cli option correctly" do
+ expect(knife.config[:bootstrap_vault_item]).to eq({"vault1"=>["item1", "item2"], "vault2"=>["item1"]})
+ end
+ end
+
context "with :distro and :bootstrap_template cli options" do
let(:bootstrap_cli_options) { [ "--bootstrap-template", "my-template", "--distro", "other-template" ] }
@@ -95,7 +102,7 @@ describe Chef::Knife::Bootstrap do
context "when :bootstrap_template config is set to a template name" do
let(:bootstrap_template) { "example" }
- let(:builtin_template_path) { File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/chef/knife/bootstrap', "example.erb"))}
+ let(:builtin_template_path) { File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/chef/knife/bootstrap/templates', "example.erb"))}
let(:chef_config_dir_template_path) { "/knife/chef/config/bootstrap/example.erb" }
@@ -520,25 +527,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 "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
+ 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 "creates the client and adds chef-vault items if vault_list is set" do
+ knife.config[:bootstrap_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[:bootstrap_vault_json] = '{ "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
+
+ 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