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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/models/module_specification'
4
+ require 'puppetfile-resolver/models/missing_module_specification'
5
+ require 'puppetfile-resolver/models/module_dependency'
6
+ require 'puppetfile-resolver/models/puppet_specification'
7
+ require 'puppetfile-resolver/models/puppet_dependency'
8
+ require 'puppetfile-resolver/models/puppetfile_dependency'
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/models/module_specification'
4
+
5
+ module PuppetfileResolver
6
+ module Models
7
+ class MissingModuleSpecification < ModuleSpecification
8
+ def initialize(options = {})
9
+ super
10
+ @origin = :missing
11
+ end
12
+
13
+ def to_s
14
+ "Missing #{name}"
15
+ end
16
+
17
+ def metadata(*_)
18
+ nil
19
+ end
20
+
21
+ def dependencies(*_)
22
+ # Modules that are missing can not depend on anything, even Puppet
23
+ []
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Models
5
+ class ModuleDependency
6
+ attr_accessor :name
7
+ attr_accessor :owner
8
+ attr_accessor :version_requirement
9
+
10
+ def initialize(options = {})
11
+ # Munge the name
12
+ # "puppetlabs/stdlib"
13
+ # "puppetlabs-stdlib"
14
+ # "puppetlabs-stdlib-1.0.0 ??"
15
+ # "stdlib"
16
+ @name = options[:name]
17
+ result = @name.split('/', 2)
18
+ if result.count > 1
19
+ @owner = result[0]
20
+ @name = result[1]
21
+ else
22
+ result = @name.split('-')
23
+ if result.count > 1
24
+ @owner = result[0]
25
+ @name = result[1]
26
+ else
27
+ @owner = options[:owner]
28
+ end
29
+ end
30
+
31
+ @version_requirement = options[:version_requirement]
32
+ end
33
+
34
+ def to_s
35
+ "#{owner}-#{name} #{version_requirement}"
36
+ end
37
+
38
+ def satisified_by?(spec)
39
+ # Missing modules are special. They should always satisfy any version range because
40
+ # we don't know what version missing modules are!
41
+ return true if spec.is_a?(MissingModuleSpecification)
42
+ raise "Specification #{spec} does not have a version" if spec.version.nil?
43
+ semantic_requirement.include?(spec.version)
44
+ end
45
+
46
+ private
47
+
48
+ def semantic_requirement
49
+ require 'semantic_puppet'
50
+
51
+ @semantic_requirement ||= ::SemanticPuppet::VersionRange.parse(@version_requirement)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/models/module_dependency'
4
+ require 'puppetfile-resolver/models/puppet_dependency'
5
+ require 'puppetfile-resolver/puppetfile'
6
+
7
+ module PuppetfileResolver
8
+ module Models
9
+ class ModuleSpecification
10
+ attr_accessor :name
11
+ attr_accessor :owner
12
+ attr_accessor :version
13
+ attr_accessor :origin # Same as R10K module :type
14
+ attr_accessor :resolver_flags
15
+
16
+ def initialize(options = {})
17
+ require 'semantic_puppet'
18
+
19
+ @name = options[:name]
20
+ @owner = options[:owner]
21
+ # Munge the name
22
+ # "puppetlabs/stdlib"
23
+ # "puppetlabs-stdlib"
24
+ # "puppetlabs-stdlib-1.0.0 ??"
25
+ # "stdlib"
26
+ unless @name.nil?
27
+ result = @name.split('/', 2)
28
+ if result.count > 1
29
+ @owner = result[0]
30
+ @name = result[1]
31
+ else
32
+ result = @name.split('-')
33
+ if result.count > 1
34
+ @owner = result[0]
35
+ @name = result[1]
36
+ end
37
+ end
38
+ end
39
+ @origin = options[:origin]
40
+ @dependencies = nil
41
+ @metadata = options[:metadata]
42
+ @resolver_flags = options[:resolver_flags].nil? ? [] : options[:resolver_flags]
43
+ @version = ::SemanticPuppet::Version.parse(options[:version]) unless options[:version].nil?
44
+ end
45
+
46
+ def to_s
47
+ prefix = case @origin
48
+ when :forge
49
+ 'Forge'
50
+ when :git
51
+ 'Git'
52
+ when :local
53
+ 'Local'
54
+ else
55
+ 'Unknown'
56
+ end
57
+ "#{prefix} #{owner}-#{name}-#{version}"
58
+ end
59
+
60
+ def to_json(*args)
61
+ {
62
+ 'name' => name,
63
+ 'owner' => owner,
64
+ 'origin' => origin,
65
+ 'version' => version.to_s
66
+ }.to_json(args)
67
+ end
68
+
69
+ def from_hash!(hash)
70
+ @name = hash['name']
71
+ @owner = hash['owner']
72
+ @origin = hash['origin']
73
+ @version = ::SemanticPuppet::Version.parse(hash['version']) unless hash['version'].nil?
74
+ self
75
+ end
76
+
77
+ def metadata(_cache, _resolver_ui)
78
+ # TODO: Later on we could resolve the metadata lazily, but for now, no need
79
+ @metadata
80
+ end
81
+
82
+ def dependencies(cache, resolver_ui)
83
+ return @dependencies unless @dependencies.nil?
84
+
85
+ return (@dependencies = []) if resolver_flags.include?(PuppetfileResolver::Puppetfile::DISABLE_ALL_DEPENDENCIES_FLAG)
86
+
87
+ meta = metadata(cache, resolver_ui)
88
+ @dependencies = []
89
+ unless meta[:dependencies].nil? || meta[:dependencies].empty?
90
+ @dependencies = meta[:dependencies].map do |dep|
91
+ ModuleDependency.new(
92
+ name: dep[:name],
93
+ version_requirement: dep[:version_requirement]
94
+ )
95
+ end
96
+ end
97
+
98
+ unless resolver_flags.include?(PuppetfileResolver::Puppetfile::DISABLE_PUPPET_DEPENDENCY_FLAG)
99
+ puppet_requirement = nil
100
+ unless meta[:requirements].nil? || meta[:requirements].empty? # rubocop:disable Style/IfUnlessModifier
101
+ puppet_requirement = meta[:requirements].find { |req| req[:name] == 'puppet' && !req[:version_requirement].nil? }
102
+ end
103
+ if puppet_requirement.nil?
104
+ @dependencies << PuppetDependency.new('>= 0')
105
+ else
106
+ @dependencies << PuppetDependency.new(puppet_requirement[:version_requirement])
107
+ end
108
+ end
109
+
110
+ @dependencies
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Models
5
+ class PuppetDependency
6
+ attr_reader :name
7
+ attr_accessor :version_requirement
8
+
9
+ def initialize(version_requirement)
10
+ @name = 'Puppet' # This name is special as modules cannot start with an uppercase letter
11
+
12
+ @version_requirement = version_requirement
13
+ end
14
+
15
+ def to_s
16
+ "#{name} #{version_requirement}"
17
+ end
18
+
19
+ def satisified_by?(spec)
20
+ # A Puppet spec with a nil version will always be satisified by a Puppet Dependency
21
+ return true if spec.version.nil?
22
+ semantic_requirement.include?(spec.version)
23
+ end
24
+
25
+ private
26
+
27
+ def semantic_requirement
28
+ require 'semantic_puppet'
29
+
30
+ @semantic_requirement ||= ::SemanticPuppet::VersionRange.parse(@version_requirement)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Models
5
+ class PuppetSpecification
6
+ attr_reader :name
7
+ attr_accessor :version
8
+
9
+ def initialize(version)
10
+ require 'semantic_puppet'
11
+
12
+ @name = 'Puppet'
13
+ @version = version.nil? ? nil : ::SemanticPuppet::Version.parse(version)
14
+ end
15
+
16
+ def to_s
17
+ @version.nil? ? name.to_s : "#{name}-#{version}"
18
+ end
19
+
20
+ def dependencies(*_)
21
+ []
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Models
5
+ class PuppetfileDependency < ModuleDependency
6
+ attr_reader :puppetfile_module
7
+
8
+ def initialize(options = {})
9
+ super(options)
10
+ @puppetfile_module = options[:puppetfile_module]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Puppetfile
5
+ # Resolver Flags
6
+ #
7
+ # DISABLE_PUPPET_DEPENDENCY_FLAG - Instructs the resolver to not consider Puppet version in its dependency traversal. Useful for modules with outdated metadata.json information.
8
+ # DISABLE_ALL_DEPENDENCIES_FLAG - Instructs the resolver to ignore any dependencies in its dependency traversal. Useful for modules with outdated metadata.json information.
9
+ #
10
+ DISABLE_PUPPET_DEPENDENCY_FLAG = :disable_puppet_dependency
11
+ DISABLE_ALL_DEPENDENCIES_FLAG = :disable_all_dependencies
12
+ end
13
+ end
14
+
15
+ require 'puppetfile-resolver/puppetfile/document'
16
+ require 'puppetfile-resolver/puppetfile/validation_errors'
17
+ require 'puppetfile-resolver/puppetfile/base_module'
18
+ require 'puppetfile-resolver/puppetfile/forge_module'
19
+ require 'puppetfile-resolver/puppetfile/git_module'
20
+ require 'puppetfile-resolver/puppetfile/invalid_module'
21
+ require 'puppetfile-resolver/puppetfile/local_module'
22
+ require 'puppetfile-resolver/puppetfile/svn_module'
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Puppetfile
5
+ FORGE_MODULE = :forge
6
+ GIT_MODULE = :git
7
+ INVALID_MODULE = :invalid
8
+ LOCAL_MODULE = :local
9
+ SVN_MODULE = :svn
10
+
11
+ class BaseModule
12
+ # The full title of the module
13
+ attr_accessor :title
14
+
15
+ # The owner of the module
16
+ attr_accessor :owner
17
+
18
+ # The name of the module
19
+ attr_accessor :name
20
+
21
+ # The version of the module
22
+ attr_accessor :version
23
+
24
+ # The location of the module instantiation in the Puppetfile document
25
+ # [DocumentLocation]
26
+ attr_accessor :location
27
+
28
+ attr_reader :module_type
29
+
30
+ # Array of flags that will instruct the resolver to change its default behaviour. Current flags are
31
+ # set out in the PuppetfileResolver::Puppetfile::..._FLAG constants
32
+ # @api private
33
+ # @return [Array[Symbol]] Array of flags that will instruct the resolver to change its default behaviour
34
+ attr_accessor :resolver_flags
35
+
36
+ def initialize(title)
37
+ @title = title
38
+ unless title.nil? # rubocop:disable Style/IfUnlessModifier
39
+ @owner, @name = parse_title(@title)
40
+ end
41
+ @location = DocumentLocation.new
42
+ @resolver_flags = []
43
+ end
44
+
45
+ def to_s
46
+ "#{self.class} #{title}-#{name}"
47
+ end
48
+
49
+ private
50
+
51
+ def parse_title(title)
52
+ if (match = title.match(/\A(\w+)\Z/))
53
+ [nil, match[1]]
54
+ elsif (match = title.match(/\A(\w+)[-\/](\w+)\Z/))
55
+ [match[1], match[2]]
56
+ else
57
+ raise ArgumentError, format("Module name (%<title>s) must match either 'modulename' or 'owner/modulename'", title: title)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/puppetfile/invalid_module'
4
+
5
+ module PuppetfileResolver
6
+ module Puppetfile
7
+ class DocumentLocation
8
+ attr_accessor :start_line # Base 0
9
+ attr_accessor :start_char # Base 0
10
+ attr_accessor :end_line # Base 0
11
+ attr_accessor :end_char # Base 0
12
+ end
13
+
14
+ class Document
15
+ attr_accessor :forge_uri
16
+ attr_reader :modules
17
+ attr_accessor :content
18
+
19
+ def initialize(puppetfile_content)
20
+ @content = puppetfile_content
21
+ @modules = []
22
+ @validation_errors = nil
23
+ end
24
+
25
+ def to_s
26
+ "PuppetfileResolver::Puppetfile::Document\n#{@content}"
27
+ end
28
+
29
+ def clear_modules
30
+ @modules = []
31
+ end
32
+
33
+ def add_module(puppet_module)
34
+ @modules << puppet_module
35
+ end
36
+
37
+ def valid?
38
+ validation_errors.empty?
39
+ end
40
+
41
+ def validation_errors
42
+ return @validation_errors unless @validation_errors.nil?
43
+
44
+ @validation_errors = []
45
+
46
+ # Check for invalid modules
47
+ modules.each do |mod|
48
+ next unless mod.is_a?(PuppetfileResolver::Puppetfile::InvalidModule)
49
+ @validation_errors << DocumentInvalidModuleError.new(mod.reason, mod)
50
+ end
51
+
52
+ # Check for duplicate module definitions
53
+ dupes = modules
54
+ .group_by { |mod| mod.name }
55
+ .select { |_, v| v.size > 1 }
56
+ .map(&:first)
57
+ dupes.each do |dupe_module_name|
58
+ duplicates = modules.select { |mod| mod.name == dupe_module_name }
59
+ @validation_errors << DocumentDuplicateModuleError.new(
60
+ "Duplicate module definition for '#{dupe_module_name}'",
61
+ duplicates[0],
62
+ duplicates.slice(1..-1)
63
+ )
64
+ end
65
+
66
+ @validation_errors
67
+ end
68
+
69
+ def resolution_validation_errors(resolution_result)
70
+ raise 'Validation can not be performed an an invalid document' unless valid?
71
+ @validation_errors = []
72
+
73
+ # Find modules which said latest but resolved to a specific version
74
+ modules.each do |mod|
75
+ next unless mod.version == :latest
76
+ resolved_module = resolution_result.specifications[mod.name]
77
+ next if resolved_module.nil? || resolved_module.is_a?(PuppetfileResolver::Models::MissingModuleSpecification)
78
+ @validation_errors << DocumentLatestVersionError.new(
79
+ "Latest version of #{mod.name} is #{resolved_module.version}",
80
+ mod,
81
+ resolved_module
82
+ )
83
+ end
84
+
85
+ # Find modules which could not be found (in the forge etc.)
86
+ modules.each do |mod|
87
+ resolved_module = resolution_result.specifications[mod.name]
88
+ next unless resolved_module.is_a?(PuppetfileResolver::Models::MissingModuleSpecification)
89
+
90
+ @validation_errors << DocumentMissingModuleError.new(
91
+ "Could not find module #{mod.title}",
92
+ mod,
93
+ resolved_module
94
+ )
95
+ end
96
+
97
+ # Find modules with missing dependencies
98
+ puppetfile_module_names = modules.map(&:name)
99
+ modules.each do |mod|
100
+ resolved_module = resolution_result.specifications[mod.name]
101
+ vertex = resolution_result.dependency_graph.vertex_named(mod.name)
102
+ next if vertex.nil? || vertex.payload.nil?
103
+ missing_successors = vertex.recursive_successors.select do |successor_vertex|
104
+ next if successor_vertex.nil?
105
+ next unless successor_vertex.payload.is_a?(PuppetfileResolver::Models::ModuleSpecification)
106
+ !puppetfile_module_names.include?(successor_vertex.payload.name)
107
+ end
108
+
109
+ next if missing_successors.empty?
110
+ missing_specs = missing_successors.map(&:payload)
111
+ missing_names = missing_specs.map { |spec| "#{spec.name}-#{spec.version}" }.join(', ')
112
+ plural = missing_successors.count == 1 ? '' : 's'
113
+ @validation_errors << DocumentMissingDependenciesError.new(
114
+ "Module #{mod.title} is missing dependent module#{plural}: #{missing_names}",
115
+ mod,
116
+ resolved_module,
117
+ missing_specs
118
+ )
119
+ end
120
+
121
+ @validation_errors
122
+ end
123
+ end
124
+ end
125
+ end