class Berkshelf::Lockfile

Constants

DEFAULT_FILENAME
DEPENDENCIES
GRAPH

Attributes

berksfile[R]

@return [Berkshelf::Berksfile]

the Berksfile for this Lockfile
filepath[R]

@return [Pathname]

the path to this Lockfile
graph[R]

@return [Lockfile::Graph]

the dependency graph

Public Class Methods

from_berksfile(berksfile) click to toggle source

Initialize a Lockfile from the given Berksfile

@param [Berkshelf::Berksfile] berksfile

the Berksfile associated with the Lockfile
# File lib/berkshelf/lockfile.rb, line 19
def from_berksfile(berksfile)
  parent = File.expand_path(File.dirname(berksfile.filepath))
  lockfile_name = "#{File.basename(berksfile.filepath)}.lock"

  filepath = File.join(parent, lockfile_name)
  new(berksfile: berksfile, filepath: filepath)
end
from_file(filepath) click to toggle source

Initialize a Lockfile from the given filepath

@param [String] filepath

filepath to the lockfile
# File lib/berkshelf/lockfile.rb, line 11
def from_file(filepath)
  new(filepath: filepath)
end
new(options = {}) click to toggle source

Create a new lockfile instance associated with the given Berksfile. If a Lockfile exists, it is automatically loaded. Otherwise, an empty instance is created and ready for use.

@option options [String] :filepath

filepath to the lockfile

@option options [Berkshelf::Berksfile] :berksfile

the Berksfile associated with this Lockfile
# File lib/berkshelf/lockfile.rb, line 55
def initialize(options = {})
  @filepath     = options[:filepath].to_s
  @berksfile    = options[:berksfile]
  @dependencies = {}
  @graph        = Graph.new(self)

  parse if File.exist?(@filepath)
end

Public Instance Methods

add(dependency) click to toggle source

Add a new cookbok to the lockfile. If an entry already exists by the given name, it will be overwritten.

@param [Dependency] dependency

the dependency to add

@return [Dependency]

# File lib/berkshelf/lockfile.rb, line 274
def add(dependency)
  @dependencies[Dependency.name(dependency)] = dependency
end
apply(name, options = {}) click to toggle source

Resolve this Berksfile and apply the locks found in the generated Berksfile.lock to the target Chef environment

@param [String] name

the name of the environment to apply the locks to

@option options [Hash] :ssl_verify (true)

Disable/Enable SSL verification during uploads

@option options [String] :envfile

Environment file to update

@raise [EnvironmentNotFound]

if the target environment was not found on the remote Chef Server

@raise [ChefConnectionError]

if you are locking cookbooks with an invalid or not-specified client
configuration
# File lib/berkshelf/lockfile.rb, line 206
def apply(name, options = {})
  locks = graph.locks.inject({}) do |hash, (dep_name, dependency)|
    hash[dep_name] = "= #{dependency.locked_version}"
    hash
  end

  if options[:envfile]
    update_environment_file(options[:envfile], locks) if options[:envfile]
  else
    Berkshelf.ridley_connection(options) do |connection|
      environment =
        begin
          Chef::Environment.from_hash(connection.get("environments/#{name}"))
        rescue Berkshelf::APIClient::ServiceNotFound
          raise EnvironmentNotFound.new(name)
        end

      environment.cookbook_versions locks
      environment.save unless options[:envfile]
    end
  end
end
cached() click to toggle source

@return [Array<CachedCookbook>]

# File lib/berkshelf/lockfile.rb, line 230
def cached
  graph.locks.values.collect(&:cached_cookbook)
end
dependencies() click to toggle source

The list of dependencies constrained in this lockfile.

@return [Array<Berkshelf::Dependency>]

the list of dependencies in this lockfile
# File lib/berkshelf/lockfile.rb, line 238
def dependencies
  @dependencies.values
end
dependency?(dependency) click to toggle source

Determine if this lockfile contains the given dependency.

@param [String, Berkshelf::Dependency] dependency

the cookbook dependency/name to determine existence of

@return [Boolean]

