Swarm has size constraints on the size of configs, but the client-side would
read content into memory, regardless its size. This could lead to either the
client reading too much into memory, or it sending data that's larger than
the size limit of gRPC, which resulted in the error not being handled by
SwarmKit and a generic gRPC error returned.
Reading a config from a file used a system.OpenSequential for reading
([FILE_FLAG_SEQUENTIAL_SCAN]). While there could be a very marginal benefit
to prevent polluting the system's cache (Windows won’t aggressively keep it
in the cache, freeing up system memory for other tasks). These details were
not documented in code, and possibly may be too marginal, but adding a comment
to outline won't hurt so this patch also adds a comment.
This patch:
- Factors out the reading code to a readConfigData, analogous to the
equivalent in secret create.
- Implements reading the data with a limit-reader to prevent reading
large files into memory.
- The limit is based on SwarmKits limits ([MaxConfigSize]), but made
twice that size, just in case larger sizes are supported in future;
the main goal is to have some constraints, and to prevent hitting
the gRPC limit.
- Updates some error messages to include STDIN (when used), or the
filename (when used).
Before this patch:
ls -lh largefile
-rw------- 1 thajeztah staff 8.1M Mar 9 00:19 largefile
docker config create nosuchfile ./nosuchfile
Error reading content from "./nosuchfile": open ./nosuchfile: no such file or directory
docker config create toolarge ./largefile
Error response from daemon: rpc error: code = ResourceExhausted desc = grpc: received message larger than max (8462870 vs. 4194304)
docker config create empty ./emptyfile
Error response from daemon: rpc error: code = InvalidArgument desc = config data must be larger than 0 and less than 1024000 bytes
cat ./largefile | docker config create toolarge -
Error response from daemon: rpc error: code = ResourceExhausted desc = grpc: received message larger than max (8462870 vs. 4194304)
cat ./emptyfile | docker config create empty -
Error response from daemon: rpc error: code = InvalidArgument desc = config data must be larger than 0 and less than 1024000 bytes
With this patch:
docker config create nosuchfile ./nosuchfile
error reading from ./nosuchfile: open ./nosuchfile: no such file or directory
docker config create empty ./emptyfile
error reading from ./emptyfile: data is empty
docker config create toolarge ./largefile
Error response from daemon: rpc error: code = InvalidArgument desc = config data must be larger than 0 and less than 1024000 bytes
cat ./largefile | docker config create toolarge -
Error response from daemon: rpc error: code = InvalidArgument desc = secret data must be larger than 0 and less than 1024000 bytes
cat ./emptyfile | docker config create empty -
error reading from STDIN: data is empty
[FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
[MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
127 lines
3.9 KiB
Go
127 lines
3.9 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/docker/cli/cli"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/cli/cli/command/completion"
|
|
"github.com/docker/cli/opts"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/moby/sys/sequential"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// CreateOptions specifies some options that are used when creating a config.
|
|
type CreateOptions struct {
|
|
Name string
|
|
TemplateDriver string
|
|
File string
|
|
Labels opts.ListOpts
|
|
}
|
|
|
|
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
|
createOpts := CreateOptions{
|
|
Labels: opts.NewListOpts(opts.ValidateLabel),
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "create [OPTIONS] CONFIG file|-",
|
|
Short: "Create a config from a file or STDIN",
|
|
Args: cli.ExactArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
createOpts.Name = args[0]
|
|
createOpts.File = args[1]
|
|
return RunConfigCreate(cmd.Context(), dockerCli, createOpts)
|
|
},
|
|
ValidArgsFunction: completion.NoComplete,
|
|
}
|
|
flags := cmd.Flags()
|
|
flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
|
|
flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver")
|
|
flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
|
|
|
return cmd
|
|
}
|
|
|
|
// RunConfigCreate creates a config with the given options.
|
|
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
|
|
apiClient := dockerCLI.Client()
|
|
|
|
configData, err := readConfigData(dockerCLI.In(), options.File)
|
|
if err != nil {
|
|
return errors.Errorf("Error reading content from %q: %v", options.File, err)
|
|
}
|
|
|
|
spec := swarm.ConfigSpec{
|
|
Annotations: swarm.Annotations{
|
|
Name: options.Name,
|
|
Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
|
|
},
|
|
Data: configData,
|
|
}
|
|
if options.TemplateDriver != "" {
|
|
spec.Templating = &swarm.Driver{
|
|
Name: options.TemplateDriver,
|
|
}
|
|
}
|
|
r, err := apiClient.ConfigCreate(ctx, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID)
|
|
return nil
|
|
}
|
|
|
|
// maxConfigSize is the maximum byte length of the [swarm.ConfigSpec.Data] field,
|
|
// as defined by [MaxConfigSize] in SwarmKit.
|
|
//
|
|
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
|
|
const maxConfigSize = 1000 * 1024 // 1000KB
|
|
|
|
// readConfigData reads the config from either stdin or the given fileName.
|
|
//
|
|
// It reads up to twice the maximum size of the config ([maxConfigSize]),
|
|
// just in case swarm's limit changes; this is only a safeguard to prevent
|
|
// reading arbitrary files into memory.
|
|
func readConfigData(in io.Reader, fileName string) ([]byte, error) {
|
|
switch fileName {
|
|
case "-":
|
|
data, err := io.ReadAll(io.LimitReader(in, 2*maxConfigSize))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading from STDIN: %w", err)
|
|
}
|
|
if len(data) == 0 {
|
|
return nil, errors.New("error reading from STDIN: data is empty")
|
|
}
|
|
return data, nil
|
|
case "":
|
|
return nil, errors.New("config file is required")
|
|
default:
|
|
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
|
|
// prevents Windows from aggressively caching it. We expect this
|
|
// file to be only read once. Given that this is expected to be
|
|
// a small file, this may not be a significant optimization, so
|
|
// we could choose to omit this, and use a regular [os.Open].
|
|
//
|
|
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
|
|
f, err := sequential.Open(fileName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
|
}
|
|
defer f.Close()
|
|
data, err := io.ReadAll(io.LimitReader(f, 2*maxConfigSize))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
|
}
|
|
if len(data) == 0 {
|
|
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
|
|
}
|
|
return data, nil
|
|
}
|
|
}
|