Files
docker-cli/cli/command/secret/create.go
Sebastiaan van Stijn 0adaf6be3b verify that DisableFlagsInUseLine is set for all commands
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>
2025-09-01 09:39:46 +02:00

142 lines
4.1 KiB
Go

package secret
import (
"context"
"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/pkg/errors"
"github.com/spf13/cobra"
)
type createOptions struct {
name string
driver string
templateDriver string
file string
labels opts.ListOpts
}
func newSecretCreateCommand(dockerCLI command.Cli) *cobra.Command {
options := createOptions{
labels: opts.NewListOpts(opts.ValidateLabel),
}
cmd := &cobra.Command{
Use: "create [OPTIONS] SECRET [file|-]",
Short: "Create a secret from a file or STDIN as content",
Args: cli.RequiresRangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
options.name = args[0]
if len(args) == 2 {
options.file = args[1]
}
return runSecretCreate(cmd.Context(), dockerCLI, options)
},
DisableFlagsInUseLine: true,
}
flags := cmd.Flags()
flags.VarP(&options.labels, "label", "l", "Secret labels")
flags.StringVarP(&options.driver, "driver", "d", "", "Secret driver")
flags.SetAnnotation("driver", "version", []string{"1.31"})
flags.StringVar(&options.templateDriver, "template-driver", "", "Template driver")
flags.SetAnnotation("template-driver", "version", []string{"1.37"})
return cmd
}
func runSecretCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error {
apiClient := dockerCLI.Client()
var secretData []byte
if options.driver != "" {
if options.file != "" {
return errors.Errorf("When using secret driver secret data must be empty")
}
} else {
var err error
secretData, err = readSecretData(dockerCLI.In(), options.file)
if err != nil {
return err
}
}
spec := swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: options.name,
Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()),
},
Data: secretData,
}
if options.driver != "" {
spec.Driver = &swarm.Driver{
Name: options.driver,
}
}
if options.templateDriver != "" {
spec.Templating = &swarm.Driver{
Name: options.templateDriver,
}
}
r, err := apiClient.SecretCreate(ctx, spec)
if err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID)
return nil
}
// maxSecretSize is the maximum byte length of the [swarm.SecretSpec.Data] field,
// as defined by [MaxSecretSize] in SwarmKit.
//
// [MaxSecretSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize
const maxSecretSize = 500 * 1024 // 500KB
// readSecretData reads the secret from either stdin or the given fileName.
//
// It reads up to twice the maximum size of the secret ([maxSecretSize]),
// just in case swarm's limit changes; this is only a safeguard to prevent
// reading arbitrary files into memory.
func readSecretData(in io.Reader, fileName string) ([]byte, error) {
switch fileName {
case "-":
data, err := io.ReadAll(io.LimitReader(in, 2*maxSecretSize))
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("secret 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*maxSecretSize))
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
}
}