Skip to content

Commit d4dd228

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 73710d4 commit d4dd228

File tree

14 files changed

+799
-292
lines changed

14 files changed

+799
-292
lines changed

pkg/build/builder/common.go

+130-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package builder
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/sha1"
67
"encoding/json"
@@ -9,28 +10,32 @@ import (
910
"math/rand"
1011
"os"
1112
"path/filepath"
13+
"sort"
14+
"strings"
1215
"time"
1316

1417
"k8s.io/apimachinery/pkg/api/errors"
1518
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/util/sets"
1620
"k8s.io/apimachinery/pkg/util/wait"
1721

1822
"github.com/docker/distribution/reference"
23+
dockercmd "github.com/docker/docker/builder/dockerfile/command"
24+
"github.com/docker/docker/builder/dockerfile/parser"
1925
"github.com/fsouza/go-dockerclient"
20-
26+
"github.com/openshift/imagebuilder"
2127
s2igit "github.com/openshift/source-to-image/pkg/scm/git"
2228
"github.com/openshift/source-to-image/pkg/util"
2329

2430
buildapiv1 "github.com/openshift/api/build/v1"
31+
buildclientv1 "github.com/openshift/client-go/build/clientset/versioned/typed/build/v1"
2532
"github.com/openshift/origin/pkg/build/builder/timing"
2633
builderutil "github.com/openshift/origin/pkg/build/builder/util"
2734
"github.com/openshift/origin/pkg/build/builder/util/dockerfile"
2835
buildutil "github.com/openshift/origin/pkg/build/util"
2936
"github.com/openshift/origin/pkg/git"
3037
imageapi "github.com/openshift/origin/pkg/image/apis/image"
3138
utilglog "github.com/openshift/origin/pkg/util/glog"
32-
33-
buildclientv1 "github.com/openshift/client-go/build/clientset/versioned/typed/build/v1"
3439
)
3540

3641
// glog is a placeholder until the builders pass an output stream down
@@ -302,8 +307,13 @@ func buildLabels(build *buildapiv1.Build, sourceInfo *git.SourceInfo) []dockerfi
302307
addBuildLabels(labels, build)
303308

