File Manager
Viewing File: cookbook_manifest.rb
# Author:: Daniel DeLeo (<dan@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 "forwardable" unless defined?(Forwardable)
require_relative "mixin/versioned_api"
require_relative "util/path_helper"
require_relative "cookbook/manifest_v0"
require_relative "cookbook/manifest_v2"
require_relative "log"
class Chef
# Handles the details of representing a cookbook in JSON form for uploading
# to a Chef Server.
class CookbookManifest
extend Forwardable
attr_reader :cookbook_version
def_delegator :@cookbook_version, :root_paths
def_delegator :@cookbook_version, :name
def_delegator :@cookbook_version, :identifier
def_delegator :@cookbook_version, :metadata
def_delegator :@cookbook_version, :full_name
def_delegator :@cookbook_version, :version
def_delegator :@cookbook_version, :frozen_version?
# Create a new CookbookManifest object for the given `cookbook_version`.
# You can subsequently call #to_h to get a Hash representation of the
# cookbook_version in the "manifest" format, or #to_json to get a JSON
# representation of the cookbook_version.
#
# The interface for this behavior is expected to change as we implement new
# manifest formats. The entire class should be considered a private API for
# now.
#
# @api private
# @param policy_mode [Boolean] whether to convert cookbooks to Hash/JSON in
# the format used by the `cookbook_artifacts` endpoint (for policyfiles).
# Setting this option also changes the behavior of #save_url and
# #force_save_url such that CookbookVersions will be uploaded to the new
# `cookbook_artifacts` API.
def initialize(cookbook_version, policy_mode: false)
@cookbook_version = cookbook_version
@policy_mode = !!policy_mode
reset!
end
# Resets all lazily computed values.
def reset!
@manifest = nil
@checksums = nil
@manifest_records_by_path = nil
true
end
# Returns a 'manifest' data structure that can be uploaded to a Chef
# Server.
#
# The format is as follows:
#
# {
# :cookbook_name => name, # String
# :metadata => metadata, # Chef::Cookbook::Metadata
# :version => version, # Chef::Version
# :name => full_name, # String of "#{name}-#{version}"
#
# :recipes => Array<FileSpec>,
# :definitions => Array<FileSpec>,
# :libraries => Array<FileSpec>,
# :attributes => Array<FileSpec>,
# :files => Array<FileSpec>,
# :templates => Array<FileSpec>,
# :resources => Array<FileSpec>,
# :providers => Array<FileSpec>,
# :root_files => Array<FileSpec>
# }
#
# Where a `FileSpec` is a Hash of the form:
#
# {
# :name => file_name,
# :path => path,
# :checksum => csum,
# :specificity => specificity
# }
#
def manifest
@manifest || generate_manifest
@manifest
end
def checksums
@manifest || generate_manifest
@checksums
end
def manifest_records_by_path
@manifest || generate_manifest
@manifest_records_by_path
end
def policy_mode?
@policy_mode
end
def to_h
CookbookManifestVersions.to_h(self)
end
alias_method :to_hash, :to_h
def to_json(*a)
result = to_h
result["json_class"] = "Chef::CookbookVersion"
Chef::JSONCompat.to_json(result, *a)
end
# Return the URL to save (PUT) this object to the server via the
# REST api. If there is an existing document on the server and it
# is marked frozen, a PUT will result in a 409 Conflict.
def save_url
if policy_mode?
"#{named_cookbook_url}/#{identifier}"
else
"#{named_cookbook_url}/#{version}"
end
end
def named_cookbook_url
"#{cookbook_url_path}/#{name}"
end
# Adds the `force=true` parameter to the upload URL. This allows
# the user to overwrite a frozen cookbook (a PUT against the
# normal #save_url raises a 409 Conflict in this case).
def force_save_url
"#{save_url}?force=true"
end
# Update this CookbookManifest from the contents of another manifest, and
# make the corresponding changes to the cookbook_version object. Required
# to provide backward compatibility with CookbookVersion#manifest= method.
def update_from(new_manifest)
@manifest = Chef::CookbookManifestVersions.from_hash(new_manifest)
@checksums = extract_checksums_from_manifest(@manifest)
@manifest_records_by_path = extract_manifest_records_by_path(@manifest)
end
# @api private
# takes a list of hashes
def add_files_to_manifest(files)
manifest[:all_files].concat(Array(files))
@checksums = extract_checksums_from_manifest(@manifest)
@manifest_records_by_path = extract_manifest_records_by_path(@manifest)
end
def files_for(part)
return root_files if part.to_s == "root_files"
manifest[:all_files].select do |file|
seg = file[:name].split("/")[0]
part.to_s == seg
end
end
def each_file(excluded_parts: [], &block)
excluded_parts = Array(excluded_parts).map(&:to_s)
manifest[:all_files].each do |file|
seg = file[:name].split("/")[0]
next if excluded_parts.include?(seg)
yield file if block_given?
end
end
def by_parent_directory
@by_parent_directory ||=
manifest[:all_files].inject({}) do |memo, file|
parts = file[:name].split("/")
parent = if parts.length == 1
"root_files"
else
parts[0]
end
memo[parent] ||= []
memo[parent] << file
memo
end
end
def root_files
manifest[:all_files].select do |file|
segment, name = file[:name].split("/")
name.nil? || segment == "root_files"
end
end
private
def cookbook_url_path
policy_mode? ? "cookbook_artifacts" : "cookbooks"
end
# See #manifest for a description of the manifest return value.
# See #preferred_manifest_record for a description an individual manifest record.
def generate_manifest
manifest = Mash.new({
all_files: [],
})
@checksums = {}
if !root_paths || root_paths.size == 0
Chef::Log.error("Cookbook #{name} does not have root_paths! Cannot generate manifest.")
raise "Cookbook #{name} does not have root_paths! Cannot generate manifest."
end
@cookbook_version.all_files.each do |file|
next if File.directory?(file)
name, path, specificity = parse_file_from_root_paths(file)
csum = checksum_cookbook_file(file)
@checksums[csum] = file
rs = Mash.new({
name: name,
path: path,
checksum: csum,
specificity: specificity,
# full_path is not a part of the normal manifest, but is very useful to keep around.
# uploaders should strip this out.
full_path: file,
})
manifest[:all_files] << rs
end
manifest[:metadata] = metadata
manifest[:version] = metadata.version
if policy_mode?
manifest[:name] = name.to_s
manifest[:identifier] = identifier
else
manifest[:name] = full_name
manifest[:cookbook_name] = name.to_s
end
@manifest_records_by_path = extract_manifest_records_by_path(manifest)
@manifest = manifest
end
def parse_file_from_root_paths(file)
root_paths.each do |root_path|
pathname = Chef::Util::PathHelper.relative_path_from(root_path, file)
parts = pathname.each_filename.take(2)
# Check if path is actually under root_path
next if parts[0] == ".."
# if we have a root_file, such as metadata.rb, the first part will be "."
return [ "root_files/#{pathname}", pathname.to_s, "default" ] if parts.length == 1
segment = parts[0]
name = File.join(segment, pathname.basename.to_s)
if segment == "templates" || segment == "files"
# Check if pathname looks like files/foo or templates/foo (unscoped)
if pathname.each_filename.to_a.length == 2
# Use root_default in case the same path exists at root_default and default
return [ name, pathname.to_s, "root_default" ]
else
return [ name, pathname.to_s, parts[1] ]
end
else
return [ name, pathname.to_s, "default" ]
end
end
Chef::Log.error("Cookbook file #{file} not under cookbook root paths #{root_paths.inspect}.")
raise "Cookbook file #{file} not under cookbook root paths #{root_paths.inspect}."
end
def extract_checksums_from_manifest(manifest)
manifest[:all_files].inject({}) do |memo, manifest_record|
memo[manifest_record[:checksum]] = nil
memo
end
end
def checksum_cookbook_file(filepath)
CookbookVersion.checksum_cookbook_file(filepath)
end
def extract_manifest_records_by_path(manifest)
manifest[:all_files].inject({}) do |memo, manifest_record|
memo[manifest_record[:path]] = manifest_record
memo
end
end
end
class CookbookManifestVersions
extend Chef::Mixin::VersionedAPIFactory
add_versioned_api_class Chef::Cookbook::ManifestV0
add_versioned_api_class Chef::Cookbook::ManifestV2
def_versioned_delegator :from_hash
def_versioned_delegator :to_hash
def_versioned_delegator :to_h
end
end