Component model

Manifests version 2.0 use a reactive component model to describe resource allocation and their interaction with the Controller Fabric. A component is a single object that is allocated, managed and destroyed by the Controller Fabric. Anything can be a component (e.g. virtual machines, files, resource pools, key stores, etc.).

You can describe a component in a manifest the following way:

application:
    components:
        workflow:
            type: workflow.Instance

Here, application is a root header element and workflow is an arbitrary component name. Component type sets a piece of code that the Fabric will use to instantiate the component (similar to a class name in your OOP programming language). For now, the only available component types are those built into the platform.

The component instance above will not do anything. To interact with the outside world, each component defines a set of zero or more interfaces:

application:
    components:
        workflow:
            type: workflow.Instance
            interfaces:
                keypair:
                    add-keypair:           send-command(string id, object privateKey)
                    get-keypair:           send-command(string id => object privateKey)
            required:
              - keypair

In the code above, “keypair” is the interface name and all the records inside it are interface members called pins. Any component that is willing to consume the keypair interface will expose a matching complimentary interface, which consists of similar receive-command pins of the same types.

An interface can be marked as required, resulting in two consequences: a requiring component will start only when there is an active peer on this interface and a required component won’t stop while someone requires it. Required interfaces are listed under the required element.

An interface can be either static or dynamic. A dynamic interface is written in the manifest under the assumption that the component type used will know what to do with the specified calls. A static interface is built into the component and does not need to appear in the manifest.

Pin Types

Commands typically affect a component’s state or ask for a response by providing some input parameters. In the example above, add-keypair has two input parameters: a key id, and a private key with respective types of string and object (we’ll talk more about primitive types later). The get-keypair command accepts an ID and returns a corresponding private key. The => sign separates input and output parameters. Some commands can also have intermediate results that are returned to the sender before the command completes. One example when such intermediate results can be convenient is a shell command, progressively returning chunks of its output stream to the caller. The type of intermediate result is separated by an additional => before the final result type.

execute-workflow: receive-command(object request => object status => object status)

As mentioned previously, the complementary type for receive-command is send-command.

Signals are values that evolve over time. When a component exposes a signal via a publish-signal(string) pin, the receiving side can choose to be notified of its updates using consume-signal(string). One signal publisher can service multiple signal consumers.

Events allow you to send multicast notifications. The pin types used are publish-event and consume-event. One event publisher can serve multiple event consumers.

The last pin type is configuration. Configuration is used to pass static configuration values around.

Two pins are considered complementary if they have the same name and complementary types. Interface B is complimentary to A if it receives all commands A sends and publishes all signals and events that A receives.

The supported primitive types are the following:

  • int
  • bool
  • string
  • unit (a void type)

There are also polymorphic (generic) types, including the following:

  • map<keyType, valueType>
  • list<valueType>

The type object refers to a record with arbitrary fields, as documented by a corresponding component.

Pin Metadata

You can attach some metadata to component pins. This metadata doesn’t affect pin matching and is only used on the UI. When you add metadata to a pin definition instead of:

ip: publish-signal(string)

write:

ip:
  type: publish-signal(string)
  # pin metadata here

Metadata can also be attached to command arguments and results:

calculate:
  type: receive-command(string arg => string res)
  arguments:
    arg:
      # command argument metadata here
  results:
    res:
      # command result metadata here

The available metadata entries are:

Names can be added to outcoming signals, configuration values, incoming command declarations, command arguments and results:

ip:
  type: publish-signal(string)
  name: IP address

Value suggestions can be added to configuration values and command arguments:

port:
  type: configuration(int)
  suggestions:
    HTTP:  80
    HTTPS: 443

Suggestions are represented as a map where keys are displayed to a user and values go to configuration.

Default values can be added to command arguments:

calculate:
  type: receive-command(int a, int b, string op => string result)
  arguments:
    op:
      name: Operation
      default: "+"
      suggestions:
        Sum:      "+"
        Multiply: "*"

