squared 0.5.25 → 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,17 +13,18 @@ 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 iidfile=p label=q network=b no-cache-filter=b o|output=q platform=q
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
30
  cap-add=b cap-drop=b entrypoint=q e|env=qq env-from-file=p i|interactive=b? l|label=q name=b
@@ -41,14 +42,14 @@ module Squared
41
42
  device-read-bps=q device-read-iops=q device-write-bps=q device-write-iops=q
42
43
  disable-content-trust=b? dns=q dns-option=q dns-search=q domainname=b entrypoint=q e|env=qq
43
44
  env-file=p expose=q gpus=q group-add=b health-cmd=q health-interval=b health-retries=i
44
- health-start-interval=q health-start-period=q health-timeout=q hostname=q io-maxbandwidth=b
45
- io-maxiops=b ip=b ip6=q ipc=b isolation=b kernel-memory=b l|label=q label-file=q link=b
46
- link-local-ip=q log-driver=b log-opt=q mac-address=q m|memory=b memory-reservation=b
47
- memory-swap=n memory-swappiness=n mount=qq name=b network=b network-alias=b oom-score-adj=b
48
- pid=b pids-limit=n platform=q p|publish=q pull=b restart=b runtime=b security-opt=q shm-size=b
49
- stop-signal=b stop-timeout=i storage-opt=q sysctl=q tmpfs=q ulimit=q u|user=b userns=b uts=b
50
- v|volume=q volume-driver=b volumes-from=b w|workdir=q].freeze,
51
- run: %w[d|detach detach-keys=q sig-proxy=b?].freeze,
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,
52
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
53
54
  cpuset-cpus=b cpuset-mems=b m|memory=b memory-reservation=b memory-swap=b pids-limit=n
54
55
  restart=q].freeze,
@@ -57,16 +58,16 @@ module Squared
57
58
  commit: %w[a|author=q c|change=q m|message=q pause=b?].freeze,
58
59
  inspect: %w[s|size f|format=q type=b].freeze,
59
60
  start: %w[a|attach i|interactive detach-keys=q].freeze,
60
- stop: %w[s|signal=b t|time=i t|timeout=i].freeze,
61
- 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,
62
63
  kill: %w[s|signal=b].freeze,
63
64
  stats: %w[a|all no-stream no-trunc format|q].freeze
64
65
  }.freeze,
65
66
  image: {
66
- list: %w[a|all q|quiet digests no-trunc tree f|filter=q format=q].freeze,
67
- push: %w[a|all-tags disable-content-trust=b? platform=q q|quiet].freeze,
68
- rm: %w[f|force no-prune platform=q].freeze,
69
- save: %w[o|output=p platform=q].freeze
67
+ ls: %w[a|all digests no-trunc q|quiet tree f|filter=q format=q].freeze,
68
+ push: %w[a|all-tags disable-content-trust=b? platform=b q|quiet].freeze,
69
+ rm: %w[f|force no-prune platform=b].freeze,
70
+ save: %w[o|output=p platform=b].freeze
70
71
  }.freeze,
71
72
  network: {
72
73
  connect: %w[alias=b driver-opt=q gw-priority=n ip=b ip6=q link=b link-local-ip=q].freeze,
@@ -80,6 +81,15 @@ module Squared
80
81
  volume: %w[volume-subpath volume-nocopy volume-opt].freeze,
81
82
  tmpfs: %w[tmpfs-size tmpfs-mode].freeze,
82
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
83
93
  }.freeze
84
94
  }.freeze
85
95
  private_constant :COMPOSEFILE, :BAKEFILE, :OPT_DOCKER, :VAL_DOCKER
@@ -98,12 +108,13 @@ module Squared
98
108
 
99
109
  subtasks({
100
110
  'build' => %i[tag context].freeze,
101
- 'compose' => %i[build run exec up down].freeze,
111
+ 'compose' => %i[build create run exec up down service].freeze,
102
112
  'bake' => %i[build check].freeze,
103
- 'image' => %i[list rm push tag save].freeze,
113
+ 'image' => %i[ls rm push tag save].freeze,
104
114
  'container' => %i[run create exec update commit inspect diff start stop restart pause unpause top stats kill
105
115
  rm].freeze,
106
- 'network' => %i[connect disconnect].freeze
116
+ 'network' => %i[connect disconnect].freeze,
117
+ 'ls' => nil
107
118
  })
108
119
 
109
120
  attr_reader :context
