Skip to content

Commit 084adb0

Browse files
committed
add warning when trying to publish env variables with OCI artifact
Signed-off-by: Guillaume Lours <[email protected]>
1 parent 8402888 commit 084adb0

File tree

8 files changed

+160
-22
lines changed

8 files changed

+160
-22
lines changed

cmd/compose/publish.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type publishOptions struct {
3030
resolveImageDigests bool
3131
ociVersion string
3232
withEnvironment bool
33+
force bool
3334
}
3435

3536
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
@@ -48,6 +49,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
4849
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
4950
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)")
5051
flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact")
52+
flags.BoolVarP(&opts.force, "force", "f", false, "Force publish without asking for confirmation")
5153

5254
return cmd
5355
}
@@ -62,5 +64,6 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service,
6264
ResolveImageDigests: opts.resolveImageDigests,
6365
OCIVersion: api.OCIVersion(opts.ociVersion),
6466
WithEnvironment: opts.withEnvironment,
67+
Force: opts.force,
6568
})
6669
}

docs/reference/compose_alpha_publish.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Publish compose application
88
| Name | Type | Default | Description |
99
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
1010
| `--dry-run` | `bool` | | Execute command in dry run mode |
11+
| `-f`, `--force` | `bool` | | Force publish without asking for confirmation |
1112
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
1213
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
1314
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |

docs/reference/docker_compose_alpha_publish.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
55
pname: docker compose alpha
66
plink: docker_compose_alpha.yaml
77
options:
8+
- option: force
9+
shorthand: f
10+
value_type: bool
11+
default_value: "false"
12+
description: Force publish without asking for confirmation
13+
deprecated: false
14+
hidden: false
15+
experimental: false
16+
experimentalcli: false
17+
kubernetes: false
18+
swarm: false
819
- option: oci-version
920
value_type: string
1021
description: |

