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:
Sebastiaan van Stijn
2026-02-04 11:03:56 +01:00
parent 8bd5bd4983
commit 79b14eaeab
5 changed files with 182 additions and 110 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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