Skip to content

Commit 6007d4c

Browse files
ndeloofglours
authored andcommitted
publish env_file references as opaque hash to prevent paths conflicts
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 69bcb96 commit 6007d4c

File tree

6 files changed

+183
-69
lines changed

6 files changed

+183
-69
lines changed

pkg/compose/publish.go

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/opencontainers/go-digest"
4040
"github.com/opencontainers/image-spec/specs-go"
4141
v1 "github.com/opencontainers/image-spec/specs-go/v1"
42+
"github.com/sirupsen/logrus"
4243
)
4344

4445
func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
@@ -65,54 +66,33 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
6566
return err
6667
}
6768

68-
named, err := reference.ParseDockerRef(repository)
69+
layers, err := s.createLayers(ctx, project, options)
6970
if err != nil {
7071
return err
7172
}
7273

73-
config := s.dockerCli.ConfigFile()
74-
75-
resolver := oci.NewResolver(config)
76-
77-
var layers []v1.Descriptor
78-
extFiles := map[string]string{}
79-
for _, file := range project.ComposeFiles {
80-
data, err := processFile(ctx, file, project, extFiles)
81-
if err != nil {
82-
return err
83-
}
84-
85-
layerDescriptor := oci.DescriptorForComposeFile(file, data)
86-
layers = append(layers, layerDescriptor)
87-
}
88-
89-
extLayers, err := processExtends(ctx, project, extFiles)
90-
if err != nil {
91-
return err
92-
}
93-
layers = append(layers, extLayers...)
94-
95-
if options.WithEnvironment {
96-
layers = append(layers, envFileLayers(project)...)
97-
}
98-
99-
if options.ResolveImageDigests {
100-
yaml, err := s.generateImageDigestsOverride(ctx, project)
101-
if err != nil {
102-
return err
103-
}
104-
105-
layerDescriptor := oci.DescriptorForComposeFile("image-digests.yaml", yaml)
106-
layers = append(layers, layerDescriptor)
107-
}
108-
10974
w := progress.ContextWriter(ctx)
11075
w.Event(progress.Event{
11176
ID: repository,
11277
Text: "publishing",
11378
Status: progress.Working,
11479
})
80+
if logrus.IsLevelEnabled(logrus.DebugLevel) {
81+
logrus.Debug("publishing layers")
82+
for _, layer := range layers {
83+
indent, _ := json.MarshalIndent(layer, "", " ")
84+
fmt.Println(string(indent))
85+
}
86+
}
11587
if !s.dryRun {
88+
named, err := reference.ParseDockerRef(repository)
89+
if err != nil {
90+
return err
91+
}
92+
93+
config := s.dockerCli.ConfigFile()
94+
resolver := oci.NewResolver(config)
95+
11696
descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
11797
if err != nil {
11898
w.Event(progress.Event{
@@ -175,11 +155,47 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
175155
return nil
176156
}
177157

158+
func (s *composeService) createLayers(ctx context.Context, project *types.Project, options api.PublishOptions) ([]v1.Descriptor, error) {
159+
var layers []v1.Descriptor
160+
extFiles := map[string]string{}
161+
envFiles := map[string]string{}
162+
for _, file := range project.ComposeFiles {
163+
data, err := processFile(ctx, file, project, extFiles, envFiles)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
layerDescriptor := oci.DescriptorForComposeFile(file, data)
169+
layers = append(layers, layerDescriptor)
170+
}
171+
172+
extLayers, err := processExtends(ctx, project, extFiles)
173+
if err != nil {
174+
return nil, err
175+
}
176+
layers = append(layers, extLayers...)
177+
178+
if options.WithEnvironment {
179+
layers = append(layers, envFileLayers(envFiles)...)
180+
}
181+
182+
if options.ResolveImageDigests {
183+
yaml, err := s.generateImageDigestsOverride(ctx, project)
184+
if err != nil {
185+
return nil, err
186+
}
187+
188+
layerDescriptor := oci.DescriptorForComposeFile("image-digests.yaml", yaml)
189+
layers = append(layers, layerDescriptor)
190+
}
191+
return layers, nil
192+
}
193+
178194
func processExtends(ctx context.Context, project *types.Project, extFiles map[string]string) ([]v1.Descriptor, error) {
179195
var layers []v1.Descriptor
180196
moreExtFiles := map[string]string{}
181197
for xf, hash := range extFiles {
182-
data, err := processFile(ctx, xf, project, moreExtFiles)
198+
data, err := processFile(ctx, xf, project, moreExtFiles, nil)
183199
if err != nil {
184200
return nil, err
185201
}
@@ -204,7 +220,7 @@ func processExtends(ctx context.Context, project *types.Project, extFiles map[st
204220
return layers, nil
205221
}
206222

207-
func processFile(ctx context.Context, file string, project *types.Project, extFiles map[string]string) ([]byte, error) {
223+
func processFile(ctx context.Context, file string, project *types.Project, extFiles map[string]string, envFiles map[string]string) ([]byte, error) {
208224
f, err := os.ReadFile(file)
209225
if err != nil {
210226
return nil, err
@@ -230,6 +246,15 @@ func processFile(ctx context.Context, file string, project *types.Project, extFi
230246
return nil, err
231247
}
232248
for name, service := range base.Services {
249+
for i, envFile := range service.EnvFiles {
250+
hash := fmt.Sprintf("%x.env", sha256.Sum256([]byte(envFile.Path)))
251+
envFiles[envFile.Path] = hash
252+
f, err = transform.ReplaceEnvFile(f, name, i, hash)
253+
if err != nil {
254+
return nil, err
255+
}
256+
}
257+
233258
if service.Extends == nil {
234259
continue
235260
}
@@ -376,18 +401,16 @@ func (s *composeService) checkEnvironmentVariables(project *types.Project, optio
376401
return envVarList, nil
377402
}
378403

379-
func envFileLayers(project *types.Project) []v1.Descriptor {
404+
func envFileLayers(files map[string]string) []v1.Descriptor {
380405
var layers []v1.Descriptor
381-
for _, service := range project.Services {
382-
for _, envFile := range service.EnvFiles {
383-
f, err := os.ReadFile(envFile.Path)
384-
if err != nil {
385-
// if we can't read the file, skip to the next one
386-
continue
387-
}
388-
layerDescriptor := oci.DescriptorForEnvFile(envFile.Path, f)
389-
layers = append(layers, layerDescriptor)
406+
for file, hash := range files {
407+
f, err := os.ReadFile(file)
408+
if err != nil {
409+
// if we can't read the file, skip to the next one
410+
continue
390411
}
412+
layerDescriptor := oci.DescriptorForEnvFile(hash, f)
413+
layers = append(layers, layerDescriptor)
391414
}
392415
return layers
393416
}

pkg/compose/publish_test.go

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ package compose
1818

1919
import (
2020
"context"
21-
"os"
21+
"slices"
2222
"testing"
2323

2424
"github.com/compose-spec/compose-go/v2/loader"
2525
"github.com/compose-spec/compose-go/v2/types"
26+
"github.com/docker/compose/v2/internal"
2627
"github.com/docker/compose/v2/pkg/api"
28+
"github.com/google/go-cmp/cmp"
2729
v1 "github.com/opencontainers/image-spec/specs-go/v1"
2830
"gotest.tools/v3/assert"
2931
)
3032

31-
func Test_processExtends(t *testing.T) {
33+
func Test_createLayers(t *testing.T) {
3234
project, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{
3335
WorkingDir: "testdata/publish/",
3436
Environment: types.Mapping{},
@@ -39,35 +41,62 @@ func Test_processExtends(t *testing.T) {
3941
},
4042
})
4143
assert.NilError(t, err)
42-
extFiles := map[string]string{}
43-
file, err := processFile(context.TODO(), "testdata/publish/compose.yaml", project, extFiles)
44+
project.ComposeFiles = []string{"testdata/publish/compose.yaml"}
45+
46+
service := &composeService{}
47+
layers, err := service.createLayers(context.TODO(), project, api.PublishOptions{
48+
WithEnvironment: true,
49+
})
4450
assert.NilError(t, err)
4551

46-
v := string(file)
47-
assert.Equal(t, v, `name: test
52+
published := string(layers[0].Data)
53+
assert.Equal(t, published, `name: test
4854
services:
4955
test:
5056
extends:
5157
file: f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml
5258
service: foo
53-
`)
5459
55-
layers, err := processExtends(context.TODO(), project, extFiles)
56-
assert.NilError(t, err)
60+
string:
61+
image: test
62+
env_file: 5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3.env
5763
58-
b, err := os.ReadFile("testdata/publish/common.yaml")
59-
assert.NilError(t, err)
60-
assert.DeepEqual(t, []v1.Descriptor{
64+
list:
65+
image: test
66+
env_file:
67+
- 5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3.env
68+
69+
mapping:
70+
image: test
71+
env_file:
72+
- path: 5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3.env
73+
`)
74+
75+
expectedLayers := []v1.Descriptor{
76+
{
77+
MediaType: "application/vnd.docker.compose.file+yaml",
78+
Annotations: map[string]string{
79+
"com.docker.compose.file": "compose.yaml",
80+
"com.docker.compose.version": internal.Version},
81+
},
6182
{
6283
MediaType: "application/vnd.docker.compose.file+yaml",
63-
Digest: "sha256:d3ba84507b56ec783f4b6d24306b99a15285f0a23a835f0b668c2dbf9c59c241",
64-
Size: 32,
6584
Annotations: map[string]string{
6685
"com.docker.compose.extends": "true",
67-
"com.docker.compose.file": "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml",
68-
"com.docker.compose.version": api.ComposeVersion,
86+
"com.docker.compose.file": "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c",
87+
"com.docker.compose.version": internal.Version,
88+
},
89+
},
90+
{
91+
MediaType: "application/vnd.docker.compose.envfile",
92+
Annotations: map[string]string{
93+
"com.docker.compose.envfile": "5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3",
94+
"com.docker.compose.version": internal.Version,
6995
},
70-
Data: b,
7196
},
72-
}, layers)
97+
}
98+
assert.DeepEqual(t, expectedLayers, layers, cmp.FilterPath(func(path cmp.Path) bool {
99+
return !slices.Contains([]string{".Data", ".Digest", ".Size"}, path.String())
100+
}, cmp.Ignore()))
101+
73102
}

pkg/compose/testdata/publish/compose.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,17 @@ services:
44
extends:
55
file: common.yaml
66
service: foo
7+
8+
string:
9+
image: test
10+
env_file: test.env
11+
12+
list:
13+
image: test
14+
env_file:
15+
- test.env
16+
17+
mapping:
18+
image: test
19+
env_file:
20+
- path: test.env
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
HELLO=WORLD

pkg/compose/transform/replace.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,52 @@ func ReplaceExtendsFile(in []byte, service string, value string) ([]byte, error)
6161
return replace(in, file.Line, file.Column, value), nil
6262
}
6363

64+
// ReplaceEnvFile changes value for service.extends.env_file in input yaml stream, preserving formatting
65+
func ReplaceEnvFile(in []byte, service string, i int, value string) ([]byte, error) {
66+
var doc yaml.Node
67+
err := yaml.Unmarshal(in, &doc)
68+
if err != nil {
69+
return nil, err
70+
}
71+
if doc.Kind != yaml.DocumentNode {
72+
return nil, fmt.Errorf("expected document kind %v, got %v", yaml.DocumentNode, doc.Kind)
73+
}
74+
root := doc.Content[0]
75+
if root.Kind != yaml.MappingNode {
76+
return nil, fmt.Errorf("expected document root to be a mapping, got %v", root.Kind)
77+
}
78+
79+
services, err := getMapping(root, "services")
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
target, err := getMapping(services, service)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
envFile, err := getMapping(target, "env_file")
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// env_file can be either a string, sequence of strings, or sequence of mappings with path attribute
95+
if envFile.Kind == yaml.SequenceNode {
96+
envFile = envFile.Content[i]
97+
if envFile.Kind == yaml.MappingNode {
98+
envFile, err = getMapping(envFile, "path")
99+
if err != nil {
100+
return nil, err
101+
}
102+
}
103+
return replace(in, envFile.Line, envFile.Column, value), nil
104+
} else {
105+
return replace(in, envFile.Line, envFile.Column, value), nil
106+
}
107+
108+
}
109+
64110
func getMapping(root *yaml.Node, key string) (*yaml.Node, error) {
65111
var node *yaml.Node
66112
l := len(root.Content)

pkg/remote/oci.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,9 @@ func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, man
217217

218218
func writeComposeFile(layer spec.Descriptor, i int, local string, content []byte) error {
219219
file := "compose.yaml"
220-
if extends, ok := layer.Annotations["com.docker.compose.extends"]; ok {
221-
if err := validatePathInBase(local, extends); err != nil {
220+
if _, ok := layer.Annotations["com.docker.compose.extends"]; ok {
221+
file = layer.Annotations["com.docker.compose.file"]
222+
if err := validatePathInBase(local, file); err != nil {
222223
return err
223224
}
224225
}

0 commit comments

Comments
 (0)