Reactors SDK¶
Nearly any language can be used to build functions for TACC’s Abaco serverless computing platform. This is the documentation for a specific, opinionated approach that embeds a client-side Python2.7/3.6 support library called Reactors in the container that hosts an Abaco function.
Reactors extends the Abaco Actors concept with:
- YAML-based configuration mechanism with environment overrides
- Support for first-class mocking and local-side testing
- Semantic aliases for Actors and other TACC.cloud API assets
- Helper methods for working with integrations like Slack and IFTTT
- Introspection of the actor’s platform-level attributes
- Advanced logging with support for redacting sensitive text
- Optimized TACC.cloud API operations
Pre-requisites¶
Start a Project¶
Concept: The Abaco CLI will automatically generate a skeleton for your Reactor that is preconfigured for easy building, testing, and deployment. This same structure lends itself well to adopting continuous integration and unit testing if you should need those practices. The list of templates is limited at present, and constrained to Python language support. This is expected to change in the future. In addition, it is expected that deeper integration with Github and Gitlab will be added to the Reactors workflow.
Let’s go!¶
Run abaco init
, specifying language (python2
or python3
for now) and a URL-safe name. A good rule-of-thumb is to name a Reactors project like one would a Git repository.
$ abaco init -l python3 -n hello_world
$ ls hello_world/
Dockerfile config.yml reactor.py requirements.txt
TEMPLATE.md message.jsonschema reactor.rc secrets.json.sample
Details on what each project file does are provided in the User Guide.
Configure the Project¶
Concept: The Dockerfile
is a recipe to build the environment where our function will run. The function itself is implemented in reactor.py
. A Python module built into the base Docker image (reactors
) works with config.yml
and message.jsonschema
to provide declarative configuration and validation. The requirements.txt
file is used with pip
in the container image to specify additional Python modules to install. Finally, the Reactors workflow uses reactor.rc
to specify name, version, and other metadata, and secrets.json
as a way to pass sensitive information into a function without committing it to the container image.
Step 1: Edit config.rc¶
Naivigate to the project directory and edit DOCKER_HUB_ORG
in config.rc
to reflect either your Docker Hub username or an organization where you have push and pull access. For example, if a person with the Docker Hub username taco
is a member of Docker Hub group cabana
, they can choose either taco
or cabana
as the value for DOCKER_HUB_ORG
Step 2: Edit config.yml¶
Change the config file to read as follows.
---
logs:
level: INFO
token: ~
dont_reveal: ~
Step 3: Create secrets.json¶
Write a JSON file with the following contents.
{"_REACTORS_DONT_REVEAL": "This is a secret"}
Write some code¶
Concept: An Abaco function is a script or binary that is set as the default command in a container, accepts a message and parameters from environment variables, and can (optionally) make use of a pre-authenticated Agave API client. Functions can be written in any language, but the Reactors Python SDK streamlines these processes and adds support for some experimental platform features.
Action: Replace the contents of reactor.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from reactors.runtime import Reactor
def main():
"""Main function"""
r = Reactor()
r.logger.info("I received: {}".format(r.context['raw_message']))
r.logger.debug("This is a DEBUG message from actor {}".format(r.uid))
r.logger.info("This is an INFO message from actor {}".format(r.uid))
r.logger.warning("This is a warning from actor {}".format(r.uid))
r.logger.info("Here's that secret value: {}".format(
r.settings.dont_reveal))
if __name__ == '__main__':
main()
|
This example illustrates use of the Reactor
object, specifically, its settings, context, and logging functions. More features and use cases are described in the User Guide and Scenarios sections.
Deploy the Reactor¶
Concept: Functions can be deployed with the abaco create
CLI command using a Docker image that has been built and pushed to a public registry. This is a very flexible approach, but it requires the authorone to execute the same series of steps each time. The abaco deploy
command implements a streamlined workflow that, with configuration guidance from reactor.rc
, automatically builds the image, pushes it, gathers environment variables, and deploys or updates the Reactor.
Action: Ensure the image builds correctly with a dry run
$ abaco deploy -R
[INFO] Build Options: --rm=true --pull
Sending build context to Docker daemon 10.75kB
Step 1/1 : FROM sd2e/reactors:python3
python3: Pulling from sd2e/reactors
Digest: sha256:789c9057306d618168193c75a6c47ca5c500bc6fcdb60dc30f27f9bf8b1af404
Status: Image is up to date for sd2e/reactors:python3
# Executing 5 build triggers
---> Using cache
---> Using cache
---> c06a54dcc66c
Successfully built c06a54dcc66c
Successfully tagged taco/hello_world:0.1
[INFO] Stopping deployment as this was only a dry run!
Action: Deploy the Reactor
$ abaco deploy
[INFO] Build Options: --rm=true --pull
Sending build context to Docker daemon 10.75kB
Step 1/1 : FROM sd2e/reactors:python3
python3: Pulling from sd2e/reactors
Digest: sha256:789c9057306d618168193c75a6c47ca5c500bc6fcdb60dc30f27f9bf8b1af404
Status: Image is up to date for sd2e/reactors:python3
# Executing 5 build triggers
---> Using cache
---> Using cache
---> Using cache
---> Using cache
---> Using cache
---> c06a54dcc66c
Successfully built c06a54dcc66c
Successfully tagged taco/hello_world:0.1
The push refers to repository [docker.io/taco/hello_world]
f9dde2603ec7: Pushed
87f9719c8a1d: Mounted from sd2e/reactors
913edbb0371b: Mounted from sd2e/reactors
0.1: digest: sha256:a944131700e2ae540dc76f2c1c2d72e3909fdfd287b42a505c339ff79615bac7 size: 7184
[INFO] Pausing to let Docker Hub register that the repo has been pushed
[INFO] Reading environment variables from secrets.json
Successfully deployed actor with ID: e6rkEBlzJ8vG4
Validate deployment¶
Concept: A Reactor that has been deployed successfully will be accessible via the actors
API and will report a status of SUBMITTED while the function is being deployed, then READY when it is prepared to accept messages.
Action: List the new actor by its identifier
$ abaco ls e6rkEBlzJ8vG4
The expected response should resemble this JSON document:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | {
"message": "Actor retrieved successfully.",
"result": {
"_links": {
"executions": "https://api.sd2e.org/actors/v2/e6rkEBlzJ8vG4/executions",
"owner": "https://api.sd2e.org/profiles/v2/taco",
"self": "https://api.sd2e.org/actors/v2/e6rkEBlzJ8vG4"
},
"createTime": "2018-06-21 14:39:16.435800",
"defaultEnvironment": {
"_REACTORS_DONT_REVEAL": "This is a secret"
},
"description": "",
"gid": 845005,
"id": "e6rkEBlzJ8vG4",
"image": "taco/hello_world:0.1",
"lastUpdateTime": "2018-06-21 14:39:16.435800",
"mounts": [
{
"container_path": "/work",
"host_path": "/work",
"mode": "rw"
},
{
"container_path": "/corral",
"host_path": "/corral/projects/TACC-Cloud",
"mode": "rw"
}
],
"name": "hello_world",
"owner": "taco",
"privileged": false,
"state": {},
"stateless": false,
"status": "READY",
"statusMessage": " ",
"tasdir": "05201/taco",
"type": "none",
"uid": 845005,
"useContainerUid": false
},
"status": "success",
"version": "0.8.0"
}
|
Note that result.status
is READY - this means the actor is ready to do work. If it reads SUBMITTED, deployment is stil in progress. If it reads ERROR, a problem has been encountered.
Test by sending a message¶
User Guide¶
The client-side SDK is based around the Reactor
class, which wraps key aspects of the current actor’s execution in a single object to provide helper functions enabled by submodules abacoid
, agaveutils
, aliases
, jsonmessages
, and process
. This guide will detail usage of Reactor
, then describe functions in the submodules that are usable on their own.
Reactor¶
Concept: A Reactor object has attributes that directly reflect the current Abaco context, attributes that provide linkages to a couple of other handy encapsulations, and some helper functions that provide or require an Agave API client. Critically, an instance of Reactor can be invoked outside the Abaco execution environment and will be populated with mock (but valid-y) attribute values and helper functions for working with TACC APIs initialized with the user’s own credentials.
Attributes¶
Every Reactor has the following attributes, built from either the Reactor’s execution environment or by means of some clever mocking code in a local test environment:
name | type | contents | source |
---|---|---|---|
aliases | aliases.store.AliasStore |
A helper for managing App and Actor aliases | Configured on instantiation with current client |
client | agavepy.agave.Agave |
An active Agave API client | Values provided by Abaco platform using agavepy.actors or local Agave API credentials. |
context | dict |
Variables passed by Abaco platform to the current execution | Values passed by Abaco and materialized via agavepy.actors or generated by mocking support functions. |
execid | string |
The current Abaco execution ID | `context.execution_id |
local | boolean |
True or False to indicate whether the function is running under Abaco or in a testing environment. |
The LOCALONLY environment variable, which can be set in local testing configurations. |
logger | `logging.StreamHandler` |
Pointer to the screen logger of loggers |
|
loggers | dict |
Python loggers screen, which logs to both STDERR and (optionally) a structured log aggregator, and slack which can log directly to a Slack channel. | Configured on instantiation, with credentials for Slack and other services provided by environment variables. |
nickname | string |
A semi-random, human memorable string. (e.g. sleek-lemur) | A call to the petname library |
pemagent | agaveutils.recursive.PemAgent |
A helper for applying recursive Agave API files permissions. Calls to it will eventually be an asynchronous. | Configured on instantiation with current client |
session | string |
A correlation key for connecting related events. | Query parameters SESSION or x-session , then nickname |
settings | attrdict.AttrDict |
Contents of config.yml , where keys are accessible in dot.notation . |
Populated at instantiation from config.yml via tacconfig.load_settings() |
state | dict |
The current Abaco actor state variable | context.state |
uid | string |
The current Abaco actor ID | `context.actor_id |
username | string |
TACC.cloud username on whose behalf the actor’s current execution is being undertaken. | Provided by `context.username under Abaco or by inspecting client when running locally. |
aliases¶
This is an instance of AliasStore
with which one can create, get, remove, and share alias entries for any actor. See AliasStore documentation for details.
client¶
This is an active AgavePy API client used to make authenticated API calls on behalf of the user that invoked execution of the Reactor. All AgavePy functions are supported.
context¶
This is a dictionary populated from environment variables passed to the container by Abaco.
Example Usage¶
>>> r = Reactor()
>>> print(r.context)
AttrDict({'HOSTNAME': '4647e1acfe46', 'LOCALONLY': '1', '_REACTOR_TEMP': '/mnt/ephemeral-01', 'raw_message_parse_log': 'Error parsing message: malformed node or string: None', 'message_dict': {}, 'SCRATCH': '/mnt/ephemeral-01', 'REACTORS_VERSION': '0.7.5', '_': '/usr/bin/python3', 'SHLVL': '1', 'PWD': '/', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'TERM': 'xterm', 'HOME': '/root', '_PROJ_STOCKYARD': '/work/projects/', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'OLDPWD': '/mnt/ephemeral-01', 'MSG': 'Hello There', _USER_WORK': '', '_PROJ_CORRAL': '/corral', 'actor_dbid': '5GJbZRQYk0VmY', 'username': 'tacocat', 'actor_id': '5GJbZRQYk0VmY', 'state': {}, 'execution_id': 'DeAzrr6ABO1pr', 'raw_message': 'Hello There', 'content_type': 'application/json'})
>>> print(r.context.raw_message)
Hello There
Context combines environment variables inherited from the container image, set in the container’s worker by Abaco, and passed to the specific execution by Abaco into a single dictionary. In addition to the string value keys, there are two important dict objects: state and message_dict
- state is a
dict
that can be read from and modified to pass information between executions of an actor without relying on an external database.- message_dict is populated by parsing a JSON message passed to the actor into a Python dictionary. This is done automatically and safely via Pythons ast and json.loads functions. If a message can’t be parsed, message_dict is set to an empty
AttrDict
. If you believe you’re sending valid JSON but it’s not resolving as a dictionary,context.raw_message_parse_log
can be inspected for clues to what is causing the failure.
execid¶
This is the unique identifier for the current execution.
local¶
This is a boolean value set based on the state of the LOCALONLY environment variable. It is intended to be used to selectively disable or enable code branches when running under local emulation.
print()
statement¶ r = Reactor()
if r.local is not True:
print('This code is not running under a test environment')
else:
print('This code is running locally')
The state of local
is set automatically by some CI support scripts, and can be set in a pytest environment using the monkeypatch fixture.
True
¶ def test_demo_local(monkeypatch):
monkeypatch.setenv('LOCALONLY', 1)
r = Reactor()
assert r.local is True
logger¶
This is an ready-to-use Python logger.
Example Usage¶
>>> r = Reactor()
>>> r.logger.info('This is a log message')
5AB11Q8XxwPK5 INFO This is a log message
A nicely formatted message is printed to STDERR
that includes the current actor ID. All logging levels (debug, info, warning, critical) are available. Logging level is set in the logs
stanza of config.yml
.
At the same time a plaintext message is sent to the standard log, it can (optionally) be sent over the network in a structured format. This is described in the advanced topics section.
Sensitive data passed into a Reactor using the secrets mechanism are redacted automatically when logged. For instance, assume API credentials for AWS have been set in a Reactor. Under a standard logging scheme, it would be very easy to print those sensitive data in build logs, screenshots, commits, and such, where it could be easily discoverable.
>>> r = Reactor()
>>> api_secret = r.settings.api.secret
>>> r.logger.info('Here are credentials: {}'.format(api_secret))
Pk4B11Q8Xxw INFO Here are credentials: ****
>>> api_url = r.settings.api.url
>>> r.logger.info('Here is API server: {}'.format(api_urk))
Pk4B11Q8Xxw INFO Here is API server: https://tacos.tacc.cloud/api/v1
loggers¶
This dict holds references to all loggers configured by a Reactor. At present, two loggers are established. The default logger, screen, prints to STDERR and (optionally) a log aggregator. The other logger, slack, allows logging directly to Slack assuming a webhook is provided when the actor is configured.
---
slack:
channel: "notifications"
icon_emoji: ":smile:"
username: "tacobot"
webhook: ~
>>> r = Reactor()
>>> r.loggers['screen'].info('This actor is a logger and a slacker')
5AB11Q8XxwPK5 INFO This actor is a logger and a slacker
>>> r.loggers['slack'].info('This actor is a logger and a slacker')
nickname¶
Inspired by the naming of Docker containers and other cloud resources, each Reactor invocation is assigned a “human meaningful, decentralized, secure” nickname generated by the petname library. By default, two-word nicknames are used, but this can be overridden with an entry in config.yml
---
nickname:
length: 3
pemagent¶
This is an instance of PemAgent
, a helper for recursive Agave files permissions management. Most permissions operations should be handled directly using AgavePy files.*Permissions
commands. The pemagent helper exists provide an optimized, and potentially asynchronous, method for doing batch operations.
session¶
This string is a correlation identifier among platform events with a common ancestry. Its value is set as follows:
- If environment variable
x-session
is not empty, session takes on its value- Else, if environment variable
SESSION
is not empty, session takes on its value- Else, session is set to the value of nickname
Critically, actors can message other actors. When this is done using Reactor.send_message
, the session value is forwarded along to the receipients as x-session
and will thus become their session identifier.
Example Usage¶
An Agave API job may send a string, such as job name or ID as SESSION, when messaging an Abaco actor. In this example, demojob
is sent as a value for SESSION
.
{ "notifications": [
{
"url": "https://api.tacc.cloud/actors/v2/eZE7XDPLzZOwo/messages?x-nonce=SD2E_KbyGjq4XOM4&channel=agavejobs&SESSION=demojob",
"event": "FINISHED",
"persistent": false
}
}
The value of session in the downstream Reactor
instance will be demojob
. If that Reactor messages another reactor, the downstream entity’s session will also be demojob
.
settings¶
state¶
uid¶
username¶
Functions¶
Assistants¶
abacoids¶
agaveutils¶
aliases¶
jsonmessages¶
process¶
Third-party Webhooks¶
Agave API Notifications¶
Finite State Machine¶
Schedule Actions¶
Automate Deployment¶
Unit Testing¶
RabbitMQ¶
AWS SNS¶
Getting Help¶
TACC.cloud Slack¶
You are welcome to join the developers and users of TACC.cloud services in TACC.cloud Slack. Helpful channels to join include #support
and #announcements
Tenant-specific Assistance¶
If you are a user from any of the following organizations, you can get help from additional listed resources.
- CyVerse
- DesignSafe
- Synergistic Discovery and Design (SD2)