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:
parent
4b70ff0ccd
commit
840288895e
@ -29,6 +29,7 @@ type publishOptions struct {
|
|||||||
*ProjectOptions
|
*ProjectOptions
|
||||||
resolveImageDigests bool
|
resolveImageDigests bool
|
||||||
ociVersion string
|
ociVersion string
|
||||||
|
withEnvironment bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
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 := cmd.Flags()
|
||||||
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
|
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
|
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{
|
return backend.Publish(ctx, project, repository, api.PublishOptions{
|
||||||
ResolveImageDigests: opts.resolveImageDigests,
|
ResolveImageDigests: opts.resolveImageDigests,
|
||||||
OCIVersion: api.OCIVersion(opts.ociVersion),
|
OCIVersion: api.OCIVersion(opts.ociVersion),
|
||||||
|
WithEnvironment: opts.withEnvironment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,9 @@ Publish compose application
|
|||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
|
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
|
||||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
| `--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 |
|
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
|
||||||
|
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |
|
||||||
|
|
||||||
|
|
||||||
<!---MARKER_GEN_END-->
|
<!---MARKER_GEN_END-->
|
||||||
|
@ -8,7 +8,7 @@ options:
|
|||||||
- option: oci-version
|
- option: oci-version
|
||||||
value_type: string
|
value_type: string
|
||||||
description: |
|
description: |
|
||||||
OCI Image/Artifact specification version (automatically determined by default)
|
OCI image/artifact specification version (automatically determined by default)
|
||||||
deprecated: false
|
deprecated: false
|
||||||
hidden: false
|
hidden: false
|
||||||
experimental: false
|
experimental: false
|
||||||
@ -25,6 +25,16 @@ options:
|
|||||||
experimentalcli: false
|
experimentalcli: false
|
||||||
kubernetes: false
|
kubernetes: false
|
||||||
swarm: 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:
|
inherited_options:
|
||||||
- option: dry-run
|
- option: dry-run
|
||||||
value_type: bool
|
value_type: bool
|
||||||
|
@ -54,6 +54,8 @@ const (
|
|||||||
// > an artifactType field, and tooling to work with artifacts should
|
// > an artifactType field, and tooling to work with artifacts should
|
||||||
// > fallback to the config.mediaType value.
|
// > fallback to the config.mediaType value.
|
||||||
ComposeEmptyConfigMediaType = "application/vnd.docker.compose.config.empty.v1+json"
|
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
|
// 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(
|
func PushManifest(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
resolver *imagetools.Resolver,
|
resolver *imagetools.Resolver,
|
||||||
|
@ -422,6 +422,7 @@ const (
|
|||||||
// PublishOptions group options of the Publish API
|
// PublishOptions group options of the Publish API
|
||||||
type PublishOptions struct {
|
type PublishOptions struct {
|
||||||
ResolveImageDigests bool
|
ResolveImageDigests bool
|
||||||
|
WithEnvironment bool
|
||||||
|
|
||||||
OCIVersion OCIVersion
|
OCIVersion OCIVersion
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package compose
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if options.ResolveImageDigests {
|
||||||
yaml, err := s.generateImageDigestsOverride(ctx, project)
|
yaml, err := s.generateImageDigestsOverride(ctx, project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -120,3 +129,49 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje
|
|||||||
}
|
}
|
||||||
return override.MarshalYAML()
|
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
|
||||||
|
}
|
||||||
|
7
pkg/e2e/fixtures/publish/compose-env-file.yml
Normal file
7
pkg/e2e/fixtures/publish/compose-env-file.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
serviceA:
|
||||||
|
image: "alpine:3.12"
|
||||||
|
env_file:
|
||||||
|
- publish.env
|
||||||
|
serviceB:
|
||||||
|
image: "alpine:3.12"
|
7
pkg/e2e/fixtures/publish/compose-environment.yml
Normal file
7
pkg/e2e/fixtures/publish/compose-environment.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
serviceA:
|
||||||
|
image: "alpine:3.12"
|
||||||
|
environment:
|
||||||
|
- "FOO=bar"
|
||||||
|
serviceB:
|
||||||
|
image: "alpine:3.12"
|
1
pkg/e2e/fixtures/publish/publish.env
Normal file
1
pkg/e2e/fixtures/publish/publish.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
FOO=bar
|
56
pkg/e2e/publish_test.go
Normal file
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if i > 0 {
|
|
||||||
_, err = f.Write([]byte("\n---\n"))
|
switch layer.MediaType {
|
||||||
if err != nil {
|
case ocipush.ComposeYAMLMediaType:
|
||||||
|
if err := writeComposeFile(layer, i, f, content); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
case ocipush.ComposeEnvFileMediaType:
|
||||||
_, err = f.Write(content)
|
if err := writeEnvFile(layer, local, content); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
|
case ocipush.ComposeEmptyConfigMediaType:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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{}
|
var _ loader.ResourceLoader = ociRemoteLoader{}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user