vendor: github.com/docker/cli v23.0.0

full diff: https://github.com/docker/cli/compare/v23.0.0-rc.1...v23.0.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
pull/1610/head
Sebastiaan van Stijn 2 years ago
parent 260117289b
commit 081447c9b1
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C

@ -7,7 +7,7 @@ require (
github.com/compose-spec/compose-go v1.9.0 github.com/compose-spec/compose-go v1.9.0
github.com/containerd/console v1.0.3 github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.6.16 github.com/containerd/containerd v1.6.16
github.com/docker/cli v23.0.0-rc.1+incompatible github.com/docker/cli v23.0.0+incompatible
github.com/docker/cli-docs-tool v0.5.1 github.com/docker/cli-docs-tool v0.5.1
github.com/docker/distribution v2.8.1+incompatible github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v23.0.0+incompatible github.com/docker/docker v23.0.0+incompatible

@ -163,8 +163,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/distribution/v3 v3.0.0-20221103125252-ebfa2a0ac0a9 h1:doprs/RuXCuN864IfxC3h2qocrt158wGv3A5mcqSZQw= github.com/distribution/distribution/v3 v3.0.0-20221103125252-ebfa2a0ac0a9 h1:doprs/RuXCuN864IfxC3h2qocrt158wGv3A5mcqSZQw=
github.com/distribution/distribution/v3 v3.0.0-20221103125252-ebfa2a0ac0a9/go.mod h1:6rIc5NMSjXjjnwzWWy3HAm9gDBu+X7aCzL8VrHIKgxM= github.com/distribution/distribution/v3 v3.0.0-20221103125252-ebfa2a0ac0a9/go.mod h1:6rIc5NMSjXjjnwzWWy3HAm9gDBu+X7aCzL8VrHIKgxM=
github.com/docker/cli v23.0.0-rc.1+incompatible h1:Vl3pcUK4/LFAD56Ys3BrqgAtuwpWd/IO3amuSL0ZbP0= github.com/docker/cli v23.0.0+incompatible h1:bcM4syaQ+EM/iczJTimMOGzvnzJBFPFEf4acS7sZ+RM=
github.com/docker/cli v23.0.0-rc.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v23.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.5.1 h1:jIk/cCZurZERhALPVKhqlNxTQGxn2kcI+56gE57PQXg= github.com/docker/cli-docs-tool v0.5.1 h1:jIk/cCZurZERhALPVKhqlNxTQGxn2kcI+56gE57PQXg=
github.com/docker/cli-docs-tool v0.5.1/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o= github.com/docker/cli-docs-tool v0.5.1/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=

@ -133,7 +133,9 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
} }
opts, flags := cli.SetupPluginRootCommand(cmd) opts, flags := cli.SetupPluginRootCommand(cmd)
cmd.SetIn(dockerCli.In())
cmd.SetOut(dockerCli.Out()) cmd.SetOut(dockerCli.Out())
cmd.SetErr(dockerCli.Err())
cmd.AddCommand( cmd.AddCommand(
plugin, plugin,

@ -61,7 +61,10 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *p
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true rootCmd.PersistentFlags().Lookup("help").Hidden = true
rootCmd.Annotations = map[string]string{"additionalHelp": "For more help on how to use Docker, head to https://docs.docker.com/go/guides/"} rootCmd.Annotations = map[string]string{
"additionalHelp": "For more help on how to use Docker, head to https://docs.docker.com/go/guides/",
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
}
// Configure registry.CertsDir() when running in rootless-mode // Configure registry.CertsDir() when running in rootless-mode
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" { if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
@ -231,9 +234,13 @@ func isExperimental(cmd *cobra.Command) bool {
} }
func additionalHelp(cmd *cobra.Command) string { func additionalHelp(cmd *cobra.Command) string {
if additionalHelp, ok := cmd.Annotations["additionalHelp"]; ok { if msg, ok := cmd.Annotations["additionalHelp"]; ok {
out := cmd.OutOrStderr()
if _, isTerminal := term.GetFdInfo(out); !isTerminal {
return msg
}
style := aec.EmptyBuilder.Bold().ANSI style := aec.EmptyBuilder.Bold().ANSI
return style.Apply(additionalHelp) return style.Apply(msg)
} }
return "" return ""
} }
@ -504,6 +511,7 @@ Run '{{.CommandPath}} COMMAND --help' for more information on a command.
{{- if hasAdditionalHelp .}} {{- if hasAdditionalHelp .}}
{{ additionalHelp . }} {{ additionalHelp . }}
{{- end}} {{- end}}
` `

@ -96,26 +96,26 @@ func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args {
return pruneFilters return pruneFilters
} }
for _, f := range dockerCli.ConfigFile().PruneFilters { for _, f := range dockerCli.ConfigFile().PruneFilters {
parts := strings.SplitN(f, "=", 2) k, v, ok := strings.Cut(f, "=")
if len(parts) != 2 { if !ok {
continue continue
} }
if parts[0] == "label" { if k == "label" {
// CLI label filter supersede config.json. // CLI label filter supersede config.json.
// If CLI label filter conflict with config.json, // If CLI label filter conflict with config.json,
// skip adding label! filter in config.json. // skip adding label! filter in config.json.
if pruneFilters.Contains("label!") && pruneFilters.ExactMatch("label!", parts[1]) { if pruneFilters.Contains("label!") && pruneFilters.ExactMatch("label!", v) {
continue continue
} }
} else if parts[0] == "label!" { } else if k == "label!" {
// CLI label! filter supersede config.json. // CLI label! filter supersede config.json.
// If CLI label! filter conflict with config.json, // If CLI label! filter conflict with config.json,
// skip adding label filter in config.json. // skip adding label filter in config.json.
if pruneFilters.Contains("label") && pruneFilters.ExactMatch("label", parts[1]) { if pruneFilters.Contains("label") && pruneFilters.ExactMatch("label", v) {
continue continue
} }
} }
pruneFilters.Add(parts[0], parts[1]) pruneFilters.Add(k, v)
} }
return pruneFilters return pruneFilters

@ -241,12 +241,11 @@ func decodeAuth(authStr string) (string, string, error) {
if n > decLen { if n > decLen {
return "", "", errors.Errorf("Something went wrong decoding auth config") return "", "", errors.Errorf("Something went wrong decoding auth config")
} }
arr := strings.SplitN(string(decoded), ":", 2) userName, password, ok := strings.Cut(string(decoded), ":")
if len(arr) != 2 { if !ok || userName == "" {
return "", "", errors.Errorf("Invalid auth configuration file") return "", "", errors.Errorf("Invalid auth configuration file")
} }
password := strings.Trim(arr[1], "\x00") return userName, strings.Trim(password, "\x00"), nil
return arr[0], password, nil
} }
// GetCredentialsStore returns a new credentials store from the settings in the // GetCredentialsStore returns a new credentials store from the settings in the
@ -301,7 +300,8 @@ func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig,
for registryHostname := range configFile.CredentialHelpers { for registryHostname := range configFile.CredentialHelpers {
newAuth, err := configFile.GetAuthConfig(registryHostname) newAuth, err := configFile.GetAuthConfig(registryHostname)
if err != nil { if err != nil {
return nil, err logrus.WithError(err).Warnf("Failed to get credentials for registry: %s", registryHostname)
continue
} }
auths[registryHostname] = newAuth auths[registryHostname] = newAuth
} }

