.github/workflows Loading last commit info...
cmd
docs
examples
internal
.gitignore
.goreleaser.yml
Makefile
README.md
generator
go.mod
go.sum
README.md

terraform-provider-bahriya

The official Terraform provider for Bahriya — manage every Bahriya resource declaratively.

Status: pre-release. Not yet published to the Terraform registry. Track progress on Nextcloud Deck card #928.


Table of contents


Installation

Once published (target state)

terraform {
  required_providers {
    bahriya = {
      source  = "bahriya-cloud/bahriya"
      version = "~> 0.1"
    }
  }
}

Currently — from source

See Building from source and Local development below.

Quickstart

terraform {
  required_providers {
    bahriya = { source = "bahriya-cloud/bahriya" }
  }
}

provider "bahriya" {}

data "bahriya_regions" "active" {
  status_filter = "active"
}

resource "bahriya_project" "web" {
  handle  = "web-prod"
  name    = "Web Production"
  regions = [data.bahriya_regions.active.regions[0].id]
}

resource "bahriya_registry" "ghcr" {
  handle   = "ghcr"
  name     = "GitHub Container Registry"
  server   = "ghcr.io"
  username = var.ghcr_username
  password = var.ghcr_token
}

resource "bahriya_secret" "db_password" {
  handle = "db-password"
  name   = "Database Password"
  value  = var.db_password
}

resource "bahriya_container" "api" {
  handle    = "api"
  name      = "API Server"
  image     = "ghcr.io/myorg/api:1.2.3"

  # Required for HTTP containers — deploy will fail without these
  containerport   = "3000"
  healthcheckpath = "/healthz"

  mincpu    = "250"
  minmemory = "256"

  autoscalingminreplicas = "2"
  activeregions          = [data.bahriya_regions.active.regions[0].id]
  project                = bahriya_project.web.id
  registry               = bahriya_registry.ghcr.handle

  secretsenvvar {
    secret = bahriya_secret.db_password.handle
    name   = "DATABASE_PASSWORD"
  }

  hostnames {
    hostname = "api.example.com"
  }
}

Provider configuration

AttributeRequiredEnv varDescription
tokenyesBAHRIYA_TOKENPersonal access token (PAT). Issued in the console under your profile.
organisation_idyesBAHRIYA_ORGANISATION_IDThe UUID of the org these resources belong to.
base_urlnoBAHRIYA_API_URLAPI root. Defaults to https://api.bahriya.cloud/console/v1.

Attributes on the provider block override env vars. For multi-org setups, use alias:

provider "bahriya" {
  alias           = "prod"
  organisation_id = var.prod_org_id
}

provider "bahriya" {
  alias           = "staging"
  organisation_id = var.staging_org_id
}

Authentication

The provider uses Bahriya personal access tokens (PATs):

  1. Sign in to the console at https://app.bahriya.cloud.
  2. Open your profile, then Tokens.
  3. Click Create token, copy the pat_... value.
export BAHRIYA_TOKEN="pat_..."
export BAHRIYA_ORGANISATION_ID="65cc42f1-..."

Resources

ResourceDescriptionE2E Verified
bahriya_projectLogical grouping of containers, secrets, registries; carries quota.Yes
bahriya_containerA workload: HTTP server, worker, or cronjob. Multi-region by default.Yes
bahriya_registryPrivate container registry credentials, scoped to an org.Yes
bahriya_secretEncrypted secret value, mountable as env var in a container.Yes
bahriya_memcachedManaged Memcached instance, attachable to containers as a cache backend.Yes
bahriya_tls_bundleTLS bundle (CA + cert + key), versioned with rotation history.Yes
bahriya_x509_certStandalone X.509 certificate (no key), versioned.Yes
bahriya_gpg_keypairGPG public + private keypair (OpenPGP armor), versioned.Yes
bahriya_ssh_keypairSSH public + private keypair, versioned.Yes
bahriya_encryption_keySymmetric encryption key (AES, ChaCha20, etc.), versioned.Yes
bahriya_env_fileKEY=VALUE env file. Injected into containers via envFrom.Yes
bahriya_yaml_configYAML config file, mounted into containers as a file.Yes
bahriya_json_configJSON config file, mounted into containers as a file.Yes
bahriya_plain_configArbitrary text config file, mounted into containers as a file.Yes
bahriya_network_policyIngress/egress network policy, attachable to projects and workloads.Yes

