github.com/crossplane/upjet@v1.3.0/docs/generating-a-provider.md (about)

     1  <!--
     2  SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     3  
     4  SPDX-License-Identifier: CC-BY-4.0
     5  -->
     6  # Generating a Crossplane provider
     7  
     8  This guide shows you how to generate a Crossplane provider based on an existing
     9  Terraform provider using Upjet. The guide uses the [Terraform GitHub provider]
    10  as the example, but the process is similar for any other Terraform provider.
    11  
    12  ## Prepare your new provider repository
    13  
    14  1. Create a new GitHub repository for the Crossplane provider by clicking the
    15  "**Use this template**" button in the [upjet-provider-template] repository. The
    16  expected repository name is in the format `provider-<name>`. For example,
    17  `provider-github`. The script in step 3 expects this format and fails if you
    18  follow a different naming convention.
    19  1. Clone the repository to your local environment and `cd` into the repository
    20  directory.
    21  1. Fetch the [upbound/build] submodule by running the following
    22  command:
    23  
    24      ```bash
    25      make submodules
    26      ```
    27  
    28  1. To setup your provider name and group run the `./hack/prepare.sh`
    29  script from the repository root to prepare the code.
    30  
    31      ```bash
    32      ./hack/prepare.sh
    33      ```
    34  
    35  1. Ensure your organization name is correct in the `Makefile` for the
    36    `PROJECT_REPO` variable.
    37  1. To configure which Terraform provider to generate from, update the following
    38  variables in the `Makefile`:
    39  
    40    | Variable | Description |
    41    | -------- | ----------- |
    42    | `TERRAFORM_PROVIDER_SOURCE` | Find this variable on the Terraform registry for the provider. You can see the source value when clicking on the "`USE PROVIDER`" dropdown button in the navigation. |
    43    |`TERRAFORM_PROVIDER_REPO` | The URL to the repository that hosts the provider's code. |
    44    | `TERRAFORM_PROVIDER_VERSION` | Find this variable on the Terraform registry for the provider. You can see the source value when clicking on the "`USE PROVIDER`" dropdown button in the navigation. |
    45    |`TERRAFORM_PROVIDER_DOWNLOAD_NAME` | The name of the provider in the [Terraform registry](https://releases.hashicorp.com/) |
    46    |`TERRAFORM_NATIVE_PROVIDER_BINARY` | The name of the binary in the Terraform provider. This follows the pattern `terraform-provider-{provider name}_v{provider version}`. |
    47    |`TERRAFORM_DOCS_PATH` | The relative path, from the root of the repository, where the provider resource documentation exist. |
    48    
    49    For example, for the [Terraform GitHub provider], the variables are:
    50  
    51    ```makefile
    52    export TERRAFORM_PROVIDER_SOURCE := integrations/github
    53    export TERRAFORM_PROVIDER_REPO := https://github.com/integrations/terraform-provider-github
    54    export TERRAFORM_PROVIDER_VERSION := 5.32.0
    55    export TERRAFORM_PROVIDER_DOWNLOAD_NAME := terraform-provider-github
    56    export TERRAFORM_NATIVE_PROVIDER_BINARY := terraform-provider-github_v5.32.0
    57    export TERRAFORM_DOCS_PATH := website/docs/r
    58    ```
    59  
    60    Refer to [the Dockerfile](https://github.com/upbound/upjet-provider-template/blob/main/cluster/images/upjet-provider-template/Dockerfile) to see the variables called when building the provider.
    61  
    62  ## Configure the provider resources
    63  
    64  1. First you need to add the `ProviderConfig` logic.
    65      - In `upjet-provider-template`, there is
    66      already boilerplate code in the file `internal/clients/github.go` which takes
    67      care of fetching secret data referenced from the `ProviderConfig` resource.
    68      - Reference the [Terraform Github provider] documentation for information on
    69      authentication and provide the necessary keys.:
    70  
    71      ```go
    72      const (
    73        ...
    74        keyBaseURL = "base_url"
    75        keyOwner = "owner"
    76        keyToken = "token"
    77      )
    78      ```
    79  
    80      ```go
    81      func TerraformSetupBuilder(version, providerSource, providerVersion string) terraform.SetupFn {
    82        ...
    83        // set provider configuration
    84        ps.Configuration = map[string]any{}
    85        if v, ok := creds[keyBaseURL]; ok {
    86          ps.Configuration[keyBaseURL] = v
    87        }
    88        if v, ok := creds[keyOwner]; ok {
    89          ps.Configuration[keyOwner] = v
    90        }
    91        if v, ok := creds[keyToken]; ok {
    92          ps.Configuration[keyToken] = v
    93        }
    94        return ps, nil
    95      }
    96      ```
    97  
    98  1. Next add external name configurations for the [github_repository] and
    99      [github_branch] Terraform resources.
   100  
   101      > [!NOTE]
   102      > Only generate resources with an external name configuration defined.
   103  
   104      - Add external name configurations for these two resources in
   105      `config/external_name.go` as an entry to the map called
   106      `ExternalNameConfigs`
   107  
   108      ```go
   109      // ExternalNameConfigs contains all external name configurations for this
   110      // provider.
   111      var ExternalNameConfigs = map[string]config.ExternalName{
   112        ...
   113        // Name is a parameter and it is also used to import the resource.
   114        "github_repository": config.NameAsIdentifier,
   115        // The import ID consists of several parameters. We'll use branch name as
   116        // the external name.
   117        "github_branch": config.TemplatedStringAsIdentifier("branch", "{{ .parameters.repository }}:{{ .external_name }}:{{ .parameters.source_branch }}"),
   118      }
   119      ```
   120  
   121      - Take a look at the documentation for configuring a resource for more
   122      information about [external name configuration](configuring-a-resource.md#external-name).
   123  
   124  1. Next add custom configurations for these two resources as follows:
   125  
   126      - Create custom configuration directory for whole repository group
   127  
   128      ```bash
   129      mkdir config/repository    
   130      ```
   131  
   132      - Create custom configuration directory for whole branch group
   133  
   134      ```bash
   135      mkdir config/branch
   136      ```
   137  
   138      - Create the repository group configuration file
   139  
   140      ```bash
   141      cat <<EOF > config/repository/config.go
   142      package repository
   143  
   144      import "github.com/crossplane/upjet/pkg/config"
   145  
   146      // Configure configures individual resources by adding custom ResourceConfigurators.
   147      func Configure(p *config.Provider) {
   148          p.AddResourceConfigurator("github_repository", func(r *config.Resource) {
   149              // We need to override the default group that upjet generated for
   150              // this resource, which would be "github"
   151              r.ShortGroup = "repository"
   152          })
   153      }
   154      EOF
   155      ```
   156  
   157      - Create the branch group configuration file
   158  
   159      > [!NOTE]
   160      > Note that you need to change `myorg/provider-github` to your organization.
   161  
   162      ```bash
   163      cat <<EOF > config/branch/config.go
   164      package branch
   165  
   166      import "github.com/crossplane/upjet/pkg/config"
   167  
   168      func Configure(p *config.Provider) {
   169          p.AddResourceConfigurator("github_branch", func(r *config.Resource) {
   170              // We need to override the default group that upjet generated for
   171              // this resource, which would be "github"
   172              r.ShortGroup = "branch"
   173  
   174              // This resource need the repository in which branch would be created
   175              // as an input. And by defining it as a reference to Repository
   176              // object, we can build cross resource referencing. See
   177              // repositoryRef in the example in the Testing section below.
   178              r.References["repository"] = config.Reference{
   179                  Type: "github.com/myorg/provider-github/apis/repository/v1alpha1.Repository",
   180              }
   181          })
   182      }
   183      EOF
   184      ```
   185  
   186      And register custom configurations in `config/provider.go`:
   187  
   188      ```diff
   189      import (
   190          ...
   191  
   192          ujconfig "github.com/upbound/crossplane/pkg/config"
   193  
   194      -   "github.com/myorg/provider-github/config/null"
   195      +   "github.com/myorg/provider-github/config/branch"
   196      +   "github.com/myorg/provider-github/config/repository"
   197       )
   198  
   199       func GetProvider() *ujconfig.Provider {
   200          ...
   201          for _, configure := range []func(provider *ujconfig.Provider){
   202                  // add custom config functions
   203      -           null.Configure,
   204      +           repository.Configure,
   205      +           branch.Configure,
   206          } {
   207                  configure(pc)
   208          }
   209      ```
   210  
   211      _To learn more about custom resource configurations (in step 7), please
   212      see the [Configuring a Resource](configuring-a-resource.md) document._
   213  
   214  1. Now we can generate our Upjet Provider:
   215  
   216      Before we run `make generate` ensure to install `goimports`
   217  
   218      ```bash
   219      go install golang.org/x/tools/cmd/goimports@latest
   220      ```
   221  
   222      ```bash
   223      make generate
   224      ```
   225  
   226  ## Testing the generated resources
   227  
   228  Now let's test our generated resources.
   229  
   230  1. First, we will create example resources under the `examples` directory:
   231  
   232     Create example directories for repository and branch groups:
   233  
   234     ```bash
   235     mkdir examples/repository
   236     mkdir examples/branch
   237  
   238     # remove the sample directory which was an example in the template
   239     rm -rf examples/null
   240     ```
   241  
   242     Create a provider secret template:
   243  
   244     ```bash
   245     cat <<EOF > examples/providerconfig/secret.yaml.tmpl
   246     apiVersion: v1
   247     kind: Secret
   248     metadata:
   249       name: example-creds
   250       namespace: crossplane-system
   251     type: Opaque
   252     stringData:
   253       credentials: |
   254         {
   255           "token": "y0ur-t0k3n"
   256         }
   257     EOF
   258     ```
   259  
   260     Create example for `repository` resource, which will use
   261     `upjet-provider-template` repo as template for the repository to be created:
   262  
   263     ```bash
   264     cat <<EOF > examples/repository/repository.yaml
   265     apiVersion: repository.github.upbound.io/v1alpha1
   266     kind: Repository
   267     metadata:
   268       name: hello-crossplane
   269     spec:
   270       forProvider:
   271         description: "Managed with Crossplane Github Provider (generated with Upjet)"
   272         visibility: public
   273         template:
   274           - owner: upbound
   275             repository: upjet-provider-template
   276       providerConfigRef:
   277         name: default
   278     EOF
   279     ```
   280  
   281     Create `branch` resource which refers to the above repository managed
   282     resource:
   283  
   284     ```bash
   285     cat <<EOF > examples/branch/branch.yaml
   286     apiVersion: branch.github.upbound.io/v1alpha1
   287     kind: Branch
   288     metadata:
   289       name: hello-upjet
   290     spec:
   291       forProvider:
   292         repositoryRef:
   293           name: hello-crossplane
   294       providerConfigRef:
   295         name: default
   296     EOF
   297     ```
   298  
   299     In order to change the `apiVersion`, you can use `WithRootGroup` and
   300     `WithShortName` options in `config/provider.go` as arguments to
   301     `ujconfig.NewProvider`.
   302  
   303  2. Generate a [Personal Access Token](https://github.com/settings/tokens) for
   304     your Github account with `repo/public_repo` and `delete_repo` scopes.
   305  
   306  3. Create `examples/providerconfig/secret.yaml` from
   307     `examples/providerconfig/secret.yaml.tmpl` and set your token in the file:
   308  
   309     ```bash
   310     GITHUB_TOKEN=<your-token-here>
   311     cat examples/providerconfig/secret.yaml.tmpl | sed -e "s/y0ur-t0k3n/${GITHUB_TOKEN}/g" > examples/providerconfig/secret.yaml
   312     ```
   313  
   314  4. Apply CRDs:
   315  
   316     ```bash
   317     kubectl apply -f package/crds
   318     ```
   319  
   320  5. Run the provider:
   321  
   322     Please make sure Terraform is installed before running the "make run"
   323     command, you can check
   324     [this guide](https://developer.hashicorp.com/terraform/downloads).
   325  
   326     ```bash
   327     make run
   328     ```
   329  
   330  6. Apply ProviderConfig and example manifests (_In another terminal since the
   331     previous command is blocking_):
   332  
   333     ```bash
   334     # Create "crossplane-system" namespace if not exists
   335     kubectl create namespace crossplane-system --dry-run=client -o yaml | kubectl apply -f -
   336  
   337     kubectl apply -f examples/providerconfig/
   338     kubectl apply -f examples/repository/repository.yaml
   339     kubectl apply -f examples/branch/branch.yaml
   340     ```
   341  
   342  7. Observe managed resources and wait until they are ready:
   343  
   344     ```bash
   345     watch kubectl get managed
   346     ```
   347  
   348     ```bash
   349     NAME                                                   READY   SYNCED   EXTERNAL-NAME                     AGE
   350     branch.branch.github.jet.crossplane.io/hello-upjet   True    True     hello-crossplane:hello-upjet   89s
   351  
   352     NAME                                                             READY   SYNCED   EXTERNAL-NAME      AGE
   353     repository.repository.github.jet.crossplane.io/hello-crossplane   True    True     hello-crossplane   89s
   354     ```
   355  
   356     Verify that repo `hello-crossplane` and branch `hello-upjet` created under
   357     your GitHub account.
   358  
   359  8. You can check the errors and events by calling `kubectl describe` for either
   360     of the resources.
   361  
   362  9. Cleanup
   363  
   364     ```bash
   365     kubectl delete -f examples/branch/branch.yaml
   366     kubectl delete -f examples/repository/repository.yaml
   367     ```
   368  
   369     Verify that the repo got deleted once deletion is completed on the control
   370     plane.
   371  
   372  ## Next steps
   373  
   374  Now that you've seen the basics of generating `CustomResourceDefinitions` for
   375  your provider, you can learn more about
   376  [configuring resources](configuring-a-resource.md) or
   377  [testing your resources](testing-with-uptest.md) with Uptest.
   378  
   379  [Terraform GitHub provider]: https://registry.terraform.io/providers/integrations/github/latest/docs
   380  [upjet-provider-template]: https://github.com/upbound/upjet-provider-template
   381  [upbound/build]: https://github.com/upbound/build
   382  [github_repository]: https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository
   383  [github_branch]: https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch