Advanced Scripting Guide
This guide provides some tips and ideas for advanced scripting. It assumes familiarity with the scripting guide and the Brigadier API.
Promises and the async
and await
keywords
Brigade supports the various methods provided by the JavaScript language for
controlling the flow of asynchronous execution. This includes chaining together
promises as well as utilization of the async
and await
keywords.
Here is an example that uses a Promise chain to organize the execution of two jobs:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
function exec(event) {
let j1 = new Job("j1", "alpine:3.14", event);
j1.primaryContainer.command = ["echo"];
j1.primaryContainer.arguments = ["hello " + event.payload];
let j2 = new Job("j2", "alpine:3.14", event);
j2.primaryContainer.command = ["echo"];
j2.primaryContainer.arguments = ["goodbye " + event.payload];
j1.run()
.then(() => {
return j2.run()
})
.then(() => {
console.log("done");
});
}
events.process();
In the example above, we use JavaScript Promise
objects for chaining two
jobs, then printing done
after the two jobs are run. Each Job.run()
call
returns a Promise
, and we call that Promise
’s then()
method.
Here’s what it looks like when the script is run:
$ brig event create --project promises --payload world --follow
Created event "882f832a-c156-4afc-9936-00d3b2d61083".
Waiting for event's worker to be RUNNING...
2021-10-04T22:33:22.078Z INFO: brigade-worker version: 5c94a15-dirty
2021-10-04T22:33:22.502Z [job: j1] INFO: Creating job j1
2021-10-04T22:33:25.052Z [job: j2] INFO: Creating job j2
done
$ brig event logs --id 882f832a-c156-4afc-9936-00d3b2d61083 --job j1
hello world
$ brig event logs --id 882f832a-c156-4afc-9936-00d3b2d61083 --job j2
goodbye world
We can rewrite the example to use await and get the same result:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
async function exec(event) {
let j1 = new Job("j1", "alpine:3.14", event);
j1.primaryContainer.command = ["echo"];
j1.primaryContainer.arguments = ["hello " + event.payload];
let j2 = new Job("j2", "alpine:3.14", event);
j2.primaryContainer.command = ["echo"];
j2.primaryContainer.arguments = ["goodbye " + event.payload];
await j1.run();
await j2.run();
console.log("done");
}
events.process();
The first thing to note about this example is that we are annotating our
exec()
function with the async
keyword. This tells the JavaScript runtime
that the function is an asynchronous handler.
The two await
statements will cause the jobs to run synchronously.
The first job will run to completion, then the second job will run to
completion. Then the console.log
function will execute.
Some people feel that using async
/await
makes code more readable. Others
prefer the Promise
notation. Brigade will support either. The same patterns
shown above can also be used with Job.concurrent()
and Job.sequence()
, as
their run()
methods return Promise
objects as well.
Error Handling
Note that when errors occur, they are thrown as exceptions. To handle this
case, use try
/catch
blocks. These can be employed with either the promise
chain approach or the async
/await
approach.
Via Promise chaining:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
function exec(event) {
let j1 = new Job("j1", "alpine:3.14", event);
j1.primaryContainer.command = ["echo"];
j1.primaryContainer.arguments = ["hello " + event.payload];
// j2 is configured to fail
let j2 = new Job("j2", "alpine:3.14", event);
j2.primaryContainer.command = ["exit"];
j2.primaryContainer.arguments = ["1"];
j1.run()
.then(() => {
return j2.run()
})
.then(() => {
console.log("done");
})
.catch(e => {
console.log(`Caught Exception ${e}`);
});
}
events.process();
Via async
/await
:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
async function exec(event) {
let j1 = new Job("j1", "alpine:3.14", event);
j1.primaryContainer.command = ["echo"];
j1.primaryContainer.arguments = ["hello " + event.payload];
// j2 is configured to fail
let j2 = new Job("j2", "alpine:3.14", event);
j2.primaryContainer.command = ["exit"];
j2.primaryContainer.arguments = ["1"];
try {
await j1.run();
await j2.run();
console.log("done");
} catch (e) {
console.log(`Caught Exception ${e}`);
}
}
events.process();
Looking at the async
/await
example, the second job (j2
) will execute
exit 1
, which will cause the container to exit with an error. When
await j2.run()
is executed, it will throw an exception because j2
exited
with an error. In our catch
block, we print the error message that we
receive.
If we run this, we’ll see something like this:
$ brig event create --project await --payload world --follow
Created event "69b5713f-b612-434f-9b52-9bcd57f044c5".
Waiting for event's worker to be RUNNING...
2021-10-04T22:45:45.808Z INFO: brigade-worker version: 5c94a15-dirty
2021-10-04T22:45:46.235Z [job: j1] INFO: Creating job j1
2021-10-04T22:45:48.826Z [job: j2] INFO: Creating job j2
Caught Exception Error: Job "j2" failed
The line Caught Exception...
shows the error that we received.
Note, however, we didn’t configure the worker to fail when we caught the
exception in the example above; we simply logged it. Although the j2
job
fails, the worker succeeds. We see this when looking at the event afterwards:
$ brig event get --id 69b5713f-b612-434f-9b52-9bcd57f044c5
ID PROJECT SOURCE TYPE AGE WORKER PHASE
69b5713f-b612-434f-9b52-9bcd57f044c5 await brigade.sh/cli exec 20s SUCCEEDED
Event "69b5713f-b612-434f-9b52-9bcd57f044c5" jobs:
NAME STARTED ENDED PHASE
j1 16s 13s SUCCEEDED
j2 11s 11s FAILED
This illustrates the following point: As the script-writer, catching exceptions from jobs (or other runnables) creates the opportunity to decide whether the workflow succeeds or fails. Perhaps we do wish to fail the worker immediately. Alternatively, perhaps we wish to execute conditional logic which would then ultimately dictate worker success.
Using Object-oriented JavaScript to Extend Job
JavaScript supports class-based, object-oriented programming. One example where
Brigade makes use of this is by providing some useful ways of working with the
Job
class. The Job
class can be extended to either preconfigure similar
jobs or to add extra functionality to a job.
The following example creates a MyJob
class that extends Job
and provides
some predefined fields:
const { events, Job } = require("@brigadecore/brigadier");
class MyJob extends Job {
constructor(name, event) {
super(name, "alpine:3.14", event);
this.primaryContainer.command = ["echo"];
this.primaryContainer.arguments = ["hello " + event.payload];
}
}
events.on("brigade.sh/cli", "exec", async event => {
const j1 = new MyJob("j1", event);
const j2 = new MyJob("j2", event);
await Job.sequence(j1, j2).run();
});
events.process();
In the example above, both j1
and j2
will have the same image and the same
command. They inherited these predefined settings from the MyJob
class. Using
inheritence in this way can reduce boilerplate code.
The fields can be selectively overwritten, as well. We could, for example, override the command arguments for the second job without affecting the first:
const { events, Job } = require("@brigadecore/brigadier");
class MyJob extends Job {
constructor(name, event) {
super(name, "alpine:3.14", event);
this.primaryContainer.command = ["echo"];
this.primaryContainer.arguments = ["hello " + event.payload];
}
}
events.on("brigade.sh/cli", "exec", async event => {
const j1 = new MyJob("j1", event);
const j2 = new MyJob("j2", event);
j2.primaryContainer.arguments = ["goodbye " + event.payload];
await Job.sequence(j1, j2).run();
});
events.process();
If we were to look at the output of these two jobs, we’d see something like this:
$ brig event create --project jobs --payload world --follow
Created event "c4906ec3-fec1-400f-8d8f-89fd6a379475".
Waiting for event's worker to be RUNNING...
2021-10-04T23:02:58.191Z INFO: brigade-worker version: 5c94a15-dirty
2021-10-04T23:02:58.545Z [job: j1] INFO: Creating job j1
2021-10-04T23:03:01.088Z [job: j2] INFO: Creating job j2
$ brig event logs --id c4906ec3-fec1-400f-8d8f-89fd6a379475 --job j1
hello world
$ brig event logs --id c4906ec3-fec1-400f-8d8f-89fd6a379475 --job j2
goodbye world
Job j2
has the different command, while j1
inherited the defaults from MyJob
.