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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +169 -0
- data/lib/puppetfile-resolver.rb +7 -0
- data/lib/puppetfile-resolver/cache/base.rb +28 -0
- data/lib/puppetfile-resolver/cache/persistent.rb +50 -0
- data/lib/puppetfile-resolver/data/ruby_ca_certs.pem +3432 -0
- data/lib/puppetfile-resolver/models.rb +8 -0
- data/lib/puppetfile-resolver/models/missing_module_specification.rb +27 -0
- data/lib/puppetfile-resolver/models/module_dependency.rb +55 -0
- data/lib/puppetfile-resolver/models/module_specification.rb +114 -0
- data/lib/puppetfile-resolver/models/puppet_dependency.rb +34 -0
- data/lib/puppetfile-resolver/models/puppet_specification.rb +25 -0
- data/lib/puppetfile-resolver/models/puppetfile_dependency.rb +14 -0
- data/lib/puppetfile-resolver/puppetfile.rb +22 -0
- data/lib/puppetfile-resolver/puppetfile/base_module.rb +62 -0
- data/lib/puppetfile-resolver/puppetfile/document.rb +125 -0
- data/lib/puppetfile-resolver/puppetfile/forge_module.rb +14 -0
- data/lib/puppetfile-resolver/puppetfile/git_module.rb +19 -0
- data/lib/puppetfile-resolver/puppetfile/invalid_module.rb +16 -0
- data/lib/puppetfile-resolver/puppetfile/local_module.rb +14 -0
- data/lib/puppetfile-resolver/puppetfile/parser/errors.rb +19 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval.rb +133 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/dsl.rb +51 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/forge.rb +50 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/git.rb +32 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/invalid.rb +27 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/local.rb +26 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/svn.rb +30 -0
- data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/puppet_module.rb +36 -0
- data/lib/puppetfile-resolver/puppetfile/svn_module.rb +16 -0
- data/lib/puppetfile-resolver/puppetfile/validation_errors.rb +106 -0
- data/lib/puppetfile-resolver/resolution_provider.rb +182 -0
- data/lib/puppetfile-resolver/resolution_result.rb +30 -0
- data/lib/puppetfile-resolver/resolver.rb +77 -0
- data/lib/puppetfile-resolver/spec_searchers/common.rb +15 -0
- data/lib/puppetfile-resolver/spec_searchers/forge.rb +75 -0
- data/lib/puppetfile-resolver/spec_searchers/git.rb +64 -0
- data/lib/puppetfile-resolver/spec_searchers/local.rb +45 -0
- data/lib/puppetfile-resolver/ui/debug_ui.rb +15 -0
- data/lib/puppetfile-resolver/ui/null_ui.rb +20 -0
- data/lib/puppetfile-resolver/util.rb +23 -0
- data/lib/puppetfile-resolver/version.rb +5 -0
- data/puppetfile-cli.rb +101 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/unit/puppetfile-resolver/puppetfile/document_spec.rb +316 -0
- data/spec/unit/puppetfile-resolver/puppetfile/parser/r10k_eval_spec.rb +460 -0
- data/spec/unit/puppetfile-resolver/resolver_spec.rb +421 -0
- metadata +124 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PuppetfileResolver
|
4
|
+
module SpecSearchers
|
5
|
+
module Common
|
6
|
+
def self.dependency_cache_id(caller, dependency)
|
7
|
+
"#{caller}-#{dependency.owner}-#{dependency.name}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.specification_cache_id(caller, specification)
|
11
|
+
"#{caller}-#{specification.owner}-#{specification.name}-#{specification.version}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puppetfile-resolver/spec_searchers/common'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'openssl'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module PuppetfileResolver
|
10
|
+
module SpecSearchers
|
11
|
+
module Forge
|
12
|
+
DEFAULT_FORGE_URI ||= 'https://forgeapi.puppet.com'
|
13
|
+
|
14
|
+
def self.find_all(dependency, cache, resolver_ui)
|
15
|
+
dep_id = ::PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(self, dependency)
|
16
|
+
|
17
|
+
# Has the information been cached?
|
18
|
+
return cache.load(dep_id) if cache.exist?(dep_id)
|
19
|
+
|
20
|
+
result = []
|
21
|
+
# Query the forge
|
22
|
+
fetch_all_module_releases(dependency.owner, dependency.name, resolver_ui) do |partial_releases|
|
23
|
+
partial_releases.each do |release|
|
24
|
+
result << Models::ModuleSpecification.new(
|
25
|
+
name: release['module']['name'],
|
26
|
+
owner: release['module']['owner']['slug'],
|
27
|
+
origin: :forge,
|
28
|
+
version: release['version'],
|
29
|
+
metadata: ::PuppetfileResolver::Util.symbolise_object(release['metadata'])
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
cache.save(dep_id, result)
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.fetch_all_module_releases(owner, name, forge_api_url = DEFAULT_FORGE_URI, resolver_ui, &block)
|
39
|
+
raise 'Requires a block to yield' unless block
|
40
|
+
uri = ::URI.parse("#{forge_api_url}/v3/releases")
|
41
|
+
params = { :module => "#{owner}-#{name}", :exclude_fields => 'readme changelog license reference tasks', :limit => 50 }
|
42
|
+
uri.query = ::URI.encode_www_form(params)
|
43
|
+
|
44
|
+
loops = 0
|
45
|
+
loop do
|
46
|
+
resolver_ui.debug { "Querying the forge for a module with #{uri}" }
|
47
|
+
|
48
|
+
http_options = { :use_ssl => uri.class == URI::HTTPS }
|
49
|
+
# Because on Windows Ruby doesn't use the Windows certificate store which has up-to date
|
50
|
+
# CA certs, we can't depend on someone setting the environment variable correctly. So use our
|
51
|
+
# static CA PEM file if SSL_CERT_FILE is not set.
|
52
|
+
http_options[:ca_file] = PuppetfileResolver::Util.static_ca_cert_file if ENV['SSL_CERT_FILE'].nil?
|
53
|
+
|
54
|
+
response = nil
|
55
|
+
Net::HTTP.start(uri.host, uri.port, http_options) do |http|
|
56
|
+
request = Net::HTTP::Get.new uri
|
57
|
+
response = http.request request
|
58
|
+
end
|
59
|
+
raise "Expected HTTP Code 200, but received #{response.code} for URI #{uri}: #{response.inspect}" unless response.code == '200'
|
60
|
+
|
61
|
+
reply = ::JSON.parse(response.body)
|
62
|
+
yield reply['results']
|
63
|
+
|
64
|
+
break if reply['pagination'].nil? || reply['pagination']['next'].nil?
|
65
|
+
uri = ::URI.parse("#{forge_api_url}#{reply['pagination']['next']}")
|
66
|
+
|
67
|
+
# Circuit breaker in case the worst happens (max 1000 module releases)
|
68
|
+
loops += 1
|
69
|
+
raise "Too many Forge API requests #{loops * 50}" if loops > 20
|
70
|
+
end
|
71
|
+
end
|
72
|
+
private_class_method :fetch_all_module_releases
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puppetfile-resolver/spec_searchers/common'
|
4
|
+
|
5
|
+
module PuppetfileResolver
|
6
|
+
module SpecSearchers
|
7
|
+
module Git
|
8
|
+
def self.find_all(puppetfile_module, dependency, cache, resolver_ui)
|
9
|
+
dep_id = ::PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(self, dependency)
|
10
|
+
# Has the information been cached?
|
11
|
+
return cache.load(dep_id) if cache.exist?(dep_id)
|
12
|
+
|
13
|
+
# We _could_ git clone this, but it'll take too long. So for now, just
|
14
|
+
# try and resolve github based repositories by crafting a URL
|
15
|
+
|
16
|
+
repo_url = nil
|
17
|
+
if puppetfile_module.remote.start_with?('git@github.com:')
|
18
|
+
repo_url = puppetfile_module.remote.slice(15..-1)
|
19
|
+
repo_url = repo_url.slice(0..-5) if repo_url.end_with?('.git')
|
20
|
+
end
|
21
|
+
if puppetfile_module.remote.start_with?('https://github.com/')
|
22
|
+
repo_url = puppetfile_module.remote.slice(19..-1)
|
23
|
+
repo_url = repo_url.slice(0..-5) if repo_url.end_with?('.git')
|
24
|
+
end
|
25
|
+
|
26
|
+
return [] if repo_url.nil?
|
27
|
+
|
28
|
+
metadata_url = 'https://raw.githubusercontent.com/' + repo_url + '/'
|
29
|
+
if puppetfile_module.ref
|
30
|
+
metadata_url += puppetfile_module.ref + '/'
|
31
|
+
elsif puppetfile_module.tag
|
32
|
+
metadata_url += puppetfile_module.tag + '/'
|
33
|
+
else
|
34
|
+
# Default to master. Should it raise?
|
35
|
+
metadata_url += 'master/'
|
36
|
+
end
|
37
|
+
metadata_url += 'metadata.json'
|
38
|
+
|
39
|
+
require 'net/http'
|
40
|
+
require 'uri'
|
41
|
+
|
42
|
+
resolver_ui.debug { "Querying the Github with #{metadata_url}" }
|
43
|
+
response = Net::HTTP.get_response(URI.parse(metadata_url))
|
44
|
+
if response.code != '200'
|
45
|
+
cache.save(dep_id, [])
|
46
|
+
return []
|
47
|
+
end
|
48
|
+
|
49
|
+
# TODO: symbolise_object should be in a Util namespace
|
50
|
+
metadata = ::PuppetfileResolver::Util.symbolise_object(::JSON.parse(response.body))
|
51
|
+
result = [Models::ModuleSpecification.new(
|
52
|
+
name: metadata[:name],
|
53
|
+
origin: :git,
|
54
|
+
version: metadata[:version],
|
55
|
+
metadata: metadata
|
56
|
+
)]
|
57
|
+
|
58
|
+
cache.save(dep_id, result)
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puppetfile-resolver/spec_searchers/common'
|
4
|
+
|
5
|
+
module PuppetfileResolver
|
6
|
+
module SpecSearchers
|
7
|
+
module Local
|
8
|
+
def self.find_all(_puppetfile_module, puppet_module_paths, dependency, cache, resolver_ui)
|
9
|
+
dep_id = ::PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(self, dependency)
|
10
|
+
# Has the information been cached?
|
11
|
+
return cache.load(dep_id) if cache.exist?(dep_id)
|
12
|
+
|
13
|
+
result = []
|
14
|
+
# Find the module in the modulepaths
|
15
|
+
puppet_module_paths.each do |module_path|
|
16
|
+
next unless Dir.exist?(module_path)
|
17
|
+
module_dir = File.expand_path(File.join(module_path, dependency.name))
|
18
|
+
next unless Dir.exist?(module_dir)
|
19
|
+
metadata_file = File.join(module_dir, 'metadata.json')
|
20
|
+
next unless File.exist?(metadata_file)
|
21
|
+
|
22
|
+
metadata = nil
|
23
|
+
begin
|
24
|
+
metadata = ::PuppetfileResolver::Util.symbolise_object(
|
25
|
+
::JSON.parse(File.open(metadata_file, 'rb:utf-8') { |f| f.read })
|
26
|
+
)
|
27
|
+
rescue StandardError => _e # rubocop:disable Lint/SuppressedException Todo
|
28
|
+
# TODO: Should really do something?
|
29
|
+
end
|
30
|
+
resolver_ui.debug { "Found local module at #{metadata_file}" }
|
31
|
+
|
32
|
+
result << Models::ModuleSpecification.new(
|
33
|
+
name: metadata[:name],
|
34
|
+
origin: :local,
|
35
|
+
version: metadata[:version],
|
36
|
+
metadata: metadata
|
37
|
+
)
|
38
|
+
end
|
39
|
+
cache.save(dep_id, result)
|
40
|
+
|
41
|
+
result
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'molinillo'
|
4
|
+
|
5
|
+
module PuppetfileResolver
|
6
|
+
module UI
|
7
|
+
class NullUI
|
8
|
+
include Molinillo::UI
|
9
|
+
|
10
|
+
# Suppress all output
|
11
|
+
def output
|
12
|
+
@output ||= File.open(File::NULL, 'w')
|
13
|
+
end
|
14
|
+
|
15
|
+
def debug?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PuppetfileResolver
|
4
|
+
module Util
|
5
|
+
def self.symbolise_object(object)
|
6
|
+
case # rubocop:disable Style/EmptyCaseCondition Ignore
|
7
|
+
when object.is_a?(Hash)
|
8
|
+
object.inject({}) do |memo, (k, v)| # rubocop:disable Style/EachWithObject Ignore
|
9
|
+
memo[k.to_sym] = symbolise_object(v)
|
10
|
+
memo
|
11
|
+
end
|
12
|
+
when object.is_a?(Array)
|
13
|
+
object.map { |i| symbolise_object(i) }
|
14
|
+
else
|
15
|
+
object
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.static_ca_cert_file
|
20
|
+
@static_ca_cert_file ||= File.expand_path(File.join(__dir__, 'data', 'ruby_ca_certs.pem'))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/puppetfile-cli.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is not supposed to be a fully fledged CLI for the resolver, as this gem is designed to be used
|
4
|
+
# as a library. The CLI is offered as an example as to how to use the library.
|
5
|
+
|
6
|
+
# Add the resolver into the load path
|
7
|
+
lib_root = File.expand_path(File.join(__dir__, 'lib'))
|
8
|
+
$LOAD_PATH.unshift(lib_root)
|
9
|
+
|
10
|
+
require 'puppetfile-resolver'
|
11
|
+
require 'optparse'
|
12
|
+
|
13
|
+
class CommandLineParser
|
14
|
+
def self.parse(options)
|
15
|
+
# Set defaults here
|
16
|
+
args = {
|
17
|
+
debug: false,
|
18
|
+
cache_dir: nil,
|
19
|
+
module_paths: [],
|
20
|
+
path: nil,
|
21
|
+
puppet_version: nil,
|
22
|
+
strict: false
|
23
|
+
}
|
24
|
+
|
25
|
+
opt_parser = OptionParser.new do |opts|
|
26
|
+
opts.banner = 'Usage: puppetfile-cli.rb [options]'
|
27
|
+
|
28
|
+
opts.on('-pPATH', '--path=PATH', 'Puppetfile to parse') do |path|
|
29
|
+
args[:path] = path
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on('-vVERSION', '--puppet_version=VERSION', 'Restrict the resolver to only modules which support the specified Puppet version') do |version|
|
33
|
+
args[:puppet_version] = version
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('-cDIR', '--cache_directory=DIR', 'Directory to the persistent on disk cache. Optional') do |cache_dir|
|
37
|
+
args[:cache_dir] = cache_dir
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on('--debug', 'Output debug information. Default is no debug output') do
|
41
|
+
args[:debug] = true
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on('-s', '--strict', 'Do not allow missing dependencies. Default false which marks dependencies as missing and does not raise an error.') do
|
45
|
+
args[:strict] = true
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('-mTEXT', '--module_paths=TEXT', Array, 'Comma delimited list of modules paths to search') do |text|
|
49
|
+
args[:module_paths] = text
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
opt_parser.parse!(options.dup)
|
54
|
+
args
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
options = CommandLineParser.parse(ARGV)
|
59
|
+
raise 'Missing --path' if options[:path].nil?
|
60
|
+
|
61
|
+
# Configure the cache
|
62
|
+
if options[:cache_dir].nil?
|
63
|
+
cache = nil
|
64
|
+
else
|
65
|
+
require 'puppetfile-resolver/cache/persistent'
|
66
|
+
cache = PuppetfileResolver::Cache::Persistent.new(options[:cache_dir])
|
67
|
+
end
|
68
|
+
|
69
|
+
# Parse the Puppetfile into an object model
|
70
|
+
content = File.open(options[:path], 'rb') { |f| f.read }
|
71
|
+
require 'puppetfile-resolver/puppetfile/parser/r10k_eval'
|
72
|
+
puppetfile = ::PuppetfileResolver::Puppetfile::Parser::R10KEval.parse(content)
|
73
|
+
|
74
|
+
# Make sure the Puppetfile is valid
|
75
|
+
unless puppetfile.valid?
|
76
|
+
puts 'Puppetfile is not valid'
|
77
|
+
puppetfile.validation_errors.each { |err| puts err }
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
|
81
|
+
# Create the resolver
|
82
|
+
resolver = PuppetfileResolver::Resolver.new(puppetfile, options[:puppet_version])
|
83
|
+
|
84
|
+
# Configure the resolver
|
85
|
+
if options[:debug]
|
86
|
+
require 'puppetfile-resolver/ui/debug_ui'
|
87
|
+
ui = PuppetfileResolver::UI::DebugUI.new
|
88
|
+
else
|
89
|
+
ui = nil
|
90
|
+
end
|
91
|
+
opts = { cache: cache, ui: ui, module_paths: options[:module_paths], allow_missing_modules: !options[:strict] }
|
92
|
+
|
93
|
+
# Resolve
|
94
|
+
result = resolver.resolve(opts)
|
95
|
+
|
96
|
+
# Output errors
|
97
|
+
result.validation_errors.each { |err| puts "Resolution Validation Error: #{err}\n" }
|
98
|
+
|
99
|
+
# Output the Graph in a DOT format
|
100
|
+
puts "\n--- Dependency Graph"
|
101
|
+
puts result.to_dot
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
root = File.join(__dir__,'..',)
|
4
|
+
# Add the language server into the load path
|
5
|
+
$LOAD_PATH.unshift(File.join(root,'lib'))
|
6
|
+
|
7
|
+
# A cache which we can preload for the purposes of testing, mimicing
|
8
|
+
# Local modules
|
9
|
+
require 'puppetfile-resolver/cache/base'
|
10
|
+
class MockLocalModuleCache < PuppetfileResolver::Cache::Base
|
11
|
+
def initialize
|
12
|
+
super(nil)
|
13
|
+
|
14
|
+
@mock_modules = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def exist?(name)
|
18
|
+
result = super
|
19
|
+
return result if result
|
20
|
+
@mock_modules.key?(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def load(name)
|
24
|
+
result = super
|
25
|
+
return result unless result.nil?
|
26
|
+
|
27
|
+
@mock_modules[name]
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_local_module_spec(name, dependencies = [], puppet_requirement = nil, version = '1.0.0')
|
31
|
+
requirements = []
|
32
|
+
requirements << { name: 'puppet', version_requirement: puppet_requirement } unless puppet_requirement.nil?
|
33
|
+
spec = PuppetfileResolver::Models::ModuleSpecification.new(
|
34
|
+
name: name,
|
35
|
+
origin: :local,
|
36
|
+
version: version,
|
37
|
+
metadata: {
|
38
|
+
dependencies: dependencies,
|
39
|
+
requirements: requirements
|
40
|
+
}
|
41
|
+
)
|
42
|
+
# Note - This is quite implementation dependant so could be fragile.
|
43
|
+
# Yes, I know it's expecting a Dependency object, but the spec is fine.
|
44
|
+
id = PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(PuppetfileResolver::SpecSearchers::Local, spec)
|
45
|
+
@mock_modules[id] = [] if @mock_modules[id].nil?
|
46
|
+
@mock_modules[id] << spec
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'puppetfile-resolver/models'
|
4
|
+
require 'puppetfile-resolver/resolution_result'
|
5
|
+
require 'puppetfile-resolver/puppetfile'
|
6
|
+
|
7
|
+
describe 'PuppetfileResolver::Puppetfile::Document' do
|
8
|
+
let(:puppetfile_content) { 'puppetfile' }
|
9
|
+
let(:subject) { PuppetfileResolver::Puppetfile::Document.new(puppetfile_content) }
|
10
|
+
|
11
|
+
describe "Document Validation" do
|
12
|
+
context 'Given an empty document' do
|
13
|
+
before(:each) do
|
14
|
+
subject.clear_modules
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should be valid' do
|
18
|
+
expect(subject.valid?).to eq(true)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should have no validation errors' do
|
22
|
+
expect(subject.validation_errors).to eq([])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'Given a document with valid modules' do
|
27
|
+
let(:module1) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
|
28
|
+
let(:module2) { PuppetfileResolver::Puppetfile::LocalModule.new('bar') }
|
29
|
+
let(:module3) { PuppetfileResolver::Puppetfile::LocalModule.new('baz') }
|
30
|
+
|
31
|
+
before(:each) do
|
32
|
+
subject.clear_modules
|
33
|
+
subject.add_module(module1)
|
34
|
+
subject.add_module(module2)
|
35
|
+
subject.add_module(module3)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should be valid' do
|
39
|
+
expect(subject.valid?).to eq(true)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should have no validation errors' do
|
43
|
+
expect(subject.validation_errors).to eq([])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'Given a document with an invalid module' do
|
48
|
+
let(:invalid_module) do
|
49
|
+
PuppetfileResolver::Puppetfile::InvalidModule.new('foo-invalid').tap { |m| m.reason = 'MockReason' }
|
50
|
+
end
|
51
|
+
|
52
|
+
before(:each) do
|
53
|
+
subject.clear_modules
|
54
|
+
subject.add_module(invalid_module)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should not be valid' do
|
58
|
+
expect(subject.valid?).to eq(false)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should have a validation error' do
|
62
|
+
expect(subject.validation_errors.count).to eq (1)
|
63
|
+
expect(subject.validation_errors[0]).to be_a(PuppetfileResolver::Puppetfile::DocumentInvalidModuleError)
|
64
|
+
expect(subject.validation_errors[0]).to have_attributes(
|
65
|
+
:message => 'MockReason',
|
66
|
+
:puppet_module => invalid_module
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'Given a document with duplicate modules' do
|
72
|
+
let(:module1) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
|
73
|
+
let(:module2) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
|
74
|
+
let(:module3) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
|
75
|
+
let(:module4) { PuppetfileResolver::Puppetfile::LocalModule.new('bar') }
|
76
|
+
let(:module5) { PuppetfileResolver::Puppetfile::LocalModule.new('bar') }
|
77
|
+
let(:module6) { PuppetfileResolver::Puppetfile::LocalModule.new('baz') }
|
78
|
+
|
79
|
+
before(:each) do
|
80
|
+
subject.clear_modules
|
81
|
+
subject.add_module(module1)
|
82
|
+
subject.add_module(module2)
|
83
|
+
subject.add_module(module3)
|
84
|
+
subject.add_module(module4)
|
85
|
+
subject.add_module(module5)
|
86
|
+
subject.add_module(module6)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should not be valid' do
|
90
|
+
expect(subject.valid?).to eq(false)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should have validation errors' do
|
94
|
+
expect(subject.validation_errors.count).to eq (2)
|
95
|
+
# Duplication foo module
|
96
|
+
err = subject.validation_errors[0]
|
97
|
+
expect(err).to be_a(PuppetfileResolver::Puppetfile::DocumentDuplicateModuleError)
|
98
|
+
expect(err).to have_attributes(
|
99
|
+
:message => /foo/,
|
100
|
+
:puppet_module => module1,
|
101
|
+
:duplicates => [module2, module3]
|
102
|
+
)
|
103
|
+
# Duplicate bar module
|
104
|
+
err = subject.validation_errors[1]
|
105
|
+
expect(err).to be_a(PuppetfileResolver::Puppetfile::DocumentDuplicateModuleError)
|
106
|
+
expect(err).to have_attributes(
|
107
|
+
:message => /bar/,
|
108
|
+
:puppet_module => module4,
|
109
|
+
:duplicates => [module5]
|
110
|
+
)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe ".resolution_validation_errors" do
|
116
|
+
# Array of modules to add to the document
|
117
|
+
let(:document_fixtures) { [] }
|
118
|
+
let(:resolution_graph) { Molinillo::DependencyGraph.new }
|
119
|
+
let(:resolution_result) { PuppetfileResolver::ResolutionResult.new(resolution_graph, subject) }
|
120
|
+
let(:resolution_specifications) { [] }
|
121
|
+
let(:resolution_dependencies) { [] }
|
122
|
+
|
123
|
+
before(:each) do
|
124
|
+
# Generate the Puppetfile document
|
125
|
+
subject.clear_modules
|
126
|
+
@document_modules = document_fixtures.map do |doc_mod|
|
127
|
+
mod = doc_mod[:class].new(doc_mod[:name])
|
128
|
+
mod.version = doc_mod[:version]
|
129
|
+
subject.add_module(mod)
|
130
|
+
mod
|
131
|
+
end
|
132
|
+
|
133
|
+
# Generate the Dependency Graph
|
134
|
+
@module_specifications = {}
|
135
|
+
# Create all of the specifications
|
136
|
+
resolution_specifications.each do |res_spec|
|
137
|
+
res_spec[:class] = PuppetfileResolver::Models::ModuleSpecification if res_spec[:class].nil?
|
138
|
+
@module_specifications[res_spec[:name]] = res_spec[:class].new(name: res_spec[:name], origin: res_spec[:origin], metadata: {}, version: res_spec[:version])
|
139
|
+
end
|
140
|
+
# Create all of the vertexes in the graph
|
141
|
+
@module_specifications.each { |name, spec| resolution_graph.add_vertex(name, spec, false) }
|
142
|
+
# Create all of the edges in the graph
|
143
|
+
resolution_dependencies.each do |dep|
|
144
|
+
dep[:class] = PuppetfileResolver::Models::ModuleDependency if dep[:class].nil?
|
145
|
+
mod_dep = dep[:class].new(name: dep[:destination], version_requirement: dep[:version_requirement])
|
146
|
+
resolution_graph.add_edge(
|
147
|
+
resolution_graph.vertex_named(dep[:origin]),
|
148
|
+
resolution_graph.vertex_named(dep[:destination]),
|
149
|
+
mod_dep
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'Given an invalid document' do
|
155
|
+
before(:each) do
|
156
|
+
allow(subject).to receive(:valid?).and_return(false)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should raise' do
|
160
|
+
expect{ subject.resolution_validation_errors(resolution_result) }.to raise_error(/invalid document/)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'Given a valid resolution' do
|
165
|
+
let(:document_fixtures) do
|
166
|
+
[
|
167
|
+
{ class: PuppetfileResolver::Puppetfile::LocalModule, name: 'foo' },
|
168
|
+
{ class: PuppetfileResolver::Puppetfile::LocalModule, name: 'bar' }
|
169
|
+
]
|
170
|
+
end
|
171
|
+
let(:resolution_specifications) do
|
172
|
+
[
|
173
|
+
{ name: 'foo', origin: :local, version: '1.0.0' },
|
174
|
+
{ name: 'bar', origin: :local, version: '1.0.0' }
|
175
|
+
]
|
176
|
+
end
|
177
|
+
let(:resolution_dependencies) do
|
178
|
+
[
|
179
|
+
{ origin: 'foo', destination: 'bar', version_requirement: '>= 0' },
|
180
|
+
]
|
181
|
+
end
|
182
|
+
it 'should return no errors' do
|
183
|
+
expect(subject.resolution_validation_errors(resolution_result)).to be_empty
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context 'Given a document with :latest' do
|
188
|
+
let(:document_fixtures) do
|
189
|
+
[
|
190
|
+
{ class: PuppetfileResolver::Puppetfile::ForgeModule, name: 'foo', version: :latest }
|
191
|
+
]
|
192
|
+
end
|
193
|
+
let(:resolution_specifications) do
|
194
|
+
[
|
195
|
+
{ name: 'foo', origin: :forge, version: '1.0.0' }
|
196
|
+
]
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'should return a DocumentLatestVersionError' do
|
200
|
+
errors = subject.resolution_validation_errors(resolution_result)
|
201
|
+
expect(errors.count).to eq(1)
|
202
|
+
error = errors[0]
|
203
|
+
expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentLatestVersionError)
|
204
|
+
expect(error).to have_attributes(
|
205
|
+
message: /foo/,
|
206
|
+
module_specification: @module_specifications['foo'],
|
207
|
+
puppet_module: @document_modules[0]
|
208
|
+
)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'Given a document with a missing module' do
|
213
|
+
let(:document_fixtures) do
|
214
|
+
[
|
215
|
+
{ class: PuppetfileResolver::Puppetfile::LocalModule, name: 'foo', version: :latest }
|
216
|
+
]
|
217
|
+
end
|
218
|
+
let(:resolution_specifications) do
|
219
|
+
[
|
220
|
+
{ class: PuppetfileResolver::Models::MissingModuleSpecification, name: 'foo' }
|
221
|
+
]
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'should return a DocumentMissingModuleError' do
|
225
|
+
errors = subject.resolution_validation_errors(resolution_result)
|
226
|
+
expect(errors.count).to eq(1)
|
227
|
+
error = errors[0]
|
228
|
+
expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingModuleError)
|
229
|
+
expect(error).to have_attributes(
|
230
|
+
message: /foo/,
|
231
|
+
module_specification: @module_specifications['foo'],
|
232
|
+
puppet_module: @document_modules[0]
|
233
|
+
)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
context 'Given a document with missing module dependencies' do
|
238
|
+
# We setup a Puppetfile that looks like
|
239
|
+
# ```
|
240
|
+
# mod 'foo', :local => true
|
241
|
+
# mod 'baz', :local => true
|
242
|
+
# mod 'waldo', :local => true
|
243
|
+
# ```
|
244
|
+
let(:document_fixtures) do
|
245
|
+
[
|
246
|
+
{ class: PuppetfileResolver::Puppetfile::LocalModule, name: 'foo', version: '1.0.0' },
|
247
|
+
{ class: PuppetfileResolver::Puppetfile::LocalModule, name: 'baz', version: '1.0.0' },
|
248
|
+
{ class: PuppetfileResolver::Puppetfile::LocalModule, name: 'waldo', version: '1.0.0' },
|
249
|
+
]
|
250
|
+
end
|
251
|
+
# We then setup a resolution graph that looks like
|
252
|
+
#
|
253
|
+
# foo --> bar --> baz --> qux
|
254
|
+
# waldo --^
|
255
|
+
#
|
256
|
+
# Where foo depends on bar, which depends on baz, which depends on qux. And
|
257
|
+
# waldo depends on baz, which depends on qux
|
258
|
+
#
|
259
|
+
# Due to the puppetfile and dependency graph:
|
260
|
+
# - foo will be missing bar and qux
|
261
|
+
# - baz will be missing qux
|
262
|
+
# - waldo will be missing qux
|
263
|
+
#
|
264
|
+
# This is to test for transitive dependencies, not just an entire dependency tree
|
265
|
+
let(:resolution_specifications) do
|
266
|
+
[
|
267
|
+
{ name: 'foo', origin: :forge, version: '1.0.0' },
|
268
|
+
{ name: 'bar', origin: :forge, version: '1.0.0' },
|
269
|
+
{ name: 'baz', origin: :forge, version: '1.0.0' },
|
270
|
+
{ name: 'qux', origin: :forge, version: '1.0.0' },
|
271
|
+
{ name: 'waldo', origin: :forge, version: '1.0.0' },
|
272
|
+
]
|
273
|
+
end
|
274
|
+
let(:resolution_dependencies) do
|
275
|
+
[
|
276
|
+
{ origin: 'foo', destination: 'bar', version_requirement: '>= 0' },
|
277
|
+
{ origin: 'bar', destination: 'baz', version_requirement: '>= 0' },
|
278
|
+
{ origin: 'baz', destination: 'qux', version_requirement: '>= 0' },
|
279
|
+
{ origin: 'waldo', destination: 'baz', version_requirement: '>= 0' },
|
280
|
+
]
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'should return transient DocumentMissingModuleError' do
|
284
|
+
errors = subject.resolution_validation_errors(resolution_result)
|
285
|
+
expect(errors.count).to eq(3)
|
286
|
+
|
287
|
+
error = errors[0]
|
288
|
+
expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingDependenciesError)
|
289
|
+
expect(error).to have_attributes(
|
290
|
+
message: /foo/,
|
291
|
+
module_specification: @module_specifications['foo'],
|
292
|
+
puppet_module: @document_modules[0],
|
293
|
+
missing_specifications: [@module_specifications['bar'], @module_specifications['qux']]
|
294
|
+
)
|
295
|
+
|
296
|
+
error = errors[1]
|
297
|
+
expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingDependenciesError)
|
298
|
+
expect(error).to have_attributes(
|
299
|
+
message: /baz/,
|
300
|
+
module_specification: @module_specifications['baz'],
|
301
|
+
puppet_module: @document_modules[1],
|
302
|
+
missing_specifications: [@module_specifications['qux']]
|
303
|
+
)
|
304
|
+
|
305
|
+
error = errors[2]
|
306
|
+
expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingDependenciesError)
|
307
|
+
expect(error).to have_attributes(
|
308
|
+
message: /waldo/,
|
309
|
+
module_specification: @module_specifications['waldo'],
|
310
|
+
puppet_module: @document_modules[2],
|
311
|
+
missing_specifications: [@module_specifications['qux']]
|
312
|
+
)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|