grandprix 0.0.4
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 +18 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +14 -0
- data/Guardfile +24 -0
- data/LICENSE +22 -0
- data/README.md +118 -0
- data/Rakefile +2 -0
- data/bin/grandprix +31 -0
- data/doc/sample/alongside_elements/elements +4 -0
- data/doc/sample/alongside_elements/sample_output +6 -0
- data/doc/sample/alongside_elements/topology.yml +11 -0
- data/doc/sample/alongside_elements_2/elements +4 -0
- data/doc/sample/alongside_elements_2/sample_output +6 -0
- data/doc/sample/alongside_elements_2/topology.yml +11 -0
- data/doc/sample/annotated_topology/elements +4 -0
- data/doc/sample/annotated_topology/sample_output +3 -0
- data/doc/sample/annotated_topology/topology.yml +14 -0
- data/doc/sample/as_a_library/Gemfile +1 -0
- data/doc/sample/as_a_library/sample.rb +29 -0
- data/doc/sample/elements_with_extra_info/elements +4 -0
- data/doc/sample/elements_with_extra_info/sample_output +3 -0
- data/doc/sample/elements_with_extra_info/topology.yml +8 -0
- data/doc/sample/simple/elements +4 -0
- data/doc/sample/simple/sample_output +4 -0
- data/doc/sample/simple/topology.yml +9 -0
- data/grandprix.gemspec +17 -0
- data/lib/grandprix.rb +9 -0
- data/lib/grandprix/elements.rb +98 -0
- data/lib/grandprix/graph.rb +105 -0
- data/lib/grandprix/planner.rb +62 -0
- data/lib/grandprix/runner.rb +6 -0
- data/lib/grandprix/version.rb +3 -0
- data/spec/lib/grandprix/elements_spec.rb +71 -0
- data/spec/lib/grandprix/graph_spec.rb +93 -0
- data/spec/lib/grandprix/planner_spec.rb +158 -0
- data/spec/lib/grandprix/runner_spec.rb +75 -0
- data/spec/matchers_spec.rb +32 -0
- data/spec/spec_helper.rb +88 -0
- data/version +1 -0
- metadata +92 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@grandprix --create
|
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in grandprix.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem "rspec"
|
8
|
+
end
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem "rb-inotify", :require => false #Notify guard of file changes
|
12
|
+
gem "libnotify", :require => false #System notifications integration
|
13
|
+
gem "guard-rspec"
|
14
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Rails example
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
watch('config/routes.rb') { "spec/routing" }
|
15
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
16
|
+
|
17
|
+
# Capybara features specs
|
18
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
|
19
|
+
|
20
|
+
# Turnip features and steps
|
21
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
22
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
23
|
+
end
|
24
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Rafael de F. Ferreira
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# Grandprix
|
2
|
+
|
3
|
+
Grandprix is a small project with the sole function of imposing an ordering for
|
4
|
+
a happens-before relation. It was created to help with deploy orchestration, to
|
5
|
+
sort out which system should be upgraded before another, based on their
|
6
|
+
dependency relationship. For instance, say we have four systems: a backend
|
7
|
+
server, two frontend servers and a client. And we want to deploy new versions of
|
8
|
+
every system.
|
9
|
+
|
10
|
+
```
|
11
|
+
|
12
|
+
----------------
|
13
|
+
---------| frontend_A |<--------
|
14
|
+
v ---------------- |
|
15
|
+
------------- ------------
|
16
|
+
| backend | | client |
|
17
|
+
------------- ------------
|
18
|
+
^ ---------------- |
|
19
|
+
---------| frontend_B |<--------
|
20
|
+
----------------
|
21
|
+
```
|
22
|
+
|
23
|
+
You give grandprix a description of the ordering dependencies and a list of
|
24
|
+
elements:
|
25
|
+
|
26
|
+
```
|
27
|
+
client:
|
28
|
+
after: [frontend_A, frontend_B]
|
29
|
+
|
30
|
+
frontend_A:
|
31
|
+
after: [backend]
|
32
|
+
|
33
|
+
frontend_B:
|
34
|
+
after: [backend]
|
35
|
+
```
|
36
|
+
|
37
|
+
```
|
38
|
+
backend
|
39
|
+
client
|
40
|
+
frontend_A
|
41
|
+
frontend_B
|
42
|
+
```
|
43
|
+
|
44
|
+
And grandprix will output a correct ordering:
|
45
|
+
|
46
|
+
```yaml
|
47
|
+
backend
|
48
|
+
frontend_A
|
49
|
+
frontend_B
|
50
|
+
client
|
51
|
+
```
|
52
|
+
|
53
|
+
We call the description of the dependencies a _topology_.
|
54
|
+
|
55
|
+
## Using from the command line
|
56
|
+
|
57
|
+
### Instalation
|
58
|
+
The project is packaged as a rubygem, just make sure you have Ruby 1.9 installed
|
59
|
+
and run:
|
60
|
+
|
61
|
+
$ gem install grandprix
|
62
|
+
|
63
|
+
### Usage
|
64
|
+
An executable called `grandprix` will be installed, and can be called like this:
|
65
|
+
|
66
|
+
$ topology -t topology_file elements_file
|
67
|
+
|
68
|
+
The elements file argument can be omitted and grandprix will read them from
|
69
|
+
the standard in.
|
70
|
+
|
71
|
+
The `doc/sample/simple` directory has sample topology and elements files.
|
72
|
+
|
73
|
+
|
74
|
+
## Using as a library
|
75
|
+
|
76
|
+
### Instalation
|
77
|
+
Add this line to your application's Gemfile:
|
78
|
+
|
79
|
+
gem 'grandprix'
|
80
|
+
|
81
|
+
And then execute:
|
82
|
+
|
83
|
+
$ bundle
|
84
|
+
|
85
|
+
### Usage
|
86
|
+
|
87
|
+
The programatic API is very similar to the command line tool. Just call the
|
88
|
+
`run!` instance method on the `Grandprix::Runner` class, passing in a hash
|
89
|
+
representing the topology and an array of elements.
|
90
|
+
|
91
|
+
The `doc/sample/as_a_library` directory has an example ruby script calling
|
92
|
+
grandprix programatically.
|
93
|
+
|
94
|
+
|
95
|
+
## More
|
96
|
+
|
97
|
+
Grandprix offers a few more conveniences, illustrated on the `doc/sample`
|
98
|
+
directories:
|
99
|
+
|
100
|
+
* Elements can contain extra data that will carry over to the output, such as
|
101
|
+
version numbers. Just append an equals sign and the info you want to each
|
102
|
+
element. Check out the `elements_with_extra_info` directory.
|
103
|
+
* The topology itself can define information that will propagate to the
|
104
|
+
output, in the form of an `annotation` attribute. See the `annotated_topology`
|
105
|
+
directory.
|
106
|
+
* Besides declaring that an element needs to come after others, grandprix also
|
107
|
+
allows for an element to specify that it must always be accompanied by
|
108
|
+
another, via the `alongside` attribute. This will extend the provided elements
|
109
|
+
input. See the `alongside_elements` and `alongside_elements_2` directories.
|
110
|
+
|
111
|
+
|
112
|
+
## Contributing
|
113
|
+
|
114
|
+
1. Fork it
|
115
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
116
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
117
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
118
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/grandprix
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'grandprix'
|
3
|
+
require 'optparse'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
def usage(options, message=nil)
|
7
|
+
puts message if message
|
8
|
+
puts "usage: topology -t topology_file elements_file"
|
9
|
+
puts " topology -t topology_file"
|
10
|
+
puts
|
11
|
+
puts " Read elements file from a named file or standard in"
|
12
|
+
puts
|
13
|
+
puts options
|
14
|
+
exit (message.nil? ? 1 : 0)
|
15
|
+
end
|
16
|
+
|
17
|
+
options = OptionParser.new do |o|
|
18
|
+
o.on("-t", "--topology FILENAME", "path to the topology YAML file") do |filename|
|
19
|
+
$topology_file = filename
|
20
|
+
end
|
21
|
+
o.on('-h') { usage(o); exit }
|
22
|
+
o.parse!
|
23
|
+
end
|
24
|
+
|
25
|
+
topology = YAML.load_file($topology_file) rescue usage(options, "invalid topology file")
|
26
|
+
|
27
|
+
elements_input = ARGF.read.chomp
|
28
|
+
elements = elements_input.lines.map(&:chomp).to_a
|
29
|
+
|
30
|
+
output = Grandprix::Runner.new.run! topology, elements
|
31
|
+
STDOUT.puts output.strings.join("\n")
|
@@ -0,0 +1,11 @@
|
|
1
|
+
frontend:
|
2
|
+
after: [backend]
|
3
|
+
alongside: [assets, images] # Frontend is always accompanied
|
4
|
+
# by assets and images
|
5
|
+
|
6
|
+
backend:
|
7
|
+
alongside: [external_backend]
|
8
|
+
|
9
|
+
images:
|
10
|
+
after: [client] #images that is an alongside dep of frontend
|
11
|
+
# can declare itself as after client
|
@@ -0,0 +1,11 @@
|
|
1
|
+
frontend:
|
2
|
+
after: [backend, assets] # frontend depends on assets in addition to
|
3
|
+
alongside: [assets, images] # having it declared alongside
|
4
|
+
|
5
|
+
backend:
|
6
|
+
alongside: [external_backend]
|
7
|
+
|
8
|
+
images:
|
9
|
+
after: [client, frontend] # Images must come before frontend, which
|
10
|
+
# is perfectly compatible with it being
|
11
|
+
# declared as alongside frontend
|
@@ -0,0 +1,14 @@
|
|
1
|
+
frontend:
|
2
|
+
after: [backend, images]
|
3
|
+
annotation: company-frontend-script #Simple string annotation
|
4
|
+
|
5
|
+
images:
|
6
|
+
annotation: #Complex annotation will serialize as JSON
|
7
|
+
recipe: image-server
|
8
|
+
script: install-images
|
9
|
+
|
10
|
+
backend:
|
11
|
+
after: [db]
|
12
|
+
annotation: #Will also serialize as JSON, an array in this case.
|
13
|
+
- This
|
14
|
+
- And that
|
@@ -0,0 +1 @@
|
|
1
|
+
gem 'grandprix'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'grandprix'
|
4
|
+
|
5
|
+
topology = {
|
6
|
+
"frontend" => {
|
7
|
+
"after" => ["backend"]
|
8
|
+
},
|
9
|
+
"backend" => {
|
10
|
+
"after" => ["db", "mq"],
|
11
|
+
"annotation" => {
|
12
|
+
"deploy-type" => "rolling",
|
13
|
+
}
|
14
|
+
},
|
15
|
+
"client" => {
|
16
|
+
"after" => ["frontend"],
|
17
|
+
"annotation" => {
|
18
|
+
"deploy-type" => "restart",
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
elements = ["frontend=v1.0.0", "db=v2.0.1", "client=v5.43.4"]
|
24
|
+
|
25
|
+
output_elements = Grandprix::Runner.new.run! topology, elements
|
26
|
+
|
27
|
+
puts "Just the names are:\n#{output_elements.names.inspect}"
|
28
|
+
|
29
|
+
puts "The full elements are:\n#{output_elements.strings.inspect}"
|
data/grandprix.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/grandprix/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Rafael de F. Ferreira"]
|
6
|
+
gem.email = ["public@rafaelferreira.net"]
|
7
|
+
gem.description = %q{Deploy assistant.}
|
8
|
+
gem.summary = %q{Deploy assistant}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "grandprix"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = File.read("version").chomp
|
17
|
+
end
|
data/lib/grandprix.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
class Grandprix::Elements
|
2
|
+
def self.build array
|
3
|
+
elements = array.map {|element| element.split '=' }
|
4
|
+
self.new(elements)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(array)
|
8
|
+
@array = array
|
9
|
+
end
|
10
|
+
|
11
|
+
#other is also an Elements object
|
12
|
+
def +(other)
|
13
|
+
self.class.new(@array + other.underlying)
|
14
|
+
end
|
15
|
+
|
16
|
+
#except is an array of names
|
17
|
+
def except(other)
|
18
|
+
self.class.new(@array.reject {|e| other.include? e.first})
|
19
|
+
end
|
20
|
+
|
21
|
+
# ordering is an array of names
|
22
|
+
def reorder(ordering)
|
23
|
+
names_in_order = ordering & names
|
24
|
+
new_array = names_in_order.map do |name|
|
25
|
+
find(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
self.class.new(new_array)
|
29
|
+
end
|
30
|
+
|
31
|
+
# extra is a hash from names to array of names
|
32
|
+
def alongside(extra)
|
33
|
+
extra_pairs = extra.flat_map do |origin_name, destination_names|
|
34
|
+
local_pair = find(origin_name)
|
35
|
+
if local_pair and local_pair.size == 2
|
36
|
+
extra = local_pair[1]
|
37
|
+
destination_names.map {|d| [d, extra] }
|
38
|
+
elsif local_pair
|
39
|
+
destination_names.map {|d| [d] }
|
40
|
+
else
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#self + extra_elements
|
46
|
+
self.class.new(@array + extra_pairs)
|
47
|
+
end
|
48
|
+
|
49
|
+
# annotations is a hash of names to metadata
|
50
|
+
def annotate(annotations)
|
51
|
+
def value_component(value)
|
52
|
+
return nil if value.nil?
|
53
|
+
return value if value.is_a? String
|
54
|
+
return JSON.dump(value)
|
55
|
+
end
|
56
|
+
|
57
|
+
new_elements = underlying.map do |element|
|
58
|
+
name = element.first
|
59
|
+
find(name).clone.tap do |row|
|
60
|
+
annotation = value_component annotations[name]
|
61
|
+
row[2] = annotation unless annotation.nil?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
self.class.new new_elements
|
66
|
+
end
|
67
|
+
|
68
|
+
def strings
|
69
|
+
underlying.map {|e| e.join("=") }
|
70
|
+
end
|
71
|
+
|
72
|
+
def names
|
73
|
+
@array.map &:first
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_a
|
77
|
+
names
|
78
|
+
end
|
79
|
+
|
80
|
+
def ==(other)
|
81
|
+
return false unless other.respond_to?(:underlying)
|
82
|
+
self.underlying == other.underlying
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
"<Elements #{self.strings.inspect}>"
|
87
|
+
end
|
88
|
+
|
89
|
+
def underlying
|
90
|
+
@array
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
def find(name)
|
95
|
+
@array.find {|e| e.first == name}
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|