Each vault/config item also has a corresponding bahriya_project_<type>_attachment resource that binds an item to a project. Once attached, the item can be referenced from a bahriya_container block to mount it inside the container.

All resource schemas are code-generated from the OpenAPI specs at packages/bahriya-openapi/specs/v1/.

The bahriya_organisation and bahriya_regions data sources are also E2E verified.

End-to-end coverage

examples/e2e-vault-configs/ is the comprehensive E2E: in a single apply/destroy it exercises every resource above plus both data sources — registry + secret and all vault/config items with their project attachments, a network policy, memcached, and HTTP + worker + cronjob containers wired to the full set of mounts, env vars, secret env vars, persistent storage and path rules. Run ./gen-fixtures.sh (once) to mint crypto fixtures and a fresh handle suffix, then terraform apply.

Secret-sync timing. A container that consumes a secret (via secretsenvvar) or an env file (via envfiles/envFrom) will only reach running once the project's bahriya-secrets bundle has synced into the namespace. On a brand-new project the first container deploy can lose that race and land in error; re-running the apply (once the sync has completed) brings it to running. This is a platform deploy-pipeline ordering concern, not a provider behaviour — the provider creates and attaches the secret and orders the container after it.

Minimum required fields

These are the fields you must set for a successful deploy. Omitting any of these will cause errors (API rejection or runtime health-check failures).

bahriya_container (HTTP)

FieldTypeExampleNotes
handlestring"api"Immutable. Cannot be reused after delete (soft-delete).
namestring"API Server"
imagestring"nginx:alpine"Full image ref including tag.
containerportstring"8080"The port your application listens on.
healthcheckpathstring"/healthz"HTTP path for liveness/readiness probes. Deploy will fail if the container does not respond 200 at this path on containerport.
mincpustring"100"Millicores.
minmemorystring"128"Megabytes.
autoscalingminreplicasstring"1"
activeregionslist["falkenstein-1"]At least one region.
projectstringbahriya_project.x.idMust be the project UUID (.id), not the handle.

bahriya_container (worker / cronjob)

Same as HTTP except containerport and healthcheckpath are not required (workers have no ingress).

bahriya_memcached

FieldTypeExampleNotes
handlestring"session-cache"Immutable. Cannot be reused after delete.
namestring"Session Cache"
memorymbnumber512Cache size in megabytes (integer, not string).
activeregionslist["helsinki-1"]At least one region.
projectstringbahriya_project.x.idMust be the project UUID (.id), not the handle.

bahriya_project

FieldTypeExampleNotes
handlestring"web-prod"Immutable. Soft-deleted.
namestring"Web Production"
regionslist["falkenstein-1"]At least one region.

bahriya_registry

FieldTypeExampleNotes
handlestring"ghcr"Released on delete (reusable).
namestring"GHCR"
serverstring"ghcr.io"Registry hostname.
usernamestring"myuser"
passwordstring(sensitive)

bahriya_secret

FieldTypeExampleNotes
handlestring"db-password"Released on delete (reusable).
namestring"DB Password"
valuestring(sensitive)

Vault items

All vault types require handle and name plus the content fields below. Handles are immutable. Items are soft-deleted (handle burned).

ResourceRequired content fieldsNotes
bahriya_tls_bundleca, cert, keyPEM-encoded. key is sensitive.
bahriya_x509_certcertPEM-encoded standalone cert (no key).
bahriya_gpg_keypairpublic_key, private_keyOpenPGP armored. Both fields marked sensitive by the spec.
bahriya_ssh_keypairpublic_key, private_keyOpenSSH format. Both fields marked sensitive by the spec.
bahriya_encryption_keyformat, keyformatbase64, hex, raw. key is sensitive.

The API parses content on create / rotate — invalid material is rejected.

Config items

