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,354 @@
1
+ # encoding: utf-8
2
+ require 'backticks'
3
+ require 'yaml'
4
+
5
+ module Docker::Compose
6
+ # A Ruby OOP interface to a docker-compose session. A session is bound to
7
+ # a particular directory and docker-compose file (which are set at initialize
8
+ # time) and invokes whichever docker-compose command is resident in $PATH.
9
+ #
10
+ # Run docker-compose commands by calling instance methods of this class and
11
+ # passing kwargs that are equivalent to the CLI options you would pass to
12
+ # the command-line tool.
13
+ #
14
+ # Note that the Ruby command methods usually expose a _subset_ of the options
15
+ # allowed by the docker-compose CLI, and that options are sometimes renamed
16
+ # for clarity, e.g. the "-d" flag always becomes the "detached:" kwarg.
17
+ class Session
18
+ # A Regex that matches all ANSI escape sequences.
19
+ ANSI = /\033\[([0-9];?)+[a-z]/
20
+
21
+ # Working directory (determines compose project name); default is Dir.pwd
22
+ attr_reader :dir
23
+
24
+ # Project name; default is not to pass a custom name
25
+ attr_reader :project_name
26
+
27
+ # Project file; default is 'docker-compose.yml'
28
+ attr_reader :file
29
+
30
+ # Reference to the last executed command.
31
+ attr_reader :last_command
32
+
33
+ def initialize(shell = Backticks::Runner.new(buffered: [:stderr], interactive: true),
34
+ dir: Dir.pwd, project_name: nil, file: 'docker-compose.yml')
35
+ @shell = shell
36
+ @project_name = project_name
37
+ @dir = dir
38
+ @file = file
39
+ @last_command = nil
40
+ end
41
+
42
+ # Validate docker-compose file and return it as Hash
43
+ # @return [Hash] the docker-compose config file
44
+ # @raise [Error] if command fails
45
+ def config(*args)
46
+ config = strip_ansi(run!('config', *args))
47
+ YAML.load(config)
48
+ end
49
+
50
+ # Monitor the logs of one or more containers.
51
+ # @param [Array] services list of String service names to show logs for
52
+ # @return [true] always returns true
53
+ # @raise [Error] if command fails
54
+ def logs(*services)
55
+ run!('logs', services)
56
+ true
57
+ end
58
+
59
+ def ps(*services)
60
+ inter = @shell.interactive
61
+ @shell.interactive = false
62
+
63
+ lines = strip_ansi(run!('ps', {q: true}, services)).split(/[\r\n]+/)
64
+ containers = Collection.new
65
+
66
+ lines.each do |id|
67
+ containers << docker_ps(strip_ansi(id))
68
+ end
69
+
70
+ containers
71
+ ensure
72
+ @shell.interactive = inter
73
+ end
74
+
75
+ # Idempotently up the given services in the project.
76
+ # @param [Array] services list of String service names to run
77
+ # @param [Boolean] detached if true, to start services in the background;
78
+ # otherwise, monitor logs in the foreground and shutdown on Ctrl+C
79
+ # @param [Integer] timeout how long to wait for each service to start
80
+ # @param [Boolean] build if true, build images before starting containers
81
+ # @param [Boolean] no_build if true, don't build images, even if they're
82
+ # missing
83
+ # @param [Boolean] no_deps if true, just run specified services without
84
+ # running the services that they depend on
85
+ # @return [true] always returns true
86
+ # @raise [Error] if command fails
87
+ def up(*services,
88
+ abort_on_container_exit: false,
89
+ detached: false, timeout: 10, build: false,
90
+ exit_code_from: nil,
91
+ no_build: false, no_deps: false, no_start: false)
92
+ o = opts(
93
+ abort_on_container_exit: [abort_on_container_exit, false],
94
+ d: [detached, false],
95
+ timeout: [timeout, 10],
96
+ build: [build, false],
97
+ exit_code_from: [exit_code_from, nil],
98
+ no_build: [no_build, false],
99
+ no_deps: [no_deps, false],
100
+ no_start: [no_start, false]
101
+ )
102
+ run!('up', o, services)
103
+ true
104
+ end
105
+
106
+ # Idempotently scales the number of containers for given services in the project.
107
+ # @param [Hash] container_count per service, e.g. {web: 2, worker: 3}
108
+ # @param [Integer] timeout how long to wait for each service to scale
109
+ def scale(container_count, timeout: 10)
110
+ args = container_count.map {|service, count| "#{service}=#{count}"}
111
+ o = opts(timeout: [timeout, 10])
112
+ run!('scale', o, *args)
113
+ end
114
+
115
+ # Take the stack down
116
+ def down(remove_volumes: false)
117
+ run!('down', opts(v: [!!remove_volumes, false]))
118
+ end
119
+
120
+ # Pull images of services
121
+ # @param [Array] services list of String service names to pull
122
+ def pull(*services)
123
+ run!('pull', *services)
124
+ end
125
+
126
+ def rm(*services, force: false, volumes: false)
127
+ o = opts(f: [force, false], v: [volumes, false])
128
+ run!('rm', o, services)
129
+ end
130
+
131
+ # Idempotently run an arbitrary command with a service container.
132
+ # @param [String] service name to run
133
+ # @param [String] cmd command statement to run
134
+ # @param [Boolean] detached if true, to start services in the background;
135
+ # otherwise, monitor logs in the foreground and shutdown on Ctrl+C
136
+ # @param [Boolean] no_deps if true, just run specified services without
137
+ # running the services that they depend on
138
+ # @param [Array] env a list of environment variables (see: -e flag)
139
+ # @param [Array] volumes a list of volumes to bind mount (see: -v flag)
140
+ # @param [Boolean] rm remove the container when done
141
+ # @param [Boolean] no_tty disable pseudo-tty allocation (see: -T flag)
142
+ # @param [String] user run as specified username or uid (see: -u flag)
143
+ # @raise [Error] if command fails
144
+ def run(service, *cmd, detached: false, no_deps: false, volumes: [], env: [], rm: false, no_tty: false, user: nil, service_ports: false)
145
+ o = opts(d: [detached, false], no_deps: [no_deps, false], rm: [rm, false], T: [no_tty, false], u: [user, nil], service_ports: [service_ports, false])
146
+ env_params = env.map { |v| { e: v } }
147
+ volume_params = volumes.map { |v| { v: v } }
148
+ run!('run', o, *env_params, *volume_params, service, cmd)
149
+ end
150
+
151
+ def restart(*services, timeout:10)
152
+ o = opts(timeout: [timeout, 10])
153
+ run!('restart', o, *services)
154
+ end
155
+
156
+ # Pause running services.
157
+ # @param [Array] services list of String service names to run
158
+ def pause(*services)
159
+ run!('pause', *services)
160
+ end
161
+
162
+ # Unpause running services.
163
+ # @param [Array] services list of String service names to run
164
+ def unpause(*services)
165
+ run!('unpause', *services)
166
+ end
167
+
168
+ # Stop running services.
169
+ # @param [Array] services list of String service names to stop
170
+ # @param [Integer] timeout how long to wait for each service to stop
171
+ # @raise [Error] if command fails
172
+ def stop(*services, timeout: 10)
173
+ o = opts(timeout: [timeout, 10])
174
+ run!('stop', o, services)
175
+ end
176
+
177
+ # Forcibly stop running services.
178
+ # @param [Array] services list of String service names to stop
179
+ # @param [String] name of murderous signal to use, default is 'KILL'
180
+ # @see Signal.list for a list of acceptable signal names
181
+ def kill(*services, signal: 'KILL')
182
+ o = opts(signal: [signal, 'KILL'])
183
+ run!('kill', o, services)
184
+ end
185
+
186
+ # Figure out which interface(s) and port a given service port has been published to.
187
+ #
188
+ # **NOTE**: if Docker Compose is communicating with a remote Docker host, this method
189
+ # returns IP addresses from the point of view of *that* host and its interfaces. If
190
+ # you need to know the address as reachable from localhost, you probably want to use
191
+ # `Mapper`.
192
+ #
193
+ # @see Docker::Compose::Mapper
194
+ #
195
+ # @param [String] service name of service from docker-compose.yml
196
+ # @param [Integer] port number of port
197
+ # @param [String] protocol 'tcp' or 'udp'
198
+ # @param [Integer] index of container (if multiple instances running)
199
+ # @return [String,nil] an ip:port pair such as "0.0.0.0:32176" or nil if the service is not running
200
+ # @raise [Error] if command fails
201
+ def port(service, port, protocol: 'tcp', index: 1)
202
+ inter = @shell.interactive
203
+ @shell.interactive = false
204
+
205
+ o = opts(protocol: [protocol, 'tcp'], index: [index, 1])
206
+ s = strip_ansi(run!('port', o, service, port).strip)
207
+ (!s.empty? && s) || nil
208
+ rescue Error => e
209
+ # Deal with docker-compose v1.11+
210
+ if e.detail =~ /No container found/i
211
+ nil
212
+ else
213
+ raise
214
+ end
215
+ ensure
216
+ @shell.interactive = inter
217
+ end
218
+
219
+ # Determine the installed version of docker-compose.
220
+ # @param [Boolean] short whether to return terse version information
221
+ # @return [String, Hash] if short==true, returns a version string;
222
+ # otherwise, returns a Hash of component-name strings to version strings
223
+ # @raise [Error] if command fails
224
+ def version(short: false)
225
+ o = opts(short: [short, false])
226
+ result = run!('version', o, file: false, dir: false)
227
+
228
+ if short
229
+ result.strip
230
+ else
231
+ lines = result.split(/[\r\n]+/)
232
+ lines.inject({}) do |h, line|
233
+ kv = line.split(/: +/, 2)
234
+ h[kv.first] = kv.last
235
+ h
236
+ end
237
+ end
238
+ end
239
+
240
+ def build(*services, force_rm: false, no_cache: false, pull: false)
241
+ o = opts(force_rm: [force_rm, false],
242
+ no_cache: [no_cache, false],
243
+ pull: [pull, false])
244
+ result = run!('build', o, services)
245
+ end
246
+
247
+ # Run a docker-compose command without validating that the CLI parameters
248
+ # make sense. Prepend project and file options if suitable.
249
+ #
250
+ # @see Docker::Compose::Shell#command
251
+ #
252
+ # @param [Array] args command-line arguments in the format accepted by
253
+ # Backticks::Runner#command
254
+ # @return [String] output of the command
255
+ # @raise [Error] if command fails
256
+ def run!(*args)
257
+ project_name_args = if @project_name
258
+ [{ project_name: @project_name }]
259
+ else
260
+ []
261
+ end
262
+ file_args = case @file
263
+ when 'docker-compose.yml'
264
+ []
265
+ when Array
266
+ # backticks sugar can't handle array values; build a list of hashes
267
+ # IMPORTANT: preserve the order of the files so overrides work correctly
268
+ file_args = @file.map { |filepath| { :file => filepath } }
269
+ else
270
+ # a single String (or Pathname, etc); use normal sugar to add it
271
+ [{ file: @file.to_s }]
272
+ end
273
+
274
+ @shell.chdir = dir
275
+ @last_command = @shell.run('docker-compose', *project_name_args, *file_args, *args).join
276
+ status = @last_command.status
277
+ out = @last_command.captured_output
278
+ err = @last_command.captured_error
279
+ status.success? || fail(Error.new(args.first, status, out+err))
280
+ out
281
+ end
282
+
283
+ private
284
+
285
+ def docker_ps(id)
286
+ cmd = @shell.run('docker', 'ps', a: true, f: "id=#{id}", no_trunc: true, format: Container::PS_FMT).join
287
+ status, out, err = cmd.status, cmd.captured_output, cmd.captured_error
288
+ raise Error.new('docker ps', status, out+err) unless status.success?
289
+ lines = out.split(/[\r\n]+/)
290
+ return nil if lines.empty?
291
+ l = strip_ansi(lines.shift)
292
+ m = parse(l)
293
+ raise Error.new('docker ps', status, "Cannot parse output: '#{l}'") unless m
294
+ raise Error.new('docker ps', status, "Cannot parse output: '#{l}'") unless m.size == 7
295
+ return Container.new(*m)
296
+ end
297
+
298
+ # strip default-valued options. the value of each kw should be a pair:
299
+ # [0] is present value
300
+ # [1] is default value
301
+ def opts(**kws)
302
+ res = {}
303
+ kws.each_pair do |kw, v|
304
+ res[kw] = v[0] unless v[0] == v[1]
305
+ end
306
+ res
307
+ end
308
+
309
+ # strip all ANSI escape sequences from str
310
+ def strip_ansi(str)
311
+ str.gsub(ANSI, '')
312
+ end
313
+
314
+ # Parse a string that consists of a sequence of values enclosed within parentheses.
315
+ # Ignore any bytes that are outside of parentheses. Values may include nested parentheses.
316
+ #
317
+ # @param [String] str e.g. "(foo) ((bar)) ... (baz)"
318
+ # @return [Array] e.g. ["foo", "bar", "baz"]
319
+ def parse(str)
320
+ fields = []
321
+ nest = 0
322
+ field = ''
323
+ str.each_char do |ch|
324
+ got = false
325
+ if nest == 0
326
+ if ch == '('
327
+ nest += 1
328
+ end
329
+ else
330
+ if ch == '('
331
+ nest += 1
332
+ field << ch
333
+ elsif ch == ')'
334
+ nest -= 1
335
+ if nest == 0
336
+ got = true
337
+ else
338
+ field << ch
339
+ end
340
+ else
341
+ field << ch
342
+ end
343
+ end
344
+
345
+ if got
346
+ fields << field
347
+ field = ''
348
+ end
349
+ end
350
+
351
+ fields
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+ module Docker::Compose::ShellPrinter
3
+ # Printer that works with the Friendly Interactive Shell (fish).
4
+ class Fish < Posix
5
+ def eval_output(command)
6
+ format('eval (%s)', command)
7
+ end
8
+
9
+ def export(name, value)
10
+ format('set -gx %s %s; ', name, single_quoted_escaped(value))
11
+ end
12
+
13
+ def unset(name)
14
+ format('set -ex %s; ', name)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ module Docker::Compose::ShellPrinter
3
+ # Printer that works with any POSIX-compliant shell e.g. sh, bash, zsh
4
+ class Posix
5
+ def comment(value)
6
+ format('# %s', value)
7
+ end
8
+
9
+ def eval_output(command)
10
+ format('eval "$(%s)"', command)
11
+ end
12
+
13
+ def export(name, value)
14
+ format('export %s=%s', name, single_quoted_escaped(value))
15
+ end
16
+
17
+ def unset(name)
18
+ format('unset %s', name)
19
+ end
20
+
21
+ protected def single_quoted_escaped(value)
22
+ # "escape" any occurrences of ' in value by closing the single-quoted
23
+ # string, concatenating a single backslash-escaped ' character, and reopening
24
+ # the single-quoted string.
25
+ #
26
+ # This relies on the shell's willingness to concatenate adjacent string
27
+ # literals. Tested under sh, bash and zsh; should work elsewhere.
28
+ escaped = value.gsub("'") { "'\\''" }
29
+
30
+ "'#{escaped}'"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ require 'etc'
3
+
4
+ module Docker::Compose
5
+ module ShellPrinter
6
+ def self.new
7
+ shell = Etc.getpwuid(Process.uid).shell
8
+ basename = File.basename(shell)
9
+
10
+ # Crappy titleize (bash -> Bash)
11
+ basename[0] = basename[0].upcase
12
+
13
+ # Find adapter class named after shell; default to posix if nothing found
14
+ klass = begin
15
+ const_get(basename.to_sym)
16
+ rescue
17
+ Posix
18
+ end
19
+
20
+ klass.new
21
+ end
22
+ end
23
+ end
24
+
25
+ require_relative 'shell_printer/posix'
26
+ require_relative 'shell_printer/fish'
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+ module Docker
3
+ module Compose
4
+ VERSION = '0.0.0'
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+ require 'net/http'
3
+
4
+ require_relative 'compose/version'
5
+ require_relative 'compose/error'
6
+ require_relative 'compose/container'
7
+ require_relative 'compose/collection'
8
+ require_relative 'compose/session'
9
+ require_relative 'compose/net_info'
10
+ require_relative 'compose/mapper'
11
+
12
+ module Docker
13
+ module Compose
14
+ # Create a new session with default options.
15
+ def self.new
16
+ Session.new
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: docker-compose
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tony Spataro
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-05-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: backticks
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Provides an OOP interface to docker-compose and facilitates container-to-host
70
+ and host-to-container networking.
71
+ email:
72
+ - xeger@xeger.net
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".coveralls.yml"
78
+ - ".github/workflows/publish.yml"
79
+ - ".gitignore"
80
+ - ".rspec"
81
+ - ".rubocop.yml"
82
+ - ".ruby-version"
83
+ - ".travis.yml"
84
+ - CHANGELOG.md
85
+ - CODE_OF_CONDUCT.md
86
+ - Gemfile
87
+ - LICENSE.txt
88
+ - README.md
89
+ - Rakefile
90
+ - bin/console
91
+ - bin/setup
92
+ - docker-compose.gemspec
93
+ - docker-compose.yml
94
+ - lib/docker/compose.rb
95
+ - lib/docker/compose/collection.rb
96
+ - lib/docker/compose/container.rb
97
+ - lib/docker/compose/error.rb
98
+ - lib/docker/compose/mapper.rb
99
+ - lib/docker/compose/net_info.rb
100
+ - lib/docker/compose/rake_tasks.rb
101
+ - lib/docker/compose/session.rb
102
+ - lib/docker/compose/shell_printer.rb
103
+ - lib/docker/compose/shell_printer/fish.rb
104
+ - lib/docker/compose/shell_printer/posix.rb
105
+ - lib/docker/compose/version.rb
106
+ homepage: https://github.com/xeger/docker-compose
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '2.0'
119
+ - - "<"
120
+ - !ruby/object:Gem::Version
121
+ version: '4.0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubygems_version: 3.2.3
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Wrapper docker-compose with added Rake smarts.
132
+ test_files: []