docker-compose 0.0.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 +7 -0
- data/.coveralls.yml +1 -0
- data/.github/workflows/publish.yml +42 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +32 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/Rakefile +7 -0
- data/bin/console +18 -0
- data/bin/setup +7 -0
- data/docker-compose.gemspec +29 -0
- data/docker-compose.yml +11 -0
- data/lib/docker/compose/collection.rb +13 -0
- data/lib/docker/compose/container.rb +80 -0
- data/lib/docker/compose/error.rb +26 -0
- data/lib/docker/compose/mapper.rb +159 -0
- data/lib/docker/compose/net_info.rb +89 -0
- data/lib/docker/compose/rake_tasks.rb +171 -0
- data/lib/docker/compose/session.rb +354 -0
- data/lib/docker/compose/shell_printer/fish.rb +17 -0
- data/lib/docker/compose/shell_printer/posix.rb +33 -0
- data/lib/docker/compose/shell_printer.rb +26 -0
- data/lib/docker/compose/version.rb +6 -0
- data/lib/docker/compose.rb +19 -0
- metadata +132 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Docker::Compose
|
3
|
+
# Uses a Session to discover information about services' IP addresses and
|
4
|
+
# ports as reachable from localhost, then maps URLs and other common network
|
5
|
+
# address formats so they point to the right host and port.
|
6
|
+
#
|
7
|
+
# **NOTE**: this class uses some heuristics to deal with cases where the
|
8
|
+
# Docker client is talking to a remote server because the `DOCKER_HOST`
|
9
|
+
# environment variable is set. In those cases, Mapper tries to determine
|
10
|
+
# the IP address of the exposed services as reachable from localhost;
|
11
|
+
# it generally makes a correct guess, but in certain super-complex networking
|
12
|
+
# scenarios it may guess wrong. Please open a GitHub issue if you find
|
13
|
+
# a situation where Mapper provides a wrong answer.
|
14
|
+
class Mapper
|
15
|
+
# Pattern that matches an "elided" host or port that should be omitted from
|
16
|
+
# output, but is needed to identify a specific container and port.
|
17
|
+
ELIDED = /^\[.+\]$/.freeze
|
18
|
+
|
19
|
+
# Regexp that can be used with gsub to strip elision marks
|
20
|
+
REMOVE_ELIDED = /[\[\]]/.freeze
|
21
|
+
|
22
|
+
BadSubstitution = Class.new(StandardError)
|
23
|
+
NoService = Class.new(RuntimeError)
|
24
|
+
|
25
|
+
# Instantiate a mapper; map some environment variables; yield to caller for
|
26
|
+
# additional processing.
|
27
|
+
#
|
28
|
+
# @param [Hash] env a set of keys/values whose values will be mapped
|
29
|
+
# @param [Session] session
|
30
|
+
# @param [NetInfo] net_info
|
31
|
+
# @yield yields with each substituted (key, value) pair
|
32
|
+
def self.map(env, session:Session.new, net_info:NetInfo.new)
|
33
|
+
mapper = new(session, net_info)
|
34
|
+
env.each_pair do |k, v|
|
35
|
+
begin
|
36
|
+
v = mapper.map(v)
|
37
|
+
yield(k, v)
|
38
|
+
rescue NoService
|
39
|
+
yield(k, nil)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create an instance of Mapper
|
45
|
+
#
|
46
|
+
# @param [Docker::Compose::Session] session
|
47
|
+
# @param [NetInfo] net_info
|
48
|
+
def initialize(session=Session.new, net_info=NetInfo.new)
|
49
|
+
docker_host = ENV['DOCKER_HOST']
|
50
|
+
if docker_host.nil? || docker_host =~ /^(\/|unix|file)/
|
51
|
+
# If DOCKER_HOST is blank, or pointing to a local socket, then we
|
52
|
+
# can trust the address information returned by `docker-compose port`.
|
53
|
+
override_host = nil
|
54
|
+
else
|
55
|
+
# If DOCKER_HOST is present, assume that containers have bound to
|
56
|
+
# whatever IP we reach it at; don't fall victim to docker-compose's
|
57
|
+
# dirty lies!
|
58
|
+
override_host = net_info.docker_routable_ip
|
59
|
+
end
|
60
|
+
|
61
|
+
@session = session
|
62
|
+
@override_host = override_host
|
63
|
+
end
|
64
|
+
|
65
|
+
# Substitute service hostnames and ports that appear in a URL or a
|
66
|
+
# host:port string. If either component of a host:port string is
|
67
|
+
# surrounded by square brackets, "elide" that component, removing it
|
68
|
+
# from the result but using it to find the correct service and port.
|
69
|
+
#
|
70
|
+
# @example map MySQL on local docker host with 3306 published to 13847
|
71
|
+
# map("tcp://db:3306") # => "tcp://127.0.0.1:13847"
|
72
|
+
#
|
73
|
+
# @example map just the hostname of MySQL on local docker host
|
74
|
+
# map("db:[3306]") # => "127.0.0.1"
|
75
|
+
#
|
76
|
+
# @example map just the port of MySQL on local docker host
|
77
|
+
# map("[db]:3306") # => "13847"
|
78
|
+
#
|
79
|
+
# @example map an array of database hosts
|
80
|
+
# map(["[db1]:3306", "[db2]:3306"])
|
81
|
+
#
|
82
|
+
# @param [String,#map] value a URI, host:port pair, or an array of either
|
83
|
+
#
|
84
|
+
# @return [String,Array] the mapped value with container-names and ports substituted
|
85
|
+
#
|
86
|
+
# @raise [BadSubstitution] if a substitution string can't be parsed
|
87
|
+
# @raise [NoService] if service is not up or does not publish port
|
88
|
+
def map(value)
|
89
|
+
if value.respond_to?(:map)
|
90
|
+
value.map { |e| map_scalar(e) }
|
91
|
+
else
|
92
|
+
map_scalar(value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Figure out which host port a given service's port has been published to,
|
97
|
+
# and/or whether that service is running. Cannot distinguish between the
|
98
|
+
# "service not running" case and the "container port not published" case!
|
99
|
+
#
|
100
|
+
# @raise [NoService] if service is not up or does not publish port
|
101
|
+
# @return [Array] (String, Integer) pair of host address and port number
|
102
|
+
def host_and_port(service, port)
|
103
|
+
result = @session.port(service, port.to_s)
|
104
|
+
if result
|
105
|
+
result.chomp!
|
106
|
+
else
|
107
|
+
raise NoService,
|
108
|
+
"Service '#{service}' not running, or does not " \
|
109
|
+
"publish port '#{port}'"
|
110
|
+
end
|
111
|
+
|
112
|
+
host, port = result.split(':')
|
113
|
+
host = @override_host if @override_host
|
114
|
+
|
115
|
+
[host, Integer(port)]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Map a single string, replacing service names with IPs and container ports
|
119
|
+
# with the host ports that they have been mapped to.
|
120
|
+
# @param [String] value
|
121
|
+
# @return [String]
|
122
|
+
def map_scalar(value)
|
123
|
+
uri = begin
|
124
|
+
URI.parse(value)
|
125
|
+
rescue
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
pair = value.split(':')
|
129
|
+
|
130
|
+
if uri && uri.scheme && uri.host
|
131
|
+
# absolute URI with scheme, authority, etc
|
132
|
+
uri.host, uri.port = host_and_port(uri.host, uri.port)
|
133
|
+
return uri.to_s
|
134
|
+
elsif pair.size == 2
|
135
|
+
# "host:port" pair; three sub-cases...
|
136
|
+
if pair.first =~ ELIDED
|
137
|
+
# output only the port
|
138
|
+
service = pair.first.gsub(REMOVE_ELIDED, '')
|
139
|
+
_, port = host_and_port(service, pair.last)
|
140
|
+
return port.to_s
|
141
|
+
elsif pair.last =~ ELIDED
|
142
|
+
# output only the hostname; resolve the port anyway to ensure that
|
143
|
+
# the service is running.
|
144
|
+
service = pair.first
|
145
|
+
port = pair.last.gsub(REMOVE_ELIDED, '')
|
146
|
+
host, = host_and_port(service, port)
|
147
|
+
return host
|
148
|
+
else
|
149
|
+
# output port:hostname pair
|
150
|
+
host, port = host_and_port(pair.first, pair.last)
|
151
|
+
return "#{host}:#{port}"
|
152
|
+
end
|
153
|
+
else
|
154
|
+
fail BadSubstitution, "Can't understand '#{value}'"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
private :map_scalar
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Docker::Compose
|
3
|
+
# Utility that gathers information about the relationship between the host
|
4
|
+
# on which the Ruby VM is running and the docker host, then makes an
|
5
|
+
# guess about the mutually routable IP addresses of each.
|
6
|
+
#
|
7
|
+
# This information can be used to tell containers how to connect to ports on
|
8
|
+
# the local host, or conversely to tell the local host how to connect to ports
|
9
|
+
# published by containers running on the docker host.
|
10
|
+
#
|
11
|
+
# The heuristic works for most cases encountered in the wild, including:
|
12
|
+
# - DOCKER_HOST is unset (assume daemon listening on 127.0.0.1)
|
13
|
+
# - DOCKER_HOST points to a socket (assume 127.0.0.1)
|
14
|
+
# - DOCKER_HOST points to a tcp, http or https address
|
15
|
+
class NetInfo
|
16
|
+
# Determine IP addresses of the local host's network interfaces.
|
17
|
+
#
|
18
|
+
# @return [Array] list of String dotted-quad IPv4 addresses
|
19
|
+
def self.ipv4_interfaces
|
20
|
+
Socket.getifaddrs
|
21
|
+
.map { |i| i.addr.ip_address if i.addr && i.addr.ipv4? }.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create a new instance of this class.
|
25
|
+
# @param [String] docker_host a URI pointing to the docker host
|
26
|
+
# @param [Array] list of String dotted-quad IPv4 addresses of local host
|
27
|
+
def initialize(docker_host = ENV['DOCKER_HOST'],
|
28
|
+
my_ips = self.class.ipv4_interfaces)
|
29
|
+
docker_host ||= 'unix:/var/run/docker.sock'
|
30
|
+
@docker_url = URI.parse(docker_host)
|
31
|
+
@my_ips = my_ips
|
32
|
+
end
|
33
|
+
|
34
|
+
# Examine local host's network interfaces; figure out which one is most
|
35
|
+
# likely to share a route with the given IP address. If no IP address
|
36
|
+
# is specified, figure out which IP the Docker daemon is reachable on
|
37
|
+
# and use that as the target IP.
|
38
|
+
#
|
39
|
+
# @param [String] target_ip IPv4 address of target
|
40
|
+
#
|
41
|
+
# @return [String] IPv4 address of host machine that _may_ be reachable from
|
42
|
+
# Docker machine
|
43
|
+
def host_routable_ip(target_ip = docker_routable_ip)
|
44
|
+
best_match = ''
|
45
|
+
best_prefix = 0
|
46
|
+
|
47
|
+
target_cps = target_ip.codepoints
|
48
|
+
|
49
|
+
@my_ips.each do |my_ip|
|
50
|
+
ip_cps = my_ip.codepoints
|
51
|
+
prefix = 0
|
52
|
+
ip_cps.each_with_index do |cp, i|
|
53
|
+
break unless target_cps[i] == cp
|
54
|
+
prefix = i
|
55
|
+
end
|
56
|
+
|
57
|
+
if prefix > best_prefix
|
58
|
+
best_match = my_ip
|
59
|
+
best_prefix = prefix
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
best_match
|
64
|
+
end
|
65
|
+
|
66
|
+
# Figure out the likely IP address of the host pointed to by
|
67
|
+
# self.docker_url.
|
68
|
+
#
|
69
|
+
# @return [String] host-reachable IPv4 address of docker host
|
70
|
+
def docker_routable_ip
|
71
|
+
case @docker_url.scheme
|
72
|
+
when 'tcp', 'http', 'https'
|
73
|
+
docker_dns = @docker_url.host
|
74
|
+
docker_port = @docker_url.port || 2376
|
75
|
+
else
|
76
|
+
# Cheap trick: for unix, file or other protocols, assume docker ports
|
77
|
+
# are proxied to localhost in addition to other interfaces
|
78
|
+
docker_dns = 'localhost'
|
79
|
+
docker_port = 2376
|
80
|
+
end
|
81
|
+
|
82
|
+
addr = Addrinfo.getaddrinfo(
|
83
|
+
docker_dns, docker_port,
|
84
|
+
Socket::AF_INET, Socket::SOCK_STREAM).first
|
85
|
+
|
86
|
+
addr && addr.ip_address
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'json'
|
3
|
+
require 'rake/tasklib'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
# In case this file is required directly
|
7
|
+
require 'docker/compose'
|
8
|
+
|
9
|
+
# Only used here, so only required here
|
10
|
+
require 'docker/compose/shell_printer'
|
11
|
+
|
12
|
+
module Docker::Compose
|
13
|
+
class RakeTasks < Rake::TaskLib
|
14
|
+
# Set the directory in which docker-compose commands will be run. Default
|
15
|
+
# is the directory in which Rakefile is located.
|
16
|
+
#
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :dir
|
19
|
+
|
20
|
+
# Set the project name. Default is not to pass a custom name.
|
21
|
+
# @return [String]
|
22
|
+
attr_accessor :project_name
|
23
|
+
|
24
|
+
# Set the name of the docker-compose file. Default is`docker-compose.yml`.
|
25
|
+
# @return [String]
|
26
|
+
attr_accessor :file
|
27
|
+
|
28
|
+
# Provide a mapping of environment variables that should be set in
|
29
|
+
# _host_ processes, e.g. when running docker:compose:env or
|
30
|
+
# docker:compose:host.
|
31
|
+
#
|
32
|
+
# The values of the environment variables can refer to names of services
|
33
|
+
# and ports defined in the docker-compose file, and this gem will substitute
|
34
|
+
# the actual IP and port that the containers are reachable on. This allows
|
35
|
+
# commands invoked via "docker:compose:host" to reach services running
|
36
|
+
# inside containers.
|
37
|
+
#
|
38
|
+
# @see Docker::Compose::Mapper for information about the substitution syntax
|
39
|
+
attr_accessor :host_env
|
40
|
+
|
41
|
+
# Extra environment variables to set before invoking host processes. These
|
42
|
+
# are set _in addition_ to server_env, but are not substituted in any way
|
43
|
+
# and must not contain any service information.
|
44
|
+
#
|
45
|
+
# Extra host env should be disjoint from host_env; if there is overlap
|
46
|
+
# between the two, then extra_host_env will "win."
|
47
|
+
attr_accessor :extra_host_env
|
48
|
+
|
49
|
+
# Services to bring up with `docker-compose up` before running any hosted
|
50
|
+
# command. This is useful if your `docker-compose.yml` contains a service
|
51
|
+
# definition for the app you will be hosting; if you host the app, you
|
52
|
+
# want to specify all of the _other_ services, but not the app itself, since
|
53
|
+
# that will run on the host.
|
54
|
+
attr_accessor :host_services
|
55
|
+
|
56
|
+
# Namespace to define the rake tasks under. Defaults to "docker:compose'.
|
57
|
+
attr_accessor :rake_namespace
|
58
|
+
|
59
|
+
# Construct Rake wrapper tasks for docker-compose. If a block is given,
|
60
|
+
# yield self to the block before defining any tasks so their behavior
|
61
|
+
# can be configured by calling #server_env=, #file= and so forth.
|
62
|
+
def initialize
|
63
|
+
self.dir = Rake.application.original_dir
|
64
|
+
self.project_name = nil
|
65
|
+
self.file = 'docker-compose.yml'
|
66
|
+
self.host_env = {}
|
67
|
+
self.extra_host_env = {}
|
68
|
+
self.rake_namespace = 'docker:compose'
|
69
|
+
yield self if block_given?
|
70
|
+
|
71
|
+
@shell = Backticks::Runner.new
|
72
|
+
@session = Docker::Compose::Session.new(@shell, dir: dir, project_name: project_name, file: file)
|
73
|
+
@net_info = Docker::Compose::NetInfo.new
|
74
|
+
@shell_printer = Docker::Compose::ShellPrinter.new
|
75
|
+
|
76
|
+
@shell.interactive = true
|
77
|
+
|
78
|
+
define
|
79
|
+
end
|
80
|
+
|
81
|
+
def define
|
82
|
+
namespace rake_namespace do
|
83
|
+
desc 'Print bash exports with IP/ports of running services'
|
84
|
+
task :env do
|
85
|
+
@shell.interactive = false # suppress useless 'port' output
|
86
|
+
|
87
|
+
tty = STDOUT.tty?
|
88
|
+
tlt = Rake.application.top_level_tasks.include?(task_name('env'))
|
89
|
+
|
90
|
+
# user invoked this task directly; print some helpful tips on
|
91
|
+
# how we intend it to be used.
|
92
|
+
print_usage if tty && tlt
|
93
|
+
|
94
|
+
export_env(print: tlt)
|
95
|
+
|
96
|
+
@shell.interactive = true
|
97
|
+
end
|
98
|
+
|
99
|
+
desc 'Run command on host, linked to services in containers'
|
100
|
+
task :host, [:command] => [task_name('env')] do |_task, args|
|
101
|
+
if host_services
|
102
|
+
@session.up(*host_services, detached: true)
|
103
|
+
else
|
104
|
+
@session.up(detached: true)
|
105
|
+
end
|
106
|
+
|
107
|
+
exec(args[:command])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
private :define
|
112
|
+
|
113
|
+
# Substitute and set environment variables that point to network ports
|
114
|
+
# published by docker-compose services. Optionally also print bash export
|
115
|
+
# statements so this information can be made available to a user's shell.
|
116
|
+
def export_env(print:)
|
117
|
+
Docker::Compose::Mapper.map(host_env,
|
118
|
+
session: @session,
|
119
|
+
net_info: @net_info) do |k, v|
|
120
|
+
ENV[k] = serialize_for_env(v)
|
121
|
+
print_env(k, ENV[k]) if print
|
122
|
+
end
|
123
|
+
|
124
|
+
extra_host_env.each do |k, v|
|
125
|
+
ENV[k] = serialize_for_env(v)
|
126
|
+
print_env(k, ENV[k]) if print
|
127
|
+
end
|
128
|
+
end
|
129
|
+
private :export_env
|
130
|
+
|
131
|
+
# Transform a Ruby value into a String that can be stored in the
|
132
|
+
# environment. This accepts nil, String, or Array and returns nil, String
|
133
|
+
# or JSON-serialized Array.
|
134
|
+
def serialize_for_env(v)
|
135
|
+
case v
|
136
|
+
when String
|
137
|
+
v
|
138
|
+
when NilClass
|
139
|
+
nil
|
140
|
+
when Array
|
141
|
+
JSON.dump(v)
|
142
|
+
else
|
143
|
+
fail ArgumentError, "Can't represent a #{v.class} in the environment"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
private :serialize_for_env
|
147
|
+
|
148
|
+
# Print an export or unset statement suitable for user's shell
|
149
|
+
def print_env(k, v)
|
150
|
+
if v
|
151
|
+
puts @shell_printer.export(k, v)
|
152
|
+
else
|
153
|
+
puts @shell_printer.unset(k)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
private :print_env
|
157
|
+
|
158
|
+
def print_usage
|
159
|
+
command = "rake #{task_name('env')}"
|
160
|
+
command = 'bundle exec ' + command if defined?(Bundler)
|
161
|
+
puts @shell_printer.comment('To export these variables to your shell, run:')
|
162
|
+
puts @shell_printer.comment(@shell_printer.eval_output(command))
|
163
|
+
end
|
164
|
+
private :print_usage
|
165
|
+
|
166
|
+
def task_name(task)
|
167
|
+
[rake_namespace, task].join(':')
|
168
|
+
end
|
169
|
+
private :task_name
|
170
|
+
end
|
171
|
+
end
|