blob: bebd8e4af128b5070bb13723a2c97907992e906a [file]
// Copyright 2098 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package cmd
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"infra/cmd/cros_test_platform/internal/autotest/artifacts"
"infra/cmd/cros_test_platform/internal/autotest/testspec"
"infra/cmd/cros_test_platform/internal/enumeration"
"infra/cmd/cros_test_platform/internal/site"
"infra/cmd/cros_test_platform/internal/utils"
"github.com/kr/pretty"
"github.com/maruel/subcommands"
"go.chromium.org/chromiumos/infra/proto/go/chromite/api"
"go.chromium.org/chromiumos/infra/proto/go/test_platform/steps"
"go.chromium.org/luci/auth/client/authcli"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/gcloud/gs"
"go.chromium.org/luci/common/logging"
)
// Enumerate is the `enumerate` subcommand implementation.
var Enumerate = &subcommands.Command{
UsageLine: "enumerate -input_json /path/to/input.json -output_json /path/to/output.json",
ShortDesc: "Enumerate tasks to execute for given requests.",
LongDesc: `Enumerate tasks to execute for given requests.
Step input and output is JSON encoded protobuf defined at
https://chromium.googlesource.com/chromiumos/infra/proto/+/master/src/test_platform/steps/enumeration.proto`,
CommandRun: func() subcommands.CommandRun {
c := &enumerateRun{}
c.authFlags.Register(&c.Flags, site.DefaultAuthOptions)
c.Flags.StringVar(&c.inputPath, "input_json", "", "Path that contains JSON encoded test_platform.steps.EnumerationRequests")
c.Flags.StringVar(&c.outputPath, "output_json", "", "Path where JSON encoded test_platform.steps.EnumerationResponses should be written.")
c.Flags.BoolVar(&c.debugLogger.enabled, "debug", false, "Print debugging information to stderr.")
return c
},
}
type enumerateRun struct {
subcommands.CommandRunBase
authFlags authcli.Flags
inputPath string
outputPath string
debugLogger debugLogger
}
func (c *enumerateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if err := c.validateArgs(args); err != nil {
fmt.Fprintln(a.GetErr(), err.Error())
c.Flags.Usage()
return exitCode(err)
}
ctx := cli.GetContext(a, c, env)
ctx = setupLogging(ctx)
err := c.innerRun(ctx, args)
if err != nil {
logApplicationError(ctx, a, err)
}
return exitCode(err)
}
func (c *enumerateRun) innerRun(ctx context.Context, args []string) error {
taggedRequests, err := c.readRequests()
if err != nil {
return err
}
if len(taggedRequests) == 0 {
return errors.Reason("zero requests").Err()
}
workspace, err := ioutil.TempDir("", "enumerate")
if err != nil {
return err
}
defer func() {
os.RemoveAll(workspace)
}()
resps := make(map[string]*steps.EnumerationResponse)
for t, r := range taggedRequests {
c.debugLogger.LogRequest(ctx, t, r)
resp, err := c.enumerateOne(ctx, workspace, t, r)
if err != nil {
return nil
}
resps[t] = resp
c.debugLogger.LogResponse(ctx, t, resp)
}
return writeResponse(ctx, c.outputPath, &steps.EnumerationResponses{
TaggedResponses: resps,
})
}
// Enumerates one request.
//
// All errors from this function should be treated as infrastructure failure in
// the enumerate step.
// User/caller errors will receive an empty enumeration with errors described in
// its body, rather than an infrastructure error.
func (c *enumerateRun) enumerateOne(ctx context.Context, workspace string, tag string, r *steps.EnumerationRequest) (*steps.EnumerationResponse, error) {
resp := &steps.EnumerationResponse{
AutotestInvocations: []*steps.EnumerationResponse_AutotestInvocation{},
}
w, err := ioutil.TempDir(workspace, "request")
if err != nil {
return nil, err
}
m := r.GetMetadata().GetTestMetadataUrl()
if m == "" {
resp.ErrorSummary = fmt.Sprintf("empty request.metadata.test_metadata_url for %s", tag)
return resp, nil
}
lp, err := c.downloadArtifacts(ctx, gs.Path(m), w)
if err != nil {
if matchesUserErrorPatterns(err) {
resp.ErrorSummary = fmt.Sprintf("%s: %s", tag, err.Error())
return resp, nil
}
return nil, err
}
tm, err := c.computeMetadata(ctx, tag, lp, w)
if err != nil {
return nil, err
}
ts, errs := c.getEnumeration(ctx, tag, tm, r)
resp.AutotestInvocations = ts
if errs != nil {
resp.ErrorSummary = errs.Error()
}
return resp, nil
}
func matchesUserErrorPatterns(err error) bool {
if strings.Contains(err.Error(), "object doesn't exist") {
return true
}
return false
}
func validateEnumeration(ts []*steps.EnumerationResponse_AutotestInvocation) ([]*steps.EnumerationResponse_AutotestInvocation, errors.MultiError) {
if len(ts) == 0 {
return ts, errors.NewMultiError(errors.Reason("empty enumeration").Err())
}
vts := make([]*steps.EnumerationResponse_AutotestInvocation, 0, len(ts))
var merr errors.MultiError
for _, t := range ts {
if err := validateInvocation(t); err != nil {
merr = append(merr, errors.Annotate(err, "validate %s", t).Err())
} else {
vts = append(vts, t)
}
}
return vts, errorsOrNil(merr)
}
func errorsOrNil(merr errors.MultiError) errors.MultiError {
if merr.First() != nil {
return merr
}
return nil
}
func validateInvocation(t *steps.EnumerationResponse_AutotestInvocation) error {
if t.GetTest().GetName() == "" {
return errors.Reason("empty name").Err()
}
if t.GetTest().GetExecutionEnvironment() == api.AutotestTest_EXECUTION_ENVIRONMENT_UNSPECIFIED {
return errors.Reason("unspecified execution environment").Err()
}
return nil
}
func (c *enumerateRun) validateArgs(args []string) error {
if len(args) > 0 {
return errors.Reason("have %d positional args, want 0", len(args)).Err()
}
if c.inputPath == "" {
return errors.Reason("-input_json not specified").Err()
}
if c.outputPath == "" {
return errors.Reason("-output_json not specified").Err()
}
return nil
}
func (c *enumerateRun) readRequests() (map[string]*steps.EnumerationRequest, error) {
var rs steps.EnumerationRequests
if err := readRequest(c.inputPath, &rs); err != nil {
return nil, err
}
return rs.TaggedRequests, nil
}
func (c *enumerateRun) gsPath(requests []*steps.EnumerationRequest) (gs.Path, error) {
if len(requests) == 0 {
panic("zero requests")
}
m := requests[0].GetMetadata().GetTestMetadataUrl()
if m == "" {
return "", errors.Reason("empty request.metadata.test_metadata_url in %s", requests[0]).Err()
}
for _, r := range requests[1:] {
o := r.GetMetadata().GetTestMetadataUrl()
if o != m {
return "", errors.Reason("mismatched test metadata URLs: %s vs %s", m, o).Err()
}
}
return gs.Path(m), nil
}
func (c *enumerateRun) downloadArtifacts(ctx context.Context, gsDir gs.Path, workspace string) (artifacts.LocalPaths, error) {
outDir := filepath.Join(workspace, "artifacts")
if err := os.Mkdir(outDir, 0750); err != nil {
return artifacts.LocalPaths{}, errors.Annotate(err, "download artifacts").Err()
}
client, err := c.newGSClient(ctx)
if err != nil {
return artifacts.LocalPaths{}, errors.Annotate(err, "download artifacts").Err()
}
lp, err := artifacts.DownloadFromGoogleStorage(ctx, client, gsDir, outDir)
if err != nil {
return artifacts.LocalPaths{}, errors.Annotate(err, "download artifacts").Err()
}
return lp, err
}
func (c *enumerateRun) newGSClient(ctx context.Context) (gs.Client, error) {
t, err := newAuthenticatedTransport(ctx, &c.authFlags)
if err != nil {
return nil, errors.Annotate(err, "create GS client").Err()
}
return gs.NewProdClient(ctx, t)
}
func (c *enumerateRun) getEnumeration(ctx context.Context, tag string, tm *api.TestMetadataResponse, request *steps.EnumerationRequest) (ts []*steps.EnumerationResponse_AutotestInvocation, errs errors.MultiError) {
defer func() {
if errs.First() != nil {
c.debugLogger.LogErrors(ctx, utils.AnnotateEach(errs, "enumerate %s", tag))
}
}()
g, err := enumeration.GetForTests(tm.Autotest, request.TestPlan.Test)
if err != nil {
return nil, errors.NewMultiError(err)
}
ts = append(ts, g...)
ts = append(ts, enumeration.GetForSuites(tm.Autotest, request.TestPlan.Suite)...)
ts = append(ts, enumeration.GetForEnumeration(request.TestPlan.GetEnumeration())...)
ts, errs = validateEnumeration(ts)
return
}
func (c *enumerateRun) computeMetadata(ctx context.Context, tag string, localPaths artifacts.LocalPaths, workspace string) (*api.TestMetadataResponse, error) {
extracted := filepath.Join(workspace, "extracted")
if err := os.Mkdir(extracted, 0750); err != nil {
return nil, errors.Annotate(err, "compute metadata for %s", tag).Err()
}
if err := artifacts.ExtractControlFiles(localPaths, extracted); err != nil {
return nil, errors.Annotate(err, "compute metadata for %s", tag).Err()
}
tm, warnings := testspec.Get(extracted)
if tm == nil {
panic(fmt.Sprintf("testspec.Get() should always return valid metadata. Got nil for %s", tag))
}
c.debugLogger.LogTestMetadata(ctx, tag, tm)
if warnings.First() != nil {
c.debugLogger.LogWarnings(ctx, utils.AnnotateEach(warnings, "compute metadata for %s", tag))
}
return tm, nil
}
// debugLogger logs various intermediate PODs, only when enabled.
//
// All public methods of debugLogger are atomic: Logs generated by concurrent
// calls to debugLogger methods are guaranteed to not be interspersed.
type debugLogger struct {
enabled bool
m sync.Mutex
}
func (l *debugLogger) LogRequest(ctx context.Context, tag string, req *steps.EnumerationRequest) {
if !l.enabled {
return
}
l.m.Lock()
defer l.m.Unlock()
defer l.debugBlock(ctx, "requests")()
logging.Infof(ctx, "Request[%s]: %s", tag, pretty.Sprint(req))
}
func (l *debugLogger) LogTestMetadata(ctx context.Context, tag string, tm *api.TestMetadataResponse) {
if !l.enabled {
return
}
l.m.Lock()
defer l.m.Unlock()
defer l.debugBlock(ctx, "metadata")()
logging.Infof(ctx, "Test Metadata[%s]: %s", tag, pretty.Sprint(tm))
}
func (l *debugLogger) LogResponse(ctx context.Context, tag string, resp *steps.EnumerationResponse) {
if !l.enabled {
return
}
l.m.Lock()
defer l.m.Unlock()
defer l.debugBlock(ctx, "responses")()
logging.Infof(ctx, "Response[%s]: %s", tag, pretty.Sprint(resp))
}
func (l *debugLogger) LogErrors(ctx context.Context, merr errors.MultiError) {
// Unlike other debug data, errors are logged regardless of l.enabled
if merr.First() == nil {
return
}
l.m.Lock()
defer l.m.Unlock()
defer l.debugBlock(ctx, "errors")()
for _, err := range merr {
logging.Errorf(ctx, "%s", err)
}
}
func (l *debugLogger) LogWarnings(ctx context.Context, merr errors.MultiError) {
if !l.enabled {
return
}
if merr.First() == nil {
return
}
l.m.Lock()
defer l.m.Unlock()
defer l.debugBlock(ctx, "warnings")()
for _, err := range merr {
logging.Errorf(ctx, "%s", err)
}
}
// Begins a block of debug log.
//
// The returned function should be deferred to close the block.
func (l *debugLogger) debugBlock(ctx context.Context, title string) func() {
logging.Infof(ctx, "## BEGIN DEBUG LOG [%s]", title)
return func() {
logging.Infof(ctx, "## END DEBUG LOG [%s]", title)
}
}