From ad750d6143c7d4d64e7e76f525e813bd9bb3cd4d Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:20:48 +0200 Subject: [PATCH] remove publish limitation on bind mount list all bind mounts and ask user validation before publishing Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- pkg/compose/publish.go | 35 ++++++++++++++++++++++++------ pkg/e2e/publish_test.go | 48 ++++++++++++++++++++++++++++------------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 97200613e..7a8a9c53c 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -236,8 +236,20 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp if ok, err := s.checkOnlyBuildSection(project); !ok || err != nil { return false, err } - if ok, err := s.checkForBindMount(project); !ok || err != nil { - return false, err + bindMounts := s.checkForBindMount(project) + if len(bindMounts) > 0 { + fmt.Println("you are about to publish bind mounts declaration within your OCI artifact.\n" + + "only the bind mount declarations will be added to the OCI artifact\n" + + "please double check that you are not mounting potential sensitive directories or data") + for key, val := range bindMounts { + _, _ = fmt.Fprintln(s.dockerCli.Out(), key) + for _, v := range val { + _, _ = fmt.Fprintf(s.dockerCli.Out(), "%s\n", v.String()) + } + } + if ok, err := acceptPublishBindMountDeclarations(s.dockerCli); err != nil || !ok { + return false, err + } } if options.AssumeYes { return true, nil @@ -325,6 +337,12 @@ func acceptPublishSensitiveData(cli command.Cli) (bool, error) { return confirm, err } +func acceptPublishBindMountDeclarations(cli command.Cli) (bool, error) { + msg := "Are you ok to publish these bind mount declarations? [y/N]: " + confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false) + return confirm, err +} + func envFileLayers(project *types.Project) []ocipush.Pushable { var layers []ocipush.Pushable for _, service := range project.Services { @@ -361,15 +379,20 @@ func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, er return true, nil } -func (s *composeService) checkForBindMount(project *types.Project) (bool, error) { - for name, config := range project.Services { +func (s *composeService) checkForBindMount(project *types.Project) map[string][]types.ServiceVolumeConfig { + allFindings := map[string][]types.ServiceVolumeConfig{} + for serviceName, config := range project.Services { + bindMounts := []types.ServiceVolumeConfig{} for _, volume := range config.Volumes { if volume.Type == types.VolumeTypeBind { - return false, fmt.Errorf("cannot publish compose file: service %q relies on bind-mount. You should use volumes", name) + bindMounts = append(bindMounts, volume) } } + if len(bindMounts) > 0 { + allFindings[serviceName] = bindMounts + } } - return true, nil + return allFindings } func (s *composeService) checkForSensitiveData(project *types.Project) ([]secrets.DetectedSecret, error) { diff --git a/pkg/e2e/publish_test.go b/pkg/e2e/publish_test.go index 92196daeb..37d35df36 100644 --- a/pkg/e2e/publish_test.go +++ b/pkg/e2e/publish_test.go @@ -30,14 +30,14 @@ func TestPublishChecks(t *testing.T) { t.Run("publish error environment", func(t *testing.T) { res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-environment.yml", - "-p", projectName, "alpha", "publish", "test/test") + "-p", projectName, "publish", "test/test") res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has environment variable(s) declared. To avoid leaking sensitive data,`}) }) t.Run("publish error env_file", func(t *testing.T) { res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-env-file.yml", - "-p", projectName, "alpha", "publish", "test/test") + "-p", projectName, "publish", "test/test") res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has env_file declared. service "serviceA" has environment variable(s) declared. To avoid leaking sensitive data,`}) @@ -45,7 +45,7 @@ To avoid leaking sensitive data,`}) t.Run("publish multiple errors env_file and environment", func(t *testing.T) { res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-multi-env-config.yml", - "-p", projectName, "alpha", "publish", "test/test") + "-p", projectName, "publish", "test/test") // we don't in which order the services will be loaded, so we can't predict the order of the error messages assert.Assert(t, strings.Contains(res.Combined(), `service "serviceB" has env_file declared.`), res.Combined()) assert.Assert(t, strings.Contains(res.Combined(), `service "serviceB" has environment variable(s) declared.`), res.Combined()) @@ -57,21 +57,21 @@ or remove sensitive data from your Compose configuration 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", "-y", "--dry-run") + "-p", projectName, "publish", "test/test", "--with-env", "-y", "--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", "-y", "--dry-run") + "-p", projectName, "publish", "test/test", "--with-env", "-y", "--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") + "-p", projectName, "publish", "test/test", "--with-env", "--dry-run") cmd.Stdin = strings.NewReader("y\n") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{ExitCode: 0}) @@ -82,7 +82,7 @@ or remove sensitive data from your Compose configuration 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") + "-p", projectName, "publish", "test/test", "--with-env", "--dry-run") cmd.Stdin = strings.NewReader("n\n") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{ExitCode: 0}) @@ -93,13 +93,13 @@ or remove sensitive data from your Compose configuration t.Run("publish with extends", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-with-extends.yml", - "-p", projectName, "alpha", "publish", "test/test", "--dry-run") + "-p", projectName, "publish", "test/test", "--dry-run") assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined()) }) t.Run("publish list env variables", func(t *testing.T) { cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-multi-env-config.yml", - "-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run") + "-p", projectName, "publish", "test/test", "--with-env", "--dry-run") cmd.Stdin = strings.NewReader("n\n") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{ExitCode: 0}) @@ -115,14 +115,32 @@ FOO=bar`), res.Combined()) }) t.Run("refuse to publish with bind mount", func(t *testing.T) { - res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-bind-mount.yml", - "-p", projectName, "alpha", "publish", "test/test", "--dry-run") - res.Assert(t, icmd.Expected{ExitCode: 1, Err: `cannot publish compose file: service "serviceA" relies on bind-mount. You should use volumes`}) + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-bind-mount.yml", + "-p", projectName, "publish", "test/test", "--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(), "you are about to publish bind mounts declaration within your OCI artifact."), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "e2e/fixtures/publish:/user-data"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these bind mount declarations? [y/N]:"), res.Combined()) + assert.Assert(t, !strings.Contains(res.Combined(), "serviceA published"), res.Combined()) + }) + + t.Run("publish with bind mount", func(t *testing.T) { + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-bind-mount.yml", + "-p", projectName, "publish", "test/test", "--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(), "you are about to publish bind mounts declaration within your OCI artifact."), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these bind mount declarations? [y/N]:"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "e2e/fixtures/publish:/user-data"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined()) }) t.Run("refuse to publish with build section only", func(t *testing.T) { res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-build-only.yml", - "-p", projectName, "alpha", "publish", "test/test", "--with-env", "-y", "--dry-run") + "-p", projectName, "publish", "test/test", "--with-env", "-y", "--dry-run") res.Assert(t, icmd.Expected{ExitCode: 1}) assert.Assert(t, strings.Contains(res.Combined(), "your Compose stack cannot be published as it only contains a build section for service(s):"), res.Combined()) assert.Assert(t, strings.Contains(res.Combined(), "serviceA"), res.Combined()) @@ -131,13 +149,13 @@ FOO=bar`), res.Combined()) t.Run("refuse to publish with local include", func(t *testing.T) { res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-local-include.yml", - "-p", projectName, "alpha", "publish", "test/test", "--dry-run") + "-p", projectName, "publish", "test/test", "--dry-run") res.Assert(t, icmd.Expected{ExitCode: 1, Err: "cannot publish compose file with local includes"}) }) t.Run("detect sensitive data", func(t *testing.T) { cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-sensitive.yml", - "-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run") + "-p", projectName, "publish", "test/test", "--with-env", "--dry-run") cmd.Stdin = strings.NewReader("n\n") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{ExitCode: 0})