An arbitrary user-defined data can be added to any pin under the userData key:

latency:
  type: publish-signal(int)
  userData:
    widget: bars
    y-axis: milliseconds

Composites

So far, we’ve discussed interfaces and pins, but haven’t shown any way to interlink two components together. To do so, one must use a composite:

  application:
    components:
        keyStore:
            type: cobalt.common.KeypairStore
        workflow:
            type: workflow.Instance
            interfaces:
                keypair-consumer:
                    add-keypair:           send-command(string id, object privateKey)
                    get-keypair:           send-command(string id => object privateKey)
    bindings:
        - [keyStore, workflow]

A composite is a component that aggregates other components and ties them together. The bindings section enumerates pairs of components that will be tested for having complementary interfaces and bound. In the example above, the binding enables the component (the one belonging to the workflow.Instance component type) to call commands on the key store. Note that the manifest is not complete and will not run because the workflow component is not configured properly.

Declaring interfaces of composites

A composite can define interfaces to be used to talk to the Tonomi Portal and other components inside hierarchical applications. The following is a relevant section for the sample above:

interfaces:
    keystore:
        get-public-key: bind(keyStore#keypair.get-public-key)
        generate-keypair: bind(keyStore#keypair.generate-keypair)

We have aliased two pins from one of the components and turned them into the root composite’s pins. The bind directive is called a mapping directive.

If a composite interface declared in that manner references any of required interfaces, the resulting interface will be required too.

If you are mapping a configuration pin then a composite will provide its supplied configuration values to pins listed in the bind directive. For configuration mappings, several pins can be specified using comma:

interfaces:
    input:
        imageId: bind(zookeeper#input.imageId, solr#input.imageId)

Since Platform version 42, composites can override their subcomponents’ pin metadata:

port:
  mapping: bind(webserver#configuration.port)
  name: Port to expose
  suggestions:
    HTTP:  80
    HTTPS: 443

All metadata values, which can be declared on subcomponent pins, can be specified on mapped pins. Metadata specified on mapped pin overrides all metadata keys that came from subcomponent pin. For example, if you have subcomponent pin with name and suggestions, and have specified only name on the mapped pin, then it will not have suggestions in its metadata.

Full interface mappings

Since release 35, a new shortcut syntax was introduced to allow to map all pins in an interface using a single bind directive. For the keystore example above, this is an equivalent code:

interfaces:
    keystore:
        "*": bind(keyStore#keypair.*)

To map all pins in an interface, * sybmol should be used instead of a pin name on both sides of the declaration. Pins declared on the composite interface will have same names as pins in the referenced interface. At runtime a behavior of such shortcut syntax is absolutely the same as if they were listed manually.

Note

You can not use complex wildcards like *-keypair on either side of a declaration.

Also, a short syntax for mappings can be intermixed with the regular one:

interfaces:
    management:
      reboot-solr: bind(solr#actions.reboot)
      "*": bind(zookeeper#actions.*)

If you need to merge several interfaces into a single interface you can specify them using comma. In this case they should not have pins with same names, except for those that have configuration type, otherwise a validation error will occur.

interfaces:
    management:
      "*": bind(zookeeper#actions.*, solr#actions.*)

Configuration

Some components need to be configured before they start. Configuration is a YAML literal, the exact format and interpretation is up to a specific component. The configuration section is located under the component’s root:

workflow:
    type: workflow.Instance
    configuration:
        configuration.workflow:
            launch:
                parameters
                ....

Configuration pins typically serve as indicators for outside components that use a particular value as a launch parameter. A component can impose a certain configuration format that allows references to such configuration pins.

workflow:
    type: workflow.Instance
        interfaces:
            input:
                app-fork: configuration(string)
                app-branch: configuration(string)
        configuration:
            input.app-fork: dieu
            input.app-branch: HEAD

Configuration pins do not partake in bindings.