This file provides guidance to AI Agents when working with code in this repository.
Upjet is a code generation framework that transforms Terraform providers into Crossplane providers.
It generates Kubernetes CRDs, reconciliation controllers, example manifests, and provider configuration from Terraform schemas.
The framework supports three Terraform execution modes:
- Terraform CLI (fork-based): Spawns terraform processes
- Terraform Plugin SDK v2 (no-fork): Direct Go library calls
- Terraform Plugin Framework (no-fork): Protocol-based communication (protov6)
# First-time setup: Initialize build submodule
make submodules
# Run linting and tests (run before opening PR)
make reviewable
# Build the project
make build
# Run unit tests
make test
# Or with specific flags:
go test -v ./pkg/...
# Run specific test
go test -v ./pkg/config -run TestExternalName
# Run linting
make lint
# Check Go modules are tidy
make modules.check
# Generate code
make generate# 1. Make changes to Upjet code
# 2. Test in a provider using replace directive in provider's go.mod:
# replace github.com/crossplane/upjet/v2 => ../upjet
# 3. Run make reviewable before committing
# 4. Open PR with example provider changesThe framework follows a layered pipeline:
Configuration Layer (pkg/config)
↓
Code Generation Pipeline (pkg/pipeline)
↓
Type Generation (pkg/types, pkg/schema)
↓
Runtime System (pkg/controller, pkg/terraform, pkg/resource)
1. Configuration Layer (pkg/config)
Provider: Root configuration mapping Terraform schemas to CRDsResource: Per-resource configuration (external names, references, sensitivity)ExternalName: Maps Terraform IDs ↔ Crossplane external names (required for all resources)
2. Code Generation Pipeline (pkg/pipeline)
Run(): Main orchestrator that generates all codeCRDGenerator: Creates*_types.gofiles with Kubernetes CRD structsControllerGenerator: Creates*_controller.gofiles with reconciliation logicTerraformedGenerator: Creates*_terraformed.gofiles implementing resource.Terraformed interfaceExampleGenerator: Creates YAML example manifests
3. Schema/Type Transformation (pkg/schema, pkg/types)
TypeBuilder: Converts Terraform schemas recursively to Go types- Field classification:
- ForProvider: User-configurable parameters
- InitProvider: Late-initialized fields
- AtProvider: Read-only observations
4. Runtime Layer (pkg/controller, pkg/terraform)
Connector: Creates workspace and ExternalClient per resourceExternalClient: Implements Observe/Create/Update/Delete operationsWorkspace: Per-resource Terraform working directory managing state files- Three execution modes switch on resource configuration:
- CLI: Spawns terraform binary (pkg/controller/external.go)
- SDK v2: Direct library calls (pkg/controller/external_tfpluginsdk.go)
- Framework: Protocol calls (pkg/controller/external_tfpluginfw.go)
Every resource MUST have an external name configuration. This is how Crossplane identifies resources in the remote system:
// Common patterns:
config.NameAsIdentifier // Uses "name" field as identifier
config.IDAsExternalName // Uses Terraform "id" as identifier
config.TemplatedStringAsIdentifier("field", "{{ .parameters.x }}")The external name is:
- Removed from spec (not user-provided)
- Extracted from Terraform state after creation
- Used for terraform import operations
Enable Kubernetes-style references between resources:
r.References["subnet_ids"] = config.Reference{
TerraformName: "aws_subnet",
Extractor: "status.atProvider.id", // Optional, defaults to external name
}
// Generates: SubnetIDRefs and SubnetIDSelector fields in CRDServer-generated fields are automatically populated into spec after creation. Configure ignored fields:
r.LateInitializer.IgnoredFields = []string{"tags", "metadata"}For resources with long create/delete times:
r.UseAsync = true // Enables concurrent async operationsControl which operations are allowed (Crossplane v1.11+):
- Observe: Read-only
- Create/Update/Delete: Allow modifications
- LateInitialize: Auto-fill from server
-
- (All): Default
// 1. Extract Terraform schema
// terraform providers schema --json > schema.json
// 2. Create provider configuration
provider := config.NewProvider(
schemaJSON,
"aws", // Terraform resource prefix
"github.com/upbound/provider-aws",
metadataYAML,
config.WithIncludeList([]string{"aws_vpc", "aws_subnet"}),
)
// 3. Configure resources
provider.AddResourceConfigurator("aws_vpc", func(r *config.Resource) {
r.ExternalName = config.NameAsIdentifier
r.References["default_security_group_id"] = config.Reference{
TerraformName: "aws_security_group",
}
})
// 4. Run pipeline
provider.ConfigureResources()
pipeline.Run(provider, nil, "./")This generates for each resource:
apis/<group>/<version>/
├── zz_<resource>_types.go # CRD types
├── zz_<resource>_terraformed.go # Spec ↔ Terraform mapping
└── zz_register.go # Type registration
internal/controller/<group>/<resource>/
└── zz_<resource>_controller.go # Reconciliation logic
examples-generated/<group>/
└── <resource>.yaml # Example manifest
| Package | Responsibility |
|---|---|
| pkg/config | Provider and resource configuration (what to generate) |
| pkg/pipeline | Code generation orchestration and generators |
| pkg/controller | Runtime reconciliation logic and external client |
| pkg/terraform | Workspace management and Terraform execution |
| pkg/resource | Terraformed interface and observation extraction |
| pkg/types | Go type generation from Terraform schemas |
| pkg/schema | Schema traversal and transformation |
| pkg/registry | Provider metadata and documentation scraping |
| cmd/scraper | Extract Terraform provider docs metadata |
| cmd/resolver | Post-process generated resolver files |
// Top-level CRD
type VPC struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec VPCSpec
Status VPCStatus
}
type VPCSpec struct {
ForProvider VPCParameters // User inputs
InitProvider VPCInitParameters // Late-initialized
ManagementPolicy xpv1.ManagementPolicy
ProviderConfigRef xpv1.Reference
}
type VPCStatus struct {
AtProvider VPCObservation // Observed outputs (read-only)
Conditions []xpv1.Condition
}- Schema Traversers: Modify schemas before generation
- Resource Configurators: Customize individual resources via AddResourceConfigurator
- Reference Injectors: Add cross-resource references
- Configuration Injectors: Inject values into Terraform config at runtime
- Custom Diffs: Customize Terraform diff computation
Observe → Check resource exists → Extract state → Late-initialize → Extract connection details Create → Convert spec to Terraform JSON → terraform apply → Extract state → Set external name Update → Regenerate config → terraform plan → terraform apply if needed Delete → terraform destroy
Type System:
- No
anytype used throughout codebase - use concrete types or type parameters - Pointer types used for optional fields in generated structs
- Type aliases avoided in favor of explicit types
Code Generation:
- All generated files prefixed with
zz_to distinguish from manually written code - Generated code uses
typewriterpackage for type-safe code generation - Interface definitions in
pkg/resource/interfaces.goandpkg/controller/interfaces.go
Testing:
- Standard Go testing only - no Ginkgo, Testify, or third-party test frameworks
- Table-driven tests strongly preferred (see
pkg/config/externalname_test.gofor examples) - Test files:
*_test.goin same package as code under test - Mock generation: Uses
golang/mock(seepkg/resource/fake/mocks/) - Integration tests: Use replace directive in provider go.mod to test Upjet changes
Go Module Management:
- Module path:
github.com/crossplane/upjet/v2 - When testing in providers: Add
replace github.com/crossplane/upjet/v2 => ../upjetto provider's go.mod - Run
make modules.checkto verify go.mod/go.sum are tidy before committing
Package Organization:
- Internal packages for generated controller code (
internal/controller/) - Public API packages under
pkg/ - Avoid circular dependencies between packages
- Use dependency injection for testing (see controller setup)
Error Handling:
- Use
github.com/pkg/errorsfor error wrapping with context - Return errors from functions, don't panic (except for impossible states)
- Wrap errors with context:
errors.Wrap(err, "cannot configure resource")
Architecture Patterns:
- External names required for all resources (Go type:
config.ExternalName) - Schema-driven generation (no manual type definitions)
- Interface-based design for extensibility (
resource.Terraformed,controller.ExternalClient) - Kubernetes-native patterns via crossplane-runtime