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