Skip to content

Commit 88b0d17

Browse files
committed
use build as common API for build scenarios
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 9e19bc8 commit 88b0d17

File tree

5 files changed

+209
-79
lines changed

5 files changed

+209
-79
lines changed

pkg/compose/build.go

Lines changed: 59 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -69,41 +69,11 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
6969
return nil
7070
}
7171
imageName := api.GetImageNameOrDefault(service, project.Name)
72-
buildOptions, err := s.toBuildOptions(project, service, imageName, options.SSHs)
72+
buildOptions, err := s.toBuildOptions(project, service, imageName, options)
7373
if err != nil {
7474
return err
7575
}
76-
buildOptions.Pull = options.Pull
7776
buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, args)
78-
buildOptions.NoCache = options.NoCache
79-
buildOptions.CacheFrom, err = buildflags.ParseCacheEntry(service.Build.CacheFrom)
80-
if err != nil {
81-
return err
82-
}
83-
if len(service.Build.AdditionalContexts) > 0 {
84-
buildOptions.Inputs.NamedContexts = toBuildContexts(service.Build.AdditionalContexts)
85-
}
86-
for _, image := range service.Build.CacheFrom {
87-
buildOptions.CacheFrom = append(buildOptions.CacheFrom, bclient.CacheOptionsEntry{
88-
Type: "registry",
89-
Attrs: map[string]string{"ref": image},
90-
})
91-
}
92-
buildOptions.Exports = []bclient.ExportEntry{{
93-
Type: "docker",
94-
Attrs: map[string]string{
95-
"load": "true",
96-
"push": fmt.Sprint(options.Push),
97-
},
98-
}}
99-
if len(buildOptions.Platforms) > 1 {
100-
buildOptions.Exports = []bclient.ExportEntry{{
101-
Type: "image",
102-
Attrs: map[string]string{
103-
"push": fmt.Sprint(options.Push),
104-
},
105-
}}
106-
}
10777
opts := map[string]build.Options{imageName: buildOptions}
10878
ids, err := s.doBuild(ctx, project, opts, options.Progress)
10979
if err != nil {
@@ -146,11 +116,14 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
146116
if quietPull {
147117
mode = xprogress.PrinterModeQuiet
148118
}
149-
opts, err := s.getBuildOptions(project, images)
119+
120+
err = s.prepareProjectForBuild(project, images)
150121
if err != nil {
151122
return err
152123
}
153-
builtImages, err := s.doBuild(ctx, project, opts, mode)
124+
builtImages, err := s.build(ctx, project, api.BuildOptions{
125+
Progress: mode,
126+
})
154127
if err != nil {
155128
return err
156129
}
@@ -172,37 +145,45 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
172145
return nil
173146
}
174147

175-
func (s *composeService) getBuildOptions(project *types.Project, images map[string]string) (map[string]build.Options, error) {
176-
opts := map[string]build.Options{}
177-
for _, service := range project.Services {
148+
func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) error {
149+
platform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
150+
for i, service := range project.Services {
178151
if service.Image == "" && service.Build == nil {
179-
return nil, fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
152+
return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
180153
}
154+
if service.Build == nil {
155+
continue
156+
}
157+
181158
imageName := api.GetImageNameOrDefault(service, project.Name)
159+
service.Image = imageName
160+
182161
_, localImagePresent := images[imageName]
162+
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
163+
service.Build = nil
164+
project.Services[i] = service
165+
continue
166+
}
183167

184-
if service.Build != nil {
185-
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
186-
continue
168+
if platform != "" {
169+
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, platform) {
170+
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", service.Name, platform)
187171
}
188-
opt, err := s.toBuildOptions(project, service, imageName, []types.SSHKey{})
189-
if err != nil {
190-
return nil, err
191-
}
192-
opt.Exports = []bclient.ExportEntry{{
193-
Type: "docker",
194-
Attrs: map[string]string{
195-
"load": "true",
196-
},
197-
}}
198-
if opt.Platforms, err = useDockerDefaultOrServicePlatform(project, service, true); err != nil {
199-
opt.Platforms = []specs.Platform{}
172+
service.Platform = platform
173+
}
174+
175+
if service.Platform == "" {
176+
// let builder to build for default platform
177+
service.Build.Platforms = nil
178+
} else {
179+
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
180+
return fmt.Errorf("service %q build configuration does not support platform: %s", service.Name, platform)
200181
}
201-
opts[imageName] = opt
202-
continue
182+
service.Build.Platforms = []string{service.Platform}
203183
}
184+
project.Services[i] = service
204185
}
205-
return opts, nil
186+
return nil
206187
}
207188

