Class: Berkshelf::Lockfile
- Inherits:
-
Object
- Object
- Berkshelf::Lockfile
- Includes:
- Mixin::Logging
- Defined in:
- lib/berkshelf/lockfile.rb
Defined Under Namespace
Classes: Graph, LockfileParser
Constant Summary collapse
- DEFAULT_FILENAME =
"Berksfile.lock".freeze
- DEPENDENCIES =
"DEPENDENCIES".freeze
- GRAPH =
"GRAPH".freeze
Instance Attribute Summary collapse
-
#berksfile ⇒ Berkshelf::Berksfile
readonly
The Berksfile for this Lockfile.
-
#filepath ⇒ Pathname
readonly
The path to this Lockfile.
-
#graph ⇒ Lockfile::Graph
readonly
The dependency graph.
Attributes included from Mixin::Logging
Class Method Summary collapse
-
.from_berksfile(berksfile) ⇒ Object
Initialize a Lockfile from the given Berksfile.
-
.from_file(filepath) ⇒ Object
Initialize a Lockfile from the given filepath.
Instance Method Summary collapse
-
#add(dependency) ⇒ Dependency
Add a new cookbok to the lockfile.
-
#apply(name, options = {}) ⇒ Object
Resolve this Berksfile and apply the locks found in the generated
Berksfile.lock
to the target Chef environment. - #cached ⇒ Array<CachedCookbook>
-
#dependencies ⇒ Array<Berkshelf::Dependency>
The list of dependencies constrained in this lockfile.
-
#dependency?(dependency) ⇒ Boolean
(also: #has_dependency?)
Determine if this lockfile contains the given dependency.
-
#find(dependency) ⇒ Berkshelf::Dependency?
Find the given dependency in this lockfile.
-
#initialize(options = {}) ⇒ Lockfile
constructor
Create a new lockfile instance associated with the given Berksfile.
- #inspect ⇒ Object
- #locks ⇒ Object
-
#parse ⇒ Object
Parse the lockfile.
-
#present? ⇒ Boolean
Determine if this lockfile actually exists on disk.
-
#reduce! ⇒ Array<Dependency>
Iterate over each top-level dependency defined in the lockfile and check if that dependency is still defined in the Berksfile.
-
#retrieve(dependency) ⇒ CachedCookbook
Retrieve information about a given cookbook that is in this lockfile.
-
#satisfies_transitive?(graph_item, checked, level = 0) ⇒ Boolean
Recursive helper method for checking if transitive dependencies (i.e. those dependencies defined in the metadata) are satisfied.
-
#save ⇒ true, false
Write the contents of the current statue of the lockfile to disk.
- #to_lock ⇒ Object
- #to_s ⇒ Object
-
#trusted? ⇒ Boolean
Determine if we can “trust” this lockfile.
-
#unlock(dependency, force = false) ⇒ Object
Remove the given dependency from this lockfile.
-
#unlock_all ⇒ Object
Completely remove all dependencies from the lockfile and underlying graph.
-
#update(dependencies) ⇒ Object
Replace the list of dependencies.
-
#update_environment_file(environment_file, locks) ⇒ Object
Update local environment file.
Constructor Details
#initialize(options = {}) ⇒ Lockfile
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.
55 56 57 58 59 60 61 62 |
# File 'lib/berkshelf/lockfile.rb', line 55 def initialize( = {}) @filepath = [:filepath].to_s @berksfile = [:berksfile] @dependencies = {} @graph = Graph.new(self) parse if File.exist?(@filepath) end |
Instance Attribute Details
#berksfile ⇒ Berkshelf::Berksfile (readonly)
Returns the Berksfile for this Lockfile.
41 42 43 |
# File 'lib/berkshelf/lockfile.rb', line 41 def berksfile @berksfile end |
#filepath ⇒ Pathname (readonly)
Returns the path to this Lockfile.
37 38 39 |
# File 'lib/berkshelf/lockfile.rb', line 37 def filepath @filepath end |
#graph ⇒ Lockfile::Graph (readonly)
Returns the dependency graph.
45 46 47 |
# File 'lib/berkshelf/lockfile.rb', line 45 def graph @graph end |
Class Method Details
.from_berksfile(berksfile) ⇒ Object
Initialize a Lockfile from the given Berksfile
19 20 21 22 23 24 25 |
# File 'lib/berkshelf/lockfile.rb', line 19 def from_berksfile(berksfile) parent = File.(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) ⇒ Object
Initialize a Lockfile from the given filepath
11 12 13 |
# File 'lib/berkshelf/lockfile.rb', line 11 def from_file(filepath) new(filepath: filepath) end |
Instance Method Details
#add(dependency) ⇒ Dependency
Add a new cookbok to the lockfile. If an entry already exists by the given name, it will be overwritten.
274 275 276 |
# File 'lib/berkshelf/lockfile.rb', line 274 def add(dependency) @dependencies[Dependency.name(dependency)] = dependency end |
#apply(name, options = {}) ⇒ Object
Resolve this Berksfile and apply the locks found in the generated Berksfile.lock
to the target Chef environment
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/berkshelf/lockfile.rb', line 206 def apply(name, = {}) locks = graph.locks.inject({}) do |hash, (dep_name, dependency)| hash[dep_name] = "= #{dependency.locked_version}" hash end if [:envfile] update_environment_file([:envfile], locks) if [:envfile] else Berkshelf.ridley_connection() 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 [:envfile] end end end |
#cached ⇒ Array<CachedCookbook>
230 231 232 |
# File 'lib/berkshelf/lockfile.rb', line 230 def cached graph.locks.values.collect(&:cached_cookbook) end |
#dependencies ⇒ Array<Berkshelf::Dependency>
The list of dependencies constrained in this lockfile.
238 239 240 |
# File 'lib/berkshelf/lockfile.rb', line 238 def dependencies @dependencies.values end |
#dependency?(dependency) ⇒ Boolean Also known as: has_dependency?
Determine if this lockfile contains the given dependency.
262 263 264 |
# File 'lib/berkshelf/lockfile.rb', line 262 def dependency?(dependency) !find(dependency).nil? end |
#find(dependency) ⇒ Berkshelf::Dependency?
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.
251 252 253 |
# File 'lib/berkshelf/lockfile.rb', line 251 def find(dependency) @dependencies[Dependency.name(dependency)] end |
#inspect ⇒ Object
510 511 512 |
# File 'lib/berkshelf/lockfile.rb', line 510 def inspect "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, dependencies: #{dependencies.inspect}>" end |
#locks ⇒ Object
278 279 280 |
# File 'lib/berkshelf/lockfile.rb', line 278 def locks graph.locks end |
#parse ⇒ Object
Parse the lockfile.
67 68 69 70 71 72 |
# File 'lib/berkshelf/lockfile.rb', line 67 def parse LockfileParser.new(self).run true rescue => e raise LockfileParserError.new(e) end |
#present? ⇒ Boolean
Determine if this lockfile actually exists on disk.
78 79 80 |
# File 'lib/berkshelf/lockfile.rb', line 78 def present? File.exist?(filepath) && !File.read(filepath).strip.empty? end |
#reduce! ⇒ Array<Dependency>
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 #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.
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 |
# 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) ⇒ CachedCookbook
Retrieve information about a given cookbook that is in this lockfile.
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# 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) ⇒ Boolean
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.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# 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 ⇒ true, false
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.
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 |
# 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 ⇒ Object
494 495 496 497 498 499 500 501 502 |
# 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 ⇒ Object
505 506 507 |
# File 'lib/berkshelf/lockfile.rb', line 505 def to_s "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}>" end |
#trusted? ⇒ Boolean
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.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# 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) ⇒ Object
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.
358 359 360 361 362 363 364 365 366 |
# 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 ⇒ Object
Completely remove all dependencies from the lockfile and underlying graph.
369 370 371 372 |
# File 'lib/berkshelf/lockfile.rb', line 369 def unlock_all @dependencies = {} @graph = Graph.new(self) end |
#update(dependencies) ⇒ Object
Replace the list of dependencies.
340 341 342 343 344 345 346 |
# 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) ⇒ Object
Update local environment file
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# 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 |