squared 0.6.9 → 0.7.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.
@@ -33,7 +33,8 @@ module Squared
33
33
  d|detach force-recreate menu no-build no-color no-deps no-log-prefix no-recreate no-start quiet-build
34
34
  quiet-pull remove-orphans V|renew-anon-volumes timestamps wait w|watch y|yes attach=b
35
35
  exit-code-from=b no-attach=b pull=b scale=i t|timeout=i wait-timeout=i].freeze,
36
- down: %w[remove-orphans v|volumes rmi=b t|timeout=i].freeze
36
+ down: %w[remove-orphans v|volumes rmi=b t|timeout=i].freeze,
37
+ publish: %w[app resolve-image-digests with-env y|yes oci-version=b].freeze
37
38
  }.freeze,
38
39
  container: {
39
40
  create: %w[init i|interactive no-healthcheck oom-kill-disable privileged P|publish-all q|quiet read-only
@@ -64,6 +65,7 @@ module Squared
64
65
  }.freeze,
65
66
  image: {
66
67
  ls: %w[a|all digests no-trunc q|quiet tree f|filter=q format=q].freeze,
68
+ pull: %w[a|all-tags platform=q q|quiet].freeze,
67
69
  push: %w[a|all-tags platform=q q|quiet].freeze,
68
70
  rm: %w[f|force no-prune platform=q].freeze,
69
71
  save: %w[o|output=p platform=q].freeze
@@ -107,9 +109,9 @@ module Squared
107
109
 
108
110
  subtasks({
109
111
  'build' => %i[tag context].freeze,
110
- 'compose' => %i[build create run exec up down service].freeze,
111
- 'bake' => %i[build check].freeze,
112
- 'image' => %i[ls rm push tag save].freeze,
112
+ 'compose' => %i[build create publish run exec up down service].freeze,
113
+ 'bake' => %i[build compose check].freeze,
114
+ 'image' => %i[ls rm pull push tag save].freeze,
113
115
  'container' => %i[run create exec update commit inspect diff start stop restart pause unpause top stats kill
114
116
  rm].freeze,
115
117
  'network' => %i[connect disconnect].freeze,
@@ -119,17 +121,19 @@ module Squared
119
121
  attr_reader :context
120
122
  attr_accessor :tag
121
123
 
122
- def initialize(*, file: nil, context: nil, tag: nil, secrets: nil, mounts: [], registry: nil, **kwargs)
124
+ def initialize(*, mounts: [], **kwargs)
123
125
  super
124
- return unless dockerfile(file).exist?
126
+ self.global = nil
127
+ return unless dockerfile(kwargs[:file]).exist?
125
128
 
126
- @context = context
127
- self.tag = tag || tagname("#{@project}:#{@version || 'latest'}")
129
+ self.tag = kwargs[:tag] || tagname("#{@project}:#{@version || 'latest'}")
130
+ @context = kwargs[:context]
128
131
  @mounts = mounts
129
- @secrets = secrets
130
- @registry = tagjoin registry, kwargs[:username]
132
+ @secrets = kwargs[:secrets]
133
+ @registry = tagjoin kwargs[:registry], kwargs[:username]
134
+ @oci = tagjoin kwargs[:oci], kwargs[:username]
131
135
  initialize_ref Docker.ref
132
- initialize_logger(**kwargs)
136
+ initialize_logger kwargs[:log]
133
137
  initialize_env(**kwargs)
134
138
  @output[4] = merge_opts(kwargs[:args], @output[4]) if kwargs[:args]
135
139
  end
@@ -163,25 +167,21 @@ module Squared
163
167
  end
164
168
  cmd << '-a' if has_value!(args, 'a', 'all') && command != 'network'
165
169
  data = VAL_DOCKER[:ls][command.to_sym]
166
- cols = if has_value!(args, 's', 'standard')
167
- data.first(data.index('CreatedAt'))
168
- else
169
- [].tap do |out|
170
- args.each do |val|
171
- if val =~ /^(\d+)$/
172
- out << data[$1.to_i.pred]
173
- elsif val =~ /^(\d+)(-|\.{2,3})(\d+)$/
174
- j = $1.to_i.pred
175
- k = $3.to_i - ($2 == '..' ? 2 : 1)
176
- out.concat(data[j..k]) if k > j
177
- end
178
- end
179
- next unless out.empty?
180
-
181
- out.replace(choice_index('Select a column', data, multiple: true, attempts: 1))
182
- end
183
- end
184
- cmd << quote_option('format', "table #{cols.map! { |val| "{{.#{val}}}" }.join("\t")}")
170
+ if has_value!(args, 's', 'standard')
171
+ cols = data.first(data.index('CreatedAt'))
172
+ else
173
+ cols = args.each_with_object([]) do |val, out|
174
+ if val =~ /^(\d+)$/
175
+ out << data[$1.to_i.pred]
176
+ elsif val =~ /^(\d+)(-|\.{2,3})(\d+)$/
177
+ j = $1.to_i.pred
178
+ k = $3.to_i - ($2 == '..' ? 2 : 1)
179
+ out.concat(data[j..k]) if k > j
180
+ end
181
+ end
182
+ cols = choice_index('Select a column', data, multiple: true, attempts: 1) if cols.empty?
183
+ end
184
+ cmd << quote_option('format', "table #{cols.map { |val| "{{.#{val}}}" }.join("\t")}")
185
185
  run(cmd, banner: false, from: :ls)
186
186
  end
187
187
  end
@@ -199,14 +199,14 @@ module Squared
199
199
  break unless bake?
200
200
 
201
201
  case flag
202
- when :build
203
- format_desc action, flag, 'opts*,target*,context|:'
202
+ when :build, :compose
203
+ format_desc action, flag, 'opts*,target*,context?|:'
204
204
  task flag do |_, args|
205
205
  args = args.to_a
206
206
  if args.first == ':'
207
207
  choice_command :bake
208
208
  else
209
- buildx :bake, args
209
+ buildx(:bake, args, from: (:'buildx:bake' if flag == :compose))
210
210
  end
211
211
  end
212
212
  when :check
@@ -239,6 +239,13 @@ module Squared
239
239
  compose!(flag, [command], service: service.empty? || service)
240
240
  end
241
241
  end
242
+ when :publish
243
+ next unless @oci
244
+
245
+ format_desc action, flag, 'tag?,repository?,opts*'
246
+ task flag, [:tag] do |_, args|
247
+ compose! flag, args.to_a
248
+ end
242
249
  else
243
250
  format_desc action, flag, 'opts*,service*|:'
244
251
  task flag do |_, args|
@@ -275,6 +282,13 @@ module Squared
275
282
  when 'image'
276
283
  case flag
277
284
  when :push
285
+ next unless @registry
286
+
287
+ format_desc action, flag, 'tag?,registry/username?,opts*'
288
+ task flag, [:tag] do |_, args|
289
+ image flag, args.to_a
290
+ end
291
+ when :pull
278
292
  format_desc action, flag, 'tag,registry/username?,opts*'
279
293
  task flag, [:tag] do |_, args|
280
294
  id = param_guard(action, flag, args: args, key: :tag)
@@ -349,7 +363,7 @@ module Squared
349
363
  [args, flags].each_with_index do |item, i|
350
364
  next unless item && (data = append_any(item, target: []))
351
365
 
352
- ret.merge(data.map! { |arg| i == 0 ? fill_option(arg) : quote_option('build-arg', arg) })
366
+ ret.merge(data.map { |arg| i == 0 ? fill_option(arg) : quote_option('build-arg', arg) })
353
367
  end
354
368
  case from
355
369
  when :run
@@ -357,13 +371,13 @@ module Squared
357
371
  when String
358
372
  ret << quote_option('secret', @secrets, double: true)
359
373
  when Hash
360
- append = lambda do |type|
361
- Array(@secrets[type]).each { |arg| ret << quote_option('secret', "type=#{type},#{arg}", double: true) }
374
+ append = lambda do |key|
375
+ ret.merge(Array(@secrets[key]).map { |arg| quote_option('secret', "type=#{key},#{arg}", double: true) })
362
376
  end
363
377
  append.call(:file)
364
378
  append.call(:env)
365
379
  else
366
- Array(@secrets).each { |arg| ret << quote_option('secret', arg) }
380
+ ret.merge(Array(@secrets).map { |arg| quote_option('secret', arg) })
367
381
  end
368
382
  if (val = option('tag', ignore: false))
369
383
  append_tag val
@@ -377,18 +391,18 @@ module Squared
377
391
  ret
378
392
  end
379
393
 
380
- def buildx(flag, opts = [], tag: nil, context: nil)
394
+ def buildx(flag, opts = [], tag: nil, context: nil, from: nil)
381
395
  cmd, opts = docker_session('buildx', opts: opts)
382
- op = OPT_DOCKER[:buildx].yield_self do |data|
383
- OptionPartition.new(opts, data[:common], cmd, project: self)
384
- .append(flag, quote: false)
385
- .parse(data[flag == :bake ? :bake : :build] + data[:shared])
386
- end
396
+ data = OPT_DOCKER[:buildx]
397
+ op = OptionPartition.new(opts, data[:common], cmd, project: self)
398
+ op.append(flag, quote: false)
399
+ .parse(data[flag == :bake ? :bake : :build] + data[:shared])
387
400
  case flag
388
401
  when :build, :context
389
402
  append_tag(tag || option('tag', ignore: false) || self.tag)
390
403
  append_context context
391
404
  when :bake
405
+ append_file(0, index: 3) unless from || op.arg?('f', 'file') || !anypath?(*COMPOSEFILE)
392
406
  unless op.empty?
393
407
  args = op.dup
394
408
  op.reset
@@ -404,11 +418,11 @@ module Squared
404
418
  end
405
419
  end
406
420
  op.clear(pass: false)
407
- run(from: :"buildx:#{flag}")
421
+ run(from: from || symjoin('buildx', flag))
408
422
  end
409
423
 
410
- def compose!(flag, opts = [], service: nil, multiple: false)
411
- from = :"compose:#{flag}"
424
+ def compose!(flag, opts = [], id: nil, service: nil, multiple: false)
425
+ from = symjoin 'compose', flag
412
426
  if flag == :service
413
427
  command = opts.first
414
428
  if service == true
@@ -422,23 +436,31 @@ module Squared
422
436
  else
423
437
  cmd, opts = docker_session('compose', opts: opts)
424
438
  op = OptionPartition.new(opts, OPT_DOCKER[:compose][:common], cmd, project: self)
425
- append_file filetype unless op.arg?('f', 'file')
439
+ append_file(filetype, force: flag == :publish) unless op.arg?('f', 'file')
426
440
  op << flag
427
441
  op.parse(OPT_DOCKER[:compose].fetch(flag, []))
428
- if op.remove(':') || service == ':'
429
- keys = Set.new
430
- read_composefile('services', target: op.values_of('f', 'file')) { |data| keys.merge(data.keys) }
431
- service = unless keys.empty?
432
- choice_index('Add services', keys, multiple: multiple, force: !multiple,
433
- attempts: multiple ? 1 : 3)
434
- end
435
- end
436
- if multiple
437
- op.concat(service) if service
438
- op.append(delim: true, escape: true, strip: /^:/)
442
+ if flag == :publish
443
+ id ||= option('tag', ignore: false) || op.shift || tagmain
444
+ registry ||= option('registry') || op.shift || @oci
445
+ emptyargs op, flag
446
+ op << shell_quote(tagjoin(registry, id))
447
+ return unless confirm_command(op.to_s, title: from, target: id)
439
448
  else
440
- raise_error ArgumentError, 'no service was selected', hint: flag unless service
441
- append_command(flag, service, op.extras, prompt: '::')
449
+ if op.remove(':') || service == ':'
450
+ keys = Set.new
451
+ read_composefile('services', target: op.values_of('f', 'file')) { |data| keys.merge(data.keys) }
452
+ service = unless keys.empty?
453
+ choice_index('Add services', keys, multiple: multiple, force: !multiple,
454
+ attempts: multiple ? 1 : 3)
455
+ end
456
+ end
457
+ if multiple
458
+ op.concat(service) if service
459
+ op.append(delim: true, escape: true, strip: /^:/)
460
+ else
461
+ raise_error ArgumentError, 'no service was selected', hint: flag unless service
462
+ append_command(flag, service, op.extras, prompt: '::')
463
+ end
442
464
  end
443
465
  end
444
466
  run(from: from)
@@ -446,35 +468,32 @@ module Squared
446
468
 
447
469
  def container(flag, opts = [], id: nil)
448
470
  cmd, opts = docker_session('container', flag, opts: opts)
449
- rc = flag == :run || flag == :create
450
- op = OPT_DOCKER[:container].yield_self do |data|
451
- list = data.fetch(flag, [])
452
- list += data[:create] if flag == :run
453
- list += data[:update] if rc
454
- OptionPartition.new(opts, list, cmd, project: self, args: rc || flag == :exec)
455
- end
456
- from = :"container:#{flag}"
471
+ data = OPT_DOCKER[:container]
472
+ list = data.fetch(flag, [])
473
+ list += data[:create] if (rc = flag == :run)
474
+ list += data[:update] if rc ||= flag == :create
475
+ op = OptionPartition.new(opts, list, cmd, project: self, args: rc || flag == :exec)
476
+ from = symjoin 'container', flag
457
477
  case flag
458
478
  when :run, :create, :exec
459
479
  if rc && !op.arg?('mount')
460
480
  all = collect_hash VAL_DOCKER[:run]
461
481
  delim = Regexp.new(",\\s*(?=#{all.join('|')})")
462
482
  Array(@mounts).each do |val|
463
- args = []
464
483
  type = nil
465
- val.split(delim).each do |opt|
466
- k, v, q = split_option opt
484
+ args = val.split(delim).each_with_object([]) do |opt, out|
485
+ k, v, q = OptionPartition.parse_option(opt)
467
486
  if k == 'type'
468
487
  case v
469
488
  when 'bind', 'volume', 'image', 'tmpfs'
470
489
  type = v
471
490
  else
472
- raise_error TypeError, "unknown: #{v}", hint: flag
491
+ raise_error TypeError, "unknown: #{v || "''"}", hint: flag
473
492
  end
474
493
  elsif all.include?(k)
475
494
  unless type
476
- VAL_DOCKER[:run].each_pair do |key, a|
477
- next unless a.include?(k)
495
+ VAL_DOCKER[:run].each_pair do |key, items|
496
+ next unless items.include?(k)
478
497
 
479
498
  type = key.to_s unless key == :common
480
499
  break
@@ -482,13 +501,14 @@ module Squared
482
501
  end
483
502
  case k
484
503
  when 'readonly', 'ro'
485
- args << k
504
+ out << k
486
505
  next
487
506
  when 'source', 'src', 'destination', 'dst', 'target', 'volume-subpath', 'image-path'
507
+ raise_error ArgumentError, "#{k}: no path value", hint: flag unless v
488
508
  v = basepath v
489
509
  v = shell_quote(v, option: false, force: false) if q == ''
490
510
  end
491
- args << "#{k}=#{q + v + q}"
511
+ out << "#{k}=#{q}#{v}#{q}"
492
512
  elsif !silent?
493
513
  log_message('unrecognized option', subject: from, hint: k)
494
514
  end
@@ -514,7 +534,7 @@ module Squared
514
534
  opts = []
515
535
  append_option('platform', target: opts, equals: true)
516
536
  opts << case option('disable-content-trust', ignore: false)
517
- when 'false', '0'
537
+ when '0', 'false'
518
538
  '--disable-content-trust=false'
519
539
  else
520
540
  '--disable-content-trust'
@@ -536,9 +556,9 @@ module Squared
536
556
  def image(flag, opts = [], sync: true, id: nil, registry: nil)
537
557
  cmd, opts = docker_session('image', flag, opts: opts)
538
558
  op = OptionPartition.new(opts, OPT_DOCKER[:image].fetch(flag, []), cmd, project: self)
539
- exception = self.exception
559
+ exception = exception?
540
560
  banner = true
541
- from = :"image:#{flag}"
561
+ from = symjoin 'image', flag
542
562
  case flag
543
563
  when :ls
544
564
  if opts.size == op.size
@@ -565,9 +585,7 @@ module Squared
565
585
  when :rm
566
586
  unless id
567
587
  if op.empty?
568
- list_image(:rm, from: from) do |val|
569
- image(:rm, opts, sync: sync, id: val)
570
- end
588
+ list_image(:rm, from: from) { |val| image(:rm, opts, sync: sync, id: val) }
571
589
  else
572
590
  op.each { |val| run(cmd.temp(val), sync: sync, from: from) }
573
591
  end
@@ -581,46 +599,46 @@ module Squared
581
599
  when :tag, :save
582
600
  list_image(flag, from: from) do |val|
583
601
  op << val
584
- if flag == :tag
585
- op << tagname("#{project}:#{op.first}")
586
- break
587
- end
602
+ next unless flag == :tag
603
+
604
+ op << tagname("#{project}:#{op.first}")
605
+ break
588
606
  end
589
- when :push
590
- id ||= option('tag', ignore: false) || tagmain
591
- registry ||= op.shift || option('registry') || @registry
592
- unless id && op.empty?
593
- if id
594
- raise_error ArgumentError, "unrecognized args: #{op.join(', ')}", hint: flag
595
- else
596
- raise_error 'no id/tag', hint: flag
597
- end
607
+ when :pull
608
+ if !id
609
+ id = tagmain
610
+ elsif !op.arg?('a', 'all-tags') && !id.include?(':')
611
+ id = "#{project}:#{id}"
612
+ end
613
+ unless registry
614
+ registry = op.shift
615
+ registry ||= option('registry') || @registry unless id.include?('/')
598
616
  end
617
+ cmd << shell_quote(tagjoin(registry, id))
618
+ when :push
619
+ id ||= option('tag', ignore: false) || op.shift || tagmain
620
+ registry ||= option('registry') || op.shift || @registry
621
+ emptyargs op, flag
599
622
  raise_error ArgumentError, 'username/registry not specified', hint: flag unless registry
600
- registry.chomp!('/')
601
- uri = shell_quote "#{registry}/#{id}"
623
+ uri = shell_quote tagjoin(registry, id)
602
624
  op << uri
603
625
  img = docker_output 'image', 'tag', id, uri
604
626
  return unless confirm_command(img.to_s, cmd.to_s, target: id, as: registry, title: from)
605
627
 
606
- cmd = img
628
+ @session = img
607
629
  sync = false
608
- exception = true
630
+ exception ||= true
609
631
  banner = false
610
632
  end
611
- run(cmd, sync: sync, exception: exception, banner: banner, from: from).tap do |ret|
612
- success?(ret, flag == :tag || flag == :save)
613
- end
633
+ success?(run(sync: sync, exception: exception, banner: banner, from: from), flag == :tag || flag == :save)
614
634
  end
615
635
 
616
636
  def network(flag, opts = [], target: nil)
617
637
  cmd, opts = docker_session('network', flag, opts: opts)
618
638
  OptionPartition.new(opts, OPT_DOCKER[:network].fetch(flag, []), cmd, project: self)
619
639
  .clear
620
- from = :"network:#{flag}"
621
- list_image(flag, docker_output('ps -a'), from: from) do |img|
622
- success?(run(cmd.temp(target, img), from: from))
623
- end
640
+ from = symjoin 'network', flag
641
+ list_image(flag, docker_output('ps -a'), from: from) { |id| success?(run(cmd.temp(target, id), from: from)) }
624
642
  end
625
643
 
626
644
  def build?
@@ -669,7 +687,7 @@ module Squared
669
687
  elsif (data = doc.dig(*keys))
670
688
  yield data
671
689
  end
672
- rescue StandardError => e
690
+ rescue => e
673
691
  log.debug e
674
692
  end
675
693
  end
@@ -686,11 +704,11 @@ module Squared
686
704
  end
687
705
 
688
706
  def append_command(flag, val, list, target: @session, prompt: ':')
689
- if list.delete(prompt)
690
- list << readline('Enter command [args]', force: flag == :exec)
691
- else
692
- env('DOCKER_ARGS') { |args| list << args }
693
- end
707
+ list << if list.delete(prompt)
708
+ readline('Enter command [args]', force: flag == :exec)
709
+ else
710
+ env('DOCKER_ARGS')
711
+ end
694
712
  case flag
695
713
  when :run
696
714
  unless session_arg?('name', target: target)
@@ -703,10 +721,10 @@ module Squared
703
721
  target << list.join(' && ') unless list.empty?
704
722
  end
705
723
 
706
- def append_file(type, target: @session, index: 2)
707
- return if !@file || (ENV['COMPOSE_FILE'] && compose?(type))
724
+ def append_file(type, target: @session, index: 2, force: false)
725
+ return unless @file && !(ENV['COMPOSE_FILE'] && compose?(type))
708
726
 
709
- unless @file.is_a?(Array)
727
+ unless @file.is_a?(Array) || force
710
728
  case type
711
729
  when 2, 4
712
730
  return
@@ -714,14 +732,7 @@ module Squared
714
732
  return unless COMPOSEFILE.select { |val| basepath!(val) }.size > 1
715
733
  end
716
734
  end
717
- files = Array(@file).map { |val| quote_option('file', basepath(val)) }
718
- if target.is_a?(Set)
719
- opts = target.to_a.insert(index, *files)
720
- target.clear
721
- .merge(opts)
722
- else
723
- target.insert(index, *files)
724
- end
735
+ target.insert(index, *Array(@file).map { |val| quote_option('file', basepath(val)) })
725
736
  end
726
737
 
727
738
  def append_context(ctx = nil, target: @session)
@@ -735,7 +746,7 @@ module Squared
735
746
  ver = option('version', target: target, ignore: false)
736
747
  case val
737
748
  when String
738
- split_escape val
749
+ val.split(',')
739
750
  else
740
751
  Array(val)
741
752
  end.each do |s|
@@ -828,7 +839,7 @@ module Squared
828
839
  end
829
840
  list_empty(hint: hint || from) if index == 1 && !y
830
841
  end
831
- rescue StandardError => e
842
+ rescue => e
832
843
  on_error e, from
833
844
  end
834
845
 
@@ -900,18 +911,18 @@ module Squared
900
911
  when :tag
901
912
  args = tagjoin @registry, tag
902
913
  when :save
903
- opts = "#{opts}.tar" unless opts.end_with?('.tar')
914
+ opts = opts.sub_ext('.tar')
904
915
  cmd << quote_option('output', File.expand_path(opts))
905
916
  if args
906
- cmd << basic_option('platform', args)
917
+ cmd << quote_option('platform', args)
907
918
  args = nil
908
919
  end
909
920
  else
910
921
  cmd << opts << '--'
911
922
  end
912
- cmd.merge(Array(out).map! { |val| val.split(/\s+/, 2).first })
923
+ cmd.merge(Array(out).map { |val| val.split(/\s+/, 2).first })
913
924
  cmd << args
914
- success?(run(cmd), ctx.start_with?(/network|tag|save/))
925
+ success?(run(cmd), ctx.start_with?('network', 'tag', 'save'))
915
926
  end
916
927
 
917
928
  def filetype(val = dockerfile)
@@ -934,11 +945,12 @@ module Squared
934
945
  end
935
946
 
936
947
  def tagjoin(*args, char: '/')
937
- args.join(char) unless (args = args.compact).empty?
948
+ args.compact!
949
+ args.map { |val| val.chomp(char) }.join(char) unless args.empty?
938
950
  end
939
951
 
940
952
  def tagname(val)
941
- val = val.split(':').map! { |s| charname(s.sub(/^\W+/, '')) }
953
+ val = val.split(':').map { |s| charname(s.sub(/^\W+/, '')) }
942
954
  ret = val.join(':')
943
955
  ret = val.first if val.size > 1 && ret.size > 128
944
956
  ret[0..127]
@@ -955,6 +967,14 @@ module Squared
955
967
  def tagmain
956
968
  tag.is_a?(Array) ? tag.first : tag
957
969
  end
970
+
971
+ def emptyargs(list, hint = nil)
972
+ raise_error ArgumentError, "unrecognized args: #{list.join(', ')}", hint: hint unless list.empty?
973
+ end
974
+
975
+ def anypath?(*args)
976
+ args.any? { |val| basepath!(val) }
977
+ end
958
978
  end
959
979
 
960
980
  Application.implement Docker