docker-compose 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a06d21989982bb658ca42414f0a20e6e511263f6
4
+ data.tar.gz: 94d12b911c12197da022fe880548d68f80225182
5
+ SHA512:
6
+ metadata.gz: 400d618d335ebb064edccbb21f074c66d1b5e82ba4b6f5a93b69ac1227cab19438df7e23ea94529f40f5598405fcd4834234881ffdc011a7c00f0d00d0467919
7
+ data.tar.gz: 180aac4998d6a1ed559f006a617a987e0ea61aa0904c9030de8ba9698f23f254b08ee907aa613b12ca79f1863a61e761668388f1e4f6d8efa816bf6b13d7b6d9
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea
11
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1 @@
1
+ 2.2.2
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.7
4
+ before_install: gem install bundler -v 1.10.6
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in docker-compose.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'pry'
8
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Tony Spataro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,154 @@
1
+ # Docker::Compose
2
+
3
+ This is a Ruby OOP wrapper for the [docker-compose](https://github.com/docker/compose)
4
+ container orchestration tool from Docker Inc. It also contains some features that
5
+ layer nicely on top of docker-compose to enhance your productivity.
6
+
7
+ Distinctive features of this gem:
8
+
9
+ 1) Simulates environment-variable substitution in the style of docker-compose
10
+ 1.5; sequences such as ${FOO} in your YML will be replaced with the
11
+ corresponding environment variable. (This feature will go away once 1.5
12
+ is released.)
13
+
14
+ 2) Environment-variable mapping that allows you to export environment variables
15
+ into your _host_ that point to network services published by containers.
16
+
17
+ Throughout this documentation we will refer to this gem as `Docker::Compose`
18
+ as opposed to the `docker-compose` tool that this gem wraps.
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'docker-compose'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install docker-compose
35
+
36
+ ## Usage
37
+
38
+ ### Invoking from Ruby code
39
+
40
+ ```ruby
41
+ require 'docker/compose'
42
+
43
+ # Create a new session in Dir.pwd using the file "docker-compose.yml"
44
+ # for fine-grained control over options, see Docker::Compose::Session#new
45
+ compose = Docker::Compose.new
46
+
47
+ compose.version
48
+
49
+ compose.up(detached:true)
50
+ ```
51
+
52
+ ### Invoking from Rake
53
+
54
+ Open your Rakefile and add the Docker::Compose tasks.
55
+
56
+ ```ruby
57
+ require 'docker/compose/rake_tasks'
58
+
59
+ Docker::Compose::RakeTasks.new do |tasks|
60
+ # customize by calling setter methods of tasks;
61
+ # see the class documentation for details
62
+ end
63
+
64
+ ```
65
+
66
+ Notice that `rake -T` now has a few additional tasks for invoking gem
67
+ functionality. You can `docker:compose:env` to print bash export statements
68
+ for host-to-container environment mapping; you can `docker:compose:up` or
69
+ `docker:compose:stop` to start and stop containers.
70
+
71
+ The `docker-compose` command is a perfectly valid way to start
72
+ and stop containers, but the gem provides some env-substitution functionality
73
+ for your YML files that will be built into docker-compose 1.5 but is not
74
+ released yet. If your YML contains `${ENV}` references, i.e. in order to
75
+ point your containers at network services running on the host, then you must
76
+ invoke docker-compose through Rake in order to peform the substitution.
77
+
78
+ ### Mapping container IPs and ports
79
+
80
+ Assuming that your app accepts its configuration in the form of environment
81
+ variables, you can use the `docker:compose:env` to export environment values
82
+ into your bash shell that point to services running inside containers. This
83
+ allows you to run the app on your host (for easier debugging and code editing)
84
+ but let it communicate with services running inside containers.
85
+
86
+ Docker::Compose uses a heuristic to figure out which IP your services
87
+ are actually reachable at; the heuristic works regardless whether you are
88
+ running "bare" docker daemon on localhost, communicating with a docker-machine
89
+ instance, or even using a cloud-hosted docker machine!
90
+
91
+ As a trivial example, let's say that your `docker-compose.yml` contains one
92
+ service, the database that your app needs in order to run.
93
+
94
+ ```yaml
95
+ db:
96
+ image: mysql:latest
97
+ environment:
98
+ MYSQL_DATABASE: myapp_development
99
+ MYSQL_ROOT_PASSWORD: opensesame
100
+ ports:
101
+ - "3306"
102
+
103
+ ```
104
+
105
+ Your app needs two inputs, `DATABASE_HOST` and `DATABASE_PORT`. You can specify
106
+ this in the env section of the Rake task:
107
+
108
+ ```ruby
109
+ Docker::Compose::RakeTasks.new do |tasks|
110
+ tasks.env = {
111
+ 'DATABASE_HOST' => 'db:[3306]'
112
+ 'DATABASE_PORT' => '[db]:3306'
113
+ }
114
+ end
115
+ ```
116
+
117
+ (If I had a `DATABASE_URL` input, I could provide a URL such as
118
+ `mysql://db/myapp_development`; Docker::Compose would parse the URL and replace
119
+ the hostname and port appropriately.)
120
+
121
+ Now, I can run my services, ask Docker::Compose to map the environment values
122
+ to the actual IP and port that `db` has been published to, and run my app:
123
+
124
+ ```bash
125
+ user@machine$ docker-compose up -d
126
+
127
+ # This prints bash code resembling the following:
128
+ # export DATABASE_HOST=127.0.0.1
129
+ # export DATABASE_PORT=34387
130
+ # We eval it, which makes the variables available to our shell and to all
131
+ # subprocesses.
132
+ user@machine$ eval "$(bundle exec rake docker:compose:env)"
133
+
134
+ user@machine$ bundle exec rackup
135
+ ```
136
+
137
+ To learn more about mapping, read the class documentation for
138
+ `Docker::Compose::Mapper`.
139
+
140
+ ## Development
141
+
142
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
143
+
144
+ 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`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
145
+
146
+ ## Contributing
147
+
148
+ Bug reports and pull requests are welcome on GitHub at https://github.com/xeger/docker-compose. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
149
+
150
+
151
+ ## License
152
+
153
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
154
+
@@ -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
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "docker/compose"
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
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'docker/compose/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "docker-compose"
8
+ spec.version = Docker::Compose::VERSION
9
+ spec.authors = ["Tony Spataro"]
10
+ spec.email = ["xeger@xeger.net"]
11
+
12
+ spec.summary = %q{Wrapper docker-compose with added Rake smarts.}
13
+ spec.description = %q{Provides an OOP interface to docker-compose and facilitates container-to-host and host-to-container networking.}
14
+ spec.homepage = "https://github.com/xeger/docker-compose"
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_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec"
25
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'compose/version'
2
+ require_relative 'compose/shell'
3
+ require_relative 'compose/session'
4
+ require_relative 'compose/net_info'
5
+ require_relative 'compose/mapper'
6
+
7
+ module Docker
8
+ module Compose
9
+ # Create a new session.
10
+ def self.new
11
+ Session.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,90 @@
1
+ require 'yaml'
2
+
3
+ module Docker::Compose::Future
4
+ module Session
5
+ # Pattern that matches an environment substitution in a docker-compose YML
6
+ # file.
7
+ # @see #substitute
8
+ SUBSTITUTION = /\$\{([A-Z0-9:_-]+)\}/
9
+
10
+ # Hook in env-var substitution by aliasing a method chain for run!
11
+ def self.included(host)
12
+ done = host.instance_methods.include?(:run_without_substitution!)
13
+ host.instance_eval do
14
+ alias_method :run_without_substitution!, :run!
15
+ alias_method :run!, :run_with_substitution!
16
+ end unless done
17
+ end
18
+
19
+ # Read docker-compose YML; perform environment variable substitution;
20
+ # write a temp file; invoke run! with the new file; delete the temp
21
+ # file afterward.
22
+ #
23
+ # This is a complete reimplementation of run! and we only alias the original
24
+ # to be good citizens.
25
+ def run_with_substitution!(*cmd)
26
+ temp = nil
27
+ project = File.basename(@dir)
28
+
29
+ # Find and purge the 'file' flag if it exists; otherwise assume we will
30
+ # substitute our default (session) file.
31
+ fn = nil
32
+ cmd.each do |item|
33
+ fn ||= item.delete(:file) if item.is_a?(Hash)
34
+ end
35
+ fn ||= @file
36
+
37
+ # Rewrite YML if the file exists and the file:false "flag" wasn't
38
+ # explicitly passed to us.
39
+ Dir.chdir(@dir) do
40
+ yml = YAML.load(File.read(fn))
41
+ yml = substitute(yml)
42
+ temp = Tempfile.new(fn, @dir)
43
+ temp.write(YAML.dump(yml))
44
+ temp.close
45
+
46
+ project_opts = {
47
+ file: temp.path,
48
+ project: File.basename(@dir)
49
+ }
50
+
51
+ result, output =
52
+ @shell.command('docker-compose', project_opts, *cmd)
53
+ (result == 0) || raise(RuntimeError,
54
+ "#{cmd.first} failed with status #{result}")
55
+ output
56
+ end
57
+ ensure
58
+ temp.unlink if temp
59
+ end
60
+
61
+ # Simulate the behavior of docker-compose 1.5: replace "${VAR}" sequences
62
+ # with the values of environment variables. Perform this recursively if
63
+ # data is a Hash or Array.
64
+ #
65
+ #
66
+ # @param [Hash,Array,String,Object] data
67
+ # @return [Hash,Array,String,Object] data with all ${ENV} references substituted
68
+ private def substitute(data)
69
+ case data
70
+ when Hash
71
+ result = {}
72
+ data.each_pair { |k, v| result[k] = substitute(v) }
73
+ when Array
74
+ result = []
75
+ data.each { |v| result << substitute(v) }
76
+ when String
77
+ result = data
78
+ while (match = SUBSTITUTION.match(result))
79
+ var = match[1]
80
+ repl = format("${%s}", var)
81
+ result.gsub!(repl, ENV[var])
82
+ end
83
+ else
84
+ result = data
85
+ end
86
+
87
+ result
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,95 @@
1
+ module Docker::Compose
2
+ # Uses a Session to discover information about services' IP addresses and
3
+ # ports as reachable from the host, then
4
+ class Mapper
5
+ # Pattern that matches an "elided" host or port that should be omitted from
6
+ # output, but is needed to identify a specific container and port.
7
+ ELIDED = /^\[.+\]$/.freeze
8
+
9
+ # Regexp that can be used with gsub to strip elision marks
10
+ REMOVE_ELIDED = /[\[\]]/.freeze
11
+
12
+ BadSubstitution = Class.new(StandardError)
13
+ NoService = Class.new(RuntimeError)
14
+
15
+ # Create an instance of Mapper
16
+ # @param [Docker::Compose::Session] session
17
+ # @param [String] host_ip IPv4 address of the host that is publishing
18
+ # Docker services (i.e. the `DOCKER_HOST` hostname or IP if you are using
19
+ # a non-clustered Docker environment)
20
+ # @param [Boolean] strict if true, raise BadSubstitution when unrecognized
21
+ # syntax is passed to #map; if false, simply return the value without
22
+ # substituting anything
23
+ def initialize(session, host_ip, strict:true)
24
+ @session = session
25
+ @host_ip = host_ip
26
+ @strict = strict
27
+ end
28
+
29
+ # Substitute service hostnames and ports that appear in a URL or a
30
+ # host:port string. If either component of a host:port string is
31
+ # surrounded by square brackets, "elide" that component, removing it
32
+ # from the result but using it to find the correct service and port.
33
+ #
34
+ # @example map MySQL on local docker host with 3306 published to 13847
35
+ # map("tcp://db:3306") # => "tcp://127.0.0.1:13847"
36
+ #
37
+ # @example map just the hostname of MySQL on local docker host
38
+ # map("db:[3306]") # => "127.0.0.1"
39
+ #
40
+ # @example map just the port of MySQL on local docker host
41
+ # map("[db]:3306") # => "13847"
42
+ #
43
+ # @param [String] value a URI or a host:port pair
44
+ #
45
+ # @raise [BadSubstitution] if a substitution string can't be parsed
46
+ # @raise [NoService] if service is not up or does not publish port
47
+ def map(value)
48
+ uri = URI.parse(value) rescue nil
49
+ pair = value.split(':')
50
+
51
+ if uri && uri.scheme && uri.host
52
+ # absolute URI with scheme, authority, etc
53
+ uri.port = published_port(uri.host, uri.port)
54
+ uri.host = @host_ip
55
+ return uri.to_s
56
+ elsif pair.size == 2
57
+ # "host:port" pair; three sub-cases...
58
+ if pair.first =~ ELIDED
59
+ # output only the port
60
+ service = pair.first.gsub(REMOVE_ELIDED, '')
61
+ port = published_port(service, pair.last)
62
+ return port.to_s
63
+ elsif pair.last =~ ELIDED
64
+ # output only the hostname; resolve the port anyway to ensure that
65
+ # the service is running.
66
+ service = pair.first
67
+ port = pair.last.gsub(REMOVE_ELIDED, '')
68
+ published_port(service, port)
69
+ return @host_ip
70
+ else
71
+ # output port:hostname pair
72
+ port = published_port(pair.first, pair.last)
73
+ return "#{@host_ip}:#{port}"
74
+ end
75
+ elsif @strict
76
+ raise BadSubstitution, "Can't understand '#{value}'"
77
+ else
78
+ return value
79
+ end
80
+ end
81
+
82
+ # Figure out which host port a given service's port has been published to,
83
+ # and/or whether that service is running. Cannot distinguish between the
84
+ # "service not running" case and the "container port not published" case!
85
+ #
86
+ # @raise [NoService] if service is not up or does not publish port
87
+ # @return [Integer] host port number, or nil if port not published
88
+ def published_port(service, port)
89
+ result = @session.port(service, port)
90
+ Integer(result.split(':').last.gsub("\n", ""))
91
+ rescue RuntimeError
92
+ raise NoService, "Service '#{service}' not running, or does not publish port '#{port}'"
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,88 @@
1
+ module Docker::Compose
2
+ # Utility that gathers information about the relationship between the host
3
+ # on which the Ruby VM is running and the docker host, then makes an
4
+ # guess about the mutually routable IP addresses of each.
5
+ #
6
+ # This information can be used to tell containers how to connect to ports on
7
+ # the local host, or conversely to tell the local host how to connect to ports
8
+ # published by containers running on the docker host.
9
+ #
10
+ # The heuristic works for most cases encountered in the wild, including:
11
+ # - DOCKER_HOST is unset (assume daemon listening on 127.0.0.1)
12
+ # - DOCKER_HOST points to a socket (assume 127.0.0.1)
13
+ # - DOCKER_HOST points to a tcp, http or https address
14
+ class NetInfo
15
+ # Determine IP addresses of the local host's network interfaces.
16
+ #
17
+ # @return [Array] list of String dotted-quad IPv4 addresses
18
+ def self.ipv4_interfaces
19
+ Socket.getifaddrs
20
+ .map { |i| i.addr.ip_address if i.addr && i.addr.ipv4? }.compact
21
+ end
22
+
23
+ # Create a new instance of this class.
24
+ # @param [String] docker_host a URI pointing to the docker host
25
+ # @param [Array] list of String dotted-quad IPv4 addresses of local host
26
+ def initialize(docker_host=ENV['DOCKER_HOST'],
27
+ my_ips=self.class.ipv4_interfaces)
28
+ docker_host ||= 'unix:/var/run/docker.sock'
29
+ @docker_url = URI.parse(docker_host)
30
+ @my_ips = my_ips
31
+ end
32
+
33
+ # Examine local host's network interfaces; figure out which one is most
34
+ # likely to share a route with the given IP address. If no IP address
35
+ # is specified, figure out which IP the Docker daemon is reachable on
36
+ # and use that as the target IP.
37
+ #
38
+ # @param [String] target_ip IPv4 address of target
39
+ #
40
+ # @return [String] IPv4 address of host machine that _may_ be reachable from
41
+ # Docker machine
42
+ def host_routable_ip(target_ip=docker_routable_ip)
43
+ best_match = ''
44
+ best_prefix = 0
45
+
46
+ target_cps = target_ip.codepoints
47
+
48
+ @my_ips.each do |my_ip|
49
+ ip_cps = my_ip.codepoints
50
+ prefix = 0
51
+ ip_cps.each_with_index do |cp, i|
52
+ break unless target_cps[i] == cp
53
+ prefix = i
54
+ end
55
+
56
+ if prefix > best_prefix
57
+ best_match = my_ip
58
+ best_prefix = prefix
59
+ end
60
+ end
61
+
62
+ best_match
63
+ end
64
+
65
+ # Figure out the likely IP address of the host pointed to by
66
+ # self.docker_url.
67
+ #
68
+ # @return [String] host-reachable IPv4 address of docker host
69
+ def docker_routable_ip
70
+ case @docker_url.scheme
71
+ when 'tcp', 'http', 'https'
72
+ docker_dns = @docker_url.host
73
+ docker_port = @docker_url.port || 2376
74
+ else
75
+ # Cheap trick: for unix or other protocols, assume docker daemon
76
+ # is listening on 127.0.0.1:2376
77
+ docker_dns = 'localhost'
78
+ docker_port = 2376
79
+ end
80
+
81
+ addr = Addrinfo.getaddrinfo(
82
+ docker_dns, docker_port,
83
+ Socket::AF_INET, Socket::SOCK_STREAM).first
84
+
85
+ addr && addr.ip_address
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,158 @@
1
+ require 'rake/tasklib'
2
+
3
+ # In case this file is required directly
4
+ require 'docker/compose'
5
+
6
+ module Docker::Compose
7
+ class RakeTasks < Rake::TaskLib
8
+ # Set the directory in which docker-compose commands will be run. Default
9
+ # is the directory in which Rakefile is located.
10
+ #
11
+ # @return [String]
12
+ attr_accessor :dir
13
+
14
+ # Set the name of the docker-compose file. Default is`docker-compose.yml`.
15
+ # @return [String]
16
+ attr_accessor :file
17
+
18
+ # Provide a mapping of environment variables that should be set in the
19
+ # _host_ shell for docker:compose:env or docker:compose:server.
20
+ # The values of the environment variables can refer to names of services
21
+ # and ports defined in the docker-compose file, and this gem will query
22
+ # docker-compose to find out which host IP and port the services are
23
+ # reachable on. This allows components running on the host to connect to
24
+ # services running inside containers.
25
+ #
26
+ # @see Docker::Compose::Mapper for information about the substitution syntax
27
+ attr_accessor :env
28
+
29
+ # Extra environment variables that should be set before invoking the command
30
+ # specified for docker:compose:server. These are set _in addition_ to env
31
+ # (and should be disjoint from env), and do not necessarily need to map the
32
+ # location of a container; they can be simple extra env values that are
33
+ # useful to change the server's behavior when it runs in cooperation
34
+ # with containers.
35
+ #
36
+ # If there is overlap between env and server_env, then keys of server_env
37
+ # will "win"; they are set last.
38
+ attr_accessor :server_env
39
+
40
+ # Command to exec on the _host_ when someone invokes docker:compose:server.
41
+ # This is used to start up all containers and then run a server that
42
+ # depends on them and is properly linked to them.
43
+ attr_accessor :server
44
+
45
+ # Construct Rake wrapper tasks for docker-compose. If a block is given,
46
+ # yield self to the block before defining any tasks so their behavior
47
+ # can be configured by calling #env=, #file= and so forth.
48
+ def initialize
49
+ self.dir = Rake.application.original_dir
50
+ self.file = 'docker-compose.yml'
51
+ self.env = {}
52
+ self.server_env = {}
53
+ yield self if block_given?
54
+
55
+ @shell = Docker::Compose::Shell.new
56
+ @session = Docker::Compose::Session.new(@shell, dir:dir, file:file)
57
+ @net_info = Docker::Compose::NetInfo.new
58
+
59
+ define
60
+ end
61
+
62
+ private def define
63
+ namespace :docker do
64
+ namespace :compose do
65
+ desc 'Print bash exports with IP/ports of running services'
66
+ task :env do
67
+ @shell.interactive = false # suppress useless 'port' output
68
+
69
+ if Rake.application.top_level_tasks.include? 'docker:compose:env'
70
+ # This task is being run as top-level; print some bash export
71
+ # statements or usage information depending on whether STDOUT
72
+ # is a tty.
73
+ if STDOUT.tty?
74
+ print_usage
75
+ else
76
+ export_env(print:true)
77
+ end
78
+ else
79
+ # This task is a dependency of something else; just export the
80
+ # environment variables for use in-process by other Rake tasks.
81
+ export_env(print:false)
82
+ end
83
+ end
84
+
85
+ desc 'Launch services needed to run this application'
86
+ task :up do
87
+ @shell.interactive = true # let user see what's happening
88
+ @session.up(detached:true)
89
+ end
90
+
91
+ desc 'Tail logs of all running services'
92
+ task :logs do
93
+ @session.logs
94
+ end
95
+
96
+ desc 'Stop services needed to run this application'
97
+ task :stop do
98
+ @session.stop
99
+ end
100
+
101
+ desc 'Run application on the host, linked to services in containers'
102
+ task :server => ['docker:compose:up', 'docker:compose:env'] do
103
+ exec(self.server)
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # Substitute and set environment variables that point to network ports
110
+ # published by docker-compose services. Optionally also print bash export
111
+ # statements so this information can be made available to a user's shell.
112
+ private def export_env(print:)
113
+ # First, do env substitutions in strict mode; don't catch BadSubstitution
114
+ # so the caller knows when he has a bogus value
115
+ mapper = Docker::Compose::Mapper.new(@session,
116
+ @net_info.docker_routable_ip)
117
+ self.env.each_pair do |k, v|
118
+ begin
119
+ v = mapper.map(v)
120
+ ENV[k] = v
121
+ print_env(k, v) if print
122
+ rescue Docker::Compose::Mapper::NoService
123
+ ENV[k] = nil
124
+ print_env(k, nil) if print
125
+ end
126
+ end
127
+
128
+ # Next, do server substitutions in non-strict mode since server_env
129
+ # can contain arbitrary values.
130
+ mapper = Docker::Compose::Mapper.new(@session,
131
+ @net_info.docker_routable_ip,
132
+ strict:false)
133
+ self.server_env.each_pair do |k, v|
134
+ v = mapper.map(v)
135
+ ENV[k] = v
136
+ print_env(k, v) if print
137
+ end
138
+ end
139
+
140
+ # Print a bash export or unset statement
141
+ private def print_env(k, v)
142
+ if v
143
+ puts format('export %s=%s', k, v)
144
+ else
145
+ puts format('unset %s # service not running', k)
146
+ end
147
+ end
148
+
149
+ private def print_usage
150
+ be = 'bundle exec ' if defined?(Bundler)
151
+ puts "# To export container network locations to your environment:"
152
+ puts %Q{eval "$(#{be}rake docker:compose:env)"}
153
+ puts
154
+ puts '# To learn which environment variables we will export:'
155
+ puts %Q{echo "$(#{be}rake docker:compose:env)"}
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,113 @@
1
+ require 'docker/compose/future/session'
2
+
3
+ module Docker::Compose
4
+ # A Ruby OOP interface to a docker-compose session. A session is bound to
5
+ # a particular directory and docker-compose file (which are set at initialize
6
+ # time) and invokes whichever docker-compose command is resident in $PATH.
7
+ #
8
+ # Run docker-compose commands by calling instance methods of this class and
9
+ # passing kwargs that are equivalent to the CLI options you would pass to
10
+ # the command-line tool.
11
+ #
12
+ # Note that the Ruby command methods usually expose a _subset_ of the options
13
+ # allowed by the docker-compose CLI, and that options are sometimes renamed
14
+ # for clarity, e.g. the "-d" flag always becomes the "detached:" kwarg.
15
+ class Session
16
+ def initialize(shell=Docker::Compose::Shell.new,
17
+ dir:Dir.pwd, file:'docker-compose.yml')
18
+ @shell = shell
19
+ @dir = dir
20
+ @file = file
21
+ end
22
+
23
+ # Monitor the logs of one or more containers.
24
+ # @param [Array] services list of String service names to show logs for
25
+ # @return [true] always returns true
26
+ # @raise [RuntimeError] if command fails
27
+ def logs(*services)
28
+ run!('logs', services)
29
+ true
30
+ end
31
+
32
+ # Idempotently run services in the project,
33
+ # @param [Array] services list of String service names to run
34
+ # @param [Boolean] detached if true, to start services in the background;
35
+ # otherwise, monitor logs in the foreground and shutdown on Ctrl+C
36
+ # @param [Integer] timeout how long to wait for each service to stostart
37
+ # @param [Boolean] no_build if true, to skip building images for services
38
+ # that have a `build:` instruction in the docker-compose file
39
+ # @param [Boolean] no_deps if true, just run specified services without
40
+ # running the services that they depend on
41
+ # @return [true] always returns true
42
+ # @raise [RuntimeError] if command fails
43
+ def up(*services,
44
+ detached:false, timeout:10, no_build:false, no_deps:false)
45
+ run!('up',
46
+ {d:detached, timeout:timeout, no_build:no_build, no_deps:no_deps},
47
+ services)
48
+ true
49
+ end
50
+
51
+ # Stop running services.
52
+ # @param [Array] services list of String service names to stop
53
+ # @param [Integer] timeout how long to wait for each service to stop
54
+ def stop(*services, timeout:10)
55
+ run!('stop', {timeout:timeout}, services)
56
+ end
57
+
58
+ # Figure out which host a port a given service port has been published to.
59
+ # @param [String] service name of service from docker-compose.yml
60
+ # @param [Integer] port number of port
61
+ # @param [String] protocol 'tcp' or 'udp'
62
+ # @param [Integer] index of container (if multiple instances running)
63
+ def port(service, port, protocol:'tcp', index:1)
64
+ run!('port', {protocol:protocol, index:index}, service, port)
65
+ end
66
+
67
+ # Determine the installed version of docker-compose.
68
+ # @param [Boolean] short whether to return terse version information
69
+ # @return [String, Hash] if short==true, returns a version string;
70
+ # otherwise, returns a Hash of component names to version strings
71
+ # @raise [RuntimeError] if command fails
72
+ def version(short:false)
73
+ result = run!('version', short:short, file:false, dir:false)
74
+
75
+ if short
76
+ result.strip
77
+ else
78
+ lines = result.split("\n")
79
+ lines.inject({}) do |h, line|
80
+ kv = line.split(/: +/, 2)
81
+ h[kv.first] = kv.last
82
+ h
83
+ end
84
+ end
85
+ end
86
+
87
+ # Run a docker-compose command without validating that the CLI parameters
88
+ # make sense. Prepend project and file options if suitable.
89
+ #
90
+ # @see Docker::Compose::Shell#command
91
+ #
92
+ # @param [Array] cmd subcommand words and options in the format accepted by
93
+ # Shell#command
94
+ # @return [String] output of the command
95
+ # @raise [RuntimeError] if command fails
96
+ def run!(*cmd)
97
+ project_opts = {
98
+ file: @file
99
+ }
100
+
101
+ Dir.chdir(@dir) do
102
+ result, output =
103
+ @shell.command('docker-compose', project_opts, *cmd)
104
+ (result == 0) || raise(RuntimeError,
105
+ "#{cmd.first} failed with status #{result}")
106
+ output
107
+ end
108
+ end
109
+
110
+ # Simulate behaviors from Docker 1.5
111
+ include Docker::Compose::Future::Session
112
+ end
113
+ end
@@ -0,0 +1,165 @@
1
+ require 'open3'
2
+
3
+ module Docker::Compose
4
+ # An easy-to-use interface for invoking commands and capturing their output.
5
+ # Instances of Shell can be interactive, which prints the command's output
6
+ # to the terminal and also allows the user to interact with the command.
7
+ class Shell
8
+ # If true, commands run in the shell will have their stdio streams tied
9
+ # to the parent process so the user can view their output and send input
10
+ # to them. Commands' stdout is still captured normally when they are
11
+ # interactive.
12
+ #
13
+ # Note that interactivity doesn't work very well because we use popen,
14
+ # which uses pipes to communicate with the child process and pipes have
15
+ # a fixed buffer size; the displayed output tends to "lag" behind the
16
+ # actual program, and bytes sent to stdin may not arrive until you send
17
+ # a lot of them!
18
+ #
19
+ # TODO: solve pipe buffering issues, perhaps with a pty...
20
+ #
21
+ # @return [Boolean]
22
+ attr_accessor :interactive
23
+
24
+ # Convert Ruby keyword arguments into CLI parameters that are compatible
25
+ # with the syntax of golang's flags package.
26
+ #
27
+ # Options are translated to CLI parameters using the following convention:
28
+ # 1) Snake-case symbols are hyphenated, e.g. :no_foo => "--no-foo"
29
+ # 2) boolean values indicate a CLI flag; true includes the flag, false or nil omits it
30
+ # 3) other values indicate a CLI option that has a value.
31
+ # 4) single character values are passed as short options e.g. "-X V"
32
+ # 5) multi-character values are passed as long options e.g. "--XXX=V"
33
+ #
34
+ def self.options(**opts)
35
+ flags = []
36
+
37
+ # Transform opts into golang flags-style command line parameters;
38
+ # append them to the command.
39
+ opts.each do |kw, arg|
40
+ if kw.length == 1
41
+ if arg == true
42
+ # true: boolean flag
43
+ flags << "-#{kw}"
44
+ elsif arg
45
+ # truthey: option that has a value
46
+ flags << "-#{kw}" << arg.to_s
47
+ else
48
+ # falsey: omit boolean flag
49
+ end
50
+ else
51
+ kw = kw.to_s.gsub('_','-')
52
+ if arg == true
53
+ # true: boolean flag
54
+ flags << "--#{kw}"
55
+ elsif arg
56
+ # truthey: option that has a value
57
+ flags << "--#{kw}=#{arg}"
58
+ else
59
+ # falsey: omit boolean flag
60
+ end
61
+ end
62
+ end
63
+
64
+ flags
65
+ end
66
+
67
+ # Create an instance of Shell.
68
+ def initialize
69
+ @interactive = false
70
+ end
71
+
72
+ # Run a shell command whose arguments and flags are expressed using some
73
+ # Rubyish sugar. This method accepts an arbitrary number of positional
74
+ # parameters; each parameter can be a Hash, an array, or a simple Object.
75
+ # Arrays and simple objects are appended to argv as "bare" words; Hashes
76
+ # are translated to golang flags and then appended to argv.
77
+ #
78
+ # @example Run docker-compose with complex parameters
79
+ # command('docker-compose', {file: 'joe.yml'}, 'up', {d:true}, 'mysvc')
80
+ #
81
+ # @see #options for information on Hash-to-flag translation
82
+ def command(*cmd)
83
+ argv = []
84
+
85
+ cmd.each do |item|
86
+ case item
87
+ when Array
88
+ # list of words to append to argv
89
+ argv.concat(item.map { |e| e.to_s })
90
+ when Hash
91
+ # list of options to convert to CLI parameters
92
+ argv.concat(self.class.options(item))
93
+ else
94
+ # single word to append to argv
95
+ argv << item.to_s
96
+ end
97
+ end
98
+
99
+ run(argv)
100
+ end
101
+
102
+ # Run a shell command. Perform no translation or substitution. Return
103
+ # the program's exit status and stdout.
104
+ #
105
+ # @param [Array] argv command to run; argv[0] is program name and the
106
+ # remaining elements are parameters and flags
107
+ # @return [Array] a pair of Integer exitstatus and String output
108
+ private def run(argv)
109
+ stdin, stdout, stderr, thr = Open3.popen3(*argv)
110
+
111
+ streams = [stdout, stderr]
112
+
113
+ if @interactive
114
+ streams << STDIN
115
+ else
116
+ stdin.close
117
+ end
118
+
119
+ output = String.new.force_encoding(Encoding::BINARY)
120
+
121
+ until streams.empty? || (streams.length == 1 && streams.first == STDIN)
122
+ ready, _, _ = IO.select(streams, [], [], 1)
123
+
124
+ if ready && ready.include?(STDIN)
125
+ input = STDIN.readpartial(1_024) rescue nil
126
+ if input
127
+ stdin.write(input)
128
+ else
129
+ # our own STDIN got closed; proxy to child's stdin
130
+ stdin.close
131
+ end
132
+ end
133
+
134
+ if ready && ready.include?(stderr)
135
+ data = stderr.readpartial(1_024) rescue nil
136
+ if data
137
+ STDERR.write(data) if @interactive
138
+ else
139
+ streams.delete(stderr)
140
+ end
141
+ end
142
+
143
+ if ready && ready.include?(stdout)
144
+ data = stdout.readpartial(1_024) rescue nil
145
+ if data
146
+ output << data
147
+ STDOUT.write(data) if @interactive
148
+ else
149
+ streams.delete(stdout)
150
+ end
151
+ end
152
+ end
153
+
154
+ # This blocks until the process exits (which probably already happened,
155
+ # given that we have received EOF on its output streams).
156
+ status = thr.value.exitstatus
157
+
158
+ [status, output]
159
+ rescue Interrupt
160
+ # Proxy Ctrl+C to our child process
161
+ Process.kill('INT', thr.pid) rescue nil
162
+ raise
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,5 @@
1
+ module Docker
2
+ module Compose
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: docker-compose
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tony Spataro
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-10-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Provides an OOP interface to docker-compose and facilitates container-to-host
56
+ and host-to-container networking.
57
+ email:
58
+ - xeger@xeger.net
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - ".rspec"
65
+ - ".ruby-version"
66
+ - ".travis.yml"
67
+ - CODE_OF_CONDUCT.md
68
+ - Gemfile
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - bin/console
73
+ - bin/setup
74
+ - docker-compose.gemspec
75
+ - lib/docker/compose.rb
76
+ - lib/docker/compose/future/session.rb
77
+ - lib/docker/compose/mapper.rb
78
+ - lib/docker/compose/net_info.rb
79
+ - lib/docker/compose/rake_tasks.rb
80
+ - lib/docker/compose/session.rb
81
+ - lib/docker/compose/shell.rb
82
+ - lib/docker/compose/version.rb
83
+ homepage: https://github.com/xeger/docker-compose
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.4.5
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Wrapper docker-compose with added Rake smarts.
107
+ test_files: []