Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add --with-env flag to publish command #12482

Merged
merged 1 commit into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type publishOptions struct {
*ProjectOptions
resolveImageDigests bool
ociVersion string
withEnvironment bool
}

func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
Expand All @@ -45,7 +46,9 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
}
flags := cmd.Flags()
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.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")

return cmd
}

Expand All @@ -58,5 +61,6 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service,
return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests,
OCIVersion: api.OCIVersion(opts.ociVersion),
WithEnvironment: opts.withEnvironment,
})
}
3 changes: 2 additions & 1 deletion docs/reference/compose_alpha_publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ Publish compose application
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--oci-version` | `string` | | OCI Image/Artifact specification version (automatically determined by default) |
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |


<!---MARKER_GEN_END-->
Expand Down
12 changes: 11 additions & 1 deletion docs/reference/docker_compose_alpha_publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ options:
- option: oci-version
value_type: string
description: |
OCI Image/Artifact specification version (automatically determined by default)
OCI image/artifact specification version (automatically determined by default)
deprecated: false
hidden: false
experimental: false
Expand All @@ -25,6 +25,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: with-env
value_type: bool
default_value: "false"
description: Include environment variables in the published OCI artifact
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
Expand Down
14 changes: 14 additions & 0 deletions internal/ocipush/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const (
// > an artifactType field, and tooling to work with artifacts should
// > fallback to the config.mediaType value.
ComposeEmptyConfigMediaType = "application/vnd.docker.compose.config.empty.v1+json"
// ComposeEnvFileMediaType is the media type for each Env File layer in the image manifest.
ComposeEnvFileMediaType = "application/vnd.docker.compose.envfile"
)

// clientAuthStatusCodes are client (4xx) errors that are authentication
Expand Down Expand Up @@ -81,6 +83,18 @@ func DescriptorForComposeFile(path string, content []byte) v1.Descriptor {
}
}

func DescriptorForEnvFile(path string, content []byte) v1.Descriptor {
return v1.Descriptor{
MediaType: ComposeEnvFileMediaType,
Digest: digest.FromString(string(content)),
Size: int64(len(content)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
"com.docker.compose.envfile": filepath.Base(path),
},
}
}

func PushManifest(
ctx context.Context,
resolver *imagetools.Resolver,
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ const (
// PublishOptions group options of the Publish API
type PublishOptions struct {
ResolveImageDigests bool
WithEnvironment bool

OCIVersion OCIVersion
}
Expand Down
57 changes: 56 additions & 1 deletion pkg/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package compose

import (
"context"
"fmt"
"os"

"github.com/compose-spec/compose-go/v2/types"
Expand All @@ -35,7 +36,11 @@ 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 := s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
err := preChecks(project, options)
if err != nil {
return err
}
err = s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
if err != nil {
return err
}
Expand Down Expand Up @@ -63,6 +68,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
})
}

if options.WithEnvironment {
layers = append(layers, envFileLayers(project)...)
}

if options.ResolveImageDigests {
yaml, err := s.generateImageDigestsOverride(ctx, project)
if err != nil {
Expand Down Expand Up @@ -120,3 +129,49 @@ 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, "+
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

service.environment may be set with a fixed value, not relying on any interpolation. Typically:

db:
    image: mysql
    environment:
      MYSQL_DATABASE: avatar
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db-password

those should not prevent compose file to be published, right ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact yes that should prevent from publishing by default because you can also have MYSQL_ROOT_PASSWORD: mySuperSecretPassword defined the same way

"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)
}
}

for _, config := range project.Configs {
if config.Environment != "" {
return fmt.Errorf("config %q is declare as an environment variable. 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", config.Name)
}
}
}

return nil
}

func envFileLayers(project *types.Project) []ocipush.Pushable {
var layers []ocipush.Pushable
for _, service := range project.Services {
for _, envFile := range service.EnvFiles {
f, err := os.ReadFile(envFile.Path)
if err != nil {
// if we can't read the file, skip to the next one
continue
}
layerDescriptor := ocipush.DescriptorForEnvFile(envFile.Path, f)
layers = append(layers, ocipush.Pushable{
Descriptor: layerDescriptor,
Data: f,
})
}
}
return layers
}
7 changes: 7 additions & 0 deletions pkg/e2e/fixtures/publish/compose-env-file.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
serviceA:
image: "alpine:3.12"
env_file:
- publish.env
serviceB:
image: "alpine:3.12"
7 changes: 7 additions & 0 deletions pkg/e2e/fixtures/publish/compose-environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
serviceA:
image: "alpine:3.12"
environment:
- "FOO=bar"
serviceB:
image: "alpine:3.12"
1 change: 1 addition & 0 deletions pkg/e2e/fixtures/publish/publish.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO=bar
56 changes: 56 additions & 0 deletions pkg/e2e/publish_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2020 Docker Compose CLI authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package e2e

import (
"strings"
"testing"

"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)

func TestPublishChecks(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-explicit-profiles"

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")
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")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has env_file declared. To avoid leaking sensitive data,`})
})

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")
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", "--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())
})
}
37 changes: 33 additions & 4 deletions pkg/remote/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,46 @@ func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, com
if err != nil {
return err
}
if i > 0 {
_, err = f.Write([]byte("\n---\n"))
if err != nil {

switch layer.MediaType {
case ocipush.ComposeYAMLMediaType:
if err := writeComposeFile(layer, i, f, content); err != nil {
return err
}
case ocipush.ComposeEnvFileMediaType:
if err := writeEnvFile(layer, local, content); err != nil {
return err
}
case ocipush.ComposeEmptyConfigMediaType:
}
_, err = f.Write(content)
}
return nil
}

func writeComposeFile(layer v1.Descriptor, i int, f *os.File, content []byte) error {
if _, ok := layer.Annotations["com.docker.compose.file"]; i > 0 && ok {
_, err := f.Write([]byte("\n---\n"))
if err != nil {
return err
}
}
_, err := f.Write(content)
return err
}

func writeEnvFile(layer v1.Descriptor, local string, content []byte) error {
envfilePath, ok := layer.Annotations["com.docker.compose.envfile"]
if !ok {
return fmt.Errorf("missing annotation com.docker.compose.envfile in layer %q", layer.Digest)
}
otherFile, err := os.Create(filepath.Join(local, envfilePath))
if err != nil {
return err
}
_, err = otherFile.Write(content)
if err != nil {
return err
}
return nil
}

Expand Down
Loading