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 ⇒ Hash
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. -
#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
-
#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.
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.
51 52 53 54 55 56 57 58 |
# File 'lib/berkshelf/lockfile.rb', line 51 def initialize( = {}) @filepath = [:filepath].to_s @berksfile = [:berksfile] @dependencies = {} @graph = Graph.new(self) parse if File.exists?(@filepath) end |
Instance Attribute Details
#berksfile ⇒ Berkshelf::Berksfile (readonly)
Returns the Berksfile for this Lockfile.
37 38 39 |
# File 'lib/berkshelf/lockfile.rb', line 37 def berksfile @berksfile end |
#filepath ⇒ Pathname (readonly)
Returns the path to this Lockfile.
33 34 35 |
# File 'lib/berkshelf/lockfile.rb', line 33 def filepath @filepath end |
#graph ⇒ Hash (readonly)
Returns the dependency graph.
41 42 43 |
# File 'lib/berkshelf/lockfile.rb', line 41 def graph @graph end |
Class Method Details
.from_berksfile(berksfile) ⇒ Object
Initialize a Lockfile from the given Berksfile
18 19 20 21 |
# File 'lib/berkshelf/lockfile.rb', line 18 def from_berksfile(berksfile) filepath = File.join(File.dirname(File.(berksfile.filepath)), Lockfile::DEFAULT_FILENAME) new(berksfile: berksfile, filepath: filepath) end |
.from_file(filepath) ⇒ Object
Initialize a Lockfile from the given filepath
10 11 12 |
# File 'lib/berkshelf/lockfile.rb', line 10 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.
256 257 258 |
# File 'lib/berkshelf/lockfile.rb', line 256 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
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/berkshelf/lockfile.rb', line 200 def apply(name, = {}) Berkshelf.ridley_connection() do |connection| environment = connection.environment.find(name) raise EnvironmentNotFound.new(name) if environment.nil? locks = graph.locks.inject({}) do |hash, (name, dependency)| hash[name] = "= #{dependency.locked_version.to_s}" hash end environment.cookbook_versions = locks environment.save end end |
#dependencies ⇒ Array<Berkshelf::Dependency>
The list of dependencies constrained in this lockfile.
220 221 222 |
# File 'lib/berkshelf/lockfile.rb', line 220 def dependencies @dependencies.values end |
#dependency?(dependency) ⇒ Boolean Also known as: has_dependency?
Determine if this lockfile contains the given dependency.
244 245 246 |
# File 'lib/berkshelf/lockfile.rb', line 244 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.
233 234 235 |
# File 'lib/berkshelf/lockfile.rb', line 233 def find(dependency) @dependencies[Dependency.name(dependency)] end |
#inspect ⇒ Object
457 458 459 |
# File 'lib/berkshelf/lockfile.rb', line 457 def inspect "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, dependencies: #{dependencies.inspect}>" end |
#parse ⇒ Object
Parse the lockfile.
63 64 65 66 67 68 |
# File 'lib/berkshelf/lockfile.rb', line 63 def parse LockfileParser.new(self).run true rescue => e raise LockfileParserError.new(e) end |
#present? ⇒ Boolean
Determine if this lockfile actually exists on disk.
74 75 76 |
# File 'lib/berkshelf/lockfile.rb', line 74 def present? File.exists?(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.
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/berkshelf/lockfile.rb', line 344 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 if cookbook = dependency.cached_cookbook Berkshelf.log.debug " Cached cookbook exists" Berkshelf.log.debug " Checking dependencies on the cached cookbook" graphed.dependencies.each do |name, constraint| Berkshelf.log.debug " Checking #{name} (#{constraint})" # Unless the cookbook still depends on this key, we want to queue it # for unlocking. This is the magic that prevents transitive # dependency leaking. unless cookbook.dependencies.has_key?(name) Berkshelf.log.debug " Not found!" unlock(name, true) end 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.
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/berkshelf/lockfile.rb', line 272 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.
153 154 155 156 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 |
# File 'lib/berkshelf/lockfile.rb', line 153 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 unless satisfies_transitive?(graphed, checked, level + 2) Berkshelf.log.debug "#{indent} Transitive are not satisifed" return false end checked[name] = true 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.
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 |
# File 'lib/berkshelf/lockfile.rb', line 422 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
441 442 443 444 445 446 447 448 449 |
# File 'lib/berkshelf/lockfile.rb', line 441 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
452 453 454 |
# File 'lib/berkshelf/lockfile.rb', line 452 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.
93 94 95 96 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 |
# File 'lib/berkshelf/lockfile.rb', line 93 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 = dependency.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.
310 311 312 313 314 315 316 317 318 |
# File 'lib/berkshelf/lockfile.rb', line 310 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.
321 322 323 324 |
# File 'lib/berkshelf/lockfile.rb', line 321 def unlock_all @dependencies = {} @graph = Graph.new(self) end |
#update(dependencies) ⇒ Object
Replace the list of dependencies.
292 293 294 295 296 297 298 |
# File 'lib/berkshelf/lockfile.rb', line 292 def update(dependencies) @dependencies = {} dependencies.each do |dependency| @dependencies[Dependency.name(dependency)] = dependency end end |