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.
@@ -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