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 }