Merge pull request #3617 from thaJeztah/bump_modules

vendor: github.com/docker/cli v29.2.1
This commit is contained in:
CrazyMax
2026-02-04 12:59:30 +01:00
committed by GitHub
51 changed files with 1015 additions and 599 deletions
+5 -5
View File
@@ -16,7 +16,7 @@ require (
github.com/creack/pty v1.1.24
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/distribution/reference v0.6.0
github.com/docker/cli v29.1.5+incompatible
github.com/docker/cli v29.2.1+incompatible
github.com/docker/cli-docs-tool v0.11.0
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0
@@ -30,8 +30,8 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/moby/buildkit v0.27.0
github.com/moby/go-archive v0.2.0
github.com/moby/moby/api v1.52.0
github.com/moby/moby/client v0.2.1
github.com/moby/moby/api v1.53.0
github.com/moby/moby/client v0.2.2
github.com/moby/policy-helpers v0.0.0-20251206004813-9fcc1a9ec5c9
github.com/moby/sys/atomicwriter v0.1.0
github.com/moby/sys/mountinfo v0.7.2
@@ -138,7 +138,7 @@ require (
github.com/go-openapi/swag/typeutils v0.25.3 // indirect
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@@ -154,7 +154,7 @@ require (
github.com/in-toto/attestation v1.1.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
+10 -10
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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
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/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
@@ -271,8 +271,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -378,8 +378,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -426,10 +426,10 @@ github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/policy-helpers v0.0.0-20251206004813-9fcc1a9ec5c9 h1:SISQT6l9QO+JGSTJ8QN0yWCueiSgfTStSU9KT+p537M=
+2 -3
View File
@@ -60,6 +60,7 @@ type Cli interface {
type DockerCli struct {
configFile *configfile.ConfigFile
options *cliflags.ClientOptions
clientOpts []client.Opt
in *streams.In
out *streams.Out
err *streams.Out
@@ -72,7 +73,6 @@ type DockerCli struct {
dockerEndpoint docker.Endpoint
contextStoreConfig *store.Config
initTimeout time.Duration
userAgent string
res telemetryResource
// baseCtx is the base context used for internal operations. In the future
@@ -533,8 +533,7 @@ func (cli *DockerCli) initialize() error {
return
}
if cli.client == nil {
ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, cli.clientOpts...); cli.initErr != nil {
return
}
}
+11 -1
View File
@@ -104,6 +104,16 @@ func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error))
}
}
// WithAPIClientOptions configures additional [client.Opt] to use when
// initializing the API client. These options have no effect if a custom
// client is set (through [WithAPIClient] or [WithInitializeClient]).
func WithAPIClientOptions(c ...client.Opt) CLIOption {
return func(cli *DockerCli) error {
cli.clientOpts = append(cli.clientOpts, c...)
return nil
}
}
// envOverrideHTTPHeaders is the name of the environment-variable that can be
// used to set custom HTTP headers to be sent by the client. This environment
// variable is the equivalent to the HttpHeaders field in the configuration
@@ -221,7 +231,7 @@ func WithUserAgent(userAgent string) CLIOption {
if userAgent == "" {
return errors.New("user agent cannot be blank")
}
cli.userAgent = userAgent
cli.clientOpts = append(cli.clientOpts, client.WithUserAgent(userAgent))
return nil
}
}
+1 -5
View File
@@ -200,7 +200,7 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
return "", err
}
encodedAuth, err := authconfig.Encode(registrytypes.AuthConfig{
return authconfig.Encode(registrytypes.AuthConfig{
Username: authConfig.Username,
Password: authConfig.Password,
ServerAddress: authConfig.ServerAddress,
@@ -210,10 +210,6 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
IdentityToken: authConfig.IdentityToken,
RegistryToken: authConfig.RegistryToken,
})
if err != nil {
return "", err
}
return encodedAuth, nil
}
// getAuthConfigKey special-cases using the full index address of the official
+43 -106
View File
@@ -22,72 +22,43 @@ type MountOpt struct {
//
//nolint:gocyclo
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))
fields, err := csvReader.Read()
if err != nil {
return err
}
mount := mounttypes.Mount{}
mount := mounttypes.Mount{
Type: mounttypes.TypeVolume, // default to volume mounts
}
volumeOptions := func() *mounttypes.VolumeOptions {
if mount.VolumeOptions == nil {
mount.VolumeOptions = &mounttypes.VolumeOptions{
Labels: make(map[string]string),
for _, field := range fields {
key, val, hasValue := strings.Cut(field, "=")
if k := strings.TrimSpace(key); k != key {
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.
key = strings.ToLower(key)
if !ok {
if !hasValue {
switch key {
case "readonly", "ro":
mount.ReadOnly = true
continue
case "volume-nocopy":
volumeOptions().NoCopy = true
continue
case "bind-nonrecursive":
return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive":
// boolean values
default:
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":
mount.Target = val
case "readonly", "ro":
mount.ReadOnly, err = strconv.ParseBool(val)
mount.ReadOnly, err = parseBoolValue(key, val, hasValue)
if err != nil {
return fmt.Errorf("invalid value for %s: %s", key, val)
return err
}
case "consistency":
mount.Consistency = mounttypes.Consistency(strings.ToLower(val))
case "bind-propagation":
bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(val))
ensureBindOptions(&mount).Propagation = mounttypes.Propagation(strings.ToLower(val))
case "bind-nonrecursive":
return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
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
// NOP
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
bindOptions().ReadOnlyNonRecursive = true
ensureBindOptions(&mount).ReadOnlyNonRecursive = true
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
// https://github.com/docker/cli/pull/4316#discussion_r1341974730
default:
return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
}
case "volume-subpath":
volumeOptions().Subpath = val
ensureVolumeOptions(&mount).Subpath = val
case "volume-nocopy":
volumeOptions().NoCopy, err = strconv.ParseBool(val)
ensureVolumeOptions(&mount).NoCopy, err = parseBoolValue(key, val, hasValue)
if err != nil {
return fmt.Errorf("invalid value for volume-nocopy: %s", val)
return err
}
case "volume-label":
setValueOnMap(volumeOptions().Labels, val)
volumeOpts := ensureVolumeOptions(&mount)
volumeOpts.Labels = setValueOnMap(volumeOpts.Labels, val)
case "volume-driver":
volumeOptions().DriverConfig.Name = val
ensureVolumeDriver(&mount).Name = val
case "volume-opt":
if volumeOptions().DriverConfig.Options == nil {
volumeOptions().DriverConfig.Options = make(map[string]string)
}
setValueOnMap(volumeOptions().DriverConfig.Options, val)
volumeDriver := ensureVolumeDriver(&mount)
volumeDriver.Options = setValueOnMap(volumeDriver.Options, val)
case "image-subpath":
imageOptions().Subpath = val
ensureImageOptions(&mount).Subpath = val
case "tmpfs-size":
sizeBytes, err := units.RAMInBytes(val)
if err != nil {
return fmt.Errorf("invalid value for %s: %s", key, val)
}
tmpfsOptions().SizeBytes = sizeBytes
ensureTmpfsOptions(&mount).SizeBytes = sizeBytes
case "tmpfs-mode":
ui64, err := strconv.ParseUint(val, 8, 32)
if err != nil {
return fmt.Errorf("invalid value for %s: %s", key, val)
}
tmpfsOptions().Mode = os.FileMode(ui64)
ensureTmpfsOptions(&mount).Mode = os.FileMode(ui64)
default:
return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
return fmt.Errorf("unknown option '%s' in '%s'", key, field)
}
}
if mount.Type == "" {
return errors.New("type is required")
}
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")
}
}
if err := validateMountOptions(&mount); err != nil {
return err
}
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
}
+41 -1
View File
@@ -6,6 +6,9 @@ package templates
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"text/template"
)
@@ -15,7 +18,7 @@ import (
var basicFunctions = template.FuncMap{
"json": formatJSON,
"split": strings.Split,
"join": strings.Join,
"join": joinElements,
"title": strings.Title, //nolint:nolintlint,staticcheck // strings.Title is deprecated, but we only use it for ASCII, so replacing with golang.org/x/text is out of scope
"lower": strings.ToLower,
"upper": strings.ToUpper,
@@ -97,3 +100,40 @@ func formatJSON(v any) string {
// Remove the trailing new line added by the encoder
return strings.TrimSpace(buf.String())
}
// joinElements joins a slice of items with the given separator. It uses
// [strings.Join] if it's a slice of strings, otherwise uses [fmt.Sprint]
// to join each item to the output.
func joinElements(elems any, sep string) (string, error) {
if elems == nil {
return "", nil
}
if ss, ok := elems.([]string); ok {
return strings.Join(ss, sep), nil
}
switch rv := reflect.ValueOf(elems); rv.Kind() { //nolint:exhaustive // ignore: too many options to make exhaustive
case reflect.Array, reflect.Slice:
var b strings.Builder
for i := range rv.Len() {
if i > 0 {
b.WriteString(sep)
}
_, _ = fmt.Fprint(&b, rv.Index(i).Interface())
}
return b.String(), nil
case reflect.Map:
var out []string
for _, k := range rv.MapKeys() {
out = append(out, fmt.Sprint(rv.MapIndex(k).Interface()))
}
// Not ideal, but trying to keep a consistent order
sort.Strings(out)
return strings.Join(out, sep), nil
default:
return "", fmt.Errorf("expected slice, got %T", elems)
}
}
+3
View File
@@ -19,3 +19,6 @@ indent_size = 2
[.golangci.yaml]
indent_size = 2
[devenv.yaml]
indent_size = 2
+7 -4
View File
@@ -1,4 +1,7 @@
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
fi
use flake . --impure
#!/usr/bin/env bash
export DIRENV_WARN_TIMEOUT=20s
eval "$(devenv direnvrc)"
use devenv
+7 -3
View File
@@ -1,6 +1,10 @@
/.devenv/
/.direnv/
/.pre-commit-config.yaml
/bin/
/build/
/var/
# Devenv
.devenv*
devenv.local.nix
devenv.local.yaml
.direnv
.pre-commit-config.yaml
+103
View File
@@ -0,0 +1,103 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1765288076,
"owner": "cachix",
"repo": "devenv",
"rev": "93c055af1e8fcac49251f1b2e1c57f78620ad351",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1765121682,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1765016596,
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "548fc44fca28a5e81c5d6b846e555e6b9c2a5a3c",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1762808025,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1764580874,
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": [
"git-hooks"
]
}
}
},
"root": "root",
"version": 7
}
+14
View File
@@ -0,0 +1,14 @@
{
pkgs,
...
}:
{
languages = {
go.enable = true;
};
packages = with pkgs; [
golangci-lint
];
}
+4
View File
@@ -0,0 +1,4 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
-294
View File
@@ -1,294 +0,0 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": [
"devenv"
],
"flake-compat": [
"devenv"
],
"git-hooks": [
"devenv"
],
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1742042642,
"narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=",
"owner": "cachix",
"repo": "cachix",
"rev": "a624d3eaf4b1d225f918de8543ed739f2f574203",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat",
"git-hooks": "git-hooks",
"nix": "nix",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1744876578,
"narHash": "sha256-8MTBj2REB8t29sIBLpxbR0+AEGJ7f+RkzZPAGsFd40c=",
"owner": "cachix",
"repo": "devenv",
"rev": "7ff7c351bba20d0615be25ecdcbcf79b57b85fe1",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"devenv"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1742649964,
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"devenv"
],
"flake-parts": "flake-parts",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_2",
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
],
"pre-commit-hooks": [
"devenv"
]
},
"locked": {
"lastModified": 1741798497,
"narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=",
"owner": "domenkozar",
"repo": "nix",
"rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.24",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1733212471,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733477122,
"narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_4"
}
}
},
"root": "root",
"version": 7
}
-46
View File
@@ -1,46 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
devenv.url = "github:cachix/devenv";
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.devenv.flakeModule
];
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
perSystem =
{ pkgs, ... }:
rec {
devenv.shells = {
default = {
languages = {
go.enable = true;
};
pre-commit.hooks = {
nixpkgs-fmt.enable = true;
};
packages = with pkgs; [
golangci-lint
];
# https://github.com/cachix/devenv/issues/528#issuecomment-1556108767
containers = pkgs.lib.mkForce { };
};
ci = devenv.shells.default;
};
};
};
}
+284 -48
View File
@@ -173,6 +173,25 @@
// Public: "I made it through!"
// }
//
// # Custom Decoding with Unmarshaler
//
// Types can implement the Unmarshaler interface to control their own decoding. The interface
// behaves similarly to how UnmarshalJSON does in the standard library. It can be used as an
// alternative or companion to a DecodeHook.
//
// type TrimmedString string
//
// func (t *TrimmedString) UnmarshalMapstructure(input any) error {
// str, ok := input.(string)
// if !ok {
// return fmt.Errorf("expected string, got %T", input)
// }
// *t = TrimmedString(strings.TrimSpace(str))
// return nil
// }
//
// See the Unmarshaler interface documentation for more details.
//
// # Other Configuration
//
// mapstructure is highly configurable. See the DecoderConfig struct
@@ -218,6 +237,17 @@ type DecodeHookFuncKind func(reflect.Kind, reflect.Kind, any) (any, error)
// values.
type DecodeHookFuncValue func(from reflect.Value, to reflect.Value) (any, error)
// Unmarshaler is the interface implemented by types that can unmarshal
// themselves. UnmarshalMapstructure receives the input data (potentially
// transformed by DecodeHook) and should populate the receiver with the
// decoded values.
//
// The Unmarshaler interface takes precedence over the default decoding
// logic for any type (structs, slices, maps, primitives, etc.).
type Unmarshaler interface {
UnmarshalMapstructure(any) error
}
// DecoderConfig is the configuration that is used to create a new decoder
// and allows customization of various aspects of decoding.
type DecoderConfig struct {
@@ -281,6 +311,13 @@ type DecoderConfig struct {
// }
Squash bool
// Deep will map structures in slices instead of copying them
//
// type Parent struct {
// Children []Child `mapstructure:",deep"`
// }
Deep bool
// Metadata is the struct that will contain extra metadata about
// the decoding. If this is nil, then no metadata will be tracked.
Metadata *Metadata
@@ -290,9 +327,15 @@ type DecoderConfig struct {
Result any
// The tag name that mapstructure reads for field names. This
// defaults to "mapstructure"
// defaults to "mapstructure". Multiple tag names can be specified
// as a comma-separated list (e.g., "yaml,json"), and the first
// matching non-empty tag will be used.
TagName string
// RootName specifies the name to use for the root element in error messages. For example:
// '<rootName>' has unset fields: <fieldName>
RootName string
// The option of the value in the tag that indicates a field should
// be squashed. This defaults to "squash".
SquashTagOption string
@@ -304,11 +347,34 @@ type DecoderConfig struct {
// MatchName is the function used to match the map key to the struct
// field name or tag. Defaults to `strings.EqualFold`. This can be used
// to implement case-sensitive tag values, support snake casing, etc.
//
// MatchName is used as a fallback comparison when the direct key lookup fails.
// See also MapFieldName for transforming field names before lookup.
MatchName func(mapKey, fieldName string) bool
// DecodeNil, if set to true, will cause the DecodeHook (if present) to run
// even if the input is nil. This can be used to provide default values.
DecodeNil bool
// MapFieldName is the function used to convert the struct field name to the map's key name.
//
// This is useful for automatically converting between naming conventions without
// explicitly tagging each field. For example, to convert Go's PascalCase field names
// to snake_case map keys:
//
// MapFieldName: func(s string) string {
// return strcase.ToSnake(s)
// }
//
// When decoding from a map to a struct, the transformed field name is used for
// the initial lookup. If not found, MatchName is used as a fallback comparison.
// Explicit struct tags always take precedence over MapFieldName.
MapFieldName func(string) string
// DisableUnmarshaler, if set to true, disables the use of the Unmarshaler
// interface. Types implementing Unmarshaler will be decoded using the
// standard struct decoding logic instead.
DisableUnmarshaler bool
}
// A Decoder takes a raw interface value and turns it into structured
@@ -445,6 +511,12 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
config.MatchName = strings.EqualFold
}
if config.MapFieldName == nil {
config.MapFieldName = func(s string) string {
return s
}
}
result := &Decoder{
config: config,
}
@@ -458,7 +530,7 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
// Decode decodes the given raw interface to the target pointer specified
// by the configuration.
func (d *Decoder) Decode(input any) error {
err := d.decode("", input, reflect.ValueOf(d.config.Result).Elem())
err := d.decode(d.config.RootName, input, reflect.ValueOf(d.config.Result).Elem())
// Retain some of the original behavior when multiple errors ocurr
var joinedErr interface{ Unwrap() []error }
@@ -540,36 +612,50 @@ func (d *Decoder) decode(name string, input any, outVal reflect.Value) error {
var err error
addMetaKey := true
switch outputKind {
case reflect.Bool:
err = d.decodeBool(name, input, outVal)
case reflect.Interface:
err = d.decodeBasic(name, input, outVal)
case reflect.String:
err = d.decodeString(name, input, outVal)
case reflect.Int:
err = d.decodeInt(name, input, outVal)
case reflect.Uint:
err = d.decodeUint(name, input, outVal)
case reflect.Float32:
err = d.decodeFloat(name, input, outVal)
case reflect.Complex64:
err = d.decodeComplex(name, input, outVal)
case reflect.Struct:
err = d.decodeStruct(name, input, outVal)
case reflect.Map:
err = d.decodeMap(name, input, outVal)
case reflect.Ptr:
addMetaKey, err = d.decodePtr(name, input, outVal)
case reflect.Slice:
err = d.decodeSlice(name, input, outVal)
case reflect.Array:
err = d.decodeArray(name, input, outVal)
case reflect.Func:
err = d.decodeFunc(name, input, outVal)
default:
// If we reached this point then we weren't able to decode it
return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind))
// Check if the target implements Unmarshaler and use it if not disabled
unmarshaled := false
if !d.config.DisableUnmarshaler {
if unmarshaler, ok := getUnmarshaler(outVal); ok {
if err = unmarshaler.UnmarshalMapstructure(input); err != nil {
err = newDecodeError(name, err)
}
unmarshaled = true
}
}
if !unmarshaled {
switch outputKind {
case reflect.Bool:
err = d.decodeBool(name, input, outVal)
case reflect.Interface:
err = d.decodeBasic(name, input, outVal)
case reflect.String:
err = d.decodeString(name, input, outVal)
case reflect.Int:
err = d.decodeInt(name, input, outVal)
case reflect.Uint:
err = d.decodeUint(name, input, outVal)
case reflect.Float32:
err = d.decodeFloat(name, input, outVal)
case reflect.Complex64:
err = d.decodeComplex(name, input, outVal)
case reflect.Struct:
err = d.decodeStruct(name, input, outVal)
case reflect.Map:
err = d.decodeMap(name, input, outVal)
case reflect.Ptr:
addMetaKey, err = d.decodePtr(name, input, outVal)
case reflect.Slice:
err = d.decodeSlice(name, input, outVal)
case reflect.Array:
err = d.decodeArray(name, input, outVal)
case reflect.Func:
err = d.decodeFunc(name, input, outVal)
default:
// If we reached this point then we weren't able to decode it
return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind))
}
}
// If we reached here, then we successfully decoded SOMETHING, so
@@ -668,7 +754,7 @@ func (d *Decoder) decodeString(name string, data any, val reflect.Value) error {
case reflect.Uint8:
var uints []uint8
if dataKind == reflect.Array {
uints = make([]uint8, dataVal.Len(), dataVal.Len())
uints = make([]uint8, dataVal.Len())
for i := range uints {
uints[i] = dataVal.Index(i).Interface().(uint8)
}
@@ -1060,8 +1146,8 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
)
}
tagValue := f.Tag.Get(d.config.TagName)
keyName := f.Name
tagValue, _ := getTagValue(f, d.config.TagName)
keyName := d.config.MapFieldName(f.Name)
if tagValue == "" && d.config.IgnoreUntaggedFields {
continue
@@ -1070,6 +1156,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
// If Squash is set in the config, we squash the field down.
squash := d.config.Squash && v.Kind() == reflect.Struct && f.Anonymous
// If Deep is set in the config, set as default value.
deep := d.config.Deep
v = dereferencePtrToStructIfNeeded(v, d.config.TagName)
// Determine the name of the key in the map
@@ -1078,12 +1167,12 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
continue
}
// If "omitempty" is specified in the tag, it ignores empty values.
if strings.Index(tagValue[index+1:], "omitempty") != -1 && isEmptyValue(v) {
if strings.Contains(tagValue[index+1:], "omitempty") && isEmptyValue(v) {
continue
}
// If "omitzero" is specified in the tag, it ignores zero values.
if strings.Index(tagValue[index+1:], "omitzero") != -1 && v.IsZero() {
if strings.Contains(tagValue[index+1:], "omitzero") && v.IsZero() {
continue
}
@@ -1103,7 +1192,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
)
}
} else {
if strings.Index(tagValue[index+1:], "remain") != -1 {
if strings.Contains(tagValue[index+1:], "remain") {
if v.Kind() != reflect.Map {
return newDecodeError(
name+"."+f.Name,
@@ -1118,6 +1207,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
continue
}
}
deep = deep || strings.Contains(tagValue[index+1:], "deep")
if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" {
keyName = keyNameTagValue
}
@@ -1164,6 +1256,41 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
valMap.SetMapIndex(reflect.ValueOf(keyName), vMap)
}
case reflect.Slice:
if deep {
var childType reflect.Type
switch v.Type().Elem().Kind() {
case reflect.Struct:
childType = reflect.TypeOf(map[string]any{})
default:
childType = v.Type().Elem()
}
sType := reflect.SliceOf(childType)
addrVal := reflect.New(sType)
vSlice := reflect.MakeSlice(sType, v.Len(), v.Cap())
if v.Len() > 0 {
reflect.Indirect(addrVal).Set(vSlice)
err := d.decode(keyName, v.Interface(), reflect.Indirect(addrVal))
if err != nil {
return err
}
}
vSlice = reflect.Indirect(addrVal)
valMap.SetMapIndex(reflect.ValueOf(keyName), vSlice)
break
}
// When deep mapping is not needed, fallthrough to normal copy
fallthrough
default:
valMap.SetMapIndex(reflect.ValueOf(keyName), v)
}
@@ -1471,7 +1598,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
remain := false
// We always parse the tags cause we're looking for other tags too
tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",")
tagParts := getTagParts(fieldType, d.config.TagName)
if len(tagParts) == 0 {
tagParts = []string{""}
}
for _, tag := range tagParts[1:] {
if tag == d.config.SquashTagOption {
squash = true
@@ -1492,6 +1622,18 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !fieldVal.IsNil() {
structs = append(structs, fieldVal.Elem().Elem())
}
case reflect.Ptr:
if fieldVal.Type().Elem().Kind() == reflect.Struct {
if fieldVal.IsNil() {
fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
}
structs = append(structs, fieldVal.Elem())
} else {
errs = append(errs, newDecodeError(
name+"."+fieldType.Name,
fmt.Errorf("unsupported type for squashed pointer: %s", fieldVal.Type().Elem().Kind()),
))
}
default:
errs = append(errs, newDecodeError(
name+"."+fieldType.Name,
@@ -1516,13 +1658,15 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field, fieldValue := f.field, f.val
fieldName := field.Name
tagValue := field.Tag.Get(d.config.TagName)
tagValue, _ := getTagValue(field, d.config.TagName)
if tagValue == "" && d.config.IgnoreUntaggedFields {
continue
}
tagValue = strings.SplitN(tagValue, ",", 2)[0]
if tagValue != "" {
fieldName = tagValue
} else {
fieldName = d.config.MapFieldName(fieldName)
}
rawMapKey := reflect.ValueOf(fieldName)
@@ -1605,8 +1749,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
}
sort.Strings(keys)
// Improve error message when name is empty by showing the target struct type
// in the case where it is empty for embedded structs.
errorName := name
if errorName == "" {
errorName = val.Type().String()
}
errs = append(errs, newDecodeError(
name,
errorName,
fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")),
))
}
@@ -1692,7 +1842,7 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool,
if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields
return true
}
if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside
if checkMapstructureTags && hasAnyTag(f, tagName) { // check for mapstructure tags inside
return true
}
}
@@ -1700,13 +1850,99 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool,
}
func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Value {
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
if v.Kind() != reflect.Ptr {
return v
}
deref := v.Elem()
derefT := deref.Type()
if isStructTypeConvertibleToMap(derefT, true, tagName) {
return deref
switch v.Elem().Kind() {
case reflect.Slice:
return v.Elem()
case reflect.Struct:
deref := v.Elem()
derefT := deref.Type()
if isStructTypeConvertibleToMap(derefT, true, tagName) {
return deref
}
return v
default:
return v
}
return v
}
func hasAnyTag(field reflect.StructField, tagName string) bool {
_, ok := getTagValue(field, tagName)
return ok
}
func getTagParts(field reflect.StructField, tagName string) []string {
tagValue, ok := getTagValue(field, tagName)
if !ok {
return nil
}
return strings.Split(tagValue, ",")
}
func getTagValue(field reflect.StructField, tagName string) (string, bool) {
for _, name := range splitTagNames(tagName) {
if tag := field.Tag.Get(name); tag != "" {
return tag, true
}
}
return "", false
}
func splitTagNames(tagName string) []string {
if tagName == "" {
return []string{"mapstructure"}
}
parts := strings.Split(tagName, ",")
result := make([]string, 0, len(parts))
for _, name := range parts {
name = strings.TrimSpace(name)
if name != "" {
result = append(result, name)
}
}
return result
}
// unmarshalerType is cached for performance
var unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
// getUnmarshaler checks if the value implements Unmarshaler and returns
// the Unmarshaler and a boolean indicating if it was found. It handles both
// pointer and value receivers.
func getUnmarshaler(val reflect.Value) (Unmarshaler, bool) {
// Skip invalid or nil values
if !val.IsValid() {
return nil, false
}
switch val.Kind() {
case reflect.Pointer, reflect.Interface:
if val.IsNil() {
return nil, false
}
}
// Check pointer receiver first (most common case)
if val.CanAddr() {
ptrVal := val.Addr()
// Quick check: if no methods, can't implement any interface
if ptrVal.Type().NumMethod() > 0 && ptrVal.Type().Implements(unmarshalerType) {
return ptrVal.Interface().(Unmarshaler), true
}
}
// Check value receiver
// Quick check: if no methods, can't implement any interface
if val.Type().NumMethod() > 0 && val.CanInterface() && val.Type().Implements(unmarshalerType) {
return val.Interface().(Unmarshaler), true
}
return nil, false
}
+14 -3
View File
@@ -1,7 +1,7 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@@ -176,13 +176,24 @@
END OF TERMS AND CONDITIONS
Copyright 2013-2018 Docker, Inc.
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+5 -1
View File
@@ -71,11 +71,15 @@ func DecodeRequestBody(r io.ReadCloser) (*registry.AuthConfig, error) {
func decode(r io.Reader) (*registry.AuthConfig, error) {
authConfig := &registry.AuthConfig{}
if err := json.NewDecoder(r).Decode(authConfig); err != nil {
dec := json.NewDecoder(r)
if err := dec.Decode(authConfig); err != nil {
// always return an (empty) AuthConfig to increase compatibility with
// the existing API.
return &registry.AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
}
if dec.More() {
return &registry.AuthConfig{}, invalid(errors.New("multiple JSON documents not allowed"))
}
return authConfig, nil
}
+15
View File
@@ -0,0 +1,15 @@
package image
import (
"time"
)
// BuildIdentity contains build reference information if image was created via build.
type BuildIdentity struct {
// Ref is the identifier for the build request. This reference can be used to
// look up the build details in BuildKit history API.
Ref string `json:"Ref,omitempty"`
// CreatedAt is the time when the build ran.
CreatedAt time.Time `json:"CreatedAt,omitempty"`
}
@@ -2,13 +2,12 @@
package image
// ----------------------------------------------------------------------------
// Code generated by `swagger generate operation`.
//
// See hack/generate-swagger-api.sh
// ----------------------------------------------------------------------------
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// HistoryResponseItem individual image layer information in response to ImageHistory operation
// HistoryResponseItem HistoryResponseItem
//
// individual image layer information in response to ImageHistory operation
//
// swagger:model HistoryResponseItem
type HistoryResponseItem struct {
+15
View File
@@ -0,0 +1,15 @@
package image
// Identity holds information about the identity and origin of the image.
// This is trusted information verified by the daemon and cannot be modified
// by tagging an image to a different name.
type Identity struct {
// Signature contains the properties of verified signatures for the image.
Signature []SignatureIdentity `json:"Signature,omitzero"`
// Pull contains remote location information if image was created via pull.
// If image was pulled via mirror, this contains the original repository location.
// After successful push this images also contains the pushed repository location.
Pull []PullIdentity `json:"Pull,omitzero"`
// Build contains build reference information if image was created via build.
Build []BuildIdentity `json:"Build,omitzero"`
}
+29
View File
@@ -105,4 +105,33 @@ type InspectResponse struct {
// WARNING: This is experimental and may change at any time without any backward
// compatibility.
Manifests []ManifestSummary `json:"Manifests,omitempty"`
// Identity holds information about the identity and origin of the image.
// This is trusted information verified by the daemon and cannot be modified
// by tagging an image to a different name.
Identity *Identity `json:"Identity,omitempty"`
}
// SignatureTimestampType is the type of timestamp used in the signature.
type SignatureTimestampType string
const (
SignatureTimestampTlog SignatureTimestampType = "Tlog"
SignatureTimestampAuthority SignatureTimestampType = "TimestampAuthority"
)
// SignatureType is the type of signature format.
type SignatureType string
const (
SignatureTypeBundleV03 SignatureType = "bundle-v0.3"
SignatureTypeSimpleSigningV1 SignatureType = "simplesigning-v1"
)
// KnownSignerIdentity is an identifier for a special signer identity that is known to the implementation.
type KnownSignerIdentity string
const (
// KnownSignerDHI is the known identity for Docker Hardened Images.
KnownSignerDHI KnownSignerIdentity = "DHI"
)
+8
View File
@@ -0,0 +1,8 @@
package image
// PullIdentity contains remote location information if image was created via pull.
// If image was pulled via mirror, this contains the original repository location.
type PullIdentity struct {
// Repository is the remote repository location the image was pulled from.
Repository string `json:"Repository,omitempty"`
}
+26
View File
@@ -0,0 +1,26 @@
package image
// SignatureIdentity contains the properties of verified signatures for the image.
type SignatureIdentity struct {
// Name is a textual description summarizing the type of signature.
Name string `json:"Name,omitempty"`
// Timestamps contains a list of verified signed timestamps for the signature.
Timestamps []SignatureTimestamp `json:"Timestamps,omitzero"`
// KnownSigner is an identifier for a special signer identity that is known to the implementation.
KnownSigner KnownSignerIdentity `json:"KnownSigner,omitempty"`
// DockerReference is the Docker image reference associated with the signature.
// This is an optional field only present in older hashedrecord signatures.
DockerReference string `json:"DockerReference,omitempty"`
// Signer contains information about the signer certificate used to sign the image.
Signer *SignerIdentity `json:"Signer,omitempty"`
// SignatureType is the type of signature format. E.g. "bundle-v0.3" or "hashedrecord".
SignatureType SignatureType `json:"SignatureType,omitempty"`
// Error contains error information if signature verification failed.
// Other fields will be empty in this case.
Error string `json:"Error,omitempty"`
// Warnings contains any warnings that occurred during signature verification.
// For example, if there was no internet connectivity and cached trust roots were used.
// Warning does not indicate a failed verification but may point to configuration issues.
Warnings []string `json:"Warnings,omitzero"`
}
+12
View File
@@ -0,0 +1,12 @@
package image
import (
"time"
)
// SignatureTimestamp contains information about a verified signed timestamp for an image signature.
type SignatureTimestamp struct {
Type SignatureTimestampType `json:"Type"`
URI string `json:"URI"`
Timestamp time.Time `json:"Timestamp"`
}
+57
View File
@@ -0,0 +1,57 @@
package image
// SignerIdentity contains information about the signer certificate used to sign the image.
// This is [certificate.Summary] with deprecated fields removed and keys in Moby uppercase style.
//
// [certificate.Summary]: https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg/fulcio/certificate#Summary
type SignerIdentity struct {
CertificateIssuer string `json:"CertificateIssuer"`
SubjectAlternativeName string `json:"SubjectAlternativeName"`
// The OIDC issuer. Should match `iss` claim of ID token or, in the case of
// a federated login like Dex it should match the issuer URL of the
// upstream issuer. The issuer is not set the extensions are invalid and
// will fail to render.
Issuer string `json:"Issuer,omitempty"` // OID 1.3.6.1.4.1.57264.1.8 and 1.3.6.1.4.1.57264.1.1 (Deprecated)
// Reference to specific build instructions that are responsible for signing.
BuildSignerURI string `json:"BuildSignerURI,omitempty"` // 1.3.6.1.4.1.57264.1.9
// Immutable reference to the specific version of the build instructions that is responsible for signing.
BuildSignerDigest string `json:"BuildSignerDigest,omitempty"` // 1.3.6.1.4.1.57264.1.10
// Specifies whether the build took place in platform-hosted cloud infrastructure or customer/self-hosted infrastructure.
RunnerEnvironment string `json:"RunnerEnvironment,omitempty"` // 1.3.6.1.4.1.57264.1.11
// Source repository URL that the build was based on.
SourceRepositoryURI string `json:"SourceRepositoryURI,omitempty"` // 1.3.6.1.4.1.57264.1.12
// Immutable reference to a specific version of the source code that the build was based upon.
SourceRepositoryDigest string `json:"SourceRepositoryDigest,omitempty"` // 1.3.6.1.4.1.57264.1.13
// Source Repository Ref that the build run was based upon.
SourceRepositoryRef string `json:"SourceRepositoryRef,omitempty"` // 1.3.6.1.4.1.57264.1.14
// Immutable identifier for the source repository the workflow was based upon.
SourceRepositoryIdentifier string `json:"SourceRepositoryIdentifier,omitempty"` // 1.3.6.1.4.1.57264.1.15
// Source repository owner URL of the owner of the source repository that the build was based on.
SourceRepositoryOwnerURI string `json:"SourceRepositoryOwnerURI,omitempty"` // 1.3.6.1.4.1.57264.1.16
// Immutable identifier for the owner of the source repository that the workflow was based upon.
SourceRepositoryOwnerIdentifier string `json:"SourceRepositoryOwnerIdentifier,omitempty"` // 1.3.6.1.4.1.57264.1.17
// Build Config URL to the top-level/initiating build instructions.
BuildConfigURI string `json:"BuildConfigURI,omitempty"` // 1.3.6.1.4.1.57264.1.18
// Immutable reference to the specific version of the top-level/initiating build instructions.
BuildConfigDigest string `json:"BuildConfigDigest,omitempty"` // 1.3.6.1.4.1.57264.1.19
// Event or action that initiated the build.
BuildTrigger string `json:"BuildTrigger,omitempty"` // 1.3.6.1.4.1.57264.1.20
// Run Invocation URL to uniquely identify the build execution.
RunInvocationURI string `json:"RunInvocationURI,omitempty"` // 1.3.6.1.4.1.57264.1.21
// Source repository visibility at the time of signing the certificate.
SourceRepositoryVisibilityAtSigning string `json:"SourceRepositoryVisibilityAtSigning,omitempty"` // 1.3.6.1.4.1.57264.1.22
}
+2 -2
View File
@@ -2,9 +2,9 @@ package jsonstream
import "encoding/json"
// JSONMessage defines a message struct. It describes
// Message defines a message struct. It describes
// the created time, where it from, status, ID of the
// message. It's used for docker events.
// message.
type Message struct {
Stream string `json:"stream,omitempty"`
Status string `json:"status,omitempty"`
+3 -3
View File
@@ -58,9 +58,9 @@ func (es *EndpointSettings) Copy() *EndpointSettings {
// EndpointIPAMConfig represents IPAM configurations for the endpoint
type EndpointIPAMConfig struct {
IPv4Address netip.Addr `json:",omitempty"`
IPv6Address netip.Addr `json:",omitempty"`
LinkLocalIPs []netip.Addr `json:",omitempty"`
IPv4Address netip.Addr `json:"IPv4Address,omitzero"`
IPv6Address netip.Addr `json:"IPv6Address,omitzero"`
LinkLocalIPs []netip.Addr `json:"LinkLocalIPs,omitempty"`
}
// Copy makes a copy of the endpoint ipam config
+5 -3
View File
@@ -11,9 +11,11 @@ import (
// in the absence of go.dev/issue/29678.
type HardwareAddr net.HardwareAddr
var _ encoding.TextMarshaler = (HardwareAddr)(nil)
var _ encoding.TextUnmarshaler = (*HardwareAddr)(nil)
var _ fmt.Stringer = (HardwareAddr)(nil)
var (
_ encoding.TextMarshaler = (HardwareAddr)(nil)
_ encoding.TextUnmarshaler = (*HardwareAddr)(nil)
_ fmt.Stringer = (HardwareAddr)(nil)
)
func (m *HardwareAddr) UnmarshalText(text []byte) error {
if len(text) == 0 {
+3 -3
View File
@@ -13,9 +13,9 @@ type IPAM struct {
// IPAMConfig represents IPAM configurations
type IPAMConfig struct {
Subnet netip.Prefix `json:",omitempty"`
IPRange netip.Prefix `json:",omitempty"`
Gateway netip.Addr `json:",omitempty"`
Subnet netip.Prefix `json:"Subnet,omitzero"`
IPRange netip.Prefix `json:"IPRange,omitzero"`
Gateway netip.Addr `json:"Gateway,omitzero"`
AuxAddress map[string]netip.Addr `json:"AuxiliaryAddresses,omitempty"`
}
+4 -4
View File
@@ -61,7 +61,7 @@ type EndpointVirtualIP struct {
// Addr is the virtual ip address.
// This field accepts CIDR notation, for example `10.0.0.1/24`, to maintain backwards
// compatibility, but only the IP address is used.
Addr netip.Prefix `json:",omitempty"`
Addr netip.Prefix `json:"Addr,omitzero"`
}
// Network represents a network.
@@ -111,7 +111,7 @@ type IPAMOptions struct {
// IPAMConfig represents ipam configuration.
type IPAMConfig struct {
Subnet netip.Prefix `json:",omitempty"`
Range netip.Prefix `json:",omitempty"`
Gateway netip.Addr `json:",omitempty"`
Subnet netip.Prefix `json:"Subnet,omitzero"`
Range netip.Prefix `json:"Range,omitzero"`
Gateway netip.Addr `json:"Gateway,omitzero"`
}
+4 -4
View File
@@ -109,14 +109,14 @@ type Limit struct {
Pids int64 `json:",omitempty"`
}
// GenericResource represents a "user defined" resource which can
// GenericResource represents a "user-defined" resource which can
// be either an integer (e.g: SSD=3) or a string (e.g: SSD=sda1)
type GenericResource struct {
NamedResourceSpec *NamedGenericResource `json:",omitempty"`
DiscreteResourceSpec *DiscreteGenericResource `json:",omitempty"`
}
// NamedGenericResource represents a "user defined" resource which is defined
// NamedGenericResource represents a "user-defined" resource which is defined
// as a string.
// "Kind" is used to describe the Kind of a resource (e.g: "GPU", "FPGA", "SSD", ...)
// Value is used to identify the resource (GPU="UUID-1", FPGA="/dev/sdb5", ...)
@@ -125,7 +125,7 @@ type NamedGenericResource struct {
Value string `json:",omitempty"`
}
// DiscreteGenericResource represents a "user defined" resource which is defined
// DiscreteGenericResource represents a "user-defined" resource which is defined
// as an integer
// "Kind" is used to describe the Kind of a resource (e.g: "GPU", "FPGA", "SSD", ...)
// Value is used to count the resource (SSD=5, HDD=3, ...)
@@ -148,7 +148,7 @@ type ResourceRequirements struct {
// Tune container memory swappiness (0 to 100) - if not specified, defaults
// to the container OS's default - generally 60, or the value predefined in
// the image; set to -1 to unset a previously set value
MemorySwappiness *int64 `json:MemorySwappiness,omitzero"`
MemorySwappiness *int64 `json:"MemorySwappiness,omitzero"`
}
// Placement represents orchestration parameters.
+6
View File
@@ -74,6 +74,7 @@ type Info struct {
FirewallBackend *FirewallInfo `json:"FirewallBackend,omitempty"`
CDISpecDirs []string
DiscoveredDevices []DeviceInfo `json:",omitempty"`
NRI *NRIInfo `json:",omitempty"`
Containerd *ContainerdInfo `json:",omitempty"`
@@ -163,3 +164,8 @@ type DeviceInfo struct {
// Example: CDI FQDN like "vendor.com/gpu=0", or other driver-specific device ID
ID string `json:"ID"`
}
// NRIInfo describes the NRI configuration.
type NRIInfo struct {
Info [][2]string `json:"Info,omitempty"`
}
+4 -1
View File
@@ -10,9 +10,12 @@ const (
// MediaTypeJSON is the MIME-Type for JSON objects.
MediaTypeJSON = "application/json"
// MediaTypeNDJSON is the MIME-Type for Newline Delimited JSON objects streams.
// MediaTypeNDJSON is the MIME-Type for Newline Delimited JSON objects streams (https://github.com/ndjson/ndjson-spec).
MediaTypeNDJSON = "application/x-ndjson"
// MediaTypeJSONLines is the MIME-Type for JSONLines objects streams (https://jsonlines.org/).
MediaTypeJSONLines = "application/jsonl"
// MediaTypeJSONSequence is the MIME-Type for JSON Text Sequences (RFC7464).
MediaTypeJSONSequence = "application/json-seq"
)
+14 -3
View File
@@ -1,7 +1,7 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@@ -176,13 +176,24 @@
END OF TERMS AND CONDITIONS
Copyright 2013-2018 Docker, Inc.
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+9 -1
View File
@@ -59,6 +59,7 @@ import (
"net/http"
"net/url"
"path"
"slices"
"strings"
"sync"
"sync/atomic"
@@ -106,7 +107,7 @@ const DummyHost = "api.moby.localhost"
// overriding the version and disable API-version negotiation.
//
// This version may be lower than the version of the api library module used.
const MaxAPIVersion = "1.52"
const MaxAPIVersion = "1.53"
// MinAPIVersion is the minimum API version supported by the client. API versions
// below this version are not considered when performing API-version negotiation.
@@ -241,6 +242,13 @@ func New(ops ...Opt) (*Client, error) {
c.client.Transport = otelhttp.NewTransport(c.client.Transport, c.traceOpts...)
if len(cfg.responseHooks) > 0 {
c.client.Transport = &responseHookTransport{
base: c.client.Transport,
hooks: slices.Clone(cfg.responseHooks),
}
}
return c, nil
}
+35 -3
View File
@@ -2,6 +2,7 @@ package client
import (
"context"
"errors"
"fmt"
"net"
"net/http"
@@ -55,10 +56,19 @@ type clientConfig struct {
// takes precedence. Either field disables API-version negotiation.
envAPIVersion string
// responseHooks is a list of custom response hooks to call on responses.
responseHooks []ResponseHook
// traceOpts is a list of options to configure the tracing span.
traceOpts []otelhttp.Option
}
// ResponseHook is called for each HTTP response returned by the daemon.
// Hooks are invoked in the order they were added.
//
// Hooks must not read or close resp.Body.
type ResponseHook func(*http.Response)
// Opt is a configuration option to initialize a [Client].
type Opt func(*clientConfig) error
@@ -133,8 +143,6 @@ func (tf testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
return tf(req)
}
func (testRoundTripper) skipConfigureTransport() bool { return true }
// WithHostFromEnv overrides the client host with the host specified in the
// DOCKER_HOST ([EnvOverrideHost]) environment variable. If DOCKER_HOST is not set,
// or set to an empty value, the host is not modified.
@@ -151,7 +159,16 @@ func WithHostFromEnv() Opt {
func WithHTTPClient(client *http.Client) Opt {
return func(c *clientConfig) error {
if client != nil {
c.client = client
// Make a clone of client so modifications do not affect
// the caller's client. Clone here instead of in New()
// as other options (WithHost) also mutate c.client.
// Cloned clients share the same CookieJar as the
// original.
hc := *client
if ht, ok := hc.Transport.(*http.Transport); ok {
hc.Transport = ht.Clone()
}
c.client = &hc
}
return nil
}
@@ -341,3 +358,18 @@ func WithTraceOptions(opts ...otelhttp.Option) Opt {
return nil
}
}
// WithResponseHook adds a ResponseHook to the client. ResponseHooks are called
// for each HTTP response returned by the daemon. Hooks are invoked in the order
// they were added.
//
// Hooks must not read or close resp.Body.
func WithResponseHook(h ResponseHook) Opt {
return func(c *clientConfig) error {
if h == nil {
return errors.New("invalid response hook: hook is nil")
}
c.responseHooks = append(c.responseHooks, h)
return nil
}
}
+23
View File
@@ -0,0 +1,23 @@
package client
import (
"net/http"
)
type responseHookTransport struct {
base http.RoundTripper
hooks []ResponseHook
}
func (t *responseHookTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := t.base.RoundTrip(req)
if err != nil {
return resp, err
}
for _, h := range t.hooks {
h(resp)
}
return resp, nil
}
+1 -1
View File
@@ -23,7 +23,7 @@ type ContainerAttachResult struct {
// ContainerAttach attaches a connection to a container in the server.
// It returns a [HijackedResponse] with the hijacked connection
// and a reader to get output. It's up to the called to close
// and a reader to get output. It's up to the caller to close
// the hijacked connection by calling [HijackedResponse.Close].
//
// The stream format on the response uses one of two formats:
+5 -7
View File
@@ -5,7 +5,7 @@ import (
"encoding/json"
"net/http"
"github.com/containerd/errdefs"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/types/container"
)
@@ -82,8 +82,7 @@ type ExecStartOptions struct {
}
// ExecStartResult holds the result of starting a container exec.
type ExecStartResult struct {
}
type ExecStartResult struct{}
// ExecStart starts an exec process already created in the docker host.
func (cli *Client) ExecStart(ctx context.Context, execID string, options ExecStartOptions) (ExecStartResult, error) {
@@ -118,7 +117,7 @@ type ExecAttachResult struct {
// ExecAttach attaches a connection to an exec process in the server.
//
// It returns a [HijackedResponse] with the hijacked connection
// and a reader to get output. It's up to the called to close
// and a reader to get output. It's up to the caller to close
// the hijacked connection by calling [HijackedResponse.Close].
//
// The stream format on the response uses one of two formats:
@@ -152,7 +151,7 @@ func (cli *Client) ExecAttach(ctx context.Context, execID string, options ExecAt
func getConsoleSize(hasTTY bool, consoleSize ConsoleSize) (*[2]uint, error) {
if consoleSize.Height != 0 || consoleSize.Width != 0 {
if !hasTTY {
return nil, errdefs.ErrInvalidArgument.WithMessage("console size is only supported when TTY is enabled")
return nil, cerrdefs.ErrInvalidArgument.WithMessage("console size is only supported when TTY is enabled")
}
return &[2]uint{consoleSize.Height, consoleSize.Width}, nil
}
@@ -160,8 +159,7 @@ func getConsoleSize(hasTTY bool, consoleSize ConsoleSize) (*[2]uint, error) {
}
// ExecInspectOptions holds options for inspecting a container exec.
type ExecInspectOptions struct {
}
type ExecInspectOptions struct{}
// ExecInspectResult holds the result of inspecting a container exec.
//
+15 -11
View File
@@ -13,11 +13,23 @@ import (
type ContainerListOptions struct {
Size bool
All bool
Latest bool
Since string
Before string
Limit int
Filters Filters
// Latest is non-functional and should not be used. Use Limit: 1 instead.
//
// Deprecated: the Latest option is non-functional and should not be used. Use Limit: 1 instead.
Latest bool
// Since is no longer supported. Use the "since" filter instead.
//
// Deprecated: the Since option is no longer supported since docker 1.12 (API 1.24). Use the "since" filter instead.
Since string
// Before is no longer supported. Use the "since" filter instead.
//
// Deprecated: the Before option is no longer supported since docker 1.12 (API 1.24). Use the "before" filter instead.
Before string
}
type ContainerListResult struct {
@@ -36,14 +48,6 @@ func (cli *Client) ContainerList(ctx context.Context, options ContainerListOptio
query.Set("limit", strconv.Itoa(options.Limit))
}
if options.Since != "" {
query.Set("since", options.Since)
}
if options.Before != "" {
query.Set("before", options.Before)
}
if options.Size {
query.Set("size", "1")
}
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"net/url"
"strings"
"github.com/containerd/errdefs"
cerrdefs "github.com/containerd/errdefs"
)
// ContainerRenameOptions represents the options for renaming a container.
@@ -28,7 +28,7 @@ func (cli *Client) ContainerRename(ctx context.Context, containerID string, opti
if options.NewName == "" || strings.TrimPrefix(options.NewName, "/") == "" {
// daemons before v29.0 did not handle the canonical name ("/") well
// let's be nice and validate it here before sending
return ContainerRenameResult{}, errdefs.ErrInvalidArgument.WithMessage("new name cannot be blank")
return ContainerRenameResult{}, cerrdefs.ErrInvalidArgument.WithMessage("new name cannot be blank")
}
query := url.Values{}
+1 -3
View File
@@ -42,8 +42,7 @@ func (cli *Client) ContainerResize(ctx context.Context, containerID string, opti
type ExecResizeOptions ContainerResizeOptions
// ExecResizeResult holds the result of resizing a container exec TTY.
type ExecResizeResult struct {
}
type ExecResizeResult struct{}
// ExecResize changes the size of the tty for an exec process running inside a container.
func (cli *Client) ExecResize(ctx context.Context, execID string, options ExecResizeOptions) (ExecResizeResult, error) {
@@ -62,5 +61,4 @@ func (cli *Client) ExecResize(ctx context.Context, execID string, options ExecRe
return ExecResizeResult{}, err
}
return ExecResizeResult{}, nil
}
+1 -1
View File
@@ -17,7 +17,7 @@ func NewJSONStreamDecoder(r io.Reader, contentType string) DecoderFn {
switch contentType {
case types.MediaTypeJSONSequence:
return json.NewDecoder(NewRSFilterReader(r)).Decode
case types.MediaTypeJSON, types.MediaTypeNDJSON:
case types.MediaTypeJSON, types.MediaTypeNDJSON, types.MediaTypeJSONLines:
fallthrough
default:
return json.NewDecoder(r).Decode
+5 -2
View File
@@ -45,12 +45,15 @@ func (r stream) Close() error {
// JSONMessages decodes the response stream as a sequence of JSONMessages.
// if stream ends or context is cancelled, the underlying [io.Reader] is closed.
func (r stream) JSONMessages(ctx context.Context) iter.Seq2[jsonstream.Message, error] {
context.AfterFunc(ctx, func() {
stop := context.AfterFunc(ctx, func() {
_ = r.Close()
})
dec := json.NewDecoder(r)
return func(yield func(jsonstream.Message, error) bool) {
defer r.Close()
defer func() {
stop() // unregister AfterFunc
r.Close()
}()
for {
var jm jsonstream.Message
err := dec.Decode(&jm)
+2
View File
@@ -276,6 +276,8 @@ func containerDiskUsageFromLegacyAPI(du *legacyDiskUsage) ContainersDiskUsage {
case container.StateRunning, container.StatePaused, container.StateRestarting:
cdu.ActiveCount++
used += c.SizeRw
case container.StateCreated, container.StateRemoving, container.StateExited, container.StateDead:
// not active
}
}
+1
View File
@@ -46,6 +46,7 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) Events
headers := http.Header{}
headers.Add("Accept", types.MediaTypeJSONSequence)
headers.Add("Accept", types.MediaTypeJSONLines)
headers.Add("Accept", types.MediaTypeNDJSON)
resp, err := cli.get(ctx, "/events", query, headers)
if err != nil {
+7 -2
View File
@@ -136,14 +136,19 @@ func newCancelReadCloser(ctx context.Context, rc io.ReadCloser) io.ReadCloser {
rc: rc,
close: sync.OnceValue(rc.Close),
}
context.AfterFunc(ctx, func() { _ = crc.Close() })
crc.stop = context.AfterFunc(ctx, func() { _ = crc.Close() })
return crc
}
type cancelReadCloser struct {
rc io.ReadCloser
close func() error
stop func() bool
}
func (c *cancelReadCloser) Read(p []byte) (int, error) { return c.rc.Read(p) }
func (c *cancelReadCloser) Close() error { return c.close() }
func (c *cancelReadCloser) Close() error {
c.stop() // unregister AfterFunc
return c.close()
}
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"net/url"
"github.com/containerd/errdefs"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/types/volume"
)
@@ -29,7 +29,7 @@ type VolumePruneResult struct {
func (cli *Client) VolumePrune(ctx context.Context, options VolumePruneOptions) (VolumePruneResult, error) {
if options.All {
if _, ok := options.Filters["all"]; ok {
return VolumePruneResult{}, errdefs.ErrInvalidArgument.WithMessage(`conflicting options: cannot specify both "all" and "all" filter`)
return VolumePruneResult{}, cerrdefs.ErrInvalidArgument.WithMessage(`conflicting options: cannot specify both "all" and "all" filter`)
}
if options.Filters == nil {
options.Filters = Filters{}
+5 -5
View File
@@ -284,7 +284,7 @@ github.com/digitorus/timestamp
# github.com/distribution/reference v0.6.0
## explicit; go 1.20
github.com/distribution/reference
# github.com/docker/cli v29.1.5+incompatible
# github.com/docker/cli v29.2.1+incompatible
## explicit
github.com/docker/cli/cli
github.com/docker/cli/cli-plugins/metadata
@@ -439,7 +439,7 @@ github.com/go-openapi/swag/yamlutils
# github.com/go-openapi/validate v0.25.1
## explicit; go 1.24.0
github.com/go-openapi/validate
# github.com/go-viper/mapstructure/v2 v2.4.0
# github.com/go-viper/mapstructure/v2 v2.5.0
## explicit; go 1.18
github.com/go-viper/mapstructure/v2
github.com/go-viper/mapstructure/v2/internal/errors
@@ -565,7 +565,7 @@ github.com/inconshreveable/mousetrap
# github.com/json-iterator/go v1.1.12
## explicit; go 1.12
github.com/json-iterator/go
# github.com/klauspost/compress v1.18.2
# github.com/klauspost/compress v1.18.3
## explicit; go 1.23
github.com/klauspost/compress
github.com/klauspost/compress/fse
@@ -748,7 +748,7 @@ github.com/moby/go-archive/tarheader
# github.com/moby/locker v1.0.1
## explicit; go 1.13
github.com/moby/locker
# github.com/moby/moby/api v1.52.0
# github.com/moby/moby/api v1.53.0
## explicit; go 1.24.0
github.com/moby/moby/api/pkg/authconfig
github.com/moby/moby/api/pkg/stdcopy
@@ -769,7 +769,7 @@ github.com/moby/moby/api/types/storage
github.com/moby/moby/api/types/swarm
github.com/moby/moby/api/types/system
github.com/moby/moby/api/types/volume
# github.com/moby/moby/client v0.2.1
# github.com/moby/moby/client v0.2.2
## explicit; go 1.24.0
github.com/moby/moby/client
github.com/moby/moby/client/internal