@ -75,7 +75,6 @@ func ConvertToHostname(url string) string {
stripped = strings.TrimPrefix(url, "https://") stripped = strings.TrimPrefix(url, "https://")
} }
nameParts := strings.SplitN(stripped, "/", 2) hostName, _, _ := strings.Cut(stripped, "/")
return hostName
return nameParts[0]
} }

@ -67,7 +67,7 @@ func (o *ClientOptions) InstallFlags(flags *pflag.FlagSet) {
} }
flags.BoolVarP(&o.Debug, "debug", "D", false, "Enable debug mode") flags.BoolVarP(&o.Debug, "debug", "D", false, "Enable debug mode")
flags.StringVarP(&o.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`) flags.StringVarP(&o.LogLevel, "log-level", "l", "info", `Set the logging level ("debug", "info", "warn", "error", "fatal")`)
flags.BoolVar(&o.TLS, "tls", dockerTLS, "Use TLS; implied by --tlsverify") flags.BoolVar(&o.TLS, "tls", dockerTLS, "Use TLS; implied by --tlsverify")
flags.BoolVar(&o.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote") flags.BoolVar(&o.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote")

@ -5,6 +5,7 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
@ -16,10 +17,12 @@ import (
type ImageManifest struct { type ImageManifest struct {
Ref *SerializableNamed Ref *SerializableNamed
Descriptor ocispec.Descriptor Descriptor ocispec.Descriptor
Raw []byte `json:",omitempty"`
// SchemaV2Manifest is used for inspection // SchemaV2Manifest is used for inspection
// TODO: Deprecate this and store manifest blobs
SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"` SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"`
// OCIManifest is used for inspection
OCIManifest *ocischema.DeserializedManifest `json:",omitempty"`
} }
// OCIPlatform creates an OCI platform from a manifest list platform spec // OCIPlatform creates an OCI platform from a manifest list platform spec
@ -53,8 +56,15 @@ func PlatformSpecFromOCI(p *ocispec.Platform) *manifestlist.PlatformSpec {
// Blobs returns the digests for all the blobs referenced by this manifest // Blobs returns the digests for all the blobs referenced by this manifest
func (i ImageManifest) Blobs() []digest.Digest { func (i ImageManifest) Blobs() []digest.Digest {
digests := []digest.Digest{} digests := []digest.Digest{}
for _, descriptor := range i.SchemaV2Manifest.References() { switch {
digests = append(digests, descriptor.Digest) case i.SchemaV2Manifest != nil:
for _, descriptor := range i.SchemaV2Manifest.References() {
digests = append(digests, descriptor.Digest)
}
case i.OCIManifest != nil:
for _, descriptor := range i.OCIManifest.References() {
digests = append(digests, descriptor.Digest)
}
} }
return digests return digests
} }
@ -65,6 +75,8 @@ func (i ImageManifest) Payload() (string, []byte, error) {
switch { switch {
case i.SchemaV2Manifest != nil: case i.SchemaV2Manifest != nil:
return i.SchemaV2Manifest.Payload() return i.SchemaV2Manifest.Payload()
case i.OCIManifest != nil:
return i.OCIManifest.Payload()
default: default:
return "", nil, errors.Errorf("%s has no payload", i.Ref) return "", nil, errors.Errorf("%s has no payload", i.Ref)
} }
@ -76,6 +88,8 @@ func (i ImageManifest) References() []distribution.Descriptor {
switch { switch {
case i.SchemaV2Manifest != nil: case i.SchemaV2Manifest != nil:
return i.SchemaV2Manifest.References() return i.SchemaV2Manifest.References()
case i.OCIManifest != nil:
return i.OCIManifest.References()
default: default:
return nil return nil
} }
@ -84,13 +98,35 @@ func (i ImageManifest) References() []distribution.Descriptor {
// NewImageManifest returns a new ImageManifest object. The values for Platform // NewImageManifest returns a new ImageManifest object. The values for Platform
// are initialized from those in the image // are initialized from those in the image
func NewImageManifest(ref reference.Named, desc ocispec.Descriptor, manifest *schema2.DeserializedManifest) ImageManifest { func NewImageManifest(ref reference.Named, desc ocispec.Descriptor, manifest *schema2.DeserializedManifest) ImageManifest {
raw, err := manifest.MarshalJSON()
if err != nil {
raw = nil
}
return ImageManifest{ return ImageManifest{
Ref: &SerializableNamed{Named: ref}, Ref: &SerializableNamed{Named: ref},
Descriptor: desc, Descriptor: desc,
Raw: raw,
SchemaV2Manifest: manifest, SchemaV2Manifest: manifest,
} }
} }
// NewOCIImageManifest returns a new ImageManifest object. The values for
// Platform are initialized from those in the image
func NewOCIImageManifest(ref reference.Named, desc ocispec.Descriptor, manifest *ocischema.DeserializedManifest) ImageManifest {
raw, err := manifest.MarshalJSON()
if err != nil {
raw = nil
}
return ImageManifest{
Ref: &SerializableNamed{Named: ref},
Descriptor: desc,
Raw: raw,
OCIManifest: manifest,
}
}
// SerializableNamed is a reference.Named that can be serialized and deserialized // SerializableNamed is a reference.Named that can be serialized and deserialized
// from JSON // from JSON
type SerializableNamed struct { type SerializableNamed struct {

@ -7,6 +7,7 @@ import (
"github.com/docker/cli/cli/manifest/types" "github.com/docker/cli/cli/manifest/types"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
@ -35,6 +36,12 @@ func fetchManifest(ctx context.Context, repo distribution.Repository, ref refere
return types.ImageManifest{}, err return types.ImageManifest{}, err
} }
return imageManifest, nil return imageManifest, nil
case *ocischema.DeserializedManifest:
imageManifest, err := pullManifestOCISchema(ctx, ref, repo, *v)
if err != nil {
return types.ImageManifest{}, err
}
return imageManifest, nil
case *manifestlist.DeserializedManifestList: case *manifestlist.DeserializedManifestList:
return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref) return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref)
} }
@ -94,6 +101,28 @@ func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distrib
return types.NewImageManifest(ref, manifestDesc, &mfst), nil return types.NewImageManifest(ref, manifestDesc, &mfst), nil
} }
func pullManifestOCISchema(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst ocischema.DeserializedManifest) (types.ImageManifest, error) {
manifestDesc, err := validateManifestDigest(ref, mfst)
if err != nil {
return types.ImageManifest{}, err
}
configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
if err != nil {
return types.ImageManifest{}, err
}
if manifestDesc.Platform == nil {
manifestDesc.Platform = &ocispec.Platform{}
}
// Fill in os and architecture fields from config JSON
if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
return types.ImageManifest{}, err
}
return types.NewOCIImageManifest(ref, manifestDesc, &mfst), nil
}
func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) { func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) {
blobs := repo.Blobs(ctx) blobs := repo.Blobs(ctx)
configJSON, err := blobs.Get(ctx, dgst) configJSON, err := blobs.Get(ctx, dgst)
@ -153,16 +182,21 @@ func pullManifestList(ctx context.Context, ref reference.Named, repo distributio
if err != nil { if err != nil {
return nil, err return nil, err
} }
v, ok := manifest.(*schema2.DeserializedManifest)
if !ok {
return nil, errors.Errorf("unsupported manifest format: %v", v)
}
manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest) manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
imageManifest, err := pullManifestSchemaV2(ctx, manifestRef, repo, *v)
var imageManifest types.ImageManifest
switch v := manifest.(type) {
case *schema2.DeserializedManifest:
imageManifest, err = pullManifestSchemaV2(ctx, manifestRef, repo, *v)
case *ocischema.DeserializedManifest:
imageManifest, err = pullManifestOCISchema(ctx, manifestRef, repo, *v)
default:
err = errors.Errorf("unsupported manifest type: %T", manifest)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -40,25 +40,23 @@ func (o *ConfigOpt) Set(value string) error {
} }
for _, field := range fields { for _, field := range fields {
parts := strings.SplitN(field, "=", 2) key, val, ok := strings.Cut(field, "=")
key := strings.ToLower(parts[0]) if !ok || key == "" {
if len(parts) != 2 {
return fmt.Errorf("invalid field '%s' must be a key=value pair", field) return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
} }
value := parts[1] // TODO(thaJeztah): these options should not be case-insensitive.
switch key { switch strings.ToLower(key) {
case "source", "src": case "source", "src":
options.ConfigName = value options.ConfigName = val
case "target": case "target":
options.File.Name = value options.File.Name = val
case "uid": case "uid":
options.File.UID = value options.File.UID = val
case "gid": case "gid":
options.File.GID = value options.File.GID = val
case "mode": case "mode":
m, err := strconv.ParseUint(value, 0, 32) m, err := strconv.ParseUint(val, 0, 32)
if err != nil { if err != nil {
return fmt.Errorf("invalid mode specified: %v", err) return fmt.Errorf("invalid mode specified: %v", err)
} }

@ -16,15 +16,16 @@ import (
// //
// The only validation here is to check if name is empty, per #25099 // The only validation here is to check if name is empty, per #25099
func ValidateEnv(val string) (string, error) { func ValidateEnv(val string) (string, error) {
arr := strings.SplitN(val, "=", 2) k, _, hasValue := strings.Cut(val, "=")
if arr[0] == "" { if k == "" {
return "", errors.New("invalid environment variable: " + val) return "", errors.New("invalid environment variable: " + val)
} }
if len(arr) > 1 { if hasValue {
// val contains a "=" (but value may be an empty string)
return val, nil return val, nil
} }
if envVal, ok := os.LookupEnv(arr[0]); ok { if envVal, ok := os.LookupEnv(k); ok {
return arr[0] + "=" + envVal, nil return k + "=" + envVal, nil
} }
return val, nil return val, nil
} }

@ -46,10 +46,10 @@ func parseKeyValueFile(filename string, emptyFn func(string) (string, bool)) ([]
currentLine++ currentLine++
// line is not empty, and not starting with '#' // line is not empty, and not starting with '#'
if len(line) > 0 && !strings.HasPrefix(line, "#") { if len(line) > 0 && !strings.HasPrefix(line, "#") {
data := strings.SplitN(line, "=", 2) variable, value, hasValue := strings.Cut(line, "=")
// trim the front of a variable, but nothing else // trim the front of a variable, but nothing else
variable := strings.TrimLeft(data[0], whiteSpaces) variable = strings.TrimLeft(variable, whiteSpaces)
if strings.ContainsAny(variable, whiteSpaces) { if strings.ContainsAny(variable, whiteSpaces) {
return []string{}, ErrBadKey{fmt.Sprintf("variable '%s' contains whitespaces", variable)} return []string{}, ErrBadKey{fmt.Sprintf("variable '%s' contains whitespaces", variable)}
} }
@ -57,18 +57,17 @@ func parseKeyValueFile(filename string, emptyFn func(string) (string, bool)) ([]
return []string{}, ErrBadKey{fmt.Sprintf("no variable name on line '%s'", line)} return []string{}, ErrBadKey{fmt.Sprintf("no variable name on line '%s'", line)}
} }
if len(data) > 1 { if hasValue {
// pass the value through, no trimming // pass the value through, no trimming
lines = append(lines, fmt.Sprintf("%s=%s", variable, data[1])) lines = append(lines, variable+"="+value)
} else { } else {
var value string
var present bool var present bool
if emptyFn != nil { if emptyFn != nil {
value, present = emptyFn(line) value, present = emptyFn(line)
} }
if present { if present {
// if only a pass-through variable is given, clean it up. // if only a pass-through variable is given, clean it up.
lines = append(lines, fmt.Sprintf("%s=%s", strings.TrimSpace(line), value)) lines = append(lines, strings.TrimSpace(variable)+"="+value)
} }
} }
} }

@ -38,14 +38,13 @@ func (o *GpuOpts) Set(value string) error {
seen := map[string]struct{}{} seen := map[string]struct{}{}
// Set writable as the default // Set writable as the default
for _, field := range fields { for _, field := range fields {
parts := strings.SplitN(field, "=", 2) key, val, withValue := strings.Cut(field, "=")
key := parts[0]
if _, ok := seen[key]; ok { if _, ok := seen[key]; ok {
return fmt.Errorf("gpu request key '%s' can be specified only once", key) return fmt.Errorf("gpu request key '%s' can be specified only once", key)
} }
seen[key] = struct{}{} seen[key] = struct{}{}
if len(parts) == 1 { if !withValue {
seen["count"] = struct{}{} seen["count"] = struct{}{}
req.Count, err = parseCount(key) req.Count, err = parseCount(key)
if err != nil { if err != nil {
@ -54,21 +53,20 @@ func (o *GpuOpts) Set(value string) error {
continue continue
} }
value := parts[1]
switch key { switch key {
case "driver": case "driver":
req.Driver = value req.Driver = val
case "count": case "count":
req.Count, err = parseCount(value) req.Count, err = parseCount(val)
if err != nil { if err != nil {
return err return err
} }
case "device": case "device":
req.DeviceIDs = strings.Split(value, ",") req.DeviceIDs = strings.Split(val, ",")
case "capabilities": case "capabilities":
req.Capabilities = [][]string{append(strings.Split(value, ","), "gpu")} req.Capabilities = [][]string{append(strings.Split(val, ","), "gpu")}
case "options": case "options":
r := csv.NewReader(strings.NewReader(value)) r := csv.NewReader(strings.NewReader(val))
optFields, err := r.Read() optFields, err := r.Read()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to read gpu options") return errors.Wrap(err, "failed to read gpu options")

@ -33,6 +33,8 @@ const (
) )
// ValidateHost validates that the specified string is a valid host and returns it. // ValidateHost validates that the specified string is a valid host and returns it.
//
// TODO(thaJeztah): ValidateHost appears to be unused; deprecate it.
func ValidateHost(val string) (string, error) { func ValidateHost(val string) (string, error) {
host := strings.TrimSpace(val) host := strings.TrimSpace(val)
// The empty string means default and is not handled by parseDockerDaemonHost // The empty string means default and is not handled by parseDockerDaemonHost
@ -69,18 +71,19 @@ func ParseHost(defaultToTLS bool, val string) (string, error) {
// parseDockerDaemonHost parses the specified address and returns an address that will be used as the host. // parseDockerDaemonHost parses the specified address and returns an address that will be used as the host.
// Depending of the address specified, this may return one of the global Default* strings defined in hosts.go. // Depending of the address specified, this may return one of the global Default* strings defined in hosts.go.
func parseDockerDaemonHost(addr string) (string, error) { func parseDockerDaemonHost(addr string) (string, error) {
addrParts := strings.SplitN(addr, "://", 2) proto, host, hasProto := strings.Cut(addr, "://")
if len(addrParts) == 1 && addrParts[0] != "" { if !hasProto && proto != "" {
addrParts = []string{"tcp", addrParts[0]} host = proto
proto = "tcp"
} }
switch addrParts[0] { switch proto {
case "tcp": case "tcp":
return ParseTCPAddr(addrParts[1], defaultTCPHost) return ParseTCPAddr(host, defaultTCPHost)
case "unix": case "unix":
return parseSimpleProtoAddr("unix", addrParts[1], defaultUnixSocket) return parseSimpleProtoAddr(proto, host, defaultUnixSocket)
case "npipe": case "npipe":
return parseSimpleProtoAddr("npipe", addrParts[1], defaultNamedPipe) return parseSimpleProtoAddr(proto, host, defaultNamedPipe)
case "fd": case "fd":
return addr, nil return addr, nil
case "ssh": case "ssh":
@ -160,16 +163,18 @@ func ParseTCPAddr(tryAddr string, defaultAddr string) (string, error) {
// ValidateExtraHost validates that the specified string is a valid extrahost and returns it. // ValidateExtraHost validates that the specified string is a valid extrahost and returns it.
// ExtraHost is in the form of name:ip where the ip has to be a valid ip (IPv4 or IPv6). // ExtraHost is in the form of name:ip where the ip has to be a valid ip (IPv4 or IPv6).
//
// TODO(thaJeztah): remove client-side validation, and delegate to the API server.
func ValidateExtraHost(val string) (string, error) { func ValidateExtraHost(val string) (string, error) {
// allow for IPv6 addresses in extra hosts by only splitting on first ":" // allow for IPv6 addresses in extra hosts by only splitting on first ":"
arr := strings.SplitN(val, ":", 2) k, v, ok := strings.Cut(val, ":")
if len(arr) != 2 || len(arr[0]) == 0 { if !ok || k == "" {
return "", fmt.Errorf("bad format for add-host: %q", val) return "", fmt.Errorf("bad format for add-host: %q", val)
} }
// Skip IPaddr validation for "host-gateway" string // Skip IPaddr validation for "host-gateway" string
if arr[1] != hostGatewayName { if v != hostGatewayName {
if _, err := ValidateIPAddress(arr[1]); err != nil { if _, err := ValidateIPAddress(v); err != nil {
return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1]) return "", fmt.Errorf("invalid IP address in add-host: %q", v)
} }
} }
return val, nil return val, nil

@ -56,21 +56,21 @@ func (m *MountOpt) Set(value string) error {
} }
setValueOnMap := func(target map[string]string, value string) { setValueOnMap := func(target map[string]string, value string) {
parts := strings.SplitN(value, "=", 2) k, v, _ := strings.Cut(value, "=")
if len(parts) == 1 { if k != "" {
target[value] = "" target[k] = v
} else {
target[parts[0]] = parts[1]
} }
} }
mount.Type = mounttypes.TypeVolume // default to volume mounts mount.Type = mounttypes.TypeVolume // default to volume mounts
// Set writable as the default // Set writable as the default
for _, field := range fields { for _, field := range fields {
parts := strings.SplitN(field, "=", 2) key, val, ok := strings.Cut(field, "=")
key := strings.ToLower(parts[0])
if len(parts) == 1 { // TODO(thaJeztah): these options should not be case-insensitive.
key = strings.ToLower(key)
if !ok {
switch key { switch key {
case "readonly", "ro": case "readonly", "ro":
mount.ReadOnly = true mount.ReadOnly = true
@ -81,64 +81,61 @@ func (m *MountOpt) Set(value string) error {
case "bind-nonrecursive": case "bind-nonrecursive":
bindOptions().NonRecursive = true bindOptions().NonRecursive = true
continue continue
default:
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
} }
} }
if len(parts) != 2 {
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
}
value := parts[1]
switch key { switch key {
case "type": case "type":
mount.Type = mounttypes.Type(strings.ToLower(value)) mount.Type = mounttypes.Type(strings.ToLower(val))
case "source", "src": case "source", "src":
mount.Source = value mount.Source = val
if strings.HasPrefix(value, "."+string(filepath.Separator)) || value == "." { if strings.HasPrefix(val, "."+string(filepath.Separator)) || val == "." {
if abs, err := filepath.Abs(value); err == nil { if abs, err := filepath.Abs(val); err == nil {
mount.Source = abs mount.Source = abs
} }
} }
case "target", "dst", "destination": case "target", "dst", "destination":
mount.Target = value mount.Target = val
case "readonly", "ro": case "readonly", "ro":
mount.ReadOnly, err = strconv.ParseBool(value) mount.ReadOnly, err = strconv.ParseBool(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid value for %s: %s", key, value) return fmt.Errorf("invalid value for %s: %s", key, val)
} }
case "consistency": case "consistency":
mount.Consistency = mounttypes.Consistency(strings.ToLower(value)) mount.Consistency = mounttypes.Consistency(strings.ToLower(val))
case "bind-propagation": case "bind-propagation":
bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value)) bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(val))
case "bind-nonrecursive": case "bind-nonrecursive":
bindOptions().NonRecursive, err = strconv.ParseBool(value) bindOptions().NonRecursive, err = strconv.ParseBool(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid value for %s: %s", key, value) return fmt.Errorf("invalid value for %s: %s", key, val)
} }
case "volume-nocopy": case "volume-nocopy":
volumeOptions().NoCopy, err = strconv.ParseBool(value) volumeOptions().NoCopy, err = strconv.ParseBool(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid value for volume-nocopy: %s", value) return fmt.Errorf("invalid value for volume-nocopy: %s", val)
} }
case "volume-label": case "volume-label":
setValueOnMap(volumeOptions().Labels, value) setValueOnMap(volumeOptions().Labels, val)
case "volume-driver": case "volume-driver":
volumeOptions().DriverConfig.Name = value volumeOptions().DriverConfig.Name = val
case "volume-opt": case "volume-opt":
if volumeOptions().DriverConfig.Options == nil { if volumeOptions().DriverConfig.Options == nil {
volumeOptions().DriverConfig.Options = make(map[string]string) volumeOptions().DriverConfig.Options = make(map[string]string)
} }
setValueOnMap(volumeOptions().DriverConfig.Options, value) setValueOnMap(volumeOptions().DriverConfig.Options, val)
case "tmpfs-size": case "tmpfs-size":
sizeBytes, err := units.RAMInBytes(value) sizeBytes, err := units.RAMInBytes(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid value for %s: %s", key, value) return fmt.Errorf("invalid value for %s: %s", key, val)
} }
tmpfsOptions().SizeBytes = sizeBytes tmpfsOptions().SizeBytes = sizeBytes
case "tmpfs-mode": case "tmpfs-mode":
ui64, err := strconv.ParseUint(value, 8, 32) ui64, err := strconv.ParseUint(val, 8, 32)
if err != nil { if err != nil {
return fmt.Errorf("invalid value for %s: %s", key, value) return fmt.Errorf("invalid value for %s: %s", key, val)
} }
tmpfsOptions().Mode = os.FileMode(ui64) tmpfsOptions().Mode = os.FileMode(ui64)
default: default:

@ -48,34 +48,33 @@ func (n *NetworkOpt) Set(value string) error {
netOpt.Aliases = []string{} netOpt.Aliases = []string{}
for _, field := range fields { for _, field := range fields {
parts := strings.SplitN(field, "=", 2) // TODO(thaJeztah): these options should not be case-insensitive.
key, val, ok := strings.Cut(strings.ToLower(field), "=")
if len(parts) < 2 { if !ok || key == "" {
return fmt.Errorf("invalid field %s", field) return fmt.Errorf("invalid field %s", field)
} }
key := strings.TrimSpace(strings.ToLower(parts[0])) key = strings.TrimSpace(key)
value := strings.TrimSpace(strings.ToLower(parts[1])) val = strings.TrimSpace(val)
switch key { switch key {
case networkOptName: case networkOptName:
netOpt.Target = value netOpt.Target = val
case networkOptAlias: case networkOptAlias:
netOpt.Aliases = append(netOpt.Aliases, value) netOpt.Aliases = append(netOpt.Aliases, val)
case networkOptIPv4Address: case networkOptIPv4Address:
netOpt.IPv4Address = value netOpt.IPv4Address = val
case networkOptIPv6Address: case networkOptIPv6Address:
netOpt.IPv6Address = value netOpt.IPv6Address = val
case driverOpt: case driverOpt:
key, value, err = parseDriverOpt(value) key, val, err = parseDriverOpt(val)
if err == nil { if err != nil {
if netOpt.DriverOpts == nil {
netOpt.DriverOpts = make(map[string]string)
}
netOpt.DriverOpts[key] = value
} else {
return err return err
} }
if netOpt.DriverOpts == nil {
netOpt.DriverOpts = make(map[string]string)
}
netOpt.DriverOpts[key] = val
default: default:
return fmt.Errorf("invalid field key %s", key) return fmt.Errorf("invalid field key %s", key)
} }
@ -116,11 +115,13 @@ func (n *NetworkOpt) NetworkMode() string {
} }
func parseDriverOpt(driverOpt string) (string, string, error) { func parseDriverOpt(driverOpt string) (string, string, error) {
parts := strings.SplitN(driverOpt, "=", 2) // TODO(thaJeztah): these options should not be case-insensitive.
if len(parts) != 2 { // TODO(thaJeztah): should value be converted to lowercase as well, or only the key?
key, value, ok := strings.Cut(strings.ToLower(driverOpt), "=")
if !ok || key == "" {
return "", "", fmt.Errorf("invalid key value pair format in driver options") return "", "", fmt.Errorf("invalid key value pair format in driver options")
} }
key := strings.TrimSpace(strings.ToLower(parts[0])) key = strings.TrimSpace(key)
value := strings.TrimSpace(strings.ToLower(parts[1])) value = strings.TrimSpace(value)
return key, value, nil return key, value, nil
} }

@ -55,7 +55,7 @@ func (opts *ListOpts) Set(value string) error {
} }
value = v value = v
} }
(*opts.values) = append((*opts.values), value) *opts.values = append(*opts.values, value)
return nil return nil
} }
@ -63,7 +63,7 @@ func (opts *ListOpts) Set(value string) error {
func (opts *ListOpts) Delete(key string) { func (opts *ListOpts) Delete(key string) {
for i, k := range *opts.values { for i, k := range *opts.values {
if k == key { if k == key {
(*opts.values) = append((*opts.values)[:i], (*opts.values)[i+1:]...) *opts.values = append((*opts.values)[:i], (*opts.values)[i+1:]...)
return return
} }
} }
@ -81,7 +81,7 @@ func (opts *ListOpts) GetMap() map[string]struct{} {
// GetAll returns the values of slice. // GetAll returns the values of slice.
func (opts *ListOpts) GetAll() []string { func (opts *ListOpts) GetAll() []string {
return (*opts.values) return *opts.values
} }
// GetAllOrEmpty returns the values of the slice // GetAllOrEmpty returns the values of the slice
@ -106,7 +106,7 @@ func (opts *ListOpts) Get(key string) bool {
// Len returns the amount of element in the slice. // Len returns the amount of element in the slice.
func (opts *ListOpts) Len() int { func (opts *ListOpts) Len() int {
return len((*opts.values)) return len(*opts.values)
} }
// Type returns a string name for this Option type // Type returns a string name for this Option type
@ -165,12 +165,8 @@ func (opts *MapOpts) Set(value string) error {
} }
value = v value = v
} }
vals := strings.SplitN(value, "=", 2) k, v, _ := strings.Cut(value, "=")
if len(vals) == 1 { opts.values[k] = v
(opts.values)[vals[0]] = ""
} else {
(opts.values)[vals[0]] = vals[1]
}
return nil return nil
} }
@ -277,16 +273,16 @@ func validateDomain(val string) (string, error) {
// //
// TODO discuss if quotes (and other special characters) should be valid or invalid for keys // TODO discuss if quotes (and other special characters) should be valid or invalid for keys
// TODO discuss if leading/trailing whitespace in keys should be preserved (and valid) // TODO discuss if leading/trailing whitespace in keys should be preserved (and valid)
func ValidateLabel(val string) (string, error) { func ValidateLabel(value string) (string, error) {
arr := strings.SplitN(val, "=", 2) key, _, _ := strings.Cut(value, "=")
key := strings.TrimLeft(arr[0], whiteSpaces) key = strings.TrimLeft(key, whiteSpaces)
if key == "" { if key == "" {
return "", fmt.Errorf("invalid label '%s': empty name", val) return "", fmt.Errorf("invalid label '%s': empty name", value)
} }
if strings.ContainsAny(key, whiteSpaces) { if strings.ContainsAny(key, whiteSpaces) {
return "", fmt.Errorf("label '%s' contains whitespaces", key) return "", fmt.Errorf("label '%s' contains whitespaces", key)
} }
return val, nil return value, nil
} }
// ValidateSysctl validates a sysctl and returns it. // ValidateSysctl validates a sysctl and returns it.
@ -305,20 +301,19 @@ func ValidateSysctl(val string) (string, error) {
"net.", "net.",
"fs.mqueue.", "fs.mqueue.",
} }
arr := strings.Split(val, "=") k, _, ok := strings.Cut(val, "=")
if len(arr) < 2 { if !ok || k == "" {
return "", fmt.Errorf("sysctl '%s' is not whitelisted", val) return "", fmt.Errorf("sysctl '%s' is not allowed", val)
} }
if validSysctlMap[arr[0]] { if validSysctlMap[k] {
return val, nil return val, nil
} }
for _, vp := range validSysctlPrefixes { for _, vp := range validSysctlPrefixes {
if strings.HasPrefix(arr[0], vp) { if strings.HasPrefix(k, vp) {
return val, nil return val, nil
} }
} }
return "", fmt.Errorf("sysctl '%s' is not whitelisted", val) return "", fmt.Errorf("sysctl '%s' is not allowed", val)
} }
// FilterOpt is a flag type for validating filters // FilterOpt is a flag type for validating filters
@ -347,11 +342,12 @@ func (o *FilterOpt) Set(value string) error {
if !strings.Contains(value, "=") { if !strings.Contains(value, "=") {
return errors.New("bad format of filter (expected name=value)") return errors.New("bad format of filter (expected name=value)")
} }
f := strings.SplitN(value, "=", 2) name, val, _ := strings.Cut(value, "=")
name := strings.ToLower(strings.TrimSpace(f[0]))
value = strings.TrimSpace(f[1])
o.filter.Add(name, value) // TODO(thaJeztah): these options should not be case-insensitive.
name = strings.ToLower(strings.TrimSpace(name))
val = strings.TrimSpace(val)
o.filter.Add(name, val)
return nil return nil
} }
@ -411,10 +407,14 @@ func ParseLink(val string) (string, string, error) {
if val == "" { if val == "" {
return "", "", fmt.Errorf("empty string specified for links") return "", "", fmt.Errorf("empty string specified for links")
} }
arr := strings.Split(val, ":") // We expect two parts, but restrict to three to allow detecting invalid formats.
arr := strings.SplitN(val, ":", 3)
// TODO(thaJeztah): clean up this logic!!
if len(arr) > 2 { if len(arr) > 2 {
return "", "", fmt.Errorf("bad format for links: %s", val) return "", "", fmt.Errorf("bad format for links: %s", val)
} }
// TODO(thaJeztah): this should trim the "/" prefix as well??
if len(arr) == 1 { if len(arr) == 1 {
return val, val, nil return val, val, nil
} }
@ -422,6 +422,7 @@ func ParseLink(val string) (string, string, error) {
// from an already created container and the format is not `foo:bar` // from an already created container and the format is not `foo:bar`
// but `/foo:/c1/bar` // but `/foo:/c1/bar`
if strings.HasPrefix(arr[0], "/") { if strings.HasPrefix(arr[0], "/") {
// TODO(thaJeztah): clean up this logic!!
_, alias := path.Split(arr[1]) _, alias := path.Split(arr[1])
return arr[0][1:], alias, nil return arr[0][1:], alias, nil
} }

@ -41,12 +41,8 @@ func readKVStrings(files []string, override []string, emptyFn func(string) (stri
func ConvertKVStringsToMap(values []string) map[string]string { func ConvertKVStringsToMap(values []string) map[string]string {
result := make(map[string]string, len(values)) result := make(map[string]string, len(values))
for _, value := range values { for _, value := range values {
kv := strings.SplitN(value, "=", 2) k, v, _ := strings.Cut(value, "=")
if len(kv) == 1 { result[k] = v
result[kv[0]] = ""
} else {
result[kv[0]] = kv[1]
}
} }
return result return result
@ -62,11 +58,11 @@ func ConvertKVStringsToMap(values []string) map[string]string {
func ConvertKVStringsToMapWithNil(values []string) map[string]*string { func ConvertKVStringsToMapWithNil(values []string) map[string]*string {
result := make(map[string]*string, len(values)) result := make(map[string]*string, len(values))
for _, value := range values { for _, value := range values {
kv := strings.SplitN(value, "=", 2) k, v, ok := strings.Cut(value, "=")
if len(kv) == 1 { if !ok {
result[kv[0]] = nil result[k] = nil
} else { } else {
result[kv[0]] = &kv[1] result[k] = &v
} }
} }
@ -81,21 +77,15 @@ func ParseRestartPolicy(policy string) (container.RestartPolicy, error) {
return p, nil return p, nil
} }
parts := strings.Split(policy, ":") k, v, _ := strings.Cut(policy, ":")
if v != "" {
if len(parts) > 2 { count, err := strconv.Atoi(v)
return p, fmt.Errorf("invalid restart policy format")
}
if len(parts) == 2 {
count, err := strconv.Atoi(parts[1])
if err != nil { if err != nil {
return p, fmt.Errorf("maximum retry count must be an integer") return p, fmt.Errorf("invalid restart policy format: maximum retry count must be an integer")
} }
p.MaximumRetryCount = count p.MaximumRetryCount = count
} }
p.Name = parts[0] p.Name = k
return p, nil return p, nil
} }

@ -42,36 +42,33 @@ func (p *PortOpt) Set(value string) error {
pConfig := swarm.PortConfig{} pConfig := swarm.PortConfig{}
for _, field := range fields { for _, field := range fields {
parts := strings.SplitN(field, "=", 2) // TODO(thaJeztah): these options should not be case-insensitive.
if len(parts) != 2 { key, val, ok := strings.Cut(strings.ToLower(field), "=")
if !ok || key == "" {
return fmt.Errorf("invalid field %s", field) return fmt.Errorf("invalid field %s", field)
} }
key := strings.ToLower(parts[0])
value := strings.ToLower(parts[1])
switch key { switch key {
case portOptProtocol: case portOptProtocol:
if value != string(swarm.PortConfigProtocolTCP) && value != string(swarm.PortConfigProtocolUDP) && value != string(swarm.PortConfigProtocolSCTP) { if val != string(swarm.PortConfigProtocolTCP) && val != string(swarm.PortConfigProtocolUDP) && val != string(swarm.PortConfigProtocolSCTP) {
return fmt.Errorf("invalid protocol value %s", value) return fmt.Errorf("invalid protocol value %s", val)
} }
pConfig.Protocol = swarm.PortConfigProtocol(value) pConfig.Protocol = swarm.PortConfigProtocol(val)
case portOptMode: case portOptMode:
if value != string(swarm.PortConfigPublishModeIngress) && value != string(swarm.PortConfigPublishModeHost) { if val != string(swarm.PortConfigPublishModeIngress) && val != string(swarm.PortConfigPublishModeHost) {
return fmt.Errorf("invalid publish mode value %s", value) return fmt.Errorf("invalid publish mode value %s", val)
} }
pConfig.PublishMode = swarm.PortConfigPublishMode(value) pConfig.PublishMode = swarm.PortConfigPublishMode(val)
case portOptTargetPort: case portOptTargetPort:
tPort, err := strconv.ParseUint(value, 10, 16) tPort, err := strconv.ParseUint(val, 10, 16)
if err != nil { if err != nil {
return err return err
} }
pConfig.TargetPort = uint32(tPort) pConfig.TargetPort = uint32(tPort)
case portOptPublishedPort: case portOptPublishedPort:
pPort, err := strconv.ParseUint(value, 10, 16) pPort, err := strconv.ParseUint(val, 10, 16)
if err != nil { if err != nil {
return err return err
} }

@ -40,25 +40,22 @@ func (o *SecretOpt) Set(value string) error {
} }
for _, field := range fields { for _, field := range fields {
parts := strings.SplitN(field, "=", 2) key, val, ok := strings.Cut(field, "=")
key := strings.ToLower(parts[0]) if !ok || key == "" {
if len(parts) != 2 {
return fmt.Errorf("invalid field '%s' must be a key=value pair", field) return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
} }
// TODO(thaJeztah): these options should not be case-insensitive.
value := parts[1] switch strings.ToLower(key) {
switch key {
case "source", "src": case "source", "src":
options.SecretName = value options.SecretName = val
case "target": case "target":
options.File.Name = value options.File.Name = val
case "uid": case "uid":
options.File.UID = value options.File.UID = val
case "gid": case "gid":
options.File.GID = value options.File.GID = val
case "mode": case "mode":
m, err := strconv.ParseUint(value, 0, 32) m, err := strconv.ParseUint(val, 0, 32)
if err != nil { if err != nil {
return fmt.Errorf("invalid mode specified: %v", err) return fmt.Errorf("invalid mode specified: %v", err)
} }

@ -14,14 +14,15 @@ type ValidatorThrottleFctType func(val string) (*blkiodev.ThrottleDevice, error)
// ValidateThrottleBpsDevice validates that the specified string has a valid device-rate format. // ValidateThrottleBpsDevice validates that the specified string has a valid device-rate format.
func ValidateThrottleBpsDevice(val string) (*blkiodev.ThrottleDevice, error) { func ValidateThrottleBpsDevice(val string) (*blkiodev.ThrottleDevice, error) {
split := strings.SplitN(val, ":", 2) k, v, ok := strings.Cut(val, ":")
if len(split) != 2 { if !ok || k == "" {
return nil, fmt.Errorf("bad format: %s", val) return nil, fmt.Errorf("bad format: %s", val)
} }
if !strings.HasPrefix(split[0], "/dev/") { // TODO(thaJeztah): should we really validate this on the client?
if !strings.HasPrefix(k, "/dev/") {
return nil, fmt.Errorf("bad format for device path: %s", val) return nil, fmt.Errorf("bad format for device path: %s", val)
} }
rate, err := units.RAMInBytes(split[1]) rate, err := units.RAMInBytes(v)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val)
} }
@ -30,26 +31,27 @@ func ValidateThrottleBpsDevice(val string) (*blkiodev.ThrottleDevice, error) {
} }
return &blkiodev.ThrottleDevice{ return &blkiodev.ThrottleDevice{
Path: split[0], Path: v,
Rate: uint64(rate), Rate: uint64(rate),
}, nil }, nil
} }
// ValidateThrottleIOpsDevice validates that the specified string has a valid device-rate format. // ValidateThrottleIOpsDevice validates that the specified string has a valid device-rate format.
func ValidateThrottleIOpsDevice(val string) (*blkiodev.ThrottleDevice, error) { func ValidateThrottleIOpsDevice(val string) (*blkiodev.ThrottleDevice, error) {
split := strings.SplitN(val, ":", 2) k, v, ok := strings.Cut(val, ":")
if len(split) != 2 { if !ok || k == "" {
return nil, fmt.Errorf("bad format: %s", val) return nil, fmt.Errorf("bad format: %s", val)
} }
if !strings.HasPrefix(split[0], "/dev/") { // TODO(thaJeztah): should we really validate this on the client?
if !strings.HasPrefix(k, "/dev/") {
return nil, fmt.Errorf("bad format for device path: %s", val) return nil, fmt.Errorf("bad format for device path: %s", val)
} }
rate, err := strconv.ParseUint(split[1], 10, 64) rate, err := strconv.ParseUint(v, 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val) return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val)
} }
return &blkiodev.ThrottleDevice{Path: split[0], Rate: rate}, nil return &blkiodev.ThrottleDevice{Path: k, Rate: rate}, nil
} }
// ThrottledeviceOpt defines a map of ThrottleDevices // ThrottledeviceOpt defines a map of ThrottleDevices
@ -77,7 +79,7 @@ func (opt *ThrottledeviceOpt) Set(val string) error {
} }
value = v value = v
} }
(opt.values) = append((opt.values), value) opt.values = append(opt.values, value)
return nil return nil
} }
@ -93,10 +95,7 @@ func (opt *ThrottledeviceOpt) String() string {
// GetList returns a slice of pointers to ThrottleDevices. // GetList returns a slice of pointers to ThrottleDevices.
func (opt *ThrottledeviceOpt) GetList() []*blkiodev.ThrottleDevice { func (opt *ThrottledeviceOpt) GetList() []*blkiodev.ThrottleDevice {
var throttledevice []*blkiodev.ThrottleDevice return append([]*blkiodev.ThrottleDevice{}, opt.values...)
throttledevice = append(throttledevice, opt.values...)
return throttledevice
} }
// Type returns the option type // Type returns the option type

@ -13,14 +13,15 @@ type ValidatorWeightFctType func(val string) (*blkiodev.WeightDevice, error)
// ValidateWeightDevice validates that the specified string has a valid device-weight format. // ValidateWeightDevice validates that the specified string has a valid device-weight format.
func ValidateWeightDevice(val string) (*blkiodev.WeightDevice, error) { func ValidateWeightDevice(val string) (*blkiodev.WeightDevice, error) {
split := strings.SplitN(val, ":", 2) k, v, ok := strings.Cut(val, ":")
if len(split) != 2 { if !ok || k == "" {
return nil, fmt.Errorf("bad format: %s", val) return nil, fmt.Errorf("bad format: %s", val)
} }
if !strings.HasPrefix(split[0], "/dev/") { // TODO(thaJeztah): should we really validate this on the client?
if !strings.HasPrefix(k, "/dev/") {
return nil, fmt.Errorf("bad format for device path: %s", val) return nil, fmt.Errorf("bad format for device path: %s", val)
} }
weight, err := strconv.ParseUint(split[1], 10, 16) weight, err := strconv.ParseUint(v, 10, 16)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid weight for device: %s", val) return nil, fmt.Errorf("invalid weight for device: %s", val)
} }
@ -29,7 +30,7 @@ func ValidateWeightDevice(val string) (*blkiodev.WeightDevice, error) {
} }
return &blkiodev.WeightDevice{ return &blkiodev.WeightDevice{
Path: split[0], Path: k,
Weight: uint16(weight), Weight: uint16(weight),
}, nil }, nil
} }
@ -42,9 +43,8 @@ type WeightdeviceOpt struct {
// NewWeightdeviceOpt creates a new WeightdeviceOpt // NewWeightdeviceOpt creates a new WeightdeviceOpt
func NewWeightdeviceOpt(validator ValidatorWeightFctType) WeightdeviceOpt { func NewWeightdeviceOpt(validator ValidatorWeightFctType) WeightdeviceOpt {
values := []*blkiodev.WeightDevice{}
return WeightdeviceOpt{ return WeightdeviceOpt{
values: values, values: []*blkiodev.WeightDevice{},
validator: validator, validator: validator,
} }
} }
@ -59,7 +59,7 @@ func (opt *WeightdeviceOpt) Set(val string) error {
} }
value = v value = v
} }
(opt.values) = append((opt.values), value) opt.values = append(opt.values, value)
return nil return nil
} }

@ -0,0 +1,107 @@
package ocischema
import (
"context"
"errors"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// Builder is a type for constructing manifests.
type Builder struct {
// bs is a BlobService used to publish the configuration blob.
bs distribution.BlobService
// configJSON references
configJSON []byte
// layers is a list of layer descriptors that gets built by successive
// calls to AppendReference.
layers []distribution.Descriptor
// Annotations contains arbitrary metadata relating to the targeted content.
annotations map[string]string
// For testing purposes
mediaType string
}
// NewManifestBuilder is used to build new manifests for the current schema
// version. It takes a BlobService so it can publish the configuration blob
// as part of the Build process, and annotations.
func NewManifestBuilder(bs distribution.BlobService, configJSON []byte, annotations map[string]string) distribution.ManifestBuilder {
mb := &Builder{
bs: bs,
configJSON: make([]byte, len(configJSON)),
annotations: annotations,
mediaType: v1.MediaTypeImageManifest,
}
copy(mb.configJSON, configJSON)
return mb
}
// SetMediaType assigns the passed mediatype or error if the mediatype is not a
// valid media type for oci image manifests currently: "" or "application/vnd.oci.image.manifest.v1+json"
func (mb *Builder) SetMediaType(mediaType string) error {
if mediaType != "" && mediaType != v1.MediaTypeImageManifest {
return errors.New("invalid media type for OCI image manifest")
}
mb.mediaType = mediaType
return nil
}
// Build produces a final manifest from the given references.
func (mb *Builder) Build(ctx context.Context) (distribution.Manifest, error) {
m := Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 2,
MediaType: mb.mediaType,
},
Layers: make([]distribution.Descriptor, len(mb.layers)),
Annotations: mb.annotations,
}
copy(m.Layers, mb.layers)
configDigest := digest.FromBytes(mb.configJSON)
var err error
m.Config, err = mb.bs.Stat(ctx, configDigest)
switch err {
case nil:
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = v1.MediaTypeImageConfig
return FromStruct(m)
case distribution.ErrBlobUnknown:
// nop
default:
return nil, err
}
// Add config to the blob store
m.Config, err = mb.bs.Put(ctx, v1.MediaTypeImageConfig, mb.configJSON)
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = v1.MediaTypeImageConfig
if err != nil {
return nil, err
}
return FromStruct(m)
}
// AppendReference adds a reference to the current ManifestBuilder.
func (mb *Builder) AppendReference(d distribution.Describable) error {
mb.layers = append(mb.layers, d.Descriptor())
return nil
}
// References returns the current references added to this builder.
func (mb *Builder) References() []distribution.Descriptor {
return mb.layers
}

@ -0,0 +1,146 @@
package ocischema
import (
"encoding/json"
"errors"
"fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
// SchemaVersion provides a pre-initialized version structure for this
// packages version of the manifest.
SchemaVersion = manifest.Versioned{
SchemaVersion: 2, // historical value here.. does not pertain to OCI or docker version
MediaType: v1.MediaTypeImageManifest,
}
)
func init() {
ocischemaFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
if err := validateManifest(b); err != nil {
return nil, distribution.Descriptor{}, err
}
m := new(DeserializedManifest)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}
dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageManifest}, err
}
err := distribution.RegisterManifestSchema(v1.MediaTypeImageManifest, ocischemaFunc)
if err != nil {
panic(fmt.Sprintf("Unable to register manifest: %s", err))
}
}
// Manifest defines a ocischema manifest.
type Manifest struct {
manifest.Versioned
// Config references the image configuration as a blob.
Config distribution.Descriptor `json:"config"`
// Layers lists descriptors for the layers referenced by the
// configuration.
Layers []distribution.Descriptor `json:"layers"`
// Annotations contains arbitrary metadata for the image manifest.
Annotations map[string]string `json:"annotations,omitempty"`
}
// References returns the descriptors of this manifests references.
func (m Manifest) References() []distribution.Descriptor {
references := make([]distribution.Descriptor, 0, 1+len(m.Layers))
references = append(references, m.Config)
references = append(references, m.Layers...)
return references
}
// Target returns the target of this manifest.
func (m Manifest) Target() distribution.Descriptor {
return m.Config
}
// DeserializedManifest wraps Manifest with a copy of the original JSON.
// It satisfies the distribution.Manifest interface.
type DeserializedManifest struct {
Manifest
// canonical is the canonical byte representation of the Manifest.
canonical []byte
}
// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
// DeserializedManifest which contains the manifest and its JSON representation.
func FromStruct(m Manifest) (*DeserializedManifest, error) {
var deserialized DeserializedManifest
deserialized.Manifest = m
var err error
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
return &deserialized, err
}
// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b))
// store manifest in canonical
copy(m.canonical, b)
// Unmarshal canonical JSON into Manifest object
var manifest Manifest
if err := json.Unmarshal(m.canonical, &manifest); err != nil {
return err
}
if manifest.MediaType != "" && manifest.MediaType != v1.MediaTypeImageManifest {
return fmt.Errorf("if present, mediaType in manifest should be '%s' not '%s'",
v1.MediaTypeImageManifest, manifest.MediaType)
}
m.Manifest = manifest
return nil
}
// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
if len(m.canonical) > 0 {
return m.canonical, nil
}
return nil, errors.New("JSON representation not initialized in DeserializedManifest")
}
// Payload returns the raw content of the manifest. The contents can be used to
// calculate the content identifier.
func (m DeserializedManifest) Payload() (string, []byte, error) {
return v1.MediaTypeImageManifest, m.canonical, nil
}
// unknownDocument represents a manifest, manifest list, or index that has not
// yet been validated
type unknownDocument struct {
Manifests interface{} `json:"manifests,omitempty"`
}
// validateManifest returns an error if the byte slice is invalid JSON or if it
// contains fields that belong to a index
func validateManifest(b []byte) error {
var doc unknownDocument
if err := json.Unmarshal(b, &doc); err != nil {
return err
}
if doc.Manifests != nil {
return errors.New("ocimanifest: expected manifest but found index")
}
return nil
}

@ -195,7 +195,7 @@ github.com/davecgh/go-spew/spew
## explicit; go 1.18 ## explicit; go 1.18
github.com/distribution/distribution/v3/digestset github.com/distribution/distribution/v3/digestset
github.com/distribution/distribution/v3/reference github.com/distribution/distribution/v3/reference
# github.com/docker/cli v23.0.0-rc.1+incompatible # github.com/docker/cli v23.0.0+incompatible
## explicit ## explicit
github.com/docker/cli/cli github.com/docker/cli/cli
github.com/docker/cli/cli-plugins/manager github.com/docker/cli/cli-plugins/manager
@ -230,6 +230,7 @@ github.com/docker/distribution
github.com/docker/distribution/digestset github.com/docker/distribution/digestset
github.com/docker/distribution/manifest github.com/docker/distribution/manifest
github.com/docker/distribution/manifest/manifestlist github.com/docker/distribution/manifest/manifestlist
github.com/docker/distribution/manifest/ocischema
github.com/docker/distribution/manifest/schema2 github.com/docker/distribution/manifest/schema2
github.com/docker/distribution/metrics github.com/docker/distribution/metrics
github.com/docker/distribution/reference github.com/docker/distribution/reference

Loading…
Cancel
Save