Object Templates and Custom Objects
In Cloudomation Engine you can store data in a similar way to a relational database, with the help of custom objects. Moreover, you can define hooks, that automatically handle lifecycle events like creating or updating a custom object.
There are other simpler ways to store data in Cloudomation Engine like Settings or Files. They are easier to use initially because they don't require the definition of an object template. However, you cannot define hooks for them or cross-reference them to each other.
The best way of storing data depends on the use-case. Generally, complex the data is suited better for custom objects while something more simple (e.g. storing a return code) can be done with settings or files.
Use Cases
You can use custom objects whenever you need to store structured information. Here are some examples:
-
Mailing lists
If your workflows send out emails (e.g. notifications on errors), you can store a mailing list of the users that should receive the email.
One way would be to store this list in a setting, and add/remove items when the mailing list changes.
A more sophisticated approach would be to use custom objects. This way you can validate entries, and define hooks (e.g. when a new user is added to the list, they receive a notification that they were signed up for the mailing list).
-
Discount codes
If you use Cloudomation Engine for processing orders from a webshop, you can store discount codes as custom objects.
You can define attributes for the discount codes (e.g. code, percentage, validity) and set up a hook that notifies your customers about new discount codes.
-
Remote development environment (RDE) management
Cloudomation DevStack relies heavily on custom objects to keep track of virtual machines (VMs) for RDEs, and automate their lifecycle events.
This is a good example of the scope of automation you can achieve with custom objects. Simply by creating a new custom object (a new RDE) you trigger a series of events:
- deploying and starting a VM from a snapshot,
- setting up the Known Hosts File and Authorized Keys File on the VM,
- providing a command to the user for connecting to the VM via SSH,
- scheduling a configurable shutdown of the VM to save costs,
- etc.
Concept
Custom objects go hand in hand with object templates.
Object Templates
Object templates are the blueprint for creating custom objects. You can define the structure that every custom object that is based on a specific object template will share.
You can create object templates just like any other Cloudomation Engine resource in the UI ("Create +" -> "More" -> "Object template").
If we stick to the comparison with a relational database, you can think of object templates as a table in a database. The attributes are the columns, with a data-type and other properties (e.g. uniqueness, nullability).
An empty object template
Hooks
Hooks help you automate lifecycle events. A hook can be something very simple, like sending an email. It can also be complex, like executing multiple stored procedures and parsing their return values or deleting a resource group at a cloud provider.
There are 3 types of hooks you can define:
- On create
- On update
- On delete
Hooks are implemented as flows. When a hook is triggered by a lifecycle event (e.g. updating a custom object), the specified flow gets executed.
The following information about the custom object gets passed as input_value
to the flow execution:
Name | Description |
---|---|
value | A key-value pair with the current values of the custom object. Gets passed on create and on update. |
old_value | A key-value pair with the values before the lifecycle event. Gets passed on update and on delete. |
custom_object | A reference to the custom object. |
custom_object_id | The id of the custom object. |
provisioning_type | The type of the lifecycle event. |
Here, two hooks are defined that will be triggered if a custom object is created or deleted
Attributes
Attributes define how and what kind of information is stored in the custom objects that are based on the object template. Each attribute has the same characteristics:
Name | Description |
---|---|
Datatype | What kind of data to store e.g. boolean or string. |
Reference | Reference to another object (applicable only if the data-type is a reference). |
Is required | Whether the attribute is required (nullability). |
Is unique | Whether multiple custom objects based on the same object template can have an attribute with the same value. |
Is hidden | Whether the attribute is shown when you open the custom object. |
Silent update | Whether an update of the attribute triggers the on update hook. |
This object template has two attributes
Attribute names have to be unique across a workspace. If you want to use the same attribute name in multiple objects templates, you can do so by prefixing them, e.g.
my_template_1_my_date
, my_template_2_my_date
, etc.
If an attribute has the data-type json
, it is automatically set to be required. You can explicitly set it to null
or {}
in the custom object to represent that the attribute has no data.
Custom Objects
Custom Objects can be created based on object templates.
Only users who have at least read access to the object template, that the custom object is based on, can see the custom object in the UI. For more on read access, refer to RBAC.
You can create custom objects just like any other Cloudomation Engine resource in the UI ("Create +" -> "Custom object" -> select the object template you want to use).
If we stick to the comparison with a relational database, you can think of custom objects as rows or entries in a table.
This custom object was created using the object template from before. You can see the attributes that were defined in the object template.
Provisioning State
Provisioning states are unique to custom objects and are not found in other Cloudomation resources.
Custom objects change during their lifecycle. They get created, updated, and deleted. The provisioning state shows, if a custom object is currently ready (stable), is going through a change, or if a previous change could not be concluded successfully.
Whenever a hook for a lifecycle event is triggered, the provisioning state changes to represent that event e.g. UPDATING
. Whether a change was concluded successfully,
depends on the the status of the execution that gets run by the hook.
If there is no hook defined for a lifecycle event, no flow gets executed and the provisioning state changes to READY
.
When we created the custom object above, the On create
hook was triggered (just like it was defined in the object template).
The provisioning state changes to CREATING
and we can also see the flow execution triggered by the hook.
The provisioning state changes to READY
once the flow execution triggered by the hook successfully finishes.
Depending on the complexity of the flow executed by the hook, a change in provisioning states can take a while, especially if the flow interacts with third-party systems.
If a custom object receives changes while it's still transitioning (i.e. the provisioning state is not READY
), the changes will be queued and the provisioning executions
will run one at a time, in the order they were created.
Example
Here's how a simple framework for storing and utilizing a mailing list could look like.
The goal is to have a mailing list that stores users. Each user has a category that defines which mails they are subscribed to. When a user is added or removed, or if their category is changed, they should get an email notification.
We will need flows for the lifecycle event hooks. We will also need a template for items on the mailing list.
Flows for the Hooks
On Create
This is the flow that gets executed on creation of a new item (i.e. a new user) on the list. It receives a reference to the user and the category in its input value and notifies the user about being added to the list.
We will call this flow "Mailing List Item on create".
import flow_api
def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
category = inputs['value']['Category']
user = inputs['value']['User_user']
user.send_mail(
subject='Added to mailing list',
text=f'You are now subscribed to all mails with category {category} and below',
)
return this.success('all done')
On Delete
This is the flow that gets executed on deletion of a new item on the list. It is almost identical to the on create flow. It receives a reference to the user and the category in its input value and notifies the user about being deleted from the list.
We will call this flow "Mailing List Item on delete".
import flow_api
def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
user = inputs['old_value']['User_user']
user.send_mail(
subject='Removed from mailing list',
text='You are not subscribed anymore to mails.',
)
return this.success('all done')
On Update
The on update flow is similar to the other flows. However, it also needs to account for the case, that the user gets changed, which is equivalent to adding the new user and deleting the old user.
We will call this flow "Mailing List Item on update".
import flow_api
def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
new_category = inputs['value']['Category']
new_user = inputs['value']['User_user']
old_category = inputs['old_value']['Category']
old_user = inputs['old_value']['User_user']
if old_user != new_user:
# the on create flow
this.flow(
'Mailing List Item on create',
value={'User_user': new_user,'Category': new_category}
)
# the on delete flow
this.flow(
'Mailing List Item on delete',
old_value={'User_user': old_user}
)
else:
new_user.send_mail(
subject='Changed category on mailing list',
text=f'You are now subscribed to all mails with category {new_category} and below (old category: {old_category}).',
)
return this.success('all done')
When calling the on create flow, we pass the argument value
. However, when calling the on delete flow, we pass the argument old_value
.
Object Template for Mailing List
We can now create the object template and define the hook, using the flows from before.
The template with the hooks defined.
Now add two attributes: User
and Category
.
The User
attribute.
User
is unique. This means that a user can only be added once to the list.
The Category
attribute.
And that's about it. Now we have a template that defines items on the mailing list. When an item is added, changed, or removed, the user will be notified.
Adding an Item to the Mailing List
Let's see how this works in action. Let's add an item to the mailing list i.e. create a custom object using the template above.
Here is the first item that we added. We selected a user and specified the category
Once you save the item (i.e. the custom object), the on create hook is triggered, and the User
is notified via email.
You can also see the provisioning state changing from CREATING
to READY
as the on create flow is executed by the hook.
Accessing the Mailing List
You can integrate the mailing list (i.e. the object template) into a workflow by accessing it's items (i.e. the custom objects). The custom_object_list
method of an object template returns all its custom objects.
Below is a flow script that takes category
, subject
, and text
as inputs. For each input there is a default set. The script fetches all users (i.e. the custom objects)
from the mailing list and then sends an email if the user's category allows it.
import flow_api
def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
category = inputs.get('category', 0)
subject = inputs.get('subject', 'This is a test')
text = inputs.get('subject', 'You friendly neighbourhood test email')
# get the mailing list
mailing_list_items = system.object_template('Mailing List').custom_object_list()
for item in mailing_list_items:
user = system.user(item.get('value')['User'], by='id')
user_category = item.get('value')['Category']
if user_category >= category:
user.send_mail(
subject=subject,
text=text,
)
return this.success('all done')