Merge pull request #3617 from thaJeztah/bump_modules
vendor: github.com/docker/cli v29.2.1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -19,3 +19,6 @@ indent_size = 2
|
||||
|
||||
[.golangci.yaml]
|
||||
indent_size = 2
|
||||
|
||||
[devenv.yaml]
|
||||
indent_size = 2
|
||||
|
||||
+7
-4
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,14 @@
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
languages = {
|
||||
go.enable = true;
|
||||
};
|
||||
|
||||
packages = with pkgs; [
|
||||
golangci-lint
|
||||
];
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||
inputs:
|
||||
nixpkgs:
|
||||
url: github:cachix/devenv-nixpkgs/rolling
|
||||
-294
@@ -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
@@ -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
@@ -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
@@ -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
@@ -71,11 +71,15 @@ func DecodeRequestBody(r io.ReadCloser) (*registry.AuthConfig, error) {
|
||||
|
||||
func decode(r io.Reader) (*registry.AuthConfig, error) {
|
||||
authConfig := ®istry.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 ®istry.AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
|
||||
}
|
||||
if dec.More() {
|
||||
return ®istry.AuthConfig{}, invalid(errors.New("multiple JSON documents not allowed"))
|
||||
}
|
||||
return authConfig, nil
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -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"`
|
||||
}
|
||||
Generated
Vendored
+5
-6
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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{}
|
||||
|
||||
Vendored
+5
-5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user