blob: 25e5c50cc10fe211585c666ad02cc20fe89f520e [file] [log] [blame]
package main
/*
Basic Task Driver example.
Run like this:
$ go run ./basic.go --logtostderr --project_id=skia-swarming-bots --task_name=basic_example -o - --local
*/
import (
"context"
"flag"
"os"
"path/filepath"
"github.com/google/uuid"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/task_driver/go/lib/os_steps"
"go.skia.org/infra/task_driver/go/td"
)
var (
// Required flags for all Task Drivers.
projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
taskId = flag.String("task_id", "", "ID of this task. This is overridden when --local is used.")
taskName = flag.String("task_name", "", "Name of the task.")
output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
local = flag.Bool("local", false, "True if running locally (as opposed to in production). This causes --task_id to be overridden.")
// A Task Driver is, of course, welcome to define any additional flags.
)
func main() {
// Start a new Task Driver run. The returned Context represents the
// root-level step, from which all other steps stem. EndRun must be
// deferred, passing in the Context returned from StartRun.
ctx := td.StartRun(projectId, taskId, taskName, output, local)
defer td.EndRun(ctx)
// Technically, a Task Driver doesn't have to do anything more with
// steps beyond this point. Any and all work would get attributed to a
// single "root" step.
// The root-level step is considered a failure in the case of any
// non-recovered panic. Note that sklog.Fatal does NOT cause a panic
// but instead uses os.Exit(). That will result in execution ending but
// the steps not being marked as finished and no metadata sent.
// Therefore, you should use td.Fatal instead.
// Generally, you want to do work in sub-steps. You can choose how
// granular you want your steps to be, but it'll be easier to debug
// Task Drivers consisting of a larger number of smaller steps.
// Do() is the simplest way to perform work as a sub-step of the current
// Step. Any returned error causes the new step to be marked as failed.
if err := td.Do(ctx, nil, func(ctx context.Context) error {
return doSomething()
}); err != nil {
sklog.Error(err)
}
// We can add properties to steps like this:
env := []string{
"MYVAR=MYVAL",
}
props := td.Props("named infra step with env").Env(env).Infra()
if err := td.Do(ctx, props, func(ctx context.Context) error {
return doSomething()
}); err != nil {
sklog.Error(err)
}
// The above creates a step which is marked as an infrastructure step.
// This has no effect on how the Task Driver runs, but it allows us to
// separate different types of failures (eg. transient network errors
// vs actual test failures) and place blame correctly.
//
// The above step also has a name, which is only used for display, and
// an environment. The environment is only applied to subprocesses (ie.
// using the exec package); we do not modify this process' environment.
// Notably, if you add a new entry to PATH, the exec package won't be
// able to find executables in that new entry, unless you also export
// the new PATH via os.Setenv() or provide the absolute path to the
// executable to the exec package. We recommend the latter, since it is
// more easily auditable.
// Please see docs for RunStepFunc.
if err := RunStepFunc(ctx); err != nil {
td.Fatal(ctx, err)
}
}
// RunStepFunc is an example of how most steps should look. It creates a step
// whose scope is the entire body of the function.
func RunStepFunc(ctx context.Context) (rvErr error) {
// Do() is really a convenience wrapper which performs StartStep() and
// EndStep() for you. Depending on the context, it may be cleaner
// to use StartStep() and EndStep() directly, as in the case of a
// step whose scope is an entire function body. In that case, we call
// StartStep() at the beginning of the function and defer EndStep().
// If you use a named return value, any error returned from the function
// will be attached to the step.
//
// Note that EndStep() takes a pointer to an error; this is because
// arguments to deferred functions are evaluated when they are deferred
// (as opposed to when they are actually called), which in this case
// would cause the error passed to EndStep() to always be nil.
ctx = td.StartStep(ctx, td.Props("function-scoped step"))
defer td.EndStep(ctx)
// Function-scoped steps are the only context in which StartStep() and
// EndStep() should be used directly. We strongly recommend against
// the following usage pattern:
//
// subStep := td.StartStep(ctx)
// err := doSomething()
// td.EndStep(subStep)
//
// This is wrong for a couple reasons:
// 1. If doSomething() panics, the panic won't be correctly attributed
// to the sub-step.
// 2. Storing subStep in the local scope can cause a number of mistakes,
// including trying to perform work before it is started or after it
// is marked finished (which causes a panic), or accidentally
// attributing work to the wrong step.
//
// Additionally, avoid storing a step as a member of a struct. This is a
// recipe for the same kinds of mistakes. You should pass around steps
// just like any other context.Context.
// As you might have noticed, steps support nesting. This is a good way
// to maintain high granularity of steps while being able to hide detail
// when it's not relevant. Note that a step does not inherit the results
// of its children; a step only fails when you call FailStep() or when
// it catches a panic. If a sub-step failure should cause its parent
// step to fail, then you should call FailStep() for the parent as well.
// The Task Driver as a whole fails when the root-level step fails, or
// when EndRun() catches a panic.
if err := td.Do(ctx, td.Props("parent step"), func(ctx context.Context) error {
if err := td.Do(ctx, td.Props("sub-step 1"), func(ctx context.Context) error {
// Perform some work.
return doSomething()
}); err != nil {
// If we don't return the error, the parent step doesn't
// inherit the step failure.
sklog.Error(err)
}
return td.Do(ctx, td.Props("sub-step 2"), func(ctx context.Context) error {
// Any error produced here will be inherited by the
// parent step.
return doSomething()
})
}); err != nil {
// The function-scoped step will not inherit the result of
// "parent step", since we don't call FailStep().
sklog.Error(err)
}
// We expect most of the work done by a Task Driver to fall into one of
// three categories:
//
// 1. Subprocesses. You can pass a context.Context associated with a
// step to any of the Run functions in the go.skia.org/infra/go/exec
// package. This causes any subprocess to run as its own step, which
// is a sub-step of the one associated with the Context. See below and
// in basic_test for example code on mocking this subprocess call out.
if _, err := exec.RunSimple(ctx, "echo helloworld"); err != nil {
return td.FailStep(ctx, err)
}
// 2. HTTP requests. We provide an HttpClient() function which
// optionally wraps an existing http.Client and causes any HTTP
// request to run as a step. If you can avoid it, do not store the
// client; any HTTP requests become sub-steps of the step which
// generated the client, and it is easy to accidentally attribute
// requests to the wrong parent step if the client is stored.
if _, err := td.HttpClient(ctx, nil).Get("http://www.google.com"); err != nil {
return td.FailStep(ctx, err)
}
// 3. OS or filesystem interactions. We provide a library of steps which
// wrap the normal Go library functions so that they can be run as
// Steps.
dir := filepath.Join(os.TempDir(), "task_driver_basic_example", uuid.New().String())
if err := os_steps.MkdirAll(ctx, dir); err != nil {
return td.FailStep(ctx, err)
}
// We can run steps in a defer, too!
defer func() {
if err := os_steps.RemoveAll(ctx, dir); err != nil {
rvErr = td.FailStep(ctx, err)
}
}()
return nil
}
// subprocessExample is accompanied by a test in basic_test.go. The test shows off how to mock out
// a call to creating a subprocess.
func subprocessExample(ctx context.Context) error {
ctx = td.StartStep(ctx, td.Props("execute llamasay (demonstration of mocking calls)"))
defer td.EndStep(ctx)
if _, err := exec.RunSimple(ctx, "llamasay hello world"); err != nil {
return td.FailStep(ctx, err)
}
if _, err := exec.RunSimple(ctx, "bearsay good night moon"); err != nil {
return td.FailStep(ctx, err)
}
return nil
}
// doSomething is a dummy function used to take the place of actual work in
// this example.
func doSomething() error {
return nil
}