github.com/crossplane/upjet@v1.3.0/pkg/config/resource.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 "time" 11 12 xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 13 "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 14 "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" 15 xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" 16 fwresource "github.com/hashicorp/terraform-plugin-framework/resource" 17 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 18 "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 19 "github.com/pkg/errors" 20 "k8s.io/apimachinery/pkg/util/json" 21 "k8s.io/apimachinery/pkg/util/sets" 22 "k8s.io/utils/ptr" 23 "sigs.k8s.io/controller-runtime/pkg/client" 24 25 "github.com/crossplane/upjet/pkg/config/conversion" 26 "github.com/crossplane/upjet/pkg/registry" 27 ) 28 29 // A ListType is a type of list. 30 type ListType string 31 32 // Types of lists. 33 const ( 34 // ListTypeAtomic means the entire list is replaced during merge. At any 35 // point in time, a single manager owns the list. 36 ListTypeAtomic ListType = "atomic" 37 38 // ListTypeSet can be granularly merged, and different managers can own 39 // different elements in the list. The list can include only scalar 40 // elements. 41 ListTypeSet ListType = "set" 42 43 // ListTypeMap can be granularly merged, and different managers can own 44 // different elements in the list. The list can include only nested types 45 // (i.e. objects). 46 ListTypeMap ListType = "map" 47 ) 48 49 // A MapType is a type of map. 50 type MapType string 51 52 // Types of maps. 53 const ( 54 // MapTypeAtomic means that the map can only be entirely replaced by a 55 // single manager. 56 MapTypeAtomic MapType = "atomic" 57 58 // MapTypeGranular means that the map supports separate managers updating 59 // individual fields. 60 MapTypeGranular MapType = "granular" 61 ) 62 63 // A StructType is a type of struct. 64 type StructType string 65 66 // Struct types. 67 const ( 68 // StructTypeAtomic means that the struct can only be entirely replaced by a 69 // single manager. 70 StructTypeAtomic StructType = "atomic" 71 72 // StructTypeGranular means that the struct supports separate managers 73 // updating individual fields. 74 StructTypeGranular StructType = "granular" 75 ) 76 77 // SetIdentifierArgumentsFn sets the name of the resource in Terraform attributes map, 78 // i.e. Main HCL file. 79 type SetIdentifierArgumentsFn func(base map[string]any, externalName string) 80 81 // NopSetIdentifierArgument does nothing. It's useful for cases where the external 82 // name is calculated by provider and doesn't have any effect on spec fields. 83 var NopSetIdentifierArgument SetIdentifierArgumentsFn = func(_ map[string]any, _ string) {} 84 85 // GetIDFn returns the ID to be used in TF State file, i.e. "id" field in 86 // terraform.tfstate. 87 type GetIDFn func(ctx context.Context, externalName string, parameters map[string]any, terraformProviderConfig map[string]any) (string, error) 88 89 // ExternalNameAsID returns the name to be used as ID in TF State file. 90 var ExternalNameAsID GetIDFn = func(_ context.Context, externalName string, _ map[string]any, _ map[string]any) (string, error) { 91 return externalName, nil 92 } 93 94 // GetExternalNameFn returns the external name extracted from the TF State. 95 type GetExternalNameFn func(tfstate map[string]any) (string, error) 96 97 // IDAsExternalName returns the TF State ID as external name. 98 var IDAsExternalName GetExternalNameFn = func(tfstate map[string]any) (string, error) { 99 if id, ok := tfstate["id"].(string); ok && id != "" { 100 return id, nil 101 } 102 return "", errors.New("cannot find id in tfstate") 103 } 104 105 // AdditionalConnectionDetailsFn functions adds custom keys to connection details 106 // secret using input terraform attributes 107 type AdditionalConnectionDetailsFn func(attr map[string]any) (map[string][]byte, error) 108 109 // NopAdditionalConnectionDetails does nothing, when no additional connection 110 // details configuration function provided. 111 var NopAdditionalConnectionDetails AdditionalConnectionDetailsFn = func(_ map[string]any) (map[string][]byte, error) { 112 return nil, nil 113 } 114 115 // ExternalName contains all information that is necessary for naming operations, 116 // such as removal of those fields from spec schema and calling Configure function 117 // to fill attributes with information given in external name. 118 type ExternalName struct { 119 // SetIdentifierArgumentFn sets the name of the resource in Terraform argument 120 // map. In many cases, there is a field called "name" in the HCL schema, however, 121 // there are cases like RDS DB Cluster where the name field in HCL is called 122 // "cluster_identifier". This function is the place that you can take external 123 // name and assign it to that specific key for that resource type. 124 SetIdentifierArgumentFn SetIdentifierArgumentsFn 125 126 // GetExternalNameFn returns the external name extracted from TF State. In most cases, 127 // "id" field contains all the information you need. You'll need to extract 128 // the format that is decided for external name annotation to use. 129 // For example the following is an Azure resource ID: 130 // /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1 131 // The function should return "mygroup1" so that it can be used to set external 132 // name if it was not set already. 133 GetExternalNameFn GetExternalNameFn 134 135 // GetIDFn returns the string that will be used as "id" key in TF state. In 136 // many cases, external name format is the same as "id" but when it is not 137 // we may need information from other places to construct it. For example, 138 // the following is an Azure resource ID: 139 // /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1 140 // The function here should use information from supplied arguments to 141 // construct this ID, i.e. "mygroup1" from external name, subscription ID 142 // from terraformProviderConfig, and others from parameters map if needed. 143 GetIDFn GetIDFn 144 145 // OmittedFields are the ones you'd like to be removed from the schema since 146 // they are specified via external name. For example, if you set 147 // "cluster_identifier" in SetIdentifierArgumentFn, then you need to omit 148 // that field. 149 // You can omit only the top level fields. 150 // No field is omitted by default. 151 OmittedFields []string 152 153 // DisableNameInitializer allows you to specify whether the name initializer 154 // that sets external name to metadata.name if none specified should be disabled. 155 // It needs to be disabled for resources whose external identifier is randomly 156 // assigned by the provider, like AWS VPC where it gets vpc-21kn123 identifier 157 // and not let you name it. 158 DisableNameInitializer bool 159 160 // IdentifierFields are the fields that are used to construct external 161 // resource identifier. We need to know these fields no matter what the 162 // management policy is including the Observe Only, different from other 163 // (required) fields. 164 IdentifierFields []string 165 } 166 167 // References represents reference resolver configurations for the fields of a 168 // given resource. Key should be the field path of the field to be referenced. 169 type References map[string]Reference 170 171 // Reference represents the Crossplane options used to generate 172 // reference resolvers for fields 173 type Reference struct { 174 // Type is the type name of the CRD if it is in the same package or 175 // <package-path>.<type-name> if it is in a different package. 176 Type string 177 // TerraformName is the name of the Terraform resource 178 // which will be referenced. The supplied resource name is 179 // converted to a type name of the corresponding CRD using 180 // the configured TerraformTypeMapper. 181 TerraformName string 182 // Extractor is the function to be used to extract value from the 183 // referenced type. Defaults to getting external name. 184 // Optional 185 Extractor string 186 // RefFieldName is the field name for the Reference field. Defaults to 187 // <field-name>Ref or <field-name>Refs. 188 // Optional 189 RefFieldName string 190 // SelectorFieldName is the field name for the Selector field. Defaults to 191 // <field-name>Selector. 192 // Optional 193 SelectorFieldName string 194 } 195 196 // Sensitive represents configurations to handle sensitive information 197 type Sensitive struct { 198 // AdditionalConnectionDetailsFn is the path for function adding additional 199 // connection details keys 200 AdditionalConnectionDetailsFn AdditionalConnectionDetailsFn 201 202 // fieldPaths keeps the mapping of sensitive fields in Terraform schema with 203 // terraform field path as key and xp field path as value. 204 fieldPaths map[string]string 205 } 206 207 // LateInitializer represents configurations that control 208 // late-initialization behaviour 209 type LateInitializer struct { 210 // IgnoredFields are the field paths to be skipped during 211 // late-initialization. Similar to other configurations, these paths are 212 // Terraform field paths concatenated with dots. For example, if we want to 213 // ignore "ebs" block in "aws_launch_template", we should add 214 // "block_device_mappings.ebs". 215 IgnoredFields []string 216 217 // ignoredCanonicalFieldPaths are the Canonical field paths to be skipped 218 // during late-initialization. This is filled using the `IgnoredFields` 219 // field which keeps Terraform paths by converting them to Canonical paths. 220 ignoredCanonicalFieldPaths []string 221 } 222 223 // GetIgnoredCanonicalFields returns the ignoredCanonicalFields 224 func (l *LateInitializer) GetIgnoredCanonicalFields() []string { 225 return l.ignoredCanonicalFieldPaths 226 } 227 228 // AddIgnoredCanonicalFields sets ignored canonical fields 229 func (l *LateInitializer) AddIgnoredCanonicalFields(cf string) { 230 if l.ignoredCanonicalFieldPaths == nil { 231 l.ignoredCanonicalFieldPaths = make([]string, 0) 232 } 233 l.ignoredCanonicalFieldPaths = append(l.ignoredCanonicalFieldPaths, cf) 234 } 235 236 // GetFieldPaths returns the fieldPaths map for Sensitive 237 func (s *Sensitive) GetFieldPaths() map[string]string { 238 return s.fieldPaths 239 } 240 241 // AddFieldPath adds the given tf path and xp path to the fieldPaths map. 242 func (s *Sensitive) AddFieldPath(tf, xp string) { 243 if s.fieldPaths == nil { 244 s.fieldPaths = make(map[string]string) 245 } 246 s.fieldPaths[tf] = xp 247 } 248 249 // OperationTimeouts allows configuring resource operation timeouts: 250 // https://www.terraform.io/language/resources/syntax#operation-timeouts 251 // Please note that, not all resources support configuring timeouts. 252 type OperationTimeouts struct { 253 Read time.Duration 254 Create time.Duration 255 Update time.Duration 256 Delete time.Duration 257 } 258 259 // NewInitializerFn returns the Initializer with a client. 260 type NewInitializerFn func(client client.Client) managed.Initializer 261 262 // TagInitializer returns a tagger to use default tag initializer. 263 var TagInitializer NewInitializerFn = func(client client.Client) managed.Initializer { 264 return NewTagger(client, "tags") 265 } 266 267 // Tagger implements the Initialize function to set external tags 268 type Tagger struct { 269 kube client.Client 270 fieldName string 271 } 272 273 // NewTagger returns a Tagger object. 274 func NewTagger(kube client.Client, fieldName string) *Tagger { 275 return &Tagger{kube: kube, fieldName: fieldName} 276 } 277 278 // Initialize is a custom initializer for setting external tags 279 func (t *Tagger) Initialize(ctx context.Context, mg xpresource.Managed) error { 280 if sets.New[xpv1.ManagementAction](mg.GetManagementPolicies()...).Equal(sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve)) { 281 // We don't want to add tags to the spec.forProvider if the resource is 282 // only being Observed. 283 return nil 284 } 285 paved, err := fieldpath.PaveObject(mg) 286 if err != nil { 287 return err 288 } 289 pavedByte, err := setExternalTagsWithPaved(xpresource.GetExternalTags(mg), paved, t.fieldName) 290 if err != nil { 291 return err 292 } 293 if err := json.Unmarshal(pavedByte, mg); err != nil { 294 return err 295 } 296 if err := t.kube.Update(ctx, mg); err != nil { 297 return err 298 } 299 return nil 300 } 301 302 func setExternalTagsWithPaved(externalTags map[string]string, paved *fieldpath.Paved, fieldName string) ([]byte, error) { 303 tags := map[string]*string{ 304 xpresource.ExternalResourceTagKeyKind: ptr.To(externalTags[xpresource.ExternalResourceTagKeyKind]), 305 xpresource.ExternalResourceTagKeyName: ptr.To(externalTags[xpresource.ExternalResourceTagKeyName]), 306 xpresource.ExternalResourceTagKeyProvider: ptr.To(externalTags[xpresource.ExternalResourceTagKeyProvider]), 307 } 308 309 if err := paved.SetValue(fmt.Sprintf("spec.forProvider.%s", fieldName), tags); err != nil { 310 return nil, err 311 } 312 pavedByte, err := paved.MarshalJSON() 313 if err != nil { 314 return nil, err 315 } 316 return pavedByte, nil 317 } 318 319 type InjectedKey struct { 320 Key string 321 DefaultValue string 322 } 323 324 // ListMapKeys is the list map keys when the server-side apply merge strategy 325 // islistType=map. 326 type ListMapKeys struct { 327 // InjectedKey can be used to inject the specified index key 328 // into the generated CRD schema for the list object when 329 // the SSA merge strategy for the parent list is `map`. 330 // If a non-zero `InjectedKey` is specified, then a field of type string with 331 // the specified name is injected into the Terraform schema and used as 332 // a list map key together with any other existing keys specified in `Keys`. 333 InjectedKey InjectedKey 334 // Keys is the set of list map keys to be used while SSA merges list items. 335 // If InjectedKey is non-zero, then it's automatically put into Keys and 336 // you must not specify the InjectedKey in Keys explicitly. 337 Keys []string 338 } 339 340 // ListMergeStrategy configures the corresponding field as list 341 // and configures its server-side apply merge strategy. 342 type ListMergeStrategy struct { 343 // ListMapKeys is the list map keys when the SSA merge strategy is 344 // `listType=map`. The keys specified here must be a set of scalar Terraform 345 // argument names to be used as the list map keys for the object list. 346 ListMapKeys ListMapKeys 347 // MergeStrategy is the SSA merge strategy for an object list. Valid values 348 // are: `atomic`, `set` and `map` 349 MergeStrategy ListType 350 } 351 352 // MergeStrategy configures the server-side apply merge strategy for the 353 // corresponding field. One and only one of the pointer members can be set 354 // and the specified merge strategy configuration must match the field's 355 // type, e.g., you cannot set MapMergeStrategy for a field of type list. 356 type MergeStrategy struct { 357 ListMergeStrategy ListMergeStrategy 358 MapMergeStrategy MapType 359 StructMergeStrategy StructType 360 } 361 362 // ServerSideApplyMergeStrategies configures the server-side apply merge strategy 363 // for the field at the specified path as the map key. The key is 364 // a Terraform configuration argument path such as a.b.c, without any 365 // index notation (i.e., array/map components do not need indices). 366 // It's an error to set a configuration option which does not match 367 // the object type at the specified path or to leave the corresponding 368 // configuration entry empty. For example, if the field at path a.b.c is 369 // a list, then ListMergeStrategy must be set and it should be the only 370 // configuration entry set. 371 type ServerSideApplyMergeStrategies map[string]MergeStrategy 372 373 // Resource is the set of information that you can override at different steps 374 // of the code generation pipeline. 375 type Resource struct { 376 // Name is the name of the resource type in Terraform, 377 // e.g. aws_rds_cluster. 378 Name string 379 380 // TerraformResource is the Terraform representation of the 381 // Terraform Plugin SDKv2 based resource. 382 TerraformResource *schema.Resource 383 384 // TerraformPluginFrameworkResource is the Terraform representation 385 // of the TF Plugin Framework based resource 386 TerraformPluginFrameworkResource fwresource.Resource 387 388 // ShortGroup is the short name of the API group of this CRD. The full 389 // CRD API group is calculated by adding the group suffix of the provider. 390 // For example, ShortGroup could be `ec2` where group suffix of the 391 // provider is `aws.crossplane.io` and in that case, the full group would 392 // be `ec2.aws.crossplane.io` 393 ShortGroup string 394 395 // Version is the version CRD will have. 396 Version string 397 398 // Kind is the kind of the CRD. 399 Kind string 400 401 // UseAsync should be enabled for resource whose creation and/or deletion 402 // takes more than 1 minute to complete such as Kubernetes clusters or 403 // databases. 404 UseAsync bool 405 406 // InitializerFns specifies the initializer functions to be used 407 // for this Resource. 408 InitializerFns []NewInitializerFn 409 410 // OperationTimeouts allows configuring resource operation timeouts. 411 OperationTimeouts OperationTimeouts 412 413 // ExternalName allows you to specify a custom ExternalName. 414 ExternalName ExternalName 415 416 // References keeps the configuration to build cross resource references 417 References References 418 419 // Sensitive keeps the configuration to handle sensitive information 420 Sensitive Sensitive 421 422 // LateInitializer configuration to control late-initialization behaviour 423 LateInitializer LateInitializer 424 425 // MetaResource is the metadata associated with the resource scraped from 426 // the Terraform registry. 427 MetaResource *registry.Resource 428 429 // Path is the resource path for the API server endpoint. It defaults to 430 // the plural name of the generated CRD. Overriding this sets both the 431 // path and the plural name for the generated CRD. 432 Path string 433 434 // SchemaElementOptions is a map from the schema element paths to 435 // SchemaElementOption for configuring options for schema elements. 436 SchemaElementOptions SchemaElementOptions 437 438 // TerraformConfigurationInjector allows a managed resource to inject 439 // configuration values in the Terraform configuration map obtained by 440 // deserializing its `spec.forProvider` value. Managed resources can 441 // use this resource configuration option to inject Terraform 442 // configuration parameters into their deserialized configuration maps, 443 // if the deserialization skips certain fields. 444 TerraformConfigurationInjector ConfigurationInjector 445 446 // TerraformCustomDiff allows a resource.Terraformed to customize how its 447 // Terraform InstanceDiff is computed during reconciliation. 448 TerraformCustomDiff CustomDiff 449 450 // ServerSideApplyMergeStrategies configures the server-side apply merge 451 // strategy for the fields at the given map keys. The map key is 452 // a Terraform configuration argument path such as a.b.c, without any 453 // index notation (i.e., array/map components do not need indices). 454 ServerSideApplyMergeStrategies ServerSideApplyMergeStrategies 455 456 Conversions []conversion.Conversion 457 458 // useTerraformPluginSDKClient indicates that a plugin SDK external client should 459 // be generated instead of the Terraform CLI-forking client. 460 useTerraformPluginSDKClient bool 461 462 // useTerraformPluginFrameworkClient indicates that a Terraform 463 // Plugin Framework external client should be generated instead of 464 // the Terraform Plugin SDKv2 client. 465 useTerraformPluginFrameworkClient bool 466 467 // OverrideFieldNames allows to manually override the relevant field name to 468 // avoid possible Go struct name conflicts that may occur after Multiversion 469 // CRDs support. During field generation, there may be fields with the same 470 // struct name calculated in the same group. For example, let X and Y 471 // resources in the same API group have a field named Tag. This field is an 472 // object type and the name calculated for the struct to be generated is 473 // TagParameters (for spec) for both resources. To avoid this conflict, upjet 474 // looks at all previously created structs in the package during generation 475 // and if there is a conflict, it puts the Kind name of the related resource 476 // in front of the next one: YTagParameters. 477 // With Multiversion CRDs support, the above conflict scenario cannot be 478 // solved in the generator when the old API group is preserved and not 479 // regenerated, because the generator does not know the object names in the 480 // old version. For example, a new API version is generated for resource X. In 481 // this case, no generation is done for the old version of X and when Y is 482 // generated, the generator is not aware of the TagParameters in X and 483 // generates TagParameters instead of YTagParameters. Thus, two object types 484 // with the same name are generated in the same package. This can be overcome 485 // by using this configuration API. 486 // The key of the map indicates the name of the field that is generated and 487 // causes the conflict, while the value indicates the name used to avoid the 488 // conflict. By convention, also used in upjet, the field name is preceded by 489 // the value of the generated Kind, for example: 490 // "TagParameters": "ClusterTagParameters" 491 OverrideFieldNames map[string]string 492 493 // requiredFields are the fields that will be marked as required in the 494 // generated CRD schema, although they are not required in the TF schema. 495 requiredFields []string 496 } 497 498 // RequiredFields returns slice of the marked as required fieldpaths. 499 func (r *Resource) RequiredFields() []string { 500 return r.requiredFields 501 } 502 503 // ShouldUseTerraformPluginSDKClient returns whether to generate an SDKv2-based 504 // external client for this Resource. 505 func (r *Resource) ShouldUseTerraformPluginSDKClient() bool { 506 return r.useTerraformPluginSDKClient 507 } 508 509 // ShouldUseTerraformPluginFrameworkClient returns whether to generate a 510 // Terraform Plugin Framework-based external client for this Resource. 511 func (r *Resource) ShouldUseTerraformPluginFrameworkClient() bool { 512 return r.useTerraformPluginFrameworkClient 513 } 514 515 // CustomDiff customizes the computed Terraform InstanceDiff. This can be used 516 // in cases where, for example, changes in a certain argument should just be 517 // dismissed. The new InstanceDiff is returned along with any errors. 518 type CustomDiff func(diff *terraform.InstanceDiff, state *terraform.InstanceState, config *terraform.ResourceConfig) (*terraform.InstanceDiff, error) 519 520 // ConfigurationInjector is a function that injects Terraform configuration 521 // values from the specified managed resource into the specified configuration 522 // map. jsonMap is the map obtained by converting the `spec.forProvider` using 523 // the JSON tags and tfMap is obtained by using the TF tags. 524 type ConfigurationInjector func(jsonMap map[string]any, tfMap map[string]any) error 525 526 // SchemaElementOptions represents schema element options for the 527 // schema elements of a Resource. 528 type SchemaElementOptions map[string]*SchemaElementOption 529 530 // SetAddToObservation sets the AddToObservation for the specified key. 531 func (m SchemaElementOptions) SetAddToObservation(el string) { 532 if m[el] == nil { 533 m[el] = &SchemaElementOption{} 534 } 535 m[el].AddToObservation = true 536 } 537 538 // AddToObservation returns true if the schema element at the specified path 539 // should be added to the CRD type's Observation type. 540 func (m SchemaElementOptions) AddToObservation(el string) bool { 541 return m[el] != nil && m[el].AddToObservation 542 } 543 544 // SetEmbeddedObject sets the EmbeddedObject for the specified key. 545 func (m SchemaElementOptions) SetEmbeddedObject(el string) { 546 if m[el] == nil { 547 m[el] = &SchemaElementOption{} 548 } 549 m[el].EmbeddedObject = true 550 } 551 552 // EmbeddedObject returns true if the schema element at the specified path 553 // should be generated as an embedded object. 554 func (m SchemaElementOptions) EmbeddedObject(el string) bool { 555 return m[el] != nil && m[el].EmbeddedObject 556 } 557 558 // SchemaElementOption represents configuration options on a schema element. 559 type SchemaElementOption struct { 560 // AddToObservation is set to true if the field represented by 561 // a schema element is to be added to the generated CRD type's 562 // Observation type. 563 AddToObservation bool 564 // EmbeddedObject is set to true if the field represented by 565 // a schema element is to be embedded into its parent instead of being 566 // generated as a single element list. 567 EmbeddedObject bool 568 }