istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/docker-builder/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 builder
    16  
    17  import (
    18  	"bytes"
    19  	"compress/gzip"
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/google/go-containerregistry/pkg/authn"
    28  	"github.com/google/go-containerregistry/pkg/name"
    29  	"github.com/google/go-containerregistry/pkg/registry"
    30  	v1 "github.com/google/go-containerregistry/pkg/v1"
    31  	"github.com/google/go-containerregistry/pkg/v1/empty"
    32  	"github.com/google/go-containerregistry/pkg/v1/mutate"
    33  	"github.com/google/go-containerregistry/pkg/v1/remote"
    34  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    35  	"github.com/google/go-containerregistry/pkg/v1/types"
    36  
    37  	"istio.io/istio/pkg/log"
    38  	"istio.io/istio/pkg/tracing"
    39  )
    40  
    41  type BuildSpec struct {
    42  	// Name is optional, for logging
    43  	Name  string
    44  	Dests []string
    45  	Args  []Args
    46  }
    47  
    48  type Args struct {
    49  	// Name is optional, for logging
    50  	Name string
    51  
    52  	// Image architecture. Required only when multiple images present
    53  	Arch string
    54  
    55  	Env        map[string]string
    56  	Labels     map[string]string
    57  	User       string
    58  	WorkDir    string
    59  	Entrypoint []string
    60  	Cmd        []string
    61  
    62  	// Base image to use
    63  	Base string
    64  
    65  	// Files contains all files, mapping destination path -> source path
    66  	Files map[string]string
    67  	// FilesBase is the base path for absolute paths in Files
    68  	FilesBase string
    69  }
    70  
    71  type baseKey struct {
    72  	arch string
    73  	name string
    74  }
    75  
    76  var (
    77  	bases   = map[baseKey]v1.Image{}
    78  	basesMu sync.RWMutex
    79  )
    80  
    81  func WarmBase(ctx context.Context, architectures []string, baseImages ...string) {
    82  	_, span := tracing.Start(ctx, "RunCrane")
    83  	defer span.End()
    84  	basesMu.Lock()
    85  	wg := sync.WaitGroup{}
    86  	wg.Add(len(baseImages) * len(architectures))
    87  	resolvedBaseImages := make([]v1.Image, len(baseImages)*len(architectures))
    88  	keys := []baseKey{}
    89  	for _, a := range architectures {
    90  		for _, b := range baseImages {
    91  			keys = append(keys, baseKey{toPlatform(a).Architecture, b})
    92  		}
    93  	}
    94  	go func() {
    95  		wg.Wait()
    96  		for i, rbi := range resolvedBaseImages {
    97  			bases[keys[i]] = rbi
    98  		}
    99  		basesMu.Unlock()
   100  	}()
   101  
   102  	t0 := time.Now()
   103  	for i, b := range keys {
   104  		b, i := b, i
   105  		go func() {
   106  			defer wg.Done()
   107  			ref, err := name.ParseReference(b.name)
   108  			if err != nil {
   109  				log.WithLabels("image", b).Warnf("base failed: %v", err)
   110  				return
   111  			}
   112  			plat := v1.Platform{
   113  				Architecture: b.arch,
   114  				OS:           "linux",
   115  			}
   116  			bi, err := remote.Image(ref, remote.WithPlatform(plat), remote.WithProgress(CreateProgress(fmt.Sprintf("base %v", ref))))
   117  			if err != nil {
   118  				log.WithLabels("image", b).Warnf("base failed: %v", err)
   119  				return
   120  			}
   121  			log.WithLabels("image", b, "step", time.Since(t0)).Infof("base loaded")
   122  			resolvedBaseImages[i] = bi
   123  		}()
   124  	}
   125  }
   126  
   127  func ByteCount(b int64) string {
   128  	const unit = 1000
   129  	if b < unit {
   130  		return fmt.Sprintf("%dB", b)
   131  	}
   132  	div, exp := int64(unit), 0
   133  	for n := b / unit; n >= unit; n /= unit {
   134  		div *= unit
   135  		exp++
   136  	}
   137  	return fmt.Sprintf("%.1f%cB",
   138  		float64(b)/float64(div), "kMGTPE"[exp])
   139  }
   140  
   141  func Build(ctx context.Context, b BuildSpec) error {
   142  	ctx, span := tracing.Start(ctx, "Build")
   143  	defer span.End()
   144  	t0 := time.Now()
   145  	lt := t0
   146  	trace := func(format string, d ...any) {
   147  		log.WithLabels("image", b.Name, "total", time.Since(t0), "step", time.Since(lt)).Infof(format, d...)
   148  		lt = time.Now()
   149  	}
   150  	if len(b.Dests) == 0 {
   151  		return fmt.Errorf("dest required")
   152  	}
   153  
   154  	// Over localhost, compression CPU can be the bottleneck. With remotes, compressing usually saves a lot of time.
   155  	compression := gzip.NoCompression
   156  	for _, d := range b.Dests {
   157  		if !strings.HasPrefix(d, "localhost") {
   158  			compression = gzip.BestSpeed
   159  			break
   160  		}
   161  	}
   162  
   163  	var images []v1.Image
   164  	for _, args := range b.Args {
   165  		plat := toPlatform(args.Arch)
   166  		baseImage := empty.Image
   167  		if args.Base != "" {
   168  			basesMu.RLock()
   169  			baseImage = bases[baseKey{arch: plat.Architecture, name: args.Base}] // todo per-arch base
   170  			basesMu.RUnlock()
   171  		}
   172  		if baseImage == nil {
   173  			log.Warnf("on demand loading base image %q", args.Base)
   174  			ref, err := name.ParseReference(args.Base)
   175  			if err != nil {
   176  				return err
   177  			}
   178  			bi, err := remote.Image(
   179  				ref,
   180  				remote.WithPlatform(plat),
   181  				remote.WithProgress(CreateProgress(fmt.Sprintf("base %v", ref))),
   182  			)
   183  			if err != nil {
   184  				return err
   185  			}
   186  			baseImage = bi
   187  		}
   188  		trace("create base")
   189  
   190  		cfgFile, err := baseImage.ConfigFile()
   191  		if err != nil {
   192  			return err
   193  		}
   194  
   195  		trace("base config")
   196  
   197  		// Set our platform on the image. This is largely for empty.Image only; others should already have correct default.
   198  		cfgFile = cfgFile.DeepCopy()
   199  		cfgFile.OS = plat.OS
   200  		cfgFile.Architecture = plat.Architecture
   201  
   202  		cfg := cfgFile.Config
   203  		for k, v := range args.Env {
   204  			cfg.Env = append(cfg.Env, fmt.Sprintf("%v=%v", k, v))
   205  		}
   206  		if args.User != "" {
   207  			cfg.User = args.User
   208  		}
   209  		if len(args.Entrypoint) > 0 {
   210  			cfg.Entrypoint = args.Entrypoint
   211  			cfg.Cmd = nil
   212  		}
   213  		if len(args.Cmd) > 0 {
   214  			cfg.Cmd = args.Cmd
   215  			cfg.Entrypoint = nil
   216  		}
   217  		if args.WorkDir != "" {
   218  			cfg.WorkingDir = args.WorkDir
   219  		}
   220  		if len(args.Labels) > 0 && cfg.Labels == nil {
   221  			cfg.Labels = map[string]string{}
   222  		}
   223  		for k, v := range args.Labels {
   224  			cfg.Labels[k] = v
   225  		}
   226  
   227  		updated, err := mutate.Config(baseImage, cfg)
   228  		if err != nil {
   229  			return err
   230  		}
   231  		trace("config")
   232  
   233  		// Pre-allocated 100mb
   234  		// TODO: cache the size of images, use exactish size
   235  		buf := bytes.NewBuffer(make([]byte, 0, 100*1024*1024))
   236  		if err := WriteArchiveFromFiles(args.FilesBase, args.Files, buf); err != nil {
   237  			return err
   238  		}
   239  		sz := ByteCount(int64(buf.Len()))
   240  
   241  		l, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
   242  			return io.NopCloser(bytes.NewReader(buf.Bytes())), nil
   243  		}, tarball.WithCompressionLevel(compression))
   244  		if err != nil {
   245  			return err
   246  		}
   247  		trace("read layer of size %v", sz)
   248  
   249  		image, err := mutate.AppendLayers(updated, l)
   250  		if err != nil {
   251  			return err
   252  		}
   253  
   254  		trace("layer")
   255  		images = append(images, image)
   256  	}
   257  
   258  	// Write Remote
   259  
   260  	err := writeImage(ctx, b, images, trace)
   261  	if err != nil {
   262  		return err
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  func writeImage(ctx context.Context, b BuildSpec, images []v1.Image, trace func(format string, d ...any)) error {
   269  	_, span := tracing.Start(ctx, "Write")
   270  	defer span.End()
   271  	var artifact remote.Taggable
   272  	if len(images) == 1 {
   273  		// Single image, just push that
   274  		artifact = images[0]
   275  	} else {
   276  		// Multiple, we need to create an index
   277  		var manifest v1.ImageIndex = empty.Index
   278  		manifest = mutate.IndexMediaType(manifest, types.DockerManifestList)
   279  		for idx, i := range images {
   280  			img := i
   281  			mt, err := img.MediaType()
   282  			if err != nil {
   283  				return fmt.Errorf("failed to get mediatype: %w", err)
   284  			}
   285  
   286  			h, err := img.Digest()
   287  			if err != nil {
   288  				return fmt.Errorf("failed to compute digest: %w", err)
   289  			}
   290  
   291  			size, err := img.Size()
   292  			if err != nil {
   293  				return fmt.Errorf("failed to compute size: %w", err)
   294  			}
   295  			plat := toPlatform(b.Args[idx].Arch)
   296  			manifest = mutate.AppendManifests(manifest, mutate.IndexAddendum{
   297  				Add: i,
   298  				Descriptor: v1.Descriptor{
   299  					MediaType: mt,
   300  					Size:      size,
   301  					Digest:    h,
   302  					Platform:  &plat,
   303  				},
   304  			})
   305  		}
   306  		artifact = manifest
   307  	}
   308  
   309  	// MultiWrite takes a Reference -> Taggable, but won't handle writing to multiple Repos. So we keep
   310  	// a map of Repository -> MultiWrite args.
   311  	remoteTargets := map[name.Repository]map[name.Reference]remote.Taggable{}
   312  
   313  	for _, dest := range b.Dests {
   314  		destRef, err := name.ParseReference(dest)
   315  		if err != nil {
   316  			return err
   317  		}
   318  		repo := destRef.Context()
   319  		if remoteTargets[repo] == nil {
   320  			remoteTargets[repo] = map[name.Reference]remote.Taggable{}
   321  		}
   322  		remoteTargets[repo][destRef] = artifact
   323  	}
   324  
   325  	for repo, mw := range remoteTargets {
   326  		prog := CreateProgress(fmt.Sprintf("upload %v", repo.String()))
   327  		if err := remote.MultiWrite(mw, remote.WithProgress(prog), remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil {
   328  			return err
   329  		}
   330  		s := repo.String()
   331  		if len(mw) == 1 {
   332  			for tag := range mw {
   333  				s = tag.String()
   334  			}
   335  		}
   336  		trace("upload %v", s)
   337  	}
   338  	return nil
   339  }
   340  
   341  func toPlatform(archString string) v1.Platform {
   342  	os, arch, _ := strings.Cut(archString, "/")
   343  	return v1.Platform{
   344  		Architecture: arch,
   345  		OS:           os,
   346  		Variant:      "", // TODO?
   347  	}
   348  }
   349  
   350  func CreateProgress(name string) chan v1.Update {
   351  	updates := make(chan v1.Update, 1000)
   352  	go func() {
   353  		lastLog := time.Time{}
   354  		for u := range updates {
   355  			if time.Since(lastLog) < time.Second && !(u.Total == u.Complete) {
   356  				// Limit to 1 log per-image per-second, unless it is the final log
   357  				continue
   358  			}
   359  			if u.Total == 0 {
   360  				continue
   361  			}
   362  			lastLog = time.Now()
   363  			log.WithLabels("action", name).Infof("progress %s/%s", ByteCount(u.Complete), ByteCount(u.Total))
   364  		}
   365  	}()
   366  	return updates
   367  }
   368  
   369  func Experiment() {
   370  	registry.New()
   371  }