ResourceRequired content fieldsNotes
bahriya_env_filecontentKEY=VALUE lines. Injected via envFrom.
bahriya_yaml_configcontentValid YAML.
bahriya_json_configcontentValid JSON.
bahriya_plain_configcontentArbitrary text. Mounted as a file.

Networking

ResourceNotable fieldsNotes
bahriya_network_policyingresspeers, egresscidrs, egressfqdns, portsAll optional. Any egresscidrs flips the target to deny-by-default outbound. egressfqdns is stored/validated now, enforced later. ports is a nested list of { port, protocol }.

Project attachments

Each vault/config type has a corresponding attachment resource (bahriya_project_tls_bundle_attachment, etc.). All take exactly two required fields:

FieldTypeDescription
project_idstringUUID of the project (bahriya_project.x.id).
handlestringHandle of the item to attach.

Both attributes force replacement on change. Attachment is also blocked at the API if a container in the project still references the item.

Data sources

Data sourceDescription
bahriya_organisationFetches the current organisation (id, name, handle, email, role).
bahriya_regionLook up a single region by ID (name, status, location).
bahriya_regionsList all regions, with optional status_filter.

Important behaviours

Handles cannot be reused (soft-delete)

For projects, containers, and memcached, deleting in Terraform does not free up the handle. These resources are soft-deleted for billing and audit reasons.

  • Use distinct handles per environment (api-prod, api-staging).
  • Treat handles as permanent identifiers.

For registries and secrets, handles are released on delete and can be reused.

Async termination

Destroying a bahriya_container or bahriya_memcached triggers async termination. The provider polls until the resource reaches terminated status (default timeout: 10 minutes).

Renaming a handle is destroy + recreate

Handles are immutable. Changing the handle attribute forces Terraform to destroy and recreate the resource.

Imports use UUIDs

terraform import bahriya_container.api 065df92e-4e46-436a-a0a0-aaaaaaaaaaaa

Sensitive fields

Registry password and secret value are marked sensitive. The API returns masked sentinels on read; the provider preserves the real value from your Terraform state.

Vault item content fields (bahriya_tls_bundle.key, bahriya_gpg_keypair.private_key / .public_key, bahriya_ssh_keypair.private_key / .public_key, bahriya_encryption_key.key) are also sensitive and follow the same masking pattern.

Removing a nested attachment block must be explicit

Container nested-list attributes (tls_bundles, x509_certs, gpg_keypairs, ssh_keypairs, encryption_keys, env_files, yaml_configs, json_configs, plain_configs, persistentvolumes, hostnames, initjobs, secretsenvvar, newenvvar) are Optional+Computed with the UseStateForUnknown plan modifier — needed for stable plans against the API's PUT-replace semantics. As a consequence, omitting a block in your .tf does NOT remove the attachment; the prior state value is re-injected silently.

To remove a vault/config attachment from a container, set the block to an explicit empty list:

resource "bahriya_container" "api" {
  # ...
  json_configs = []   # explicit removal — works
  # plain_configs    # omitting — does NOT remove; prior list is preserved
}

Detach guard

Attempting to detach a vault/config item from a project while a container in that project still references it returns 409 CONFLICT with the offending container's handle. Update the container to remove the reference first.

Container Update waits for status

After a terraform apply that modifies a container, the provider waits for the container to reach running (or terminal error) before completing the apply. This avoids "inconsistent result after apply" errors when the post-PUT fetch would otherwise race the helm rollout.

Common pitfalls

SymptomCauseFix
Container reaches error status after deployMissing or wrong healthcheckpath. The platform probes containerport + healthcheckpath to decide if the container is healthy.Set healthcheckpath to a path your app responds 200 on (e.g. / or /healthz).
project attribute rejectedPassing the project handle instead of UUID.Use bahriya_project.x.id, not .handle.
terraform plan shows unknown fieldUsing snake_case names (container_port, min_cpu). The provider uses flat lowercase names from the API.Use containerport, mincpu, minmemory, activeregions, etc.
Handle already taken after destroySoft-deleted resources don't release handles.Use a new handle (e.g. api-v2).

Examples

Runnable configs live under examples/:

