Skip to content
Docs BETA

Build a provisioner with the starter

Goal

For platform teams. In this task you write a provisioner for one type. The starter library answers the HTTP calls of the contract for you. You write only what happens to the real resource.

Before you start

This is not available yet. The starter library has no download. You can write a provisioner without it: Provisioner API reference describes every call that the control plane makes.

Steps

Know what the starter does

The starter adds the HTTP side of the contract to a Spring Boot application. It:

Without your own class, the starter answers success for every change and provisions nothing. Your class replaces that behavior.

Write the main function

A provisioner is a Spring Boot application. The whole main function is:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class PostgresProvisionerApplication

fun main(args: Array<String>) {
    runApplication<PostgresProvisionerApplication>(*args)
}

Name the type in application.yml

The file application.yml says which pair the provisioner handles:

spring:
  application:
    name: postgres-provisioner

server:
  port: 8080

provisioner:
  stub:
    kind: managed-resource
    type: postgres

management:
  endpoints:
    web:
      exposure:
        include: health

The settings of the starter begin with provisioner.stub. This is the name of the starter, also for a real provisioner. The keys kind and type name the pair. The health setting makes GET /actuator/health answer, so that a platform can check the provisioner.

A provisioner that handles several pairs supplies its own behavior, and it does not set a single pair here.

Write the class that does the work

Write one class that implements ProvisionerBehavior, and make it a Spring bean. The starter then stops using its own answer. The interface has three methods:

interface ProvisionerBehavior {
    fun plan(changeSet: ResourceChangeSet): ProvisionerOutcome
    fun apply(changeSet: ResourceChangeSet): ProvisionerOutcome
    fun inspect(request: ResourceInspectRequest): ResourceInspectOutcome
}

This is a skeleton. It reports success for each change and does no real work:

import com.joomiz.provisioner.api.Outcome
import com.joomiz.provisioner.api.ProvisionerOutcome
import com.joomiz.provisioner.api.ProvisionerOutcomes
import com.joomiz.provisioner.api.ResourceChange
import com.joomiz.provisioner.api.ResourceChangeSet
import com.joomiz.provisioner.api.ResourceInspectOutcome
import com.joomiz.provisioner.api.ResourceInspectRequest
import com.joomiz.provisioner.api.ResourceInspectResult
import com.joomiz.provisioner.behavior.ProvisionerBehavior
import org.springframework.stereotype.Component

@Component
class PostgresBehavior : ProvisionerBehavior {

    override fun plan(changeSet: ResourceChangeSet): ProvisionerOutcome =
        outcomeOf(changeSet) { change -> "Would handle '${change.id}'. Nothing was changed." }

    override fun apply(changeSet: ResourceChangeSet): ProvisionerOutcome =
        outcomeOf(changeSet) { change -> "Handled '${change.id}'." }

    override fun inspect(request: ResourceInspectRequest): ResourceInspectOutcome =
        ResourceInspectOutcome(
            request.resources.map { ResourceInspectResult.unknown(it.id, "This provisioner cannot read the real resource.") },
        )

    private fun outcomeOf(changeSet: ResourceChangeSet, describe: (ResourceChange) -> String) = ProvisionerOutcome(
        outcome = Outcome.SUCCEEDED,
        description = "Handled ${changeSet.changes.size} change(s).",
        results = changeSet.changes.map { change ->
            ProvisionerOutcomes.resourceResult(change.id, change.environment, Outcome.SUCCEEDED, describe(change), outputs = null)
        },
    )
}

Your class must follow these rules:

Run it

Build the project, and start it as any Spring Boot application. When it is ready, the health address answers:

curl -s -w '\nHTTP %{http_code}\n' "$PROVISIONER_URL/actuator/health"
{"groups":["liveness","readiness"],"status":"UP"}
HTTP 200

PROVISIONER_URL holds the address of your provisioner.

Register it

Register the provisioner by hand, as Register a provisioner shows. Or let it register itself at start. Set these properties, with the address of your control plane and the address of the provisioner as the control plane sees it:

provisioner.stub.self-register.enabled=true
provisioner.stub.self-register.control-plane-url=https://joomiz.example.com
provisioner.stub.self-register.url=https://postgres-provisioner.example.com

The name of the registration is the value of spring.application.name. To choose another name, set provisioner.stub.self-register.name.

The registration needs a token with the role operator. Give it in this environment variable, from the secret store of your platform:

PROVISIONER_STUB_SELFREGISTER_TOKEN

Do not write it in a file of your project. Without a token, the control plane answers 401, the provisioner logs it, and it keeps running unregistered.

The start fails, and names the property, if control-plane-url or url is missing or is not an http or https address. The registration is tried up to 10 times, 3 seconds apart. You can change both values with provisioner.stub.self-register.attempts and provisioner.stub.self-register.retry-delay.

Test it against the contract

Send a request to each call, as the control plane does. This is a change set 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": []
    }
  ]
}

Send it to POST /plan:

curl -s -w '\nHTTP %{http_code}\n' -X POST "$PROVISIONER_URL/plan" -H 'Content-Type: application/json' -d @plan-request.json

This is the answer of the skeleton above:

{"outcome":"succeeded","description":"Handled 1 change(s).","results":[{"id":"shop-db","environment":"test","outcome":"succeeded","description":"Would handle 'shop-db'. Nothing was changed.","outputs":null}]}
HTTP 200

Send the same file to POST /apply. Then ask POST /inspect what exists. Provisioner API reference shows the requests and the answers of every call, and the answers for a request that is not valid.

Check

Troubleshooting

Symptom Cause Fix
The start fails and names provisioner.stub.kind The pair is not named, and you did not write your own behavior. Set kind and type, or supply your own ProvisionerBehavior.
The start fails and names self-register.url Self-registration is on, and an address is missing. Set control-plane-url and url.
The log shows a 401 for the registration The token is missing or has no operator role. Set the token in the environment variable.
The log shows a 400 for the address The control plane may not call the address. Allow the address of your provisioner.
A plan says the provisioner sent an unexpected response The answer does not follow the contract, or the call was not sent. Compare your answer with Provisioner API reference.
The control plane says reported no result for this resource Your answer has no result for a change. Return one result for each id.

Next

Read how a provisioner reports work that takes minutes in Support slow work in a provisioner.