blob: 9421a8f67fc83cf1b4f5fde23e7a5df321c57d93 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package internal
import (
"bytes"
"context"
"fmt"
"os/exec"
"regexp"
"strings"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/luciexe/build"
"infra/cros/cmd/common_lib/common"
)
const (
Oauth2Username = "oauth2accesstoken"
Oauth2Password = "$(gcloud auth print-access-token)"
// "host/project/containerName:tag"
ContainerFormat = "%s/%s/%s:%s"
)
// gcloudAuthOnceMap helps ensure GcloudAuth is only run once per registry.
var gcloudAuthOnceMap = map[string]struct{}{}
// GcloudAuth signs into gcloud and attempts to activate the service account
// given by the dockerKeyFile.
func GcloudAuth(ctx context.Context, registry, dockerKeyFile string) (err error) {
if registry == "" {
registry = common.DefaultDockerHost
}
if _, ok := gcloudAuthOnceMap[registry]; ok {
return
}
gcloudAuthOnceMap[registry] = struct{}{}
step, ctx := build.StartStep(ctx, fmt.Sprintf("Gcloud Auth (%s)", registry))
defer func() { step.End(err) }()
_, _, err = activateServiceAccount(ctx, dockerKeyFile)
if err != nil {
err = errors.Annotate(err, "failed to activate service account").Err()
return
}
password, _, err := getPassword(ctx)
if err != nil {
err = errors.Annotate(err, "failed to get password").Err()
return
}
_, _, err = login(ctx, registry, password)
if err != nil {
err = errors.Annotate(err, "failed to login").Err()
return
}
return
}
// execCommand generically runs commands with options for stdin and directory.
func execCommand(ctx context.Context, displayName, command string, args []string, stdin, dir string) (stdout string, stderr string, err error) {
step, ctx := build.StartStep(ctx, fmt.Sprintf("Run cmd: %s", displayName))
defer func() { step.End(err) }()
var se, so bytes.Buffer
cmd := exec.CommandContext(ctx, command, args...)
cmd.Stderr = &se
cmd.Stdout = &so
if stdin != "" {
cmd.Stdin = strings.NewReader(stdin)
}
if dir != "" {
cmd.Dir = dir
}
defer func() {
stdout = so.String()
stderr = se.String()
logging.Infof(ctx, "STDOUT: %s", stdout)
logging.Infof(ctx, "STDERR: %s", stderr)
}()
err = cmd.Run()
return
}
// activateServiceAccount runs the `gcloud auth activate-service-account` command.
func activateServiceAccount(ctx context.Context, dockerKeyFile string) (stdout string, stderr string, err error) {
if dockerKeyFile == "" {
return
}
args := []string{"auth", "activate-service-account", "--key-file", dockerKeyFile}
return execCommand(ctx, "Activate Service Account", "gcloud", args, "", "")
}
// getPassword runs the `glcoud auth print-access-token` command.
func getPassword(ctx context.Context) (stdout string, stderr string, err error) {
args := []string{"auth", "print-access-token"}
return execCommand(ctx, "Get Password", "gcloud", args, "", "")
}
// login runs the `docker login` command.
func login(ctx context.Context, registry, password string) (stdout string, stderr string, err error) {
args := []string{"login", "-u", Oauth2Username, "--password-stdin", registry}
return execCommand(ctx, "Login", "docker", args, password, "")
}
// buildImage runs the `docker build` command.
func buildImage(ctx context.Context, dir, fullname string) (stdout string, stderr string, err error) {
args := []string{"build", "-t", fullname, "."}
return execCommand(ctx, "Build Image", "docker", args, "", dir)
}
// pushImage runs the `docker push` command.
func pushImage(ctx context.Context, fullname string) (stdout string, stderr string, err error) {
args := []string{"push", fullname}
return execCommand(ctx, "Push Image", "docker", args, "", "")
}
// buildAndPush builds and pushes the docker image to the artifact
// directory and returns the sha produced.
func buildAndPush(ctx context.Context, dir, host, project, name, tag string) (sha string, err error) {
step, ctx := build.StartStep(ctx, "Build and Push")
defer func() { step.End(err) }()
fullname := fmt.Sprintf(ContainerFormat, host, project, name, tag)
_, _, err = buildImage(ctx, dir, fullname)
if err != nil {
err = errors.Annotate(err, "failed to build image").Err()
return
}
stdout, _, err := pushImage(ctx, fullname)
if err != nil {
err = errors.Annotate(err, "failed to push image").Err()
return
}
sha, err = extractDigestFromPushOutput(stdout)
if err != nil {
err = errors.Annotate(err, "name").Err()
return
}
return
}
// extractDigestFromPushOutput grabs the sha256 from the output
// of the `docker push` command.
func extractDigestFromPushOutput(stdout string) (digest string, err error) {
digestRegex, _ := regexp.Compile("sha256:[a-fA-F0-9]{64}")
digest = digestRegex.FindString(stdout)
if digest == "" {
err = fmt.Errorf("failed to find digest from output")
}
return
}