DirectoryWhat it demonstrates
project-only/Minimal project resource
registry-secret/Registry + secret with variable inputs
data-sources/All 3 data sources (organisation, region, regions)
container/Full container deployment with registry, secrets, hostname
memcached/Cache instance inside a project
complete/Everything together

Building from source

Requires Go 1.23+.

git clone <repo-url>
cd terraform-provider-bahriya
make build        # builds ./bin/terraform-provider-bahriya
make test         # runs all 63 unit tests
make generate     # regenerates resource code from OpenAPI specs
make verify       # generate + git diff (CI gate)

Local development

Add a dev_overrides block to ~/.terraformrc:

provider_installation {
  dev_overrides {
    "bahriya-cloud/bahriya" = "/path/to/terraform-provider-bahriya/bin"
  }
  direct {}
}

Then just make build and run terraform plan/apply — no terraform init needed between rebuilds.

Set credentials via environment variables:

export BAHRIYA_TOKEN="pat_..."
export BAHRIYA_ORGANISATION_ID="..."
# Optional: override API URL for dev
export BAHRIYA_API_URL="http://localhost:8080/console/v1"

Architecture

This is a code-generated provider. The OpenAPI specs at packages/bahriya-openapi/specs/v1/ are the source of truth. A Go generator reads those specs and emits Terraform resources with full CRUD, ImportState, diff short-circuit, and status polling.

What's generated

  • All 5 resource files (internal/resources/*_resource.go) — ~3,800 LOC total
  • Resource registry (internal/resources/registry.go)
  • Terraform schemas with nested attributes for complex types (Container has 6 nested object types)
  • Plan modifiers (RequiresReplace for handles, UseStateForUnknown for computed fields)
  • Sensitive field preservation (password, value)

What's hand-written

  • Provider scaffold (internal/provider/) — schema, Configure, env-var fallbacks
  • HTTP client (internal/client/) — PAT auth, retries, typed errors
  • Data sources (internal/datasources/) — organisation, region, regions
  • Transforms (internal/transforms/) — envvar/secret dict-to-array, x-implies
  • Lifecycle (internal/lifecycle/) — WaitForStatus with backoff
  • Helpers (internal/resources/helpers.go) — small utilities for generated code
  • Code generator (cmd/generator/) — parser, model, renderer, template

Generated files are committed to git. make verify fails CI if they drift.

Releasing

The source of truth is on 1x.ax (origin). GitHub (github) is a mirror that feeds the Terraform Registry. Push to both:

# Push code
git push origin && git push github

# Push tags (triggers the GitHub Actions release workflow)
git push origin --tags && git push github --tags

To cut a release:

git tag v0.1.0
git push origin && git push origin --tags && git push github && git push github --tags

The GitHub Actions workflow runs GoReleaser, builds cross-platform binaries, signs the checksums with GPG, and publishes the release. The Terraform Registry picks it up automatically.

Repository layout

terraform-provider-bahriya/
├── cmd/
│   ├── terraform-provider-bahriya/   # plugin entrypoint
│   └── generator/                    # codegen tool
│       ├── templates/                # text/template for resources
│       └── parser_test.go            # generator unit tests
├── internal/
│   ├── client/       # HTTP client + tests
│   ├── provider/     # provider block + tests
│   ├── resources/    # GENERATED — do not edit by hand
│   ├── datasources/  # data sources + tests
│   ├── transforms/   # envvar, secret, implies + tests
│   └── lifecycle/    # WaitForStatus + tests
├── examples/         # 6 runnable Terraform configs
├── Makefile
├── go.mod
└── .gitignore

Testing

63 unit tests across all packages:

make test           # runs go test ./internal/... and cmd/generator
PackageTestsCoverage
cmd/generator16Parser, model helpers, type resolution, nested $ref
internal/client10HTTP envelope, auth, retry, error types
internal/datasources17Metadata, schema, configure, response parsing
internal/provider5Metadata, schema, resource counts, firstNonEmpty
internal/lifecycle5Wait-for-status (target, terminal, timeout, cancel)
internal/transforms11Envvar, secret, implies round-trips
Please wait...
Connection lost or session expired, reload to recover
Page is in error, reload to recover