blob: e6114a5d3e09236cbd43d2cbb91dd458f0c690dd [file] [log] [blame]
package main
/*
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"
"os"
"path/filepath"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/urfave/cli/v2"
"go.skia.org/infra/cd/go/cd"
"go.skia.org/infra/go/docker"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/gitauth"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
"go.skia.org/infra/task_driver/go/lib/git_steps"
"go.skia.org/infra/task_driver/go/td"
)
func main() {
const (
flagCommit = "commit"
flagCommitSubject = "commit-subject"
flagEmail = "email"
flagLouhiExecutionID = "louhi-execution-id"
flagLouhiPubSubProject = "louhi-pubsub-project"
flagRepo = "repo"
flagSourceRepo = "source-repo"
flagSourceCommit = "source-commit"
flagTarget = "target"
flagUser = "user"
flagWorkspace = "workspace"
flagExtraBazelArg = "extra-bazel-arg"
)
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: "URL of the repo to update.",
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.MultiStringFlag{
Target: &cli.StringSliceFlag{
Name: flagExtraBazelArg,
Usage: "Extra argument(s) to pass to Bazel.",
},
},
&cli.StringFlag{
Name: flagSourceCommit,
Usage: "Commit hash which triggered the build.",
Required: false,
},
&cli.StringFlag{
Name: flagSourceRepo,
Usage: "URL of the repo which triggered the build.",
Required: 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.StringSlice(flagExtraBazelArg))
},
},
{
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,
},
&cli.StringFlag{
Name: flagLouhiPubSubProject,
Usage: "GCP project used for sending Louhi pub/sub notifications.",
Required: false,
},
&cli.StringFlag{
Name: flagLouhiExecutionID,
Usage: "Execution ID of the Louhi flow.",
Required: false,
},
&cli.StringFlag{
Name: flagSourceCommit,
Usage: "Commit hash which triggered the build.",
Required: false,
},
&cli.StringFlag{
Name: flagSourceRepo,
Usage: "URL of the repo which triggered the build.",
Required: false,
},
},
Action: func(ctx *cli.Context) error {
dc, err := docker.NewClient(ctx.Context)
if err != nil {
return skerr.Wrap(err)
}
return updateRefs(ctx.Context, dc, ctx.String(flagRepo), ctx.String(flagWorkspace), ctx.String(flagEmail), ctx.String(flagLouhiPubSubProject), ctx.String(flagLouhiExecutionID), ctx.String(flagSourceRepo), ctx.String(flagSourceCommit))
},
},
{
Name: "upload-cl",
Description: "Upload a CL with any changes in the local checkout.",
Usage: "upload-cl <options>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagCommitSubject,
Usage: "Commit message subject line.",
Required: true,
},
&cli.StringFlag{
Name: flagLouhiPubSubProject,
Usage: "GCP project used for sending Louhi pub/sub notifications.",
Required: false,
},
&cli.StringFlag{
Name: flagLouhiExecutionID,
Usage: "Execution ID of the Louhi flow.",
Required: false,
},
&cli.StringFlag{
Name: flagSourceCommit,
Usage: "Commit hash which triggered the build.",
Required: false,
},
&cli.StringFlag{
Name: flagSourceRepo,
Usage: "URL of the repo which triggered the build.",
Required: false,
},
&cli.StringFlag{
Name: flagEmail,
Usage: "Email address to attribute the build.",
Required: true,
},
},
Action: func(ctx *cli.Context) error {
cwd, err := os.Getwd()
if err != nil {
return skerr.Wrap(err)
}
// Initialize git authentication.
ts, err := git_steps.Init(ctx.Context, true)
if err != nil {
return td.FailStep(ctx.Context, err)
}
if _, err := gitauth.New(ctx.Context, ts, "/tmp/.gitcookies", true, ctx.String(flagEmail)); err != nil {
return td.FailStep(ctx.Context, err)
}
return cd.MaybeUploadCL(ctx.Context, cwd, ctx.String(flagCommitSubject), ctx.String(flagSourceRepo), ctx.String(flagSourceCommit), ctx.String(flagLouhiPubSubProject), ctx.String(flagLouhiExecutionID))
},
},
},
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.
fakeProjectId := ""
fakeTaskId := ""
fakeTaskName := ""
output := "-"
local := true
ctx := td.StartRun(&fakeProjectId, &fakeTaskId, &fakeTaskName, &output, &local)
defer td.EndRun(ctx)
// Enable retries with backoff for all commands.
ctx = exec.WithRetryContext(ctx, &backoff.ExponentialBackOff{
InitialInterval: 5 * time.Second,
RandomizationFactor: 0.5,
Multiplier: 2,
MaxInterval: time.Minute,
MaxElapsedTime: 15 * time.Minute,
Clock: backoff.SystemClock,
})
// Run the app.
if err := app.RunContext(ctx, os.Args); err != nil {
td.Fatal(ctx, err)
}
}
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"
)
// buildImagesJSON describes the structure of buildImagesJSONFile.
type buildImagesJSON struct {
Images []*SingleImageInfo `json:"images"`
}
type SingleImageInfo struct {
Image string `json:"image"`
Tag string `json:"tag"`
}
// 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)
}
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("Write %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.
// 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 := os.MkdirTemp("", "")
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
}