This replaces the visitAll recursive function with a test that verifies that the option is set for all commands and subcommands, so that it doesn't have to be modified at runtime. We currently still have to loop over all functions for the setValidateArgs call, but that can be looked at separately. 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"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/docker/cli/cli"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/cli/opts"
|
|
"github.com/moby/moby/api/types/swarm"
|
|
"github.com/moby/sys/sequential"
|
|
"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 runCreate(cmd.Context(), dockerCLI, createOpts)
|
|
},
|
|
ValidArgsFunction: cobra.NoFileCompletions,
|
|
DisableFlagsInUseLine: true,
|
|
}
|
|
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
|
|
}
|
|
|
|
// runCreate creates a config with the given options.
|
|
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error {
|
|
apiClient := dockerCLI.Client()
|
|
|
|
configData, err := readConfigData(dockerCLI.In(), options.file)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading content from %q: %v", options.file, err)
|
|
}
|
|
|
|
spec := swarm.ConfigSpec{
|
|
Annotations: swarm.Annotations{
|
|
Name: options.name,
|
|
Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()),
|
|
},
|
|
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
|
|
}
|
|
}
|