Provisioner API reference
For platform teams. This page lists what a provisioner must implement: the calls, the fields, the limits and the answers. The contract is version 0.2.0 of the Joomiz provisioner API. It uses OpenAPI 3.1.
What a provisioner is explains the idea. Build a provisioner with the starter shows how to write one.
Rules for the whole API
- The control plane calls the provisioner. The provisioner never calls the control plane for the work of a plan or an apply.
- The bodies are JSON. Every object is closed, so a field that this page does not list is refused. The exception is
specandoutputs, which take any object that the catalog entry of the type allows. - Every call is for one application and one pair of a category (
kind) and a type. - A failure of the work is a
200answer with the outcomefailed. A status other than200means that the call itself failed. - Text fields are free text. The control plane limits their length and removes control characters before it shows them.
- The answers on this page have line breaks and spaces added, so that you can read them. A provisioner can send the same JSON on one line. The words in
descriptionare examples.
Calls
| Call | Required | Request | Answers |
|---|---|---|---|
POST /plan | Yes | Change set | 200 outcome, or 202 operation |
POST /apply | Yes | Change set | 200 outcome, or 202 operation |
POST /inspect | Yes | Inspect request | 200 inspect outcome |
GET /operations/{operationId} | Only for a provisioner that answers 202 | None | 200 operation outcome, or 404 |
The paths are relative to the address that you register.
plan
POST /plan shows what apply would do. It changes nothing in the system that the provisioner manages. This is a request for one database. Save it as plan-request.json:
{
"applicationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"kind": "managed-resource",
"resourceType": "postgres",
"changes": [
{
"id": "shop-db",
"environment": "test",
"action": "created",
"description": "Create a PostgreSQL database named shop-db.",
"spec": {
"flavor": "cf-plan-small",
"version": "16",
"storage-gb": 20,
"multi-az": false
},
"unresolvedFields": []
}
]
}
curl -s -w '\nHTTP %{http_code}\n' -X POST "$PROVISIONER_URL/plan" -H 'Content-Type: application/json' -d @plan-request.json
{
"outcome": "succeeded",
"description": "Handled the database shop-db.",
"results": [
{
"id": "shop-db",
"environment": "test",
"outcome": "succeeded",
"description": "Handled the database shop-db.",
"outputs": {
"password": "https://vault.example.com/secrets/shop-db-password"
}
}
]
}
HTTP 200
apply
POST /apply makes the changes. It must give the same end state when it runs again with the same change set. A repeat for a resource that is already correct is a success. The request is the same as for plan, and unresolvedFields is always empty.
{
"outcome": "succeeded",
"description": "Handled the database shop-db.",
"results": [
{
"id": "shop-db",
"environment": "test",
"outcome": "succeeded",
"description": "Handled the database shop-db.",
"outputs": {
"password": "https://vault.example.com/secrets/shop-db-password"
}
}
]
}
HTTP 200
inspect
POST /inspect reports what the listed resources look like now. It changes nothing and is safe to repeat. The control plane calls it before it computes a plan, to find drift. It has no 202 answer.
{
"applicationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"environment": "test",
"resources": [
{
"kind": "managed-resource",
"resourceType": "postgres",
"id": "shop-db",
"spec": {
"flavor": "cf-plan-small",
"version": "16",
"storage-gb": 20,
"multi-az": false
}
}
]
}
{
"results": [
{
"id": "shop-db",
"state": "present",
"spec": {
"flavor": "cf-plan-small",
"version": "16",
"storage-gb": 20,
"multi-az": false
},
"reason": null
}
]
}
HTTP 200
Say only what you know:
presentcarries the real settings that you can read. Leave out a setting that you cannot read.absentmeans that the resource does not exist.unknowncarries a short reason. Use it when you cannot tell. Never answerpresentwith a guess.
The control plane does not send a setting whose recorded value is a ${{ ... }} reference. Do not report such a setting back.
Any status other than 200 is a failed inspection. For 404, 405 and 501, the plan shows that the provisioner does not implement inspection, and the plan continues from the record.
operations
GET /operations/{operationId} answers for an operation that you accepted with 202. The answer is always 200, and the outcome is in the body. Support slow work in a provisioner shows the flow.
{
"outcome": "in_progress",
"description": "Pipeline run 4821 is at the deploy stage.",
"results": []
}
HTTP 200
An unknown ID answers 404 with no body. The control plane fails the work at once.
Fields of the requests
Change set
The body of plan and apply.
| Field | Type | Rule |
|---|---|---|
applicationId | string | Required. A UUID of exactly 36 characters. |
kind | string | Required. One of target, tool, deployment, managed-resource and consumed-resource. |
resourceType | string | Required. At most 255 characters. |
changes | array | Required. From 1 to 1000 changes. |
A provisioner that receives a pair it does not handle answers 200 with the outcome failed.
Change
| Field | Type | Rule |
|---|---|---|
id | string | Required. At most 255 characters. It is unique for one environment. |
environment | string or null | Required. At most 255 characters. It is null for a tooling change. |
action | string | Required. One of created, updated and removed. |
description | string | Required. At most 4096 characters. Text for a person. Do not parse it. |
spec | object or null | Required. The settings of the resource, as the catalog entry of the type allows. It is null for removed. |
unresolvedFields | array of strings | Required. Up to 512 entries of up to 1024 characters. Names the settings that hold a placeholder because their value is not known yet. |
Treat id, environment and resourceType as names, and check them before you use them to name or find anything. The values of spec come from a user. Check every value against what your system accepts.
Inspect request
| Field | Type | Rule |
|---|---|---|
applicationId | string | Required. A UUID of exactly 36 characters. |
environment | string | Required. At most 255 characters. |
resources | array | Required. From 1 to 1000 resources. |
Each resource has the required fields kind, resourceType (at most 255 characters), id (at most 255 characters) and spec. The spec is the configuration that Joomiz applied last.
Fields of the answers
Outcome
The answer of plan and apply with status 200.
| Field | Type | Rule |
|---|---|---|
outcome | string | Required. succeeded or failed. There is no partial outcome. |
description | string | Required. At most 4096 characters. Put the reason here when the outcome is failed. |
results | array | Required. From 0 to 1000 results. Return one for each change. |
The control plane matches a result to a change by id. A change with no result counts as failed. Either level can fail the call.
Result
| Field | Type | Rule |
|---|---|---|
id | string | Required. The id of the change, unchanged. At most 255 characters. |
environment | string or null | Required. The environment of the change, unchanged. |
outcome | string | Required. succeeded or failed. |
description | string | Required. At most 4096 characters. |
outputs | object or null | Required. What the change produced, or null. Always null for failed. |
The control plane checks outputs against the outputs entry of the type in the catalog. An output that does not fit fails that result. Report a sensitive output as an https:// address, with no user name and no password, that names where the value is stored.
Accepted operation
The answer 202 of plan and apply.
| Field | Type | Rule |
|---|---|---|
operationId | string | Required. A UUID of exactly 36 characters, chosen by the provisioner. |
The body has no other field.
Operation outcome
The answer 200 of GET /operations/{operationId}.
| Field | Type | Rule |
|---|---|---|
outcome | string | Required. in_progress, succeeded or failed. |
description | string | Required. At most 4096 characters. A progress note, and the reason at the end. |
results | array | Required. From 0 to 1000 results. Empty while in_progress. |
A final answer is final. The control plane never asks again about that ID.
Inspect outcome
The answer 200 of POST /inspect.
| Field | Type | Rule |
|---|---|---|
results | array | Required. From 0 to 1000 inspect results. Return one for each resource. |
Each inspect result has four required fields:
| Field | Type | Rule |
|---|---|---|
id | string | The id of the resource, unchanged. At most 255 characters. |
state | string | present, absent or unknown. |
spec | object or null | The real settings for present. null otherwise. |
reason | string or null | A short reason for unknown. At most 512 characters. null otherwise. |
A resource with no result, or with more than one, counts as unknown. A result that breaks these rules counts as unknown.
Answers for a bad request
A provisioner that is written with the starter refuses a request that is not valid with 400 and a problem detail. The body lists each problem under violations. This is the answer for a request with an extra field resourceTyp:
{"detail":"the request body is not a valid ResourceChangeSet: resourceTyp is not a recognised field","instance":"/plan","status":400,"title":"Malformed change set","type":"https://joomiz.example.invalid/problems/provisioner/malformed-change-set","violations":["resourceTyp is not a recognised field"]}
HTTP 400
This is the answer for an empty list changes:
{"detail":"the request body is not a valid ResourceChangeSet: changes must hold at least 1 entry","instance":"/plan","status":400,"title":"Malformed change set","type":"https://joomiz.example.invalid/problems/provisioner/malformed-change-set","violations":["changes must hold at least 1 entry"]}
HTTP 400
This is the answer for a body that ends in the middle of the JSON:
{"detail":"the request body is not a valid ResourceChangeSet: body is not readable JSON: Unexpected end-of-input: expected close marker for Object (start marker at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN])","instance":"/plan","status":400,"title":"Malformed change set","type":"https://joomiz.example.invalid/problems/provisioner/malformed-change-set","violations":["body is not readable JSON: Unexpected end-of-input: expected close marker for Object (start marker at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN])"]}
HTTP 400
The control plane treats every status other than 200 and 202 as a failed call. A pair that the provisioner does not handle is not a bad request. The provisioner answers 200 with the outcome failed:
{
"outcome": "failed",
"description": "This provisioner owns managed-resource/postgres; the change set names managed-resource/mysql.",
"results": [
{
"id": "shop-db",
"environment": "test",
"outcome": "failed",
"description": "This provisioner owns managed-resource/postgres; the change set names managed-resource/mysql.",
"outputs": null
}
]
}
HTTP 200
Limits
| What | Limit |
|---|---|
changes and resources in a request | 1 to 1000 entries |
results in an answer | 0 to 1000 entries |
id, environment and resourceType | 255 characters |
description | 4096 characters |
unresolvedFields | 512 entries of 1024 characters |
reason of an inspect result | 512 characters |
applicationId and operationId | exactly 36 characters |
| Longest wait of an operation | 30 minutes (PT30M) |
| Time between two polls of an operation | 10 seconds by default. The control plane setting joomiz.jobs.poll-interval changes it |
Version
This page describes version 0.2.0. The call POST /inspect is required in this version. A provisioner written for version 0.1.0 does not serve it, so it is not a version 0.2.0 provisioner. The control plane still plans with such a provisioner. It reports that drift detection failed for the type, and it plans from its own record.
The paths have no version prefix.
See also
- Build a provisioner with the starter shows how to answer these calls.
- Support slow work in a provisioner explains the
202answer. - Register a provisioner shows how to tell the control plane about yours.