puppetfile-resolver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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