algo 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.
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: []