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,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: []