diff --git a/.gitignore b/.gitignore index edfb25f..7fea79a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/terraform-provider-ghpvaultsecret +/terraform-provider-vault-secrets-ghp *.zip *.out *.test @@ -8,7 +8,3 @@ dist/ *.tfstate *.tfstate.backup .env - -# e2e artifacts -test/plugins/ -test/dev.tfrc diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml index 8425c6c..eae9c39 100644 --- a/.woodpecker/release.yml +++ b/.woodpecker/release.yml @@ -22,9 +22,9 @@ steps: commands: - | VERSION=$$(echo ${CI_COMMIT_TAG} | sed 's/^v//') - FILE="terraform-provider-ghpvaultsecret_$${VERSION}_linux_amd64.zip" + FILE="terraform-provider-vault-secrets-ghp_$${VERSION}_linux_amd64.zip" curl -f -X PUT \ - "https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/terraform-unkin/files/unkin/ghpvaultsecret/$${FILE}" \ + "https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/terraform-unkin/files/unkin/vault-secrets-ghp/$${FILE}" \ -H "Content-Type: application/zip" \ --data-binary @"$${FILE}" depends_on: [package] diff --git a/Makefile b/Makefile index 1d9d254..5a25464 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ -.PHONY: build install test lint fmt clean tidy package e2e patch minor major check-go +.PHONY: build install test lint fmt clean tidy package patch minor major check-go -BINARY := terraform-provider-ghpvaultsecret +BINARY := terraform-provider-vault-secrets-ghp VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-dev") OS_ARCH := linux_amd64 INSTALL_VERSION := $(shell echo $(VERSION) | sed 's/^v//') -INSTALL_DIR := ~/.terraform.d/plugins/git.unkin.net/unkin/ghpvaultsecret/$(INSTALL_VERSION)/$(OS_ARCH) +INSTALL_DIR := ~/.terraform.d/plugins/git.unkin.net/unkin/vault-secrets-ghp/$(INSTALL_VERSION)/$(OS_ARCH) ZIP := $(BINARY)_$(INSTALL_VERSION)_$(OS_ARCH).zip GO_VERSION_REQUIRED := 1.25 @@ -36,10 +36,6 @@ package: build python3 -c "import zipfile,sys; z=zipfile.ZipFile(sys.argv[1],'w',zipfile.ZIP_DEFLATED); z.write(sys.argv[2]); z.close()" $(ZIP) $(BINARY)_v$(INSTALL_VERSION) rm $(BINARY)_v$(INSTALL_VERSION) -# End-to-end: boots Vault + mock ghp + the plugin and applies real terraform. -e2e: - ./scripts/e2e.sh - clean: rm -f $(BINARY) *.zip diff --git a/README.md b/README.md index 55b3e0a..fb8c812 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,43 @@ -# terraform-provider-ghpvaultsecret +# terraform-provider-vault-secrets-ghp -A Terraform provider that manages the **ghp token secrets engine** +A Terraform/OpenTofu provider that manages the **ghp token secrets engine** ([`vault-plugin-secrets-ghp`](https://git.unkin.net/unkin/vault-plugin-secrets-ghp)) on HashiCorp Vault or OpenBao, so the engine's mount, config, and roles can be driven declaratively (e.g. from `terraform-vault`). -Source address: `git.unkin.net/unkin/ghpvaultsecret` (declare it under the local -name `ghpvaultsecret`, so its resources are `ghpvaultsecret_*`). +Source address: `git.unkin.net/unkin/vault-secrets-ghp` (declare it under the +local name `ghp`, so its resources are `ghp_*`). ## Resources | Resource | Manages | |----------|---------| -| `ghpvaultsecret_secret_backend` | Mounts the engine at a path and writes its `config` (ghp base URL, TLS, seeded service token). | -| `ghpvaultsecret_secret_role` | A role: `token_type`, `installation_id`, `app_record_id`, `repositories`, `scopes`, `session_prefix`, `ttl`, `max_ttl`. | +| `ghp_secret_backend` | Mounts the engine at a path and writes its `config` (ghp base URL, TLS, seeded service token). | +| `ghp_secret_role` | A role: `token_type`, `installation_id`, `app_record_id`, `repositories`, `scopes`, `session_prefix`, `ttl`, `max_ttl`. | ## Usage ```hcl terraform { required_providers { - ghpvaultsecret = { - source = "git.unkin.net/unkin/ghpvaultsecret" + ghp = { + source = "git.unkin.net/unkin/vault-secrets-ghp" } } } -provider "ghpvaultsecret" { +provider "ghp" { # address / token fall back to VAULT_ADDR / VAULT_TOKEN. } -resource "ghpvaultsecret_secret_backend" "ghp" { +resource "ghp_secret_backend" "ghp" { path = "ghp" base_url = "https://ghp.unkin.net" admin_token = var.ghp_admin_token } -resource "ghpvaultsecret_secret_role" "ci" { - backend = ghpvaultsecret_secret_backend.ghp.path +resource "ghp_secret_role" "ci" { + backend = ghp_secret_backend.ghp.path name = "ci" token_type = "agent" installation_id = 12345 @@ -64,8 +64,8 @@ resource "ghpvaultsecret_secret_role" "ci" { ## Import ```sh -terraform import ghpvaultsecret_secret_backend.ghp ghp -terraform import ghpvaultsecret_secret_role.ci ghp/roles/ci +terraform import ghp_secret_backend.ghp ghp +terraform import ghp_secret_role.ci ghp/roles/ci ``` ## Development @@ -74,12 +74,11 @@ terraform import ghpvaultsecret_secret_role.ci ghp/roles/ci make build # build the provider binary make install # install into ~/.terraform.d/plugins for local use make test # unit tests (race) -make e2e # apply real terraform against Vault + a mock ghp (Docker) make package # build the release zip ``` Releases are tag-driven (`make patch|minor|major`): a Woodpecker pipeline builds -`terraform-provider-ghpvaultsecret__linux_amd64.zip` and PUTs it to the -artifactapi terraform registry -(`.../api/v2/remotes/terraform-unkin/files/unkin/ghpvaultsecret/`), which +`terraform-provider-vault-secrets-ghp__linux_amd64.zip` and PUTs it to +the artifactapi terraform registry +(`.../api/v2/remotes/terraform-unkin/files/unkin/vault-secrets-ghp/`), which signs it server-side. Install it via the bare `source` address above. diff --git a/examples/main.tf b/examples/main.tf new file mode 100644 index 0000000..b4655ba --- /dev/null +++ b/examples/main.tf @@ -0,0 +1,45 @@ +terraform { + required_providers { + ghp = { + source = "git.unkin.net/unkin/vault-secrets-ghp" + version = "0.0.1" + } + } +} + +provider "ghp" { + # address defaults to $VAULT_ADDR, token to $VAULT_TOKEN +} + +variable "ghp_admin_token" { + type = string + sensitive = true +} + +resource "ghp_secret_backend" "ghp" { + path = "ghp" + base_url = "https://ghp.unkin.net" + admin_token = var.ghp_admin_token +} + +# Agent role scoped to a ghp App installation for CI. +resource "ghp_secret_role" "ci" { + backend = ghp_secret_backend.ghp.path + name = "ci" + token_type = "agent" + installation_id = 12345 + repositories = ["unkin/prodenv"] + scopes = ["contents:read", "pull_requests:write"] + ttl = 3600 + max_ttl = 28800 +} + +# Proxy (OAuth-backed) role, no installation binding. +resource "ghp_secret_role" "proxy" { + backend = ghp_secret_backend.ghp.path + name = "proxy" + token_type = "proxy" + scopes = ["contents:read"] + ttl = 1800 + max_ttl = 14400 +} diff --git a/examples/resources/ghpvaultsecret_secret_backend/main.tf b/examples/resources/ghp_secret_backend/main.tf similarity index 75% rename from examples/resources/ghpvaultsecret_secret_backend/main.tf rename to examples/resources/ghp_secret_backend/main.tf index 49626dc..3ea4296 100644 --- a/examples/resources/ghpvaultsecret_secret_backend/main.tf +++ b/examples/resources/ghp_secret_backend/main.tf @@ -1,17 +1,17 @@ terraform { required_providers { - ghpvaultsecret = { - source = "git.unkin.net/unkin/ghpvaultsecret" + ghp = { + source = "git.unkin.net/unkin/vault-secrets-ghp" } } } -provider "ghpvaultsecret" { +provider "ghp" { # address / token fall back to VAULT_ADDR / VAULT_TOKEN. } # Mount the ghp secrets engine and seed it with a service token. -resource "ghpvaultsecret_secret_backend" "ghp" { +resource "ghp_secret_backend" "ghp" { path = "ghp" base_url = "https://ghp.unkin.net" ca_cert = file("${path.module}/ghp-ca.pem") diff --git a/examples/resources/ghpvaultsecret_secret_role/main.tf b/examples/resources/ghp_secret_role/main.tf similarity index 80% rename from examples/resources/ghpvaultsecret_secret_role/main.tf rename to examples/resources/ghp_secret_role/main.tf index e6d5038..8925bfa 100644 --- a/examples/resources/ghpvaultsecret_secret_role/main.tf +++ b/examples/resources/ghp_secret_role/main.tf @@ -1,8 +1,8 @@ # A role that mints short-lived agent tokens bound to a ghp App installation, # scoped to contents and pull requests. Reading ghp/creds/ci returns a # lease-bound token that ghp revokes when the lease ends. -resource "ghpvaultsecret_secret_role" "ci" { - backend = ghpvaultsecret_secret_backend.ghp.path +resource "ghp_secret_role" "ci" { + backend = ghp_secret_backend.ghp.path name = "ci" token_type = "agent" installation_id = 12345 diff --git a/go.mod b/go.mod index 4604409..b9c6a40 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.unkin.net/unkin/terraform-provider-ghpvaultsecret +module git.unkin.net/unkin/terraform-provider-vault-secrets-ghp go 1.25 diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5dbf4e1..ba43a06 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -29,10 +29,10 @@ func New(version string) func() provider.Provider { } func (p *ghpProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { - // Source address is git.unkin.net/unkin/ghpvaultsecret; resources are - // prefixed "ghpvaultsecret_" (declare it under the local name - // "ghpvaultsecret" in required_providers). - resp.TypeName = "ghpvaultsecret" + // The provider's source address is git.unkin.net/unkin/vault-secrets-ghp, + // but its resources are prefixed "ghp_" (declare it in required_providers + // under the local name "ghp"), mirroring how google-beta ships google_*. + resp.TypeName = "ghp" resp.Version = p.version } diff --git a/main.go b/main.go index 974c80c..e9c7861 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,7 @@ +// Command terraform-provider-vault-secrets-ghp is the Terraform/OpenTofu +// provider for the vault-plugin-secrets-ghp secrets engine: it manages the +// engine's mount + connection config and its token-minting roles on HashiCorp +// Vault or OpenBao. package main import ( @@ -7,7 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" - "git.unkin.net/unkin/terraform-provider-ghpvaultsecret/internal/provider" + "git.unkin.net/unkin/terraform-provider-vault-secrets-ghp/internal/provider" ) var version = "0.0.1" @@ -18,7 +22,7 @@ func main() { flag.Parse() opts := providerserver.ServeOpts{ - Address: "git.unkin.net/unkin/ghpvaultsecret", + Address: "git.unkin.net/unkin/vault-secrets-ghp", Debug: debug, } diff --git a/scripts/e2e.sh b/scripts/e2e.sh deleted file mode 100755 index 2f491cd..0000000 --- a/scripts/e2e.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash -# -# End-to-end test for terraform-provider-ghpvaultsecret. -# -# Builds the sibling ghp plugin and this provider, boots Vault + a mock ghp -# REST API in Docker, then runs a real `terraform apply` through the provider to -# mount the engine, seed the service token, and create a role. It asserts a -# token can be minted from the role, then `terraform destroy` and verifies the -# mount is gone. -# -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -PLUGIN_REPO="${PLUGIN_REPO:-${ROOT_DIR}/../vault-plugin-secrets-ghp}" -COMPOSE_FILE="${ROOT_DIR}/test/docker-compose.yml" -COMPOSE="docker compose -f ${COMPOSE_FILE}" -TF="${TF:-terraform}" -E2E_DIR="${ROOT_DIR}/test/e2e" -PLUGIN_BIN="vault-plugin-secrets-ghp" -PROVIDER_BIN="terraform-provider-ghpvaultsecret" - -GHP_ADDR="http://127.0.0.1:3000" -export VAULT_ADDR="http://127.0.0.1:8200" -export VAULT_TOKEN="root" -export PLUGIN_SRC="${PLUGIN_REPO}" - -red() { printf '\033[31m%s\033[0m\n' "$*"; } -green() { printf '\033[32m%s\033[0m\n' "$*"; } -blue() { printf '\033[34m==> %s\033[0m\n' "$*"; } -fail() { red "FAIL: $*"; exit 1; } - -cleanup() { - blue "Cleaning up" - if [ -d "${E2E_DIR}" ]; then - (cd "${E2E_DIR}" && TF_CLI_CONFIG_FILE="${ROOT_DIR}/test/dev.tfrc" "${TF}" destroy -auto-approve >/dev/null 2>&1 || true) - rm -f "${E2E_DIR}"/terraform.tfstate* "${E2E_DIR}"/.terraform.lock.hcl - rm -rf "${E2E_DIR}/.terraform" - fi - ${COMPOSE} down -v >/dev/null 2>&1 || true -} -trap cleanup EXIT - -wait_for() { - local desc="$1"; shift - local retries="${WAIT_RETRIES:-90}" i=0 - until "$@" >/dev/null 2>&1; do - i=$((i + 1)) - [ "$i" -ge "$retries" ] && fail "timed out waiting for ${desc}" - sleep 2 - done - green "ready: ${desc}" -} - -jq_field() { python3 -c "import sys,json;print(json.load(sys.stdin)$1)"; } - -# --------------------------------------------------------------------------- -blue "Building ghp plugin from ${PLUGIN_REPO}" -[ -d "${PLUGIN_REPO}" ] || fail "plugin repo not found at ${PLUGIN_REPO} (set PLUGIN_REPO)" -mkdir -p "${ROOT_DIR}/test/plugins" -( cd "${PLUGIN_REPO}" && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" \ - -o "${ROOT_DIR}/test/plugins/${PLUGIN_BIN}" ./cmd/vault-plugin-secrets-ghp ) - -blue "Building the provider" -( cd "${ROOT_DIR}" && go build -o "${PROVIDER_BIN}" . ) - -blue "Writing terraform dev_overrides config" -cat > "${ROOT_DIR}/test/dev.tfrc" </dev/null - -# --------------------------------------------------------------------------- -blue "terraform apply (mount engine + config + role via the provider)" -( cd "${E2E_DIR}" && "${TF}" apply -auto-approve ) -green "apply succeeded" - -blue "Verifying the mount and role exist" -${COMPOSE} exec -T vault vault secrets list 2>/dev/null | grep -q '^ghp/' \ - || fail "ghp mount not found after apply" -${COMPOSE} exec -T vault vault read ghp/roles/ci >/dev/null \ - || fail "role ci not found after apply" -green "mount + role present" - -blue "Minting a token from the terraform-managed role" -JSON="$(${COMPOSE} exec -T vault vault read -format=json ghp/creds/ci)" -TOKEN="$(printf '%s' "${JSON}" | jq_field '["data"]["token"]')" -TYPE="$(printf '%s' "${JSON}" | jq_field '["data"]["token_type"]')" -LEASE="$(printf '%s' "${JSON}" | jq_field '["lease_id"]')" -[ -n "${TOKEN}" ] || fail "no token minted" -[ "${TYPE}" = "agent" ] || fail "unexpected token_type ${TYPE}" -green "minted token ${TOKEN:0:10}... (type ${TYPE}, lease ${LEASE})" - -blue "Revoking the lease" -${COMPOSE} exec -T vault vault lease revoke "${LEASE}" >/dev/null -green "lease revoked" - -# --------------------------------------------------------------------------- -blue "terraform destroy (unmount engine)" -( cd "${E2E_DIR}" && "${TF}" destroy -auto-approve ) -${COMPOSE} exec -T vault vault secrets list 2>/dev/null | grep -q '^ghp/' \ - && fail "ghp mount still present after destroy" || true -green "mount removed by destroy" - -green "ALL PROVIDER END-TO-END CHECKS PASSED" diff --git a/test/docker-compose.yml b/test/docker-compose.yml deleted file mode 100644 index 2b907d5..0000000 --- a/test/docker-compose.yml +++ /dev/null @@ -1,47 +0,0 @@ -# E2E stack for the provider: a mock ghp REST API (run from the sibling plugin -# repo) + a Vault dev server with the ghp plugin mounted. Bind mounts use ":z" -# for SELinux (Fedora/RHEL). MOCKGHP_ADMIN_TOKEN is an ephemeral test fixture, -# not a real credential. -services: - ghp: - image: golang:1.25-alpine - working_dir: /src - environment: - MOCKGHP_ADDR: ":3000" - MOCKGHP_ADMIN_TOKEN: "ghpsvc_e2e_fixture" - GOFLAGS: "-mod=mod" - # PLUGIN_SRC is the sibling vault-plugin-secrets-ghp checkout, which ships - # the mock ghp server under test/mockghp. - command: ["go", "run", "./test/mockghp"] - volumes: - - ${PLUGIN_SRC:-../vault-plugin-secrets-ghp}:/src:ro,z - ports: - - "3000:3000" - healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"] - interval: 3s - timeout: 3s - retries: 40 - - vault: - image: hashicorp/vault:1.18 - depends_on: - ghp: - condition: service_healthy - cap_add: - - IPC_LOCK - environment: - VAULT_DEV_ROOT_TOKEN_ID: root - VAULT_ADDR: http://127.0.0.1:8200 - VAULT_TOKEN: root - command: ["server", "-dev", "-dev-listen-address=0.0.0.0:8200", "-config=/vault/vault.hcl"] - volumes: - - ./plugins:/vault/plugins:ro,z - - ./vault/vault.hcl:/vault/vault.hcl:ro,z - ports: - - "8200:8200" - healthcheck: - test: ["CMD", "vault", "status", "-address=http://127.0.0.1:8200"] - interval: 3s - timeout: 3s - retries: 20 diff --git a/test/e2e/main.tf b/test/e2e/main.tf deleted file mode 100644 index 9e69c76..0000000 --- a/test/e2e/main.tf +++ /dev/null @@ -1,34 +0,0 @@ -terraform { - required_providers { - ghpvaultsecret = { - source = "git.unkin.net/unkin/ghpvaultsecret" - } - } -} - -provider "ghpvaultsecret" { - address = "http://127.0.0.1:8200" - token = "root" -} - -resource "ghpvaultsecret_secret_backend" "ghp" { - path = "ghp" - description = "ghp token engine (e2e)" - # Reachable from inside the vault container, where the plugin runs. - base_url = "http://ghp:3000" - tls_skip_verify = true - - # Ephemeral test fixture, matched by the mock ghp MOCKGHP_ADMIN_TOKEN. - admin_token = "ghpsvc_e2e_fixture" -} - -resource "ghpvaultsecret_secret_role" "ci" { - backend = ghpvaultsecret_secret_backend.ghp.path - name = "ci" - token_type = "agent" - installation_id = 12345 - repositories = ["unkin/prodenv"] - scopes = ["contents:read", "pull_requests:write"] - ttl = 3600 - max_ttl = 86400 -} diff --git a/test/vault/vault.hcl b/test/vault/vault.hcl deleted file mode 100644 index 1567808..0000000 --- a/test/vault/vault.hcl +++ /dev/null @@ -1,4 +0,0 @@ -# Combined with `-dev` so the dev server has a plugin_directory to register the -# ghp plugin binary mounted from ./plugins. -plugin_directory = "/vault/plugins" -api_addr = "http://127.0.0.1:8200"