docker-compose 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|