true if the dependency exists, false otherwise
# File lib/berkshelf/lockfile.rb, line 262
def dependency?(dependency)
  !find(dependency).nil?
end
Also aliased as: has_dependency?
find(dependency) click to toggle source

Find the given dependency in this lockfile. This method accepts a dependency attribute which may either be the name of a cookbook (String) or an actual cookbook dependency.

@param [String, Berkshelf::Dependency] dependency

the cookbook dependency/name to find

@return [Berkshelf::Dependency, nil]

the cookbook dependency from this lockfile or nil if one was not found
# File lib/berkshelf/lockfile.rb, line 251
def find(dependency)
  @dependencies[Dependency.name(dependency)]
end
has_dependency?(dependency)
Alias for: dependency?
inspect() click to toggle source

@private

# File lib/berkshelf/lockfile.rb, line 510
def inspect
  "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, dependencies: #{dependencies.inspect}>"
end
locks() click to toggle source
# File lib/berkshelf/lockfile.rb, line 278
def locks
  graph.locks
end
parse() click to toggle source

Parse the lockfile.

@return true

# File lib/berkshelf/lockfile.rb, line 67
def parse
  LockfileParser.new(self).run
  true
rescue => e
  raise LockfileParserError.new(e)
end
present?() click to toggle source

Determine if this lockfile actually exists on disk.

@return [Boolean]

true if this lockfile exists on the disk, false otherwise
# File lib/berkshelf/lockfile.rb, line 78
def present?
  File.exist?(filepath) && !File.read(filepath).strip.empty?
end
reduce!() click to toggle source

Iterate over each top-level dependency defined in the lockfile and check if that dependency is still defined in the Berksfile.