208189
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
@@ -243,7 +224,7 @@ func (s *composeService) doBuild(ctx context.Context, project *types.Project, op
243224
return s.doBuildBuildkit(ctx, opts, mode)
244225
}
245226

246-
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string, sshKeys []types.SSHKey) (build.Options, error) {
227+
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string, options api.BuildOptions) (build.Options, error) {
247228
var tags []string
248229
tags = append(tags, imageTag)
249230

@@ -272,8 +253,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
272253
sessionConfig := []session.Attachable{
273254
authprovider.NewDockerAuthProvider(s.configFile()),
274255
}
275-
if len(sshKeys) > 0 || len(service.Build.SSH) > 0 {
276-
sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, sshKeys...))
256+
if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 {
257+
sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...))
277258
if err != nil {
278259
return build.Options{}, err
279260
}
@@ -298,20 +279,37 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
298279

299280
imageLabels := getImageBuildLabels(project, service)
300281

282+
exports := []bclient.ExportEntry{{
283+
Type: "docker",
284+
Attrs: map[string]string{
285+
"load": "true",
286+
"push": fmt.Sprint(options.Push),
287+
},
288+
}}
289+
if len(service.Build.Platforms) > 1 {
290+
exports = []bclient.ExportEntry{{
291+
Type: "image",
292+
Attrs: map[string]string{
293+
"push": fmt.Sprint(options.Push),
294+
},
295+
}}
296+
}
297+
301298
return build.Options{
302299
Inputs: build.Inputs{
303300
ContextPath: service.Build.Context,
304301
DockerfileInline: service.Build.DockerfileInline,
305302
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
303+
NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
306304
},
307305
CacheFrom: cacheFrom,
308306
CacheTo: cacheTo,
309-
NoCache: service.Build.NoCache,
310-
Pull: service.Build.Pull,
307+
NoCache: service.Build.NoCache || options.NoCache,
308+
Pull: service.Build.Pull || options.Pull,
311309
BuildArgs: buildArgs,
312310
Tags: tags,
313311
Target: service.Build.Target,
314-
Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
312+
Exports: exports,
315313
Platforms: plats,
316314
Labels: imageLabels,
317315
NetworkMode: service.Build.Network,

pkg/compose/build_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 compose
18+
19+
import (
20+
"testing"
21+
22+
"github.com/compose-spec/compose-go/types"
23+
"gotest.tools/v3/assert"
24+
)
25+
26+
func TestPrepareProjectForBuild(t *testing.T) {
27+
t.Run("build service platform", func(t *testing.T) {
28+
project := types.Project{
29+
Services: []types.ServiceConfig{
30+
{
31+
Name: "test",
32+
Image: "foo",
33+
Build: &types.BuildConfig{
34+
Context: ".",
35+
Platforms: []string{
36+
"linux/amd64",
37+
"linux/arm64",
38+
"alice/32",
39+
},
40+
},
41+
Platform: "alice/32",
42+
},
43+
},
44+
}
45+
46+
s := &composeService{}
47+
err := s.prepareProjectForBuild(&project, nil)
48+
assert.NilError(t, err)
49+
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"alice/32"})
50+
})
51+
52+
t.Run("build DOCKER_DEFAULT_PLATFORM", func(t *testing.T) {
53+
project := types.Project{
54+
Environment: map[string]string{
55+
"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
56+
},
57+
Services: []types.ServiceConfig{
58+
{
59+
Name: "test",
60+
Image: "foo",
61+
Build: &types.BuildConfig{
62+
Context: ".",
63+
Platforms: []string{
64+
"linux/amd64",
65+
"linux/arm64",
66+
},
67+
},
68+
},
69+
},
70+
}
71+
72+
s := &composeService{}
73+
err := s.prepareProjectForBuild(&project, nil)
74+
assert.NilError(t, err)
75+
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"linux/amd64"})
76+
})
77+
78+
t.Run("skip existing image", func(t *testing.T) {
79+
project := types.Project{
80+
Services: []types.ServiceConfig{
81+
{
82+
Name: "test",
83+
Image: "foo",
84+
Build: &types.BuildConfig{
85+
Context: ".",
86+
},
87+
},
88+
},
89+
}
90+
91+
s := &composeService{}
92+
err := s.prepareProjectForBuild(&project, map[string]string{"foo": "exists"})
93+
assert.NilError(t, err)
94+
assert.Check(t, project.Services[0].Build == nil)
95+
})
96+
97+
t.Run("unsupported build platform", func(t *testing.T) {
98+
project := types.Project{
99+
Environment: map[string]string{
100+
"DOCKER_DEFAULT_PLATFORM": "commodore/64",
101+
},
102+
Services: []types.ServiceConfig{
103+
{
104+
Name: "test",
105+
Image: "foo",
106+
Build: &types.BuildConfig{
107+
Context: ".",
108+
Platforms: []string{
109+
"linux/amd64",
110+
"linux/arm64",
111+
},
112+
},
113+
},
114+
},
115+
}
116+
117+
s := &composeService{}
118+
err := s.prepareProjectForBuild(&project, nil)
119+
assert.Check(t, err != nil)
120+
})
121+
}

