Manage Spotlight with Terraform

Once a workspace is under Terraform, its teams, lines, skills, integrations, hooks, harness profiles, secrets, token trusts and runner configuration change through terraform apply. You need:

  • Terraform 1.11 or later. The module holds write-only values, such as secret values, which older versions cannot send.
  • A workspace token for the workspace. If you don't have one, issue a workspace token with the scopes for what the module manages: configure, plus secrets if it holds secrets, runners if it holds runner pools or runner trusts, and trusts if it holds workspace token trusts.

Export the workspace

To have the agent write the first Terraform for you, adapted to your code and tools, let the agent write your team's first Terraform instead, then go on to Point the provider at Spotlight.

Otherwise, write the workspace's export into the folder where the configuration will live, usually a repository of its own. You need curl and jq. Put the token in SPOTLIGHT_API_TOKEN, replace <slug> with the workspace's short name from its URL, and run this in that folder:

export SPOTLIGHT_API_TOKEN=<workspace token>
api=https://app.usespotlight.ai/api/resources/workspaces
id=$(curl -fsS -H "Authorization: Bearer $SPOTLIGHT_API_TOKEN" "$api/import?address=<slug>" | jq -r .id)
curl -fsS -H "Authorization: Bearer $SPOTLIGHT_API_TOKEN" "$api/$id/export" \
  | jq -c '.files[]' | while read -r file; do
      path=$(jq -r .path <<<"$file"); mkdir -p "$(dirname "$path")"
      jq -j .content <<<"$file" > "$path"
    done

It writes:

FileHolds
main.tfA resource for everything in the workspace, each with an import block beside it
variables.tfA sensitive variable for each value Spotlight never answers, such as a secret's value; left out when there are none
prompts/Each station's prompt, as prompts/<team>/<line>/<station>.md, read with file()
judges/Each station judge's rubric, as judges/<team>/<line>/<station>.md, read with file(); left out when no judge has a rubric. Claims are written in the judge block.
skills/Each skill's files, as skills/<team>/<skill>/, read with file()

Point the provider at Spotlight

Give the provider Spotlight's address and the token through the environment. On a self-hosted Spotlight, use your deployment's address in place of app.usespotlight.ai.

export SPOTLIGHT_URL=https://app.usespotlight.ai
export SPOTLIGHT_API_TOKEN=<workspace token>

main.tf has no provider block. To set the address and token in the configuration instead, add one and keep the token in a variable:

provider "spotlight" {
  url   = "https://app.usespotlight.ai"
  token = var.spotlight_token
}

variable "spotlight_token" {
  type      = string
  sensitive = true
}

Keep the token out of version control.

Give the variables their values

Give each variable in variables.tf that has no default the value Spotlight holds for it, for example in a terraform.tfvars file you keep out of version control:

secret_linear_api_key = "<the secret's value>"

A variable for a pull pool's token defaults to null and needs no value to adopt the pool.

Adopt the workspace

terraform init
terraform apply

The first apply imports every resource through its import block and plans no change. If the plan shows changes, the workspace changed after you downloaded it: download it again and repeat.

After the apply you can delete the import blocks, or leave them; Terraform ignores an import block for a resource already in its state.

Make changes

Edit the configuration and run terraform apply. The plan checks every expression in an integration or a hook against Spotlight before anything is written, so a misspelt field fails the plan at the attribute that holds it.

If a plan fails with 403, the diagnostic names the scope the token lacks. Issue a new token that also has that scope.

If the configuration is changed outside Terraform, through the resource API, the next plan changes it back.

Add a workspace to an existing configuration

A token cannot create a workspace. A person creates one by signing up, and Terraform adopts it with an import block that names its slug:

import {
  to = spotlight_workspace.acme
  id = "acme"
}

resource "spotlight_workspace" "acme" {
  name = "Acme"
  slug = "acme"
}

Every other resource can be imported by an id of the form <workspace id>/<address>, as the downloaded module does, or by its identity in Terraform 1.12 and later:

import {
  to = spotlight_line.core_delivery
  identity = {
    id = "<line id>"
  }
}

Stop managing a workspace

To stop managing the workspace without deleting it, replace its resource with a removed block:

removed {
  from = spotlight_workspace.acme

  lifecycle {
    destroy = false
  }
}

On this page