[BCID] Add build-images Go program
This will be packaged inside the "cd" Docker image to be used in Louhi
CD pipelines.
Change-Id: I2de2e672dd7ae1ced0629692f6a5f2d881423ec5
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/583457
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Eric Boren <borenet@google.com>
diff --git a/cd/Dockerfile b/cd/Dockerfile
index a67976e..75e7cd3 100644
--- a/cd/Dockerfile
+++ b/cd/Dockerfile
@@ -55,6 +55,7 @@
ARG CIPD_ROOT
ENV CIPD_ROOT=$CIPD_ROOT
COPY --from=install_pkgs ${CIPD_ROOT} ${CIPD_ROOT}
+COPY ./build-images /bin/build-images
ENV GOPATH=/go
RUN mkdir -p ${GOPATH}
ENV PATH="${CIPD_ROOT}/bin:${CIPD_ROOT}/go/bin:${CIPD_ROOT}:${GOPATH}/bin:${PATH}"
diff --git a/cd/Makefile b/cd/Makefile
new file mode 100644
index 0000000..a97b23f
--- /dev/null
+++ b/cd/Makefile
@@ -0,0 +1,9 @@
+include ../make/bazel.mk
+
+.PHONY: build-images
+build-images:
+ $(BAZEL) build //cd/go/build-images:build-images
+
+.PHONY: release
+release: build-images
+ ./build_release
\ No newline at end of file
diff --git a/cd/build_release b/cd/build_release
index 05fb846..9636a96 100755
--- a/cd/build_release
+++ b/cd/build_release
@@ -12,6 +12,7 @@
INSTALL_DIR="install -d --verbose --backup=none"
${INSTALL} --mode=644 -T Dockerfile ${ROOT}/Dockerfile
${INSTALL} --mode=644 -T ../infra/config/recipes.cfg ${ROOT}/recipes.cfg
+${INSTALL} --mode=755 -T ../_bazel_bin/cd/go/build-images/build-images_/build-images ${ROOT}/build-images
}
source ../bash/docker_build.sh
diff --git a/cd/go/build-images/BUILD.bazel b/cd/go/build-images/BUILD.bazel
new file mode 100644
index 0000000..c03ef80
--- /dev/null
+++ b/cd/go/build-images/BUILD.bazel
@@ -0,0 +1,29 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("//bazel/go:go_test.bzl", "go_test")
+
+go_library(
+ name = "build-images_lib",
+ srcs = ["main.go"],
+ importpath = "go.skia.org/infra/cd/go/build-images",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//go/common",
+ "//go/exec",
+ "//go/git",
+ "//task_driver/go/td",
+ "@org_golang_x_sync//errgroup",
+ ],
+)
+
+go_binary(
+ name = "build-images",
+ embed = [":build-images_lib"],
+ visibility = ["//visibility:public"],
+)
+
+go_test(
+ name = "build-images_test",
+ srcs = ["main_test.go"],
+ embed = [":build-images_lib"],
+ deps = ["@com_github_stretchr_testify//require"],
+)
diff --git a/cd/go/build-images/main.go b/cd/go/build-images/main.go
new file mode 100644
index 0000000..caa7b40
--- /dev/null
+++ b/cd/go/build-images/main.go
@@ -0,0 +1,153 @@
+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.
+*/
+
+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.")
+
+ // We're using the task driver framework because it provides logging and
+ // helpful insight into what's occurring as the program runs.
+ fakeProjectId := ""
+ fakeTaskId := ""
+ fakeTaskName := ""
+ output := "-"
+ local := true
+ 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 {
+ 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")
+
+ // 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)
+ })
+ }
+ if err := eg.Wait(); err != nil {
+ td.Fatal(ctx, err)
+ }
+}
+
+// shallowClone creates a shallow clone of the given repo at the given commit.
+// Returns the location of the checkout or any error which occurred.
+func shallowClone(ctx context.Context, repoURL, commit string) (string, error) {
+ ctx = td.StartStep(ctx, td.Props("Clone"))
+ defer td.EndStep(ctx)
+
+ checkoutDir, err := ioutil.TempDir("", "")
+ if err != nil {
+ return "", td.FailStep(ctx, err)
+ }
+ git, err := git.Executable(ctx)
+ if err != nil {
+ return "", td.FailStep(ctx, err)
+ }
+ if _, err := exec.RunCwd(ctx, checkoutDir, git, "init"); err != nil {
+ return "", td.FailStep(ctx, err)
+ }
+ if _, err := exec.RunCwd(ctx, checkoutDir, git, "remote", "add", "origin", repoURL); err != nil {
+ return "", td.FailStep(ctx, err)
+ }
+ if _, err := exec.RunCwd(ctx, checkoutDir, git, "fetch", "--depth=1", "origin", commit); err != nil {
+ return "", td.FailStep(ctx, err)
+ }
+ if _, err := exec.RunCwd(ctx, checkoutDir, git, "checkout", "FETCH_HEAD"); err != nil {
+ return "", td.FailStep(ctx, err)
+ }
+ 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/main_test.go b/cd/go/build-images/main_test.go
new file mode 100644
index 0000000..925014a
--- /dev/null
+++ b/cd/go/build-images/main_test.go
@@ -0,0 +1,16 @@
+package main
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestBazelTargetToDockerTag(t *testing.T) {
+ tc := map[string]string{
+ "//task_scheduler:task_scheduler_jc_container": "bazel/task_scheduler:task_scheduler_jc_container",
+ }
+ for input, expect := range tc {
+ require.Equal(t, expect, bazelTargetToDockerTag(input))
+ }
+}