Asset Schema Reference
Asset Schema are defined as TypeScript functions that use the AssetBuilder API. This document augments the API documentation by laying out practical examples of each option, and briefly explaining what they do.
Schema Basics
Asset Schema define:
- Component properties
- Resource properties
- Input and Output sockets
- Secrets the asset requires
- Secrets the asset defines
The Builder Pattern
The Builder Pattern allows you to create an object, configure it, and eventually call build()
on it to finalize and export the data. For example:
const asset = new AssetBuilder();
const keyName = new PropBuilder()
.setName("KeyName")
.setKind("string")
.setWidget(new PropWidgetDefinitionBuilder().setKind("text").build())
.build();
asset.addProp(keyName);
return asset.build();
There are 3 Builders
in this snippet - the AssetBuilder
, PropBuilder
, and PropWidgetDefinitionBuilder
. The pattern is always the same:
- Create the Builder.
- Configure the object.
- Call
build()
.
The asset object
The AssetBuilder API is used to define the schema for an asset, such as a component. Schema definitions always begin by instantiating a new AssetBuilder
, and end by returning the value of asset.build()
.
const asset = new AssetBuilder();
return asset.build();
Component properties
Properties are added to an asset with the PropBuilder API and the addProp()
function of the AssetBuilder
.
Properties map to the underlying data structure of an asset, and specify things like field validations and influence the attribute panel's UI.
There are 6 kinds of properties, corresponding roughly to the standard JavaScript types:
- array
- boolean
- integer
- map
- object
- string
INFO
The difference between a map
and an object
is that maps take arbitrary key/value pairs, while objects
have defined properties.
All properties have:
- A Kind, which defines its fundamental data type, specified by
setKind()
. - A Name, specified by
setName()
. - An optional Validation Format, specified by
setValidationFormat()
which uses Joi to specify valid values. - An optional Default Value, specified by
setDefaultValue()
. - An optional boolean field that hides the property from the attributes UI, specified with
setHidden()
. - An optional Widget configuration, that determines how the field is presented in the attributes panel, specified with
setWidget()
.
Boolean properties
A boolean property named IsPublic
with a default value of false
.
const isPublicProp = new PropBuilder()
.setName("IsPublic")
.setKind("boolean")
.setDefaultValue(false)
.build();
String properties
This example specifies a property named KeyName
, whose value is a string:
const keyNameProp = new PropBuilder()
.setKind("string")
.setName("KeyName")
.build();
asset.addProp(keyNameProp);
A string with options, that displays as a select box, with a default value:
const keyType = new PropBuilder()
.setName("KeyType")
.setKind("string")
.setWidget(new PropWidgetDefinitionBuilder()
.setKind("select")
.addOption("rsa", "rsa")
.addOption("ed25519", "ed25519")
.build())
.setDefaultValue("rsa")
.build();
asset.addProp(keyType);
A string with a complex regular expression validation:
const cidrBlockProp = new PropBuilder()
.setKind("string")
.setName("CidrBlock")
.setValidationFormat(
Joi
.string()
.regex(/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/)(1[6-9]|2[0-8])$/)
.messages({
'string.pattern.base': 'Must be a valid IPv4 CIDR with CIDR Blocks between /16 and /28'
})
)
.build();
asset.addProp(cidrBlockProp);
Integer properties
A Throughput integer property:
const throughputProp = new PropBuilder()
.setName("Throughput")
.setKind("integer")
.build()
asset.addProp(throughputProp);
Map properties
A map named ResourceTags
:
const resourceTags = new PropBuilder()
.setKind("map")
.setName("ResourceTags")
.setEntry(new PropBuilder()
.setKind("string")
.setName("tag")
)
.build();
asset.addProp(resourceTags);
Maps use the setEntry
API to define the property kind of their value. Their keys are always string
. The name of the Entry is displayed in the attribute panel, but is not present in the resulting data structure.
So this map would produce a data structure like:
{
"van halen": "great band",
"beyonce": "also great"
}
Any property kind is a valid map entry.
A more complex map of objects:
const resourceTags = new PropBuilder()
.setKind("map")
.setName("ResourceTags")
.setEntry(new PropBuilder()
.setName("Francis")
.setKind("object")
.addChild(new PropBuilder()
.setName("Bacon")
.setKind("boolean")
.build()
)
)
.build();
asset.addProp(resourceTags);
Would produce:
{
"van halen": { "bacon": false },
"beyonce": { "bacon": true },
}
Object properties
An AdvancedConfiguration
object, with 3 properties: InstanceProfileArn
, EbsOptimized
, and UserData
:
const advancedConfigurationProp = new PropBuilder()
.setName("AdvancedConfiguration")
.setKind("object")
.addChild(new PropBuilder()
.setKind("string")
.setName("InstanceProfileArn")
.build())
.addChild(new PropBuilder()
.setKind("boolean")
.setName("EbsOptimized")
.build())
.addChild(new PropBuilder()
.setKind("string")
.setName("UserData")
.build())
.build()
asset.addProp(advancedConfigurationProp);
Objects use the addChild
interface to specify their properties. Any property kind can be a child property of an object.
This would produce an object that looks like this:
{
InstanceProfileArn: "arn:...",
EbsOptimized: false,
UserData: "...",
}
Array properties
A SecurityGroups
array of strings:
const securityGroupsProp = new PropBuilder()
.setKind("array")
.setName("SecurityGroups")
.setEntry(new PropBuilder()
.setKind("string")
.setName("SecurityGroup")
.build())
.build();
asset.addProp(securityGroupsProp);
Array's use the setEntry
API to define the kind of their members. Any kind of property is valid as an array entry.
An array of objects:
const tagSpecificationsProp = new PropBuilder()
.setName("TagSpecifications")
.setKind("array")
.setEntry(
new PropBuilder()
.setName("TagSpecificationsChild")
.setKind("object")
.addChild(
new PropBuilder()
.setName("Key")
.setKind("string")
.build(),
)
.addChild(
new PropBuilder()
.setName("Value")
.setKind("string")
.build(),
)
.build(),
)
.build();
asset.addProp(tagSpecificationsProp);
Widgets
Widgets define how properties are displayed. Each kind of property has a default widget, but it can be useful to alter the display on occasion (like the example above with KeyPair options.) Widgets are set with the PropWidgetDefinitionBuilder, and used through the setWidget()
method on a PropBuilder
.
Available widget types are:
- array
- checkbox
- color
- comboBox
- header
- map
- select
- text
- textArea
- codeEditor
- password
The select
widget and the comboBox
widget both accept a list of options, set with the setOption()
method on the builder.
Resource Properties
Resource Properties are used to extract information from Resources, and store them as hidden properties. They are generally used as sources for output sockets.
They are defined the same way as component properties, but with the setHidden(true)
option set on the PropBuilder
, and are added to the asset with addResourceProp()
rather than addProp()
.
For example:
const instanceIdProp = new PropBuilder()
.setName("InstanceId")
.setKind("string")
.setHidden(true)
.build();
asset.addResourceProp(instanceIdProp);
Would extract the InstanceId
from the resource information if it exists, and populate this property.
Input and Output Sockets
Sockets are defined with the SocketDefinitionBuilder API. They consist of:
- Names, that appear next to the sockets
- Arity, which defines if one or many other components can connect to this socket
- Connection annotations, which specify what other sockets are allowed to connect to this socket
The only difference between an Input and Output socket is how it is added to the asset. Input sockets are added with asset.addInputSocket(..)
, and Output sockets are added with asset.addOutputSocket(..)
.
Here is an example of an Image ID socket input, that accepts many inputs:
const imageIdSocket = new SocketDefinitionBuilder()
.setName("Image ID")
.setArity("many")
.build();
asset.addInputSocket(imageIdSocket);
Or an Instance ID output socket with a connection annotation and a single output:
const instanceProfileSocket = new SocketDefinitionBuilder()
.setName("Instance Profile")
.setArity("one")
.setConnectionAnnotation("Arn<string>")
.build();
asset.addOutputSocket(instanceProfileSocket);
Connection Annotations
Connections annotations are used as simple type signatures for sockets that allow for matching less specific to more specific annotations. Each socket can have multiple connection annotations. By default, sockets have a connection annotation that matches their name.
Given a connection annotation like:
setConnectionAnnotation(port<string>)
The socket would accept connections of docker<port<string>>
(more specific), port<string>>
(exactly the annotation) but not string
(less specific) alone.
Secrets the asset requires
When an asset requires a secret, it is specified with the SecretPropBuilder API. It creates both a property that allows a secret of the given kind to be set, but also an input socket that populates it.
They consist of:
- The name of the secret prop
- The secret kind, which corresponds to the name of its secret definition
- Connection annotations for the secret properties associated input socket.
Here is an example of an AWS Credential:
const credentialProp = new SecretPropBuilder()
.setName("credential")
.setSecretKind("AWS Credential")
.build();
asset.addSecretProp(credentialProp)
This would allow the asset to accept any AWS Credential
.
Secrets the asset defines
Assets that define secrets should only define secrets. They use the SecretDefinitionBuilder to define themselves, and are added to the asset with the defineSecret()
method. They consist of:
- A name for the credential
- Props that define the shape of the secret itself
- Connection annotations that apply to the output socket for the secret
For example, the Docker Hub Credential:
function main() {
const credential = new SecretDefinitionBuilder()
.setName("Docker Hub Credential")
.addProp(
new PropBuilder()
.setName("Username")
.setKind("string")
.setWidget(
new PropWidgetDefinitionBuilder()
.setKind("password")
.build()
).build())
.addProp(
new PropBuilder()
.setName("Password")
.setKind("string")
.setWidget(
new PropWidgetDefinitionBuilder()
.setKind("password")
.build()
).build())
.build();
return new AssetBuilder()
.defineSecret(credential)
.build()
}
Would allow users to add a Docker Hub Credential secret type, with two values, Username and Password.
Tips for Schema Creation
- Resist the temptation to abstract the resource you are modeling. Assets in System Initiative work best when they are as close to 1:1 models of the upstream.
- Frequently the right set of properties for a component mirror what they need when they are created.
Using the si-generator to generate schema for AWS Services
The System Initiative source code repository contains a program that will automatically generate schema for AWS services. Check out the repository, and navigate to the bin/si-generator
directory.
Ensure you have the aws cli installed.
Start by finding the create
API for the asset you wish to model. For example, if you were modeling the AWS EC2 Key Pair, the command would be aws ec2 create-key-pair
.
Then run the generator:
$ deno run ./main.ts asset ec2 create-key-pair
The resulting schema definition will correspond to the CLI skeleton of the AWS CLI:
function main() {
const asset = new AssetBuilder();
const keyNameProp = new PropBuilder()
.setName("KeyName")
.setKind("string")
.build();
asset.addProp(keyNameProp);
const dryRunProp = new PropBuilder()
.setName("DryRun")
.setKind("boolean")
.build();
asset.addProp(dryRunProp);
const keyTypeProp = new PropBuilder()
.setName("KeyType")
.setKind("string")
.build();
asset.addProp(keyTypeProp);
const tagSpecificationsProp = new PropBuilder()
.setName("TagSpecifications")
.setKind("array")
.setEntry(
new PropBuilder()
.setName("TagSpecificationsChild")
.setKind("object")
.addChild(
new PropBuilder()
.setName("ResourceType")
.setKind("string")
.build(),
)
.addChild(
new PropBuilder()
.setName("Tags")
.setKind("array")
.setEntry(
new PropBuilder()
.setName("TagsChild")
.setKind("object")
.addChild(
new PropBuilder()
.setName("Key")
.setKind("string")
.build(),
)
.addChild(
new PropBuilder()
.setName("Value")
.setKind("string")
.build(),
)
.build(),
)
.build(),
)
.build(),
)
.build();
asset.addProp(tagSpecificationsProp);
const keyFormatProp = new PropBuilder()
.setName("KeyFormat")
.setKind("string")
.build();
asset.addProp(keyFormatProp);
const credentialProp = new SecretPropBuilder()
.setName("credential")
.setSecretKind("AWS Credential")
.build();
asset.addSecretProp(credentialProp);
const regionSocket = new SocketDefinitionBuilder()
.setArity("one")
.setName("Region")
.build();
asset.addInputSocket(regionSocket);
// Add any props needed for information that isn't
// strictly part of the object domain here.
const extraProp = new PropBuilder()
.setKind("object")
.setName("extra")
.addChild(
new PropBuilder()
.setKind("string")
.setName("Region")
.setValueFrom(
new ValueFromBuilder()
.setKind("inputSocket")
.setSocketName("Region")
.build(),
).build(),
)
.build();
asset.addProp(extraProp);
return asset.build();
}