summaryrefslogtreecommitdiff
path: root/knife/lib/chef/knife/bootstrap/client_builder.rb
diff options
context:
space:
mode:
Diffstat (limited to 'knife/lib/chef/knife/bootstrap/client_builder.rb')
-rw-r--r--knife/lib/chef/knife/bootstrap/client_builder.rb212
1 files changed, 212 insertions, 0 deletions
diff --git a/knife/lib/chef/knife/bootstrap/client_builder.rb b/knife/lib/chef/knife/bootstrap/client_builder.rb
new file mode 100644
index 0000000000..b1e69d90db
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap/client_builder.rb
@@ -0,0 +1,212 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) 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 "chef/node" unless defined?(Chef::Node)
+require "chef/server_api" unless defined?(Chef::ServerAPI)
+require "chef/api_client" unless defined?(Chef::APIClient)
+require "chef/api_client/registration" unless defined?(Chef::APIClient::Registration)
+require "tmpdir" unless defined?(Dir.mktmpdir)
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+ class ClientBuilder
+
+ # @return [Hash] knife merged config, typically @config
+ attr_accessor :config
+ # @return [Hash] chef config object
+ attr_accessor :chef_config
+ # @return [Chef::Knife::UI] ui object for output
+ attr_accessor :ui
+ # @return [Chef::ApiClient] client saved on run
+ attr_reader :client
+
+ # @param 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(config: {}, knife_config: nil, chef_config: {}, ui: nil)
+ @config = config
+ unless knife_config.nil?
+ @config = knife_config
+ Chef.deprecated(:knife_bootstrap_apis, "The knife_config option to the Bootstrap::ClientBuilder object is deprecated and has been renamed to just 'config'")
+ end
+ @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}")
+
+ @client = 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 config
+ def node_name
+ config[:chef_node_name]
+ end
+
+ # @return [String] environment from the config
+ def environment
+ config[:environment]
+ end
+
+ # @return [String] run_list from the config
+ def run_list
+ config[:run_list]
+ end
+
+ # @return [String] policy_name from the config
+ def policy_name
+ config[:policy_name]
+ end
+
+ # @return [String] policy_group from the config
+ def policy_group
+ config[:policy_group]
+ end
+
+ # @return [Hash,Array] Object representation of json first-boot attributes from the config
+ def first_boot_attributes
+ 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.policy_name = policy_name if policy_name
+ node.policy_group = policy_group if policy_group
+ (config[:tags] || []).each do |tag|
+ node.tags << tag
+ end
+ 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(relative_path)
+ true
+ rescue Net::HTTPClientException => e
+ raise unless e.response.code == "404"
+
+ false
+ end
+
+ # @return [Chef::ServerAPI] REST client using the client credentials
+ def client_rest
+ @client_rest ||= Chef::ServerAPI.new(chef_server_url, client_name: node_name, signing_key_filename: client_path)
+ end
+
+ # @return [Chef::ServerAPI] REST client using the cli user's knife credentials
+ # this uses the users's credentials
+ def rest
+ @rest ||= Chef::ServerAPI.new(chef_server_url)
+ end
+ end
+ end
+ end
+end