github.com/crossplane/upjet@v1.3.0/pkg/types/builder.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package types 6 7 import ( 8 "fmt" 9 "go/token" 10 "go/types" 11 "sort" 12 "strings" 13 14 "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 15 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 16 twtypes "github.com/muvaf/typewriter/pkg/types" 17 "github.com/pkg/errors" 18 "k8s.io/utils/ptr" 19 20 "github.com/crossplane/upjet/pkg/config" 21 ) 22 23 const ( 24 wildcard = "*" 25 26 emptyStruct = "struct{}" 27 28 // ref: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules 29 celEscapeSequence = "__%s__" 30 // description for an injected list map key field in the context of the 31 // server-side apply object list merging 32 descriptionInjectedKey = "This is an injected field with a default value for being able to merge items of the parent object list." 33 ) 34 35 var ( 36 // ref: https://github.com/google/cel-spec/blob/v0.6.0/doc/langdef.md#syntax 37 celReservedKeywords = []string{"true", "false", "null", "in", "as", "break", "const", "continue", 38 "else", "for", "function", "if", "import", "let", "loop", "package", "namespace", "return", "var", 39 "void", "while"} 40 ) 41 42 // Generated is a struct that holds generated types 43 type Generated struct { 44 Types []*types.Named 45 Comments twtypes.Comments 46 47 ForProviderType *types.Named 48 InitProviderType *types.Named 49 AtProviderType *types.Named 50 51 ValidationRules string 52 } 53 54 // Builder is used to generate Go type equivalence of given Terraform schema. 55 type Builder struct { 56 Package *types.Package 57 58 genTypes []*types.Named 59 comments twtypes.Comments 60 validationRules string 61 } 62 63 // NewBuilder returns a new Builder. 64 func NewBuilder(pkg *types.Package) *Builder { 65 return &Builder{ 66 Package: pkg, 67 comments: twtypes.Comments{}, 68 } 69 } 70 71 // Build returns parameters and observation types built out of Terraform schema. 72 func (g *Builder) Build(cfg *config.Resource) (Generated, error) { 73 if err := injectServerSideApplyListMergeKeys(cfg); err != nil { 74 return Generated{}, errors.Wrapf(err, "cannot inject server-side apply merge keys for resource %q", cfg.Name) 75 } 76 77 fp, ap, ip, err := g.buildResource(cfg.TerraformResource, cfg, nil, nil, false, cfg.Kind) 78 return Generated{ 79 Types: g.genTypes, 80 Comments: g.comments, 81 ForProviderType: fp, 82 InitProviderType: ip, 83 AtProviderType: ap, 84 ValidationRules: g.validationRules, 85 }, errors.Wrapf(err, "cannot build the Types for resource %q", cfg.Name) 86 } 87 88 func injectServerSideApplyListMergeKeys(cfg *config.Resource) error { //nolint:gocyclo // Easier to follow the logic in a single function 89 for f, s := range cfg.ServerSideApplyMergeStrategies { 90 if s.ListMergeStrategy.MergeStrategy != config.ListTypeMap { 91 continue 92 } 93 if s.ListMergeStrategy.ListMapKeys.InjectedKey.Key == "" && len(s.ListMergeStrategy.ListMapKeys.Keys) == 0 { 94 return errors.Errorf("list map keys configuration for the object list %q is empty", f) 95 } 96 if s.ListMergeStrategy.ListMapKeys.InjectedKey.Key == "" { 97 continue 98 } 99 sch := config.GetSchema(cfg.TerraformResource, f) 100 if sch == nil { 101 return errors.Errorf("cannot find the Terraform schema for the argument at the path %q", f) 102 } 103 if sch.Type != schema.TypeList && sch.Type != schema.TypeSet { 104 return errors.Errorf("fieldpath %q is not a Terraform list or set", f) 105 } 106 el, ok := sch.Elem.(*schema.Resource) 107 if !ok { 108 return errors.Errorf("fieldpath %q is a Terraform list or set but its element type is not a Terraform *schema.Resource", f) 109 } 110 for k := range el.Schema { 111 if k == s.ListMergeStrategy.ListMapKeys.InjectedKey.Key { 112 return errors.Errorf("element schema for the object list %q already contains the argument key %q", f, k) 113 } 114 } 115 el.Schema[s.ListMergeStrategy.ListMapKeys.InjectedKey.Key] = &schema.Schema{ 116 Type: schema.TypeString, 117 Required: true, 118 Description: descriptionInjectedKey, 119 } 120 if s.ListMergeStrategy.ListMapKeys.InjectedKey.DefaultValue != "" { 121 el.Schema[s.ListMergeStrategy.ListMapKeys.InjectedKey.Key].Default = s.ListMergeStrategy.ListMapKeys.InjectedKey.DefaultValue 122 } 123 } 124 return nil 125 } 126 127 func (g *Builder) buildResource(res *schema.Resource, cfg *config.Resource, tfPath []string, xpPath []string, asBlocksMode bool, names ...string) (*types.Named, *types.Named, *types.Named, error) { //nolint:gocyclo 128 // NOTE(muvaf): There can be fields in the same CRD with same name but in 129 // different types. Since we generate the type using the field name, there 130 // can be collisions. In order to be able to generate unique names consistently, 131 // we need to process all fields in the same order all the time. 132 keys := sortedKeys(res.Schema) 133 134 typeNames, err := NewTypeNames(names, g.Package, cfg.OverrideFieldNames) 135 if err != nil { 136 return nil, nil, nil, err 137 } 138 139 r := &resource{} 140 for _, snakeFieldName := range keys { 141 var reference *config.Reference 142 cPath := fieldPath(append(tfPath, snakeFieldName)) 143 ref, ok := cfg.References[cPath] 144 // if a reference is configured and the field does not belong to status 145 if ok && !IsObservation(res.Schema[snakeFieldName]) { 146 reference = &ref 147 } 148 149 var f *Field 150 switch { 151 case res.Schema[snakeFieldName].Sensitive: 152 var drop bool 153 f, drop, err = NewSensitiveField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode) 154 if err != nil { 155 return nil, nil, nil, err 156 } 157 if drop { 158 continue 159 } 160 case reference != nil: 161 f, err = NewReferenceField(g, cfg, r, res.Schema[snakeFieldName], reference, snakeFieldName, tfPath, xpPath, names, asBlocksMode) 162 if err != nil { 163 return nil, nil, nil, err 164 } 165 default: 166 f, err = NewField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode) 167 if err != nil { 168 return nil, nil, nil, err 169 } 170 } 171 f.AddToResource(g, r, typeNames, cfg.SchemaElementOptions.AddToObservation(cPath)) 172 } 173 174 paramType, obsType, initType := g.AddToBuilder(typeNames, r) 175 return paramType, obsType, initType, nil 176 } 177 178 // AddToBuilder adds fields to the Builder. 179 func (g *Builder) AddToBuilder(typeNames *TypeNames, r *resource) (*types.Named, *types.Named, *types.Named) { 180 // NOTE(muvaf): Not every struct has both computed and configurable fields, 181 // so some types we generate here are empty and unnecessary. However, 182 // there are valid types with zero fields and we don't have the information 183 // to differentiate between valid zero fields and unnecessary one. So we generate 184 // two structs for every complex type. 185 // See usage of wafv2EmptySchema() in aws_wafv2_web_acl here: 186 // https://github.com/hashicorp/terraform-provider-aws/blob/main/aws/wafv2_helper.go#L13 187 paramType := types.NewNamed(typeNames.ParameterTypeName, types.NewStruct(r.paramFields, r.paramTags), nil) 188 g.genTypes = append(g.genTypes, paramType) 189 190 initType := types.NewNamed(typeNames.InitTypeName, types.NewStruct(r.initFields, r.initTags), nil) 191 g.genTypes = append(g.genTypes, initType) 192 193 obsType := types.NewNamed(typeNames.ObservationTypeName, types.NewStruct(r.obsFields, r.obsTags), nil) 194 g.genTypes = append(g.genTypes, obsType) 195 196 for _, p := range r.topLevelRequiredParams { 197 g.validationRules += "\n" 198 sp := sanitizePath(p.path) 199 if p.includeInit { 200 g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s) || (has(self.initProvider) && has(self.initProvider.%s))",message="spec.forProvider.%s is a required parameter"`, sp, sp, p.path) 201 } else { 202 g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s)",message="spec.forProvider.%s is a required parameter"`, sp, p.path) 203 } 204 } 205 206 return paramType, obsType, initType 207 } 208 209 func (g *Builder) buildSchema(f *Field, cfg *config.Resource, names []string, cpath string, r *resource) (types.Type, types.Type, error) { //nolint:gocyclo 210 switch f.Schema.Type { 211 case schema.TypeBool: 212 return types.NewPointer(types.Universe.Lookup("bool").Type()), nil, nil 213 case schema.TypeFloat: 214 return types.NewPointer(types.Universe.Lookup("float64").Type()), nil, nil 215 case schema.TypeInt: 216 return types.NewPointer(types.Universe.Lookup("int64").Type()), nil, nil 217 case schema.TypeString: 218 return types.NewPointer(types.Universe.Lookup("string").Type()), nil, nil 219 case schema.TypeMap, schema.TypeList, schema.TypeSet: 220 names = append(names, f.Name.Camel) 221 if f.Schema.Type != schema.TypeMap { 222 // We don't want to have a many-to-many relationship in case of a Map, since we use SecretReference as 223 // the type of XP field. In this case, we want to have a one-to-many relationship which is handled at 224 // runtime in the controller. 225 f.TerraformPaths = append(f.TerraformPaths, wildcard) 226 f.CRDPaths = append(f.CRDPaths, wildcard) 227 } 228 var elemType types.Type 229 var initElemType types.Type 230 switch et := f.Schema.Elem.(type) { 231 case schema.ValueType: 232 switch et { 233 case schema.TypeBool: 234 elemType = types.Universe.Lookup("bool").Type() 235 case schema.TypeFloat: 236 elemType = types.Universe.Lookup("float64").Type() 237 case schema.TypeInt: 238 elemType = types.Universe.Lookup("int64").Type() 239 case schema.TypeString: 240 elemType = types.Universe.Lookup("string").Type() 241 case schema.TypeMap, schema.TypeList, schema.TypeSet, schema.TypeInvalid: 242 return nil, nil, errors.Errorf("element type of %s is basic but not one of known basic types", fieldPath(names)) 243 } 244 initElemType = elemType 245 case *schema.Schema: 246 newf, err := NewField(g, cfg, r, et, f.Name.Snake, f.TerraformPaths, f.CRDPaths, names, false) 247 if err != nil { 248 return nil, nil, err 249 } 250 elemType = newf.FieldType 251 initElemType = elemType 252 case *schema.Resource: 253 var asBlocksMode bool 254 // TODO(muvaf): We skip the other type once we choose one of param 255 // or obs types. This might cause some fields to be completely omitted. 256 if f.Schema.ConfigMode == schema.SchemaConfigModeAttr { 257 asBlocksMode = true 258 } 259 paramType, obsType, initType, err := g.buildResource(et, cfg, f.TerraformPaths, f.CRDPaths, asBlocksMode, names...) 260 if err != nil { 261 return nil, nil, errors.Wrapf(err, "cannot infer type from resource schema of element type of %s", fieldPath(names)) 262 } 263 initElemType = initType 264 265 switch { 266 case IsObservation(f.Schema): 267 if obsType == nil { 268 return nil, nil, errors.Errorf("element type of %s is computed but the underlying schema does not return observation type", fieldPath(names)) 269 } 270 elemType = obsType 271 // There are some types that are computed and not optional (observation field) but also has nested fields 272 // that can go under spec. This check prevents the elimination of fields in parameter type, by checking 273 // whether the schema in observation type has nested parameter (spec) fields. 274 if paramType.Underlying().String() != emptyStruct { 275 var tParam, tInit types.Type 276 if cfg.SchemaElementOptions.EmbeddedObject(cpath) { 277 tParam = types.NewPointer(paramType) 278 tInit = types.NewPointer(initType) 279 } else { 280 tParam = types.NewSlice(paramType) 281 tInit = types.NewSlice(initType) 282 } 283 r.addParameterField(f, types.NewField(token.NoPos, g.Package, f.Name.Camel, tParam, false)) 284 r.addInitField(f, types.NewField(token.NoPos, g.Package, f.Name.Camel, tInit, false), g, nil) 285 } 286 default: 287 if paramType == nil { 288 return nil, nil, errors.Errorf("element type of %s is configurable but the underlying schema does not return a parameter type", fieldPath(names)) 289 } 290 elemType = paramType 291 // There are some types that are parameter field but also has nested fields that can go under status. 292 // This check prevents the elimination of fields in observation type, by checking whether the schema in 293 // parameter type has nested observation (status) fields. 294 if obsType.Underlying().String() != emptyStruct { 295 var t types.Type 296 if cfg.SchemaElementOptions.EmbeddedObject(cpath) { 297 t = types.NewPointer(obsType) 298 } else { 299 t = types.NewSlice(obsType) 300 } 301 field := types.NewField(token.NoPos, g.Package, f.Name.Camel, t, false) 302 r.addObservationField(f, field) 303 } 304 } 305 // if unset 306 // see: https://github.com/crossplane/upjet/issues/177 307 case nil: 308 elemType = types.Universe.Lookup("string").Type() 309 initElemType = elemType 310 default: 311 return nil, nil, errors.Errorf("element type of %s should be either schema.Resource or schema.Schema", fieldPath(names)) 312 } 313 314 // if the singleton list is to be replaced by an embedded object 315 if cfg.SchemaElementOptions.EmbeddedObject(cpath) { 316 return types.NewPointer(elemType), types.NewPointer(initElemType), nil 317 } 318 // NOTE(muvaf): Maps and slices are already pointers, so we don't need to 319 // wrap them even if they are optional. 320 if f.Schema.Type == schema.TypeMap { 321 return types.NewMap(types.Universe.Lookup("string").Type(), elemType), types.NewMap(types.Universe.Lookup("string").Type(), initElemType), nil 322 } 323 return types.NewSlice(elemType), types.NewSlice(initElemType), nil 324 case schema.TypeInvalid: 325 return nil, nil, errors.Errorf("invalid schema type %s", f.Schema.Type.String()) 326 default: 327 return nil, nil, errors.Errorf("unexpected schema type %s", f.Schema.Type.String()) 328 } 329 } 330 331 // TypeNames represents the parameter and observation name of the resource. 332 type TypeNames struct { 333 ParameterTypeName *types.TypeName 334 InitTypeName *types.TypeName 335 ObservationTypeName *types.TypeName 336 } 337 338 // NewTypeNames returns a new TypeNames object. 339 func NewTypeNames(fieldPaths []string, pkg *types.Package, overrideFieldNames map[string]string) (*TypeNames, error) { 340 paramTypeName, err := generateTypeName("Parameters", pkg, overrideFieldNames, fieldPaths...) 341 if err != nil { 342 return nil, errors.Wrapf(err, "cannot generate parameters type name of %s", fieldPath(fieldPaths)) 343 } 344 paramName := types.NewTypeName(token.NoPos, pkg, paramTypeName, nil) 345 346 initTypeName, err := generateTypeName("InitParameters", pkg, overrideFieldNames, fieldPaths...) 347 if err != nil { 348 return nil, errors.Wrapf(err, "cannot generate init parameters type name of %s", fieldPath(fieldPaths)) 349 } 350 initName := types.NewTypeName(token.NoPos, pkg, initTypeName, nil) 351 352 obsTypeName, err := generateTypeName("Observation", pkg, overrideFieldNames, fieldPaths...) 353 if err != nil { 354 return nil, errors.Wrapf(err, "cannot generate observation type name of %s", fieldPath(fieldPaths)) 355 } 356 obsName := types.NewTypeName(token.NoPos, pkg, obsTypeName, nil) 357 358 // We insert them to the package scope so that the type name calculations in 359 // recursive calls are checked against their upper level type's name as well. 360 pkg.Scope().Insert(paramName) 361 pkg.Scope().Insert(initName) 362 pkg.Scope().Insert(obsName) 363 364 return &TypeNames{ParameterTypeName: paramName, InitTypeName: initName, ObservationTypeName: obsName}, nil 365 } 366 367 type resource struct { 368 paramFields, initFields, obsFields []*types.Var 369 paramTags, initTags, obsTags []string 370 topLevelRequiredParams []*topLevelRequiredParam 371 } 372 373 type topLevelRequiredParam struct { 374 path string 375 includeInit bool 376 } 377 378 func newTopLevelRequiredParam(path string, includeInit bool) *topLevelRequiredParam { 379 return &topLevelRequiredParam{path: path, includeInit: includeInit} 380 } 381 382 func (r *resource) addParameterField(f *Field, field *types.Var) { 383 requiredBySchema := !f.Schema.Optional || f.Required 384 // Note(turkenh): We are collecting the top level required parameters that 385 // are not identifier fields. This is for generating CEL validation rules for 386 // those parameters and not to require them if the management policy is set 387 // Observe Only. In other words, if we are not creating or managing the 388 // resource, we don't need to provide those parameters which are: 389 // - requiredBySchema => required 390 // - !f.Identifier => not identifiers - i.e. region, zone, etc. 391 // - len(f.CanonicalPaths) == 1 => top level, i.e. not a nested field 392 // TODO (lsviben): We should add CEL rules for all required fields, 393 // not just the top level ones, due to having all forProvider 394 // fields now optional. CEL rules should check if a field is 395 // present either in forProvider or initProvider. 396 // https://github.com/crossplane/upjet/issues/239 397 if requiredBySchema && !f.Identifier && len(f.CanonicalPaths) == 1 { 398 requiredBySchema = false 399 // If the field is not a terraform field, we should not require it in init, 400 // as it is not an initProvider field. 401 r.topLevelRequiredParams = append(r.topLevelRequiredParams, newTopLevelRequiredParam(f.TransformedName, f.TFTag != "-")) 402 } 403 404 // Note(lsviben): Only fields which are not also initProvider fields should have a required kubebuilder comment. 405 f.Comment.Required = ptr.To(requiredBySchema && !f.isInit()) 406 407 // For removing omitempty tag from json tag, we are just checking if the field is required by the schema. 408 if requiredBySchema { 409 // Required fields should not have omitempty tag in json tag. 410 // TODO(muvaf): This overrides user intent if they provided custom 411 // JSON tag. 412 r.paramTags = append(r.paramTags, fmt.Sprintf(`json:"%s" tf:"%s"`, strings.TrimSuffix(f.JSONTag, ",omitempty"), f.TFTag)) 413 } else { 414 r.paramTags = append(r.paramTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag)) 415 } 416 417 r.paramFields = append(r.paramFields, field) 418 } 419 420 func (r *resource) addInitField(f *Field, field *types.Var, g *Builder, typeNames *types.TypeName) { 421 // If the field is not an init field, we don't add it. 422 if !f.isInit() { 423 return 424 } 425 426 r.initTags = append(r.initTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag)) 427 428 // If the field is a nested type, we need to add it as the init type. 429 if f.InitType != nil { 430 field = types.NewField(token.NoPos, g.Package, f.Name.Camel, f.InitType, false) 431 } 432 433 r.initFields = append(r.initFields, field) 434 435 if f.Reference != nil { 436 r.addReferenceFields(g, typeNames, f, true) 437 } 438 } 439 440 func (r *resource) addObservationField(f *Field, field *types.Var) { 441 for _, obsF := range r.obsFields { 442 if obsF.Name() == field.Name() { 443 // If the field is already added, we don't add it again. 444 // Some nested types could have been previously added as an 445 // observation type while building their schema: https://github.com/crossplane/upjet/blob/b89baca4ae24c8fbd8eb403c353ca18916093e5e/pkg/types/builder.go#L206 446 return 447 } 448 } 449 r.obsFields = append(r.obsFields, field) 450 r.obsTags = append(r.obsTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag)) 451 } 452 453 func (r *resource) addReferenceFields(g *Builder, paramName *types.TypeName, field *Field, isInit bool) { 454 refFields, refTags := g.generateReferenceFields(paramName, field) 455 if isInit { 456 r.initTags = append(r.initTags, refTags...) 457 r.initFields = append(r.initFields, refFields...) 458 } else { 459 r.paramTags = append(r.paramTags, refTags...) 460 r.paramFields = append(r.paramFields, refFields...) 461 } 462 } 463 464 // generateTypeName generates a unique name for the type if its original name 465 // is used by another one. It adds the former field names recursively until it 466 // finds a unique name. 467 func generateTypeName(suffix string, pkg *types.Package, overrideFieldNames map[string]string, names ...string) (calculated string, _ error) { 468 defer func() { 469 if v, ok := overrideFieldNames[calculated]; ok { 470 calculated = v 471 } 472 }() 473 n := names[len(names)-1] + suffix 474 for i := len(names) - 2; i >= 0; i-- { 475 if pkg.Scope().Lookup(n) == nil { 476 calculated = n 477 return 478 } 479 n = names[i] + n 480 } 481 if pkg.Scope().Lookup(n) == nil { 482 calculated = n 483 return 484 } 485 // start from 2 considering the 1st of this type is the one without an 486 // index. 487 for i := 2; i < 10; i++ { 488 nn := fmt.Sprintf("%s_%d", n, i) 489 if pkg.Scope().Lookup(nn) == nil { 490 calculated = nn 491 return 492 } 493 } 494 return "", errors.Errorf("could not generate a unique name for %s", n) 495 } 496 497 // IsObservation returns whether the specified Schema belongs to an observed 498 // attribute, i.e., whether it's a required computed field. 499 func IsObservation(s *schema.Schema) bool { 500 // NOTE(muvaf): If a field is not optional but computed, then it's 501 // definitely an observation field. 502 // If it's optional but also computed, then it means the field has a server 503 // side default but user can change it, so it needs to go to parameters. 504 return s.Computed && !s.Optional 505 } 506 507 func sortedKeys(m map[string]*schema.Schema) []string { 508 if len(m) == 0 { 509 return nil 510 } 511 keys := make([]string, len(m)) 512 i := 0 513 for k := range m { 514 keys[i] = k 515 i++ 516 } 517 sort.Strings(keys) 518 return keys 519 } 520 521 func fieldPath(parts []string) string { 522 seg := make(fieldpath.Segments, len(parts)) 523 for i, p := range parts { 524 if p == wildcard { 525 continue 526 } 527 seg[i] = fieldpath.Field(p) 528 } 529 return seg.String() 530 } 531 532 func fieldPathWithWildcard(parts []string) string { 533 seg := make(fieldpath.Segments, len(parts)) 534 for i, p := range parts { 535 seg[i] = fieldpath.Field(p) 536 } 537 return seg.String() 538 } 539 540 func sanitizePath(p string) string { 541 for _, reserved := range celReservedKeywords { 542 if p == reserved { 543 return fmt.Sprintf(celEscapeSequence, p) 544 } 545 } 546 return p 547 }