squared 0.5.11 → 0.6.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.
@@ -13,21 +13,22 @@ module Squared
13
13
  buildx: {
14
14
  common: %w[builder=b D|debug],
15
15
  build: %w[add-host=q annotation=q attest=q build-arg=qq build-context=qq cache-from=q cache-to=q
16
- cgroup-parent=b ent=q iidfile=p label=q a-file=p network=b no-cache-filter=b o|output=q platform=b
16
+ cgroup-parent=b iidfile=p label=q a-file=p network=b no-cache-filter=b o|output=q platform=b
17
17
  q|quiet secret=qq shm-size=b ssh=qq t|tag=b target=b ulimit=q].freeze,
18
18
  bake: %w[print list=q set=q].freeze,
19
19
  shared: %w[check load no-cache pull push allow=q call=b? f|file=p metadata-file=p progress=b provenance=q
20
20
  sbom=q].freeze
21
21
  }.freeze,
22
22
  compose: {
23
- common: %w[all-resources compatibility dry-run ansi|b env-file=p f|file=p parallel=n profile=b progress=b
23
+ common: %w[all-resources ansi|b compatibility dry-run env-file=p f|file=p parallel=n profile=b progress=b
24
24
  project-directory=p p|project-name=e].freeze,
25
25
  build: %w[check no-cache print pull push with-dependencies q|quiet build-arg=qq builder=b m|memory=b
26
26
  provenance=q sbom=q ssh=qq].freeze,
27
+ create: %w[build force-recreate no-build no-recreate quiet-pull remove-orphans y|yes pull=b scale=i].freeze,
27
28
  exec: %w[d|detach privileged e|env=qq index=i T|no-TTY=b? user=e w|workdir=q].freeze,
28
29
  run: %w[build d|detach no-deps q|quiet quiet-build quiet-pull remove-orphans rm P|service-ports use-aliases
29
- cap-add=b cap-drop=b q e|env=qq env-from-file=p i|interactive=b? l|label=q name=b T|no-TTY=b?
30
- p|publish=e pull=b u|user=e v|volume=q w|workdir=q].freeze,
30
+ cap-add=b cap-drop=b entrypoint=q e|env=qq env-from-file=p i|interactive=b? l|label=q name=b
31
+ T|no-TTY=b? p|publish=q pull=b u|user=e v|volume=q w|workdir=q].freeze,
31
32
  up: %w[abort-on-container-exit abort-on-container-failure always-recreate-deps attach-dependencies build
32
33
  d|detach force-recreate menu no-build no-color no-deps no-log-prefix no-recreate no-start quiet-build
33
34
  quiet-pull remove-orphans V|renew-anon-volumes timestamps wait w|watch y|yes attach=b
@@ -36,39 +37,40 @@ module Squared
36
37
  }.freeze,
37
38
  container: {
38
39
  create: %w[init i|interactive no-healthcheck oom-kill-disable privileged P|publish-all q|quiet read-only
39
- rm runtime t|tty use-api-socket io-maxbandwidth=b io-maxiops=b add-host=q annotation=q a|attach=b
40
- blkio-weight=i blkio-weight-device=i cap-add=b cap-drop=b cgroup-parent=b cgroupns=b cidfile=p
41
- device=q device-cgroup-rule=q device-read-bps=q device-read-iops=q device-write-bps=q
42
- device-write-iops=q disable-content-trust=b? dns=e dns-option=e dns-search=e domainname=b
43
- entrypoint=q e|env=qq env-file=p expose=e gpus=q group-add=b health-cmd=q health-interval=b ip6=e
44
- ipc=b isolation=b kernel-memory=b l|label=q label-file=p link=b link-local-ip=b log-driver=b
45
- log-opt=q mac-address=e m|memory=b memory-reservation=b memory-swap=n memory-swappiness=n
46
- mount=qq name=b network=b network-alias=b oom-score-adj=b pid=b pids-limit=n platform=b
47
- p|publish=e pull=b restart=b runtime=b security-opt=q shm-size=b stop-signal=b stop-timeout=i
48
- storage-opt=q sysctl=q tmpfs=q ulimit=q u|user=b userns=b uts=b v|volume=q volume-driver=b
49
- volumes-from=b w|workdir=q].freeze,
50
- run: %w[d|detach detach-keys=q sig-proxy=b?].freeze,
40
+ rm t|tty use-api-socket add-host=q annotation=q a|attach=b blkio-weight=i blkio-weight-device=i
41
+ cap-add=b cap-drop=b cgroup-parent=b cgroupns=b cidfile=p device=q device-cgroup-rule=q
42
+ device-read-bps=q device-read-iops=q device-write-bps=q device-write-iops=q
43
+ disable-content-trust=b? dns=q dns-option=q dns-search=q domainname=b entrypoint=q e|env=qq
44
+ env-file=p expose=q gpus=q group-add=b health-cmd=q health-interval=b health-retries=i
45
+ health-start-interval=q health-start-period=q health-timeout=q io-maxbandwidth=b io-maxiops=b
46
+ ip=b ip6=q ipc=b isolation=b kernel-memory=b l|label=q label-file=q link=b link-local-ip=q
47
+ log-driver=b log-opt=q mac-address=q m|memory=b memory-reservation=b memory-swap=n
48
+ memory-swappiness=n mount=qq name=b network=b network-alias=b oom-score-adj=b pid=b pids-limit=n
49
+ platform=b p|publish=q pull=b restart=b runtime=b security-opt=q shm-size=b stop-signal=b
50
+ stop-timeout=i storage-opt=q sysctl=q tmpfs=q ulimit=q u|user=b userns=b uts=b v|volume=q
51
+ volume-driver=b volumes-from=b w|workdir=q].freeze,
52
+ run: %w[d|detach detach-keys=q hostname=q sig-proxy=b?].freeze,
51
53
  update: %w[blkio-weight=i cpu-period=i cpu-quota=i cpu-rt-period=i cpu-rt-runtime=i c|cpu-shares=i cpus=f
52
54
  cpuset-cpus=b cpuset-mems=b m|memory=b memory-reservation=b memory-swap=b pids-limit=n
53
55
  restart=q].freeze,
54
56
  exec: %w[d|detach i|interactive privileged t|tty detach-keys=q e|env=qq env-file=p user=e
55
57
  w|workdir=q].freeze,
56
58
  commit: %w[a|author=q c|change=q m|message=q pause=b?].freeze,
57
- inspect: %w[s|size f|format=q].freeze,
59
+ inspect: %w[s|size f|format=q type=b].freeze,
58
60
  start: %w[a|attach i|interactive detach-keys=q].freeze,
59
- stop: %w[s|signal=b t|time=i t|timeout=i].freeze,
60
- restart: %w[s|signal=b t|time=i t|timeout=i].freeze,
61
+ stop: %w[s|signal=b t|timeout=i].freeze,
62
+ restart: %w[s|signal=b t|timeout=i].freeze,
61
63
  kill: %w[s|signal=b].freeze,
62
64
  stats: %w[a|all no-stream no-trunc format|q].freeze
63
65
  }.freeze,
