github.com/crossplane/upjet@v1.3.0/pkg/config/provider.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package config
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"regexp"
    11  
    12  	tfjson "github.com/hashicorp/terraform-json"
    13  	fwprovider "github.com/hashicorp/terraform-plugin-framework/provider"
    14  	fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
    15  	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    16  	"github.com/pkg/errors"
    17  
    18  	"github.com/crossplane/upjet/pkg/registry"
    19  	conversiontfjson "github.com/crossplane/upjet/pkg/types/conversion/tfjson"
    20  )
    21  
    22  // ResourceConfiguratorFn is a function that implements the ResourceConfigurator
    23  // interface
    24  type ResourceConfiguratorFn func(r *Resource)
    25  
    26  // Configure configures a resource by calling ResourceConfiguratorFn
    27  func (c ResourceConfiguratorFn) Configure(r *Resource) {
    28  	c(r)
    29  }
    30  
    31  // ResourceConfigurator configures a Resource
    32  type ResourceConfigurator interface {
    33  	Configure(r *Resource)
    34  }
    35  
    36  // A ResourceConfiguratorChain chains multiple ResourceConfigurators.
    37  type ResourceConfiguratorChain []ResourceConfigurator
    38  
    39  // Configure configures a resource by calling each ResourceConfigurator in the
    40  // chain serially.
    41  func (cc ResourceConfiguratorChain) Configure(r *Resource) {
    42  	for _, c := range cc {
    43  		c.Configure(r)
    44  	}
    45  }
    46  
    47  // BasePackages keeps lists of packages that needs to be registered as API
    48  // and controllers. Typically, we expect to see ProviderConfig packages here.
    49  // These APIs and controllers belong to non-generated (manually maintained)
    50  // resources.
    51  type BasePackages struct {
    52  	APIVersion []string
    53  	// Deprecated: Use ControllerMap instead.
    54  	Controller    []string
    55  	ControllerMap map[string]string
    56  }
    57  
    58  // Provider holds configuration for a provider to be generated with Upjet.
    59  type Provider struct {
    60  	// TerraformResourcePrefix is the prefix used in all resources of this
    61  	// Terraform provider, e.g. "aws_". Defaults to "<prefix>_". This is being
    62  	// used while setting some defaults like Kind of the resource. For example,
    63  	// for "aws_rds_cluster", we drop "aws_" prefix and its group ("rds") to set
    64  	// Kind of the resource as "Cluster".
    65  	TerraformResourcePrefix string
    66  
    67  	// RootGroup is the root group that all CRDs groups in the provider are based
    68  	// on, e.g. "aws.upbound.io".
    69  	// Defaults to "<TerraformResourcePrefix>.upbound.io".
    70  	RootGroup string
    71  
    72  	// ShortName is the short name of the provider. Typically, added as a CRD
    73  	// category, e.g. "awsjet". Default to "<prefix>jet". For more details on CRD
    74  	// categories, see: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#categories
    75  	ShortName string
    76  
    77  	// ModulePath is the go module path for the Crossplane provider repo, e.g.
    78  	// "github.com/upbound/provider-aws"
    79  	ModulePath string
    80  
    81  	// FeaturesPackage is the relative package patch for the features package to
    82  	// configure the features behind the feature gates.
    83  	FeaturesPackage string
    84  
    85  	// BasePackages keeps lists of base packages that needs to be registered as
    86  	// API and controllers. Typically, we expect to see ProviderConfig packages
    87  	// here.
    88  	BasePackages BasePackages
    89  
    90  	// DefaultResourceOptions is a list of config.ResourceOption that will be
    91  	// applied to all resources before any user-provided options are applied.
    92  	DefaultResourceOptions []ResourceOption
    93  
    94  	// SkipList is a list of regex for the Terraform resources to be skipped.
    95  	// For example, to skip generation of "aws_shield_protection_group", one
    96  	// can add "aws_shield_protection_group$". To skip whole aws waf group, one
    97  	// can add "aws_waf.*" to the list.
    98  	SkipList []string
    99  
   100  	// MainTemplate is the template string to be used to render the
   101  	// provider subpackage main program. If this is set, the generated provider
   102  	// is broken up into subpackage families partitioned across the API groups.
   103  	// A monolithic provider is also generated to
   104  	// ensure backwards-compatibility.
   105  	MainTemplate string
   106  
   107  	// skippedResourceNames is a list of Terraform resource names
   108  	// available in the Terraform provider schema, but
   109  	// not in the include list or in the skip list, meaning that
   110  	// the corresponding managed resources are not generated.
   111  	skippedResourceNames []string
   112  
   113  	// IncludeList is a list of regex for the Terraform resources to be
   114  	// included and reconciled via the Terraform CLI.
   115  	// For example, to include "aws_shield_protection_group" into
   116  	// the generated resources, one can add "aws_shield_protection_group$".
   117  	// To include whole aws waf group, one can add "aws_waf.*" to the list.
   118  	// Defaults to []string{".+"} which would include all resources.
   119  	IncludeList []string
   120  
   121  	// TerraformPluginSDKIncludeList is a list of regex for the Terraform resources
   122  	// implemented with Terraform Plugin SDKv2 to be included and reconciled
   123  	// in the no-fork architecture (without the Terraform CLI).
   124  	// For example, to include "aws_shield_protection_group" into
   125  	// the generated resources, one can add "aws_shield_protection_group$".
   126  	// To include whole aws waf group, one can add "aws_waf.*" to the list.
   127  	// Defaults to []string{".+"} which would include all resources.
   128  	TerraformPluginSDKIncludeList []string
   129  
   130  	// TerraformPluginFrameworkIncludeList is a list of regex for the Terraform
   131  	// resources implemented with Terraform Plugin Framework  to be included and
   132  	// reconciled in the no-fork architecture (without the Terraform CLI).
   133  	// For example, to include "aws_shield_protection_group" into
   134  	// the generated resources, one can add "aws_shield_protection_group$".
   135  	// To include whole aws waf group, one can add "aws_waf.*" to the list.
   136  	// Defaults to []string{".+"} which would include all resources.
   137  	TerraformPluginFrameworkIncludeList []string
   138  
   139  	// Resources is a map holding resource configurations where key is Terraform
   140  	// resource name.
   141  	Resources map[string]*Resource
   142  
   143  	// TerraformProvider is the Terraform provider in Terraform Plugin SDKv2
   144  	// compatible format
   145  	TerraformProvider *schema.Provider
   146  
   147  	// TerraformPluginFrameworkProvider is the Terraform provider reference
   148  	// in Terraform Plugin Framework compatible format
   149  	TerraformPluginFrameworkProvider fwprovider.Provider
   150  
   151  	// refInjectors is an ordered list of `ReferenceInjector`s for
   152  	// injecting references across this Provider's resources.
   153  	refInjectors []ReferenceInjector
   154  
   155  	// resourceConfigurators is a map holding resource configurators where key
   156  	// is Terraform resource name.
   157  	resourceConfigurators map[string]ResourceConfiguratorChain
   158  }
   159  
   160  // ReferenceInjector injects cross-resource references across the resources
   161  // of this Provider.
   162  type ReferenceInjector interface {
   163  	InjectReferences(map[string]*Resource) error
   164  }
   165  
   166  // A ProviderOption configures a Provider.
   167  type ProviderOption func(*Provider)
   168  
   169  // WithRootGroup configures RootGroup for resources of this Provider.
   170  func WithRootGroup(s string) ProviderOption {
   171  	return func(p *Provider) {
   172  		p.RootGroup = s
   173  	}
   174  }
   175  
   176  // WithShortName configures ShortName for resources of this Provider.
   177  func WithShortName(s string) ProviderOption {
   178  	return func(p *Provider) {
   179  		p.ShortName = s
   180  	}
   181  }
   182  
   183  // WithIncludeList configures IncludeList for this Provider.
   184  func WithIncludeList(l []string) ProviderOption {
   185  	return func(p *Provider) {
   186  		p.IncludeList = l
   187  	}
   188  }
   189  
   190  // WithTerraformPluginSDKIncludeList configures the TerraformPluginSDKIncludeList for this Provider,
   191  // with the given Terraform Plugin SDKv2-based resource name list
   192  func WithTerraformPluginSDKIncludeList(l []string) ProviderOption {
   193  	return func(p *Provider) {
   194  		p.TerraformPluginSDKIncludeList = l
   195  	}
   196  }
   197  
   198  // WithTerraformPluginFrameworkIncludeList configures the
   199  // TerraformPluginFrameworkIncludeList for this Provider, with the given
   200  // Terraform Plugin Framework-based resource name list
   201  func WithTerraformPluginFrameworkIncludeList(l []string) ProviderOption {
   202  	return func(p *Provider) {
   203  		p.TerraformPluginFrameworkIncludeList = l
   204  	}
   205  }
   206  
   207  // WithTerraformProvider configures the TerraformProvider for this Provider.
   208  func WithTerraformProvider(tp *schema.Provider) ProviderOption {
   209  	return func(p *Provider) {
   210  		p.TerraformProvider = tp
   211  	}
   212  }
   213  
   214  // WithTerraformPluginFrameworkProvider configures the
   215  // TerraformPluginFrameworkProvider for this Provider.
   216  func WithTerraformPluginFrameworkProvider(tp fwprovider.Provider) ProviderOption {
   217  	return func(p *Provider) {
   218  		p.TerraformPluginFrameworkProvider = tp
   219  	}
   220  }
   221  
   222  // WithSkipList configures SkipList for this Provider.
   223  func WithSkipList(l []string) ProviderOption {
   224  	return func(p *Provider) {
   225  		p.SkipList = l
   226  	}
   227  }
   228  
   229  // WithBasePackages configures BasePackages for this Provider.
   230  func WithBasePackages(b BasePackages) ProviderOption {
   231  	return func(p *Provider) {
   232  		p.BasePackages = b
   233  	}
   234  }
   235  
   236  // WithDefaultResourceOptions configures DefaultResourceOptions for this
   237  // Provider.
   238  func WithDefaultResourceOptions(opts ...ResourceOption) ProviderOption {
   239  	return func(p *Provider) {
   240  		p.DefaultResourceOptions = opts
   241  	}
   242  }
   243  
   244  // WithReferenceInjectors configures an ordered list of `ReferenceInjector`s
   245  // for this Provider. The configured reference resolvers are executed in order
   246  // to inject cross-resource references across this Provider's resources.
   247  func WithReferenceInjectors(refInjectors []ReferenceInjector) ProviderOption {
   248  	return func(p *Provider) {
   249  		p.refInjectors = refInjectors
   250  	}
   251  }
   252  
   253  // WithFeaturesPackage configures FeaturesPackage for this Provider.
   254  func WithFeaturesPackage(s string) ProviderOption {
   255  	return func(p *Provider) {
   256  		p.FeaturesPackage = s
   257  	}
   258  }
   259  
   260  func WithMainTemplate(template string) ProviderOption {
   261  	return func(p *Provider) {
   262  		p.MainTemplate = template
   263  	}
   264  }
   265  
   266  // NewProvider builds and returns a new Provider from provider
   267  // tfjson schema, that is generated using Terraform CLI with:
   268  // `terraform providers schema --json`
   269  func NewProvider(schema []byte, prefix string, modulePath string, metadata []byte, opts ...ProviderOption) *Provider { //nolint:gocyclo
   270  	ps := tfjson.ProviderSchemas{}
   271  	if err := ps.UnmarshalJSON(schema); err != nil {
   272  		panic(err)
   273  	}
   274  	if len(ps.Schemas) != 1 {
   275  		panic(fmt.Sprintf("there should exactly be 1 provider schema but there are %d", len(ps.Schemas)))
   276  	}
   277  	var rs map[string]*tfjson.Schema
   278  	for _, v := range ps.Schemas {
   279  		rs = v.ResourceSchemas
   280  		break
   281  	}
   282  	resourceMap := conversiontfjson.GetV2ResourceMap(rs)
   283  	providerMetadata, err := registry.NewProviderMetadataFromFile(metadata)
   284  	if err != nil {
   285  		panic(errors.Wrap(err, "cannot load provider metadata"))
   286  	}
   287  
   288  	p := &Provider{
   289  		ModulePath:              modulePath,
   290  		TerraformResourcePrefix: fmt.Sprintf("%s_", prefix),
   291  		RootGroup:               fmt.Sprintf("%s.upbound.io", prefix),
   292  		ShortName:               prefix,
   293  		BasePackages:            DefaultBasePackages,
   294  		IncludeList: []string{
   295  			// Include all Resources
   296  			".+",
   297  		},
   298  		Resources:             map[string]*Resource{},
   299  		resourceConfigurators: map[string]ResourceConfiguratorChain{},
   300  	}
   301  
   302  	for _, o := range opts {
   303  		o(p)
   304  	}
   305  
   306  	p.skippedResourceNames = make([]string, 0, len(resourceMap))
   307  	terraformPluginFrameworkResourceFunctionsMap := terraformPluginFrameworkResourceFunctionsMap(p.TerraformPluginFrameworkProvider)
   308  	for name, terraformResource := range resourceMap {
   309  		if len(terraformResource.Schema) == 0 {
   310  			// There are resources with no schema, that we will address later.
   311  			fmt.Printf("Skipping resource %s because it has no schema\n", name)
   312  		}
   313  		// if in both of the include lists, the new behavior prevails
   314  		isTerraformPluginSDK := matches(name, p.TerraformPluginSDKIncludeList)
   315  		isPluginFrameworkResource := matches(name, p.TerraformPluginFrameworkIncludeList)
   316  		isCLIResource := matches(name, p.IncludeList)
   317  		if (isTerraformPluginSDK && isPluginFrameworkResource) || (isTerraformPluginSDK && isCLIResource) || (isPluginFrameworkResource && isCLIResource) {
   318  			panic(errors.Errorf(`resource %q is specified in more than one include list. It should appear in at most one of the lists "IncludeList", "TerraformPluginSDKIncludeList" or "TerraformPluginFrameworkIncludeList"`, name))
   319  		}
   320  		if len(terraformResource.Schema) == 0 || matches(name, p.SkipList) || (!matches(name, p.IncludeList) && !isTerraformPluginSDK && !isPluginFrameworkResource) {
   321  			p.skippedResourceNames = append(p.skippedResourceNames, name)
   322  			continue
   323  		}
   324  		if isTerraformPluginSDK {
   325  			if p.TerraformProvider == nil || p.TerraformProvider.ResourcesMap[name] == nil {
   326  				panic(errors.Errorf("resource %q is configured to be reconciled with Terraform Plugin SDK"+
   327  					"but either config.Provider.TerraformProvider is not configured or the Go schema does not exist for the resource", name))
   328  			}
   329  			terraformResource = p.TerraformProvider.ResourcesMap[name]
   330  			if terraformResource.Schema == nil {
   331  				if terraformResource.SchemaFunc == nil {
   332  					p.skippedResourceNames = append(p.skippedResourceNames, name)
   333  					fmt.Printf("Skipping resource %s because it has no schema and no schema function\n", name)
   334  					continue
   335  				}
   336  				terraformResource.Schema = terraformResource.SchemaFunc()
   337  			}
   338  		}
   339  
   340  		var terraformPluginFrameworkResource fwresource.Resource
   341  		if isPluginFrameworkResource {
   342  			resourceFunc := terraformPluginFrameworkResourceFunctionsMap[name]
   343  			if p.TerraformPluginFrameworkProvider == nil || resourceFunc == nil {
   344  				panic(errors.Errorf("resource %q is configured to be reconciled with Terraform Plugin Framework"+
   345  					"but either config.Provider.TerraformPluginFrameworkProvider is not configured or the provider doesn't have the resource.", name))
   346  			}
   347  
   348  			terraformPluginFrameworkResource = resourceFunc()
   349  		}
   350  
   351  		p.Resources[name] = DefaultResource(name, terraformResource, terraformPluginFrameworkResource, providerMetadata.Resources[name], p.DefaultResourceOptions...)
   352  		p.Resources[name].useTerraformPluginSDKClient = isTerraformPluginSDK
   353  		p.Resources[name].useTerraformPluginFrameworkClient = isPluginFrameworkResource
   354  	}
   355  	for i, refInjector := range p.refInjectors {
   356  		if err := refInjector.InjectReferences(p.Resources); err != nil {
   357  			panic(errors.Wrapf(err, "cannot inject references using the configured ReferenceInjector at index %d", i))
   358  		}
   359  	}
   360  	return p
   361  }
   362  
   363  // AddResourceConfigurator adds resource specific configurators.
   364  func (p *Provider) AddResourceConfigurator(resource string, c ResourceConfiguratorFn) { //nolint:interfacer
   365  	// Note(turkenh): nolint reasoning - easier to provide a function without
   366  	// converting to an explicit type supporting the ResourceConfigurator
   367  	// interface. Since this function would be a frequently used one, it should
   368  	// be a reasonable simplification.
   369  	p.resourceConfigurators[resource] = append(p.resourceConfigurators[resource], c)
   370  }
   371  
   372  // SetResourceConfigurator sets ResourceConfigurator for a resource. This will
   373  // override all previously added ResourceConfigurators for this resource.
   374  func (p *Provider) SetResourceConfigurator(resource string, c ResourceConfigurator) {
   375  	p.resourceConfigurators[resource] = ResourceConfiguratorChain{c}
   376  }
   377  
   378  // ConfigureResources configures resources with provided ResourceConfigurator's
   379  func (p *Provider) ConfigureResources() {
   380  	for name, c := range p.resourceConfigurators {
   381  		// if not skipped & included & configured via the default configurator
   382  		if r, ok := p.Resources[name]; ok {
   383  			c.Configure(r)
   384  		}
   385  	}
   386  }
   387  
   388  // GetSkippedResourceNames returns a list of Terraform resource names
   389  // available in the Terraform provider schema, but
   390  // not in the include list or in the skip list, meaning that
   391  // the corresponding managed resources are not generated.
   392  func (p *Provider) GetSkippedResourceNames() []string {
   393  	return p.skippedResourceNames
   394  }
   395  
   396  func matches(name string, regexList []string) bool {
   397  	for _, r := range regexList {
   398  		ok, err := regexp.MatchString(r, name)
   399  		if err != nil {
   400  			panic(errors.Wrap(err, "cannot match regular expression"))
   401  		}
   402  		if ok {
   403  			return true
   404  		}
   405  	}
   406  	return false
   407  }
   408  
   409  func terraformPluginFrameworkResourceFunctionsMap(provider fwprovider.Provider) map[string]func() fwresource.Resource {
   410  	if provider == nil {
   411  		return make(map[string]func() fwresource.Resource, 0)
   412  	}
   413  
   414  	ctx := context.TODO()
   415  	resourceFunctions := provider.Resources(ctx)
   416  	resourceFunctionsMap := make(map[string]func() fwresource.Resource, len(resourceFunctions))
   417  
   418  	providerMetadata := fwprovider.MetadataResponse{}
   419  	provider.Metadata(ctx, fwprovider.MetadataRequest{}, &providerMetadata)
   420  
   421  	for _, resourceFunction := range resourceFunctions {
   422  		resource := resourceFunction()
   423  
   424  		resourceTypeNameReq := fwresource.MetadataRequest{
   425  			ProviderTypeName: providerMetadata.TypeName,
   426  		}
   427  		resourceTypeNameResp := fwresource.MetadataResponse{}
   428  		resource.Metadata(ctx, resourceTypeNameReq, &resourceTypeNameResp)
   429  
   430  		resourceFunctionsMap[resourceTypeNameResp.TypeName] = resourceFunction
   431  	}
   432  
   433  	return resourceFunctionsMap
   434  }