Rules

Rules lay over the provider’s schemas what a Terraform module adds around a bare resource, and say which HTTP error a provider’s failure stands for:

resources:
  foreman_host:
    attributes:
      name:
        pattern: '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$'
      provision_method:
        enum: [build, image]
      enable_bmc:
        hidden: true                      # not the caller's choice
  foreman_domain:
    attributes:
      fullname:
        default: spec.name                # when the caller sends none
  grafana_folder:
    attributes:
      org_id:
        value: ns                         # the service's, not the caller's
        hidden: true                      # nor shown to them
settingcontractrequest
valueattribute removed: sending it is a 400set by the service; a resource whose state holds another value is another namespace’s (404)
defaultoptional, x-default and the description name the expressionused when the attribute is absent; on an update the current value wins, so a rename keeps the uid
pattern, minLength, maxLength, enumadded to the attribute’s schemachecked by the contract, and on a default’s result
hiddenattribute removed from request and responsesending it is a 400; the provider’s default applies, or the rule’s value/default
descriptionreplaces the provider’s

An unknown key, a misspelt maxlength included, stops the service.

Errors#

pkg/tofu classifies what it can: a configuration the provider refuses is a 422 invalid_configuration, a change that needs replacing the resource a 409 replace_required, an unknown id a 404. Any other failure is a 502, with the provider’s text. The errors list matches that text with regular expressions, in order, and gives the HTTP error it stands for; one none matches stays a 502 upstream_error:

errors:
  - code: already_exists                       # the problem's "code"
    match: 'statusCode: \[422\][\s\S]*has already been taken'
    status: 409
  - code: upstream_unavailable
    match: connection refused|no such host
    status: 503
    retryable: true

In single quotes, YAML keeps backslashes as they are: '\(' is the regular expression \(. title defaults to the status’s text.

Tested with Grafana: see Testing.

Expressions#

value and default are CEL expressions, the language of Kubernetes’ x-kubernetes-validations, with its strings and regex extensions:

spec.title.replace(" ", "-")Platform Team → Platform-Team
spec.title.lowerAscii()lower case
"%s-%s".format([namespace, spec.title])formatting
regex.replace(spec.title, "[^a-z]", "_")regular expressions
has(spec.uid) ? spec.uid : "none"whether the caller sent an attribute

value reads ns, the namespace, only (CEL reserves the word namespace); default reads ns and spec.<attribute>. JSON numbers are doubles in CEL, which does not mix them with integers: spec.size + 1.0, not spec.size + 1. The file is checked against the schemas at start: a type the provider does not have, an unknown or computed attribute, an expression that does not compile or reads anything else stops the service.

No contract is kept in the repository: the service generates it at start from the file and the provider’s schemas, and serves it at /-/openapi/<provider>/v1.json and in Swagger UI at /-/docs. tofu-api gen writes it without starting the service, or checks a file before it is deployed: it fails as the service would.