@@ -136,114 +147,168 @@ module Squared
136
147
  Docker.subtasks do |action, flags|
137
148
  next if task_pass?(action)
138
149
 
139
- namespace action do
140
- flags.each do |flag|
141
- case action
142
- when 'build'
143
- case flag
144
- 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'
145
197
  format_desc(action, flag, 'opts*', before: flag == :tag ? 'name' : 'dir')
146
198
  task flag, [flag] do |_, args|
147
199
  param = param_guard(action, flag, args: args, key: flag)
148
200
  buildx(:build, args.extras, "#{flag}": param)
149
201
  end
150
- end
151
- when 'bake'
152
- break unless bake?
153
-
154
- case flag
155
- when :build
156
- format_desc action, flag, 'opts*,target*,context?|:'
157
- task flag do |_, args|
158
- args = args.to_a
159
- if args.first == ':'
160
- choice_command :bake
161
- else
162
- 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
163
215
  end
164
- end
165
- when :check
166
- format_desc action, flag, 'target'
167
- task flag, [:target] do |_, args|
168
- target = param_guard(action, flag, args: args, key: :target)
169
- buildx :bake, ['allow=fs.read=*', 'call=check', target]
170
- end
171
- end
172
- when 'compose'
173
- break unless compose?
174
-
175
- case flag
176
- when :build, :up, :down
177
- format_desc action, flag, 'opts*,service*|:'
178
- task flag do |_, args|
179
- compose! flag, args.to_a
180
- end
181
- when :exec, :run
182
- format_desc action, flag, "service|:,command#{flag == :exec ? '' : '?'}|::,args*,opts*"
183
- task flag, [:service] do |_, args|
184
- service = param_guard(action, flag, args: args, key: :service)
185
- compose!(flag, args.extras, service: service)
186
- end
187
- end
188
- when 'container'
189
- case flag
190
- when :exec, :commit
191
- format_desc(action, flag, flag == :exec ? 'id/name,opts*,args+|:' : 'id/name,tag?,opts*')
192
- task flag, [:id] do |_, args|
193
- if flag == :exec && !args.id
194
- choice_command flag
195
- else
196
- id = param_guard(action, flag, args: args, key: :id)
197
- 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]
198
221
  end
199
222
  end
200
- when :run, :create
201
- format_desc action, flag, 'image,opts*,args*|:'
202
- task flag, [:image] do |_, args|
203
- if args.image
204
- container(flag, args.extras, id: args.image)
205
- else
206
- 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)
207
250
  end
208
251
  end
209
- else
210
- format_desc action, flag, "opts*,id/name#{flag == :update || flag == :inspect ? '+' : '*'}"
211
- task flag do |_, args|
212
- 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
213
278
  end
214
- end
215
- when 'image'
216
- case flag
217
- when :push
218
- format_desc action, flag, 'tag,registry/username?,opts*'
219
- task flag, [:tag] do |_, args|
220
- id = param_guard(action, flag, args: args, key: :tag)
221
- 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
222
301
  end
223
- else
224
- format_desc(action, flag, case flag
225
- when :rm, :save then 'id*,opts*'
226
- when :tag then 'version?'
227
- else 'opts*,args*'
228
- end)
229
- task flag do |_, args|
230
- args = args.to_a
231
- if args.empty? && flag != :list
232
- 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)
233
307
  else
234
- image flag, args
308
+ choice_command flag
235
309
  end
236
310
  end
237
311
  end
238
- when 'network'
239
- format_desc action, flag, 'target,opts*'
240
- task flag, [:target] do |_, args|
241
- if args.target
242
- network(flag, args.extras, target: args.target)
243
- else
244
- choice_command flag
245
- end
246
- end
247
312
  end
248
313
  end
249
314
  end
@@ -254,28 +319,29 @@ module Squared
254
319
  def clean(*, sync: invoked_sync?('clean'), **)
255
320
  if runnable?(@clean)
256
321
  super
257
- elsif sync || option('y', prefix: 'docker')
258
- image :rm
322
+ else
323
+ image(:rm, sync: sync)
259
324
  end
260
325
  end
261
326
 
262
327
  def compose(opts, flags = nil, script: false, args: nil, from: :run, **)
263
- return opts if script == false
328
+ return opts unless script
264
329
 
330
+ ret = docker_session
265
331
  if from == :run
266
332
  if bake?(n = filetype)
267
- ret = docker_session 'buildx bake'
333
+ ret << 'buildx bake'
268
334
  append_file n
269
335
  from = :bake
