lib/berkshelf/lockfile.rb in berkshelf-5.5.0 vs lib/berkshelf/lockfile.rb in berkshelf-5.6.0

- old
+ new

@@ -1,6 +1,6 @@ -require_relative 'dependency' +require_relative "dependency" module Berkshelf class Lockfile class << self # Initialize a Lockfile from the given filepath @@ -22,14 +22,14 @@ filepath = File.join(parent, lockfile_name) new(berksfile: berksfile, filepath: filepath) end end - DEFAULT_FILENAME = 'Berksfile.lock'.freeze + DEFAULT_FILENAME = "Berksfile.lock".freeze - DEPENDENCIES = 'DEPENDENCIES'.freeze - GRAPH = 'GRAPH'.freeze + DEPENDENCIES = "DEPENDENCIES".freeze + GRAPH = "GRAPH".freeze include Berkshelf::Mixin::Logging # @return [Pathname] # the path to this Lockfile @@ -92,11 +92,11 @@ # edge case is handed by the installer. # # @return [Boolean] # true if this lockfile is trusted, false otherwise def trusted? - Berkshelf.log.info 'Checking if lockfile is trusted' + Berkshelf.log.info "Checking if lockfile is trusted" checked = {} berksfile.dependencies.each do |dependency| Berkshelf.log.debug "Checking #{dependency}" @@ -152,11 +152,11 @@ # @param [Hash] checked # the list of already checked dependencies # # @return [Boolean] def satisfies_transitive?(graph_item, checked, level = 0) - indent = ' '*(level + 2) + 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" @@ -202,12 +202,12 @@ # @raise [ChefConnectionError] # if you are locking cookbooks with an invalid or not-specified client # configuration def apply(name, options = {}) locks = graph.locks.inject({}) do |hash, (name, dependency)| - hash[name] = "= #{dependency.locked_version.to_s}" - hash + hash[name] = "= #{dependency.locked_version}" + hash end if options[:envfile] update_environment_file(options[:envfile], locks) if options[:envfile] else @@ -295,11 +295,11 @@ end unless locked.installed? name = locked.name version = locked.locked_version || locked.version_constraint - raise CookbookNotFound.new(name, version, 'in the cookbook store') + raise CookbookNotFound.new(name, version, "in the cookbook store") end locked.cached_cookbook end @@ -318,15 +318,15 @@ raise EnvironmentFileNotFound.new(environment_file) end json_environment = JSON.parse(File.read(environment_file)) - json_environment['cookbook_versions'] = locks + json_environment["cookbook_versions"] = locks json = JSON.pretty_generate(json_environment) - File.open(environment_file, 'w'){ |f| f.puts(json) } + File.open(environment_file, "w") { |f| f.puts(json) } Berkshelf.log.info "Updated environment file #{environment_file}" end # Replace the list of dependencies. @@ -393,11 +393,10 @@ 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}" @@ -439,15 +438,15 @@ end end # Iteratively remove orphan dependencies orphans = true - while orphans do + while orphans orphans = false graph.each do |cookbook| name = cookbook.name - unless dependency?(name) or graph.dependency?(name) + unless dependency?(name) || graph.dependency?(name) Berkshelf.log.debug "#{cookbook} identified as orphan; removing it" unlock(name) orphans = true end end @@ -459,11 +458,10 @@ Berkshelf.log.debug " #{line.chomp}" end Berkshelf.log.debug "" 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. @@ -471,11 +469,11 @@ # @return [true, false] # true if the lockfile was saved, false otherwise def save return false if dependencies.empty? - tempfile = Tempfile.new(['Berksfile', '.lock']) + tempfile = Tempfile.new(["Berksfile", ".lock"]) tempfile.write(to_lock) tempfile.rewind tempfile.close @@ -507,361 +505,357 @@ # @private def inspect "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, dependencies: #{dependencies.inspect}>" end - private - # The class responsible for parsing the lockfile and turning it into a # useful data structure. - class LockfileParser - NAME_VERSION = '(?! )(.*?)(?: \(([^-]*)(?:-(.*))?\))?'.freeze - DEPENDENCY_PATTERN = /^ {2}#{NAME_VERSION}$/.freeze - DEPENDENCIES_PATTERN = /^ {4}#{NAME_VERSION}$/.freeze - OPTION_PATTERN = /^ {4}(.+)\: (.+)/.freeze + class LockfileParser + NAME_VERSION = '(?! )(.*?)(?: \(([^-]*)(?:-(.*))?\))?'.freeze + DEPENDENCY_PATTERN = /^ {2}#{NAME_VERSION}$/ + DEPENDENCIES_PATTERN = /^ {4}#{NAME_VERSION}$/ + OPTION_PATTERN = /^ {4}(.+)\: (.+)/ - # Create a new lockfile parser. - # - # @param [Lockfile] - def initialize(lockfile) - @lockfile = lockfile - @berksfile = lockfile.berksfile - end + # Create a new lockfile parser. + # + # @param [Lockfile] + def initialize(lockfile) + @lockfile = lockfile + @berksfile = lockfile.berksfile + end - # Parse the lockfile contents, adding the correct things to the lockfile. - # - # @return [true] - def run - @parsed_dependencies = {} + # Parse the lockfile contents, adding the correct things to the lockfile. + # + # @return [true] + def run + @parsed_dependencies = {} - contents = File.read(@lockfile.filepath) + contents = File.read(@lockfile.filepath) - if contents.strip.empty? - Berkshelf.formatter.warn "Your lockfile at '#{@lockfile.filepath}' " \ - "is empty. I am going to parse it anyway, but there is a chance " \ - "that a larger problem is at play. If you manually edited your " \ - "lockfile, you may have corrupted it." - end + if contents.strip.empty? + Berkshelf.formatter.warn "Your lockfile at '#{@lockfile.filepath}' " \ + "is empty. I am going to parse it anyway, but there is a chance " \ + "that a larger problem is at play. If you manually edited your " \ + "lockfile, you may have corrupted it." + end - if contents.strip[0] == '{' - Berkshelf.formatter.warn "It looks like you are using an older " \ - "version of the lockfile. Attempting to convert..." + if contents.strip[0] == "{" + Berkshelf.formatter.warn "It looks like you are using an older " \ + "version of the lockfile. Attempting to convert..." - dependencies = "#{Lockfile::DEPENDENCIES}\n" - graph = "#{Lockfile::GRAPH}\n" + dependencies = "#{Lockfile::DEPENDENCIES}\n" + graph = "#{Lockfile::GRAPH}\n" - begin - hash = JSON.parse(contents) - rescue JSON::ParserError - Berkshelf.formatter.warn "Could not convert lockfile! This is a " \ - "problem. You see, previous versions of the lockfile were " \ - "actually a lie. It lied to you about your version locks, and we " \ - "are really sorry about that.\n\n" \ - "Here's the good news - we fixed it!\n\n" \ - "Here's the bad news - you probably should not trust your old " \ - "lockfile. You should manually delete your old lockfile and " \ - "re-run the installer." - end + begin + hash = JSON.parse(contents) + rescue JSON::ParserError + Berkshelf.formatter.warn "Could not convert lockfile! This is a " \ + "problem. You see, previous versions of the lockfile were " \ + "actually a lie. It lied to you about your version locks, and we " \ + "are really sorry about that.\n\n" \ + "Here's the good news - we fixed it!\n\n" \ + "Here's the bad news - you probably should not trust your old " \ + "lockfile. You should manually delete your old lockfile and " \ + "re-run the installer." + end - hash['dependencies'] && hash['dependencies'].sort .each do |name, info| - dependencies << " #{name} (>= 0.0.0)\n" - info.each do |key, value| - unless key == 'locked_version' - dependencies << " #{key}: #{value}\n" - end + hash["dependencies"] && hash["dependencies"].sort .each do |name, info| + dependencies << " #{name} (>= 0.0.0)\n" + info.each do |key, value| + unless key == "locked_version" + dependencies << " #{key}: #{value}\n" end - - graph << " #{name} (#{info['locked_version']})\n" end - contents = "#{dependencies}\n#{graph}" + graph << " #{name} (#{info['locked_version']})\n" end - contents.split(/(?:\r?\n)+/).each do |line| - if line == Lockfile::DEPENDENCIES - @state = :dependency - elsif line == Lockfile::GRAPH - @state = :graph - else - send("parse_#{@state}", line) - end - end + contents = "#{dependencies}\n#{graph}" + end - @parsed_dependencies.each do |name, options| - graph_item = @lockfile.graph.find(name) - options[:locked_version] = graph_item.version if graph_item - - dependency = Dependency.new(@berksfile, name, options) - @lockfile.add(dependency) + contents.split(/(?:\r?\n)+/).each do |line| + if line == Lockfile::DEPENDENCIES + @state = :dependency + elsif line == Lockfile::GRAPH + @state = :graph + else + send("parse_#{@state}", line) end + end - true + @parsed_dependencies.each do |name, options| + graph_item = @lockfile.graph.find(name) + options[:locked_version] = graph_item.version if graph_item + + dependency = Dependency.new(@berksfile, name, options) + @lockfile.add(dependency) end - private + true + end - # Parse a dependency line. - # - # @param [String] line - def parse_dependency(line) - if line =~ DEPENDENCY_PATTERN - name, version = $1, $2 + private - @parsed_dependencies[name] ||= {} - @parsed_dependencies[name][:constraint] = version if version - @current_dependency = @parsed_dependencies[name] - elsif line =~ OPTION_PATTERN - key, value = $1, $2 - @current_dependency[key.to_sym] = value - end - end + # Parse a dependency line. + # + # @param [String] line + def parse_dependency(line) + if line =~ DEPENDENCY_PATTERN + name, version = $1, $2 - # Parse a graph line. - # - # @param [String] line - def parse_graph(line) - if line =~ DEPENDENCY_PATTERN - name, version = $1, $2 + @parsed_dependencies[name] ||= {} + @parsed_dependencies[name][:constraint] = version if version + @current_dependency = @parsed_dependencies[name] + elsif line =~ OPTION_PATTERN + key, value = $1, $2 + @current_dependency[key.to_sym] = value + end + end - @lockfile.graph.find(name) || @lockfile.graph.add(name, version) - @current_lock = name - elsif line =~ DEPENDENCIES_PATTERN - name, constraint = $1, $2 - @lockfile.graph.find(@current_lock).add_dependency(name, constraint) - end - end + # Parse a graph line. + # + # @param [String] line + def parse_graph(line) + if line =~ DEPENDENCY_PATTERN + name, version = $1, $2 + + @lockfile.graph.find(name) || @lockfile.graph.add(name, version) + @current_lock = name + elsif line =~ DEPENDENCIES_PATTERN + name, constraint = $1, $2 + @lockfile.graph.find(@current_lock).add_dependency(name, constraint) + end end + end # The class representing an internal graph. - class Graph - include Enumerable + class Graph + include Enumerable - # Create a new Lockfile graph. - # - # Some clarifying terminology: - # - # yum-epel (0.2.0) <- lock - # yum (~> 3.0) <- dependency - # - # @return [Graph] - def initialize(lockfile) - @lockfile = lockfile - @berksfile = lockfile.berksfile - @graph = {} - end + # Create a new Lockfile graph. + # + # Some clarifying terminology: + # + # yum-epel (0.2.0) <- lock + # yum (~> 3.0) <- dependency + # + # @return [Graph] + def initialize(lockfile) + @lockfile = lockfile + @berksfile = lockfile.berksfile + @graph = {} + end - # @yield [Hash<String] - def each(&block) - @graph.values.each(&block) - end + # @yield [Hash<String] + def each(&block) + @graph.values.each(&block) + end - # The list of locks for this graph. Dependencies are retrieved from the - # lockfile, then the Berksfile, and finally a new dependency object is - # created if none of those exist. - # - # @return [Hash<String, Dependency>] - # a key-value hash where the key is the name of the cookbook and the - # value is the locked dependency - def locks - @graph.sort.inject({}) do |hash, (name, item)| - dependency = @lockfile.find(name) || - @berksfile && @berksfile.find(name) || - Dependency.new(@berksfile, name) + # The list of locks for this graph. Dependencies are retrieved from the + # lockfile, then the Berksfile, and finally a new dependency object is + # created if none of those exist. + # + # @return [Hash<String, Dependency>] + # a key-value hash where the key is the name of the cookbook and the + # value is the locked dependency + def locks + @graph.sort.inject({}) do |hash, (name, item)| + dependency = @lockfile.find(name) || + @berksfile && @berksfile.find(name) || + Dependency.new(@berksfile, name) - # We need to make a copy of the dependency, or else we could be - # modifying an existing object that other processes depend on! - dependency = dependency.dup - dependency.locked_version = item.version unless dependency.locked_version + # We need to make a copy of the dependency, or else we could be + # modifying an existing object that other processes depend on! + dependency = dependency.dup + dependency.locked_version = item.version unless dependency.locked_version - hash[item.name] = dependency - hash - end + hash[item.name] = dependency + hash end + end - # Find a given dependency in the graph. - # - # @param [Dependency, String] - # the name/dependency to find - # - # @return [GraphItem, nil] - # the item for the name - def find(dependency) - @graph[Dependency.name(dependency)] - end + # Find a given dependency in the graph. + # + # @param [Dependency, String] + # the name/dependency to find + # + # @return [GraphItem, nil] + # the item for the name + def find(dependency) + @graph[Dependency.name(dependency)] + end - # Find if the given lock exists? - # - # @param [Dependency, String] - # the name/dependency to find - # - # @return [true, false] - def lock?(dependency) - !find(dependency).nil? - end - alias_method :has_lock?, :lock? + # Find if the given lock exists? + # + # @param [Dependency, String] + # the name/dependency to find + # + # @return [true, false] + def lock?(dependency) + !find(dependency).nil? + end + alias_method :has_lock?, :lock? - # Determine if this graph contains the given dependency. This method is - # used by the lockfile when adding or removing dependencies to see if a - # dependency can be safely removed. - # - # @param [Dependency, String] dependency - # the name/dependency to find - # - # @option options [String, Array<String>] :ignore - # the list of dependencies to ignore - def dependency?(dependency, options = {}) - name = Dependency.name(dependency) - ignore = Hash[*Array(options[:ignore]).map { |i| [i, true] }.flatten] + # Determine if this graph contains the given dependency. This method is + # used by the lockfile when adding or removing dependencies to see if a + # dependency can be safely removed. + # + # @param [Dependency, String] dependency + # the name/dependency to find + # + # @option options [String, Array<String>] :ignore + # the list of dependencies to ignore + def dependency?(dependency, options = {}) + name = Dependency.name(dependency) + ignore = Hash[*Array(options[:ignore]).map { |i| [i, true] }.flatten] - @graph.values.each do |item| - next if ignore[item.name] + @graph.values.each do |item| + next if ignore[item.name] - if item.dependencies.key?(name) - return true - end + if item.dependencies.key?(name) + return true end + end - false + false + end + alias_method :has_dependency?, :dependency? + + # Add each a new {GraphItem} to the graph. + # + # @param [#to_s] name + # the name of the cookbook + # @param [#to_s] version + # the version of the lock + # + # @return [GraphItem] + def add(name, version) + @graph[name.to_s] = GraphItem.new(name, version) + end + + # Recursively remove any dependencies from the graph unless they exist as + # top-level dependencies or nested dependencies. + # + # @param [Dependency, String] dependency + # the name/dependency to remove + # + # @option options [String, Array<String>] :ignore + # the list of dependencies to ignore + def remove(dependency, options = {}) + name = Dependency.name(dependency) + + if @lockfile.dependency?(name) + return end - alias_method :has_dependency?, :dependency? - # Add each a new {GraphItem} to the graph. - # - # @param [#to_s] name - # the name of the cookbook - # @param [#to_s] version - # the version of the lock - # - # @return [GraphItem] - def add(name, version) - @graph[name.to_s] = GraphItem.new(name, version) + if dependency?(name, options) + return end - # Recursively remove any dependencies from the graph unless they exist as - # top-level dependencies or nested dependencies. - # - # @param [Dependency, String] dependency - # the name/dependency to remove - # - # @option options [String, Array<String>] :ignore - # the list of dependencies to ignore - def remove(dependency, options = {}) - name = Dependency.name(dependency) + # Grab the nested dependencies for this particular entry so we can + # recurse and try to remove them from the graph. + locked = @graph[name] + nested_dependencies = locked && locked.dependencies.keys || [] - if @lockfile.dependency?(name) - return - end + # Now delete the entry + @graph.delete(name) - if dependency?(name, options) - return - end + # Recursively try to delete the remaining dependencies for this item + nested_dependencies.each(&method(:remove)) + end - # Grab the nested dependencies for this particular entry so we can - # recurse and try to remove them from the graph. - locked = @graph[name] - nested_dependencies = locked && locked.dependencies.keys || [] + # Update the graph with the given cookbooks. This method destroys the + # existing dependency graph with this new result! + # + # @param [Array<CachedCookbook>] + # the list of cookbooks to populate the graph with + def update(cookbooks) + @graph = {} - # Now delete the entry - @graph.delete(name) - - # Recursively try to delete the remaining dependencies for this item - nested_dependencies.each(&method(:remove)) + cookbooks.each do |cookbook| + @graph[cookbook.cookbook_name.to_s] = GraphItem.new( + cookbook.cookbook_name, + cookbook.version, + cookbook.dependencies + ) end + end - # Update the graph with the given cookbooks. This method destroys the - # existing dependency graph with this new result! - # - # @param [Array<CachedCookbook>] - # the list of cookbooks to populate the graph with - def update(cookbooks) - @graph = {} + # Write the contents of the graph to the lockfile format. + # + # The resulting format looks like: + # + # GRAPH + # apache2 (1.8.14) + # yum-epel (0.2.0) + # yum (~> 3.0) + # + # @example lockfile.graph.to_lock #=> "GRAPH\n apache2 (1.18.14)\n..." + # + # @return [String] + # + def to_lock + out = "#{Lockfile::GRAPH}\n" + @graph.sort.each do |name, item| + out << " #{name} (#{item.version})\n" - cookbooks.each do |cookbook| - @graph[cookbook.cookbook_name.to_s] = GraphItem.new( - cookbook.cookbook_name, - cookbook.version, - cookbook.dependencies, - ) + unless item.dependencies.empty? + item.dependencies.sort.each do |name, constraint| + out << " #{name} (#{constraint})\n" + end end end - # Write the contents of the graph to the lockfile format. + out + end + + # A single item inside the graph. + class GraphItem + # The name of the cookbook that corresponds to this graph item. # - # The resulting format looks like: + # @return [String] + # the name of the cookbook + attr_reader :name + + # The locked version for this graph item. # - # GRAPH - # apache2 (1.8.14) - # yum-epel (0.2.0) - # yum (~> 3.0) - # - # @example lockfile.graph.to_lock #=> "GRAPH\n apache2 (1.18.14)\n..." - # # @return [String] + # the locked version of the graph item (as a string) + attr_reader :version + + # The list of dependencies and their constraints. # - def to_lock - out = "#{Lockfile::GRAPH}\n" - @graph.sort.each do |name, item| - out << " #{name} (#{item.version})\n" + # @return [Hash<String, String>] + # the list of dependencies for this graph item, where the key + # corresponds to the name of the dependency and the value is the + # version constraint. + attr_reader :dependencies - unless item.dependencies.empty? - item.dependencies.sort.each do |name, constraint| - out << " #{name} (#{constraint})\n" - end - end - end + # Create a new graph item. + def initialize(name, version, dependencies = {}) + @name = name.to_s + @version = version.to_s + @dependencies = dependencies + end - out + # Add a new dependency to the list. + # + # @param [#to_s] name + # the name to use + # @param [#to_s] constraint + # the version constraint to use + def add_dependency(name, constraint) + @dependencies[name.to_s] = constraint.to_s end - private + def set_dependencies(dependencies) + @dependencies = dependencies.to_hash + end - # A single item inside the graph. - class GraphItem - # The name of the cookbook that corresponds to this graph item. - # - # @return [String] - # the name of the cookbook - attr_reader :name - - # The locked version for this graph item. - # - # @return [String] - # the locked version of the graph item (as a string) - attr_reader :version - - # The list of dependencies and their constraints. - # - # @return [Hash<String, String>] - # the list of dependencies for this graph item, where the key - # corresponds to the name of the dependency and the value is the - # version constraint. - attr_reader :dependencies - - # Create a new graph item. - def initialize(name, version, dependencies = {}) - @name = name.to_s - @version = version.to_s - @dependencies = dependencies - end - - # Add a new dependency to the list. - # - # @param [#to_s] name - # the name to use - # @param [#to_s] constraint - # the version constraint to use - def add_dependency(name, constraint) - @dependencies[name.to_s] = constraint.to_s - end - - def set_dependencies(dependencies) - @dependencies = dependencies.to_hash - end - - # @private - def to_s - "#{name} (#{version})" - end - end + # @private + def to_s + "#{name} (#{version})" + end end + end end end