|
| 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 | |
0 commit comments