1
0
mirror of https://github.com/docker/compose.git synced 2025-04-08 17:05:13 +02:00

add --with-env flag to publish command

this flag allow publishing env variables in the Compose OCI artifact

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours 2025-01-21 09:51:56 +01:00
parent 4b70ff0ccd
commit 840288895e
11 changed files with 196 additions and 11 deletions

@ -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 {
@ -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
}
@ -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,
})
}

@ -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-->

@ -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
@ -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

@ -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
@ -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,

@ -422,6 +422,7 @@ const (
// PublishOptions group options of the Publish API
type PublishOptions struct {
ResolveImageDigests bool
WithEnvironment bool
OCIVersion OCIVersion
}

@ -18,6 +18,7 @@ package compose
import (
"context"
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/types"
@ -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
}
@ -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 {
@ -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, "+
"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
}

@ -0,0 +1,7 @@
services:
serviceA:
image: "alpine:3.12"
env_file:
- publish.env
serviceB:
image: "alpine:3.12"

@ -0,0 +1,7 @@
services:
serviceA:
image: "alpine:3.12"
environment:
- "FOO=bar"
serviceB:
image: "alpine:3.12"

@ -0,0 +1 @@
FOO=bar

56
pkg/e2e/publish_test.go Normal file

@ -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())
})
}

@ -154,18 +154,47 @@ 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
}
}
_, err = f.Write(content)
if err != nil {
return err
case ocipush.ComposeEnvFileMediaType:
if err := writeEnvFile(layer, local, content); err != nil {
return err
}
case ocipush.ComposeEmptyConfigMediaType:
}
}
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
}
var _ loader.ResourceLoader = ociRemoteLoader{}