270
336
  elsif compose?(n)
271
- ret = docker_session 'compose build'
337
+ ret << 'compose build'
272
338
  append_file n
273
339
  from = :compose
274
340
  else
275
- ret = docker_session 'build'
341
+ ret << 'build'
276
342
  end
277
343
  else
278
- ret = docker_session from
344
+ ret << from
279
345
  end
280
346
  case opts
281
347
  when String
@@ -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,57 +386,80 @@ 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)
329
397
  append_context context
330
398
  when :bake
331
- op.append(escape: true, strip: /^:/, clear: true)
332
- op.push(context) if context
399
+ unless op.empty?
400
+ args = op.dup
401
+ op.reset
402
+ if Dir.exist?(args.last)
403
+ if projectpath?(val = args.pop)
404
+ context = val
405
+ else
406
+ op.push(val)
407
+ end
408
+ end
409
+ op.append(args, escape: true, strip: /^:/)
410
+ contextdir context if context
411
+ end
333
412
  end
334
413
  op.clear(pass: false)
335
414
  run(from: :"buildx:#{flag}")
336
415
  end
337
416
 
338
- def compose!(flag, opts = [], service: nil)
339
- cmd, opts = docker_session('compose', opts: opts)
340
- op = OptionPartition.new(opts, OPT_DOCKER[:compose][:common], cmd, project: self)
341
- append_file filetype unless op.arg?('f', 'file')
342
- op << flag
343
- op.parse(OPT_DOCKER[:compose].fetch(flag, []))
344
- multiple = case flag
345
- when :build, :up, :down then true
346
- else false
347
- end
348
- if op.remove(':') || service == ':'
349
- keys = Set.new
350
- read_composefile('services', target: op.values_of('f', 'file')) { |data| keys.merge(data.keys) }
351
- service = unless keys.empty?
352
- choice_index('Add services', keys, multiple: multiple, force: !multiple,
353
- attempts: multiple ? 1 : 5)
354
- end
355
- end
356
- if multiple
357
- op.concat(service) if service
358
- 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)
359
429
  else
360
- raise_error('no services were found', hint: flag) unless service
361
- 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
362
450
  end
363
- run(from: :"compose:#{flag}")
451
+ run(from: from)
364
452
  end
365
- alias compose_ compose!
366
453
 
367
454
  def container(flag, opts = [], id: nil)
368
455
  cmd, opts = docker_session('container', flag, opts: opts)
369
456
  rc = flag == :run || flag == :create
370
- list = OPT_DOCKER[:container].fetch(flag, [])
371
- list += OPT_DOCKER[:container][:create] if flag == :run
372
- list += OPT_DOCKER[:container][:update] if rc
373
- 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
374
463
  from = :"container:#{flag}"
375
464
  case flag
376
465
  when :run, :create, :exec
@@ -387,12 +476,12 @@ module Squared
387
476
  when 'bind', 'volume', 'image', 'tmpfs'
388
477
  type = v
389
478
  else
390
- raise_error("unknown type: #{v}", hint: flag)
479
+ raise_error TypeError, "unknown: #{v}", hint: flag
391
480
  end
392
481
  elsif all.include?(k)
393
482
  unless type
394
- VAL_DOCKER[:run].each_pair do |key, a|
395
- next unless a.include?(k)
483
+ VAL_DOCKER[:run].each_pair do |key, val|
484
+ next unless val.include?(k)
396
485
 
397
486
  type = key.to_s unless key == :common
398
487
  break
@@ -411,18 +500,18 @@ module Squared
411
500
  log_message(Logger::INFO, 'unrecognized option', subject: from, hint: k)
412
501
  end
413
502
  end
414
- raise_error('missing type', hint: flag) unless type
503
+ raise_error TypeError, 'none specified', hint: flag unless type
415
504
  cmd << "--mount type=#{type},#{args.join(',')}"
416
505
  end
417
506
  end
418
507
  append_command(flag, id || tagmain, op.extras)
419
- when :update, :inspect
420
- raise_error('missing container', hint: flag) if op.empty?
508
+ when :update
509
+ raise_error ArgumentError, 'missing container', hint: flag if op.empty?
421
510
  op.append(escape: true, strip: /^:/)
422
511
  when :commit
423
512
  latest = op.shift || tagmain
424
513
  cmd << id << latest