64
66
  image: {
65
- list: %w[a|all q|quiet digests no-trunc tree f|filter=q format=q].freeze,
67
+ ls: %w[a|all digests no-trunc q|quiet tree f|filter=q format=q].freeze,
66
68
  push: %w[a|all-tags disable-content-trust=b? platform=b q|quiet].freeze,
67
69
  rm: %w[f|force no-prune platform=b].freeze,
68
70
  save: %w[o|output=p platform=b].freeze
69
71
  }.freeze,
70
72
  network: {
71
- connect: %w[alias=b driver-opt=q gw-priority=n ip=b ip6=b link=b link-local-ip=b].freeze,
73
+ connect: %w[alias=b driver-opt=q gw-priority=n ip=b ip6=q link=b link-local-ip=q].freeze,
72
74
  disconnect: %w[f|force].freeze
73
75
  }.freeze
74
76
  }.freeze
@@ -79,6 +81,15 @@ module Squared
79
81
  volume: %w[volume-subpath volume-nocopy volume-opt].freeze,
80
82
  tmpfs: %w[tmpfs-size tmpfs-mode].freeze,
81
83
  image: %w[image-path].freeze
84
+ }.freeze,
85
+ ls: {
86
+ compose: %w[Name Image Command Service RunningFor Status Ports CreatedAt ExitCode Health ID Labels
87
+ LocalVolumes Mounts Names Networks Project Publishers Size State].freeze,
88
+ container: %w[ID Image Command RunningFor Status Ports Names CreatedAt Labels LocalVolumes Mounts Networks
89
+ Platform Size State].freeze,
90
+ image: %w[Repository Tag ID Containers CreatedSince Size CreatedAt Digest SharedSize UniqueSize
91
+ VirtualSize].freeze,
92
+ network: %w[ID Name Driver Scope CreatedAt IPv4 IPv6 Internal Labels].freeze
82
93
  }.freeze
83
94
  }.freeze
84
95
  private_constant :COMPOSEFILE, :BAKEFILE, :OPT_DOCKER, :VAL_DOCKER
@@ -97,12 +108,13 @@ module Squared
97
108
 
98
109
  subtasks({
99
110
  'build' => %i[tag context].freeze,
100
- 'compose' => %i[build run exec up down].freeze,
111
+ 'compose' => %i[build create run exec up down service].freeze,
101
112
  'bake' => %i[build check].freeze,
102
- 'image' => %i[list rm push tag save].freeze,
113
+ 'image' => %i[ls rm push tag save].freeze,
103
114
  'container' => %i[run create exec update commit inspect diff start stop restart pause unpause top stats kill
104
115
  rm].freeze,
105
- 'network' => %i[connect disconnect].freeze
116
+ 'network' => %i[connect disconnect].freeze,
117
+ 'ls' => nil
106
118
  })
107
119
 
108
120
  attr_reader :context
@@ -135,114 +147,168 @@ module Squared
135
147
  Docker.subtasks do |action, flags|
136
148
  next if task_pass?(action)
137
149
 
