istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/docker-builder/crane.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"path"
    21  	"path/filepath"
    22  	"time"
    23  
    24  	"golang.org/x/sync/errgroup"
    25  
    26  	"istio.io/istio/pkg/log"
    27  	testenv "istio.io/istio/pkg/test/env"
    28  	"istio.io/istio/pkg/tracing"
    29  	"istio.io/istio/pkg/util/sets"
    30  	"istio.io/istio/tools/docker-builder/builder"
    31  	"istio.io/istio/tools/docker-builder/dockerfile"
    32  )
    33  
    34  // RunCrane builds docker images using go-containerregistry, rather than relying on Docker. This
    35  // works by parsing each Dockerfile and determining the resulting image config (labels, entrypoint,
    36  // env vars, etc) as well as all files that should be copied in. Notably, RUN is not supported. This
    37  // is not a problem for Istio, as all of our images do any building outside the docker context
    38  // anyway.
    39  //
    40  // Once we have determined the config, we use the go-containerregistry to apply this config on top of
    41  // the configured base image, and add a new layer for all the copies. This layer is constructed in a
    42  // highly optimized manner - rather than copying things around from original source, to docker
    43  // staging folder, to docker context, to a tar file, etc, we directly read the original source files
    44  // into memory and stream them into an in memory tar buffer.
    45  //
    46  // Building in this way ends up being roughly 10x faster than docker. Future work to enable
    47  // sha256-simd (https://github.com/google/go-containerregistry/issues/1330) makes this even faster -
    48  // pushing all images drops to sub-second times, with the registry being the bottleneck (which could
    49  // also use sha256-simd possibly).
    50  func RunCrane(ctx context.Context, a Args) error {
    51  	ctx, span := tracing.Start(ctx, "RunCrane")
    52  	defer span.End()
    53  	g := errgroup.Group{}
    54  
    55  	variants := sets.New(a.Variants...)
    56  	// hasDoubleDefault checks if we defined both DefaultVariant and PrimaryVariant. If we did, these
    57  	// are the same exact docker build, just requesting different tags. As an optimization, and to ensure
    58  	// byte-for-byte identical images, we will collapse these into a single build with multiple tags.
    59  	hasDoubleDefault := variants.Contains(DefaultVariant) && variants.Contains(PrimaryVariant)
    60  
    61  	// First, construct our build plan. Doing this first allows us to figure out which base images we will need,
    62  	// so we can pull them in the background
    63  	builds := []builder.BuildSpec{}
    64  	bases := sets.New[string]()
    65  	for _, v := range a.Variants {
    66  		for _, t := range a.Targets {
    67  			b := builder.BuildSpec{
    68  				Name:  t,
    69  				Dests: extractTags(a, t, v, hasDoubleDefault),
    70  			}
    71  			for _, arch := range a.Architectures {
    72  				p := a.PlanFor(arch).Find(t)
    73  				if p == nil {
    74  					continue
    75  				}
    76  				df := p.Dockerfile
    77  				dargs := createArgs(a, t, v, arch)
    78  				args, err := dockerfile.Parse(df, dockerfile.WithArgs(dargs), dockerfile.IgnoreRuns())
    79  				if err != nil {
    80  					return fmt.Errorf("parse: %v", err)
    81  				}
    82  				args.Arch = arch
    83  				args.Name = t
    84  				// args.Files provides a mapping from final destination -> docker context source
    85  				// docker context is virtual, so we need to rewrite the "docker context source" to the real path of disk
    86  				plan := a.PlanFor(arch).Find(args.Name)
    87  				if plan == nil {
    88  					continue
    89  				}
    90  				// Plan is a list of real file paths, but we don't have a strong mapping from "docker context source"
    91  				// to "real path on disk". We do have a weak mapping though, by reproducing docker-copy.sh
    92  				for dest, src := range args.Files {
    93  					translated, err := translate(plan.Dependencies(), src)
    94  					if err != nil {
    95  						return err
    96  					}
    97  					args.Files[dest] = translated
    98  				}
    99  				bases.Insert(args.Base)
   100  				b.Args = append(b.Args, args)
   101  			}
   102  			builds = append(builds, b)
   103  		}
   104  	}
   105  
   106  	// Warm up our base images while we are building everything. This isn't pulling them -- we actually
   107  	// never pull them -- but it is pulling the config file from the remote registry.
   108  	builder.WarmBase(ctx, a.Architectures, sets.SortedList(bases)...)
   109  
   110  	// Build all dependencies
   111  	makeStart := time.Now()
   112  	for _, arch := range a.Architectures {
   113  		if err := RunMake(ctx, a, arch, a.PlanFor(arch).Targets()...); err != nil {
   114  			return err
   115  		}
   116  	}
   117  	log.WithLabels("runtime", time.Since(makeStart)).Infof("make complete")
   118  
   119  	// Finally, construct images and push them
   120  	dockerStart := time.Now()
   121  	for _, b := range builds {
   122  		b := b
   123  		g.Go(func() error {
   124  			if err := builder.Build(ctx, b); err != nil {
   125  				return fmt.Errorf("build %v: %v", b.Name, err)
   126  			}
   127  			return nil
   128  		})
   129  	}
   130  	if err := g.Wait(); err != nil {
   131  		return err
   132  	}
   133  	log.WithLabels("runtime", time.Since(dockerStart)).Infof("images complete")
   134  	return nil
   135  }
   136  
   137  // translate takes a "docker context path" and a list of real paths and finds the real path for the associated
   138  // docker context path. Example: translate([out/linux_amd64/binary, foo/other], amd64/binary) -> out/linux_amd64/binary
   139  func translate(plan []string, src string) (string, error) {
   140  	src = filepath.Clean(src)
   141  	base := filepath.Base(src)
   142  
   143  	// TODO: this currently doesn't handle multi-arch
   144  	// Likely docker.yaml should explicitly declare multi-arch targets
   145  	for _, p := range plan {
   146  		pb := filepath.Base(p)
   147  		if pb == base {
   148  			return absPath(p), nil
   149  		}
   150  	}
   151  
   152  	// Next check for folder. This should probably be arbitrary depth
   153  	// Example: plan=[certs], src=certs/foo.bar
   154  	dir := filepath.Dir(src)
   155  	for _, p := range plan {
   156  		pb := filepath.Base(p)
   157  		if pb == dir {
   158  			return absPath(filepath.Join(p, base)), nil
   159  		}
   160  	}
   161  
   162  	return "", fmt.Errorf("failed to find real source for %v. plan: %+v", src, plan)
   163  }
   164  
   165  func absPath(p string) string {
   166  	if path.IsAbs(p) {
   167  		return p
   168  	}
   169  	return path.Join(testenv.IstioSrc, p)
   170  }