puppetfile-resolver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +169 -0
  4. data/lib/puppetfile-resolver.rb +7 -0
  5. data/lib/puppetfile-resolver/cache/base.rb +28 -0
  6. data/lib/puppetfile-resolver/cache/persistent.rb +50 -0
  7. data/lib/puppetfile-resolver/data/ruby_ca_certs.pem +3432 -0
  8. data/lib/puppetfile-resolver/models.rb +8 -0
  9. data/lib/puppetfile-resolver/models/missing_module_specification.rb +27 -0
  10. data/lib/puppetfile-resolver/models/module_dependency.rb +55 -0
  11. data/lib/puppetfile-resolver/models/module_specification.rb +114 -0
  12. data/lib/puppetfile-resolver/models/puppet_dependency.rb +34 -0
  13. data/lib/puppetfile-resolver/models/puppet_specification.rb +25 -0
  14. data/lib/puppetfile-resolver/models/puppetfile_dependency.rb +14 -0
  15. data/lib/puppetfile-resolver/puppetfile.rb +22 -0
  16. data/lib/puppetfile-resolver/puppetfile/base_module.rb +62 -0
  17. data/lib/puppetfile-resolver/puppetfile/document.rb +125 -0
  18. data/lib/puppetfile-resolver/puppetfile/forge_module.rb +14 -0
  19. data/lib/puppetfile-resolver/puppetfile/git_module.rb +19 -0
  20. data/lib/puppetfile-resolver/puppetfile/invalid_module.rb +16 -0
  21. data/lib/puppetfile-resolver/puppetfile/local_module.rb +14 -0
  22. data/lib/puppetfile-resolver/puppetfile/parser/errors.rb +19 -0
  23. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval.rb +133 -0
  24. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/dsl.rb +51 -0
  25. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/forge.rb +50 -0
  26. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/git.rb +32 -0
  27. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/invalid.rb +27 -0
  28. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/local.rb +26 -0
  29. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/svn.rb +30 -0
  30. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/puppet_module.rb +36 -0
  31. data/lib/puppetfile-resolver/puppetfile/svn_module.rb +16 -0
  32. data/lib/puppetfile-resolver/puppetfile/validation_errors.rb +106 -0
  33. data/lib/puppetfile-resolver/resolution_provider.rb +182 -0
  34. data/lib/puppetfile-resolver/resolution_result.rb +30 -0
  35. data/lib/puppetfile-resolver/resolver.rb +77 -0
  36. data/lib/puppetfile-resolver/spec_searchers/common.rb +15 -0
  37. data/lib/puppetfile-resolver/spec_searchers/forge.rb +75 -0
  38. data/lib/puppetfile-resolver/spec_searchers/git.rb +64 -0
  39. data/lib/puppetfile-resolver/spec_searchers/local.rb +45 -0
  40. data/lib/puppetfile-resolver/ui/debug_ui.rb +15 -0
  41. data/lib/puppetfile-resolver/ui/null_ui.rb +20 -0
  42. data/lib/puppetfile-resolver/util.rb +23 -0
  43. data/lib/puppetfile-resolver/version.rb +5 -0
  44. data/puppetfile-cli.rb +101 -0
  45. data/spec/spec_helper.rb +48 -0
  46. data/spec/unit/puppetfile-resolver/puppetfile/document_spec.rb +316 -0
  47. data/spec/unit/puppetfile-resolver/puppetfile/parser/r10k_eval_spec.rb +460 -0
  48. data/spec/unit/puppetfile-resolver/resolver_spec.rb +421 -0
  49. metadata +124 -0
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval/module/invalid'
4
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval/module/forge'
5
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval/module/git'
6
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval/module/local'
7
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval/module/svn'
8
+
9
+ module PuppetfileResolver
10
+ module Puppetfile
11
+ module Parser
12
+ module R10KEval
13
+ module PuppetModule
14
+ def self.from_puppetfile(title, args)
15
+ return Module::Git.to_document_module(title, args) if Module::Git.implements?(title, args)
16
+ return Module::Svn.to_document_module(title, args) if Module::Svn.implements?(title, args)
17
+ return Module::Local.to_document_module(title, args) if Module::Local.implements?(title, args)
18
+ return Module::Forge.to_document_module(title, args) if Module::Forge.implements?(title, args)
19
+
20
+ Module::Invalid.to_document_module(title, args)
21
+ end
22
+
23
+ def self.parse_title(title)
24
+ if (match = title.match(/\A(\w+)\Z/))
25
+ [nil, match[1]]
26
+ elsif (match = title.match(/\A(\w+)[-\/](\w+)\Z/))
27
+ [match[1], match[2]]
28
+ else
29
+ raise ArgumentError, format("Module name (%<title>s) must match either 'modulename' or 'owner/modulename'", title: title)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/puppetfile/base_module'
4
+
5
+ module PuppetfileResolver
6
+ module Puppetfile
7
+ class SvnModule < BaseModule
8
+ attr_accessor :remote
9
+
10
+ def initialize(title)
11
+ super
12
+ @module_type = SVN_MODULE
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Puppetfile
5
+ class DocumentValidationErrorBase
6
+ attr_accessor :message
7
+ attr_accessor :puppet_module
8
+
9
+ def initialize(message, puppet_module)
10
+ @message = message
11
+ @puppet_module = puppet_module
12
+ end
13
+
14
+ def to_s
15
+ "#{puppet_module.name}: #{message}"
16
+ end
17
+ end
18
+
19
+ # Validation Error classes for parsing Puppetfiles
20
+ class DocumentInvalidModuleError < DocumentValidationErrorBase
21
+ end
22
+
23
+ class DocumentDuplicateModuleError < DocumentValidationErrorBase
24
+ attr_accessor :duplicates
25
+
26
+ def initialize(message, puppet_module, duplicates)
27
+ super(message, puppet_module)
28
+ @duplicates = duplicates
29
+ end
30
+ end
31
+
32
+ # Terminal Errors during resolution
33
+ class DocumentResolveError < StandardError
34
+ attr_reader :molinillo_error
35
+
36
+ def initialize(message, molinillo_error)
37
+ @molinillo_error = molinillo_error
38
+ super(message)
39
+ end
40
+ end
41
+
42
+ class DocumentCircularDependencyError < DocumentResolveError
43
+ def initialize(puppetfile_document, molinillo_error)
44
+ @puppetfile_document = puppetfile_document
45
+ super(molinillo_error.message, molinillo_error)
46
+ end
47
+
48
+ def puppetfile_modules
49
+ module_names = @molinillo_error.dependencies.map(&:name)
50
+ @puppetfile_document.modules.select { |mod| module_names.include?(mod.name) }
51
+ end
52
+ end
53
+
54
+ class DocumentVersionConflictError < DocumentResolveError
55
+ def initialize(molinillo_error)
56
+ super(molinillo_error.message_with_trees(solver_name: 'Puppetfile Resolver'), molinillo_error)
57
+ end
58
+
59
+ def puppetfile_modules
60
+ puppetfile_modules = []
61
+ molinillo_error.conflicts.reduce(''.dup) do |_o, (_name, conflict)|
62
+ # We don't actually care about the dependency tree,
63
+ # only the leaves within. So just grab all of leaves and
64
+ # find all of the modules in the Puppetfile document
65
+ conflict
66
+ .requirement_trees
67
+ .flatten
68
+ .uniq
69
+ .select { |req| req.is_a?(PuppetfileResolver::Models::PuppetfileDependency) }
70
+ .each do |req|
71
+ puppetfile_modules << req.puppetfile_module unless puppetfile_modules.include?(req.puppetfile_module)
72
+ end
73
+ end
74
+
75
+ puppetfile_modules
76
+ end
77
+ end
78
+
79
+ # Resolution Validation Error classes for validating
80
+ # a valid Puppetfile against a dependency resolution
81
+ class DocumentResolutionErrorBase < DocumentValidationErrorBase
82
+ attr_accessor :puppet_module
83
+ attr_accessor :module_specification
84
+
85
+ def initialize(message, puppet_module, module_specification)
86
+ super(message, puppet_module)
87
+ @module_specification = module_specification
88
+ end
89
+ end
90
+
91
+ class DocumentLatestVersionError < DocumentResolutionErrorBase
92
+ end
93
+
94
+ class DocumentMissingModuleError < DocumentResolutionErrorBase
95
+ end
96
+
97
+ class DocumentMissingDependenciesError < DocumentResolutionErrorBase
98
+ attr_accessor :missing_specifications
99
+
100
+ def initialize(message, puppet_module, module_specification, missing_specifications)
101
+ super(message, puppet_module, module_specification)
102
+ @missing_specifications = missing_specifications
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'molinillo'
4
+ require 'puppetfile-resolver/cache/base'
5
+ require 'puppetfile-resolver/spec_searchers/forge'
6
+ require 'puppetfile-resolver/spec_searchers/git'
7
+ require 'puppetfile-resolver/spec_searchers/local'
8
+
9
+ module PuppetfileResolver
10
+ class ResolutionProvider
11
+ include Molinillo::SpecificationProvider
12
+
13
+ # options
14
+ # module_paths : Array of paths
15
+ # strict_mode : [Boolean] Whether missing dependencies throw an error (default: false)
16
+ def initialize(puppetfile_document, puppet_version, resolver_ui, options = {})
17
+ require 'semantic_puppet'
18
+
19
+ @puppetfile_document = puppetfile_document
20
+ raise 'The UI object must be of type Molinillo::UI' if resolver_ui.nil? || !resolver_ui.is_a?(Molinillo::UI)
21
+ @resolver_ui = resolver_ui
22
+ # TODO: This default crap should move to the resolve class and we just validate (and raise) here
23
+ @puppet_module_paths = options[:module_paths].nil? ? [] : options[:module_paths]
24
+ @allow_missing_modules = options[:allow_missing_modules].nil? ? true : options[:allow_missing_modules] == true
25
+ # There can be only one puppet specification in existance so we pre-load here.
26
+ @puppet_specification = Models::PuppetSpecification.new(puppet_version)
27
+ @module_info = {}
28
+ @cache = options[:cache].nil? ? Cache::Base.new : options[:cache]
29
+ end
30
+
31
+ # Search for the specifications that match the given dependency.
32
+ # The specifications in the returned array will be considered in reverse
33
+ # order, so the latest version ought to be last.
34
+ # @note This method should be 'pure', i.e. the return value should depend
35
+ # only on the `dependency` parameter.
36
+ #
37
+ # @param [Object] dependency
38
+ # @return [Array<Object>] the specifications that satisfy the given
39
+ # `dependency`.
40
+ def search_for(dependency)
41
+ case dependency
42
+ when Models::PuppetDependency
43
+ result = find_puppet_specifications(dependency)
44
+ when Models::ModuleDependency
45
+ result = find_all_module_specifications(dependency).select do |spec|
46
+ dependency.satisified_by?(spec)
47
+ end
48
+ else
49
+ # No idea how we got here?!?!
50
+ raise ArgumentError, "Unknown Dependency type #{dependency.class}"
51
+ end
52
+
53
+ return result if result.empty? || result.count == 1
54
+ # Reverse sort by version
55
+ result.sort! { |a, b| a.version > b.version ? 1 : -1 }
56
+ end
57
+
58
+ def find_puppet_specifications(dependency)
59
+ # Puppet specifications are a bit special as there can be only one (Highlander style)
60
+ dependency.satisified_by?(@puppet_specification) ? [@puppet_specification] : []
61
+ end
62
+
63
+ # Returns the dependencies of `specification`.
64
+ # @note This method should be 'pure', i.e. the return value should depend
65
+ # only on the `specification` parameter.
66
+ #
67
+ # @param [Object] specification
68
+ # @return [Array<Object>] the dependencies that are required by the given
69
+ # `specification`.
70
+ def dependencies_for(specification)
71
+ specification.dependencies(@cache, @resolver_ui)
72
+ end
73
+
74
+ # Returns the name for the given `dependency`.
75
+ # @note This method should be 'pure', i.e. the return value should depend
76
+ # only on the `dependency` parameter.
77
+ #
78
+ # @param [Object] dependency
79
+ # @return [String] the name for the given `dependency`.
80
+ def name_for(dependency)
81
+ dependency.name
82
+ end
83
+
84
+ # Determines whether the given `requirement` is satisfied by the given
85
+ # `spec`, in the context of the current `activated` dependency graph.
86
+ #
87
+ # @param [Object] requirement
88
+ # @param [DependencyGraph] activated the current dependency graph in the
89
+ # resolution process.
90
+ # @param [Object] spec
91
+ # @return [Boolean] whether `requirement` is satisfied by `spec` in the
92
+ # context of the current `activated` dependency graph.
93
+ def requirement_satisfied_by?(requirement, _activated, spec)
94
+ requirement.satisified_by?(spec)
95
+ end
96
+
97
+ def name_for_explicit_dependency_source
98
+ 'Puppetfile'
99
+ end
100
+
101
+ def name_for_locking_dependency_source
102
+ 'Puppetfile'
103
+ end
104
+
105
+ def sort_dependencies(dependencies, activated, conflicts) # rubocop:disable Lint/UnusedMethodArgument You're drunk rubocop
106
+ dependencies.sort_by do |dependency|
107
+ name = name_for(dependency)
108
+ [
109
+ activated.vertex_named(name).payload ? 0 : 1,
110
+ conflicts[name] ? 0 : 1
111
+ ]
112
+ end
113
+ end
114
+
115
+ # Returns whether this dependency, which has no possible matching
116
+ # specifications, can safely be ignored.
117
+ #
118
+ # @param [Object] dependency
119
+ # @return [Boolean] whether this dependency can safely be skipped.
120
+ def allow_missing?(dependency)
121
+ # Puppet dependencies must _always_ be resolvable
122
+ return false if dependency.is_a?(Models::PuppetDependency)
123
+ # Explicit Puppetfile dependencies must _always_ be resolvable
124
+ return false if dependency.is_a?(Models::PuppetfileDependency)
125
+ @allow_missing_modules
126
+ end
127
+
128
+ private
129
+
130
+ def find_all_module_specifications(dependency)
131
+ return @module_info[dependency.name] unless @module_info[dependency.name].nil?
132
+
133
+ @module_info[dependency.name] = []
134
+
135
+ # Find the module as specified in the Puppetfile?
136
+ mod = @puppetfile_document.modules.find { |item| item.name == dependency.name }
137
+ unless mod.nil?
138
+ case mod.module_type
139
+ when Puppetfile::FORGE_MODULE
140
+ @module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Forge.find_all(dependency, @cache, @resolver_ui) }
141
+ when Puppetfile::GIT_MODULE
142
+ @module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Git.find_all(mod, dependency, @cache, @resolver_ui) }
143
+ else # rubocop:disable Style/EmptyElse
144
+ # Errr.... Nothing
145
+ end
146
+ end
147
+ return @module_info[dependency.name] unless @module_info[dependency.name].empty?
148
+
149
+ # It's not in the Puppetfile, so perhaps it's in our modulepath?
150
+ @module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Local.find_all(mod, @puppet_module_paths, dependency, @cache, @resolver_ui) }
151
+ return @module_info[dependency.name] unless @module_info[dependency.name].empty?
152
+
153
+ # It's not in the Puppetfile and not on disk, so perhaps it's on the Forge?
154
+ # The forge needs an owner and name to be able to resolve
155
+ if dependency.name && dependency.owner # rubocop:disable Style/IfUnlessModifier
156
+ @module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Forge.find_all(dependency, @cache, @resolver_ui) }
157
+ end
158
+
159
+ # If we can't find any specifications for the module and we're allowing missing modules
160
+ # then create a MissingModuleSpecification for the purposes of the dependency graph
161
+ if @allow_missing_modules && @module_info[dependency.name].empty? # rubocop:disable Style/IfUnlessModifier
162
+ @module_info[dependency.name] << Models::MissingModuleSpecification.new(name: dependency.name)
163
+ end
164
+ @module_info[dependency.name]
165
+ end
166
+
167
+ def safe_spec_search(dependency)
168
+ results = yield
169
+ # The PuppetfileDependency has the resolver flags, so we need to inject them into the specifications
170
+ return results unless dependency.is_a?(PuppetfileResolver::Models::PuppetfileDependency) || results.empty?
171
+ results.each { |spec| spec.resolver_flags = dependency.puppetfile_module.resolver_flags }
172
+
173
+ results
174
+ rescue StandardError => e
175
+ if @allow_missing_modules
176
+ @resolver_ui.debug { "Error while querying a specification searcher #{e.inspect}" }
177
+ return []
178
+ end
179
+ raise
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'molinillo'
4
+
5
+ module PuppetfileResolver
6
+ class ResolutionResult
7
+ attr_reader :dependency_graph
8
+
9
+ def initialize(dependency_graph, puppetfile_document)
10
+ raise "Expected Molinillo::DependencyGraph but got #{dependency_graph.class}" unless dependency_graph.is_a?(Molinillo::DependencyGraph)
11
+ @dependency_graph = dependency_graph
12
+ @puppetfile_document = puppetfile_document
13
+ end
14
+
15
+ def specifications
16
+ # Note - Later rubies have `.transform_values` however we support old Ruby versions
17
+ result = {}
18
+ @dependency_graph.vertices.each { |key, vertex| result[key] = vertex.payload }
19
+ result
20
+ end
21
+
22
+ def to_dot
23
+ @dependency_graph.to_dot
24
+ end
25
+
26
+ def validation_errors
27
+ @puppetfile_document.resolution_validation_errors(self)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'molinillo'
4
+ require 'puppetfile-resolver/resolution_provider'
5
+ require 'puppetfile-resolver/resolution_result'
6
+ require 'puppetfile-resolver/models'
7
+
8
+ module PuppetfileResolver
9
+ class Resolver
10
+ attr_reader :puppetfile
11
+ attr_reader :dependencies_to_resolve
12
+
13
+ def initialize(puppetfile_document, puppet_version = nil)
14
+ @puppetfile = puppetfile_document
15
+ raise 'Puppetfile is not valid' unless @puppetfile.valid?
16
+ @puppet_version = puppet_version
17
+
18
+ @dependencies_to_resolve = dependencies_from_puppetfile
19
+ end
20
+
21
+ # options
22
+ # :cache => Cache Object
23
+ # :module_paths => Array[String]
24
+ def resolve(options = {})
25
+ if options[:ui]
26
+ raise 'The UI object must be of type Molinillo::UI' unless options[:ui].is_a?(Molinillo::UI)
27
+ ui = options[:ui]
28
+ else
29
+ require 'puppetfile-resolver/ui/null_ui'
30
+ ui = PuppetfileResolver::UI::NullUI.new
31
+ end
32
+ provider = ResolutionProvider.new(@puppetfile, @puppet_version, ui, options)
33
+
34
+ resolver = Molinillo::Resolver.new(provider, ui)
35
+ begin
36
+ result = resolver.resolve(dependencies_to_resolve)
37
+ rescue Molinillo::VersionConflict => e
38
+ # Wrap the Molinillo error
39
+ new_e = PuppetfileResolver::Puppetfile::DocumentVersionConflictError.new(e)
40
+ raise new_e, new_e.message, e.backtrace
41
+ rescue Molinillo::CircularDependencyError => e
42
+ # Wrap the Molinillo error
43
+ new_e = PuppetfileResolver::Puppetfile::DocumentCircularDependencyError.new(@puppetfile, e)
44
+ raise new_e, new_e.message, e.backtrace
45
+ end
46
+ ResolutionResult.new(result, @puppetfile)
47
+ end
48
+
49
+ private
50
+
51
+ def dependencies_from_puppetfile
52
+ result = []
53
+ @puppetfile.modules.each do |mod|
54
+ # Use an open version unless we get a valid version number
55
+ if mod.version.nil? || mod.version == :latest
56
+ version = '>= 0' # Note the `>=` is important. Don't use `>`
57
+ else
58
+ version = "=#{mod.version}"
59
+ end
60
+
61
+ result << Models::PuppetfileDependency.new(
62
+ name: mod.title,
63
+ version_requirement: version,
64
+ puppetfile_module: mod
65
+ )
66
+ end
67
+ # We also depend on Puppet, so add an open ended requirement if no version
68
+ # was specified or add a strict version requirement
69
+ if @puppet_version.nil?
70
+ result << Models::PuppetDependency.new('>= 0')
71
+ else
72
+ result << Models::PuppetDependency.new(@puppet_version.to_s)
73
+ end
74
+ result
75
+ end
76
+ end
77
+ end