class Berkshelf::Lockfile
Constants
- DEFAULT_FILENAME
- DEPENDENCIES
- GRAPH
Attributes
@return [Berkshelf::Berksfile]
the Berksfile for this Lockfile
@return [Pathname]
the path to this Lockfile
@return [Lockfile::Graph]
the dependency graph
Public Class Methods
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
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
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 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
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
@return [Array<CachedCookbook>]
# File lib/berkshelf/lockfile.rb, line 230 def cached graph.locks.values.collect(&:cached_cookbook) end
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
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
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
@private
# File lib/berkshelf/lockfile.rb, line 510 def inspect "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, dependencies: #{dependencies.inspect}>" end
# File lib/berkshelf/lockfile.rb, line 278 def locks graph.locks end
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
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
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 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
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
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
@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
@private
# File lib/berkshelf/lockfile.rb, line 505 def to_s "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}>" end
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
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
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
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 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