425
- raise_error("unknown args: #{op.join(', ')}", hint: flag) unless op.empty?
514
+ raise_error ArgumentError, "unrecognized args: #{op.join(', ')}", hint: flag unless op.empty?
426
515
  return unless confirm_command(cmd.to_s, title: from, target: id, as: latest)
427
516
 
428
517
  registry = option('registry') || @registry
@@ -441,38 +530,12 @@ module Squared
441
530
  return image(:push, opts, id: latest, registry: registry)
442
531
  else
443
532
  if op.empty?
444
- status = []
445
- no = true
446
- case flag
447
- when :inspect, :diff
448
- no = false
449
- when :start
450
- status = %w[created exited]
451
- no = false
452
- when :stop, :pause
453
- status = %w[running restarting]
454
- when :restart
455
- status = %w[running paused exited]
456
- when :unpause
457
- status << 'paused'
458
- no = false
459
- when :top, :stats
460
- status << 'running'
461
- cmd << '--no-stream' if flag == :stats
462
- no = false
463
- when :kill
464
- status = %w[running restarting paused]
465
- when :rm
466
- status = %w[created exited dead]
467
- end
468
- ps = docker_output('ps -a', *status.map { |s| quote_option('filter', "status=#{s}") })
469
- list_image(flag, ps, no: no, hint: "status: #{status.join(', ')}", from: from) do |img|
470
- run(cmd.temp(img), from: from)
471
- 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) }
472
536
  return
473
- else
474
- op.append(escape: true, strip: /^:/)
475
537
  end
538
+ op.append(escape: true, strip: /^:/)
476
539
  end
477
540
  run(from: from)
478
541
  end
@@ -480,16 +543,21 @@ module Squared
480
543
  def image(flag, opts = [], sync: true, id: nil, registry: nil)
481
544
  cmd, opts = docker_session('image', flag, opts: opts)
482
545
  op = OptionPartition.new(opts, OPT_DOCKER[:image].fetch(flag, []), cmd, project: self)
483
- exception = @exception
546
+ exception = self.exception
484
547
  banner = true
485
548
  from = :"image:#{flag}"
486
549
  case flag
487
- when :list
550
+ when :ls
488
551
  if opts.size == op.size
489
552
  index = 0
490
553
  name = nil
