Skip to content

Commit 4c5eb66

Browse files
CopilotBreee
andauthored
docs: add "perfect upjet provider" agent skill
Co-authored-by: Breee <11966385+Breee@users.noreply.github.com>
1 parent aed4bec commit 4c5eb66

12 files changed

Lines changed: 1058 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
name: upjet-provider
3+
description: Build a production-grade upjet v2 Crossplane provider from any Terraform provider, following the crossplane-contrib/provider-keycloak reference architecture — repo scaffold, code generation pipeline, external-name/reference configuration, CI, e2e tests, schema-diff and upstream-release automation. Use when creating a new Crossplane provider, porting a Terraform provider to Crossplane, or auditing an existing upjet provider for missing pieces.
4+
---
5+
6+
# The Perfect Upjet Provider
7+
8+
This skill describes end-to-end how to build a Crossplane provider from a Terraform
9+
provider using [upjet](https://github.com/crossplane/upjet) v2, matching the
10+
architecture of [`crossplane-contrib/provider-keycloak`](https://github.com/crossplane-contrib/provider-keycloak)
11+
— the reference implementation this skill is derived from.
12+
13+
Throughout, substitute:
14+
15+
| Placeholder | Example (keycloak) | Example (litellm) |
16+
|---|---|---|
17+
| `<name>` | `keycloak` | `litellm` |
18+
| `<ORG>` | `crossplane-contrib` | `corewire` |
19+
| `<TF_SOURCE>` | `keycloak/keycloak` | `BerriAI/litellm` |
20+
| `<TF_REPO>` | `https://github.com/keycloak/terraform-provider-keycloak` | `https://github.com/BerriAI/terraform-provider-litellm` |
21+
| `<ROOT_GROUP>` | `keycloak.crossplane.io` | `litellm.crossplane.io` |
22+
23+
## Before you start — inputs you must collect
24+
25+
1. **Terraform provider repo + latest release tag** (`<TF_REPO>`, `v<TERRAFORM_PROVIDER_VERSION>`).
26+
2. **Registry source address** (`<TF_SOURCE>`, as used in `required_providers`).
27+
3. **Whether the provider is SDKv2 (`helper/schema`), plugin-framework, or muxed.**
28+
This decides `WithTerraformPluginSDKIncludeList` vs `WithTerraformPluginFrameworkIncludeList`.
29+
4. **The exported provider constructor** (e.g. `provider.KeycloakProvider(nil)`,
30+
`litellm.Provider()`) — needed for the no-fork runtime path.
31+
5. **Docs path inside the TF repo** (`docs/resources`, sometimes `website/docs/r`).
32+
6. **Auth model** — which provider-config attributes are required, which are secret.
33+
7. **Release asset naming** — `<name>_<version>_<os>_<arch>.zip`, needed to fetch the
34+
schema for the supported `PLATFORMS`.
35+
36+
Do not guess these. Fetch the upstream repo and read `main.go` / `docs/` first.
37+
38+
## Build order
39+
40+
Work in this order; each step is verifiable on its own.
41+
42+
1. **Scaffold the repository** → `references/repository-layout.md`
43+
2. **Makefile + build submodule** → `references/build-system.md`
44+
3. **`config/` — schema, metadata, external names, per-group config** → `references/config-patterns.md`
45+
4. **`internal/clients/<name>.go` — the `terraform.SetupFn`** → `references/clients.md`
46+
5. **`cmd/provider/main.go` — the controller manager** → `references/provider-main.md`
47+
6. **`generate/generate.go` — the generation pipeline**, then run `make generate` → `references/generation-pipeline.md`
48+
7. **Examples + e2e (chainsaw/uptest)** → `references/testing.md`
49+
8. **CI, release, and upstream-tracking automation** → `references/automation.md`
50+
9. **Docs (`README`, `CONTRIBUTING`, repo-local `SKILL.md`/`AGENTS.md`)**
51+
52+
A provider is only "complete" when every item in `references/checklist.md` is ticked.
53+
54+
## Non-negotiable rules
55+
56+
- **Terraform CLI version must stay `< 1.6`.** Terraform 1.6+ is BSL-licensed and
57+
cannot be used in an Apache-2.0 project. The Makefile must *fail* on a higher
58+
version (`check-terraform-version`).
59+
- **No-fork only.** Embed the Terraform provider as a Go dependency and pass it via
60+
`ujconfig.WithTerraformProvider(p)`. Never ship a `terraform` binary in the image
61+
and never shell out to it at runtime. The runtime image is `distroless/static`
62+
with a single Go binary.
63+
- **`config/schema.json` and `config/provider-metadata.yaml` are committed.**
64+
They are `//go:embed`-ed, so generation and runtime never need network access.
65+
- **`config/external_name.go` is the single source of truth** for which Terraform
66+
resources are exposed. `config/generated.lst` is derived from it by
67+
`cmd/generatedlist` and verified in CI (`make generated-lst-check`).
68+
- **Never hand-edit generated output**: `apis/**/zz_*.go`, `internal/controller/**/zz_*.go`,
69+
`package/crds/`, `examples-generated/`. Change `config/` and re-run `make generate`.
70+
- **CI must run `make generate` and fail on a dirty tree** (`check-diff`). This is
71+
what keeps generated code honest.
72+
- **Every exposed managed resource needs an e2e example** and must be listed in a
73+
`cluster/test/cases*.txt` file; enforce it with a coverage check.
74+
- **Bumping the upstream Terraform provider is never "just a version bump".**
75+
Gate it on `make schema-version-diff` (state-schema version changes) and
76+
`make crddiff` (breaking CRD changes).
77+
78+
## Quick reference — the commands every provider must support
79+
80+
```bash
81+
make submodules # init the crossplane/build submodule (first run)
82+
make generate # schema -> docs -> types -> CRDs -> controllers -> lists
83+
make build # build the provider binary + xpkg
84+
make test # unit tests
85+
make lint # golangci-lint
86+
make local-deploy # kind + crossplane + locally built provider
87+
make e2e # local-deploy + uptest/chainsaw suite
88+
make schema-version-diff # TF state-schema drift vs base branch (CI)
89+
make schema-diff OLD_PROVIDER_VERSION=x.y.z # manual two-version schema diff
90+
make crddiff # breaking CRD change detection (CI)
91+
make generated-lst-check # generated.lst is in sync with external_name.go
92+
```
93+
94+
Always run `make lint` (and `make generate`) before committing Go changes.
95+
96+
## Reference files
97+
98+
| File | Read it when |
99+
|---|---|
100+
| `references/repository-layout.md` | Creating the repo skeleton; what every file/dir is for |
101+
| `references/build-system.md` | Writing the Makefile: schema fetch, docs pull, platforms, tool pinning |
102+
| `references/config-patterns.md` | Choosing external names, references, groups, kinds, sensitive fields |
103+
| `references/clients.md` | Writing `TerraformSetupBuilder`, credential handling, session/client pooling |
104+
| `references/provider-main.md` | Controller-manager wiring, flags, feature gates, safe-start, webhooks |
105+
| `references/generation-pipeline.md` | `generate/generate.go` directives and what each stage produces |
106+
| `references/testing.md` | Examples, uptest/chainsaw, e2e case lists, coverage gates |
107+
| `references/automation.md` | CI jobs, release workflows, schema-diff issues, upstream release checks |
108+
| `references/troubleshooting.md` | Known upjet v2 / crossplane-runtime v2 compile and runtime pitfalls |
109+
| `references/checklist.md` | Final completeness checklist for a "perfect" provider |
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# CI, release and upstream-tracking automation
2+
3+
## `.github/workflows/ci.yml`
4+
5+
Jobs, in dependency order:
6+
7+
| Job | What it does |
8+
|---|---|
9+
| `detect-noop` | `fkirc/skip-duplicate-actions`; optionally computes the e2e scope via `scripts/e2e_dag.py select` |
10+
| `lint` | `golangci/golangci-lint-action` with the pinned `GOLANGCILINT_VERSION` |
11+
| `check-diff` | `make generate` then `git diff --exit-code`; plus `make generated-lst-check`, `make e2e-cases-check`, docs freshness |
12+
| `unit-tests` | `make test` (+ race tests), upload coverage |
13+
| `build` / `build-publish` | QEMU + buildx multi-arch image and xpkg; publish on `main` / `release-*` |
14+
| `e2e-tests` | kind + Crossplane + `make uptest`, matrixed over upstream versions where relevant |
15+
| `schema-version-diff` | `make schema-version-diff` — flags Terraform state-schema version bumps |
16+
| `crddiff` | `make crddiff` — flags breaking CRD changes |
17+
18+
Cache Go build output using `make go.cachedir` as the cache key input.
19+
20+
`check-diff` is the most valuable job in the whole pipeline: it proves the committed
21+
generated code matches `config/`.
22+
23+
## Release workflows
24+
25+
- `auto-release.yaml` — scheduled: if there are relevant changes since the last tag
26+
and CI is green, bump the semver tag and cut a GitHub Release with generated notes.
27+
- `tag.yml` — `workflow_dispatch` with `version` + `message` inputs for manual tagging.
28+
- `quick-release.yml` (optional) — scheduled multi-arch image/xpkg publish, with an
29+
optional `crane copy` mirror to a second registry.
30+
- `backport.yml` (optional) — label-driven backports to `release-*` branches.
31+
32+
## Upstream Terraform provider tracking
33+
34+
Two complementary automations, both **dry-run on pull requests** (and path-filtered
35+
to their own sources so they don't run on unrelated PRs) and **live on schedule**:
36+
37+
### `schema-diff-issues.yml` → `scripts/schema_diff_issues.py`
38+
39+
Compares `config/schema.json` (everything upstream offers) against
40+
`config/generated.lst` (what we expose) and files one GitHub issue per
41+
not-yet-exposed resource, skipping resources already tracked by an open issue.
42+
This turns "which resources are still missing?" into a live backlog.
43+
44+
### `provider-release-check.yml` → `scripts/check_provider_release.py`
45+
46+
- `--mode issue`: compare the latest upstream release to `TERRAFORM_PROVIDER_VERSION`
47+
in the Makefile; file a tracking issue with the full bump checklist.
48+
- `--mode bump`: perform the mechanical bump in the working tree — update the
49+
Makefile, `go get` the new provider module, `go mod tidy`, `make generate` — and
50+
emit a PR body summarizing the schema diff (via `version_diff.py`). Committing,
51+
branching and PR creation stay in the workflow so git identity is controlled there.
52+
53+
### `scripts/version_diff.py`
54+
55+
Given `generated.lst`, an old `schema.json` and a new one, reports new resources,
56+
removed resources, `SchemaVersion` bumps (which imply Terraform state migrations)
57+
and per-attribute additions/removals for generated resources. Exit codes:
58+
`0` no changes, `1` changes detected, `2` error.
59+
60+
## Renovate
61+
62+
`.github/renovate.json` should:
63+
64+
- keep Go modules and GitHub Actions current (grouped, auto-merge for patch/minor),
65+
- treat the **Terraform provider dependency specially** — either exclude it from
66+
auto-merge or require the `provider-release-check` flow, because bumping it
67+
regenerates every CRD and may need a state-schema migration,
68+
- pick up `# renovate: datasource=github-releases depName=...` comments in the
69+
Makefile so `TERRAFORM_PROVIDER_VERSION` is tracked too.
70+
71+
## Repository hygiene
72+
73+
`CODEOWNERS`, a `Dockerfile` based on a digest-pinned `gcr.io/distroless/static`
74+
running as `USER 65532`, and `package/crossplane.yaml` declaring
75+
`capabilities: [safe-start]`.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Build system
2+
3+
The Makefile is layered on the `crossplane/build` submodule
4+
(`git submodule add https://github.com/crossplane/build build`, branch `master`),
5+
which supplies `common.mk`, `output.mk`, `golang.mk`, `k8s_tools.mk`,
6+
`imagelight.mk`, `xpkg.mk`, `local.xpkg.mk`, `controlplane.mk`.
7+
8+
## Variables
9+
10+
```makefile
11+
PROJECT_NAME ?= provider-<name>
12+
PROJECT_REPO ?= github.com/<ORG>/$(PROJECT_NAME)
13+
14+
export TERRAFORM_VERSION ?= 1.5.7 # MUST stay < 1.6 (BSL)
15+
TERRAFORM_VERSION_VALID := $(shell [ "$(TERRAFORM_VERSION)" = "`printf "$(TERRAFORM_VERSION)\n1.6" | sort -V | head -n1`" ] && echo 1 || echo 0)
16+
17+
export TERRAFORM_PROVIDER_SOURCE ?= <TF_SOURCE>
18+
export TERRAFORM_PROVIDER_REPO ?= <TF_REPO>
19+
# renovate: datasource=github-releases depName=<owner>/terraform-provider-<name>
20+
export TERRAFORM_PROVIDER_VERSION ?= x.y.z
21+
export TERRAFORM_PROVIDER_DOWNLOAD_NAME ?= terraform-provider-<name>
22+
export TERRAFORM_PROVIDER_DOWNLOAD_URL_PREFIX ?= ${TERRAFORM_PROVIDER_REPO}/releases/download/v$(TERRAFORM_PROVIDER_VERSION)
23+
export TERRAFORM_NATIVE_PROVIDER_BINARY ?= terraform-provider-<name>_v$(TERRAFORM_PROVIDER_VERSION)
24+
export TERRAFORM_DOCS_PATH ?= docs/resources
25+
export TERRAFORM_FILE_MIRROR ?= .terraform.d/plugins
26+
export TERRAFORM_FILE_MIRROR_REPO ?= ${TERRAFORM_FILE_MIRROR}/registry.terraform.io
27+
28+
PLATFORMS ?= linux_amd64 linux_arm64
29+
GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/provider $(GO_PROJECT)/cmd/generator
30+
GO_LDFLAGS += -X $(GO_PROJECT)/internal/version.Version=$(VERSION)
31+
GO_SUBDIRS += cmd internal apis config
32+
TERRAFORM_PROVIDER_SCHEMA := config/schema.json
33+
```
34+
35+
Pin the toolchain explicitly so CI and local runs agree: `GO_REQUIRED_VERSION`,
36+
`GOLANGCILINT_VERSION`, `KUBECTL_VERSION`, `KIND_VERSION`, `UP_VERSION`,
37+
`UPTEST_VERSION`, `CHAINSAW_VERSION`, `CROSSPLANE_VERSION`, `CROSSPLANE_CLI_VERSION`.
38+
39+
## Schema acquisition (`config/schema.json`)
40+
41+
Terraform CLI is used **only at generation time** to dump the provider schema:
42+
43+
1. `download-tf-provider-platforms` → for each entry in `PLATFORMS`, download
44+
`${DOWNLOAD_URL_PREFIX}/${DOWNLOAD_NAME}_${VERSION}_${PLATFORM}.zip` into a
45+
filesystem mirror at `$(WORK_DIR)/terraform/$(TERRAFORM_FILE_MIRROR_REPO)/...`
46+
and unzip it. Use `curl --retry 5 --retry-delay 5 --retry-all-errors`.
47+
2. Write `main.tf.json` with `required_providers` pinned to
48+
`<TF_SOURCE>@$(TERRAFORM_PROVIDER_VERSION)` and `config.tfrc` with a
49+
`provider_installation { filesystem_mirror { ... include = ["*/*/*"] } }` block.
50+
3. `TF_CLI_CONFIG_FILE=... terraform init -no-color` then
51+
`terraform providers schema -json=true > config/schema.json`.
52+
53+
Hook it into generation with `generate.init: $(TERRAFORM_PROVIDER_SCHEMA) pull-docs`.
54+
55+
## Docs acquisition (`pull-docs`)
56+
57+
Sparse, shallow, blobless clone of the TF provider repo at the pinned tag, checking
58+
out only `$(TERRAFORM_DOCS_PATH)`:
59+
60+
```makefile
61+
git clone -c advice.detachedHead=false --depth 1 --filter=blob:none \
62+
--branch "v$(TERRAFORM_PROVIDER_VERSION)" --sparse "$(TERRAFORM_PROVIDER_REPO)" \
63+
"$(WORK_DIR)/$(TERRAFORM_PROVIDER_SOURCE)"
64+
git -C "$(WORK_DIR)/$(TERRAFORM_PROVIDER_SOURCE)" sparse-checkout set "$(TERRAFORM_DOCS_PATH)"
65+
```
66+
67+
The scraper in `generate/generate.go` reads from `../.work/${TERRAFORM_PROVIDER_SOURCE}/${TERRAFORM_DOCS_PATH}`.
68+
69+
## Generation hooks
70+
71+
```makefile
72+
generate.init: $(TERRAFORM_PROVIDER_SCHEMA) pull-docs
73+
generate.done: generated-lst # + e2e-index, docs-gen if you have them
74+
```
75+
76+
`generated-lst` / `generated-lst-check` run `go run ./cmd/generatedlist [--check] config/generated.lst`.
77+
78+
## Local development and e2e
79+
80+
```makefile
81+
controlplane.up: # kind create cluster + helm install crossplane (pinned chart)
82+
local-deploy: build controlplane.up local.xpkg.deploy.provider.$(PROJECT_NAME)
83+
uptest: # uptest e2e "$(UPTEST_EXAMPLE_LIST)" --setup-script=cluster/test/setup.sh \
84+
# --default-conditions="Test" --default-timeout=2400s
85+
e2e: local-deploy uptest
86+
```
87+
88+
`UPTEST_EXAMPLE_LIST := $(shell grep -v '^\#' cluster/test/cases.txt | paste -sd ',' -)`.
89+
90+
## Schema diffing
91+
92+
- `schema-version-diff` (CI): read `TERRAFORM_PROVIDER_VERSION` and `config/schema.json`
93+
from `${GITHUB_BASE_REF}` via `git cat-file -p`, then run
94+
`./scripts/version_diff.py config/generated.lst <old> config/schema.json`.
95+
- `schema-diff OLD_PROVIDER_VERSION=x.y.z` (manual): download the old provider
96+
binary for the host platform, dump its schema with terraform, diff against the
97+
current one.
98+
- `crddiff` (CI, optional but recommended): `uptest crddiff revision` against the base
99+
branch CRDs to catch breaking CRD changes; allow overriding with
100+
`CRDDIFF_ALLOW_BREAKING=true` for intentional major bumps.
101+
102+
## Misc targets
103+
104+
`submodules`, `go.cachedir` (cache key for GitHub Actions), `cobertura` (coverage
105+
report excluding `zz_*`), `run` (out-of-cluster provider), `test.race` for
106+
concurrency-sensitive packages.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Completeness checklist
2+
3+
A provider is "perfect" (provider-keycloak-grade) when every box below is ticked.
4+
5+
## Scaffold
6+
7+
- [ ] `build/` submodule wired; `make submodules` works from a clean clone
8+
- [ ] `Makefile` with pinned `TERRAFORM_VERSION` (< 1.6) and a `check-terraform-version` guard
9+
- [ ] `TERRAFORM_PROVIDER_*` variables set, with a `# renovate:` comment on the version
10+
- [ ] `PLATFORMS` covers every platform you publish (`linux_amd64 linux_arm64`)
11+
- [ ] `Dockerfile` on digest-pinned `gcr.io/distroless/static`, `USER 65532`, no terraform binary
12+
- [ ] `package/crossplane.yaml` with `capabilities: [safe-start]`
13+
- [ ] `.golangci.yml`, `CODEOWNERS`, `.github/renovate.json`, `LICENSE`
14+
15+
## Configuration
16+
17+
- [ ] `config/schema.json` and `config/provider-metadata.yaml` committed and embedded
18+
- [ ] `config/external_name.go` covers every resource you intend to expose
19+
- [ ] Include lists match the plugin type (SDKv2 vs framework)
20+
- [ ] `KnownReferencers()` wires the recurring foreign-key fields
21+
- [ ] One `config/<group>/config.go` per API group, registered in `GetProvider`
22+
- [ ] Sensitive fields marked `Sensitive` **before** the first release
23+
- [ ] `config/generated.lst` generated and checked in CI
24+
25+
## Code
26+
27+
- [ ] `internal/clients/<name>.go` with `TerraformSetupBuilder` + unit tests
28+
- [ ] Required provider attributes validated with explicit errors; credentials never logged
29+
- [ ] `cmd/provider/main.go` with metrics, feature gates, `PollJitter`, leader election
30+
- [ ] `cmd/generator/main.go`, `cmd/generatedlist/main.go` (+ `cmd/crdconversion` if multi-version)
31+
- [ ] `internal/features`, `internal/version`
32+
- [ ] `apis/v1beta1` ProviderConfig types (crossplane-runtime v2 shapes)
33+
34+
## Generation
35+
36+
- [ ] `generate/generate.go` with all stages: clean → scrape → generator → controller-gen → angryjet → resolver
37+
- [ ] `generate.init` fetches schema and docs; `generate.done` refreshes derived lists
38+
- [ ] `make generate` is idempotent — a second run leaves the tree clean
39+
40+
## Testing
41+
42+
- [ ] Unit tests for all hand-written packages; `-race` where concurrency matters
43+
- [ ] `examples/` manifest for every exposed resource
44+
- [ ] `cluster/test/setup.sh` + `cases.txt`; `make e2e` green against a real backend
45+
- [ ] Coverage gate: no example missing, no orphan example
46+
- [ ] Conversion tests if CRDs are multi-version
47+
48+
## Automation
49+
50+
- [ ] `ci.yml` with detect-noop, lint, check-diff, unit-tests, build, e2e
51+
- [ ] `schema-version-diff` and `crddiff` jobs
52+
- [ ] `auto-release` + `tag` workflows
53+
- [ ] `schema-diff-issues` workflow + `scripts/schema_diff_issues.py`
54+
- [ ] `provider-release-check` workflow + `scripts/check_provider_release.py`
55+
- [ ] `scripts/version_diff.py`
56+
- [ ] Scheduled workflows dry-run on PRs and are path-filtered
57+
58+
## Documentation
59+
60+
- [ ] `README.md`: install, ProviderConfig, a worked example, dev quickstart
61+
- [ ] `CONTRIBUTING.md`: how to expose a new resource (edit `external_name.go`,
62+
add a group config, `make generate`, add an example, add to `cases.txt`)
63+
- [ ] Repo-local `SKILL.md` / `AGENTS.md` for AI agents, listing the generated
64+
paths that must never be hand-edited
65+
- [ ] Every deviation from this checklist documented with its reason

0 commit comments

Comments
 (0)