diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go index 29bc670f6e..eb40c9847e 100644 --- a/cmd/compose/publish.go +++ b/cmd/compose/publish.go @@ -30,6 +30,7 @@ type publishOptions struct { resolveImageDigests bool ociVersion string withEnvironment bool + force bool } 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 flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests") flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)") flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact") + flags.BoolVarP(&opts.force, "force", "f", false, "Force publish without asking for confirmation") return cmd } @@ -62,5 +64,6 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, ResolveImageDigests: opts.resolveImageDigests, OCIVersion: api.OCIVersion(opts.ociVersion), WithEnvironment: opts.withEnvironment, + Force: opts.force, }) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 06aea8064f..2f95780488 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -421,6 +421,7 @@ const ( type PublishOptions struct { ResolveImageDigests bool WithEnvironment bool + Force bool OCIVersion OCIVersion } diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index fddafe3c4f..f96a2afdac 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -17,9 +17,14 @@ package compose import ( + "bufio" "context" "fmt" + "golang.org/x/exp/maps" "os" + "strings" + + "github.com/docker/cli/cli/command" "github.com/compose-spec/compose-go/v2/types" "github.com/distribution/reference" @@ -36,10 +41,13 @@ func (s *composeService) Publish(ctx context.Context, project *types.Project, re } func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error { - err := preChecks(project, options) + accept, err := s.preChecks(project, options) if err != nil { return err } + if !accept { + return nil + } err = s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true}) if err != nil { return err @@ -126,21 +134,55 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje return override.MarshalYAML() } -func preChecks(project *types.Project, options api.PublishOptions) error { - if !options.WithEnvironment { - for _, service := range project.Services { - if len(service.EnvFiles) > 0 { - return fmt.Errorf("service %q has env_file declared. To avoid leaking sensitive data, "+ - "you must either explicitly allow the sending of environment variables by using the --with-env flag,"+ - " or remove sensitive data from your Compose configuration", service.Name) - } - if len(service.Environment) > 0 { - return fmt.Errorf("service %q has environment variable(s) declared. To avoid leaking sensitive data, "+ - "you must either explicitly allow the sending of environment variables by using the --with-env flag,"+ - " or remove sensitive data from your Compose configuration", service.Name) - } +func (s *composeService) preChecks(project *types.Project, options api.PublishOptions) (bool, error) { + envVariables := types.MappingWithEquals{} + for _, service := range project.Services { + if err := s.checkEnvironmentVariables(service, options); err != nil { + return false, err + } + maps.Copy(envVariables, service.Environment) + } + if !options.Force && len(envVariables) > 0 { + fmt.Println("you are about to publish environment variables within your OCI artifact.\n" + + "please double check that you are not leaking sensitive data") + for key, val := range envVariables { + fmt.Fprintf(s.dockerCli.Out(), "%s=%v\n", key, *val) } + return acceptPublishEnvVariables(s.dockerCli) } + return true, nil +} +func (s *composeService) checkEnvironmentVariables(service types.ServiceConfig, options api.PublishOptions) error { + if !options.WithEnvironment { + if len(service.EnvFiles) > 0 { + return fmt.Errorf("service %q has env_file declared. To avoid leaking sensitive data, "+ + "you must either explicitly allow the sending of environment variables by using the --with-env flag,"+ + " or remove sensitive data from your Compose configuration", service.Name) + } + if len(service.Environment) > 0 { + return fmt.Errorf("service %q has environment variable(s) declared. To avoid leaking sensitive data, "+ + "you must either explicitly allow the sending of environment variables by using the --with-env flag,"+ + " or remove sensitive data from your Compose configuration", service.Name) + } + } return nil } + +func acceptPublishEnvVariables(cli command.Cli) (bool, error) { + fmt.Fprint(cli.Out(), "Are you ok to publish these environment variables? [y/N]: ") + reader := bufio.NewReader(cli.In()) + input, err := reader.ReadString('\n') + if err != nil { + return false, err + } + input = strings.ToLower(strings.TrimSpace(input)) + switch input { + case "", "n", "no": + return false, nil + case "y", "yes": + return true, nil + default: // anything else reject the consent + return false, nil + } +} diff --git a/pkg/e2e/fixtures/publish/publish.env b/pkg/e2e/fixtures/publish/publish.env index c075a74be9..7a41ee4485 100644 --- a/pkg/e2e/fixtures/publish/publish.env +++ b/pkg/e2e/fixtures/publish/publish.env @@ -1 +1,2 @@ FOO=bar +QUIX= diff --git a/pkg/e2e/publish_test.go b/pkg/e2e/publish_test.go index 2f7ad239c8..98537c081c 100644 --- a/pkg/e2e/publish_test.go +++ b/pkg/e2e/publish_test.go @@ -42,15 +42,37 @@ func TestPublishChecks(t *testing.T) { t.Run("publish success environment", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-environment.yml", - "-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run") + "-p", projectName, "alpha", "publish", "test/test", "--with-env", "-f", "--dry-run") assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined()) assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined()) }) t.Run("publish success env_file", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml", + "-p", projectName, "alpha", "publish", "test/test", "--with-env", "-f", "--dry-run") + assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined()) + }) + + t.Run("publish approve validation message", func(t *testing.T) { + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml", "-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run") + cmd.Stdin = strings.NewReader("y\n") + res := icmd.RunCmd(cmd) + res.Assert(t, icmd.Expected{ExitCode: 0}) + assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables? [y/N]:"), res.Combined()) assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined()) assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined()) }) + + t.Run("publish refuse validation message", func(t *testing.T) { + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml", + "-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run") + cmd.Stdin = strings.NewReader("n\n") + res := icmd.RunCmd(cmd) + res.Assert(t, icmd.Expected{ExitCode: 0}) + assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables? [y/N]:"), res.Combined()) + assert.Assert(t, !strings.Contains(res.Combined(), "test/test publishing"), res.Combined()) + assert.Assert(t, !strings.Contains(res.Combined(), "test/test published"), res.Combined()) + }) }