lib/chef-cli/policyfile_services/push_archive.rb
# # 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 "zlib" require "archive/tar/minitar" require_relative "../service_exceptions" require_relative "../policyfile_lock" require "chef/server_api" require_relative "../policyfile/uploader" module ChefCLI module PolicyfileServices class PushArchive USTAR_INDICATOR = "ustar\0".force_encoding(Encoding::ASCII_8BIT).freeze attr_reader :archive_file attr_reader :policy_group attr_reader :root_dir attr_reader :ui attr_reader :config attr_reader :policyfile_lock def initialize(archive_file: nil, policy_group: nil, root_dir: nil, ui: nil, config: nil) @archive_file = archive_file @policy_group = policy_group @root_dir = root_dir || Dir.pwd @ui = ui @config = config @policyfile_lock = nil end def archive_file_path File.expand_path(archive_file, root_dir) end def run unless File.exist?(archive_file_path) raise InvalidPolicyArchive, "Archive file #{archive_file_path} not found" end stage_unpacked_archive do |staging_dir| read_policyfile_lock(staging_dir) uploader.upload end rescue => e raise PolicyfilePushArchiveError.new("Failed to publish archived policy", e) end # @api private def uploader ChefCLI::Policyfile::Uploader.new(policyfile_lock, policy_group, ui: ui, http_client: http_client, policy_document_native_api: config.policy_document_native_api) end # @api private def http_client @http_client ||= Chef::ServerAPI.new(config.chef_server_url, signing_key_filename: config.client_key, client_name: config.node_name) end private def read_policyfile_lock(staging_dir) policyfile_lock_path = File.join(staging_dir, "Policyfile.lock.json") if looks_like_old_format_archive?(staging_dir) raise InvalidPolicyArchive, <<~MESSAGE This archive is in an unsupported format. This archive was created with an older version of ChefCLI. This version of ChefCLI does not support archives in the older format. Please Re-create the archive with a newer version of ChefCLI or Workstation. MESSAGE end unless File.exist?(policyfile_lock_path) raise InvalidPolicyArchive, "Archive does not contain a Policyfile.lock.json" end unless File.directory?(File.join(staging_dir, "cookbook_artifacts")) raise InvalidPolicyArchive, "Archive does not contain a cookbook_artifacts directory" end policy_data = load_policy_data(policyfile_lock_path) storage_config = Policyfile::StorageConfig.new.use_policyfile_lock(policyfile_lock_path) @policyfile_lock = ChefCLI::PolicyfileLock.new(storage_config).build_from_archive(policy_data) missing_cookbooks = policyfile_lock.cookbook_locks.select do |name, lock| !lock.installed? end unless missing_cookbooks.empty? message = "Archive does not have all cookbooks required by the Policyfile.lock. " + "Missing cookbooks: '#{missing_cookbooks.keys.join('", "')}'." raise InvalidPolicyArchive, message end end def load_policy_data(policyfile_lock_path) FFI_Yajl::Parser.parse(IO.read(policyfile_lock_path)) end def stage_unpacked_archive p = Process.pid t = Time.new.utc.strftime("%Y%m%d%H%M%S") Dir.mktmpdir("chefcli-push-archive-#{p}-#{t}") do |staging_dir| unpack_to(staging_dir) yield staging_dir end end def unpack_to(staging_dir) Mixlib::Archive.new(archive_file_path).extract(staging_dir) rescue => e raise InvalidPolicyArchive, "Archive file #{archive_file_path} could not be unpacked. #{e}" end def looks_like_old_format_archive?(staging_dir) cookbooks_dir = File.join(staging_dir, "cookbooks") data_bags_dir = File.join(staging_dir, "data_bags") cookbook_artifacts_dir = File.join(staging_dir, "cookbook_artifacts") policies_dir = File.join(staging_dir, "policies") policy_groups_dir = File.join(staging_dir, "policy_groups") # Old archives just had these two dirs have_old_dirs = File.exist?(cookbooks_dir) && File.exist?(data_bags_dir) # New archives created by `chef export` will have all of these; it's # also possible we'll encounter an "artisanal" archive, which might # only be missing one of these by accident. In that case we want to # trigger a different error than we're detecting here. have_any_new_dirs = File.exist?(cookbook_artifacts_dir) || File.exist?(policies_dir) || File.exist?(policy_groups_dir) have_old_dirs && !have_any_new_dirs end end end end