491
- opts.reverse_each { |opt| break opts.delete(opt) if (name = opt[/^name=["']?(.+?)["']?$/, 1]) }
492
- 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|
493
561
  container(:run, if name
494
562
  opts.dup << "name=#{index == 0 ? name : "#{name}-#{index}"}"
495
563
  else
@@ -498,19 +566,12 @@ module Squared
498
566
  index += 1
499
567
  end
500
568
  return
501
- else
502
- op.clear
503
569
  end
570
+ op.clear
504
571
  when :rm
505
- if id
506
- op << id
507
- if option('y')
508
- exception = false
509
- banner = false
510
- end
511
- else
572
+ unless id
512
573
  if op.empty?
513
- list_image(:rm, docker_output('image ls -a'), from: from) do |val|
574
+ list_image(:rm, from: from) do |val|
514
575
  image(:rm, opts, sync: sync, id: val)
515
576
  end
516
577
  else
@@ -518,8 +579,13 @@ module Squared
518
579
  end
519
580
  return
520
581
  end
582
+ op << id
583
+ if option('y')
584
+ exception = false
585
+ banner = false
586
+ end
521
587
  when :tag, :save
522
- list_image(flag, docker_output('image ls -a'), from: from) do |val|
588
+ list_image(flag, from: from) do |val|
523
589
  op << val
524
590
  if flag == :tag
525
591
  op << tagname("#{project}:#{op.first}")
@@ -529,8 +595,14 @@ module Squared
529
595
  when :push
530
596
  id ||= option('tag', ignore: false) || tagmain
531
597
  registry ||= op.shift || option('registry') || @registry
532
- raise_error(id ? "unknown args: #{op.join(', ')}" : 'no id/tag given', hint: flag) unless id && op.empty?
533
- 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
534
606
  registry.chomp!('/')
535
607
  uri = shell_quote "#{registry}/#{id}"
536
608
  op << uri
@@ -542,17 +614,18 @@ module Squared
542
614
  exception = true
543
615
  banner = false
544
616
  end
545
- ret = run(cmd, sync: sync, exception: exception, banner: banner, from: from)
546
- 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
547
620
  end
548
621
 
549
622
  def network(flag, opts = [], target: nil)
550
623
  cmd, opts = docker_session('network', flag, opts: opts)
551
- op = OptionPartition.new(opts, OPT_DOCKER[:network].fetch(flag, []), cmd, project: self)
552
- op.clear
624
+ OptionPartition.new(opts, OPT_DOCKER[:network].fetch(flag, []), cmd, project: self)
625
+ .clear
553
626
  from = :"network:#{flag}"
554
627
  list_image(flag, docker_output('ps -a'), from: from) do |img|
555
- print_success if success?(run(cmd.temp(target, img), from: from))
628
+ success?(run(cmd.temp(target, img), from: from))
556
629
  end
557
630
  end
558
631
 
@@ -579,10 +652,10 @@ module Squared
579
652
  def dockerfile(val = nil)
580
653
  if val
581
654
  @file = if val.is_a?(Array)
582
- val = val.select { |file| basepath(file).exist? }
655
+ val = val.select { |file| exist?(file) }
583
656
  val.size > 1 ? val : val.first
584
657
  elsif val == true
585
- DIR_DOCKER.find { |file| basepath(file).exist? }
658
+ DIR_DOCKER.find { |file| exist?(file) }
586
659
  elsif val != 'Dockerfile'
587
660
  val
588
661
  end
@@ -611,8 +684,7 @@ module Squared
611
684
  return session('docker', *cmd) unless opts
612
685
 
613
686
  op = OptionPartition.new(opts, OPT_DOCKER[:common], project: self)
614
- ret = session('docker', *op.to_a, *cmd)
615
- [ret, op.extras]
687
+ [session('docker', *op.to_a, *cmd), op.extras]
616
688
  end
617
689
 
618
690
  def docker_output(*cmd, **kwargs)
@@ -622,8 +694,8 @@ module Squared
622
694
  def append_command(flag, val, list, target: @session, prompt: ':')
623
695
  if list.delete(prompt)
624
696
  list << readline('Enter command [args]', force: flag == :exec)
625
- elsif (args = env('DOCKER_ARGS'))
626
- list << args
697
+ else
698
+ env('DOCKER_ARGS') { |args| list << args }
627
699
  end
628
700
  case flag
629
701
  when :run
@@ -631,7 +703,7 @@ module Squared
631
703
  target << basic_option('name', dnsname("#{name}_%s" % rand_s(6)))
632
704
  end
633
705
  when :exec
634
- raise_error('no command args', hint: flag) if list.empty?
706
+ raise_error ArgumentError, 'nothing to execute', hint: flag if list.empty?
635
707
  end
636
708
  target << val << list.shift
637
709
  target << list.join(' && ') unless list.empty?
@@ -683,17 +755,45 @@ module Squared
683
755
  end
684
756
  end
685
757
 
686
- def list_image(flag, cmd, hint: nil, no: true, from: nil)
687
- pwd_set(from: from) do
688
- found = false
689
- index = 0
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)
788
+ pwd_set do
789
+ index = 1
690
790
  all = option('all', prefix: 'docker')
691
791
  y = from == :'image:rm' && option('y', prefix: 'docker')
692
792
  pat = /\b(?:#{dnsname(name)}|#{tagname(project)}|#{tagmain.split(':', 2).first})\b/
693
- IO.popen(session_done(cmd << '--format=json')).each do |line|
793
+ IO.popen(cmd.temp('--format=json')).each do |line|
694
794
  data = JSON.parse(line)
695
795
  id = data['ID']
696
- 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(':')
697
797
  rt = nil if rt.empty?
698
798
  aa = data['Names'] || (if rt && data['Repository']
699
799
  dd = true
@@ -705,16 +805,16 @@ module Squared
705
805
  next unless all || ee.match?(pat) || aa.match?(pat)
706
806
 
707
807
  unless y
708
- bb = index.succ.to_s
709
- cc = bb.size + 1
808
+ bb = index.to_s
809
+ cc = bb.size.succ
710
810
  a = sub_style(ee, styles: theme[:inline])
711
- 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)}"
712
812
  e = time_format(time_since(data['CreatedAt']), pass: ['ms'])
713
813
  f = sub_style(ARG[:BORDER][0], styles: theme[:inline])
714
- g = ' ' * (cc + 1)
814
+ g = ' ' * cc.succ
715
815
  h = "#{sub_style(bb.rjust(cc), styles: theme[:current])} #{f} "
716
- puts unless index == 0
717
- 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")
718
818
  cols = %w[Tag Status Ports]
719
819
  cols << case flag
720
820
  when :connect, :disconnect
@@ -729,89 +829,104 @@ module Squared
729
829
  end
730
830
  w = 9 + flag.to_s.size + 4 + ee.size
731
831
  puts g + sub_style(ARG[:BORDER][6] + (ARG[:BORDER][1] * w), styles: theme[:inline])
732
- found = true
733
832
  index += 1
734
- next unless confirm("#{h + b}?", no ? 'N' : 'Y', timeout: 60)
833
+ next unless confirm("#{h + b}?", no ? 'N' : 'Y')
735
834
 
736
835
  puts if printfirst?
737
836
  end
738
837
  yield id
739
838
  end
740
- 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
741
840
  end
742
841
  rescue StandardError => e
743
842
  on_error e, from
744
843
  end
745
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
+
746
850
  def confirm_command(*args, title: nil, target: nil, as: nil)
747
851
  return false unless title && target
748
852
 
749
853
  puts unless printfirst?
750
854
  t = title.to_s.split(':')
751
855
  emphasize(args, title: message(t.first.upcase, *t.drop(1)), border: borderstyle, sub: [
752
- { pat: /\A(\w+(?: => \w+)+)(.*)\z/, styles: theme[:header] },
753
- { pat: /\A(.+)\z/, styles: theme[:caution] }
856
+ opt_style(theme[:header], /\A(\w+(?: => \w+)+)(.*)\z/),
857
+ opt_style(theme[:caution], /\A(.+)\z/)
754
858
  ])
755
859
  printsucc
756
860
  a = t.last.capitalize
757
861
  b = sub_style(target, styles: theme[:subject])
758
862
  c = as && sub_style(as, styles: theme[:inline])
759
- confirm("#{a} #{b}#{c ? " as #{c}" : ''}?", 'N', timeout: 60)
760
- end
761
-
762
- def choice_command(flag)
763
- msg, cmd, index = case flag
764
- when :exec
765
- ['Choose a container', 'ps -a', 0]
766
- when :bake
767
- ['Choose a target', 'buildx bake --list=type=targets', 0]
768
- when :connect, :disconnect
769
- ['Choose a network', 'network ls', 0]
770
- else
771
- ['Choose an image', 'images -a', 2]
772
- 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
773
883
  lines = `#{docker_output(cmd)}`.lines
774
884
  header = lines.shift
775
885
  if lines.empty?
776
- puts log_message(Logger::INFO, 'none found', subject: name, hint: "docker #{cmd}")
777
- else
778
- puts " # #{header}"
779
- multiple = false
780
- parse = ->(val) { val.split(/\s+/)[index] }
781
- ctx = flag.to_s
782
- case flag
783
- when :run, :exec
784
- values = [['Options', flag == :run], ['Arguments', flag == :exec]]
785
- when :rm, :bake
786
- values = ['Options']
787
- multiple = true
788
- ctx = flag == :rm ? 'image rm' : "buildx bake -f #{shell_quote(dockerfile)}"
789
- when :save
790
- values = [['Output', true], 'Platform']
791
- multiple = true
792
- when :connect, :disconnect
793
- values = ['Options', ['Container', true]]
794
- ctx = "network #{flag}"
795
- end
796
- out, opts, args = choice_index(msg, lines, multiple: multiple, values: values)
797
- cmd = docker_output ctx
798
- case flag
799
- when :tag
800
- args = tagjoin @registry, tag
801
- when :save
802
- opts = "#{opts}.tar" unless opts.end_with?('.tar')
803
- cmd << quote_option('output', File.expand_path(opts))
804
- if args
805
- cmd << basic_option('platform', args)
806
- args = nil
807
- end
808
- else
809
- 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
810
923
  end
811
- cmd.merge(Array(out).map! { |val| parse.call(val) })
812
- cmd << args
813
- print_success if success?(run(cmd), ctx.start_with?(/network|tag|save/))
924
+ else
925
+ cmd << opts << '--'
814
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)/))
815
930
  end
816
931
 
817
932
  def filetype(val = dockerfile)
@@ -840,10 +955,9 @@ module Squared
840
955
 
841
956
  def tagname(val)
842
957
  val = val.split(':').map! { |s| charname(s.sub(/^\W+/, '')) }
843
- val.join(':').yield_self do |s|
844
- s = val.first if val.size > 1 && s.size > 128
845
- s[0..127]
846
- end
958
+ ret = val.join(':')
959
+ ret = val.first if val.size > 1 && ret.size > 128
960
+ ret[0..127]
847
961
  end
848
962
 
849
963
  def dnsname(val)