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 }