pkg/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ const (
423423
type PublishOptions struct {
424424
ResolveImageDigests bool
425425
WithEnvironment bool
426+
Force bool
426427

427428
OCIVersion OCIVersion
428429
}

pkg/compose/publish.go

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
package compose
1818

1919
import (
20+
"bufio"
2021
"context"
2122
"fmt"
2223
"os"
24+
"strings"
25+
26+
"github.com/docker/cli/cli/command"
2327

2428
"github.com/compose-spec/compose-go/v2/types"
2529
"github.com/distribution/reference"
@@ -36,10 +40,13 @@ func (s *composeService) Publish(ctx context.Context, project *types.Project, re
3640
}
3741

3842
func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
39-
err := preChecks(project, options)
43+
accept, err := s.preChecks(project, options)
4044
if err != nil {
4145
return err
4246
}
47+
if !accept {
48+
return nil
49+
}
4350
err = s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
4451
if err != nil {
4552
return err
@@ -130,31 +137,80 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje
130137
return override.MarshalYAML()
131138
}
132139

133-
func preChecks(project *types.Project, options api.PublishOptions) error {
134-
if !options.WithEnvironment {
135-
for _, service := range project.Services {
136-
if len(service.EnvFiles) > 0 {
137-
return fmt.Errorf("service %q has env_file declared. To avoid leaking sensitive data, "+
138-
"you must either explicitly allow the sending of environment variables by using the --with-env flag,"+
139-
" or remove sensitive data from your Compose configuration", service.Name)
140-
}
141-
if len(service.Environment) > 0 {
142-
return fmt.Errorf("service %q has environment variable(s) declared. To avoid leaking sensitive data, "+
143-
"you must either explicitly allow the sending of environment variables by using the --with-env flag,"+
144-
" or remove sensitive data from your Compose configuration", service.Name)
140+
func (s *composeService) preChecks(project *types.Project, options api.PublishOptions) (bool, error) {
141+
envVariables, err := s.checkEnvironmentVariables(project, options)
142+
if err != nil {
143+
return false, err
144+
}
145+
if !options.Force && len(envVariables) > 0 {
146+
fmt.Println("you are about to publish environment variables within your OCI artifact.\n" +
147+
"please double check that you are not leaking sensitive data")
148+
for key, val := range envVariables {
149+
_, _ = fmt.Fprintln(s.dockerCli.Out(), "Service/Config ", key)
150+
for k, v := range val {
151+
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%s=%v\n", k, *v)
145152
}
146153
}
154+
return acceptPublishEnvVariables(s.dockerCli)
155+
}
156+
return true, nil
157+
}
158+
159+
func (s *composeService) checkEnvironmentVariables(project *types.Project, options api.PublishOptions) (map[string]types.MappingWithEquals, error) {
160+
envVarList := map[string]types.MappingWithEquals{}
161+
errorList := map[string][]string{}
162+
163+
for _, service := range project.Services {
164+
if len(service.EnvFiles) > 0 {
165+
errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has env_file declared.", service.Name))
166+
}
167+
if len(service.Environment) > 0 {
168+
errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has environment variable(s) declared.", service.Name))
169+
envVarList[service.Name] = service.Environment
170+
}
171+
}
172+
173+
for _, config := range project.Configs {
174+
if config.Environment != "" {
175+
errorList[config.Name] = append(errorList[config.Name], fmt.Sprintf("config %q is declare as an environment variable.", config.Name))
176+
envVarList[config.Name] = types.NewMappingWithEquals([]string{fmt.Sprintf("%s=%s", config.Name, config.Environment)})
177+
}
178+
}
147179

148-
for _, config := range project.Configs {
149-
if config.Environment != "" {
150-
return fmt.Errorf("config %q is declare as an environment variable. To avoid leaking sensitive data, "+
151-
"you must either explicitly allow the sending of environment variables by using the --with-env flag,"+
152-
" or remove sensitive data from your Compose configuration", config.Name)
180+
if !options.WithEnvironment && len(errorList) > 0 {
181+
errorMsgSuffix := "To avoid leaking sensitive data, you must either explicitly allow the sending of environment variables by using the --with-env flag,\n" +
182+
"or remove sensitive data from your Compose configuration"
183+
errorMsg := ""
184+
for _, errors := range errorList {
185+
for _, err := range errors {
186+
errorMsg += fmt.Sprintf("%s\n", err)
153187
}
154188
}
189+
return nil, fmt.Errorf("%s%s", errorMsg, errorMsgSuffix)
190+
155191
}
192+
return envVarList, nil
193+
}
156194

157-
return nil
195+
func acceptPublishEnvVariables(cli command.Cli) (bool, error) {
196+
_, err := fmt.Fprint(cli.Out(), "Are you ok to publish these environment variables? [y/N]: ")
197+
if err != nil {
198+
return false, err
199+
}
200+
reader := bufio.NewReader(cli.In())
201+
input, err := reader.ReadString('\n')
202+
if err != nil {
203+
return false, err
204+
}
205+
input = strings.ToLower(strings.TrimSpace(input))
206+
switch input {
207+
case "", "n", "no":
208+
return false, nil
209+
case "y", "yes":
210+
return true, nil
211+
default: // anything else reject the consent
212+
return false, nil
213+
}
158214
}
159215

160216
func envFileLayers(project *types.Project) []ocipush.Pushable {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
serviceA:
3+
image: "alpine:3.12"
4+
environment:
5+
- "FOO=bar"
6+
serviceB:
7+
image: "alpine:3.12"
8+
env_file:
9+
- publish.env
10+
environment:
11+
- "BAR=baz"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
FOO=bar
2+
QUIX=

pkg/e2e/publish_test.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,80 @@ func TestPublishChecks(t *testing.T) {
3131
t.Run("publish error environment", func(t *testing.T) {
3232
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-environment.yml",
3333
"-p", projectName, "alpha", "publish", "test/test")
34-
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has environment variable(s) declared. To avoid leaking sensitive data,`})
34+
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has environment variable(s) declared.
35+
To avoid leaking sensitive data,`})
3536
})
3637

3738
t.Run("publish error env_file", func(t *testing.T) {
3839
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-env-file.yml",
3940
"-p", projectName, "alpha", "publish", "test/test")
40-
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has env_file declared. To avoid leaking sensitive data,`})
41+
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has env_file declared.
42+
service "serviceA" has environment variable(s) declared.
43+
To avoid leaking sensitive data,`})
44+
})
45+
46+
t.Run("publish multiple errors env_file and environment", func(t *testing.T) {
47+
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-multi-env-config.yml",
48+
"-p", projectName, "alpha", "publish", "test/test")
49+
// we don't in which order the services will be loaded, so we can't predict the order of the error messages
50+
assert.Assert(t, strings.Contains(res.Combined(), `service "serviceB" has env_file declared.`), res.Combined())
51+
assert.Assert(t, strings.Contains(res.Combined(), `service "serviceB" has environment variable(s) declared.`), res.Combined())
52+
assert.Assert(t, strings.Contains(res.Combined(), `service "serviceA" has environment variable(s) declared.`), res.Combined())
53+
assert.Assert(t, strings.Contains(res.Combined(), `To avoid leaking sensitive data, you must either explicitly allow the sending of environment variables by using the --with-env flag,
54+
or remove sensitive data from your Compose configuration
55+
`), res.Combined())
4156
})
4257

4358
t.Run("publish success environment", func(t *testing.T) {
4459
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-environment.yml",
45-
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
60+
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "-f", "--dry-run")
4661
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
4762
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
4863
})
4964

5065
t.Run("publish success env_file", func(t *testing.T) {
5166
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml",
67+
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "-f", "--dry-run")
68+
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
69+
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
70+
})
71+
72+
t.Run("publish approve validation message", func(t *testing.T) {
73+
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml",
5274
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
75+
cmd.Stdin = strings.NewReader("y\n")
76+
res := icmd.RunCmd(cmd)
77+
res.Assert(t, icmd.Expected{ExitCode: 0})
78+
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables? [y/N]:"), res.Combined())
5379
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
5480
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
5581
})
82+
83+
t.Run("publish refuse validation message", func(t *testing.T) {
84+
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml",
85+
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
86+
cmd.Stdin = strings.NewReader("n\n")
87+
res := icmd.RunCmd(cmd)
88+
res.Assert(t, icmd.Expected{ExitCode: 0})
89+
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables? [y/N]:"), res.Combined())
90+
assert.Assert(t, !strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
91+
assert.Assert(t, !strings.Contains(res.Combined(), "test/test published"), res.Combined())
92+
})
93+
94+
t.Run("publish list env variables", func(t *testing.T) {
95+
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-multi-env-config.yml",
96+
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
97+
cmd.Stdin = strings.NewReader("n\n")
98+
res := icmd.RunCmd(cmd)
99+
res.Assert(t, icmd.Expected{ExitCode: 0})
100+
assert.Assert(t, strings.Contains(res.Combined(), `you are about to publish environment variables within your OCI artifact.
101+
please double check that you are not leaking sensitive data`), res.Combined())
102+
assert.Assert(t, strings.Contains(res.Combined(), `Service/Config serviceA
103+
FOO=bar`), res.Combined())
104+
assert.Assert(t, strings.Contains(res.Combined(), `Service/Config serviceB`), res.Combined())
105+
// we don't know in which order the env variables will be loaded
106+
assert.Assert(t, strings.Contains(res.Combined(), `FOO=bar`), res.Combined())
107+
assert.Assert(t, strings.Contains(res.Combined(), `BAR=baz`), res.Combined())
108+
assert.Assert(t, strings.Contains(res.Combined(), `QUIX=`), res.Combined())
109+
})
56110
}

0 commit comments

Comments
 (0)