docker-compose 0.0.0

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