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,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