class Berkshelf::Berksfile

Constants

DEFAULT_API_URL
EXCLUDED_VCS_FILES_WHEN_VENDORING

Don’t vendor VCS files. Reference GNU tar –exclude-vcs: www.gnu.org/software/tar/manual/html_section/tar_49.html

Attributes

filepath[R]

@return [String]

The path on disk to the file representing this instance of Berksfile
preferred_solver[R]

@return [Symbol]

The solver engine preferred by this instance of Berksfile
required_solver[R]

@return [Symbol]

The solver engine required by this instance of Berksfile

Public Class Methods

from_file(file, options = {}) click to toggle source

@param [#to_s] file

a path on disk to a Berksfile to instantiate from

@return [Berksfile]

# File lib/berkshelf/berksfile.rb, line 23
def from_file(file, options = {})
  raise BerksfileNotFound.new(file) unless File.exist?(file)

  begin
    new(file, options).evaluate_file(file)
  rescue => ex
    raise BerksfileReadError.new(ex)
  end
end
from_options(options = {}) click to toggle source

Instantiate a Berksfile from the given options. This method is used heavily by the CLI to reduce duplication.

@param (see Berksfile#initialize)

# File lib/berkshelf/berksfile.rb, line 13
def from_options(options = {})
  options[:berksfile] ||= File.join(Dir.pwd, Berkshelf::DEFAULT_FILENAME)
  symbolized = Hash[options.map { |k, v| [k.to_sym, v] }]
  from_file(options[:berksfile], symbolized.select { |k,| %i{except only delete}.include? k })
end
new(path, options = {}) click to toggle source

Create a new Berksfile object.

@param [String] path

path on disk to the file containing the contents of this Berksfile

@option options [Symbol, Array<String>] :except

Group(s) to exclude which will cause any dependencies marked as a member of the
group to not be installed

@option options [Symbol, Array<String>] :only

Group(s) to include which will cause any dependencies marked as a member of the
group to be installed and all others to be ignored
# File lib/berkshelf/berksfile.rb, line 67
def initialize(path, options = {})
  @filepath         = File.expand_path(path)
  @dependencies     = {}
  @sources          = {}
  @delete           = options[:delete]

  # defaults for what solvers to use
  @required_solver  = nil
  @preferred_solver = :gecode

  if options[:except] && options[:only]
    raise ArgumentError, "Cannot specify both :except and :only!"
  elsif options[:except]
    except = Array(options[:except]).collect(&:to_sym)
    @filter = ->(dependency) { (except & dependency.groups).empty? }
  elsif options[:only]
    only = Array(options[:only]).collect(&:to_sym)
    @filter = ->(dependency) { !(only & dependency.groups).empty? }
  else
    @filter = ->(dependency) { true }
  end
end

Public Instance Methods

[](name) click to toggle source

@param [String] name

name of the dependency to return

@return [Dependency]

# File lib/berkshelf/berksfile.rb, line 373
def [](name)
  @dependencies[name]
end
Also aliased as: get_dependency
add_dependency(name, constraint = nil, options = {}) click to toggle source

Add a dependency of the given name and constraint to the array of dependencies.

@param [String] name

the name of the dependency to add

@param [String, Semverse::Constraint] constraint

the constraint to lock the dependency to

@option options [Symbol, Array] :group

the group or groups that the cookbook belongs to

@option options [String] :path

a filepath to the cookbook on your local disk

@option options [String] :git

the Git URL to clone

@raise [DuplicateDependencyDefined] if a dependency is added whose name conflicts

with a dependency who has already been added.

@return [Array<Dependency]

# File lib/berkshelf/berksfile.rb, line 281
def add_dependency(name, constraint = nil, options = {})
  if @dependencies[name]
    # Only raise an exception if the dependency is a true duplicate
    groups = (options[:group].nil? || options[:group].empty?) ? [:default] : options[:group]
    unless (@dependencies[name].groups & groups).empty?
      raise DuplicateDependencyDefined.new(name)
    end
  end

  # this appears to be dead code
  # if options[:path]
  #  metadata_file = File.join(options[:path], "metadata.rb")
  # end

  options[:constraint] = constraint

  @dependencies[name] = Dependency.new(self, name, options)
end
cookbook(*args) click to toggle source

Add a cookbook dependency to the Berksfile to be retrieved and have its dependencies recursively retrieved and resolved.

@example a cookbook dependency that will be retrieved from one of the default locations

cookbook 'artifact'

@example a cookbook dependency that will be retrieved from a path on disk

cookbook 'artifact', path: '/Users/reset/code/artifact'

@example a cookbook dependency that will be retrieved from a Git server

cookbook 'artifact', git: 'https://github.com/chef/artifact-cookbook.git'

@overload cookbook(name, version_constraint, options = {})

@param [#to_s] name
@param [#to_s] version_constraint

@option options [Symbol, Array] :group
  the group or groups that the cookbook belongs to
@option options [String] :path
  a filepath to the cookbook on your local disk
@option options [String] :git
  the Git URL to clone

@see PathLocation
@see GitLocation

@overload cookbook(name, options = {})

@param [#to_s] name

@option options [Symbol, Array] :group
  the group or groups that the cookbook belongs to
@option options [String] :path
  a filepath to the cookbook on your local disk
@option options [String] :git
  the Git URL to clone

@see PathLocation
@see GitLocation
# File lib/berkshelf/berksfile.rb, line 148
def cookbook(*args)
  options = args.last.is_a?(Hash) ? args.pop : {}
  name, constraint = args

  options[:path] &&= File.expand_path(options[:path], File.dirname(filepath))
  options[:group] = Array(options[:group])

  if @active_group
    options[:group] += @active_group
  end

  add_dependency(name, constraint, **options)
end
cookbooks() click to toggle source

Behaves the same as {Berksfile#dependencies}, but this method returns an array of CachedCookbook objects instead of dependency objects. This method relies on the {Berksfile#retrieve_locked} method to load the proper cached cookbook from the Berksfile + lockfile combination.

@see [Berksfile#dependencies]

for a description of the +options+ hash

@see [Berksfile#retrieve_locked]

for a list of possible exceptions that might be raised and why

@return [Array<CachedCookbook>]

# File lib/berkshelf/berksfile.rb, line 330
def cookbooks
  dependencies.map { |dependency| retrieve_locked(dependency) }
end
dependencies() click to toggle source

@return [Array<Dependency>]

# File lib/berkshelf/berksfile.rb, line 313
def dependencies
  @dependencies.values.sort.select(&@filter)
end
extension(name) click to toggle source

Activate a Berkshelf extension at runtime.

@example Activate the Mercurial extension

extension 'hg'

@raise [LoadError]

if the extension cannot be loaded

@param [String] name

the name of the extension to activate

@return [true]

# File lib/berkshelf/berksfile.rb, line 102
def extension(name)
  require "berkshelf/#{name}"
  true
rescue LoadError
  raise LoadError, "Could not load an extension by the name `#{name}'. " \
    "Please make sure it is installed."
end
find(name) click to toggle source

Find a dependency defined in this berksfile by name.

@param [String] name

the name of the cookbook dependency to search for

@return [Dependency, nil]

the cookbook dependency, or nil if one does not exist
# File lib/berkshelf/berksfile.rb, line 340
def find(name)
  @dependencies[name]
end
find_chefignore(path) click to toggle source

backcompat with ridley lookup of chefignore

# File lib/berkshelf/berksfile.rb, line 608
def find_chefignore(path)
  filename = "chefignore"

  Pathname.new(path).ascend do |dir|
    next unless dir.directory?

    [
      dir.join(filename),
      dir.join("cookbooks", filename),
      dir.join(".chef",     filename),
    ].each do |possible|
      return possible.expand_path.to_s if possible.exist?
    end
  end

  nil
end
get_dependency(name)
Alias for: []
group(*args) { || ... } click to toggle source
# File lib/berkshelf/berksfile.rb, line 163
def group(*args)
  @active_group = args
  yield
  @active_group = nil
end
groups() click to toggle source

@return [Hash]

a hash containing group names as keys and an array of Dependencies
that are a member of that group as values

Example:
  {
    nautilus: [
      #<Dependency: nginx (~> 1.0.0)>,
      #<Dependency: mysql (~> 1.2.4)>
    ],
    skarner: [
      #<Dependency: nginx (~> 1.0.0)>
    ]
  }
# File lib/berkshelf/berksfile.rb, line 358
def groups
  {}.tap do |groups|
    dependencies.each do |dependency|
      dependency.groups.each do |group|
        groups[group] ||= []
        groups[group] << dependency
      end
    end
  end
end
has_dependency?(dependency) click to toggle source

Check if the Berksfile has the given dependency, taking into account group and –only/–except flags.

@param [String, Dependency] dependency

the dependency or name of dependency to check presence of

@return [Boolean]

# File lib/berkshelf/berksfile.rb, line 307
def has_dependency?(dependency)
  name = Dependency.name(dependency)
  dependencies.map(&:name).include?(name)
end
install() click to toggle source

Install the dependencies listed in the Berksfile, respecting the locked versions in the Berksfile.lock.

  1. Check that a lockfile exists. If a lockfile does not exist, all dependencies are considered to be “unlocked”. If a lockfile is specified, a definition is created via the following algorithm:

    • For each source, see if there exists a locked version that still satisfies the version constraint in the Berksfile. If there exists such a source, remove it from the list of unlocked sources. If not, then either a version constraint has changed, or a new source has been added to the Berksfile. In the event that a locked_source exists, but it no longer satisfies the constraint, this method will raise a {OutdatedCookbookSource}, and inform the user to run berks update COOKBOOK to remedy the issue.

    • Remove any locked sources that no longer exist in the Berksfile (i.e. a cookbook source was removed from the Berksfile).

  2. Resolve the collection of locked and unlocked dependencies.

  3. Write out a new lockfile.

@raise [OutdatedDependency]

if the lockfile constraints do not satisfy the Berksfile constraints

@return [Array<CachedCookbook>]

# File lib/berkshelf/berksfile.rb, line 404
def install
  Installer.new(self).run
end
list() click to toggle source

The cached cookbooks installed by this Berksfile.

@raise [LockfileNotFound]

if there is no lockfile

@raise [CookbookNotFound]

if a listed source could not be found

@return [Hash<Dependency, CachedCookbook>]

the list of dependencies as keys and the cached cookbook as the value
# File lib/berkshelf/berksfile.rb, line 460
def list
  validate_lockfile_present!
  validate_lockfile_trusted!
  validate_dependencies_installed!

  lockfile.graph.locks.values
end
lockfile() click to toggle source

Get the lockfile corresponding to this Berksfile. This is necessary because the user can specify a different path to the Berksfile. So assuming the lockfile is named “Berksfile.lock” is a poor assumption.

@return [Lockfile]

the lockfile corresponding to this berksfile, or a new Lockfile if one does
not exist
# File lib/berkshelf/berksfile.rb, line 737
def lockfile
  @lockfile ||= Lockfile.from_berksfile(self)
end
metadata(options = {}) click to toggle source

Use a Cookbook metadata file to determine additional cookbook dependencies to retrieve. All dependencies found in the metadata will use the default locations set in the Berksfile (if any are set) or the default locations defined by Berkshelf.

@param [Hash] options

@option options [String] :path

path to the metadata file
# File lib/berkshelf/berksfile.rb, line 178
def metadata(options = {})
  path = options[:path] || File.dirname(filepath)

  loader = Chef::Cookbook::CookbookVersionLoader.new(path)
  loader.load!
  cookbook_version = loader.cookbook_version
  metadata = cookbook_version.metadata

  add_dependency(metadata.name, nil, path: path, metadata: true)
end
outdated(*names, include_non_satisfying: false) click to toggle source

List of all the cookbooks which have a newer version found at a source that satisfies the constraints of your dependencies.

@param [Boolean] include_non_satisfying

include cookbooks that would not satisfy the given constraints in the
+Berksfile+. Defaults to false.

@return [Hash]

a hash of cached cookbooks and their latest version grouped by their
remote API source. The hash will be empty if there are no newer
cookbooks for any of your dependencies (that still satisfy the given)
constraints in the +Berksfile+.

@example

berksfile.outdated #=> {
  "nginx" => {
    "local" => #<Version 1.8.0>,
    "remote" => {
      #<Source uri: "https://supermarket.chef.io"> #=> #<Version 2.6.2>
    }
  }
}
# File lib/berkshelf/berksfile.rb, line 490
def outdated(*names, include_non_satisfying: false)
  validate_lockfile_present!
  validate_lockfile_trusted!
  validate_dependencies_installed!
  validate_cookbook_names!(names)

  lockfile.graph.locks.inject({}) do |hash, (name, dependency)|
    sources.each do |source|
      cookbooks = source.versions(name)

      latest = cookbooks.select do |cookbook|
        (include_non_satisfying || dependency.version_constraint.satisfies?(cookbook.version)) &&
          Semverse::Version.coerce(cookbook.version) > dependency.locked_version
      end.max_by(&:version)

      unless latest.nil?
        hash[name] ||= {
          "local" => dependency.locked_version,
          "remote" => {
            source => Semverse::Version.coerce(latest.version),
          },
        }
      end
    end

    hash
  end
end
package(path) click to toggle source

Package the given cookbook for distribution outside of berkshelf. If the name attribute is not given, all cookbooks in the Berksfile will be packaged.

@param [String] path

the path where the tarball will be created

@raise [PackageError]

@return [String]

the path to the package
# File lib/berkshelf/berksfile.rb, line 594
def package(path)
  packager = Packager.new(path)
  packager.validate!

  outdir = Dir.mktmpdir do |temp_dir|
    Berkshelf.ui.mute { vendor(File.join(temp_dir, "cookbooks")) }
    packager.run(temp_dir)
  end

  Berkshelf.formatter.package(outdir)
  outdir
end
retrieve_locked(dependency) click to toggle source

Retrieve information about a given cookbook that is installed by this Berksfile. Unlike {#find}, which returns a dependency, this method returns the corresponding CachedCookbook for the given name.

@raise [LockfileNotFound]

if there is no lockfile containing that cookbook

@raise [CookbookNotFound]

if there is a lockfile with a cookbook, but the cookbook is not downloaded

@param [Dependency] name

the name of the cookbook to find

@return [CachedCookbook]

the CachedCookbook that corresponds to the given name parameter
# File lib/berkshelf/berksfile.rb, line 447
def retrieve_locked(dependency)
  lockfile.retrieve(dependency)
end
solver(name, precedence = :preferred) click to toggle source

Configure a specific engine for the ‘solve’ gem to use when computing dependencies. You may optionally specify how strong a requirement this is. If omitted, the default precedence is :preferred.

If :required is specified and cannot be loaded, Resolver#resolve will raise an ArgumentError. If :preferred is specified and cannot be loaded, Resolver#resolve silently catch any errors and use whatever default method the ‘solve’ gem provides (as of 2.0.1, solve defaults to :ruby).

@example

solver :gecode
solver :gecode, :preferred
solver :gecode, :required
solver :ruby
solver :ruby, :preferred
solver :ruby, :required

@param [Symbol] name

name of engine for solver gem to use for depsolving

@param [Symbol] precedence

how strong a requirement using this solver is
valid values are :required, :preferred

@raise [ArgumentError]

# File lib/berkshelf/berksfile.rb, line 237
def solver(name, precedence = :preferred)
  if name && precedence == :required
    @required_solver = name
  elsif name && precedence == :preferred
    @preferred_solver = name
  else
    raise ArgumentError, "Invalid solver precedence ':#{precedence}'"
  end
end
source(api_url, **options) click to toggle source

Add a Berkshelf API source to use when building the index of known cookbooks. The indexes will be searched in the order they are added. If a cookbook is found in the first source then a cookbook in a second source would not be used.

@example

source "https://supermarket.chef.io"
source "https://berks-api.riotgames.com"

@param [String] api_url

url for the api to add

@param [Hash] options

extra source options

@raise [InvalidSourceURI]

@return [Array<Source>]

# File lib/berkshelf/berksfile.rb, line 207
def source(api_url, **options)
  source = Source.new(self, api_url, **options)
  @sources[source.uri.to_s] = source
end
source_for(name, version) click to toggle source

@param [Dependency] dependency

the dependency to find the source for
# File lib/berkshelf/berksfile.rb, line 259
def source_for(name, version)
  sources.find { |source| source.cookbook(name, version) }
end
sources() click to toggle source

@return [Array<Source>]

# File lib/berkshelf/berksfile.rb, line 249
def sources
  if @sources.empty?
    raise NoAPISourcesDefined
  else
    @sources.values
  end
end
update(*names) click to toggle source

Update the given set of dependencies (or all if no names are given).

@option options [String, Array<String>] :cookbooks

Names of the cookbooks to retrieve dependencies for
# File lib/berkshelf/berksfile.rb, line 412
def update(*names)
  validate_lockfile_present!
  validate_cookbook_names!(names)

  Berkshelf.log.info "Updating cookbooks"

  # Calculate the list of cookbooks to unlock
  if names.empty?
    Berkshelf.log.debug "  Unlocking all the things!"
    lockfile.unlock_all
  else
    names.each do |name|
      Berkshelf.log.debug "  Unlocking #{name}"
      lockfile.unlock(name, true)
    end
  end

  # NOTE: We intentionally do NOT pass options to the installer
  install
end
upload(*args) click to toggle source

Upload the cookbooks installed by this Berksfile

@overload upload(names = [])

@param [Array<String>] names
  the list of cookbooks (by name) to upload to the remote Chef Server

@overload upload(names = [], options = {})

@param [Array<String>] names
  the list of cookbooks (by name) to upload to the remote Chef Server
@param [Hash<Symbol, Object>] options
  the list of options to pass to the uploader

@option options [Boolean] :force (false)
  upload the cookbooks even if the version already exists and is frozen
  on the remote Chef Server
@option options [Boolean] :freeze (true)
  freeze the uploaded cookbooks on the remote Chef Server so that it
  cannot be overwritten on future uploads
@option options [Hash] :ssl_verify (true)
  use SSL verification while connecting to the remote Chef Server
@option options [Boolean] :halt_on_frozen (false)
  raise an exception ({FrozenCookbook}) if one of the cookbooks already
  exists on the remote Chef Server and is frozen
@option options [String] :server_url
  the URL (endpoint) to the remote Chef Server
@option options [String] :client_name
  the client name for the remote Chef Server
@option options [String] :client_key
  the client key (pem) for the remote Chef Server

@example Upload all cookbooks

berksfile.upload

@example Upload the ‘apache2’ and ‘mysql’ cookbooks

berksfile.upload('apache2', 'mysql')

@example Upload and freeze all cookbooks

berksfile.upload(freeze: true)

@example Upload and freeze the ‘chef-sugar` cookbook

berksfile.upload('chef-sugar', freeze: true)

@raise [UploadFailure]

if you are uploading cookbooks with an invalid or not-specified client key

@raise [DependencyNotFound]

if one of the given cookbooks is not a dependency defined in the Berksfile

@raise [FrozenCookbook]

if the cookbook being uploaded is a {metadata} cookbook and is already
frozen on the remote Chef Server; indirect dependencies or non-metadata
dependencies are just skipped

@return [Array<CachedCookbook>]

the list of cookbooks that were uploaded to the Chef Server
# File lib/berkshelf/berksfile.rb, line 575
def upload(*args)
  validate_lockfile_present!
  validate_lockfile_trusted!
  validate_dependencies_installed!

  Uploader.new(self, *args).run
end
vendor(destination) click to toggle source

Install the Berksfile or Berksfile.lock and then sync the cached cookbooks into directories within the given destination matching their name.

@param [String] destination

filepath to vendor cookbooks to

@return [String, nil]

the expanded path cookbooks were vendored to or nil if nothing was vendored
# File lib/berkshelf/berksfile.rb, line 634
def vendor(destination)
  Dir.mktmpdir("vendor") do |scratch|
    cached_cookbooks = install

    return nil if cached_cookbooks.empty?

    cached_cookbooks.each do |cookbook|
      Berkshelf.formatter.vendor(cookbook, destination)

      cookbook_destination = File.join(scratch, cookbook.cookbook_name)
      FileUtils.mkdir_p(cookbook_destination)

      # Dir.glob does not support backslash as a File separator
      src   = cookbook.path.to_s.tr("\\", "/")
      files = FileSyncer.glob(File.join(src, "**/*"))

      # strip directories
      files.reject! { |file_path| File.directory?(file_path) }

      # convert to relative Pathname objects for chefignore
      files.map! { |file_path| Chef::Util::PathHelper.relative_path_from(cookbook.path.to_s, file_path) }

      chefignore = Chef::Cookbook::Chefignore.new(find_chefignore(cookbook.path.to_s) || cookbook.path.to_s)

      # apply chefignore
      files.reject! { |file_path| chefignore.ignored?(file_path) }

      # convert Pathname objects back to strings
      files.map!(&:to_s)

      # copy each file to destination
      files.each do |rpath|
        FileUtils.mkdir_p( File.join(cookbook_destination, File.dirname(rpath)) )
        FileUtils.cp( File.join(cookbook.path.to_s, rpath), File.join(cookbook_destination, rpath) )
      end

      cookbook.compile_metadata(cookbook_destination)
    end

    # Don't vendor the raw metadata (metadata.rb). The raw metadata is
    # unecessary for the client, and this is required until compiled metadata
    # (metadata.json) takes precedence over raw metadata in the Chef-Client.
    #
    # We can change back to including the raw metadata in the future after
    # this has been fixed or just remove these comments. There is no
    # circumstance that I can currently think of where raw metadata should
    # ever be read by the client.
    #
    # - Jamie
    #
    # See the following tickets for more information:
    #
    #   * https://tickets.opscode.com/browse/CHEF-4811
    #   * https://tickets.opscode.com/browse/CHEF-4810
    FileSyncer.sync(scratch, destination, exclude: EXCLUDED_VCS_FILES_WHEN_VENDORING, delete: @delete)
  end

  destination
end
verify() click to toggle source

Perform a validation with ‘Validator#validate` on each cached cookbook associated with the Lockfile of this Berksfile.

This function will return true or raise the first errors encountered.

# File lib/berkshelf/berksfile.rb, line 698
def verify
  validate_lockfile_present!
  validate_lockfile_trusted!
  Berkshelf.formatter.msg "Verifying (#{lockfile.cached.length}) cookbook(s)..."
  Validator.validate(lockfile.cached)
  true
end
viz(outfile = nil, format = "png") click to toggle source

Visualize the current Berksfile as a “graph” using DOT.

@param [String] outfile

the name/path to outfile the file

@return [String] path

the path where the image was written
# File lib/berkshelf/berksfile.rb, line 713
def viz(outfile = nil, format = "png")
  outfile = File.join(Dir.pwd, outfile || "graph.png")

  validate_lockfile_present!
  validate_lockfile_trusted!
  vizualiser = Visualizer.from_lockfile(lockfile)

  case format
  when "dot"
    vizualiser.to_dot_file(outfile)
  when "png"
    vizualiser.to_png(outfile)
  else
    raise ConfigurationError, "Vizualiser format #{format} not recognised."
  end
end

Private Instance Methods

validate_cookbook_names!(names) click to toggle source

Determine if any cookbooks were specified that aren’t in our shelf.

@param [Array<String>] names

a list of cookbook names

@raise [DependencyNotFound]

if a cookbook name is given that does not exist
# File lib/berkshelf/berksfile.rb, line 795
def validate_cookbook_names!(names)
  missing = names - lockfile.graph.locks.keys

  unless missing.empty?
    raise DependencyNotFound.new(missing)
  end
end
validate_dependencies_installed!() click to toggle source

Ensure that all dependencies in the lockfile are installed on this system. You should validate that the lockfile can be trusted before using this method.

@raise [DependencyNotInstalled]

if the dependency in the lockfile is not in the Berkshelf shelf on
this system

@return [true]

# File lib/berkshelf/berksfile.rb, line 778
def validate_dependencies_installed!
  lockfile.graph.locks.each do |_, dependency|
    unless dependency.installed?
      raise DependencyNotInstalled.new(dependency)
    end
  end

  true
end
validate_lockfile_present!() click to toggle source

Ensure the lockfile is present on disk.

@raise [LockfileNotFound]

if the lockfile does not exist on disk

@return [true]

# File lib/berkshelf/berksfile.rb, line 749
def validate_lockfile_present!
  raise LockfileNotFound unless lockfile.present?

  true
end
validate_lockfile_trusted!() click to toggle source

Ensure that all dependencies defined in the Berksfile exist in this lockfile.

@raise [LockfileOutOfSync]

if there are dependencies specified in the Berksfile which do not
exist (or are not satisifed by) the lockfile

@return [true]

# File lib/berkshelf/berksfile.rb, line 763
def validate_lockfile_trusted!
  raise LockfileOutOfSync unless lockfile.trusted?

  true
end