algo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bb278f86fccf497cb5ece005a59c45ac56929fef
4
+ data.tar.gz: 8cf7c48ff992326edf08c6500cb48de73e8be902
5
+ SHA512:
6
+ metadata.gz: 4b3a4d6561a8adda98ad72b85e5b6e22896e5b8fad5f719adc81b9702b5b7bbcd416b50d9c04e02516cc1bb5aad81b4493d0533d82927875a9babb2f797c646c
7
+ data.tar.gz: d54c9d0a2d304a8fbf8fa635f988241ba2f4b925b0c787034d93cbbc2413368287470041a2b6fcaf4a44aecd78b8006fe12aa3f1610947599860a166abbd379e
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in algo.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Algo
2
+ Docker container orchestration tool for swarm cluster.
3
+
4
+ ## Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'algo'
10
+ ```
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install algo
19
+
20
+
21
+ ## Usage
22
+
23
+ ### Definition
24
+
25
+ ```rb
26
+ cluster 'awesomecluster' do
27
+
28
+ # Define service/network prefix for cluster
29
+ prefix 'awsm'
30
+
31
+ # Define cluster wide available environment variable
32
+ env 'CLUSTER_ENV', 'PRODUCTION'
33
+
34
+ # Define cluster wide available label
35
+ label 'com.example.sample', 'clusterwidelabel'
36
+
37
+ # Define network
38
+ network 'net1'
39
+
40
+ # Define service
41
+ service 'name' do
42
+ image 'quay.io/yss44/curl'
43
+ replicas 3
44
+ command 'sh'
45
+ args '-ic', "while true; do curl -s awsm-nginx > /dev/null; echo $?; sleep 3; done"
46
+
47
+ update_parallelism 2
48
+
49
+ # Service related environment variable
50
+ env 'APP_DOMAIN', 'example.com'
51
+
52
+ network 'net1'
53
+ end
54
+
55
+ # Define another service
56
+ service 'nginx' do
57
+ image 'nginx:alpine'
58
+ replicas 2
59
+ network 'net1'
60
+ end
61
+
62
+ end
63
+ ```
64
+
65
+ ### Execution
66
+
67
+ ```sh
68
+ # Prepare playground for algo
69
+ docker-machine create --driver virtualbox \
70
+ --virtualbox-boot2docker-url="https://github.com/boot2docker/boot2docker/releases/download/v1.12.0-rc4/boot2docker-experimental.iso" \
71
+ algo
72
+ eval $(docker-machine env algo)
73
+
74
+ # Create initial cluster
75
+ algo apply examples/awesomecluster.rb
76
+ # Applying to cluster awesomecluster...
77
+ # network: awsm-net1, status: created
78
+ # service: awsm-name, status: created
79
+ # service: awsm-nginx, status: created
80
+ # Complete applying for cluster awesomecluster!
81
+
82
+ # Change configuration
83
+ sed -i s/replicas 2/replicas 1/g examples/awesomecluster.rb
84
+
85
+ # Dry-run
86
+ algo apply examples/awesomecluster.rb --dry-run
87
+ # Running with dry-run mode...
88
+ # Applying to cluster awesomecluster...
89
+ # network: awsm-net1, status: ok
90
+ # service: awsm-name, status: ok
91
+ # service: awsm-nginx, status: changed
92
+ # Complete applying for cluster awesomecluster!
93
+
94
+ # Apply changes
95
+ algo apply examples/awesomecluster.rb --dry-run
96
+ # Applying to cluster awesomecluster...
97
+ # network: awsm-net1, status: ok
98
+ # service: awsm-name, status: ok
99
+ # service: awsm-nginx, status: changed
100
+ # Complete applying for cluster awesomecluster!
101
+
102
+ # Dry-run terminating cluster
103
+ algo apply examples/awesomecluster.rb --dry-run
104
+ # Running with dry-run mode...
105
+ # Terminating cluster awesomecluster...
106
+ # service: awsm-name, status: removed
107
+ # service: awsm-nginx, status: removed
108
+ # network: awsm-net1, status: removed
109
+ # Complete Termination for cluster awesomecluster...
110
+
111
+ # Terminate cluster
112
+ algo apply examples/awesomecluster.rb
113
+ # Terminating cluster awesomecluster...
114
+ # service: awsm-name, status: removed
115
+ # service: awsm-nginx, status: removed
116
+ # network: awsm-net1, status: removed
117
+ # Complete Termination for cluster awesomecluster...
118
+ ```
119
+
120
+ ## Development
121
+
122
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
123
+
124
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
125
+
126
+ ## Contributing
127
+
128
+ 1. Fork it ( https://github.com/yoshiso/algo/fork )
129
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
130
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
131
+ 4. Push to the branch (`git push origin my-new-feature`)
132
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/algo.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'algo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "algo"
8
+ spec.version = Algo::VERSION
9
+ spec.authors = ["yoshiso"]
10
+ spec.email = ["nya060@gmail.com"]
11
+
12
+ spec.summary = %q{Docker container orchestration tool for swarm cluster.}
13
+ spec.description = %q{Docker container orchestration tool for swarm cluster.}
14
+ spec.homepage = "https://github.com/yoshiso/algo"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency 'activesupport', '~> 4.0'
23
+ spec.add_dependency 'excon', '0.51.0'
24
+ spec.add_dependency 'thor'
25
+ spec.add_development_dependency "bundler", "~> 1.12"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "pry", "~> 0.10"
29
+ end
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "algo"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ cluster 'awesomecluster' do
2
+
3
+ # Define service/network prefix for cluster
4
+ prefix 'awsm'
5
+
6
+ # Define cluster wide available environment variable
7
+ env 'CLUSTER_ENV', 'PRODUCTION'
8
+
9
+ # Define cluster wide available label
10
+ label 'com.example.sample', 'clusterwidelabel'
11
+
12
+ # Define network
13
+ network 'net1'
14
+
15
+ # Define service
16
+ service 'name' do
17
+ image 'quay.io/yss44/curl'
18
+ replicas 3
19
+ command 'sh'
20
+ args '-ic', "while true; do curl -s awsm-nginx > /dev/null; echo $?; sleep 3; done"
21
+
22
+ update_parallelism 2
23
+
24
+ env 'APP_DOMAIN', 'example.com'
25
+
26
+ network 'net1'
27
+ end
28
+
29
+ # Define another service
30
+ service 'nginx' do
31
+ image 'nginx:alpine'
32
+ replicas 2
33
+ network 'net1'
34
+ end
35
+
36
+ end
data/exe/algo ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "algo"
5
+
6
+ Algo::Cli.start
data/lib/algo/cli.rb ADDED
@@ -0,0 +1,155 @@
1
+ module Algo
2
+ class Cli < Thor
3
+ class ValidationError < StandardError; end
4
+
5
+ class ServiceValidator
6
+ def initialize srv_spec
7
+ @srv_spec = srv_spec
8
+ end
9
+
10
+ def validate
11
+ begin
12
+ @srv = Algo::Docker::Service.find(@srv_spec['Name'])
13
+ rescue Algo::Docker::Error::NotFoundError
14
+ @srv = nil
15
+ end
16
+ check_networks
17
+ end
18
+
19
+ def self.validate srv_spec
20
+ new(srv_spec).validate
21
+ end
22
+
23
+ private
24
+
25
+ def check_networks
26
+ return true if @srv.blank? or @srv.spec.networks.blank?
27
+ srv_networks = @srv.spec.networks.map { |n| { 'Target' => n.info['Name'] } }
28
+ unless srv_networks != @srv_spec['Networks']
29
+ @srv_spec['Networks'] = @srv.spec.networks.map { |n| { 'Target' => n.info['Id'] } }
30
+ return true
31
+ end
32
+ raise ValidationError, 'changing network in service is not supported'
33
+ end
34
+ end
35
+
36
+ class ServiceUpdator
37
+
38
+ def initialize srv_spec, options
39
+ @srv_spec = srv_spec
40
+ @options = options
41
+ end
42
+
43
+ def update
44
+ begin
45
+ srv = Algo::Docker::Service.find(@srv_spec['Name'])
46
+ if srv.raw_spec == @srv_spec
47
+ puts "service: #{@srv_spec['Name']}, status: ok"
48
+ return
49
+ end
50
+ srv.update @srv_spec unless dryrun?
51
+ puts "service: #{@srv_spec['Name']}, status: changed"
52
+ rescue Algo::Docker::Error::NotFoundError
53
+ Algo::Docker::Service.create(@srv_spec) unless dryrun?
54
+ puts "service: #{@srv_spec['Name']}, status: created"
55
+ end
56
+ end
57
+
58
+ def self.update srv_spec, dryrun=false
59
+ new(srv_spec, {dryrun: dryrun}).update
60
+ end
61
+
62
+ private
63
+
64
+ def dryrun?
65
+ @options[:dryrun]
66
+ end
67
+ end
68
+
69
+ desc 'apply [INVENTRY_FILE]', 'Apply configuration to clusters'
70
+ option :'dry-run', type: :boolean, default: false
71
+ option :'url', type: :string, desc: 'docker swarm url like tcp://localhost:2375'
72
+ option :'client_key', type: :string, desc: 'docker swarm client key path'
73
+ option :'client_sert', type: :string, desc: 'docker swarm client sert path'
74
+ option :'ssl_ca_file', type: :string, desc: 'docker swarm ssl ca file path'
75
+ option :'scheme', type: :string, desc: 'docker swarm connection scheme'
76
+ def apply inventry
77
+ Algo::Docker.url = options[:host] if options[:host]
78
+ Algo::Docker.options = docker_opts if docker_opts.present?
79
+ puts 'Running with dry-run mode...' if options[:'dry-run']
80
+ configuration = Algo::Dsl.load({}, inventry)
81
+ configuration.each do |cluster|
82
+ puts "Applying to cluster #{cluster['name']}..."
83
+
84
+ cluster['networks'].each do |net_spec|
85
+ begin
86
+ net = Algo::Docker::Network.find net_spec['Name']
87
+ puts "network: #{net_spec['Name']}, status: ok"
88
+ rescue Algo::Docker::Error::NotFoundError
89
+ Algo::Docker::Network.create net_spec unless options[:'dry-run']
90
+ puts "network: #{net_spec['Name']}, status: created"
91
+ end
92
+ end
93
+
94
+ cluster['services'].each do |srv_spec|
95
+ ServiceValidator.validate srv_spec
96
+ end
97
+ cluster['services'].each do |srv_spec|
98
+ ServiceUpdator.update srv_spec, options[:'dry-run']
99
+ end
100
+ Algo::Docker::Service.all
101
+ .select { |srv| srv.spec.name.start_with?("#{cluster['prefix']}-") }
102
+ .select { |srv| ! srv.spec.name.in? cluster['services'].map { |spec| spec['Name'] } }
103
+ .map { |srv|
104
+ srv_name = srv.spec.name
105
+ srv.remove unless options[:'dry-run']
106
+ puts "service: #{srv_name}, status: removed"
107
+ }
108
+ Algo::Docker::Network.all(skip_default=true)
109
+ .select { |net| net.info['Name'].start_with?("#{cluster['prefix']}-") }
110
+ .select { |net| ! net.info['Name'].in? cluster['networks'].map { |net_spec| net_spec['Name'] } }
111
+ .map { |net|
112
+ net_name = net.info['Name']
113
+ net.remove unless options[:'dry-run']
114
+ puts "network: #{net_name}, status: removed"
115
+ }
116
+ puts "Complete applying for cluster #{cluster['name']}!"
117
+ end
118
+ rescue ValidationError => e
119
+ puts 'configuration validation failed because ' + e.message
120
+ end
121
+
122
+ desc 'rm [INVENTRY_FILE]', 'Terminate clusters'
123
+ option :'dry-run', type: :boolean, default: false
124
+ def rm inventry
125
+ puts 'Running with dry-run mode...' if options[:'dry-run']
126
+ configuration = Algo::Dsl.load({}, inventry)
127
+ configuration.each do |cluster|
128
+ puts "Terminating cluster #{cluster['name']}..."
129
+ Algo::Docker::Service.all
130
+ .select { |srv| srv.spec.name.start_with?("#{cluster['prefix']}-") }
131
+ .map { |srv|
132
+ srv_name = srv.spec.name
133
+ srv.remove unless options[:'dry-run']
134
+ puts "service: #{srv_name}, status: removed"
135
+ }
136
+ Algo::Docker::Network.all(skip_default=true)
137
+ .select { |net| net.info['Name'].start_with?("#{cluster['prefix']}-") }
138
+ .select { |net| ! net.info['Name'].in? cluster['networks'].map { |net_spec| "#{cluster['prefix']}-#{net_spec['Name']}" } }
139
+ .map { |net|
140
+ net_name = net.info['Name']
141
+ net.remove unless options[:'dry-run']
142
+ puts "network: #{net_name}, status: removed"
143
+ }
144
+ puts "Complete Termination for cluster #{cluster['name']}..."
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def docker_opts
151
+ options.slice(:client_key, :client_sert, :ssl_ca_file, :scheme)
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,26 @@
1
+ module Algo
2
+ module Docker
3
+ class Base
4
+ include Docker::Error
5
+
6
+ attr_reader :id, :info
7
+
8
+ def initialize(connection, hash={})
9
+ unless connection.is_a?(Docker::Connection)
10
+ raise ArgumentError, "Expected a Docker::Connection, got: #{connection}."
11
+ end
12
+ normalize_hash(hash)
13
+ @connection, @info, @id = connection, hash, hash['Id']
14
+ raise ArgumentError, "Must have id, got: #{hash}" unless @id
15
+ end
16
+
17
+ private
18
+ attr_accessor :connection
19
+
20
+ def normalize_hash(hash)
21
+ hash["Id"] ||= hash.delete("ID")
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,98 @@
1
+ module Algo
2
+ module Docker
3
+ class Connection
4
+ include Docker::Error
5
+ attr_reader :url, :options
6
+
7
+ # Create a new Connection. This method takes a url (String) and options
8
+ # (Hash). These are passed to Excon, so any options valid for `Excon.new`
9
+ # can be passed here.
10
+ def initialize(url, opts)
11
+ case
12
+ when !url.is_a?(String)
13
+ raise ArgumentError, "Expected a String, got: '#{url}'"
14
+ when !opts.is_a?(Hash)
15
+ raise ArgumentError, "Expected a Hash, got: '#{opts}'"
16
+ else
17
+ uri = URI.parse(url)
18
+ if uri.scheme == "unix"
19
+ @url, @options = 'unix:///', {:socket => uri.path}.merge(opts)
20
+ elsif uri.scheme =~ /^(https?|tcp)$/
21
+ @url, @options = url, opts
22
+ else
23
+ @url, @options = "http://#{uri}", opts
24
+ end
25
+ end
26
+ end
27
+
28
+ # The actual client that sends HTTP methods to the Docker server. This value
29
+ # is not cached, since doing so may cause socket errors after bad requests.
30
+ def resource
31
+ Excon.new(url, options)
32
+ end
33
+ private :resource
34
+
35
+ # Send a request to the server with the `
36
+ def request(*args, &block)
37
+ request = compile_request_params(*args, &block)
38
+ # log_request(request)
39
+ response = resource.request(request).body
40
+ JSON.parse(response) unless response.blank?
41
+ rescue Excon::Errors::BadRequest => ex
42
+ raise ClientError, ex.response.body
43
+ rescue Excon::Errors::Unauthorized => ex
44
+ raise UnauthorizedError, ex.response.body
45
+ rescue Excon::Errors::NotFound => ex
46
+ raise NotFoundError, ex.response.body
47
+ rescue Excon::Errors::Conflict => ex
48
+ raise ConflictError, ex.response.body
49
+ rescue Excon::Errors::InternalServerError => ex
50
+ raise ServerError, ex.response.body
51
+ rescue Excon::Errors::Timeout => ex
52
+ raise TimeoutError, ex.message
53
+ end
54
+
55
+ # def log_request(request)
56
+ # if Docker.logger
57
+ # Docker.logger.debug(
58
+ # [request[:method], request[:path], request[:query], request[:body]]
59
+ # )
60
+ # end
61
+ # end
62
+
63
+ # Delegate all HTTP methods to the #request.
64
+ [:get, :put, :post, :delete].each do |method|
65
+ define_method(method) { |*args, &block| request(method, *args, &block) }
66
+ end
67
+
68
+ def to_s
69
+ "Docker::Connection { :url => #{url}, :options => #{options} }"
70
+ end
71
+
72
+ private
73
+ # Given an HTTP method, path, optional query, extra options, and block,
74
+ # compiles a request.
75
+ def compile_request_params(http_method, path, query = nil, opts = nil, &block)
76
+ query ||= {}
77
+ opts ||= {}
78
+ headers = opts.delete(:headers) || {}
79
+ content_type = opts[:body].nil? ? 'text/plain' : 'application/json'
80
+ user_agent = "github.com:yoshiso/algo v#{Algo::VERSION}"
81
+ {
82
+ :method => http_method,
83
+ :path => "/v#{Docker::API_VERSION}#{path}",
84
+ :query => query,
85
+ :headers => { 'Content-Type' => content_type,
86
+ 'User-Agent' => user_agent,
87
+ }.merge(headers),
88
+ :expects => (200..204).to_a << 304,
89
+ :idempotent => http_method == :get,
90
+ :request_block => block
91
+ }.merge(opts).reject { |_, v| v.nil? }
92
+ end
93
+
94
+
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,44 @@
1
+ # This module holds the Errors for the gem.
2
+ module Algo
3
+ module Docker
4
+ module Error
5
+
6
+ # The default error. It's never actually raised, but can be used to catch all
7
+ # gem-specific errors that are thrown as they all subclass from this.
8
+ class DockerError < StandardError; end
9
+
10
+ # Raised when invalid arguments are passed to a method.
11
+ class ArgumentError < DockerError; end
12
+
13
+ # Raised when a request returns a 400.
14
+ class ClientError < DockerError; end
15
+
16
+ # Raised when a request returns a 401.
17
+ class UnauthorizedError < DockerError; end
18
+
19
+ # Raised when a request returns a 404.
20
+ class NotFoundError < DockerError; end
21
+
22
+ # Raised when a request returns a 409.
23
+ class ConflictError < DockerError; end
24
+
25
+ # Raised when a request returns a 500.
26
+ class ServerError < DockerError; end
27
+
28
+ # Raised when there is an unexpected response code / body.
29
+ class UnexpectedResponseError < DockerError; end
30
+
31
+ # Raised when there is an incompatible version of Docker.
32
+ class VersionError < DockerError; end
33
+
34
+ # Raised when a request times out.
35
+ class TimeoutError < DockerError; end
36
+
37
+ # Raised when login fails.
38
+ class AuthenticationError < DockerError; end
39
+
40
+ # Raised when an IO action fails.
41
+ class IOError < DockerError; end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ module Algo
2
+ module Docker
3
+ class Network < Base
4
+ DEFAULT_NETWORKS = %w(ingress none host bridge docker_gwbridge)
5
+
6
+ def inspect
7
+ "<Algo::Docker::Network name=#{info['Name']} scope=#{info['Scope']}>"
8
+ end
9
+
10
+ def info
11
+ @info = self.class.find(@info["Id"]).info unless @info["Name"]
12
+ @info
13
+ end
14
+
15
+ def to_h
16
+ @info
17
+ end
18
+
19
+ def remove
20
+ self.class.remove @info['Id']
21
+ end
22
+
23
+ def self.find(id, conn=Docker.connection)
24
+ new(conn, conn.get("/networks/#{id}"))
25
+ end
26
+
27
+ def self.remove(id_or_name, conn=Docker.connection)
28
+ conn.delete("/networks/#{id_or_name}")
29
+ end
30
+
31
+ def self.create(init_spec, conn=Docker.connection)
32
+ new(conn, conn.post("/networks/create", nil, body: JSON.generate(init_spec)))
33
+ end
34
+
35
+ def self.all(skip_default=false, conn=Docker.connection)
36
+ hashes = conn.get('/networks')
37
+ hashes.select { |h| !skip_default || !h['Name'].in?(DEFAULT_NETWORKS) }
38
+ .map{ |h| new(conn, h) }
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,84 @@
1
+ module Algo
2
+
3
+ module Docker
4
+
5
+ class Service < Base
6
+
7
+ Spec = Struct.new('Spec', :name, :task_template, :mode, :update_config,
8
+ :networks, :endpoint_spec)
9
+
10
+ attr_reader :spec
11
+
12
+ def initialize conn, hash
13
+ super(conn, hash)
14
+ @spec = nil
15
+ end
16
+
17
+ def info
18
+ @info = Docker::Service.find(@info["Id"]).info if @info["Spec"].blank?
19
+ @info
20
+ end
21
+
22
+ def spec
23
+ if @spec.blank?
24
+ @spec = Spec.new(
25
+ info["Spec"]["Name"],
26
+ info["Spec"]["TaskTemplate"],
27
+ info["Spec"]["Mode"],
28
+ info["Spec"]["UpdateConfig"],
29
+ info["Spec"]["Networks"].tap { |network|
30
+ unless network.blank?
31
+ break network.map { |net| Network.new(@connection, {'Id' => net['Target']}) }
32
+ end
33
+ },
34
+ info["Spec"]["EndpointSpec"]
35
+ )
36
+ end
37
+ @spec
38
+ end
39
+
40
+ def raw_spec
41
+ info["Spec"]
42
+ end
43
+
44
+ def inspect
45
+ "<Algo::Docker::Service name=#{spec.name}>"
46
+ end
47
+
48
+ def remove
49
+ self.class.remove info['Id']
50
+ end
51
+
52
+ def update next_spec
53
+ self.class.update info['Id'], info['Version']['Index'], next_spec
54
+ @info = self.class.find(@info["Id"]).info
55
+ end
56
+
57
+ def self.find(id_or_name, conn=Docker.connection)
58
+ new(conn, conn.get("/services/#{id_or_name}"))
59
+ end
60
+
61
+ def self.create(init_spec, conn=Docker.connection)
62
+ new(conn, conn.post('/services/create', nil, body: JSON.generate(init_spec)))
63
+ end
64
+
65
+ def self.remove(id_or_name, conn=Docker.connection)
66
+ conn.delete("/services/#{id_or_name}")
67
+ end
68
+
69
+ def self.update(id_or_name, version, next_spec, conn=Docker.connection)
70
+ conn.post("/services/#{id_or_name}/update",
71
+ { version: version },
72
+ body: JSON.generate(next_spec))
73
+ end
74
+
75
+ def self.all(conn=Docker.connection)
76
+ hashes = conn.get('/services')
77
+ hashes.map{ |h| new(conn, h) }
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,5 @@
1
+ module Algo
2
+ module Docker
3
+ API_VERSION = '1.24'
4
+ end
5
+ end
@@ -0,0 +1,63 @@
1
+ module Algo
2
+ module Docker
3
+ require 'algo/docker/version'
4
+ require 'algo/docker/error'
5
+ require 'algo/docker/base'
6
+ require 'algo/docker/connection'
7
+
8
+ # Entities
9
+ require 'algo/docker/network'
10
+ require 'algo/docker/service'
11
+
12
+ def connection
13
+ @connection ||= Connection.new(url, options)
14
+ end
15
+
16
+ def url
17
+ @url || env_url
18
+ end
19
+
20
+ def url=(new_url)
21
+ @url = new_url
22
+ end
23
+
24
+ def options
25
+ @options || env_options
26
+ end
27
+
28
+ def options=(new_options)
29
+ @options = env_options.merge(new_options)
30
+ end
31
+
32
+ def env_url
33
+ ENV['DOCKER_URL'] || ENV['DOCKER_HOST']
34
+ end
35
+
36
+ def env_options
37
+ if cert_path = ENV['DOCKER_CERT_PATH']
38
+ {
39
+ client_cert: File.join(cert_path, 'cert.pem'),
40
+ client_key: File.join(cert_path, 'key.pem'),
41
+ ssl_ca_file: File.join(cert_path, 'ca.pem'),
42
+ scheme: 'https'
43
+ }.merge(ssl_options)
44
+ else
45
+ {}
46
+ end
47
+ end
48
+
49
+ def ssl_options
50
+ if ENV['DOCKER_SSL_VERIFY'] == 'false'
51
+ {
52
+ ssl_verify_peer: false
53
+ }
54
+ else
55
+ {}
56
+ end
57
+ end
58
+
59
+ module_function :env_options, :url, :url=, :options, :options=, :env_url, :ssl_options,
60
+ :connection
61
+
62
+ end
63
+ end
@@ -0,0 +1,47 @@
1
+ module Algo
2
+ class Dsl
3
+ module Cluster
4
+
5
+ class Context
6
+ include Dsl::Network
7
+ include Dsl::Service
8
+
9
+ attr_reader :context
10
+
11
+ def initialize name
12
+ @context = {
13
+ "services" => [],
14
+ "networks" => [],
15
+ "env" => [],
16
+ "labels" => {},
17
+ 'name' => name,
18
+ 'prefix' => name
19
+ }
20
+ end
21
+
22
+ # Assign cluster-wide used prefix.
23
+ # @param [String] pref_name
24
+ def prefix pref_name
25
+ @context['prefix'] = pref_name
26
+ end
27
+
28
+ def env key, val
29
+ @context['env'] << "#{key}=#{val}"
30
+ end
31
+
32
+ def label key, val
33
+ @context['labels'][key] = val
34
+ end
35
+
36
+ end
37
+
38
+ def cluster name, &block
39
+ ctx = Cluster::Context.new(name).tap do |ctx|
40
+ ctx.instance_eval(&block)
41
+ end
42
+ @clusters << ctx.context
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ module Algo
2
+ class Dsl
3
+ module Network
4
+
5
+ class Context
6
+
7
+ attr_reader :context
8
+
9
+ def initialize name, cluster
10
+ @cluster = cluster
11
+ @context = {
12
+ 'Name' => "#{cluster_prefix}#{name}",
13
+ 'Driver' => 'overlay',
14
+ 'CheckDuplicate' => true,
15
+ 'EnableIPv6' => false,
16
+ 'IPAM' => {
17
+ 'Config' => [],
18
+ 'Driver' => 'default',
19
+ 'Options' => {}
20
+ },
21
+ 'Internal' => false,
22
+ 'Labels' => cluster['labels'],
23
+ 'Options' => {}
24
+ }
25
+ end
26
+
27
+ def internal
28
+ @context['Internal'] = true
29
+ end
30
+
31
+ def label key, val
32
+ @context['Labels'][key] = val
33
+ end
34
+
35
+ def ipv6
36
+ @context['EnableIPv6'] = true
37
+ end
38
+
39
+ private
40
+
41
+ def cluster_prefix
42
+ "#{@cluster['prefix']}-" if @cluster['prefix']
43
+ end
44
+
45
+ end
46
+
47
+ def network name, &block
48
+ raise 'should be called in cluster' unless @context
49
+ ctx = Network::Context.new(name, @context).tap do |ctx|
50
+ ctx.instance_eval(&block) if block_given?
51
+ end
52
+ @context['networks'] << ctx.context
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,110 @@
1
+ module Algo
2
+ class Dsl
3
+ module Service
4
+
5
+ class Context
6
+ attr_reader :context
7
+
8
+ def initialize name, cluster
9
+ @cluster = cluster
10
+ @context = {
11
+ 'Name' => "#{cluster_prefix}#{name}",
12
+ 'TaskTemplate' => {
13
+ 'ContainerSpec' => {
14
+ 'Image' => nil
15
+ }
16
+ },
17
+ 'Mode' => {
18
+ 'Replicated' => {
19
+ 'Replicas' => 1
20
+ }
21
+ },
22
+ 'Labels' => @cluster['labels']
23
+ }
24
+ @context['TaskTemplate']['ContainerSpec']['Env'] = @cluster['env'] if @cluster['env'].present?
25
+ end
26
+
27
+ # ContainerSpec
28
+
29
+ def image image_name
30
+ @context['TaskTemplate']['ContainerSpec']['Image'] = image_name
31
+ end
32
+
33
+ def command *item
34
+ @context['TaskTemplate']['ContainerSpec']['Command'] = item
35
+ end
36
+
37
+ def args *items
38
+ @context['TaskTemplate']['ContainerSpec']['Args'] = items
39
+ end
40
+
41
+ def env key, val
42
+ @context['TaskTemplate']['ContainerSpec']['Env'] ||= []
43
+ @context['TaskTemplate']['ContainerSpec']['Env'] << "#{key}=#{val}"
44
+ end
45
+
46
+ # @param [String] period period string like 30s, 1m, 4h
47
+ def stop_grace_period period
48
+ if period.end_with?('s')
49
+ period = period.chomp('s').to_i
50
+ elsif period.end_with?('m')
51
+ period = period.chomp('m').to_i * 60
52
+ elsif period.end_with?('h')
53
+ period = period.chomp('m').to_i * 60 * 60
54
+ else
55
+ raise
56
+ end
57
+ @context['TaskTemplate']['ContainerSpec']['StopGracePeriod'] = period * 1000000000
58
+ end
59
+
60
+ # Label
61
+
62
+ def label key, val
63
+ @context['Labels'] ||= {}
64
+ @context['Labels'][key] = val
65
+ end
66
+
67
+ # Mode
68
+
69
+ def replicas replica_size
70
+ @context['Mode']['Replicated']['Replicas']= replica_size
71
+ end
72
+
73
+ # UpdateConfig
74
+
75
+ def update_parallelism n
76
+ @context['UpdateConfig'] ||= {}
77
+ @context['UpdateConfig']['Parallelism']= n
78
+ end
79
+
80
+ def update_delay n
81
+ @context['UpdateConfig'] ||= {}
82
+ @context['UpdateConfig']['Delay']= n
83
+ end
84
+
85
+ # Networks
86
+
87
+ def network name
88
+ @context['Networks'] ||= []
89
+ @context['Networks'] << { 'Target' => "#{cluster_prefix}#{name}" }
90
+ end
91
+
92
+ private
93
+
94
+ def cluster_prefix
95
+ "#{@cluster['prefix']}-" if @cluster['prefix']
96
+ end
97
+
98
+ end
99
+
100
+ def service name, &block
101
+ raise 'should be called in cluster' unless @context
102
+ ctx = Service::Context.new(name, @context).tap do |ctx|
103
+ ctx.instance_eval(&block)
104
+ end
105
+ @context['services'] << ctx.context
106
+ end
107
+
108
+ end
109
+ end
110
+ end
data/lib/algo/dsl.rb ADDED
@@ -0,0 +1,34 @@
1
+ module Algo
2
+ class Dsl
3
+ require 'algo/dsl/service'
4
+ require 'algo/dsl/network'
5
+ require 'algo/dsl/cluster'
6
+
7
+ include Dsl::Cluster
8
+
9
+ attr_reader :options
10
+
11
+ CLUSTER_DEFAULT = {}
12
+
13
+ def result
14
+ @clusters
15
+ end
16
+
17
+ def self.load(options, path = nil)
18
+ dsl = new(options).tap do |dsl|
19
+ dsl._load_from(path)
20
+ end
21
+ dsl.result
22
+ end
23
+
24
+ def initialize(options)
25
+ @options = CLUSTER_DEFAULT.dup
26
+ @options.merge!(options)
27
+ @clusters = []
28
+ end
29
+
30
+ def _load_from(path)
31
+ instance_eval(File.read(path), path) if path
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Algo
2
+ VERSION = "0.1.0"
3
+ end
data/lib/algo.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'excon'
2
+ require 'thor'
3
+ require 'json'
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+
7
+ module Algo
8
+ require "algo/version"
9
+ require "algo/docker"
10
+ require "algo/dsl"
11
+ require "algo/cli"
12
+ # Your code goes here...
13
+ end
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: algo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - yoshiso
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-07-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: excon
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.51.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.51.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.10'
111
+ description: Docker container orchestration tool for swarm cluster.
112
+ email:
113
+ - nya060@gmail.com
114
+ executables:
115
+ - algo
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - ".rspec"
121
+ - ".travis.yml"
122
+ - Gemfile
123
+ - README.md
124
+ - Rakefile
125
+ - algo.gemspec
126
+ - bin/console
127
+ - bin/setup
128
+ - examples/awesome-cluster.rb
129
+ - exe/algo
130
+ - lib/algo.rb
131
+ - lib/algo/cli.rb
132
+ - lib/algo/docker.rb
133
+ - lib/algo/docker/base.rb
134
+ - lib/algo/docker/connection.rb
135
+ - lib/algo/docker/error.rb
136
+ - lib/algo/docker/network.rb
137
+ - lib/algo/docker/service.rb
138
+ - lib/algo/docker/version.rb
139
+ - lib/algo/dsl.rb
140
+ - lib/algo/dsl/cluster.rb
141
+ - lib/algo/dsl/network.rb
142
+ - lib/algo/dsl/service.rb
143
+ - lib/algo/version.rb
144
+ homepage: https://github.com/yoshiso/algo
145
+ licenses:
146
+ - MIT
147
+ metadata: {}
148
+ post_install_message:
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ requirements: []
163
+ rubyforge_project:
164
+ rubygems_version: 2.5.1
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: Docker container orchestration tool for swarm cluster.
168
+ test_files: []