blob: 55d205b01f540bffc7afeb7a2c116dceecb6ef32 [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"
"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() {
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.
fakeProjectId := ""
fakeTaskId := ""
fakeTaskName := ""
output := "-"
local := true
ctx := td.StartRun(&fakeProjectId, &fakeTaskId, &fakeTaskName, &output, &local)
defer td.EndRun(ctx)
// 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"`
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)
}
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 := 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
}