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.
Files changed (41) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +14 -0
  5. data/Guardfile +24 -0
  6. data/LICENSE +22 -0
  7. data/README.md +118 -0
  8. data/Rakefile +2 -0
  9. data/bin/grandprix +31 -0
  10. data/doc/sample/alongside_elements/elements +4 -0
  11. data/doc/sample/alongside_elements/sample_output +6 -0
  12. data/doc/sample/alongside_elements/topology.yml +11 -0
  13. data/doc/sample/alongside_elements_2/elements +4 -0
  14. data/doc/sample/alongside_elements_2/sample_output +6 -0
  15. data/doc/sample/alongside_elements_2/topology.yml +11 -0
  16. data/doc/sample/annotated_topology/elements +4 -0
  17. data/doc/sample/annotated_topology/sample_output +3 -0
  18. data/doc/sample/annotated_topology/topology.yml +14 -0
  19. data/doc/sample/as_a_library/Gemfile +1 -0
  20. data/doc/sample/as_a_library/sample.rb +29 -0
  21. data/doc/sample/elements_with_extra_info/elements +4 -0
  22. data/doc/sample/elements_with_extra_info/sample_output +3 -0
  23. data/doc/sample/elements_with_extra_info/topology.yml +8 -0
  24. data/doc/sample/simple/elements +4 -0
  25. data/doc/sample/simple/sample_output +4 -0
  26. data/doc/sample/simple/topology.yml +9 -0
  27. data/grandprix.gemspec +17 -0
  28. data/lib/grandprix.rb +9 -0
  29. data/lib/grandprix/elements.rb +98 -0
  30. data/lib/grandprix/graph.rb +105 -0
  31. data/lib/grandprix/planner.rb +62 -0
  32. data/lib/grandprix/runner.rb +6 -0
  33. data/lib/grandprix/version.rb +3 -0
  34. data/spec/lib/grandprix/elements_spec.rb +71 -0
  35. data/spec/lib/grandprix/graph_spec.rb +93 -0
  36. data/spec/lib/grandprix/planner_spec.rb +158 -0
  37. data/spec/lib/grandprix/runner_spec.rb +75 -0
  38. data/spec/matchers_spec.rb +32 -0
  39. data/spec/spec_helper.rb +88 -0
  40. data/version +1 -0
  41. metadata +92 -0
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+ *.swp
18
+ *.swo
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
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
@@ -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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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,4 @@
1
+ frontend=v1
2
+ client=v3
3
+ backend=v5
4
+
@@ -0,0 +1,6 @@
1
+ external_backend=v5
2
+ backend=v5
3
+ client=v3
4
+ assets=v1
5
+ frontend=v1
6
+ images=v1
@@ -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,4 @@
1
+ frontend=v1
2
+ client=v3
3
+ backend=v5
4
+
@@ -0,0 +1,6 @@
1
+ external_backend=v5
2
+ backend=v5
3
+ client=v3
4
+ assets=v1
5
+ frontend=v1
6
+ images=v1
@@ -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,4 @@
1
+ frontend=1.0.0
2
+ images=2.0.3
3
+ backend=4.0.0
4
+
@@ -0,0 +1,3 @@
1
+ images=2.0.3={"recipe":"image-server","script":"install-images"}
2
+ backend=4.0.0=["This","And that"]
3
+ frontend=1.0.0=company-frontend-script
@@ -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}"
@@ -0,0 +1,4 @@
1
+ frontend=1.0.0
2
+ client=2.0.3
3
+ backend=4.0.0
4
+
@@ -0,0 +1,3 @@
1
+ backend=4.0.0
2
+ frontend=1.0.0
3
+ client=2.0.3
@@ -0,0 +1,8 @@
1
+ frontend:
2
+ after: [backend, assets]
3
+
4
+ backend:
5
+ after: [db]
6
+
7
+ client:
8
+ after: [frontend]
@@ -0,0 +1,4 @@
1
+ backend
2
+ client
3
+ frontend_A
4
+ frontend_B
@@ -0,0 +1,4 @@
1
+ backend
2
+ frontend_A
3
+ frontend_B
4
+ client
@@ -0,0 +1,9 @@
1
+ client:
2
+ after: [frontend_A, frontend_B]
3
+
4
+ frontend_A:
5
+ after: [backend]
6
+
7
+ frontend_B:
8
+ after: [backend]
9
+
@@ -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
@@ -0,0 +1,9 @@
1
+ module Grandprix
2
+ # Your code goes here...
3
+ end
4
+
5
+ require 'json'
6
+ require 'grandprix/elements'
7
+ require 'grandprix/graph'
8
+ require 'grandprix/planner'
9
+ require 'grandprix/runner'
@@ -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