138
- namespace action do
139
- flags.each do |flag|
140
- case action
141
- when 'build'
142
- case flag
143
- when :tag, :context
150
+ if flags.nil?
151
+ case action
152
+ when 'ls'
153
+ format_desc(action, nil, VAL_DOCKER[:ls].keys, after: 'a/ll?,s/tandard?,range*', arg: nil)
154
+ task action, [:command] do |_, args|
155
+ command = param_guard(action, 'command', args: args, key: :command)
156
+ args = args.extras
157
+ a = args.delete('a') || args.delete('all')
158
+ ls = case command
159
+ when 'network'
160
+ a = nil
161
+ 'ls'
162
+ when 'image', 'container'
163
+ 'ls'
164
+ when 'compose'
165
+ 'ps'
166
+ else
167
+ raise_error ArgumentError, 'unrecognized command', hint: command
168
+ end
169
+ data = VAL_DOCKER[:ls][command.to_sym]
170
+ if args.delete('s') || args.delete('standard')
171
+ cols = data.first(data.index('CreatedAt'))
172
+ else
173
+ cols = []
174
+ args.each do |val|
175
+ if val =~ /^(\d+)$/
176
+ cols << data[$1.to_i.pred]
177
+ elsif val =~ /^(\d+)(-|\.{2,3})(\d+)$/
178
+ j = $1.to_i.pred
179
+ k = $3.to_i - ($2 == '..' ? 2 : 1)
180
+ cols.concat(data[j..k]) if k > j
181
+ end
182
+ end
183
+ if cols.empty?
184
+ cols = choice_index('Select a column', data, multiple: true, force: true, attempts: 1)
185
+ end
186
+ end
187
+ cmd = docker_output(command, ls, a && '-a')
188
+ cmd << quote_option('format', "table #{cols.map! { |val| "{{.#{val}}}" }.join("\t")}")
189
+ run(cmd, banner: false, from: :ls)
190
+ end
191
+ end
192
+ else
193
+ namespace action do
194
+ flags.each do |flag|
195
+ case action
196
+ when 'build'
144
197
  format_desc(action, flag, 'opts*', before: flag == :tag ? 'name' : 'dir')
145
198
  task flag, [flag] do |_, args|
146
199
  param = param_guard(action, flag, args: args, key: flag)
147
200
  buildx(:build, args.extras, "#{flag}": param)
148
201
  end
149
- end
150
- when 'bake'
151
- break unless bake?
152
-
153
- case flag
154
- when :build
155
- format_desc action, flag, 'opts*,target*,context?|:'
156
- task flag do |_, args|
157
- args = args.to_a
158
- if args.first == ':'
159
- choice_command :bake
160
- else
161
- buildx :bake, args
202
+ when 'bake'
203
+ break unless bake?
204
+
205
+ case flag
206
+ when :build
207
+ format_desc action, flag, 'opts*,target*,context?|:'
208
+ task flag do |_, args|
209
+ args = args.to_a
210
+ if args.first == ':'
211
+ choice_command :bake
212
+ else
213
+ buildx :bake, args
214
+ end
162
215
  end
163
- end
164
- when :check
165
- format_desc action, flag, 'target'
166
- task flag, [:target] do |_, args|
167
- target = param_guard(action, flag, args: args, key: :target)
168
- buildx :bake, ['allow=fs.read=*', 'call=check', target]
169
- end
170
- end
171
- when 'compose'
172
- break unless compose?
173
-
174
- case flag
175
- when :build, :up, :down
176
- format_desc action, flag, 'opts*,service*|:'
177
- task flag do |_, args|
178
- compose! flag, args.to_a
179
- end
180
- when :exec, :run
181
- format_desc action, flag, "service|:,command#{flag == :exec ? '' : '?'}|::,args*,opts*"
182
- task flag, [:service] do |_, args|
183
- service = param_guard(action, flag, args: args, key: :service)
184
- compose!(flag, args.extras, service: service)
185
- end
186
- end
187
- when 'container'
188
- case flag
189
- when :exec, :commit
190
- format_desc(action, flag, flag == :exec ? 'id/name,opts*,args+|:' : 'id/name,tag?,opts*')
191
- task flag, [:id] do |_, args|
192
- if flag == :exec && !args.id
193
- choice_command flag
194
- else
195
- id = param_guard(action, flag, args: args, key: :id)
196
- container(flag, args.extras, id: id)
216
+ when :check
217
+ format_desc action, flag, 'target'
218
+ task flag, [:target] do |_, args|
219
+ target = param_guard(action, flag, args: args, key: :target)
220
+ buildx :bake, ['allow=fs.read=*', 'call=check', target]
197
221
  end
198
222
  end
199
- when :run, :create
200
- format_desc action, flag, 'image,opts*,args*|:'
201
- task flag, [:image] do |_, args|
202
- if args.image
203
- container(flag, args.extras, id: args.image)
204
- else
205
- choice_command flag
223
+ when 'compose'
224
+ break unless compose?
225
+
226
+ case flag
227
+ when :exec, :run
228
+ format_desc action, flag, "service|:,command#{flag == :exec ? '' : '?'}|::,args*,opts*"
229
+ task flag, [:service] do |_, args|
230
+ service = param_guard(action, flag, args: args, key: :service)
231
+ compose!(flag, args.extras, service: service)
232
+ end
233
+ when :service
234
+ cmds = %w[down kill pause restart rm start stop top unpause watch].freeze
235
+ format_desc(action, flag, cmds, arg: nil, after: 'name+|:')
236
+ task flag, [:command] do |_, args|
237
+ command = param_guard(action, flag, args: args, key: :command)
238
+ raise_error ArgumentError, 'unrecognized command', hint: command unless cmds.include?(command)
239
+ service = args.extras
240
+ if service.first == ':'
241
+ choice_command flag, command
242
+ else
243
+ compose!(flag, [command], service: service.empty? || service)
244
+ end
245
+ end
246
+ else
247
+ format_desc action, flag, 'opts*,service*|:'
248
+ task flag do |_, args|
249
+ compose!(flag, args.to_a, multiple: true)
206
250
  end
207
251
  end
208
- else
209
- format_desc action, flag, "opts*,id/name#{flag == :update ? '+' : '*'}"
210
- task flag do |_, args|
211
- container flag, args.to_a
252
+ when 'container'
253
+ case flag
254
+ when :exec, :commit
255
+ format_desc(action, flag, flag == :exec ? 'id/name,opts*,args+|:' : 'id/name,tag?,opts*')
256
+ task flag, [:id] do |_, args|
257
+ if flag == :exec && !args.id
258
+ choice_command flag
259
+ else
260
+ id = param_guard(action, flag, args: args, key: :id)
261
+ container(flag, args.extras, id: id)
262
+ end
263
+ end
264
+ when :run, :create
265
+ format_desc action, flag, 'image,opts*,args*|:'
266
+ task flag, [:image] do |_, args|
267
+ if args.image
268
+ container(flag, args.extras, id: args.image)
269
+ else
270
+ choice_command flag
271
+ end
272
+ end
273
+ else
274
+ format_desc action, flag, "opts*,id/name#{flag == :update ? '+' : '*'}"
275
+ task flag do |_, args|
276
+ container flag, args.to_a
277
+ end
212
278
  end
213
- end
214
- when 'image'
215
- case flag
216
- when :push
217
- format_desc action, flag, 'tag,registry/username?,opts*'
218
- task flag, [:tag] do |_, args|
219
- id = param_guard(action, flag, args: args, key: :tag)
220
- image(flag, args.extras, id: id)
279
+ when 'image'
280
+ case flag
281
+ when :push
282
+ format_desc action, flag, 'tag,registry/username?,opts*'
283
+ task flag, [:tag] do |_, args|
284
+ id = param_guard(action, flag, args: args, key: :tag)
285
+ image(flag, args.extras, id: id)
286
+ end
287
+ else
288
+ format_desc(action, flag, case flag
289
+ when :rm, :save then 'id*,opts*'
290
+ when :tag then 'version?'
291
+ else 'opts*,args*'
292
+ end)
293
+ task flag do |_, args|
294
+ args = args.to_a
295
+ if !args.empty? || flag == :ls
296
+ image flag, args
297
+ else
298
+ choice_command flag
299
+ end
300
+ end
221
301
  end
222
- else
223
- format_desc(action, flag, case flag
224
- when :rm, :save then 'id*,opts*'
225
- when :tag then 'version?'
226
- else 'opts*,args*'
227
- end)
228
- task flag do |_, args|
229
- args = args.to_a
230
- if args.empty? && flag != :list
231
- choice_command flag
302
+ when 'network'
303
+ format_desc action, flag, 'target,opts*'
304
+ task flag, [:target] do |_, args|
305
+ if args.target
306
+ network(flag, args.extras, target: args.target)
232
307
  else
233
- image flag, args
308
+ choice_command flag
234
309
  end
235
310
  end
236
311
  end
237
- when 'network'
238
- format_desc action, flag, 'target,opts*'
239
- task flag, [:target] do |_, args|
240
- if args.target
241
- network(flag, args.extras, target: args.target)
242
- else
243
- choice_command flag
244
- end
245
- end
246
312
  end
247
313
  end
248
314
  end
@@ -259,7 +325,7 @@ module Squared
259
325
  end
260
326
 
261
327
  def compose(opts, flags = nil, script: false, args: nil, from: :run, **)
262
- return opts if script == false
328
+ return opts unless script
263
329
 
264
330
  ret = docker_session
265
331
  if from == :run
@@ -285,10 +351,10 @@ module Squared
285
351
  when Enumerable
286
352
  ret.merge(opts.to_a)
287
353
  end
288
- [args, flags].each_with_index do |target, index|
289
- if (data = append_any(target, target: []))
290
- ret.merge(data.map { |arg| index == 0 ? fill_option(arg) : quote_option('build-arg', arg) })
291
- end
354
+ [args, flags].each_with_index do |item, i|
355
+ next unless item && (data = append_any(item, target: []))
356
+
357
+ ret.merge(data.map! { |arg| i == 0 ? fill_option(arg) : quote_option('build-arg', arg) })
292
358
  end
293
359
  case from
294
360
  when :run
@@ -311,8 +377,8 @@ module Squared
311
377
  end
312
378
  append_context
313
379
  when :bake, :compose
314
- option(from == :bake ? 'target' : 'service', ignore: false) do |a|
315
- ret.merge(split_escape(a).map! { |b| shell_quote(b) })
380
+ option(from == :bake ? 'target' : 'service', ignore: false) do |val|
381
+ ret.merge(split_escape(val).map! { |s| shell_quote(s) })
316
382
  end
317
383
  end
318
384
  ret
@@ -320,9 +386,11 @@ module Squared
320
386
 
321
387
  def buildx(flag, opts = [], tag: nil, context: nil)
322
388
  cmd, opts = docker_session('buildx', opts: opts)
323
- op = OptionPartition.new(opts, OPT_DOCKER[:buildx][:common], cmd, project: self)
324
- op << flag
325
- op.parse(OPT_DOCKER[:buildx][flag == :bake ? :bake : :build] + OPT_DOCKER[:buildx][:shared])
389
+ op = OPT_DOCKER[:buildx].yield_self do |data|
390
+ OptionPartition.new(opts, data[:common], cmd, project: self)
391
+ .append(flag, quote: false)
392
+ .parse(data[flag == :bake ? :bake : :build] + data[:shared])
393
+ end
326
394
  case flag
327
395
  when :build, :context
328
396
  append_tag(tag || option('tag', ignore: false) || self.tag)
@@ -346,46 +414,56 @@ module Squared
346
414
  run(from: :"buildx:#{flag}")
347
415
  end
348
416
 
349
- def compose!(flag, opts = [], service: nil)
350
- cmd, opts = docker_session('compose', opts: opts)
351
- op = OptionPartition.new(opts, OPT_DOCKER[:compose][:common], cmd, project: self)
352
- append_file filetype unless op.arg?('f', 'file')
353
- op << flag
354
- op.parse(OPT_DOCKER[:compose].fetch(flag, []))
355
- multiple = case flag
356
- when :build, :up, :down then true
357
- else false
358
- end
359
- if op.remove(':') || service == ':'
360
- keys = Set.new
361
- read_composefile('services', target: op.values_of('f', 'file')) { |data| keys.merge(data.keys) }
362
- service = unless keys.empty?
363
- choice_index('Add services', keys, multiple: multiple, force: !multiple,
364
- attempts: multiple ? 1 : 5)
365
- end
366
- end
367
- if multiple
368
- op.concat(service) if service
369
- op.append(delim: true, escape: true, strip: /^:/)
417
+ def compose!(flag, opts = [], service: nil, multiple: false)
418
+ from = :"compose:#{flag}"
419
+ if flag == :service
420
+ command = opts.first
421
+ if service == true
422
+ cmd, status = filter_ps command, from
423
+ lines = IO.popen(cmd.temp('--services')).map(&:strip).reject(&:empty?)
424
+ return list_empty(hint: status) if lines.empty?
425
+
426
+ service = choice_index('Choose a service', lines, multiple: true, force: true, attempts: 1)
427
+ end
428
+ docker_session('compose', command, '--', *service)
370
429
  else
371
- raise_error('no services were found', hint: flag) unless service
372
- append_command(flag, service, op.extras, prompt: '::')
430
+ cmd, opts = docker_session('compose', opts: opts)
431
+ op = OptionPartition.new(opts, OPT_DOCKER[:compose][:common], cmd, project: self)
432
+ append_file filetype unless op.arg?('f', 'file')
433
+ op << flag
434
+ op.parse(OPT_DOCKER[:compose].fetch(flag, []))
435
+ if op.remove(':') || service == ':'
436
+ keys = Set.new
437
+ read_composefile('services', target: op.values_of('f', 'file')) { |data| keys.merge(data.keys) }
438
+ service = unless keys.empty?
439
+ choice_index('Add services', keys, multiple: multiple, force: !multiple,
440
+ attempts: multiple ? 1 : 3)
441
+ end
442
+ end
443
+ if multiple
444
+ op.concat(service) if service
445
+ op.append(delim: true, escape: true, strip: /^:/)
446
+ else
447
+ raise_error ArgumentError, 'no service was selected', hint: flag unless service
448
+ append_command(flag, service, op.extras, prompt: '::')
449
+ end
373
450
  end
374
- run(from: :"compose:#{flag}")
451
+ run(from: from)
375
452
  end
376
453
 
377
454
  def container(flag, opts = [], id: nil)
378
455
  cmd, opts = docker_session('container', flag, opts: opts)
379
456
  rc = flag == :run || flag == :create
380
- list = OPT_DOCKER[:container].fetch(flag, [])
381
- list += OPT_DOCKER[:container][:create] if flag == :run
382
- list += OPT_DOCKER[:container][:update] if rc
383
- op = OptionPartition.new(opts, list, cmd, project: self, args: rc || flag == :exec)
457
+ op = OPT_DOCKER[:container].yield_self do |data|
458
+ list = data.fetch(flag, [])
459
+ list += data[:create] if flag == :run
460
+ list += data[:update] if rc
461
+ OptionPartition.new(opts, list, cmd, project: self, args: rc || flag == :exec)
462
+ end
384
463
  from = :"container:#{flag}"
385
464
  case flag
386
465
  when :run, :create, :exec
387
466
  if rc && !op.arg?('mount')
388
- run = VAL_DOCKER[:run]
389
467
  all = collect_hash VAL_DOCKER[:run]
390
468
  delim = Regexp.new(",\\s*(?=#{all.join('|')})")
391
469
  Array(@mounts).each do |val|
@@ -398,11 +476,11 @@ module Squared
398
476
  when 'bind', 'volume', 'image', 'tmpfs'
399
477
  type = v
400
478
  else
401
- raise_error("unknown type: #{v}", hint: flag)
479
+ raise_error TypeError, "unknown: #{v}", hint: flag
402
480
  end
403
481
  elsif all.include?(k)
404
482
  unless type
405
- run.each_pair do |key, val|
483
+ VAL_DOCKER[:run].each_pair do |key, val|
406
484
  next unless val.include?(k)
407
485
 
408
486
  type = key.to_s unless key == :common
@@ -422,18 +500,18 @@ module Squared
422
500
  log_message(Logger::INFO, 'unrecognized option', subject: from, hint: k)
423
501
  end
424
502
  end
425
- raise_error('missing type', hint: flag) unless type
503
+ raise_error TypeError, 'none specified', hint: flag unless type
426
504
  cmd << "--mount type=#{type},#{args.join(',')}"
427
505
  end
428
506
  end
429
507
  append_command(flag, id || tagmain, op.extras)
430
508
  when :update
431
- raise_error('missing container', hint: flag) if op.empty?
509
+ raise_error ArgumentError, 'missing container', hint: flag if op.empty?
432
510
  op.append(escape: true, strip: /^:/)
433
511
  when :commit
434
512
  latest = op.shift || tagmain
435
513
  cmd << id << latest
436
- raise_error("unknown args: #{op.join(', ')}", hint: flag) unless op.empty?
514
+ raise_error ArgumentError, "unrecognized args: #{op.join(', ')}", hint: flag unless op.empty?
437
515
  return unless confirm_command(cmd.to_s, title: from, target: id, as: latest)
438
516
 
439
517
  registry = option('registry') || @registry
@@ -452,38 +530,12 @@ module Squared
452
530
  return image(:push, opts, id: latest, registry: registry)
453
531
  else
454
532
  if op.empty?
455
- status = []
456
- no = true
457
- case flag
458
- when :inspect, :diff
459
- no = false
460
- when :start
461
- status = %w[created exited]
462
- no = false
463
- when :stop, :pause
464
- status = %w[running restarting]
465
- when :restart
466
- status = %w[running paused exited]
467
- when :unpause
468
- status << 'paused'
469
- no = false
470
- when :top, :stats
471
- status << 'running'
472
- cmd << '--no-stream' if flag == :stats
473
- no = false
474
- when :kill
475
- status = %w[running restarting paused]
476
- when :rm
477
- status = %w[created exited dead]
478
- end
479
- ps = docker_output('ps -a', *status.map { |s| quote_option('filter', "status=#{s}") })
480
- list_image(flag, ps, no: no, hint: "status: #{status.join(', ')}", from: from) do |img|
481
- run(cmd.temp(img), from: from)
482
- end
533
+ ps, status, no = filter_ps flag, from
534
+ cmd << '--no-stream' if flag == :stats
535
+ list_image(flag, ps, no: no, hint: status, from: from) { |img| run(cmd.temp(img), from: from) }
483
536
  return
484
- else
485
- op.append(escape: true, strip: /^:/)
486
537
  end
538
+ op.append(escape: true, strip: /^:/)
487
539
  end
488
540
  run(from: from)
489
541
  end
@@ -491,16 +543,21 @@ module Squared
491
543
  def image(flag, opts = [], sync: true, id: nil, registry: nil)
492
544
  cmd, opts = docker_session('image', flag, opts: opts)
493
545
  op = OptionPartition.new(opts, OPT_DOCKER[:image].fetch(flag, []), cmd, project: self)
494
- exception = @exception
546
+ exception = self.exception
495
547
  banner = true
496
548
  from = :"image:#{flag}"
497
549
  case flag
498
- when :list
550
+ when :ls
499
551
  if opts.size == op.size
500
552
  index = 0
501
553
  name = nil
502
- opts.reverse_each { |opt| break opts.delete(opt) if (name = opt[/^name=["']?(.+?)["']?$/, 1]) }
503
- list_image(:run, cmd << '-a', from: from) do |val|
554
+ opts.reverse_each do |opt|
555
+ if (name = opt[/^name=["']?(.+?)["']?$/, 1])
556
+ opts.delete(opt)
557
+ break
558
+ end
559
+ end
560
+ list_image(:run, from: from) do |val|
504
561
  container(:run, if name
505
562
  opts.dup << "name=#{index == 0 ? name : "#{name}-#{index}"}"
506
563
  else
@@ -509,19 +566,12 @@ module Squared
509
566
  index += 1
510
567
  end
511
568
  return
512
- else
513
- op.clear
514
569
  end
570
+ op.clear
515
571
  when :rm
516
- if id
517
- op << id
518
- if option('y')
519
- exception = false
520
- banner = false
521
- end
522
- else
572
+ unless id
523
573
  if op.empty?
524
- list_image(:rm, docker_output('image ls -a'), from: from) do |val|
574
+ list_image(:rm, from: from) do |val|
525
575
  image(:rm, opts, sync: sync, id: val)
526
576
  end
527
577
  else
@@ -529,8 +579,13 @@ module Squared
529
579
  end
530
580
  return
531
581
  end
582
+ op << id
583
+ if option('y')
584
+ exception = false
585
+ banner = false
586
+ end
532
587
  when :tag, :save
533
- list_image(flag, docker_output('image ls -a'), from: from) do |val|
588
+ list_image(flag, from: from) do |val|
534
589
  op << val
535
590
  if flag == :tag
536
591
  op << tagname("#{project}:#{op.first}")
@@ -540,8 +595,14 @@ module Squared
540
595
  when :push
541
596
  id ||= option('tag', ignore: false) || tagmain
542
597
  registry ||= op.shift || option('registry') || @registry
543
- raise_error(id ? "unknown args: #{op.join(', ')}" : 'no id/tag given', hint: flag) unless id && op.empty?
544
- raise_error('username/registry not provided', hint: flag) unless registry
598
+ unless id && op.empty?
599
+ if id
600
+ raise_error ArgumentError, "unrecognized args: #{op.join(', ')}", hint: flag
601
+ else
602
+ raise_error 'no id/tag', hint: flag
603
+ end
604
+ end
605
+ raise_error ArgumentError, 'username/registry not specified', hint: flag unless registry
545
606
  registry.chomp!('/')
546
607
  uri = shell_quote "#{registry}/#{id}"
547
608
  op << uri
@@ -553,17 +614,18 @@ module Squared
553
614
  exception = true
554
615
  banner = false
555
616
  end
556
- ret = run(cmd, sync: sync, exception: exception, banner: banner, from: from)
557
- print_success if success?(ret, flag == :tag || flag == :save)
617
+ run(cmd, sync: sync, exception: exception, banner: banner, from: from).tap do |ret|
618
+ success?(ret, flag == :tag || flag == :save)
619
+ end
558
620
  end
559
621
 
560
622
  def network(flag, opts = [], target: nil)
561
623
  cmd, opts = docker_session('network', flag, opts: opts)
562
- op = OptionPartition.new(opts, OPT_DOCKER[:network].fetch(flag, []), cmd, project: self)
563
- op.clear
624
+ OptionPartition.new(opts, OPT_DOCKER[:network].fetch(flag, []), cmd, project: self)
625
+ .clear
564
626
  from = :"network:#{flag}"
565
627
  list_image(flag, docker_output('ps -a'), from: from) do |img|
566
- print_success if success?(run(cmd.temp(target, img), from: from))
628
+ success?(run(cmd.temp(target, img), from: from))
567
629
  end
568
630
  end
569
631
 
@@ -590,10 +652,10 @@ module Squared
590
652
  def dockerfile(val = nil)
591
653
  if val
592
654
  @file = if val.is_a?(Array)
593
- val = val.select { |file| basepath(file).exist? }
655
+ val = val.select { |file| exist?(file) }
594
656
  val.size > 1 ? val : val.first
595
657
  elsif val == true
596
- DIR_DOCKER.find { |file| basepath(file).exist? }
658
+ DIR_DOCKER.find { |file| exist?(file) }
597
659
  elsif val != 'Dockerfile'
598
660
  val
599
661
  end
@@ -622,8 +684,7 @@ module Squared
622
684
  return session('docker', *cmd) unless opts
623
685
 
624
686
  op = OptionPartition.new(opts, OPT_DOCKER[:common], project: self)
625
- ret = session('docker', *op.to_a, *cmd)
626
- [ret, op.extras]
687
+ [session('docker', *op.to_a, *cmd), op.extras]
627
688
  end
628
689
 
629
690
  def docker_output(*cmd, **kwargs)
@@ -633,8 +694,8 @@ module Squared
633
694
  def append_command(flag, val, list, target: @session, prompt: ':')
634
695
  if list.delete(prompt)
635
696
  list << readline('Enter command [args]', force: flag == :exec)
636
- elsif (args = env('DOCKER_ARGS'))
637
- list << args
697
+ else
698
+ env('DOCKER_ARGS') { |args| list << args }
638
699
  end
639
700
  case flag
640
701
  when :run
@@ -642,7 +703,7 @@ module Squared
642
703
  target << basic_option('name', dnsname("#{name}_%s" % rand_s(6)))
643
704
  end
644
705
  when :exec
645
- raise_error('no command args', hint: flag) if list.empty?
706
+ raise_error ArgumentError, 'nothing to execute', hint: flag if list.empty?
646
707
  end
647
708
  target << val << list.shift
648
709
  target << list.join(' && ') unless list.empty?
@@ -694,17 +755,45 @@ module Squared
694
755
  end
695
756
  end
696
757
 
697
- def list_image(flag, cmd, hint: nil, from: nil, no: true)
758
+ def filter_ps(flag, from = :'container:ps')
759
+ no = false
760
+ status = case flag.to_sym
761
+ when :start
762
+ %w[created exited]
763
+ when :stop, :pause
764
+ no = true
765
+ %w[running restarting]
766
+ when :restart
767
+ no = true
768
+ %w[running paused exited]
769
+ when :unpause
770
+ %w[paused]
771
+ when :top, :stats, :watch
772
+ %w[running]
773
+ when :kill
774
+ no = true
775
+ %w[running paused restarting]
776
+ when :rm
777
+ no = true
778
+ %w[created exited dead]
779
+ else
780
+ []
781
+ end
782
+ cmd = docker_output("#{from.to_s.split(':').first} ps -a",
783
+ *status.map { |s| quote_option('filter', "status=#{s}") })
784
+ [cmd, status, no]
785
+ end
786
+
787
+ def list_image(flag, cmd = docker_output('image ls -a'), hint: nil, from: nil, no: true)
698
788
  pwd_set do
699
- found = false
700
- index = 0
789
+ index = 1
701
790
  all = option('all', prefix: 'docker')
702
791
  y = from == :'image:rm' && option('y', prefix: 'docker')
703
792
  pat = /\b(?:#{dnsname(name)}|#{tagname(project)}|#{tagmain.split(':', 2).first})\b/
704
- IO.popen(session_done(cmd << '--format=json')).each do |line|
793
+ IO.popen(cmd.temp('--format=json')).each do |line|
705
794
  data = JSON.parse(line)
706
795
  id = data['ID']
707
- rt = [data['Repository'], data['Tag']].reject { |val| val == '<none>' }.join(':')
796
+ rt = [data['Repository'], data['Tag']].reject { |val| val.to_s.empty? || val == '<none>' }.join(':')
708
797
  rt = nil if rt.empty?
709
798
  aa = data['Names'] || (if rt && data['Repository']
710
799
  dd = true
@@ -716,16 +805,16 @@ module Squared
716
805
  next unless all || ee.match?(pat) || aa.match?(pat)
717
806
 
718
807
  unless y
719
- bb = index.succ.to_s
720
- cc = bb.size + 1
808
+ bb = index.to_s
809
+ cc = bb.size.succ
721
810
  a = sub_style(ee, styles: theme[:inline])
722
- b = "Execute #{sub_style(flag, styles: theme[:active])} on #{a}#{ee == id ? '' : " (#{id})"}"
811
+ b = "Execute #{sub_style(flag, styles: theme[:active])} on #{a.subhint(ee == id ? nil : id)}"
723
812
  e = time_format(time_since(data['CreatedAt']), pass: ['ms'])
724
813
  f = sub_style(ARG[:BORDER][0], styles: theme[:inline])
725
- g = ' ' * (cc + 1)
814
+ g = ' ' * cc.succ
726
815
  h = "#{sub_style(bb.rjust(cc), styles: theme[:current])} #{f} "
727
- puts unless index == 0
728
- puts "#{h + sub_style(aa, styles: theme[:subject])} (created #{e} ago)"
816
+ puts unless index == 1
817
+ puts (h + sub_style(aa, styles: theme[:subject])).subhint("created #{e} ago")
729
818
  cols = %w[Tag Status Ports]
730
819
  cols << case flag
731
820
  when :connect, :disconnect
@@ -740,89 +829,104 @@ module Squared
740
829
  end
741
830
  w = 9 + flag.to_s.size + 4 + ee.size
742
831
  puts g + sub_style(ARG[:BORDER][6] + (ARG[:BORDER][1] * w), styles: theme[:inline])
743
- found = true
744
832
  index += 1
745
- next unless confirm("#{h + b}?", no ? 'N' : 'Y', timeout: 60)
833
+ next unless confirm("#{h + b}?", no ? 'N' : 'Y')
746
834
 
747
835
  puts if printfirst?
748
836
  end
749
837
  yield id
750
838
  end
751
- puts log_message(Logger::INFO, 'none detected', subject: name, hint: hint || from) if !found && !y
839
+ list_empty(hint: hint || from) if index == 1 && !y
752
840
  end
753
841
  rescue StandardError => e
754
842
  on_error e, from
755
843
  end
756
844
 
845
+ def list_empty(subject: name, hint: nil, **kwargs)
846
+ hint = "status: #{hint.join(', ')}" if hint.is_a?(Array)
847
+ puts log_message(Logger::INFO, 'none detected', subject: subject, hint: hint, **kwargs)
848
+ end
849
+
757
850
  def confirm_command(*args, title: nil, target: nil, as: nil)
758
851
  return false unless title && target
759
852
 
760
853
  puts unless printfirst?
761
854
  t = title.to_s.split(':')
762
855
  emphasize(args, title: message(t.first.upcase, *t.drop(1)), border: borderstyle, sub: [
763
- { pat: /\A(\w+(?: => \w+)+)(.*)\z/, styles: theme[:header] },
764
- { pat: /\A(.+)\z/, styles: theme[:caution] }
856
+ opt_style(theme[:header], /\A(\w+(?: => \w+)+)(.*)\z/),
857
+ opt_style(theme[:caution], /\A(.+)\z/)
765
858
  ])
766
859
  printsucc
767
860
  a = t.last.capitalize
768
861
  b = sub_style(target, styles: theme[:subject])
769
862
  c = as && sub_style(as, styles: theme[:inline])
770
- confirm("#{a} #{b}#{c ? " as #{c}" : ''}?", 'N', timeout: 60)
771
- end
772
-
773
- def choice_command(flag)
774
- msg, cmd, index = case flag
775
- when :exec
776
- ['Choose a container', 'ps -a', 0]
777
- when :bake
778
- ['Choose a target', 'buildx bake --list=type=targets', 0]
779
- when :connect, :disconnect
780
- ['Choose a network', 'network ls', 0]
781
- else
782
- ['Choose an image', 'images -a', 2]
783
- end
863
+ confirm "#{a} #{b}#{c ? " as #{c}" : ''}?", 'N'
864
+ end
865
+
866
+ def choice_command(flag, *action)
867
+ msg, cmd = case flag
868
+ when :exec
869
+ ['Choose a container', 'ps -a']
870
+ when :bake
871
+ ['Choose a target', 'buildx bake --list=type=targets']
872
+ when :connect, :disconnect
873
+ ['Choose a network', 'network ls']
874
+ when :service
875
+ ['Choose a service',
876
+ 'compose ps -a ' \
877
+ "--format='table {{.Service}}\t{{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Status}}\t{{.Ports}}'"]
878
+ else
879
+ ['Choose an image',
880
+ 'images -a ' \
881
+ "--format='table {{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.CreatedSince}}\t{{.Size}}'"]
882
+ end
784
883
  lines = `#{docker_output(cmd)}`.lines
785
884
  header = lines.shift
786
885
  if lines.empty?
787
- puts log_message(Logger::INFO, 'none found', subject: name, hint: "docker #{cmd}")
788
- else
789
- puts " # #{header}"
790
- multiple = false
791
- parse = ->(val) { val.split(/\s+/)[index] }
792
- ctx = flag.to_s
793
- case flag
794
- when :run, :exec
795
- values = [['Options', flag == :run], ['Arguments', flag == :exec]]
796
- when :rm, :bake
797
- values = ['Options']
798
- multiple = true
799
- ctx = flag == :rm ? 'image rm' : "buildx bake -f #{shell_quote(dockerfile)}"
800
- when :save
801
- values = [['Output', true], 'Platform']
802
- multiple = true
803
- when :connect, :disconnect
804
- values = ['Options', ['Container', true]]
805
- ctx = "network #{flag}"
806
- end
807
- out, opts, args = choice_index(msg, lines, multiple: multiple, values: values)
808
- cmd = docker_output ctx
809
- case flag
810
- when :tag
811
- args = tagjoin @registry, tag
812
- when :save
813
- opts = "#{opts}.tar" unless opts.end_with?('.tar')
814
- cmd << quote_option('output', File.expand_path(opts))
815
- if args
816
- cmd << basic_option('platform', args)
817
- args = nil
818
- end
819
- else
820
- cmd << opts << '--'
886
+ puts log_message(Logger::INFO, 'none found', subject: name,
887
+ hint: "docker #{cmd.split(' ', 3)[0...2].join(' ')}")
888
+ return
889
+ end
890
+ puts " # #{header}"
891
+ multiple = false
892
+ parse = ->(val) { val.split(/\s+/)[0] }
893
+ ctx = flag.to_s
894
+ case flag
895
+ when :run, :exec
896
+ values = [['Options', flag == :run], ['Arguments', flag == :exec]]
897
+ when :rm, :bake
898
+ values = ['Options']
899
+ multiple = true
900
+ ctx = flag == :rm ? 'image rm' : "buildx bake -f #{shell_quote(dockerfile)}"
901
+ when :save
902
+ values = [['Output', true], 'Platform']
903
+ multiple = true
904
+ when :service
905
+ values = []
906
+ multiple = true
907
+ ctx = 'compose'
908
+ when :connect, :disconnect
909
+ values = ['Options', ['Container', true]]
910
+ ctx = "network #{flag}"
911
+ end
912
+ out, opts, args = choice_index(msg, lines, multiple: multiple, values: values)
913
+ cmd = docker_output(ctx, *action)
914
+ case flag
915
+ when :tag
916
+ args = tagjoin @registry, tag
917
+ when :save
918
+ opts = "#{opts}.tar" unless opts.end_with?('.tar')
919
+ cmd << quote_option('output', File.expand_path(opts))
920
+ if args
921
+ cmd << basic_option('platform', args)
922
+ args = nil
821
923
  end
822
- cmd.merge(Array(out).map! { |val| parse.call(val) })
823
- cmd << args
824
- print_success if success?(run(cmd), ctx.start_with?(/(?:network|tag|save)/))
924
+ else
925
+ cmd << opts << '--'
825
926
  end
927
+ cmd.merge(Array(out).map! { |val| parse.call(val) })
928
+ cmd << args
929
+ success?(run(cmd), ctx.start_with?(/(?:network|tag|save)/))
826
930
  end
827
931
 
828
932
  def filetype(val = dockerfile)
@@ -851,10 +955,9 @@ module Squared
851
955
 
852
956
  def tagname(val)
853
957
  val = val.split(':').map! { |s| charname(s.sub(/^\W+/, '')) }
854
- val.join(':').yield_self do |s|
855
- s = val.first if val.size > 1 && s.size > 128
856
- s[0..127]
857
- end
958
+ ret = val.join(':')
959
+ ret = val.first if val.size > 1 && ret.size > 128
960
+ ret[0..127]
858
961
  end
859
962
 
860
963
  def dnsname(val)