Skip to content

Commit 3a35b16

Browse files
committed
secret create: refactor, use limit reader, and touch up errors
Swarm has size constraints on the size of secrets, 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 secret from a file was added in [moby@c6f0b7f], which 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: - Rewrites readSecretData to not return a nil-error if no file was set, in stead only calling it when not using a driver. - Implements reading the data with a limit-reader to prevent reading large files into memory. - The limit is based on SwarmKits limits ([MaxSecretSize]), 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 secret create nosuchfile ./nosuchfile Error reading content from "./nosuchfile": open ./nosuchfile: no such file or directory docker secret create toolarge ./largefile Error response from daemon: rpc error: code = ResourceExhausted desc = grpc: received message larger than max (8462870 vs. 4194304) docker secret create empty ./emptyfile Error response from daemon: rpc error: code = InvalidArgument desc = secret data must be larger than 0 and less than 512000 bytes cat ./largefile | docker secret create toolarge - Error response from daemon: rpc error: code = ResourceExhausted desc = grpc: received message larger than max (8462870 vs. 4194304) cat ./emptyfile | docker secret create empty - Error response from daemon: rpc error: code = InvalidArgument desc = secret data must be larger than 0 and less than 512000 bytes With this patch: docker secret create nosuchfile ./nosuchfile error reading from ./nosuchfile: open ./nosuchfile: no such file or directory docker secret create empty ./emptyfile error reading from ./emptyfile: data is empty docker secret create toolarge ./largefile Error response from daemon: rpc error: code = InvalidArgument desc = secret data must be larger than 0 and less than 512000 bytes cat ./largefile | docker secret create toolarge - Error response from daemon: rpc error: code = InvalidArgument desc = secret data must be larger than 0 and less than 512000 bytes cat ./emptyfile | docker secret create empty - error reading from STDIN: data is empty [moby@c6f0b7f]: moby/moby@c6f0b7f [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN [MaxSecretSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 2d74733 commit 3a35b16

2 files changed

Lines changed: 60 additions & 27 deletions

File tree

cli/command/secret/create.go

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,19 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command {
5252
func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createOptions) error {
5353
client := dockerCli.Client()
5454

55-
if options.driver != "" && options.file != "" {
56-
return errors.Errorf("When using secret driver secret data must be empty")
55+
var secretData []byte
56+
if options.driver != "" {
57+
if options.file != "" {
58+
return errors.Errorf("When using secret driver secret data must be empty")
59+
}
60+
} else {
61+
var err error
62+
secretData, err = readSecretData(dockerCli.In(), options.file)
63+
if err != nil {
64+
return err
65+
}
5766
}
5867

59-
secretData, err := readSecretData(dockerCli.In(), options.file)
60-
if err != nil {
61-
return errors.Errorf("Error reading content from %q: %v", options.file, err)
62-
}
6368
spec := swarm.SecretSpec{
6469
Annotations: swarm.Annotations{
6570
Name: options.name,
@@ -82,26 +87,54 @@ func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createO
8287
return err
8388
}
8489

85-
fmt.Fprintln(dockerCli.Out(), r.ID)
90+
_, _ = fmt.Fprintln(dockerCli.Out(), r.ID)
8691
return nil
8792
}
8893

89-
func readSecretData(in io.ReadCloser, file string) ([]byte, error) {
90-
// Read secret value from external driver
91-
if file == "" {
92-
return nil, nil
93-
}
94-
if file != "-" {
95-
var err error
96-
in, err = sequential.Open(file)
94+
// maxSecretSize is the maximum byte length of the [swarm.SecretSpec.Data] field,
95+
// as defined by [MaxSecretSize] in SwarmKit.
96+
//
97+
// [MaxSecretSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize
98+
const maxSecretSize = 500 * 1024 // 500KB
99+
100+
// readSecretData reads the secret from either stdin or the given fileName.
101+
//
102+
// It reads up to twice the maximum size of the secret ([maxSecretSize]),
103+
// just in case swarm's limit changes; this is only a safeguard to prevent
104+
// reading arbitrary files into memory.
105+
func readSecretData(in io.Reader, fileName string) ([]byte, error) {
106+
switch fileName {
107+
case "-":
108+
data, err := io.ReadAll(io.LimitReader(in, 2*maxSecretSize))
97109
if err != nil {
98-
return nil, err
110+
return nil, fmt.Errorf("error reading from STDIN: %w", err)
99111
}
100-
defer in.Close()
101-
}
102-
data, err := io.ReadAll(in)
103-
if err != nil {
104-
return nil, err
112+
if len(data) == 0 {
113+
return nil, errors.New("error reading from STDIN: data is empty")
114+
}
115+
return data, nil
116+
case "":
117+
return nil, errors.New("secret file is required")
118+
default:
119+
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
120+
// prevents Windows from aggressively caching it. We expect this
121+
// file to be only read once. Given that this is expected to be
122+
// a small file, this may not be a significant optimization, so
123+
// we could choose to omit this, and use a regular [os.Open].
124+
//
125+
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
126+
f, err := sequential.Open(fileName)
127+
if err != nil {
128+
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
129+
}
130+
defer f.Close()
131+
data, err := io.ReadAll(io.LimitReader(f, 2*maxSecretSize))
132+
if err != nil {
133+
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
134+
}
135+
if len(data) == 0 {
136+
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
137+
}
138+
return data, nil
105139
}
106-
return data, nil
107140
}

cli/command/secret/create_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func TestSecretCreateErrors(t *testing.T) {
5656
}
5757

5858
func TestSecretCreateWithName(t *testing.T) {
59-
name := "foo"
59+
const name = "secret-with-name"
6060
data, err := os.ReadFile(filepath.Join("testdata", secretDataFile))
6161
assert.NilError(t, err)
6262

@@ -89,7 +89,7 @@ func TestSecretCreateWithDriver(t *testing.T) {
8989
expectedDriver := &swarm.Driver{
9090
Name: "secret-driver",
9191
}
92-
name := "foo"
92+
const name = "secret-with-driver"
9393

9494
cli := test.NewFakeCli(&fakeClient{
9595
secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
@@ -118,7 +118,7 @@ func TestSecretCreateWithTemplatingDriver(t *testing.T) {
118118
expectedDriver := &swarm.Driver{
119119
Name: "template-driver",
120120
}
121-
const name = "foo"
121+
const name = "secret-with-template-driver"
122122

123123
cli := test.NewFakeCli(&fakeClient{
124124
secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
@@ -137,7 +137,7 @@ func TestSecretCreateWithTemplatingDriver(t *testing.T) {
137137
})
138138

139139
cmd := newSecretCreateCommand(cli)
140-
cmd.SetArgs([]string{name})
140+
cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)})
141141
assert.Check(t, cmd.Flags().Set("template-driver", expectedDriver.Name))
142142
assert.NilError(t, cmd.Execute())
143143
assert.Check(t, is.Equal("ID-"+name, strings.TrimSpace(cli.OutBuffer().String())))
@@ -148,7 +148,7 @@ func TestSecretCreateWithLabels(t *testing.T) {
148148
"lbl1": "Label-foo",
149149
"lbl2": "Label-bar",
150150
}
151-
const name = "foo"
151+
const name = "secret-with-labels"
152152

153153
cli := test.NewFakeCli(&fakeClient{
154154
secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) {

0 commit comments

Comments
 (0)