Skip to content

Commit 8402888

Browse files
committed
add --with-env flag to publish command
this flag allow publishing env variables in the Compose OCI artifact Signed-off-by: Guillaume Lours <[email protected]>
1 parent 4b70ff0 commit 8402888

File tree

11 files changed

+193
-8
lines changed

11 files changed

+193
-8
lines changed

cmd/compose/publish.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type publishOptions struct {
2929
*ProjectOptions
3030
resolveImageDigests bool
3131
ociVersion string
32+
withEnvironment bool
3233
}
3334

3435
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
@@ -45,7 +46,9 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
4546
}
4647
flags := cmd.Flags()
4748
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
48-
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI Image/Artifact specification version (automatically determined by default)")
49+
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)")
50+
flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact")
51+
4952
return cmd
5053
}
5154

@@ -58,5 +61,6 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service,
5861
return backend.Publish(ctx, project, repository, api.PublishOptions{
5962
ResolveImageDigests: opts.resolveImageDigests,
6063
OCIVersion: api.OCIVersion(opts.ociVersion),
64+
WithEnvironment: opts.withEnvironment,
6165
})
6266
}

docs/reference/compose_alpha_publish.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ Publish compose application
88
| Name | Type | Default | Description |
99
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
1010
| `--dry-run` | `bool` | | Execute command in dry run mode |
11-
| `--oci-version` | `string` | | OCI Image/Artifact specification version (automatically determined by default) |
11+
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
1212
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
13+
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |
1314

1415

1516
<!---MARKER_GEN_END-->

docs/reference/docker_compose_alpha_publish.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ options:
88
- option: oci-version
99
value_type: string
1010
description: |
11-
OCI Image/Artifact specification version (automatically determined by default)
11+
OCI image/artifact specification version (automatically determined by default)
1212
deprecated: false
1313
hidden: false
1414
experimental: false
@@ -25,6 +25,16 @@ options:
2525
experimentalcli: false
2626
kubernetes: false
2727
swarm: false
28+
- option: with-env
29+
value_type: bool
30+
default_value: "false"
31+
description: Include environment variables in the published OCI artifact
32+
deprecated: false
33+
hidden: false
34+
experimental: false
35+
experimentalcli: false
36+
kubernetes: false
37+
swarm: false
2838
inherited_options:
2939
- option: dry-run
3040
value_type: bool

internal/ocipush/push.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ const (
5454
// > an artifactType field, and tooling to work with artifacts should
5555
// > fallback to the config.mediaType value.
5656
ComposeEmptyConfigMediaType = "application/vnd.docker.compose.config.empty.v1+json"
57+
// ComposeEnvFileMediaType is the media type for each Env File layer in the image manifest.
58+
ComposeEnvFileMediaType = "application/vnd.docker.compose.envfile"
5759
)
5860

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

86+
func DescriptorForEnvFile(path string, content []byte) v1.Descriptor {
87+
return v1.Descriptor{
88+
MediaType: ComposeEnvFileMediaType,
89+
Digest: digest.FromString(string(content)),
90+
Size: int64(len(content)),
91+
Annotations: map[string]string{
92+
"com.docker.compose.version": api.ComposeVersion,
93+
"com.docker.compose.envfile": filepath.Base(path),
94+
},
95+
}
96+
}
97+
8498
func PushManifest(
8599
ctx context.Context,
86100
resolver *imagetools.Resolver,

pkg/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ const (
422422
// PublishOptions group options of the Publish API
423423
type PublishOptions struct {
424424
ResolveImageDigests bool
425+
WithEnvironment bool
425426

426427
OCIVersion OCIVersion
427428
}

pkg/compose/publish.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package compose
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"os"
2223

2324
"github.com/compose-spec/compose-go/v2/types"
@@ -35,7 +36,11 @@ func (s *composeService) Publish(ctx context.Context, project *types.Project, re
3536
}
3637

3738
func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
38-
err := s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
39+
err := preChecks(project, options)
40+
if err != nil {
41+
return err
42+
}
43+
err = s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
3944
if err != nil {
4045
return err
4146
}
@@ -63,6 +68,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
6368
})
6469
}
6570

71+
if options.WithEnvironment {
72+
layers = append(layers, envFileLayers(project)...)
73+
}
74+
6675
if options.ResolveImageDigests {
6776
yaml, err := s.generateImageDigestsOverride(ctx, project)
6877
if err != nil {
@@ -120,3 +129,49 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje
120129
}
121130
return override.MarshalYAML()
122131
}
132+
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)
145+
}
146+
}
147+
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)
153+
}
154+
}
155+
}
156+
157+
return nil
158+
}
159+
160+
func envFileLayers(project *types.Project) []ocipush.Pushable {
161+
var layers []ocipush.Pushable
162+
for _, service := range project.Services {
163+
for _, envFile := range service.EnvFiles {
164+
f, err := os.ReadFile(envFile.Path)
165+
if err != nil {
166+
// if we can't read the file, skip to the next one
167+
continue
168+
}
169+
layerDescriptor := ocipush.DescriptorForEnvFile(envFile.Path, f)
170+
layers = append(layers, ocipush.Pushable{
171+
Descriptor: layerDescriptor,
172+
Data: f,
173+
})
174+
}
175+
}
176+
return layers
177+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
serviceA:
3+
image: "alpine:3.12"
4+
env_file:
5+
- publish.env
6+
serviceB:
7+
image: "alpine:3.12"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
serviceA:
3+
image: "alpine:3.12"
4+
environment:
5+
- "FOO=bar"
6+
serviceB:
7+
image: "alpine:3.12"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FOO=bar

pkg/e2e/publish_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright 2020 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2e
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
"gotest.tools/v3/assert"
24+
"gotest.tools/v3/icmd"
25+
)
26+
27+
func TestPublishChecks(t *testing.T) {
28+
c := NewParallelCLI(t)
29+
const projectName = "compose-e2e-explicit-profiles"
30+
31+
t.Run("publish error environment", func(t *testing.T) {
32+
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-environment.yml",
33+
"-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,`})
35+
})
36+
37+
t.Run("publish error env_file", func(t *testing.T) {
38+
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-env-file.yml",
39+
"-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+
})
42+
43+
t.Run("publish success environment", func(t *testing.T) {
44+
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-environment.yml",
45+
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
46+
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
47+
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
48+
})
49+
50+
t.Run("publish success env_file", func(t *testing.T) {
51+
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml",
52+
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
53+
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
54+
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
55+
})
56+
}

0 commit comments

Comments
 (0)