semantic_puppet 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.
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