sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/hack/prowimagebuilder/main.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bufio" 21 "context" 22 "flag" 23 "fmt" 24 "io" 25 "os" 26 "os/exec" 27 "path" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/sirupsen/logrus" 34 "sigs.k8s.io/prow/pkg/flagutil" 35 "sigs.k8s.io/yaml" 36 ) 37 38 const ( 39 defaultArch = "linux/amd64" 40 allArch = "all" 41 42 gatherStaicScriptName = "gather-static.sh" 43 44 // Relative to root of the repo 45 defaultProwImageListFile = ".prow-images.yaml" 46 47 defaultWorkersCount = 10 48 defaultRetry = 3 49 50 // noOpKoDocerRepo is used when images are not pushed 51 noOpKoDocerRepo = "ko.local" 52 ) 53 54 var ( 55 rootDir string 56 otherArches = []string{ 57 "linux/arm64", 58 "linux/s390x", 59 "linux/ppc64le", 60 } 61 defaultTags = []string{ 62 "latest", 63 "latest-root", 64 } 65 ) 66 67 func init() { 68 out, err := runCmd(nil, "git", "rev-parse", "--show-toplevel") 69 if err != nil { 70 logrus.WithError(err).Error("Failed getting git root dir") 71 os.Exit(1) 72 } 73 rootDir = out 74 75 if _, err := runCmdInDirFunc(path.Join(rootDir, "hack/tools"), nil, "go", "build", "-o", path.Join(rootDir, "_bin/ko"), "github.com/google/ko"); err != nil { 76 logrus.WithError(err).Error("Failed ensure ko") 77 os.Exit(1) 78 } 79 } 80 81 type options struct { 82 dockerRepo string 83 prowImageListFile string 84 images flagutil.Strings 85 workers int 86 push bool 87 maxRetry int 88 } 89 90 // Mock for unit testing purpose 91 var runCmdInDirFunc = runCmdInDir 92 93 func runCmdInDir(dir string, additionalEnv []string, cmd string, args ...string) (string, error) { 94 log := logrus.WithFields(logrus.Fields{"cmd": cmd, "args": args}) 95 command := exec.Command(cmd, args...) 96 if dir != "" { 97 command.Dir = dir 98 } 99 command.Env = append(os.Environ(), additionalEnv...) 100 stdOut, err := command.StdoutPipe() 101 if err != nil { 102 return "", err 103 } 104 stdErr, err := command.StderrPipe() 105 if err != nil { 106 return "", err 107 } 108 if err := command.Start(); err != nil { 109 return "", err 110 } 111 scanner := bufio.NewScanner(stdOut) 112 var allOut string 113 for scanner.Scan() { 114 out := scanner.Text() 115 allOut = allOut + out 116 logrus.WithField("cmd", command.Args).Info(out) 117 } 118 allErr, _ := io.ReadAll(stdErr) 119 err = command.Wait() 120 if len(allErr) > 0 { 121 if err != nil { 122 log.Error(string(allErr)) 123 } else { 124 log.Warn(string(allErr)) 125 } 126 } 127 return strings.TrimSpace(allOut), err 128 } 129 130 func runCmd(additionalEnv []string, cmd string, args ...string) (string, error) { 131 return runCmdInDirFunc(rootDir, additionalEnv, cmd, args...) 132 } 133 134 type imageDef struct { 135 Dir string `json:"dir"` 136 Arch string `json:"arch"` 137 remainingRetry int 138 } 139 140 type imageDefs struct { 141 Defs []imageDef `json:"images"` 142 } 143 144 func loadImageDefs(p string) ([]imageDef, error) { 145 b, err := os.ReadFile(p) 146 if err != nil { 147 return nil, err 148 } 149 var res imageDefs 150 if err := yaml.Unmarshal(b, &res); err != nil { 151 return nil, err 152 } 153 return res.Defs, nil 154 } 155 156 func allBaseTags() ([]string, error) { 157 gitTag, err := gitTag() 158 if err != nil { 159 return nil, err 160 } 161 // Add a `ko-<GIT_TAG>` tag so that it's easy to identify images built from 162 // ko vs. images built from bazel, in case there is a revert needed. 163 // TODO(chaodaiG): remove `ko-` tag once the images produced by ko proved to 164 // be working 165 return append(defaultTags, gitTag, "ko-"+gitTag), nil 166 } 167 168 func allTags(arch string) ([]string, error) { 169 baseTags, err := allBaseTags() 170 if err != nil { 171 return nil, err 172 } 173 174 var allTags = baseTags 175 for _, otherArch := range otherArches { 176 if arch != allArch && arch != otherArch { 177 continue 178 } 179 for _, base := range baseTags { 180 // So far only platform supported is linux, trimming off the linux/ 181 // prefix so that there is no slash in tag. Also for consistency reasons. 182 platform := strings.Replace(otherArch, "linux/", "", 1) 183 allTags = append(allTags, fmt.Sprintf("%s-%s", base, platform)) 184 } 185 } 186 return allTags, nil 187 } 188 189 var datePrefix string 190 191 // gitTag returns YYYYMMDD-<GIT_TAG> 192 // In order to ensure a consistent date value across the runtime of this process 193 // when run near midnight UTC, we cache the value of date. 194 // We don't cache the git SHA since a change in that would be meaningful. 195 func gitTag() (string, error) { 196 var err error 197 if datePrefix == "" { 198 if datePrefix, err = runCmd(nil, "date", "+v%Y%m%d"); err != nil { 199 return "", err 200 } 201 } 202 postfix, err := runCmd(nil, "git", "describe", "--always", "--dirty") 203 if err != nil { 204 return "", err 205 } 206 return fmt.Sprintf("%s-%s", datePrefix, postfix), nil 207 } 208 209 func runGatherStaticScript(id *imageDef, args ...string) error { 210 script := path.Join(rootDir, id.Dir, gatherStaicScriptName) 211 if _, err := os.Lstat(script); err != nil { 212 if !os.IsNotExist(err) { 213 return err 214 } 215 return nil 216 } 217 if _, err := runCmd(nil, script, args...); err != nil { 218 return err 219 } 220 return nil 221 } 222 223 func setup(id *imageDef) error { 224 return runGatherStaticScript(id) 225 } 226 227 func teardown(id *imageDef) error { 228 return runGatherStaticScript(id, "--cleanup") 229 } 230 231 func buildAndPush(id *imageDef, dockerRepos []string, push bool) error { 232 logger := logrus.WithField("image", id.Dir) 233 logger.Info("Build and push") 234 start := time.Now() 235 defer func(logger *logrus.Entry, start time.Time) { 236 logger.WithField("duration", time.Since(start).String()).Info("Duration of image building.") 237 }(logger, start) 238 // So far only supports certain arch 239 isSupportedArch := (id.Arch == defaultArch || id.Arch == allArch) 240 for _, otherArch := range otherArches { 241 if id.Arch == otherArch { 242 isSupportedArch = true 243 } 244 } 245 if !isSupportedArch { 246 return fmt.Errorf("Arch '%s' not supported, only support %v", id.Arch, append([]string{defaultArch, allArch}, otherArches...)) 247 } 248 publishArgs := []string{"publish", fmt.Sprintf("--tarball=_bin/%s.tar", path.Base(id.Dir)), "--push=false"} 249 if push { 250 publishArgs = []string{"publish", "--push=true"} 251 } 252 tags, err := allTags(id.Arch) 253 if err != nil { 254 return fmt.Errorf("collecting tags: %w", err) 255 } 256 for _, tag := range tags { 257 publishArgs = append(publishArgs, fmt.Sprintf("--tags=%s", tag)) 258 } 259 publishArgs = append(publishArgs, "--base-import-paths", "--platform="+id.Arch, "./"+id.Dir) 260 261 defer teardown(id) 262 if err := setup(id); err != nil { 263 return fmt.Errorf("setup: %w", err) 264 } 265 // ko only supports a single docker repo at a time; we run ko repeatedly 266 // against different docker repos to support pushing to multiple docker 267 // repos. This process utilizes the built-in cache of ko, so that pushing 268 // to subsequent identical docker repo(s) is relatively cheap. 269 for _, dockerRepo := range dockerRepos { 270 logger.WithField("args", publishArgs).Info("Running ko.") 271 if _, err = runCmd([]string{"KO_DOCKER_REPO=" + dockerRepo}, "_bin/ko", publishArgs...); err != nil { 272 return fmt.Errorf("running ko: %w", err) 273 } 274 } 275 return nil 276 } 277 278 func (o *options) imageAllowed(image string) bool { 279 return len(o.images.Strings()) == 0 || o.images.StringSet().Has(image) 280 } 281 282 func main() { 283 var o options 284 flag.StringVar(&o.prowImageListFile, "prow-images-file", path.Join(rootDir, defaultProwImageListFile), "Yaml file contains list of prow images") 285 flag.Var(&o.images, "image", "Images to be built, must be part of --prow-images-file, can be passed in repeatedly") 286 flag.StringVar(&o.dockerRepo, "ko-docker-repo", os.Getenv("KO_DOCKER_REPO"), "Dockers repos, separated by comma") 287 flag.IntVar(&o.workers, "workers", defaultWorkersCount, "Number of workers in parallel") 288 flag.BoolVar(&o.push, "push", false, "whether push or not") 289 flag.IntVar(&o.maxRetry, "retry", defaultRetry, "Number of times retrying for each image") 290 flag.Parse() 291 292 if !o.push && o.dockerRepo == "" { 293 o.dockerRepo = noOpKoDocerRepo 294 } 295 // By default ensures timestamp of images, ref: 296 // https://github.com/google/ko#why-are-my-images-all-created-in-1970 297 if err := os.Setenv("SOURCE_DATE_EPOCH", strconv.Itoa(int(time.Now().Unix()))); err != nil { 298 logrus.WithError(err).Error("Failed setting SOURCE_DATE_EPOCH") 299 os.Exit(1) 300 } 301 302 // Set VERSION for embedding versions with go build 303 gitTag, err := gitTag() 304 if err != nil { 305 logrus.WithError(err).Error("Failed get git tag") 306 os.Exit(1) 307 } 308 if err := os.Setenv("VERSION", gitTag); err != nil { 309 logrus.WithError(err).Error("Failed setting VERSION") 310 os.Exit(1) 311 } 312 313 ids, err := loadImageDefs(o.prowImageListFile) 314 if err != nil { 315 logrus.WithError(err).WithField("prow-image-file", o.prowImageListFile).Error("Failed loading") 316 os.Exit(1) 317 } 318 319 var wg sync.WaitGroup 320 imageChan := make(chan imageDef, 10) 321 errChan := make(chan error, len(ids)) 322 doneChan := make(chan imageDef, len(ids)) 323 // Start workers 324 ctx, cancel := context.WithCancel(context.Background()) 325 defer cancel() 326 for i := 0; i < o.workers; i++ { 327 go func(ctx context.Context, imageChan chan imageDef, errChan chan error, doneChan chan imageDef) { 328 for { 329 select { 330 case id := <-imageChan: 331 err := buildAndPush(&id, strings.Split(o.dockerRepo, ","), o.push) 332 if err != nil { 333 if id.remainingRetry > 0 { 334 // Let another routine handle this, better luck maybe? 335 id.remainingRetry-- 336 imageChan <- id 337 // Don't call wg.Done() as we are not done yet 338 continue 339 } 340 errChan <- fmt.Errorf("building image for %s failed: %w", id.Dir, err) 341 } 342 doneChan <- id 343 case <-ctx.Done(): 344 return 345 } 346 } 347 }(ctx, imageChan, errChan, doneChan) 348 } 349 350 var targetImagesCount int 351 for _, id := range ids { 352 id := id 353 if !o.imageAllowed(id.Dir) { 354 logrus.WithFields(logrus.Fields{"allowed-images": o.images, "image": id.Dir}).Info("Skipped.") 355 continue 356 } 357 id.remainingRetry = o.maxRetry 358 if id.Arch == "" { 359 id.Arch = defaultArch 360 } 361 // Feed into channel instead 362 wg.Add(1) 363 imageChan <- id 364 targetImagesCount++ 365 } 366 367 // This is used for testing images building, let's make sure it does something. 368 if targetImagesCount == 0 { 369 logrus.Error("There is no image to build.") 370 os.Exit(1) 371 } 372 373 go func(ctx context.Context, wg *sync.WaitGroup, doneChan chan imageDef) { 374 var done int 375 for { 376 select { 377 case id := <-doneChan: 378 done++ 379 logrus.WithFields(logrus.Fields{"image": id.Dir, "done": done, "total": targetImagesCount}).Info("Done with image.") 380 wg.Done() 381 case <-ctx.Done(): 382 return 383 } 384 } 385 }(ctx, &wg, doneChan) 386 387 wg.Wait() 388 for { 389 select { 390 case err := <-errChan: 391 logrus.WithError(err).Error("Failed.") 392 os.Exit(1) 393 default: 394 return 395 } 396 } 397 }