Skip to content

Commit 52005a7

Browse files
Implement multi-stage Docker builds by leveraging imagebuilder
Unless the user sets the imageOptimizationPolicy to None, use imagebuilder to perform multi-stage builds.
1 parent 79a6ace commit 52005a7

File tree

5 files changed

+109
-11
lines changed

5 files changed

+109
-11
lines changed

pkg/build/builder/docker.go

+62-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package builder
33
import (
44
"context"
55
"fmt"
6+
"github.com/openshift/imagebuilder"
67
"os"
78
"os/exec"
89
"path/filepath"
@@ -14,6 +15,7 @@ import (
1415

1516
corev1 "k8s.io/api/core/v1"
1617
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/apimachinery/pkg/util/sets"
1719

1820
s2iapi "github.com/openshift/source-to-image/pkg/api"
1921
"github.com/openshift/source-to-image/pkg/tar"
@@ -86,7 +88,11 @@ func (d *DockerBuilder) Build() error {
8688

8789
buildTag := randomBuildTag(d.build.Namespace, d.build.Name)
8890
dockerfilePath := getDockerfilePath(buildDir, d.build)
89-
imageNames := getDockerfileFrom(dockerfilePath)
91+
92+
imageNames, multiStage, err := rewriteDockerfileForImages(dockerfilePath, d.build)
93+
if err != nil {
94+
return err
95+
}
9096
if len(imageNames) == 0 {
9197
return fmt.Errorf("no FROM image in Dockerfile")
9298
}
@@ -126,6 +132,14 @@ func (d *DockerBuilder) Build() error {
126132
}
127133
}
128134

135+
if s := d.build.Spec.Strategy.DockerStrategy; s != nil && multiStage {
136+
if s.ImageOptimizationPolicy == nil {
137+
policy := buildapiv1.ImageOptimizationSkipLayers
138+
s.ImageOptimizationPolicy = &policy
139+
glog.V(2).Infof("Detected multi-stage Dockerfile, image will be built with imageOptimizationPolicy set to SkipLayers")
140+
}
141+
}
142+
129143
startTime := metav1.Now()
130144
err = d.dockerBuild(buildDir, buildTag, d.build.Spec.Source.Secrets)
131145

@@ -465,7 +479,7 @@ func insertEnvAfterFrom(node *parser.Node, env []corev1.EnvVar) error {
465479
return nil
466480
}
467481

468-
// getDockerfilefrom returns all the images behind "FROM" instruction in the dockerfile
482+
// getDockerfileFrom returns all the images behind "FROM" instruction in the Dockerfile.
469483
func getDockerfileFrom(dockerfilePath string) []string {
470484
var froms []string
471485
if "" == dockerfilePath {
@@ -487,3 +501,49 @@ func getDockerfileFrom(dockerfilePath string) []string {
487501
}
488502
return froms
489503
}
504+
505+
// rewriteDockerfileForImages returns all qualified images referenced by the Dockerfile, whether the
506+
// build is a multi-stage build, or returns an error.
507+
// TODO: implement image rewriting (so that multiple replacements can be made in multi-stage image).
508+
func rewriteDockerfileForImages(dockerfilePath string, build *buildapiv1.Build) ([]string, bool, error) {
509+
if len(dockerfilePath) == 0 {
510+
return nil, false, nil
511+
}
512+
node, err := parseDockerfile(dockerfilePath)
513+
if err != nil {
514+
return nil, false, err
515+
}
516+
names := make(map[string]string)
517+
images := sets.NewString()
518+
stages := imagebuilder.NewStages(node, imagebuilder.NewBuilder(nil))
519+
for _, stage := range stages {
520+
for _, child := range stage.Node.Children {
521+
switch {
522+
case child.Value == dockercmd.From && child.Next != nil:
523+
image := child.Next.Value
524+
names[stage.Name] = image
525+
images.Insert(image)
526+
case child.Value == dockercmd.Copy:
527+
if ref, ok := nodeHasFromRef(child); ok {
528+
if len(ref) > 0 {
529+
if image, ok := names[ref]; !ok {
530+
images.Insert(image)
531+
}
532+
}
533+
}
534+
}
535+
}
536+
}
537+
return images.List(), len(stages) > 0, nil
538+
}
539+
540+
func nodeHasFromRef(node *parser.Node) (string, bool) {
541+
for _, arg := range node.Flags {
542+
switch {
543+
case strings.HasPrefix(arg, "--from="):
544+
from := strings.TrimPrefix(arg, "--from=")
545+
return from, true
546+
}
547+
}
548+
return "", false
549+
}

pkg/build/builder/dockerutil.go

+20-6
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ func buildDirectImage(dir string, ignoreFailures bool, opts *docker.BuildImageOp
223223
MemorySwap: opts.Memswap,
224224
}
225225

226+
if len(opts.BuildBinds) > 0 {
227+
var s []string
228+
if err := json.Unmarshal([]byte(opts.BuildBinds), s); err != nil {
229+
return fmt.Errorf("the build bindings were not a valid string array: %v", err)
230+
}
231+
e.HostConfig.Binds = append(e.HostConfig.Binds, s...)
232+
}
233+
226234
e.Out, e.ErrOut = opts.OutputStream, opts.OutputStream
227235

228236
// use a keyring
@@ -288,13 +296,19 @@ func buildDirectImage(dir string, ignoreFailures bool, opts *docker.BuildImageOp
288296
if err != nil {
289297
return err
290298
}
291-
if err := e.Prepare(b, node, ""); err != nil {
292-
return err
293-
}
294-
if err := e.Execute(b, node); err != nil {
295-
return err
299+
stages := imagebuilder.NewStages(node, b)
300+
var stageExecutor *dockerclient.ClientExecutor
301+
for _, stage := range stages {
302+
stageExecutor = e.WithName(stage.Name)
303+
if err := stageExecutor.Prepare(stage.Builder, stage.Node, ""); err != nil {
304+
return err
305+
}
306+
if err := stageExecutor.Execute(stage.Builder, stage.Node); err != nil {
307+
return err
308+
}
296309
}
297-
return e.Commit(b)
310+
return stageExecutor.Commit(stages[len(stages)-1].Builder)
311+
298312
})
299313
}
300314

pkg/build/builder/util_darwin.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package builder
2+
3+
import (
4+
s2iapi "github.com/openshift/source-to-image/pkg/api"
5+
)
6+
7+
// getContainerNetworkConfig determines whether the builder is running as a container
8+
// by examining /proc/self/cgroup. This context is then passed to source-to-image.
9+
// It returns a suitable argument for NetworkMode. If the container platform is
10+
// CRI-O, it also returns a path for /etc/resolv.conf, suitable for bindmounting.
11+
func getContainerNetworkConfig() (string, string, error) {
12+
return "", "", nil
13+
}
14+
15+
// GetCGroupLimits returns a struct populated with cgroup limit values gathered
16+
// from the local /sys/fs/cgroup filesystem. Overflow values are set to
17+
// math.MaxInt64.
18+
func GetCGroupLimits() (*s2iapi.CGroupLimits, error) {
19+
return &s2iapi.CGroupLimits{CPUShares: 1024}, nil
20+
}
21+
22+
// getCgroupParent determines the parent cgroup for a container from
23+
// within that container.
24+
func getCgroupParent() (string, error) {
25+
return "", nil
26+
}

pkg/build/builder/util_linux.go

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// +build linux
2-
31
package builder
42

53
import (

pkg/build/builder/util_unsupported.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// +build !linux
1+
// +build !linux,!darwin
22

33
package builder
44

0 commit comments

Comments
 (0)