304309
kv := make([]dockerfile.KeyValue, 0, len(labels)+len(build.Spec.Output.ImageLabels))
305-
for k, v := range labels {
306-
kv = append(kv, dockerfile.KeyValue{Key: k, Value: v})
310+
keys := make([]string, 0, len(labels))
311+
for k := range labels {
312+
keys = append(keys, k)
313+
}
314+
sort.Strings(keys)
315+
for _, k := range keys {
316+
kv = append(kv, dockerfile.KeyValue{Key: k, Value: labels[k]})
307317
}
308318
// override autogenerated labels with user provided labels
309319
for _, lbl := range build.Spec.Output.ImageLabels {
@@ -339,7 +349,12 @@ func readSourceInfo() (*git.SourceInfo, error) {
339349
// Also append the environment variables and labels in the Dockerfile.
340350
func addBuildParameters(dir string, build *buildapiv1.Build, sourceInfo *git.SourceInfo) error {
341351
dockerfilePath := getDockerfilePath(dir, build)
342-
node, err := parseDockerfile(dockerfilePath)
352+
353+
in, err := ioutil.ReadFile(dockerfilePath)
354+
if err != nil {
355+
return err
356+
}
357+
node, err := imagebuilder.ParseDockerfile(bytes.NewBuffer(in))
343358
if err != nil {
344359
return err
345360
}
@@ -358,29 +373,128 @@ func addBuildParameters(dir string, build *buildapiv1.Build, sourceInfo *git.Sou
358373
}
359374

360375
// Append build info as environment variables.
361-
err = appendEnv(node, buildEnv(build, sourceInfo))
362-
if err != nil {
376+
if err := appendEnv(node, buildEnv(build, sourceInfo)); err != nil {
363377
return err
364378
}
365379

366380
// Append build labels.
367-
err = appendLabel(node, buildLabels(build, sourceInfo))
368-
if err != nil {
381+
if err := appendLabel(node, buildLabels(build, sourceInfo)); err != nil {
369382
return err
370383
}
371384

372385
// Insert environment variables defined in the build strategy.
373-
err = insertEnvAfterFrom(node, build.Spec.Strategy.DockerStrategy.Env)
374-
if err != nil {
386+
if err := insertEnvAfterFrom(node, build.Spec.Strategy.DockerStrategy.Env); err != nil {
375387
return err
376388
}
377389

378-
instructions := dockerfile.ParseTreeToDockerfile(node)
390+
replaceImagesFromSource(node, build.Spec.Source.Images)
391+
392+
out := dockerfile.Write(node)
393+
glog.V(4).Infof("Replacing dockerfile\n%s\nwith:\n%s", string(in), string(out))
394+
return overwriteFile(dockerfilePath, out)
395+
}
396+
397+
// replaceImagesFromSource updates a single or multi-stage Dockerfile with any replacement
398+
// image sources ('FROM <name>' and 'COPY --from=<name>'). It operates on exact string matches
399+
// and performs no interpretation of names from the Dockerfile.
400+
func replaceImagesFromSource(node *parser.Node, imageSources []buildapiv1.ImageSource) {
401+
replacements := make(map[string]string)
402+
for _, image := range imageSources {
403+
if image.From.Kind != "DockerImage" || len(image.From.Name) == 0 {
404+
continue
405+
}
406+
for _, name := range image.As {
407+
replacements[name] = image.From.Name
408+
}
409+
}
410+
names := make(map[string]string)
411+
stages := imagebuilder.NewStages(node, imagebuilder.NewBuilder(nil))
412+
for _, stage := range stages {
413+
for _, child := range stage.Node.Children {
414+
switch {
415+
case child.Value == dockercmd.From && child.Next != nil:
416+
image := child.Next.Value
417+
if replacement, ok := replacements[image]; ok {
418+
child.Next.Value = replacement
419+
}
420+
names[stage.Name] = image
421+
case child.Value == dockercmd.Copy:
422+
if ref, ok := nodeHasFromRef(child); ok {
423+
if len(ref) > 0 {
424+
if _, ok := names[ref]; !ok {
425+
if replacement, ok := replacements[ref]; ok {
426+
nodeReplaceFromRef(child, replacement)
427+
}
428+
}
429+
}
430+
}
431+
}
432+
}
433+
}
434+
}
435+
436+
// findReferencedImages returns all qualified images referenced by the Dockerfile, whether the
437+
// build is a multi-stage build, or returns an error.
438+
func findReferencedImages(dockerfilePath string) ([]string, bool, error) {
439+
if len(dockerfilePath) == 0 {
440+
return nil, false, nil
441+
}
442+
node, err := imagebuilder.ParseFile(dockerfilePath)
443+
if err != nil {
444+
return nil, false, err
445+
}
446+
names := make(map[string]string)
447+
images := sets.NewString()
448+
stages := imagebuilder.NewStages(node, imagebuilder.NewBuilder(nil))
449+
for _, stage := range stages {
450+
for _, child := range stage.Node.Children {
451+
switch {
452+
case child.Value == dockercmd.From && child.Next != nil:
453+
image := child.Next.Value
454+
names[stage.Name] = image
455+
images.Insert(image)
456+
case child.Value == dockercmd.Copy:
457+
if ref, ok := nodeHasFromRef(child); ok {
458+
if len(ref) > 0 {
459+
if _, ok := names[ref]; !ok {
460+
images.Insert(ref)
461+
}
462+
}
463+
}
464+
}
465+
}
466+
}
467+
return images.List(), len(stages) > 1, nil
468+
}
379469

380-
// Overwrite the Dockerfile.
381-
fi, err := os.Stat(dockerfilePath)
470+
func overwriteFile(name string, out []byte) error {
471+
f, err := os.OpenFile(name, os.O_TRUNC|os.O_WRONLY, 0)
382472
if err != nil {
383473
return err
384474
}
385-
return ioutil.WriteFile(dockerfilePath, instructions, fi.Mode())
475+
if _, err := f.Write(out); err != nil {
476+
f.Close()
477+
return err
478+
}
479+
return f.Close()
480+
}
481+
482+
func nodeHasFromRef(node *parser.Node) (string, bool) {
483+
for _, arg := range node.Flags {
484+
switch {
485+
case strings.HasPrefix(arg, "--from="):
486+
from := strings.TrimPrefix(arg, "--from=")
487+
return from, true
488+
}
489+
}
490+
return "", false
491+
}
492+
493+
func nodeReplaceFromRef(node *parser.Node, name string) {
494+
for i, arg := range node.Flags {
495+
switch {
496+
case strings.HasPrefix(arg, "--from="):
497+
node.Flags[i] = fmt.Sprintf("--from=%s", name)
498+
}
499+
}
386500
}

0 commit comments

Comments
 (0)