# Terraform Provider for Obot
> [!WARNING]
> This provider is under active development. Resources, attributes, and
> behavior are subject to change.
A Terraform provider for Obot, built with the Terraform Plugin Framework.
The provider connects with an API-scoped key and manages MCP tunnels,
individual default MCP catalog sources, remote MCP catalog entries, Git
credentials for private sources, and catalog MCP access policies. Read-only data sources expose current
identity, authentication-provider metadata, tunnel metadata, catalog
inventory, Git credential IDs, and access policies. It is published on the
Terraform Registry as [`obot-platform/obot`](https://registry.terraform.io/providers/obot-platform/obot).
See the [catalog and access-policy guide](docs/guides/catalogs-and-access-policies.md)
for adding, adopting, and removing catalog sources, looking up the
current authentication provider, and importing or removing Everything.
See the [tunnel guide](docs/guides/mcp-tunnels.md) for tunnel credentials and import.
## Use the provider
Obtain an API-scoped key for the target server with the Obot CLI:
```shell
export OBOT_BASE_URL="https://obot.example.com/api"
export OBOT_TOKEN="$(obot login --scope api --print-token)"
```
Then use the provider:
```hcl
terraform {
required_providers {
obot = {
source = "obot-platform/obot"
version = "~> 0.1"
}
}
}
provider "obot" {}
data "obot_current_user" "current" {}
```
The provider requires Terraform 1.0 or later; write-only Git credential tokens
require Terraform 1.11 or later, and importing by resource identity
requires Terraform 1.12 or later. Releases are tested with Terraform 1.14
against `main` and the newest patch of each Obot release series listed in
[`obot-versions.json`](obot-versions.json) (currently v0.26 and v0.27).
Use an Owner or Admin API key to manage catalog sources and access policies.
The provider accepts `base_url` and sensitive `token` attributes; explicit
values take precedence over `OBOT_BASE_URL` and `OBOT_TOKEN`. Supply an app
URL or API base URL; a trailing `/api` is added when needed. Configuration
validates API-key scope and the authenticated identity before reads.
Each provider configuration owns an independent client, including aliases
pointing at different Obot instances.
To test unreleased provider changes, use the development overrides described
under [Build and local Terraform development](#build-and-local-terraform-development).
## Development requirements
- Go >= 1.25.8 for the provider, as specified in `go.mod`
- Go >= 1.27 to build the current Obot CLI from source
- Terraform >= 1.13
- Python >= 3.10 for local test tooling
- Docker running locally for Obot test instances
- OpenSSL >= 1.1.1 for the HTTPS catalog test fixtures
- GNU Make and golangci-lint v2.14.0
Provider source: `obot-platform/obot`. Go module:
`github.com/obot-platform/terraform-provider-obot`.
## Obtain Obot source and CLI
Two modes are supported:
| Ref | Server | CLI |
| --- | --- | --- |
| `main` (default) | Published `ghcr.io/obot-platform/obot:main` image | Built from the main source checkout |
| Version tag, such as `v0.26.3` | Published image for that tag | GitHub release binary verified against its published checksum |
```shell
make obot-source
make obot-cli
make obot-source OBOT_REF=v0.26.3
make obot-cli OBOT_REF=v0.26.3
```
Source and CLI caches live under ignored `.obot/`. Main is intentionally a
moving development target. Cached source advances with an explicit refresh;
each CLI build is cached by the resolved source commit. The image is pulled
when an instance starts, and its actual ID and digest are recorded. The main
image and source can be published at different times.
```shell
python3 scripts/obot.py cli --ref main --refresh
python3 scripts/obot.py cli --ref v0.26.3 --build
python3 scripts/obot.py source --ref main --directory /path/to/obot
python3 scripts/obot.py cli --ref main --directory /path/to/obot
```
An existing checkout is used without changing its files or Git refs. You can
also set `OBOT_SOURCE_DIR`. Managed source refresh refuses local changes.
CLI/source commands print the resulting path to stdout and progress to stderr.
Using a release CLI does not require checking out or building Obot.
## Run one or more test instances
```shell
make obot-up OBOT_INSTANCE=dev
make obot-up OBOT_INSTANCE=release OBOT_REF=v0.26.3
python3 scripts/obot.py status dev
python3 scripts/obot.py status release
```
Each named instance gets its own loopback port, data volume, database, local
auth sessions, and API keys. The default initial account is:
- Email: `test_obot_admin@example.com`
- Password: `TestObotAdmin123!`
- Role: Owner, including administrative permissions
These are public credentials for disposable local test instances.
Set `OBOT_TEST_PASSWORD` consistently during creation and subsequent commands
to use another password. Use `up --email` to choose another initial owner.
The automatic multiple-instance test runner uses
`test_obot_admin_2@example.com`, `test_obot_admin_3@example.com`, etc. for
additional owners so tests can detect connections to the wrong instance.
Startup activates the local owner, sets the password, and waits for the
default catalog. No external identity provider or model-provider key is needed.
```shell
eval "$(python3 scripts/obot.py env dev)"
OBOT_TOKEN="$(python3 scripts/obot.py token dev)" && export OBOT_TOKEN
```
This works in a sandbox or terminal without a desktop keyring. The helper
signs in with the known local-auth credentials, creates an API-scoped key,
and invokes `obot login --scope api --print-token` with that key in its
environment. The assignment captures the token without printing it.
On a desktop with a working OS keyring, you can instead use a fresh
interactive login:
```shell
OBOT_TOKEN="$(obot login --scope api --print-token)" && export OBOT_TOKEN
```
Sign in using the credentials above and complete its device-code prompt.
The CLI stores the key in the OS keyring. An error mentioning
`org.freedesktop.secrets` means the Linux Secret Service is unavailable;
use the test helper command above for the named local instance.
For headless tests, run a child command with credentials prepared automatically:
```shell
make test-acceptance
```
The helper signs in with local auth, creates an API-scoped seed key, and runs
`obot login --scope api --print-token` with that key. It validates the
resulting identity and passes the token only through the child environment.
This works without a browser or desktop keyring in CI.
Add users as tests need them:
```shell
python3 scripts/obot.py user-add dev test_obot_user@example.com --role basic
python3 scripts/obot.py exec dev --email test_obot_user@example.com -- make test-acceptance-env
```
Additional users use the same test password. Available roles are `basic`,
`admin`, `owner`, `auditor`, `power-user`, and `power-user-plus`.
Use `up --port 8080` for a fixed port, or `up --image IMAGE` for a custom
image. Obot's Docker backend requires its daemon socket during startup.
Inside Discobox, the tooling mounts `/run/docker.sock:/var/run/docker.sock`.
Other environments, including GitHub Actions, use
`/var/run/docker.sock:/var/run/docker.sock`. Set `OBOT_TEST_DOCKER_SOCKET`
or use `up --docker-socket` to override the socket path on the Docker host.
Inspect failures and remove instances by name:
```shell
python3 scripts/obot.py logs dev
make obot-down OBOT_INSTANCE=dev
make obot-down OBOT_INSTANCE=release
```
Failed manual startup preserves its container and metadata for diagnosis.
The logs command saves sanitized output to `.obot/logs/`. Teardown checks
project ownership labels and removes only the named instance's container and
volume.
## Run tests
Run `make help` to see the commands. The three usual choices are:
| Command | Obot instance | Authentication and cleanup |
| --- | --- | --- |
| `make test` | None | Runs Go and Python unit tests; always disables acceptance tests |
| `make test-acceptance` | Existing `dev` instance | Logs in automatically; leaves the instance running |
| `make test-acceptance-fresh` | Creates fresh instances | Logs in automatically; removes the instances afterward |
```shell
make test
make test-acceptance
make test-acceptance OBOT_INSTANCE=release
make test-acceptance-fresh OBOT_REF=main OBOT_INSTANCES=2
make test-acceptance-fresh OBOT_REF=v0.27.0 OBOT_INSTANCES=2
```
Two further suites run the compiled provider through the Terraform CLI, as a
user's pipeline does, on fresh instances with the same catalog fixtures:
```shell
make test-e2e OBOT_REF=main
make test-upgrade UPGRADE_FROM=v0.27.0 UPGRADE_TO=main
```
`test-e2e` applies [`tests/e2e`](tests/e2e/main.tf), which uses every resource
and data source with postconditions on what the API returns. It requires an
empty plan after create, after an in-place update of every object, and after
refresh. It then discards state and re-imports every object, first by ID and
then by identity, and requires only the documented Git credential token resend.
Finally it destroys and confirms through the API that the objects are gone and
seeded defaults remain.
`test-upgrade` applies the same scenario on one version, replaces the server
with another image on the same data volume, and requires the earlier API key to
work and the plan to be empty before updating and destroying on the new version.
When the embedded PostgreSQL major version changes, as from v0.27 to main's
PostgreSQL 18, it first follows Obot's documented dump-and-restore procedure.
Both suites accept `--keep` through `python3 scripts/e2e.py` for diagnosis.
Fresh acceptance runs include tests that add, adopt, and remove default catalog
sources while leaving other sources and API-created entries in place, sync a
private Git source through a Git credential, import the seeded Everything policy,
and delete that policy while preserving another rule. The HTTPS fixture
server and temporary CA live only for the run. `openssl` is required for
these tests. `--keep` retains containers for diagnosis but stops the fixture
server. Existing-instance runs skip tests that change seeded defaults unless
`OBOT_TEST_MANAGE_DEFAULTS=1` is explicitly supplied; catalog tests also need
`OBOT_TEST_CATALOG_SOURCE_INITIAL`, `OBOT_TEST_CATALOG_SOURCE_REPLACEMENT`, and
`OBOT_TEST_CATALOG_SOURCE_EXTRA` pointing to deterministic, trusted fixture sources,
and the private Git source test needs `OBOT_TEST_CATALOG_GIT_SOURCE`,
`OBOT_TEST_CATALOG_GIT_HOST`, and `OBOT_TEST_CATALOG_GIT_TOKEN`. Use fresh instances for
the complete suite.
Choose individual acceptance tests with
`TESTARGS='-run=TestAccCurrentUser_basic'` on any acceptance target.
An existing instance must first have been created with `make obot-up` or
`scripts/obot.py up`; its saved metadata supplies the URL and owner email.
For an external instance, set `OBOT_BASE_URL`, `OBOT_TOKEN`, and
`OBOT_TEST_EMAIL`, then run `make test-acceptance-env`.
`make test-smoke OBOT_INSTANCES=2` creates fresh instances and checks Owner
and Basic-user login, denied administrative access for Basic users, and
isolation between instances. With multiple instances it also runs a Terraform plan with the
compiled provider and aliases, checking each server's distinct identity.
The fresh acceptance runner prepares the matching CLI, starts fresh
instances, runs Terraform acceptance tests, and removes the instances.
Use `python3 scripts/obot.py testacc --ref main --instances 2 --keep` to
preserve them for diagnosis. Both runners save sanitized logs on failure.
A custom test command can follow `--`.
Tests receive `OBOT_BASE_URL`, `OBOT_TOKEN`, `OBOT_TEST_EMAIL`, `TF_ACC=1`,
and `OBOT_TEST_INSTANCES` (a JSON array of named connections). Tokens are not
written to instance metadata.
Manual test instances start with empty MCP catalog sources. Fresh acceptance
instances use deterministic catalogs served locally over verified HTTPS: three
public YAML files and a private Git repository requiring a token generated for
the run. They trust a temporary test CA and allow the Docker host's private
address; these fixtures need no public catalog or external GitHub credentials.
The older `testacc`, `testacc-local`, and `obot-smoke` targets remain aliases
for `test-acceptance-env`, `test-acceptance-fresh`, and `test-smoke`.
The in-process HashiCorp acceptance harness does not isolate provider aliases;
the multi-instance acceptance test uses separate factory names. The compiled
provider's actual alias behavior is checked separately by the smoke suite.
## Build and local Terraform development
Use the existing instance's name as the target prefix:
```shell
make dev-plan
make dev-apply
make dev-plan
make dev-plan-destroy
make dev-destroy
```
For an instance named `release`, use `make release-plan`, `make release-apply`,
and so on. Create the named instance with `make obot-up OBOT_INSTANCE=release`
first if needed. These targets rebuild the provider, configure its local
development override, and authenticate automatically without a desktop keyring.
They leave the Obot Docker instance running.
The shared configuration is `examples/dev/main.tf`, which reads and outputs
the authenticated user's email. Each instance has independent local Terraform
state and plugin data under ignored `.obot/terraform/NAME/`. The helper uses
Terraform's local backend; keep the development example on that backend.
`plan` previews changes. `apply` generates a fresh plan and asks for approval.
`plan-destroy` runs `terraform plan -destroy` to preview removal; `destroy`
asks for approval before removing Terraform-managed objects. By default, the
helpers do not save plan files. The current example only reads a data source,
so destruction removes its state and output without deleting the Obot user.
Pass extra Terraform options with `TFARGS`, for example
`make dev-plan TFARGS=-no-color`.
Lower-level build and configuration commands are also available:
```shell
make build
make lint
make dev
export TF_CLI_CONFIG_FILE="$PWD/dev.tfrc"
terraform -chdir=examples/dev providers schema -json
```
`make build` writes `bin/terraform-provider-obot`; `make install` installs
to `GOBIN`, or `$(go env GOPATH)/bin` when unset. `make dev` writes an ignored
checkout-specific CLI configuration using
[Terraform development overrides](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers).
Re-run it after rebuilding or moving the checkout.
Skip `terraform init` for this example: it still attempts to resolve the
unpublished provider in the Registry. Terraform's development-override warning
is expected. `make dev-check` checks the schema and validates the configuration
without connecting to Obot. Prefer the named Make targets for plan/apply so
authentication and Terraform state stay associated with the same instance.
Use `unset TF_CLI_CONFIG_FILE` to return to your normal Terraform settings.
## MCP tunnels
Use `obot_mcp_tunnel` to define a tunnel and capture its one-time, sensitive
agent token. `obot_mcp_tunnel` and `obot_mcp_tunnels` data sources return
metadata and a token preview; Obot cannot recover a full token through reads.
```hcl
resource "obot_mcp_tunnel" "customer" {
display_name = "Customer private network"
allowed_urls = ["https://mcp.internal.example/mcp"]
token_rotation_version = 0
}
```
Import an existing definition by its generated ID:
```shell
terraform import obot_mcp_tunnel.customer mt1example
```
Terraform 1.12+ can also import with an `import` block's `identity = { id = ... }`.
Match its metadata and keep `token_rotation_version = 0` for adoption. Import
preserves its ID and credential, leaves connected agents alone, and warns
that `token` is null. It never substitutes a preview. Metadata updates and
refresh do not rotate the credential. With matching configuration, the next
plan has no changes.
Increase `token_rotation_version` to explicitly rotate and capture a new token.
Obot immediately revokes the previous credential and disconnects its agents;
update their secret and restart them. If Obot detects an external rotation,
refresh clears the stale token and warns. Secret-export examples reject a
null token with a precondition. Sensitive tokens remain in Terraform state;
use a protected backend.
The [tunnel guide](docs/guides/mcp-tunnels.md) walks through creation, AWS and
Vault publication, agent startup, import, rotation, recovery, and deletion.
See the [resource reference](docs/resources/mcp_tunnel.md) for its schema and
both import forms.
To put a tunnel behind a catalog entry, use `obot_mcp_catalog_entry`, which
manages remote entries created directly in the default catalog, and grant
access with `obot_mcp_access_rule`. Obot does not accept tunnel references in
entries synchronized from a catalog source. Header values are write-only
(`value_wo`, Terraform 1.11+). See the
[resource reference](docs/resources/mcp_catalog_entry.md).
Acceptance tests cover creation, metadata changes, empty allowlists, stable
refresh, explicit rotation, live CLI connectivity and revoked-token rejection,
CLI and import-block adoption, null-token export rejection, external deletion
and drift, data-source lookup/listing, invalid inputs, dependency failures,
and destruction. Unit tests also cover API permission failures, safe error
redaction, malformed token responses, and partially successful updates.
## Skills and documentation
The project skills are versioned under `.agents/skills/`:
- `obot-source`: acquire and inspect upstream source
- `obot-cli`: obtain the CLI and authenticate
- `obot-test-instance`: manage isolated Docker instances and test users
Run `make generate` after schema or example changes and commit the generated
`docs/` files. The generator is pinned in `tools/go.mod`.
Preview the generated documentation locally with:
```shell
make docs-preview
```
Open `http://localhost:8765/`, or set `DOCS_PORT` to choose another port.
The command installs pinned preview dependencies in an isolated environment
under ignored `.obot/` and serves the existing `docs/` files. Run
`make generate` first when schema or example changes need to be regenerated.
Stop the preview with Ctrl-C.
Use this command for local previews so their appearance stays consistent.
The checked-in theme uses a white background, dark text, light-gray navigation,
restrained blue links, and standard syntax highlighting. It stays light
regardless of desktop color preferences. Keep theme changes in
`scripts/docs_preview.css`; do not recreate temporary preview servers with
other color schemes. The local preview approximates Terraform Registry;
use the [official Registry documentation preview](https://registry.terraform.io/tools/doc-preview)
for final rendering checks. Published docs remain Markdown, styled by the Registry.
## CI and releases
CI builds and lints the provider, runs Go and tooling unit tests, and checks
generated documentation. Its acceptance jobs, which wait for the build, run the smoke, acceptance,
and end-to-end suites against `main` and each version in `obot-versions.json`,
with two Docker instances, using the same commands as local development.
### Supported Obot versions
[`obot-versions.json`](obot-versions.json) is the single list of supported Obot
versions. Each entry is a release series such as `v0.27`, meaning its newest
stable patch when CI runs, or an exact tag such as `v0.26.3` to pin one. List
the oldest supported series first; `main` is always tested. Preview what CI
will test with:
```shell
make obot-versions
```
When Obot ships a new minor release, add its series, and remove series you stop
supporting. You do not have to do this before CI notices: the nightly run
finds the release itself.
### Nightly compatibility tests
Obot changes daily while this repository may not, so
[`nightly.yml`](.github/workflows/nightly.yml) runs every day at 07:17 UTC,
and on demand from the Actions tab. It needs no secrets: Obot runs in Docker on
the runner, and issues use the workflow's own `GITHUB_TOKEN`.
| Leg | Versions | What it catches |
| --- | --- | --- |
| Acceptance | `main` and each supported release | API changes affecting any resource or data source behavior |
| End to end | `main` and each supported release | Breaks visible only through the compiled binary and Terraform CLI, import, and refresh |
| Upgrade | each release to the next, newest release to `main` | Server migrations that change existing objects, leaving a non-empty plan |
| Released provider | newest provider tag against each version | Breaks in the provider users actually run; skipped until the first release |
Discovery: if Obot has a stable release newer than every listed series, the
nightly run tests it too and opens an `obot-version-drift` issue asking for the
file to be updated. The issue closes when it is.
Flakes: each leg retries once. A leg that passes only on retry is shown as
flaky in the summary but does not fail the run; repeated failures do.
Provenance: each leg records the image digest and the Obot commit the server
reports. For `main`, the Obot CLI used by the tests is built from that same
commit, so an image published before or after the source cannot cause a false
failure.
Alerting: a scheduled run that fails opens one `nightly-failure` issue, or
comments on the open one, with the summary table. When `main` fails, the
summary links the Obot commit range since the last passing run, which is
usually where to look. The issue closes on the next passing run. Manual runs
change issues only with the `manage_issues` input.
Reproduce a failing leg locally with the `make` command shown in its job log;
sanitized server logs are in each leg's artifact.
Pushing a `v*` tag, such as `v0.1.0`, runs GoReleaser to build, sign, and
publish a GitHub release, which the Terraform Registry then picks up.
[RELEASING.md](RELEASING.md) covers the one-time Registry setup and the
release checklist.
## License
MPL-2.0. Derived from
[HashiCorp's Terraform Plugin Framework scaffold](https://github.com/hashicorp/terraform-provider-scaffolding-framework).