build: respect dependency order for classic builder

When using the "classic" (non-BuildKit) builder, ensure that
services are iterated in dependency order for a build so that
it's possible to guarantee the presence of a base image that's
been added as a dependency with `depends_on`. This is a very
common pattern when using base images with Compose.

A fix for BuildKit is blocked currently until we can rely on a
newer version of the engine (see docker/compose#9324)[^1].

[^1]: https://github.com/docker/compose/issues/9232#issuecomment-1060389808

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
This commit is contained in:
Milas Bowman 2022-06-22 16:13:08 -04:00
parent e5dcb8a8f8
commit b2cd089bae
7 changed files with 74 additions and 4 deletions

View File

@ -205,7 +205,7 @@ func (s *composeService) doBuild(ctx context.Context, project *types.Project, op
return nil, nil
}
if buildkitEnabled, err := s.dockerCli.BuildKitEnabled(); err != nil || !buildkitEnabled {
return s.doBuildClassic(ctx, opts)
return s.doBuildClassic(ctx, project, opts)
}
return s.doBuildBuildkit(ctx, project, opts, mode)
}

View File

@ -27,6 +27,7 @@ import (
"runtime"
"strings"
"github.com/compose-spec/compose-go/types"
buildx "github.com/docker/buildx/build"
"github.com/docker/cli/cli/command/image/build"
dockertypes "github.com/docker/docker/api/types"
@ -41,15 +42,24 @@ import (
"github.com/pkg/errors"
)
func (s *composeService) doBuildClassic(ctx context.Context, opts map[string]buildx.Options) (map[string]string, error) {
func (s *composeService) doBuildClassic(ctx context.Context, project *types.Project, opts map[string]buildx.Options) (map[string]string, error) {
var nameDigests = make(map[string]string)
var errs error
for name, o := range opts {
err := project.WithServices(nil, func(service types.ServiceConfig) error {
imageName := getImageName(service, project.Name)
o, ok := opts[imageName]
if !ok {
return nil
}
digest, err := s.doBuildClassicSimpleImage(ctx, o)
if err != nil {
errs = multierror.Append(errs, err).ErrorOrNil()
}
nameDigests[name] = digest
nameDigests[imageName] = digest
return nil
})
if err != nil {
return nil, err
}
return nameDigests, errs

View File

@ -201,3 +201,40 @@ func TestBuildTags(t *testing.T) {
res.Assert(t, icmd.Expected{Out: expectedOutput})
})
}
func TestBuildImageDependencies(t *testing.T) {
doTest := func(t *testing.T, cli *CLI) {
resetState := func() {
cli.RunDockerComposeCmd(t, "down", "--rmi=all", "-t=0")
}
resetState()
t.Cleanup(resetState)
// the image should NOT exist now
res := cli.RunDockerOrExitError(t, "image", "inspect", "build-dependencies_service")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "Error: No such image: build-dependencies_service",
})
res = cli.RunDockerComposeCmd(t, "build")
t.Log(res.Combined())
res = cli.RunDockerCmd(t,
"image", "inspect", "--format={{ index .RepoTags 0 }}",
"build-dependencies_service")
res.Assert(t, icmd.Expected{Out: "build-dependencies_service:latest"})
}
t.Run("ClassicBuilder", func(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"DOCKER_BUILDKIT=0",
"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
))
doTest(t, cli)
})
t.Run("BuildKit", func(t *testing.T) {
t.Skip("See https://github.com/docker/compose/issues/9232")
})
}

View File

@ -0,0 +1,5 @@
FROM alpine
COPY hello.txt /hello.txt
CMD [ "/bin/true" ]

View File

@ -0,0 +1,12 @@
services:
base:
image: base
build:
context: .
dockerfile: base.dockerfile
service:
depends_on:
- base
build:
context: .
dockerfile: service.dockerfile

View File

@ -0,0 +1 @@
this file was copied from base -> service

View File

@ -0,0 +1,5 @@
FROM alpine
COPY --from=base /hello.txt /hello.txt
CMD [ "cat", "/hello.txt" ]