In this guide, we will generate a Crossplane provider based on an existing Terraform provider using Terrajet.
We have chosen Terraform GitHub provider as an example, but the process will be quite similar for any other Terraform provider.
-
Generate a GitHub repository for the Crossplane provider by hitting the "Use this template" button in provider-jet-template repository.
-
Clone the repository to your local and
cd
into the repository directory. -
Replace
template
with your provider name.-
Export
ProviderName
:export ProviderNameLower=github export ProviderNameUpper=GitHub
-
Run the
./hack/prepare.sh
script from repo root to prepare the repo, e.g., to replace all occurrences oftemplate
with your provider name:./hack/prepare.sh
-
-
Configure your repo for the Terraform provider binary and schema:
-
Update the following variables in
Makefile
for Terraform Provider:export TERRAFORM_PROVIDER_SOURCE := integrations/github export TERRAFORM_PROVIDER_VERSION := 4.17.0 export TERRAFORM_PROVIDER_DOWNLOAD_NAME := terraform-provider-github export TERRAFORM_PROVIDER_DOWNLOAD_URL_PREFIX := https://releases.hashicorp.com/terraform-provider-github/4.17.0
You could find
TERRAFORM_PROVIDER_SOURCE
andTERRAFORM_PROVIDER_VERSION
in Terraform GitHub provider documentation by hitting the "USE PROVIDER" button. Check this line in controller Dockerfile to see how these variables are used to build the provider plugin binary. -
Update import path of the Terraform provider schema package in the following two files:
cmd/generator/main.go
andcmd/provider/main.go
Provider schema package is typically under the
<provider-name>
directory in the GitHub repository of the Terraform provider, e.g. ingithub
directory for the GitHub provider.import ( ... - tf "github.com/hashicorp/terraform-provider-hashicups/hashicups" + tf "github.com/turkenh/terraform-provider-github/v4/github" ... )
Run:
go mod tidy
Please note, we are temporarily using a fork of terraform-provider-github repo as a workaround to this issue.
-
If your provider uses an old version (<v2) of terraform-plugin-sdk, convert resource map to v2 schema as follows (in
cmd/generator/main.go
, uncomment related section):import ( "github.com/crossplane/terrajet/pkg/types/conversion" ) func main() { ... resourceMap := conversion.GetV2ResourceMap(tf.Provider()) pipeline.Run(config.GetProvider(resourceMap), absRootDir) }
In a similar way in
cmd/provider/main.go
:import ( "github.com/crossplane/terrajet/pkg/types/conversion" ) func main() { ... resourceMap := conversion.GetV2ResourceMap(tf.Provider()) kingpin.FatalIfError(controller.Setup(mgr, log, rl, setup, ws, pconfig.GetProvider(resourceMap), 1), "Cannot setup Template controllers") kingpin.FatalIfError(mgr.Start(ctrl.SetupSignalHandler()), "Cannot start controller manager") }
And in
go.mod
file, set the followingreplace directive
(uncomment related section):github.com/hashicorp/terraform-plugin-sdk => github.com/turkenh/terraform-plugin-sdk v1.17.2-patch1
Run:
go mod tidy
-
-
Implement
ProviderConfig
logic. Inprovider-jet-template
, there is already a boilerplate code in fileinternal/clients/${ProviderNameLower}.go
which takes care of properly fetching secret data referenced fromProviderConfig
resource.For our GitHub provider, we need to check Terraform documentation for provider configuration and provide the keys there:
const ( keyBaseURL = "base_url" keyOwner = "owner" keyToken = "token" // GitHub credentials environment variable names envToken = "GITHUB_TOKEN" ) func TerraformSetupBuilder(version, providerSource, providerVersion string) terraform.SetupFn { ... // set provider configuration ps.Configuration = map[string]interface{}{} if v, ok := githubCreds[keyBaseURL]; ok { ps.Configuration[keyBaseURL] = v } if v, ok := githubCreds[keyOwner]; ok { ps.Configuration[keyOwner] = v } // set environment variables for sensitive provider configuration ps.Env = []string{ fmt.Sprintf(fmtEnvVar, envToken, githubCreds[keyToken]), } return ps, nil }
-
Before generating all resources that the provider has, let's go step by step and only start with generating
github_repository
andgithub_branch
resources.To limit the resources to be generated, we need to provide an include list option with
tjconfig.WithIncludeList
in fileconfig/provider.go
:pc := tjconfig.NewProvider(resourceMap, resourcePrefix, modulePath, tjconfig.WithDefaultResourceFn(defaultResourceFn), tjconfig.WithIncludeList([]string{ "github_repository$", "github_branch$", }))
-
Finally, we would need to add some custom configurations for these two resources as follows:
# Create custom configuration directory for whole repository group mkdir config/repository # Create custom configuration directory for whole branch group mkdir config/branch
cat <<EOF > config/repository/config.go package repository import "github.com/crossplane/terrajet/pkg/config" // Configure configures individual resources by adding custom ResourceConfigurators. func Configure(p *config.Provider) { p.AddResourceConfigurator("github_repository", func(r *config.Resource) { // we need to override the default group that terrajet generated for // this resource, which would be "github" r.ShortGroup = "repository" }) } EOF
cat <<EOF > config/branch/config.go package branch import "github.com/crossplane/terrajet/pkg/config" func Configure(p *config.Provider) { p.AddResourceConfigurator("github_branch", func(r *config.Resource) { // we need to override the default group that terrajet generated for // this resource, which would be "github" r.ShortGroup = "branch" // Identifier for this resource is assigned by the provider. In other // words it is not simply the name of the resource. r.ExternalName = config.IdentifierFromProvider // This resource need the repository in which branch would be created // as an input. And by defining it as a reference to Repository // object, we can build cross resource referencing. See // repositoryRef in the example in the Testing section below. r.References["repository"] = config.Reference{ Type: "github.com/crossplane-contrib/provider-jet-github/apis/repository/v1alpha1.Repository", } }) } EOF
And register custom configurations in
config/provider.go
:import ( tjconfig "github.com/crossplane/terrajet/pkg/config" "github.com/crossplane/terrajet/pkg/types/conversion" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" tf "github.com/turkenh/terraform-provider-github/v4/github" + "github.com/crossplane-contrib/provider-jet-github/config/branch" + "github.com/crossplane-contrib/provider-jet-github/config/repository" ) func GetProvider() *tjconfig.Provider { ... for _, configure := range []func(provider *tjconfig.Provider){ add custom config functions + repository.Configure, + branch.Configure, } { configure(pc) }
To learn more about custom resource configurations (in step 7), please see the Configuring a Resource document.
-
Now we can generate our Terrajet Provider:
make generate
To add more resources, please follow the steps between 6-8 for each resource.
Alternatively, you can drop the tjconfig.WithIncludeList
option in provider
Configuration which would generate all resources, and you can add resource
configurations as a next step.
Now let's test our generated resources.
-
First, we will create example resources under the
examples
directory:Create example directories for repository and branch groups:
mkdir examples/repository mkdir examples/branch # remove the sample directory which was an example in the template rm -rf examples/sample
Create a provider secret template:
cat <<EOF > examples/providerconfig/secret.yaml.tmpl apiVersion: v1 kind: Secret metadata: name: example-creds namespace: crossplane-system type: Opaque stringData: credentials: | { "token": "y0ur-t0k3n" } EOF
Create example for
repository
resource, which will useprovider-jet-template
repo as template for the repository to be created:cat <<EOF > examples/repository/repository.yaml apiVersion: repository.github.jet.crossplane.io/v1alpha1 kind: Repository metadata: name: hello-crossplane spec: forProvider: description: "Managed with Crossplane Github Provider (generated with Terrajet)" visibility: public template: - owner: crossplane-contrib repository: provider-jet-template providerConfigRef: name: default EOF
Create
branch
resource which refers to the above repository managed resource:cat <<EOF > examples/branch/branch.yaml apiVersion: branch.github.jet.crossplane.io/v1alpha1 kind: Branch metadata: name: hello-terrajet spec: forProvider: branch: hello-terrajet repositoryRef: name: hello-crossplane providerConfigRef: name: default EOF
-
Generate a Personal Access Token for your Github account with
repo/public_repo
anddelete_repo
scopes. -
Create
examples/providerconfig/secret.yaml
fromexamples/providerconfig/secret.yaml.tmpl
and set your token in the file:GITHUB_TOKEN=<your-token-here> cat examples/providerconfig/secret.yaml.tmpl | sed -e "s/y0ur-t0k3n/${GITHUB_TOKEN}/g" > examples/providerconfig/secret.yaml
-
Apply CRDs:
kubectl apply -f package/crds
-
Run the provider:
make run
-
Apply ProviderConfig and example manifests (In another terminal since the previous command is blocking):
# Create "crossplane-system" namespace if not exists kubectl create namespace crossplane-system --dry-run=client -o yaml | kubectl apply -f - kubectl apply -f examples/providerconfig/ kubectl apply -f examples/repository/repository.yaml kubectl apply -f examples/branch/branch.yaml
-
Observe managed resources and wait until they are ready:
watch kubectl get managed
NAME READY SYNCED EXTERNAL-NAME AGE branch.branch.github.jet.crossplane.io/hello-terrajet True True hello-crossplane:hello-terrajet 89s NAME READY SYNCED EXTERNAL-NAME AGE repository.repository.github.jet.crossplane.io/hello-crossplane True True hello-crossplane 89s
Verify that repo
hello-crossplane
and branchhello-terrajet
created under your GitHub account. -
Cleanup
kubectl delete -f examples/branch/branch.yaml kubectl delete -f examples/repository/repository.yaml
Verify that the repo got deleted once deletion is completed on the control plane.