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 }