pkg/compose/dependencies_test.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,26 @@ import (
2727
"gotest.tools/v3/assert"
2828
)
2929

30-
var project = types.Project{
31-
Services: []types.ServiceConfig{
32-
{
33-
Name: "test1",
34-
DependsOn: map[string]types.ServiceDependency{
35-
"test2": {},
30+
func createTestProject() *types.Project {
31+
return &types.Project{
32+
Services: []types.ServiceConfig{
33+
{
34+
Name: "test1",
35+
DependsOn: map[string]types.ServiceDependency{
36+
"test2": {},
37+
},
3638
},
37-
},
38-
{
39-
Name: "test2",
40-
DependsOn: map[string]types.ServiceDependency{
41-
"test3": {},
39+
{
40+
Name: "test2",
41+
DependsOn: map[string]types.ServiceDependency{
42+
"test3": {},
43+
},
44+
},
45+
{
46+
Name: "test3",
4247
},
4348
},
44-
{
45-
Name: "test3",
46-
},
47-
},
49+
}
4850
}
4951

5052
func TestTraversalWithMultipleParents(t *testing.T) {
@@ -97,7 +99,7 @@ func TestInDependencyUpCommandOrder(t *testing.T) {
9799
t.Cleanup(cancel)
98100

99101
var order []string
100-
err := InDependencyOrder(ctx, &project, func(ctx context.Context, service string) error {
102+
err := InDependencyOrder(ctx, createTestProject(), func(ctx context.Context, service string) error {
101103
order = append(order, service)
102104
return nil
103105
})
@@ -110,7 +112,7 @@ func TestInDependencyReverseDownCommandOrder(t *testing.T) {
110112
t.Cleanup(cancel)
111113

112114
var order []string
113-
err := InReverseDependencyOrder(ctx, &project, func(ctx context.Context, service string) error {
115+
err := InReverseDependencyOrder(ctx, createTestProject(), func(ctx context.Context, service string) error {
114116
order = append(order, service)
115117
return nil
116118
})

pkg/compose/watch.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
7878
needRebuild := make(chan fileMapping)
7979
needSync := make(chan fileMapping)
8080

81+
err := s.prepareProjectForBuild(project, nil)
82+
if err != nil {
83+
return err
84+
}
85+
8186
eg, ctx := errgroup.WithContext(ctx)
8287
eg.Go(func() error {
8388
clock := clockwork.NewRealClock()

pkg/e2e/build_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,11 @@ func TestBuildImageDependencies(t *testing.T) {
265265
})
266266

267267
t.Run("BuildKit", func(t *testing.T) {
268-
t.Skip("See https://github.com/docker/compose/issues/9232")
268+
cli := NewParallelCLI(t, WithEnv(
269+
"DOCKER_BUILDKIT=1",
270+
"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
271+
))
272+
doTest(t, cli)
269273
})
270274
}
271275

0 commit comments

Comments
 (0)