If the dependency is no longer present in the Berksfile, it is “safely” removed using {Lockfile#unlock} and {Lockfile#remove}. This prevents the lockfile from “leaking” dependencies when they have been removed from the Berksfile, but still remained locked in the lockfile.

If the dependency exists, a constraint comparison is conducted to verify that the locked dependency still satisifes the original constraint. This handles the edge case where a user has updated or removed a constraint on a dependency that already existed in the lockfile.

@raise [OutdatedDependency]

if the constraint exists, but is no longer satisifed by the existing
locked version

@return [Array<Dependency>]

# File lib/berkshelf/lockfile.rb, line 392
def reduce!
  Berkshelf.log.info "Reducing lockfile"

  Berkshelf.log.debug "Current lockfile:"
  Berkshelf.log.debug ""
  to_lock.each_line do |line|
    Berkshelf.log.debug "  #{line.chomp}"
  end
  Berkshelf.log.debug ""

  # Unlock any locked dependencies that are no longer in the Berksfile
  Berkshelf.log.debug "Unlocking dependencies no longer in the Berksfile"

  dependencies.each do |dependency|
    Berkshelf.log.debug "  Checking #{dependency}"

    if berksfile.has_dependency?(dependency.name)
      Berkshelf.log.debug "    Skipping unlock for #{dependency.name} (exists in the Berksfile)"
    else
      Berkshelf.log.debug "    Unlocking #{dependency.name}"
      unlock(dependency, true)
    end
  end

  # Remove any transitive dependencies
  Berkshelf.log.debug "Removing transitive dependencies"

  berksfile.dependencies.each do |dependency|
    Berkshelf.log.debug "  Checking #{dependency}"

    graphed = graph.find(dependency)

    if graphed.nil?
      Berkshelf.log.debug "    Skipping (not graphed)"
      next
    end

    unless dependency.version_constraint.satisfies?(graphed.version)
      Berkshelf.log.debug "    Constraints are not satisfied!"
      raise OutdatedDependency.new(graphed, dependency)
    end

    # Locking dependency version to the graphed version if
    # constraints are satisfied by it.
    dependency.locked_version = graphed.version

    if ( cookbook = dependency.cached_cookbook )
      Berkshelf.log.debug "    Cached cookbook exists"
      Berkshelf.log.debug "    Updating cookbook dependencies if required"
      graphed.set_dependencies(cookbook.dependencies)
    end
  end

  # Iteratively remove orphan dependencies
  orphans = true
  while orphans
    orphans = false
    graph.each do |cookbook|
      name = cookbook.name
      unless dependency?(name) || graph.dependency?(name)
        Berkshelf.log.debug "#{cookbook} identified as orphan; removing it"
        unlock(name)
        orphans = true
      end
    end
  end

  Berkshelf.log.debug "New lockfile:"
  Berkshelf.log.debug ""
  to_lock.each_line do |line|
    Berkshelf.log.debug "  #{line.chomp}"
  end
  Berkshelf.log.debug ""
end
retrieve(dependency) click to toggle source

Retrieve information about a given cookbook that is in this lockfile.

@raise [DependencyNotFound]

if this lockfile does not have the given dependency

@raise [CookbookNotFound]

if this lockfile has the dependency, but the cookbook is not installed

@param [String, Dependency] dependency

the dependency or name of the dependency to find

@return [CachedCookbook]

the CachedCookbook that corresponds to the given name parameter
# File lib/berkshelf/lockfile.rb, line 294
def retrieve(dependency)
  locked = graph.locks[Dependency.name(dependency)]

  if locked.nil?
    raise DependencyNotFound.new(Dependency.name(dependency))
  end

  unless locked.installed?
    name    = locked.name
    version = locked.locked_version || locked.version_constraint
    raise CookbookNotFound.new(name, version, "in the cookbook store")
  end

  locked.cached_cookbook
end
satisfies_transitive?(graph_item, checked, level = 0) click to toggle source

Recursive helper method for checking if transitive dependencies (i.e. those dependencies defined in the metadata) are satisfied. This method is used in calculating the trustworthiness of a lockfile.

@param [GraphItem] graph_item

the graph item to check transitive dependencies for

@param [Hash] checked

the list of already checked dependencies

@return [Boolean]

# File lib/berkshelf/lockfile.rb, line 157
def satisfies_transitive?(graph_item, checked, level = 0)
  indent = " " * (level + 2)

  Berkshelf.log.debug "#{indent}Checking transitive dependencies for #{graph_item}"

  if checked[graph_item.name]
    Berkshelf.log.debug "#{indent}  Already checked - skipping"
    return true
  end

  graph_item.dependencies.each do |name, constraint|
    Berkshelf.log.debug "#{indent}  Checking #{name} (#{constraint})"

    graphed = graph.find(name)
    if graphed.nil?
      Berkshelf.log.debug "#{indent}  Not graphed - cannot be satisifed"
      return false
    end

    unless Semverse::Constraint.new(constraint).satisfies?(graphed.version)
      Berkshelf.log.debug "#{indent}  Version constraint is not satisfied"
      return false
    end

    checked[name] = true

    unless satisfies_transitive?(graphed, checked, level + 2)
      Berkshelf.log.debug "#{indent}  Transitive are not satisifed"
      return false
    end
  end
end
save() click to toggle source

Write the contents of the current statue of the lockfile to disk. This method uses an atomic file write. A temporary file is created, written, and then copied over the existing one. This ensures any partial updates or failures do no affect the lockfile. The temporary file is ensured deletion.

@return [true, false]

true if the lockfile was saved, false otherwise
# File lib/berkshelf/lockfile.rb, line 475
def save
  return false if dependencies.empty?

  tempfile = Tempfile.new(["Berksfile", ".lock"])

  tempfile.write(to_lock)

  tempfile.rewind
  tempfile.close

  # Move the lockfile into place
  FileUtils.cp(tempfile.path, filepath)

  true
ensure
  tempfile.unlink if tempfile
end
to_lock() click to toggle source

@private

# File lib/berkshelf/lockfile.rb, line 494
def to_lock
  out = "#{DEPENDENCIES}\n"
  dependencies.sort.each do |dependency|
    out << dependency.to_lock
  end
  out << "\n"
  out << graph.to_lock
  out
end
to_s() click to toggle source

@private

# File lib/berkshelf/lockfile.rb, line 505
def to_s
  "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}>"
end
trusted?() click to toggle source

Determine if we can “trust” this lockfile. A lockfile is trustworthy if:

1. All dependencies defined in the Berksfile are present in this
   lockfile
2. Each dependency's transitive dependencies are contained and locked
   in the lockfile
3. Each dependency's constraint in the Berksfile is still satisifed by
   the currently locked version

This method does not account for leaky dependencies (i.e. dependencies defined in the lockfile that are no longer present in the Berksfile); this edge case is handed by the installer.

@return [Boolean]

true if this lockfile is trusted, false otherwise
# File lib/berkshelf/lockfile.rb, line 97
def trusted?
  Berkshelf.log.info "Checking if lockfile is trusted"

  checked = {}

  berksfile.dependencies.each do |dependency|
    Berkshelf.log.debug "Checking #{dependency}"

    locked = find(dependency)
    if locked.nil?
      Berkshelf.log.debug "  Not in lockfile - cannot be trusted!"
      return false
    end

    graphed = graph.find(dependency)
    if graphed.nil?
      Berkshelf.log.debug "  Not in graph - cannot be trusted!"
      return false
    end

    if ( cookbook = locked.cached_cookbook )
      Berkshelf.log.debug "  Detected there is a cached cookbook"

      unless (cookbook.dependencies.keys - graphed.dependencies.keys).empty?
        Berkshelf.log.debug "  Cached cookbook has different dependencies - cannot be trusted!"
        return false
      end
    end

    unless dependency.location == locked.location
      Berkshelf.log.debug "  Different location - cannot be trusted!"
      Berkshelf.log.debug "    Dependency location: #{dependency.location.inspect}"
      Berkshelf.log.debug "    Locked location:     #{locked.location.inspect}"
      return false
    end

    unless dependency.version_constraint.satisfies?(graphed.version)
      Berkshelf.log.debug "  Version constraint is not satisified - cannot be trusted!"
      return false
    end

    unless satisfies_transitive?(graphed, checked)
      Berkshelf.log.debug "  Transitive dependencies not satisfies - cannot be trusted!"
      return false
    end
  end

  true
end
unlock(dependency, force = false) click to toggle source

Remove the given dependency from this lockfile. This method accepts a dependency attribute which may either be the name of a cookbook, as a String or an actual {Dependency} object.

This method first removes the dependency from the list of top-level dependencies. Then it uses a recursive algorithm to safely remove any other dependencies from the graph that are no longer needed.

@param [String] dependency

the name of the cookbook to remove
# File lib/berkshelf/lockfile.rb, line 358
def unlock(dependency, force = false)
  @dependencies.delete(Dependency.name(dependency))

  if force
    graph.remove(dependency, ignore: graph.locks.keys)
  else
    graph.remove(dependency)
  end
end
unlock_all() click to toggle source

Completely remove all dependencies from the lockfile and underlying graph.

# File lib/berkshelf/lockfile.rb, line 369
def unlock_all
  @dependencies = {}
  @graph        = Graph.new(self)
end
update(dependencies) click to toggle source

Replace the list of dependencies.

@param [Array<Berkshelf::Dependency>] dependencies

the list of dependencies to update
# File lib/berkshelf/lockfile.rb, line 340
def update(dependencies)
  @dependencies = {}

  dependencies.each do |dependency|
    @dependencies[Dependency.name(dependency)] = dependency
  end
end
update_environment_file(environment_file, locks) click to toggle source

Update local environment file

@param [String] environment_file

path to the envfile to update

@param [Hash] locks

A hash of cookbooks and versions to update the environment with

@raise [EnvironmentFileNotFound]

If environment file doesn't exist
# File lib/berkshelf/lockfile.rb, line 320
def update_environment_file(environment_file, locks)
  unless File.exist?(environment_file)
    raise EnvironmentFileNotFound.new(environment_file)
  end

  json_environment = JSON.parse(File.read(environment_file))

  json_environment["cookbook_versions"] = locks

  json = JSON.pretty_generate(json_environment)

  File.open(environment_file, "w") { |f| f.puts(json) }

  Berkshelf.log.info "Updated environment file #{environment_file}"
end