Skip to content

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:

typescript
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:

  1. Create the Builder.
  2. Configure the object.
  3. 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().

typescript
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.

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
const throughputProp = new PropBuilder()
    .setName("Throughput")
    .setKind("integer")
    .build()
asset.addProp(throughputProp);

Map properties

A map named ResourceTags:

typescript
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:

typescript
{
  "van halen": "great band",
  "beyonce": "also great"
}

Any property kind is a valid map entry.

A more complex map of objects:

typescript
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:

typescript
{
  "van halen": { "bacon": false },
  "beyonce": { "bacon": true },
}

Object properties

An AdvancedConfiguration object, with 3 properties: InstanceProfileArn, EbsOptimized, and UserData:

typescript
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:

typescript
{
  InstanceProfileArn: "arn:...",
  EbsOptimized: false,
  UserData: "...",
}

Array properties

A SecurityGroups array of strings:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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

  1. 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.
  2. 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:

shell
$ deno run ./main.ts asset ec2 create-key-pair

The resulting schema definition will correspond to the CLI skeleton of the AWS CLI:

typescript
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();
}