File Manager
Viewing File: cookbook_manifest.rb
# Author:: Daniel DeLeo (<dan@chef.io>)
# Copyright:: Copyright 2015-2016, 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"
require "chef/util/path_helper"
require "chef/log"
class Chef
# Handles the details of representing a cookbook in JSON form for uploading
# to a Chef Server.
class CookbookManifest
# Duplicates the same constant in CookbookVersion. We cannot remove it
# there because it is treated by other code as part of CookbookVersion's
# public API (also used in some deprecated methods).
COOKBOOK_SEGMENTS = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ].freeze
extend Forwardable
attr_reader :cookbook_version
def_delegator :@cookbook_version, :root_paths
def_delegator :@cookbook_version, :segment_filenames
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_hash 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 inferface 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. This endpoint is currently under active
# development and the format is expected to change frequently, therefore
# the result of #manifest, #to_hash, and #to_json will not be stable when
# `policy_mode` is enabled.
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_hash
result = manifest.dup
result["frozen?"] = frozen_version?
result["chef_type"] = "cookbook_version"
result.to_hash
end
def to_json(*a)
result = to_hash
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 = Mash.new new_manifest
@checksums = extract_checksums_from_manifest(@manifest)
@manifest_records_by_path = extract_manifest_records_by_path(@manifest)
COOKBOOK_SEGMENTS.each do |segment|
next unless @manifest.has_key?(segment)
filenames = @manifest[segment].map { |manifest_record| manifest_record["name"] }
cookbook_version.replace_segment_filenames(segment, filenames)
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({
:recipes => Array.new,
:definitions => Array.new,
:libraries => Array.new,
:attributes => Array.new,
:files => Array.new,
:templates => Array.new,
:resources => Array.new,
:providers => Array.new,
:root_files => Array.new,
})
@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_SEGMENTS.each do |segment|
segment_filenames(segment).each do |segment_file|
next if File.directory?(segment_file)
path, specificity = parse_segment_file_from_root_paths(segment, segment_file)
file_name = File.basename(path)
csum = checksum_cookbook_file(segment_file)
@checksums[csum] = segment_file
rs = Mash.new({
:name => file_name,
:path => path,
:checksum => csum,
:specificity => specificity,
})
manifest[segment] << rs
end
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_segment_file_from_root_paths(segment, segment_file)
root_paths.each do |root_path|
pathname = Chef::Util::PathHelper.relative_path_from(root_path, segment_file)
parts = pathname.each_filename.take(2)
# Check if path is actually under root_path
next if parts[0] == ".."
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 [ pathname.to_s, "root_default" ]
else
return [ pathname.to_s, parts[1] ]
end
else
return [ pathname.to_s, "default" ]
end
end
Chef::Log.error("Cookbook file #{segment_file} not under cookbook root paths #{root_paths.inspect}.")
raise "Cookbook file #{segment_file} not under cookbook root paths #{root_paths.inspect}."
end
def extract_checksums_from_manifest(manifest)
checksums = {}
COOKBOOK_SEGMENTS.each do |segment|
next unless manifest.has_key?(segment)
manifest[segment].each do |manifest_record|
checksums[manifest_record[:checksum]] = nil
end
end
checksums
end
def checksum_cookbook_file(filepath)
CookbookVersion.checksum_cookbook_file(filepath)
end
def extract_manifest_records_by_path(manifest)
manifest_records_by_path = {}
COOKBOOK_SEGMENTS.each do |segment|
next unless manifest.has_key?(segment)
manifest[segment].each do |manifest_record|
manifest_records_by_path[manifest_record[:path]] = manifest_record
end
end
manifest_records_by_path
end
end
end