Skip to content

App Extensions

Available since 6.4, current schema since 7.4

Application (“app”) extensions add a custom deployable product to MetalSoft: a button in the Infrastructure Designer that renders a form, provisions infrastructure (servers, networks, IPs, DNS records, shared drives) and runs your Ansible playbooks to install and manage the product on top — for example an OpenShift or VMware Cloud Foundation cluster.

This page describes the lifecycle. For the full definition schema see the extension definition reference, and for the Ansible side (inventory, variables, scaling) see Structuring Ansible bundles.

  1. The user clicks the extension’s button in the Infrastructure Designer and a form is shown with the fields defined in the inputs section:

  1. The instance arrays, logical networks, IP allocations and DNS records declared in the infrastructure section are created, with input values substituted via #/input/<label> references.
  2. Upon Deploy, the resources are provisioned and the onCreate tasks execute (preDeploy tasks before provisioning completes, postDeploy tasks after the servers are up).
  1. The user clicks the deployed extension in the Infrastructure Designer and a form opens allowing changes to the editable input parameters (inputs marked setOnly: true cannot be changed).
  2. Upon deploy, resources are adjusted (e.g. instances added or removed when a count input changed) and the onEdit tasks execute.

This is also how scale-out and scale-in work: the platform tells your playbooks which hosts are being added or removed via dedicated inventory groups. See Scale-out and scale-in.

  1. The user deletes the extension instance in the Infrastructure Designer.
  2. Upon deploy, the onDelete tasks execute and then all resources are released.

A minimal application extension definition (extension.json). Note the array form of the lifecycle sections — each entry binds a stage (preDeploy or postDeploy) to a list of tasks:

{
"kind": "ExtensionDefinition",
"schemaVersion": "1.1",
"name": "My Cluster",
"label": "my_cluster",
"extensionType": "application",
"vendor": "MetalSoft",
"extensionVersion": "1.0.0",
"description": "Deploys my clustered product",
"icon": "none",
"dependencies": {
"controllerVersion": "v7.4.0"
},
"inputs": [
{
"label": "compute_nodes",
"name": "compute_nodes",
"inputType": "ExtensionInputInteger",
"options": { "minValue": 1 },
"defaultValue": 3
},
{
"label": "compute_node_server_type",
"name": "compute_node_server_type",
"inputType": "ExtensionInputServerType",
"options": {}
},
{
"label": "compute_nodes_os_template",
"name": "compute_nodes_os_template",
"inputType": "ExtensionInputOsTemplate",
"options": { "osFamily": "Linux" }
}
],
"outputs": [
{
"label": "console_url",
"name": "console_url",
"outputType": "string"
}
],
"infrastructure": {
"instanceArrays": [
{
"label": "compute",
"instanceCount": "#/input/compute_nodes",
"serverType": "#/input/compute_node_server_type",
"osTemplate": "#/input/compute_nodes_os_template",
"customVariables": [
{ "name": "cluster_role", "value": "worker" }
]
}
]
},
"assets": [
{
"label": "my_ansible_bundle",
"name": "My Ansible bundle",
"assetType": "AnsibleBundle",
"url": "https://repo.example.com/extensions/my-cluster-v1.0.0.zip"
}
],
"onCreate": [
{
"stage": "postDeploy",
"tasks": [
{
"label": "createStep",
"taskType": "ExtensionTaskAnsible",
"options": {
"asset": "my_ansible_bundle",
"playbook": "deploy.yaml",
"executionTimeout": 3600,
"executionTimeoutTick": 30
}
}
]
}
],
"onEdit": [
{
"stage": "preDeploy",
"tasks": [
{
"label": "scaleInStep",
"taskType": "ExtensionTaskAnsible",
"options": {
"asset": "my_ansible_bundle",
"playbook": "scale.yaml",
"executionTimeout": 3600,
"executionTimeoutTick": 30
}
}
]
},
{
"stage": "postDeploy",
"tasks": [
{
"label": "scaleOutStep",
"taskType": "ExtensionTaskAnsible",
"options": {
"asset": "my_ansible_bundle",
"playbook": "scale.yaml",
"executionTimeout": 3600,
"executionTimeoutTick": 30
}
}
]
}
],
"onDelete": [
{
"stage": "preDeploy",
"tasks": [
{
"label": "cleanupStep",
"taskType": "ExtensionTaskAnsible",
"options": {
"asset": "my_ansible_bundle",
"playbook": "cleanup.yaml",
"executionTimeout": 1800,
"executionTimeoutTick": 30
}
}
]
}
]
}

Complete production examples (OpenShift, VMware Cloud Foundation, Incus) are available in the metalsoft-extensions repository.

Definitions written for 6.4/7.0 used infrastructure.instanceGroups and lifecycle sections keyed by stage (onCreate: { postDeploy: [...] }). The current schema uses infrastructure.instanceArrays and lifecycle sections as arrays of { "stage": ..., "tasks": [...] } objects, as shown above.

metalcloud-cli extension list # list all extensions
metalcloud-cli extension get # get an extension
metalcloud-cli extension create # create an extension (kind: application | workflow | action)
metalcloud-cli extension update # update an extension (draft only)
metalcloud-cli extension publish # draft -> active
metalcloud-cli extension archive # archive an extension
metalcloud-cli extension make-public # control visibility

It is not possible to delete an extension, only to archive it. Publishing an extension allows users other than the owner to deploy it. See the Extensions overview for the full lifecycle.

Note that after an extension is published, it must also be enabled per site, on the site configuration page. This is also where the extension’s configVars are set — the operator-provided, site-level values (e.g. DNS resolvers) that every instance of the extension receives at runtime.

A separate set of extension-instance commands manipulates extension instances — the deployments users create from an extension.