[BCID] Add update-references subcommand to build-images
This should allow us to use the same entrypoint for multiple stages of
the pipeline, which will simplify the information we have to carry over
between stages.
Change-Id: I33c5cb7b6d9fbd26afa31587566f3fe6b24c59a5
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/583742
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Eric Boren <borenet@google.com>
diff --git a/cd/go/build-images/BUILD.bazel b/cd/go/build-images/BUILD.bazel
index c03ef80..bb94c2b 100644
--- a/cd/go/build-images/BUILD.bazel
+++ b/cd/go/build-images/BUILD.bazel
@@ -3,14 +3,23 @@
go_library(
name = "build-images_lib",
- srcs = ["main.go"],
+ srcs = [
+ "build.go",
+ "main.go",
+ "update-refs.go",
+ ],
importpath = "go.skia.org/infra/cd/go/build-images",
visibility = ["//visibility:private"],
deps = [
- "//go/common",
"//go/exec",
+ "//go/gerrit/rubberstamper",
"//go/git",
+ "//go/gitauth",
+ "//go/skerr",
+ "//go/util",
+ "//task_driver/go/lib/git_steps",
"//task_driver/go/td",
+ "@com_github_urfave_cli_v2//:cli",
"@org_golang_x_sync//errgroup",
],
)
diff --git a/cd/go/build-images/build.go b/cd/go/build-images/build.go
new file mode 100644
index 0000000..d455f4e
--- /dev/null
+++ b/cd/go/build-images/build.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "path"
+ "strings"
+ "time"
+
+ "go.skia.org/infra/go/exec"
+ "go.skia.org/infra/go/gitauth"
+ "go.skia.org/infra/go/skerr"
+ "go.skia.org/infra/task_driver/go/lib/git_steps"
+ "go.skia.org/infra/task_driver/go/td"
+ "golang.org/x/sync/errgroup"
+)
+
+func build(ctx context.Context, commit, repo, workspace, username, email string, targets []string, rbe bool) error {
+ ctx = td.StartStep(ctx, td.Props("Build Images"))
+ defer td.EndStep(ctx)
+
+ // Initialize git authentication.
+ ts, err := git_steps.Init(ctx, true)
+ if err != nil {
+ return td.FailStep(ctx, err)
+ }
+ if _, err := gitauth.New(ts, "/tmp/.gitcookies", true, email); err != nil {
+ return td.FailStep(ctx, err)
+ }
+
+ bazelTargetToImagePath := make(map[string]string, len(targets))
+ for _, target := range targets {
+ targetSplit := strings.Split(target, ":")
+ if len(targetSplit) != 3 {
+ return td.FailStep(ctx, skerr.Fmt("Invalid target specification %q; expected \"//bazel-target:bazel-target:gcr.io/image/path\"", target))
+ }
+ bazelTarget := strings.Join(targetSplit[:2], ":")
+ imagePath := targetSplit[2]
+ bazelTargetToImagePath[bazelTarget] = imagePath
+ }
+
+ // Create a shallow clone of the repo.
+ checkoutDir, err := shallowClone(ctx, repo, commit)
+ if err != nil {
+ return td.FailStep(ctx, err)
+ }
+
+ // Create the timestamped Docker image tag.
+ timestamp := time.Now().UTC().Format("2006-01-02T15_04_05Z")
+ imageTag := fmt.Sprintf("%s-%s-%s-%s", timestamp, username, commit[:7], "clean")
+
+ // Perform the builds concurrently.
+ imageInfo := &buildImagesJSON{
+ Images: make([]SingleImageInfo, 0, len(bazelTargetToImagePath)),
+ }
+ eg, ctx := errgroup.WithContext(ctx)
+ for bazelTarget, imagePath := range bazelTargetToImagePath {
+ // https://golang.org/doc/faq#closures_and_goroutines
+ bazelTarget := bazelTarget
+ louhiImageTag := fmt.Sprintf("louhi_ws/%s:%s", imagePath, imageTag)
+ imageInfo.Images = append(imageInfo.Images, SingleImageInfo{
+ Image: imagePath,
+ Tag: louhiImageTag,
+ })
+ eg.Go(func() error {
+ return bazelRun(ctx, checkoutDir, bazelTarget, louhiImageTag, rbe)
+ })
+ }
+ if err := eg.Wait(); err != nil {
+ return td.FailStep(ctx, err)
+ }
+ return writeBuildImagesJSON(ctx, workspace, imageInfo)
+}
+
+// bazelTargetToDockerTag converts a Bazel target specification to a Docker
+// image tag which is applied to the image during the Bazel build.
+func bazelTargetToDockerTag(target string) string {
+ return path.Join("bazel", target)
+}
+
+// bazelRun executes `bazel run` for the given target and applies the given tag
+// to the resulting image.
+func bazelRun(ctx context.Context, cwd, target, louhiImageTag string, rbe bool) error {
+ ctx = td.StartStep(ctx, td.Props(fmt.Sprintf("Build %s", target)))
+ defer td.EndStep(ctx)
+
+ cmd := []string{"bazelisk", "run"}
+ if rbe {
+ cmd = append(cmd, "--config=remote", "--google_default_credentials")
+ }
+ cmd = append(cmd, target)
+ if _, err := exec.RunCwd(ctx, cwd, cmd...); err != nil {
+ return td.FailStep(ctx, err)
+ }
+ if _, err := exec.RunCwd(ctx, cwd, "docker", "tag", bazelTargetToDockerTag(target), louhiImageTag); err != nil {
+ return td.FailStep(ctx, err)
+ }
+ return nil
+}
diff --git a/cd/go/build-images/main.go b/cd/go/build-images/main.go
index caa7b40..cc20db0 100644
--- a/cd/go/build-images/main.go
+++ b/cd/go/build-images/main.go
@@ -1,35 +1,119 @@
package main
-import (
- "context"
- "flag"
- "fmt"
- "io/ioutil"
- "os/user"
- "path"
- "strings"
- "time"
-
- "go.skia.org/infra/go/common"
- "go.skia.org/infra/go/exec"
- "go.skia.org/infra/go/git"
- "go.skia.org/infra/task_driver/go/td"
- "golang.org/x/sync/errgroup"
-)
-
/*
build-images is used for building Skia Infrastructure Docker images using
Bazel. It is intended to run inside of Louhi as part of a CD pipeline.
*/
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/urfave/cli/v2"
+
+ "go.skia.org/infra/go/exec"
+ "go.skia.org/infra/go/git"
+ "go.skia.org/infra/go/util"
+ "go.skia.org/infra/task_driver/go/td"
+)
+
func main() {
- // Setup.
- commit := flag.String("commit", "", "Commit at which to build the image.")
- repo := flag.String("repo", "", "Repository URL.")
- workspace := flag.String("workspace", "", "Path to Louhi workspace.")
- targets := common.NewMultiStringFlag("target", nil, "Bazel target + image path pairs in the form \"//bazel-package:bazel-target:gcr.io/image/path\". ")
- rbe := flag.Bool("rbe", false, "Whether or not to use Bazel RBE.")
- username := flag.String("user", "", "User name to attribute the build. If not specified, attempt to determine automatically.")
+ const (
+ flagCommit = "commit"
+ flagRepo = "repo"
+ flagWorkspace = "workspace"
+ flagTarget = "target"
+ flagRBE = "rbe"
+ flagUser = "user"
+ flagEmail = "email"
+ )
+ app := &cli.App{
+ Name: "build-images",
+ Description: `build-images is used for building Skia Infrastructure Docker images using Bazel. It is intended to run inside of Louhi as part of a CD pipeline.`,
+ Commands: []*cli.Command{
+ {
+ Name: "build",
+ Description: "Build Docker images.",
+ Usage: "build-images <options>",
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: flagCommit,
+ Usage: "Commit at which to build the image(s).",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: flagRepo,
+ Usage: "Repository URL.",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: flagWorkspace,
+ Usage: "Path to Louhi workspace.",
+ Required: true,
+ },
+ &cli.StringSliceFlag{
+ Name: flagTarget,
+ Usage: "Bazel target + image path pairs in the form \"//bazel-package:bazel-target:gcr.io/image/path\".",
+ Required: true,
+ },
+ &cli.BoolFlag{
+ Name: flagRBE,
+ Usage: "Whether or not to use Bazel RBE",
+ Value: false,
+ },
+ &cli.StringFlag{
+ Name: flagUser,
+ Usage: "User name to attribute the build.",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: flagEmail,
+ Usage: "Email address to attribute the build.",
+ Required: true,
+ },
+ },
+ Action: func(ctx *cli.Context) error {
+ return build(ctx.Context, ctx.String(flagCommit), ctx.String(flagRepo), ctx.String(flagWorkspace), ctx.String(flagUser), ctx.String(flagEmail), ctx.StringSlice(flagTarget), ctx.Bool(flagRBE))
+ },
+ },
+ {
+ Name: "update-references",
+ Description: "Update references to the images we built.",
+ Usage: "update-references <options>",
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: flagRepo,
+ Usage: "Repository URL.",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: flagWorkspace,
+ Usage: "Path to Louhi workspace.",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: flagUser,
+ Usage: "User name to attribute the build.",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: flagEmail,
+ Usage: "Email address to attribute the build.",
+ Required: true,
+ },
+ },
+ Action: func(ctx *cli.Context) error {
+ return updateRefs(ctx.Context, ctx.String(flagRepo), ctx.String(flagWorkspace), ctx.String(flagUser), ctx.String(flagEmail))
+ },
+ },
+ },
+ Usage: "build-images <subcommand>",
+ }
// We're using the task driver framework because it provides logging and
// helpful insight into what's occurring as the program runs.
@@ -41,60 +125,61 @@
ctx := td.StartRun(&fakeProjectId, &fakeTaskId, &fakeTaskName, &output, &local)
defer td.EndRun(ctx)
- if *commit == "" {
- td.Fatalf(ctx, "--commit is required.")
- }
- if *repo == "" {
- td.Fatalf(ctx, "--repo is required.")
- }
- if *workspace == "" {
- td.Fatalf(ctx, "--workspace is required.")
- }
- if len(*targets) == 0 {
- td.Fatalf(ctx, "At least one --target is required.")
- }
- bazelTargetToImagePath := make(map[string]string, len(*targets))
- for _, target := range *targets {
- targetSplit := strings.Split(target, ":")
- if len(targetSplit) != 3 {
- td.Fatalf(ctx, "Invalid target specification %q; expected \"//bazel-target:bazel-target:gcr.io/image/path\"", target)
- }
- bazelTarget := strings.Join(targetSplit[:2], ":")
- imagePath := targetSplit[2]
- bazelTargetToImagePath[bazelTarget] = imagePath
- }
-
- // Create a shallow clone of the repo.
- checkoutDir, err := shallowClone(ctx, *repo, *commit)
- if err != nil {
+ // Run the app.
+ if err := app.RunContext(ctx, os.Args); err != nil {
td.Fatal(ctx, err)
}
+}
- // Create the timestamped Docker image tag.
- // 2022-09-21T13_13_46Z-louhi-02b6ac9-clean
- ts := time.Now().UTC().Format("2006-01-02T15_04_05Z")
- if *username == "" {
- userObj, err := user.Current() // TODO(borenet): Will this work in Louhi?
- if err != nil {
- td.Fatal(ctx, err)
- }
- *username = userObj.Username
- }
- imageTag := fmt.Sprintf("%s-%s-%s-%s", ts, *username, (*commit)[:7], "clean")
+const (
+ // buildImagesJSONFile persists information about the images built by this
+ // program between invocations. This is necessary because the push to the
+ // image repository must be performed using the built-in Louhi stage in
+ // order to create the attestation which can be used to verify the image.
+ // After that is done, we invoke build-images again to obtain the sha256
+ // sum for each image (which can only be done after the push) and update
+ // the image references in Git.
+ buildImagesJSONFile = "build-images.json"
+)
- // Perform the builds concurrently.
- eg, ctx := errgroup.WithContext(ctx)
- for bazelTarget, imagePath := range bazelTargetToImagePath {
- // https://golang.org/doc/faq#closures_and_goroutines
- bazelTarget := bazelTarget
- louhiImageTag := fmt.Sprintf("louhi_ws/%s:%s", imagePath, imageTag)
- eg.Go(func() error {
- return bazelRun(ctx, checkoutDir, bazelTarget, louhiImageTag, *rbe)
- })
+// buildImagesJSON describes the structure of buildImagesJSONFile.
+type buildImagesJSON struct {
+ Images []SingleImageInfo `json:"images"`
+}
+
+type SingleImageInfo struct {
+ Image string `json:"image"`
+ Tag string `json:"tag"`
+ Sha256 string `json:"sha256"`
+}
+
+// readBuildImagesJSON reads the buildImagesJSONFile.
+func readBuildImagesJSON(ctx context.Context, workspace string) (*buildImagesJSON, error) {
+ ctx = td.StartStep(ctx, td.Props(fmt.Sprintf("Read %s", buildImagesJSONFile)))
+ defer td.EndStep(ctx)
+
+ f := filepath.Join(workspace, buildImagesJSONFile)
+ var imageInfo buildImagesJSON
+ if err := util.WithReadFile(f, func(r io.Reader) error {
+ return json.NewDecoder(r).Decode(&imageInfo)
+ }); err != nil {
+ return nil, td.FailStep(ctx, err)
}
- if err := eg.Wait(); err != nil {
- td.Fatal(ctx, err)
+ return &imageInfo, nil
+}
+
+// writeBuildImagesJSON writes the buildImagesJSONFile.
+func writeBuildImagesJSON(ctx context.Context, workspace string, imageInfo *buildImagesJSON) error {
+ ctx = td.StartStep(ctx, td.Props(fmt.Sprintf("Read %s", buildImagesJSONFile)))
+ defer td.EndStep(ctx)
+
+ f := filepath.Join(workspace, buildImagesJSONFile)
+ if err := util.WithWriteFile(f, func(w io.Writer) error {
+ return json.NewEncoder(w).Encode(imageInfo)
+ }); err != nil {
+ return td.FailStep(ctx, err)
}
+ return nil
}
// shallowClone creates a shallow clone of the given repo at the given commit.
@@ -125,29 +210,3 @@
}
return checkoutDir, nil
}
-
-// bazelTargetToDockerTag converts a Bazel target specification to a Docker
-// image tag which is applied to the image during the Bazel build.
-func bazelTargetToDockerTag(target string) string {
- return path.Join("bazel", target)
-}
-
-// bazelRun executes `bazel run` for the given target and applies the given tag
-// to the resulting image.
-func bazelRun(ctx context.Context, cwd, target, louhiImageTag string, rbe bool) error {
- ctx = td.StartStep(ctx, td.Props(fmt.Sprintf("Build %s", target)))
- defer td.EndStep(ctx)
-
- cmd := []string{"bazelisk", "run"}
- if rbe {
- cmd = append(cmd, "--config=remote", "--google_default_credentials")
- }
- cmd = append(cmd, target)
- if _, err := exec.RunCwd(ctx, cwd, cmd...); err != nil {
- return td.FailStep(ctx, err)
- }
- if _, err := exec.RunCwd(ctx, cwd, "docker", "tag", bazelTargetToDockerTag(target), louhiImageTag); err != nil {
- return td.FailStep(ctx, err)
- }
- return nil
-}
diff --git a/cd/go/build-images/update-refs.go b/cd/go/build-images/update-refs.go
new file mode 100644
index 0000000..75cd4a9
--- /dev/null
+++ b/cd/go/build-images/update-refs.go
@@ -0,0 +1,93 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "go.skia.org/infra/go/exec"
+ "go.skia.org/infra/go/gerrit/rubberstamper"
+ "go.skia.org/infra/go/git"
+ "go.skia.org/infra/go/gitauth"
+ "go.skia.org/infra/go/skerr"
+ "go.skia.org/infra/task_driver/go/lib/git_steps"
+ "go.skia.org/infra/task_driver/go/td"
+)
+
+func updateRefs(ctx context.Context, repo, workspace, username, email string) error {
+ ctx = td.StartStep(ctx, td.Props("Update References"))
+ defer td.EndStep(ctx)
+
+ // Initialize git authentication.
+ ts, err := git_steps.Init(ctx, true)
+ if err != nil {
+ return td.FailStep(ctx, err)
+ }
+ if _, err := gitauth.New(ts, "/tmp/.gitcookies", true, email); err != nil {
+ return td.FailStep(ctx, err)
+ }
+
+ imageInfo, err := readBuildImagesJSON(ctx, workspace)
+ if err != nil {
+ return td.FailStep(ctx, err)
+ }
+
+ // First, obtain the sha256 sums for the images.
+ for _, image := range imageInfo.Images {
+ imageAndTag := fmt.Sprintf("%s:%s", image.Image, image.Tag)
+ if _, err := exec.RunCwd(ctx, ".", "docker", "pull", imageAndTag); err != nil {
+ return td.FailStep(ctx, err)
+ }
+ output, err := exec.RunCwd(ctx, ".", "docker", "inspect", "--format='{{index .RepoDigests 0}}'", imageAndTag)
+ if err != nil {
+ return td.FailStep(ctx, err)
+ }
+ split := strings.Split(strings.TrimSpace(output), "@")
+ if len(split) != 2 {
+ return td.FailStep(ctx, skerr.Fmt("Failed to obtain sha256 sum for %s; expected <image>@<sha256> but got %q", image.Image, output))
+ }
+ image.Sha256 = split[1]
+ }
+
+ // Create a shallow clone of the repo.
+ checkoutDir, err := shallowClone(ctx, repo, git.DefaultRef)
+ if err != nil {
+ return td.FailStep(ctx, err)
+ }
+
+ // Create a branch.
+ gitExec, err := git.Executable(ctx)
+ if err != nil {
+ return td.FailStep(ctx, err)
+ }
+ if _, err := exec.RunCwd(ctx, checkoutDir, gitExec, "checkout", "-b", "update", "-t", git.DefaultRemoteBranch); err != nil {
+ return td.FailStep(ctx, err)
+ }
+
+ // Find-and-replace each of the image references.
+ for _, image := range imageInfo.Images {
+ if _, err := exec.RunCwd(ctx, checkoutDir, "find", "./", "-type", "f", "-exec", "sed", "-r", "-i", fmt.Sprintf("s;%s@sha256:[a-f0-9]+;%s@sha256:%s;g", image.Image, image.Image, image.Sha256), "{}", "\\;"); err != nil {
+ return td.FailStep(ctx, err)
+ }
+ }
+
+ // Did we change anything?
+ if _, err := exec.RunCwd(ctx, checkoutDir, gitExec, "diff", "--exit-code"); err != nil {
+ // If so, create a CL.
+ imageList := make([]string, 0, len(imageInfo.Images))
+ for _, image := range imageInfo.Images {
+ imageList = append(imageList, image.Image)
+ }
+ commitMsg := fmt.Sprintf(`Update %s
+
+%s`, strings.Join(imageList, ", "), rubberstamper.RandomChangeID())
+ if _, err := exec.RunCwd(ctx, checkoutDir, gitExec, "commit", "-a", "-m", commitMsg); err != nil {
+ return td.FailStep(ctx, err)
+ }
+ if _, err := exec.RunCwd(ctx, checkoutDir, gitExec, "push", git.DefaultRemote, rubberstamper.PushRequestAutoSubmit); err != nil {
+ return td.FailStep(ctx, err)
+ }
+ }
+
+ return nil
+}