semantic_puppet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .rbenv-*
2
+ .yardoc
3
+ coverage
4
+ doc
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -m markdown
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## [Unreleased version][Unreleased date]
6
+ ### Changed
7
+
8
+ ## 0.1.0 - 2015-03-23
9
+ ### Added
10
+ - initial release in concert with current Puppet Module Tool v4.0.0 behavior
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "https://rubygems.org"
2
+
3
+ group :development do
4
+ gem 'rake'
5
+ gem 'rubygems-tasks'
6
+ end
7
+
8
+ group :test do
9
+ gem 'rspec'
10
+ end
11
+
12
+ group :metrics do
13
+ gem 'cane', :platform => [ :mri_19, :mri_20, :mri_21 ]
14
+ gem 'simplecov', :platform => [ :mri_19, :mri_20, :mri_21 ]
15
+ end
16
+
17
+ group :doc do
18
+ gem 'yard', '~> 0.8', :platform => [ :mri_19, :mri_20, :mri_21 ]
19
+ gem 'redcarpet', :platform => [ :mri_19, :mri_20, :mri_21 ]
20
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,38 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ cane (2.6.1)
5
+ parallel
6
+ diff-lcs (1.2.5)
7
+ docile (1.1.1)
8
+ multi_json (1.8.2)
9
+ parallel (0.9.1)
10
+ rake (10.1.0)
11
+ redcarpet (3.0.0)
12
+ rspec (2.14.1)
13
+ rspec-core (~> 2.14.0)
14
+ rspec-expectations (~> 2.14.0)
15
+ rspec-mocks (~> 2.14.0)
16
+ rspec-core (2.14.7)
17
+ rspec-expectations (2.14.4)
18
+ diff-lcs (>= 1.1.3, < 2.0)
19
+ rspec-mocks (2.14.4)
20
+ rubygems-tasks (0.2.4)
21
+ simplecov (0.8.2)
22
+ docile (~> 1.1.0)
23
+ multi_json
24
+ simplecov-html (~> 0.8.0)
25
+ simplecov-html (0.8.0)
26
+ yard (0.8.7.3)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ cane
33
+ rake
34
+ redcarpet
35
+ rspec
36
+ rubygems-tasks
37
+ simplecov
38
+ yard (~> 0.8)
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ SemanticPuppet
2
+ ==============
3
+
4
+ Library of useful tools for working with Semantic Versions and module
5
+ dependencies.
6
+
7
+ Description
8
+ -----------
9
+
10
+ Library of tools used by Puppet to parse, validate, and compare Semantic
11
+ Versions and Version Ranges and to query and resolve module dependencies.
12
+
13
+ For sparse, but accurate documentation, please see the docs directory.
14
+
15
+ Note that this is a 0 release version, and things can change. Expect that the
16
+ version and version range code to stay relatively stable, but the module
17
+ dependency code is expected to change.
18
+
19
+ This library is used by a number of Puppet Labs projects, including
20
+ [Puppet](https://github.com/puppetlabs/puppet) and
21
+ [r10k](https://github.com/puppetlabs/r10k).
22
+
23
+ Requirements
24
+ ------------
25
+
26
+ Semantic_puppet will work on several ruby versions, including 1.9.3, 2.0.0, and
27
+ 2.1.0. Ruby 1.8.7 is immediately deprecated as it is in
28
+ [r10k](https://github.com/puppetlabs/r10k).
29
+
30
+ No gem/library requirements.
31
+
32
+ Installation
33
+ ------------
34
+
35
+ ### Rubygems
36
+
37
+ For general use, you should install semantic_puppet from Ruby gems:
38
+
39
+ gem install semantic_puppet
40
+
41
+ ### Github
42
+
43
+ If you have more specific needs or plan on modifying semantic_puppet you can
44
+ install it out of a git repository:
45
+
46
+ git clone git://github.com/puppetlabs/semantic_puppet
47
+
48
+ Usage
49
+ -----
50
+
51
+ SemanticPuppet is intended to be used as a library.
52
+
53
+ ### Verison Range Operator Support
54
+
55
+ SemanticPuppet will support the same version range operators as those used when
56
+ publishing modules to [Puppet Forge](https://forge.puppetlabs.com) which is
57
+ documented at
58
+ [Publishing Modules on the Puppet Forge](https://docs.puppetlabs.com/puppet/latest/reference/modules_publishing.html#dependencies-in-metadatajson).
59
+
60
+ Contributors
61
+ ------------
62
+
63
+ Pieter van de Bruggen wrote the library originally, with additions by Alex
64
+ Dreyer, Jesse Scott and Anderson Mills.
data/Rakefile ADDED
@@ -0,0 +1,69 @@
1
+ # RSpec tasks
2
+ begin
3
+ require 'rspec/core/rake_task'
4
+
5
+ # Create the 'spec' task
6
+ RSpec::Core::RakeTask.new(:spec) do |task|
7
+ task.rspec_opts = '--color'
8
+ end
9
+
10
+ namespace :spec do
11
+ desc "Run the test suite and generate coverage metrics"
12
+ task :coverage => [ :simplecov, :spec ]
13
+
14
+ # Add test coverage to the 'spec' task.
15
+ task :simplecov do
16
+ ENV['COVERAGE'] = '1'
17
+ end
18
+ end
19
+
20
+ task :default => :spec
21
+ rescue LoadError
22
+ warn "[Warning]: Could not load `rspec`."
23
+ end
24
+
25
+ # YARD tasks
26
+ begin
27
+ require 'yard'
28
+ require 'yard/rake/yardoc_task'
29
+
30
+ YARD::Rake::YardocTask.new(:doc) do |yardoc|
31
+ yardoc.files = [ 'lib/**/*.rb', '-', '**/*.md' ]
32
+ end
33
+ rescue LoadError
34
+ warn "[Warning]: Could not load `yard`."
35
+ end
36
+
37
+ # Cane tasks
38
+ begin
39
+ require 'cane/rake_task'
40
+
41
+ Cane::RakeTask.new(:cane) do |cane|
42
+ cane.add_threshold 'coverage/.last_run.json', :>=, 100
43
+ cane.abc_max = 15
44
+ end
45
+
46
+ Rake::Task['cane'].prerequisites << Rake::Task['spec:coverage']
47
+ Rake::Task[:default].clear_prerequisites
48
+ task :default => :cane
49
+ rescue LoadError
50
+ warn "[Warning]: Could not load `cane`."
51
+ end
52
+
53
+ # Gem tasks
54
+ begin
55
+ require 'rubygems/tasks'
56
+
57
+ task :gem => 'gem:build'
58
+ task :validate => [ 'cane', 'doc', 'gem:validate' ]
59
+
60
+ namespace :gem do
61
+ Gem::Tasks.new(
62
+ :tag => { :format => 'v%s' },
63
+ :sign => { :checksum => true, :pgp => true },
64
+ :build => { :tar => true }
65
+ )
66
+ end
67
+ rescue LoadError
68
+ warn "[Warning]: Could not load `rubygems/tasks`."
69
+ end
@@ -0,0 +1,7 @@
1
+ module SemanticPuppet
2
+ autoload :Version, 'semantic_puppet/version'
3
+ autoload :VersionRange, 'semantic_puppet/version_range'
4
+ autoload :Dependency, 'semantic_puppet/dependency'
5
+
6
+ VERSION = '0.1.0'
7
+ end
@@ -0,0 +1,181 @@
1
+ require 'semantic_puppet'
2
+
3
+ module SemanticPuppet
4
+ module Dependency
5
+ extend self
6
+
7
+ autoload :Graph, 'semantic_puppet/dependency/graph'
8
+ autoload :GraphNode, 'semantic_puppet/dependency/graph_node'
9
+ autoload :ModuleRelease, 'semantic_puppet/dependency/module_release'
10
+ autoload :Source, 'semantic_puppet/dependency/source'
11
+
12
+ autoload :UnsatisfiableGraph, 'semantic_puppet/dependency/unsatisfiable_graph'
13
+
14
+ # @!group Sources
15
+
16
+ # @return [Array<Source>] a frozen copy of the {Source} list
17
+ def sources
18
+ (@sources ||= []).dup.freeze
19
+ end
20
+
21
+ # Appends a new {Source} to the current list.
22
+ # @param source [Source] the {Source} to add
23
+ # @return [void]
24
+ def add_source(source)
25
+ sources
26
+ @sources << source
27
+ nil
28
+ end
29
+
30
+ # Clears the current list of {Source}s.
31
+ # @return [void]
32
+ def clear_sources
33
+ sources
34
+ @sources.clear
35
+ nil
36
+ end
37
+
38
+ # @!endgroup
39
+
40
+ # Fetches a graph of modules and their dependencies from the currently
41
+ # configured list of {Source}s.
42
+ #
43
+ # @todo Return a specialized "Graph" object.
44
+ # @todo Allow for external constraints to be added to the graph.
45
+ # @see #sources
46
+ # @see #add_source
47
+ # @see #clear_sources
48
+ #
49
+ # @param modules [{ String => String }]
50
+ # @return [Graph] the root of a dependency graph
51
+ def query(modules)
52
+ constraints = Hash[modules.map { |k, v| [ k, VersionRange.parse(v) ] }]
53
+
54
+ graph = Graph.new(constraints)
55
+ fetch_dependencies(graph)
56
+ return graph
57
+ end
58
+
59
+ # Given a graph result from {#query}, this method will resolve the graph of
60
+ # dependencies, if possible, into a flat list of the best suited modules. If
61
+ # the dependency graph does not have a suitable resolution, this method will
62
+ # raise an exception to that effect.
63
+ #
64
+ # @param graph [Graph] the root of a dependency graph
65
+ # @return [Array<ModuleRelease>] the list of releases to act on
66
+ def resolve(graph)
67
+ catch :next do
68
+ return walk(graph, graph.dependencies.dup)
69
+ end
70
+ raise UnsatisfiableGraph.new(graph)
71
+ end
72
+
73
+ # Fetches all available releases for the given module name.
74
+ #
75
+ # @param name [String] the module name to find releases for
76
+ # @return [Array<ModuleRelease>] the available releases
77
+ def fetch_releases(name)
78
+ releases = {}
79
+
80
+ sources.each do |source|
81
+ source.fetch(name).each do |dependency|
82
+ releases[dependency.version] ||= dependency
83
+ end
84
+ end
85
+
86
+ return releases.values
87
+ end
88
+
89
+ private
90
+
91
+ # Iterates over a changing set of dependencies in search of the best
92
+ # solution available. Fitness is specified as meeting all the constraints
93
+ # placed on it, being {ModuleRelease#satisfied? satisfied}, and having the
94
+ # greatest version number (with stability being preferred over prereleases).
95
+ #
96
+ # @todo Traversal order is not presently guaranteed.
97
+ #
98
+ # @param graph [Graph] the root of a dependency graph
99
+ # @param dependencies [{ String => Array<ModuleRelease> }] the dependencies
100
+ # @param considering [Array<GraphNode>] the set of releases being tested
101
+ # @return [Array<GraphNode>] the list of releases to use, if successful
102
+ def walk(graph, dependencies, *considering)
103
+ return considering if dependencies.empty?
104
+
105
+ # Selecting a dependency from the collection...
106
+ name = dependencies.keys.sort.first
107
+ deps = dependencies.delete(name)
108
+
109
+ # ... (and stepping over it if we've seen it before) ...
110
+ unless (deps & considering).empty?
111
+ return walk(graph, dependencies, *considering)
112
+ end
113
+
114
+ # ... we'll iterate through the list of possible versions in order.
115
+ preferred_releases(deps).reverse_each do |dep|
116
+
117
+ # We should skip any releases that violate any module's constraints.
118
+ unless [graph, *considering].all? { |x| x.satisfies_constraints?(dep) }
119
+ next
120
+ end
121
+
122
+ # We should skip over any releases that violate graph-level constraints.
123
+ potential_solution = considering.dup << dep
124
+ unless graph.satisfies_graph? potential_solution
125
+ next
126
+ end
127
+
128
+ catch :next do
129
+ # After adding any new dependencies and imposing our own constraints
130
+ # on existing dependencies, we'll mark ourselves as "under
131
+ # consideration" and recurse.
132
+ merged = dependencies.merge(dep.dependencies) { |_,a,b| a & b }
133
+
134
+ # If all subsequent dependencies resolved well, the recursive call
135
+ # will return a completed dependency list. If there were problems
136
+ # resolving our dependencies, we'll catch `:next`, which will cause
137
+ # us to move to the next possibility.
138
+ return walk(graph, merged, *potential_solution)
139
+ end
140
+ end
141
+
142
+ # Once we've exhausted all of our possible versions, we know that our
143
+ # last choice was unusable, so we'll unwind the stack and make a new
144
+ # choice.
145
+ throw :next
146
+ end
147
+
148
+ # Given a {ModuleRelease}, this method will iterate through the current
149
+ # list of {Source}s to find the complete list of versions available for its
150
+ # dependencies.
151
+ #
152
+ # @param node [GraphNode] the node to fetch details for
153
+ # @return [void]
154
+ def fetch_dependencies(node, cache = {})
155
+ node.dependency_names.each do |name|
156
+ unless cache.key?(name)
157
+ cache[name] = fetch_releases(name)
158
+ cache[name].each { |dep| fetch_dependencies(dep, cache) }
159
+ end
160
+
161
+ node << cache[name]
162
+ end
163
+ end
164
+
165
+ # Given a list of potential releases, this method returns the most suitable
166
+ # releases for exploration. Only {ModuleRelease#satisfied? satisfied}
167
+ # releases are considered, and releases with stable versions are preferred.
168
+ #
169
+ # @param releases [Array<ModuleRelease>] a list of potential releases
170
+ # @return [Array<ModuleRelease>] releases open for consideration
171
+ def preferred_releases(releases)
172
+ satisfied = releases.select { |x| x.satisfied? }
173
+
174
+ if satisfied.any? { |x| x.version.stable? }
175
+ return satisfied.select { |x| x.version.stable? }
176
+ else
177
+ return satisfied
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,60 @@
1
+ require 'semantic_puppet/dependency'
2
+
3
+ module SemanticPuppet
4
+ module Dependency
5
+ class Graph
6
+ include GraphNode
7
+
8
+ attr_reader :modules
9
+
10
+ # Create a new instance of a dependency graph.
11
+ #
12
+ # @param modules [{String => VersionRange}] the required module
13
+ # set and their version constraints
14
+ def initialize(modules = {})
15
+ @modules = modules.keys
16
+
17
+ modules.each do |name, range|
18
+ add_constraint('initialize', name, range.to_s) do |node|
19
+ range === node.version
20
+ end
21
+
22
+ add_dependency(name)
23
+ end
24
+ end
25
+
26
+ # Constrains graph solutions based on the given block. Graph constraints
27
+ # are used to describe fundamental truths about the tooling or module
28
+ # system (e.g.: module names contain a namespace component which is
29
+ # dropped during install, so module names must be unique excluding the
30
+ # namespace).
31
+ #
32
+ # @example Ensuring a single source for all modules
33
+ # @graph.add_constraint('installed', mod.name) do |nodes|
34
+ # nodes.count { |node| node.source } == 1
35
+ # end
36
+ #
37
+ # @see #considering_solution?
38
+ #
39
+ # @param source [String, Symbol] a name describing the source of the
40
+ # constraint
41
+ # @yieldparam nodes [Array<GraphNode>] the nodes to test the constraint
42
+ # against
43
+ # @yieldreturn [Boolean] whether the node passed the constraint
44
+ # @return [void]
45
+ def add_graph_constraint(source, &block)
46
+ constraints[:graph] << [ source, block ]
47
+ end
48
+
49
+ # Checks the proposed solution (or partial solution) against the graph's
50
+ # constraints.
51
+ #
52
+ # @see #add_graph_constraint
53
+ #
54
+ # @return [Boolean] true if none of the graph constraints are violated
55
+ def satisfies_graph?(solution)
56
+ constraints[:graph].all? { |_, check| check[solution] }
57
+ end
58
+ end
59
+ end
60
+ end