| package main | |
| /* | |
| Basic Task Driver example. | |
| Run like this: | |
| $ go run ./basic.go --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 | |
| } |