vendor: github.com/docker/cli v29.2.1
full diff: https://github.com/docker/cli/compare/v29.2.0...v29.2.1 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
@@ -16,7 +16,7 @@ require (
|
|||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/cli v29.2.0+incompatible
|
github.com/docker/cli v29.2.1+incompatible
|
||||||
github.com/docker/cli-docs-tool v0.11.0
|
github.com/docker/cli-docs-tool v0.11.0
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||||
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
|
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
|
||||||
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
||||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||||
|
|||||||
+43
-106
@@ -22,72 +22,43 @@ type MountOpt struct {
|
|||||||
//
|
//
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (m *MountOpt) Set(value string) error {
|
func (m *MountOpt) Set(value string) error {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return errors.New("value is empty")
|
||||||
|
}
|
||||||
|
|
||||||
csvReader := csv.NewReader(strings.NewReader(value))
|
csvReader := csv.NewReader(strings.NewReader(value))
|
||||||
fields, err := csvReader.Read()
|
fields, err := csvReader.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mount := mounttypes.Mount{}
|
mount := mounttypes.Mount{
|
||||||
|
Type: mounttypes.TypeVolume, // default to volume mounts
|
||||||
|
}
|
||||||
|
|
||||||
volumeOptions := func() *mounttypes.VolumeOptions {
|
for _, field := range fields {
|
||||||
if mount.VolumeOptions == nil {
|
key, val, hasValue := strings.Cut(field, "=")
|
||||||
mount.VolumeOptions = &mounttypes.VolumeOptions{
|
if k := strings.TrimSpace(key); k != key {
|
||||||
Labels: make(map[string]string),
|
return fmt.Errorf("invalid option '%s' in '%s': option should not have whitespace", k, field)
|
||||||
|
}
|
||||||
|
if hasValue {
|
||||||
|
v := strings.TrimSpace(val)
|
||||||
|
if v == "" {
|
||||||
|
return fmt.Errorf("invalid value for '%s': value is empty", key)
|
||||||
|
}
|
||||||
|
if v != val {
|
||||||
|
return fmt.Errorf("invalid value for '%s' in '%s': value should not have whitespace", key, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mount.VolumeOptions.DriverConfig == nil {
|
|
||||||
mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
|
|
||||||
}
|
|
||||||
return mount.VolumeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
imageOptions := func() *mounttypes.ImageOptions {
|
|
||||||
if mount.ImageOptions == nil {
|
|
||||||
mount.ImageOptions = new(mounttypes.ImageOptions)
|
|
||||||
}
|
|
||||||
return mount.ImageOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
bindOptions := func() *mounttypes.BindOptions {
|
|
||||||
if mount.BindOptions == nil {
|
|
||||||
mount.BindOptions = new(mounttypes.BindOptions)
|
|
||||||
}
|
|
||||||
return mount.BindOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpfsOptions := func() *mounttypes.TmpfsOptions {
|
|
||||||
if mount.TmpfsOptions == nil {
|
|
||||||
mount.TmpfsOptions = new(mounttypes.TmpfsOptions)
|
|
||||||
}
|
|
||||||
return mount.TmpfsOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
setValueOnMap := func(target map[string]string, value string) {
|
|
||||||
k, v, _ := strings.Cut(value, "=")
|
|
||||||
if k != "" {
|
|
||||||
target[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mount.Type = mounttypes.TypeVolume // default to volume mounts
|
|
||||||
// Set writable as the default
|
|
||||||
for _, field := range fields {
|
|
||||||
key, val, ok := strings.Cut(field, "=")
|
|
||||||
|
|
||||||
// TODO(thaJeztah): these options should not be case-insensitive.
|
// TODO(thaJeztah): these options should not be case-insensitive.
|
||||||
key = strings.ToLower(key)
|
key = strings.ToLower(key)
|
||||||
|
|
||||||
if !ok {
|
if !hasValue {
|
||||||
switch key {
|
switch key {
|
||||||
case "readonly", "ro":
|
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive":
|
||||||
mount.ReadOnly = true
|
// boolean values
|
||||||
continue
|
|
||||||
case "volume-nocopy":
|
|
||||||
volumeOptions().NoCopy = true
|
|
||||||
continue
|
|
||||||
case "bind-nonrecursive":
|
|
||||||
return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||||
}
|
}
|
||||||
@@ -106,14 +77,14 @@ func (m *MountOpt) Set(value string) error {
|
|||||||
case "target", "dst", "destination":
|
case "target", "dst", "destination":
|
||||||
mount.Target = val
|
mount.Target = val
|
||||||
case "readonly", "ro":
|
case "readonly", "ro":
|
||||||
mount.ReadOnly, err = strconv.ParseBool(val)
|
mount.ReadOnly, err = parseBoolValue(key, val, hasValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid value for %s: %s", key, val)
|
return err
|
||||||
}
|
}
|
||||||
case "consistency":
|
case "consistency":
|
||||||
mount.Consistency = mounttypes.Consistency(strings.ToLower(val))
|
mount.Consistency = mounttypes.Consistency(strings.ToLower(val))
|
||||||
case "bind-propagation":
|
case "bind-propagation":
|
||||||
bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(val))
|
ensureBindOptions(&mount).Propagation = mounttypes.Propagation(strings.ToLower(val))
|
||||||
case "bind-nonrecursive":
|
case "bind-nonrecursive":
|
||||||
return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
|
return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
|
||||||
case "bind-recursive":
|
case "bind-recursive":
|
||||||
@@ -121,86 +92,52 @@ func (m *MountOpt) Set(value string) error {
|
|||||||
case "enabled": // read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.12, otherwise writable
|
case "enabled": // read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.12, otherwise writable
|
||||||
// NOP
|
// NOP
|
||||||
case "disabled": // previously "bind-nonrecursive=true"
|
case "disabled": // previously "bind-nonrecursive=true"
|
||||||
bindOptions().NonRecursive = true
|
ensureBindOptions(&mount).NonRecursive = true
|
||||||
case "writable": // conforms to the default read-only bind-mount of Docker v24; read-only mounts are recursively mounted but not recursively read-only
|
case "writable": // conforms to the default read-only bind-mount of Docker v24; read-only mounts are recursively mounted but not recursively read-only
|
||||||
bindOptions().ReadOnlyNonRecursive = true
|
ensureBindOptions(&mount).ReadOnlyNonRecursive = true
|
||||||
case "readonly": // force recursively read-only, or raise an error
|
case "readonly": // force recursively read-only, or raise an error
|
||||||
bindOptions().ReadOnlyForceRecursive = true
|
ensureBindOptions(&mount).ReadOnlyForceRecursive = true
|
||||||
// TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass
|
// TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass
|
||||||
// https://github.com/docker/cli/pull/4316#discussion_r1341974730
|
// https://github.com/docker/cli/pull/4316#discussion_r1341974730
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
|
return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
|
||||||
}
|
}
|
||||||
case "volume-subpath":
|
case "volume-subpath":
|
||||||
volumeOptions().Subpath = val
|
ensureVolumeOptions(&mount).Subpath = val
|
||||||
case "volume-nocopy":
|
case "volume-nocopy":
|
||||||
volumeOptions().NoCopy, err = strconv.ParseBool(val)
|
ensureVolumeOptions(&mount).NoCopy, err = parseBoolValue(key, val, hasValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid value for volume-nocopy: %s", val)
|
return err
|
||||||
}
|
}
|
||||||
case "volume-label":
|
case "volume-label":
|
||||||
setValueOnMap(volumeOptions().Labels, val)
|
volumeOpts := ensureVolumeOptions(&mount)
|
||||||
|
volumeOpts.Labels = setValueOnMap(volumeOpts.Labels, val)
|
||||||
case "volume-driver":
|
case "volume-driver":
|
||||||
volumeOptions().DriverConfig.Name = val
|
ensureVolumeDriver(&mount).Name = val
|
||||||
case "volume-opt":
|
case "volume-opt":
|
||||||
if volumeOptions().DriverConfig.Options == nil {
|
volumeDriver := ensureVolumeDriver(&mount)
|
||||||
volumeOptions().DriverConfig.Options = make(map[string]string)
|
volumeDriver.Options = setValueOnMap(volumeDriver.Options, val)
|
||||||
}
|
|
||||||
setValueOnMap(volumeOptions().DriverConfig.Options, val)
|
|
||||||
case "image-subpath":
|
case "image-subpath":
|
||||||
imageOptions().Subpath = val
|
ensureImageOptions(&mount).Subpath = val
|
||||||
case "tmpfs-size":
|
case "tmpfs-size":
|
||||||
sizeBytes, err := units.RAMInBytes(val)
|
sizeBytes, err := units.RAMInBytes(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid value for %s: %s", key, val)
|
return fmt.Errorf("invalid value for %s: %s", key, val)
|
||||||
}
|
}
|
||||||
tmpfsOptions().SizeBytes = sizeBytes
|
ensureTmpfsOptions(&mount).SizeBytes = sizeBytes
|
||||||
case "tmpfs-mode":
|
case "tmpfs-mode":
|
||||||
ui64, err := strconv.ParseUint(val, 8, 32)
|
ui64, err := strconv.ParseUint(val, 8, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid value for %s: %s", key, val)
|
return fmt.Errorf("invalid value for %s: %s", key, val)
|
||||||
}
|
}
|
||||||
tmpfsOptions().Mode = os.FileMode(ui64)
|
ensureTmpfsOptions(&mount).Mode = os.FileMode(ui64)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
|
return fmt.Errorf("unknown option '%s' in '%s'", key, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mount.Type == "" {
|
if err := validateMountOptions(&mount); err != nil {
|
||||||
return errors.New("type is required")
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if mount.Target == "" {
|
|
||||||
return errors.New("target is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if mount.VolumeOptions != nil && mount.Type != mounttypes.TypeVolume {
|
|
||||||
return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mount.Type)
|
|
||||||
}
|
|
||||||
if mount.ImageOptions != nil && mount.Type != mounttypes.TypeImage {
|
|
||||||
return fmt.Errorf("cannot mix 'image-*' options with mount type '%s'", mount.Type)
|
|
||||||
}
|
|
||||||
if mount.BindOptions != nil && mount.Type != mounttypes.TypeBind {
|
|
||||||
return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mount.Type)
|
|
||||||
}
|
|
||||||
if mount.TmpfsOptions != nil && mount.Type != mounttypes.TypeTmpfs {
|
|
||||||
return fmt.Errorf("cannot mix 'tmpfs-*' options with mount type '%s'", mount.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mount.BindOptions != nil {
|
|
||||||
if mount.BindOptions.ReadOnlyNonRecursive {
|
|
||||||
if !mount.ReadOnly {
|
|
||||||
return errors.New("option 'bind-recursive=writable' requires 'readonly' to be specified in conjunction")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mount.BindOptions.ReadOnlyForceRecursive {
|
|
||||||
if !mount.ReadOnly {
|
|
||||||
return errors.New("option 'bind-recursive=readonly' requires 'readonly' to be specified in conjunction")
|
|
||||||
}
|
|
||||||
if mount.BindOptions.Propagation != mounttypes.PropagationRPrivate {
|
|
||||||
return errors.New("option 'bind-recursive=readonly' requires 'bind-propagation=rprivate' to be specified in conjunction")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.values = append(m.values, mount)
|
m.values = append(m.values, mount)
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
package opts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/moby/moby/api/types/mount"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validateMountOptions performs client-side validation of mount options. Similar
|
||||||
|
// validation happens on the daemon side, but this validation allows us to
|
||||||
|
// produce user-friendly errors matching command-line options.
|
||||||
|
func validateMountOptions(m *mount.Mount) error {
|
||||||
|
if err := validateExclusiveOptions(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.BindOptions != nil {
|
||||||
|
if m.BindOptions.ReadOnlyNonRecursive && !m.ReadOnly {
|
||||||
|
return errors.New("option 'bind-recursive=writable' requires 'readonly' to be specified in conjunction")
|
||||||
|
}
|
||||||
|
if m.BindOptions.ReadOnlyForceRecursive {
|
||||||
|
if !m.ReadOnly {
|
||||||
|
return errors.New("option 'bind-recursive=readonly' requires 'readonly' to be specified in conjunction")
|
||||||
|
}
|
||||||
|
if m.BindOptions.Propagation != mount.PropagationRPrivate {
|
||||||
|
// FIXME(thaJeztah): this is missing daemon-side validation
|
||||||
|
//
|
||||||
|
// docker run --rm --mount type=bind,src=/var/run,target=/foo,bind-recursive=readonly,readonly alpine
|
||||||
|
// # no error
|
||||||
|
return errors.New("option 'bind-recursive=readonly' requires 'bind-propagation=rprivate' to be specified in conjunction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateExclusiveOptions checks if the given mount config only contains
|
||||||
|
// options for the given mount-type.
|
||||||
|
//
|
||||||
|
// This is the client-side equivalent of [mounts.validateExclusiveOptions] in
|
||||||
|
// the daemon, but with error-messages matching client-side flags / options.
|
||||||
|
//
|
||||||
|
// [mounts.validateExclusiveOptions]: https://github.com/moby/moby/blob/v2.0.0-beta.6/daemon/volume/mounts/validate.go#L31-L50
|
||||||
|
func validateExclusiveOptions(m *mount.Mount) error {
|
||||||
|
if m.Type == "" {
|
||||||
|
return errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Type != mount.TypeBind && m.BindOptions != nil {
|
||||||
|
return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", m.Type)
|
||||||
|
}
|
||||||
|
if m.Type != mount.TypeVolume && m.VolumeOptions != nil {
|
||||||
|
return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", m.Type)
|
||||||
|
}
|
||||||
|
if m.Type != mount.TypeImage && m.ImageOptions != nil {
|
||||||
|
return fmt.Errorf("cannot mix 'image-*' options with mount type '%s'", m.Type)
|
||||||
|
}
|
||||||
|
if m.Type != mount.TypeTmpfs && m.TmpfsOptions != nil {
|
||||||
|
return fmt.Errorf("cannot mix 'tmpfs-*' options with mount type '%s'", m.Type)
|
||||||
|
}
|
||||||
|
if m.Type != mount.TypeCluster && m.ClusterOptions != nil {
|
||||||
|
return fmt.Errorf("cannot mix 'cluster-*' options with mount type '%s'", m.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBoolValue returns the boolean value represented by the string. It returns
|
||||||
|
// true if no value is set.
|
||||||
|
//
|
||||||
|
// It is similar to [strconv.ParseBool], but only accepts 1, true, 0, false.
|
||||||
|
// Any other value returns an error.
|
||||||
|
func parseBoolValue(key string, val string, hasValue bool) (bool, error) {
|
||||||
|
if !hasValue {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
switch val {
|
||||||
|
case "1", "true":
|
||||||
|
return true, nil
|
||||||
|
case "0", "false":
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf(`invalid value for '%s': invalid boolean value (%q): must be one of "true", "1", "false", or "0" (default "true")`, key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureVolumeOptions(m *mount.Mount) *mount.VolumeOptions {
|
||||||
|
if m.VolumeOptions == nil {
|
||||||
|
m.VolumeOptions = &mount.VolumeOptions{}
|
||||||
|
}
|
||||||
|
return m.VolumeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureVolumeDriver(m *mount.Mount) *mount.Driver {
|
||||||
|
ensureVolumeOptions(m)
|
||||||
|
if m.VolumeOptions.DriverConfig == nil {
|
||||||
|
m.VolumeOptions.DriverConfig = &mount.Driver{}
|
||||||
|
}
|
||||||
|
return m.VolumeOptions.DriverConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureImageOptions(m *mount.Mount) *mount.ImageOptions {
|
||||||
|
if m.ImageOptions == nil {
|
||||||
|
m.ImageOptions = &mount.ImageOptions{}
|
||||||
|
}
|
||||||
|
return m.ImageOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureBindOptions(m *mount.Mount) *mount.BindOptions {
|
||||||
|
if m.BindOptions == nil {
|
||||||
|
m.BindOptions = &mount.BindOptions{}
|
||||||
|
}
|
||||||
|
return m.BindOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureTmpfsOptions(m *mount.Mount) *mount.TmpfsOptions {
|
||||||
|
if m.TmpfsOptions == nil {
|
||||||
|
m.TmpfsOptions = &mount.TmpfsOptions{}
|
||||||
|
}
|
||||||
|
return m.TmpfsOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func setValueOnMap(target map[string]string, keyValue string) map[string]string {
|
||||||
|
k, v, _ := strings.Cut(keyValue, "=")
|
||||||
|
if k == "" {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
if target == nil {
|
||||||
|
target = map[string]string{}
|
||||||
|
}
|
||||||
|
target[k] = v
|
||||||
|
return target
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -284,7 +284,7 @@ github.com/digitorus/timestamp
|
|||||||
# github.com/distribution/reference v0.6.0
|
# github.com/distribution/reference v0.6.0
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
github.com/distribution/reference
|
github.com/distribution/reference
|
||||||
# github.com/docker/cli v29.2.0+incompatible
|
# github.com/docker/cli v29.2.1+incompatible
|
||||||
## explicit
|
## explicit
|
||||||
github.com/docker/cli/cli
|
github.com/docker/cli/cli
|
||||||
github.com/docker/cli/cli-plugins/metadata
|
github.com/docker/cli/cli-plugins/metadata
|
||||||
|
|||||||
Reference in New Issue
Block a user