github.com/crossplane/upjet@v1.3.0/pkg/transformers/resolver.go (about) 1 // SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package transformers 6 7 import ( 8 "fmt" 9 "go/ast" 10 "go/format" 11 "go/token" 12 "path/filepath" 13 "slices" 14 "strings" 15 16 "github.com/crossplane/crossplane-runtime/pkg/logging" 17 "github.com/pkg/errors" 18 "github.com/spf13/afero" 19 "golang.org/x/tools/go/ast/astutil" 20 "golang.org/x/tools/go/packages" 21 ) 22 23 const ( 24 varManagedResource = "m" 25 varManagedResourceList = "l" 26 commentFileTransformed = "// Code transformed by upjet. DO NOT EDIT." 27 28 defaultLoadMode = packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax 29 ) 30 31 // Resolver transforms the source resolver implementations so that 32 // the resolution source managed resources are no longer statically typed 33 // and thus, the implementations no longer need to import the corresponding 34 // API packages. This transformer is helpful in preventing the import cycles 35 // described in https://github.com/crossplane/upjet/issues/96 36 // and elsewhere. Please see TransformPackages for the details of the 37 // transformation applied. 38 type Resolver struct { 39 // the FS implementation used for storing the transformed output 40 fs afero.Fs 41 // the API group suffix to be used for the resolution source 42 // managed resources, such as aws.upbound.io. Then a sample 43 // API group for a resource is ec2.aws.upbound.io. 44 apiGroupSuffix string 45 // API group overrides for the provider. Certain providers need 46 // to rename the short API group names they use, breaking the 47 // convention that the short group name matches the package name. 48 // An example is upbound/provider-azure, where the ResourceGroup.azure 49 // resource's short API group is the empty string. This map allows such 50 // providers to control the names of the generated API group names by this 51 // Resolver transformer. 52 apiGroupOverrides map[string]string 53 // the API resolver package that contains the 54 // `GetManagedResource("group", "version", "kind", "listKind")` 55 // function. This function is used to initialize a managed resource and 56 // its list type, owned by the provider, with the given API group, version, 57 // kind and list kind. Signature of the resolver function is as follows: 58 // func GetManagedResource(group, version, kind, listKind string) (xpresource.Managed, xpresource.ManagedList, error) 59 apiResolverPackage string 60 // When set, any errors encountered while loading the source packages is 61 // silently ignored if a logger is not configured, 62 // or logged via the configured logger. 63 // We need to set this when, for example, loading resolver implementations 64 // with import cycles, or when transforming just one package and not loading 65 // the referenced typed. 66 ignorePackageLoadErrors bool 67 logger logging.Logger 68 config *packages.Config 69 } 70 71 // NewResolver initializes a new Resolver with the specified configuration. 72 func NewResolver(fs afero.Fs, apiGroupSuffix, apiResolverPackage string, ignorePackageLoadErrors bool, logger logging.Logger, opts ...ResolverOption) *Resolver { 73 if logger == nil { 74 logger = logging.NewNopLogger() 75 } 76 r := &Resolver{ 77 fs: fs, 78 apiGroupSuffix: apiGroupSuffix, 79 apiResolverPackage: apiResolverPackage, 80 ignorePackageLoadErrors: ignorePackageLoadErrors, 81 logger: logger, 82 config: &packages.Config{ 83 Mode: defaultLoadMode, 84 }, 85 } 86 for _, o := range opts { 87 o(r) 88 } 89 return r 90 } 91 92 // ResolverOption is an option used to configure the Resolver. 93 type ResolverOption func(resolver *Resolver) 94 95 // WithLoaderConfig configures the package loader config for a Resolver. 96 func WithLoaderConfig(c *packages.Config) ResolverOption { 97 return func(r *Resolver) { 98 r.config = c 99 } 100 } 101 102 // WithAPIGroupOverrides configures the API group overrides for a Resolver. 103 // Certain providers need to rename the short API group names they use, 104 // breaking the convention that the short group name matches the package name. 105 // An example is upbound/provider-azure, where the ResourceGroup.azure 106 // resource's short API group is the empty string. 107 func WithAPIGroupOverrides(overrides map[string]string) ResolverOption { 108 return func(r *Resolver) { 109 r.apiGroupOverrides = overrides 110 } 111 } 112 113 // TransformPackages applies the dynamic resolver transformation to 114 // the resolver modules loaded from the specified patterns and 115 // implemented in the specified resolver files. If `r.ignorePackageLoadErrors` 116 // is set, any errors encountered while loading the source packages are 117 // ignored. This may be required when the transformation source files have 118 // compile errors, such as import cycles. The transformed resolver 119 // implementations will use the specified API group suffix, such as, 120 // "aws.upbound.io" when determining the API groups of the resolution 121 // source managed resources. 122 // A sample transformation implemented by this transformer is from: 123 // ``` 124 // 125 // func (mg *Subnet) ResolveReferences(ctx context.Context, c client.Reader) error { 126 // r := reference.NewAPIResolver(c, mg) 127 // 128 // var rsp reference.ResolutionResponse 129 // var err error 130 // 131 // rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ 132 // CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.VPCID), 133 // Extract: reference.ExternalName(), 134 // Reference: mg.Spec.ForProvider.VPCIDRef, 135 // Selector: mg.Spec.ForProvider.VPCIDSelector, 136 // To: reference.To{ 137 // List: &VPCList{}, 138 // Managed: &VPC{}, 139 // }, 140 // }) 141 // if err != nil { 142 // return errors.Wrap(err, "mg.Spec.ForProvider.VPCID") 143 // } 144 // mg.Spec.ForProvider.VPCID = reference.ToPtrValue(rsp.ResolvedValue) 145 // mg.Spec.ForProvider.VPCIDRef = rsp.ResolvedReference 146 // 147 // ``` 148 // to the following: 149 // ``` 150 // 151 // func (mg *Subnet) ResolveReferences(ctx context.Context, c client.Reader) error { 152 // var m xpresource.Managed 153 // var l xpresource.ManagedList 154 // r := reference.NewAPIResolver(c, mg) 155 // 156 // var rsp reference.ResolutionResponse 157 // var err error 158 // { 159 // m, l, err = apisresolver.GetManagedResource("ec2.aws.upbound.io", "v1beta1", "VPC", "VPCList") 160 // if err != nil { 161 // return errors.Wrap(err, "failed to get the reference target managed resource and its list for reference resolution") 162 // } 163 // 164 // rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ 165 // CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.VPCID), 166 // Extract: reference.ExternalName(), 167 // Reference: mg.Spec.ForProvider.VPCIDRef, 168 // Selector: mg.Spec.ForProvider.VPCIDSelector, 169 // To: reference.To{List: l, Managed: m}, 170 // }) 171 // } 172 // if err != nil { 173 // return errors.Wrap(err, "mg.Spec.ForProvider.VPCID") 174 // } 175 // mg.Spec.ForProvider.VPCID = reference.ToPtrValue(rsp.ResolvedValue) 176 // mg.Spec.ForProvider.VPCIDRef = rsp.ResolvedReference 177 // 178 // ``` 179 func (r *Resolver) TransformPackages(resolverFilePattern string, patterns ...string) error { 180 pkgs, err := packages.Load(r.config, patterns...) 181 if err != nil { 182 return errors.Wrapf(err, "failed to load the packages using the patterns %q", strings.Join(patterns, ",")) 183 } 184 185 for _, p := range pkgs { 186 if err := toError(p); err != nil { 187 if !r.ignorePackageLoadErrors { 188 return errors.Wrapf(err, "failed to load the package %q", p.Name) 189 } 190 r.logger.Info("Encounter the following issues when loading a package", "package", p.Name, "issues", err.Error()) 191 } 192 for i, f := range p.GoFiles { 193 if filepath.Base(f) != resolverFilePattern { 194 continue 195 } 196 if err := r.transformResolverFile(p.Fset, p.Syntax[i], f, strings.Trim(r.apiGroupSuffix, ".")); err != nil { 197 return errors.Wrapf(err, "failed to transform the resolver file %s", f) 198 } 199 } 200 } 201 return nil 202 } 203 204 func toError(p *packages.Package) error { 205 if p == nil || len(p.Errors) == 0 { 206 return nil 207 } 208 sb := &strings.Builder{} 209 for _, e := range p.Errors { 210 if _, err := fmt.Fprintln(sb, e); err != nil { 211 return errors.Wrap(err, "failed to write the package parse error to the string builder") 212 } 213 } 214 return errors.New(sb.String()) 215 } 216 217 type importUsage struct { 218 path string 219 used bool 220 } 221 222 func addTransformedComment(fset *token.FileSet, node *ast.File) bool { 223 cMap := ast.NewCommentMap(fset, node, node.Comments) 224 cgl := cMap[node] 225 for _, cg := range cgl { 226 for _, c := range cg.List { 227 if c != nil && c.Text == commentFileTransformed { 228 return false 229 } 230 } 231 } 232 switch { 233 case len(cgl) == 0: 234 cgl = []*ast.CommentGroup{ 235 { 236 List: []*ast.Comment{ 237 { 238 Text: commentFileTransformed, 239 Slash: node.FileStart, 240 }, 241 }, 242 }, 243 } 244 245 default: 246 cgl[0].List = append(cgl[0].List, &ast.Comment{ 247 Text: commentFileTransformed, 248 Slash: cgl[0].End(), 249 }) 250 } 251 cMap[node] = cgl 252 return true 253 } 254 255 func (r *Resolver) transformResolverFile(fset *token.FileSet, node *ast.File, filePath, apiGroupSuffix string) error { //nolint:gocyclo // Arguably, easier to follow 256 if !addTransformedComment(fset, node) { 257 return nil 258 } 259 // add resolution source variable declarations to the `ResolveReferences` 260 // function bodies. 261 importMap := addMRVariableDeclarations(node) 262 // Map to track imports used in reference.To structs 263 importsUsed := make(map[string]importUsage) 264 // assign is the assignment statement that assigns the values returned from 265 // `APIResolver.Resolve` or `APIResolver.ResolveMultiple` to the local 266 // variables in the MR kind's `ResolveReferences` function. 267 var assign *ast.AssignStmt 268 // block is the MR kind's `ResolveReferences` function's body block. 269 // We use this to find the correct place to inject MR variable 270 // declarations, calls to the type registry and error checks, etc. 271 var block *ast.BlockStmt 272 // these are the GVKs for the MR kind and the associated list kind 273 var group, version, kind, listKind string 274 275 // traverse the AST loaded from the given source file to remove the 276 // cross API-group import statements from it. This helps in avoiding 277 // the import cycles related to the cross-resource references. 278 var inspectErr error 279 ast.Inspect(node, func(n ast.Node) bool { 280 switch x := n.(type) { 281 // this transformer takes care of removing the unneeded import statements 282 // (after the transformation), which are the target cross API-group 283 // references we are trying to avoid to prevent import cycles and appear 284 // in cross-resource reference targets. 285 case *ast.ImportSpec: 286 // initially, mark all imports as needed 287 key := "" 288 if x.Name != nil { 289 key = x.Name.Name 290 } else { 291 key = x.Path.Value 292 } 293 importsUsed[key] = importUsage{ 294 path: strings.Trim(x.Path.Value, `"`), 295 used: true, 296 } 297 298 // keep a hold of the `ResolveReferences` function body so that we can 299 // properly inject variable declarations, error checks, etc. into the 300 // correct positions. 301 case *ast.FuncDecl: 302 block = x.Body 303 304 // keep a hold of the `APIResolver.Resolve` and 305 // `APIResolver.ResolveMultiple` return value assignments as we will 306 // inject code right above it. 307 case *ast.AssignStmt: 308 assign = x 309 310 // we will attempt to transform expressions such as 311 // `reference.To{List: &v1beta1.MRList{}, Managed: &v1beta1.MR{}}` 312 // into: 313 // `reference.To{List: l, Managed: m}`, where 314 // l and m are local variables holding the correctly types MR kind 315 // and MR list kind objects as the reference targets. 316 // Such expressions are the primary sources of cross API-group 317 // import statements. 318 // Cross API-group extractors are rare, and they should be 319 // handled when they're being added, this transformer does not 320 // consider them. 321 case *ast.KeyValueExpr: 322 // check if the key is "To" and the value is a CompositeLit 323 if key, ok := x.Key.(*ast.Ident); ok && key.Name == "To" { 324 // prevent a previous GVK from being reused 325 group, version, kind, listKind = "", "", "", "" 326 if cl, ok := x.Value.(*ast.CompositeLit); ok { 327 // check if there are any package qualifiers in the CompositeLit 328 for _, elt := range cl.Elts { 329 if kv, ok := elt.(*ast.KeyValueExpr); ok { 330 if uexpr, ok := kv.Value.(*ast.UnaryExpr); ok { 331 if cl, ok := uexpr.X.(*ast.CompositeLit); ok { 332 // then the reference target resides in another API group 333 // and the composite literal is a selector expression such as 334 // v1beta1.MR. In this case, we deduce the GV of the MR and 335 // list using the selector expression and the corresponding 336 // import statements (with the name as the expression). 337 // Kind and list kind are determined from the field selector. 338 key := kv.Key.(*ast.Ident).Name 339 if sexpr, ok := cl.Type.(*ast.SelectorExpr); ok { 340 if ident, ok := sexpr.X.(*ast.Ident); ok { 341 path := importsUsed[ident.Name].path 342 importsUsed[ident.Name] = importUsage{ 343 path: path, 344 used: false, 345 } 346 // we will parse the import path such as: 347 // github.com/upbound/provider-aws/apis/ec2/v1beta1 348 // and extract the GV information from it. 349 tokens := strings.Split(path, "/") 350 // e.g., v1beta1 351 version = tokens[len(tokens)-1] 352 // e.g., ec2.aws.upbound.io 353 group = fmt.Sprintf("%s.%s", tokens[len(tokens)-2], apiGroupSuffix) 354 // apply any configured group name overrides 355 group = r.overrideGroupName(group) 356 // extract the kind and list kind names from the field 357 // selector. 358 if sexpr.Sel != nil { 359 if key == "List" { 360 listKind = sexpr.Sel.Name 361 } else { 362 kind = sexpr.Sel.Name 363 } 364 } 365 } 366 } else { 367 // then the reference target is in the same package as the 368 // source. We still transform it for uniformity and 369 // in the future, the source and target might still be 370 // moved to different packages. 371 // The GV information comes from file name in this case: 372 // apis/cur/v1beta1/zz_generated.resolvers.go 373 tokens := strings.Split(filePath, "/") 374 // e.g., v1beta1 375 version = tokens[len(tokens)-2] 376 // e.g., cur.aws.upbound.io 377 group = fmt.Sprintf("%s.%s", tokens[len(tokens)-3], apiGroupSuffix) 378 // apply any configured group name overrides 379 group = r.overrideGroupName(group) 380 if ident, ok := cl.Type.(*ast.Ident); ok { 381 if key == "List" { 382 listKind = ident.Name 383 } else { 384 kind = ident.Name 385 } 386 } 387 } 388 } 389 } 390 } 391 } 392 393 // we will error if we could not determine the reference target GVKs 394 // for the MR and its list kind. 395 if group == "" || version == "" || kind == "" || listKind == "" { 396 inspectErr = errors.Errorf("failed to extract the GVKs for the reference targets. Group: %q, Version: %q, Kind: %q, List Kind: %q", group, version, kind, listKind) 397 return false 398 } 399 400 // replace the value with a CompositeLit of type reference.To 401 // It's transformed into: 402 // reference.To{List: l, Managed: m} 403 x.Value = &ast.CompositeLit{ 404 Type: &ast.SelectorExpr{ 405 X: ast.NewIdent("reference"), 406 Sel: ast.NewIdent("To"), 407 }, 408 // here, l & m 409 Elts: []ast.Expr{ 410 &ast.KeyValueExpr{ 411 Key: ast.NewIdent("List"), 412 Value: ast.NewIdent(varManagedResourceList), 413 }, 414 &ast.KeyValueExpr{ 415 Key: ast.NewIdent("Managed"), 416 Value: ast.NewIdent(varManagedResource), 417 }, 418 }, 419 } 420 421 // get the statements including the import statements we need to make 422 // calls to the type registry. 423 mrImports, stmts := getManagedResourceStatements(group, version, kind, listKind, r.apiResolverPackage) 424 // insert the statements that implement type registry lookups 425 if !insertStatements(stmts, block, assign) { 426 inspectErr = errors.Errorf("failed to insert the type registry lookup statements for Group: %q, Version: %q, Kind: %q, List Kind: %q", group, version, kind, listKind) 427 return false 428 } 429 // add the new import statements we need to implement the 430 // type registry lookups. 431 for k, v := range mrImports { 432 importMap[k] = v 433 } 434 } 435 } 436 } 437 return true 438 }) 439 440 if inspectErr != nil { 441 return errors.Wrap(inspectErr, "failed to inspect the resolver file for transformation") 442 } 443 444 // remove the imports that are no longer used. 445 for _, decl := range node.Decls { 446 if gd, ok := decl.(*ast.GenDecl); ok && gd.Tok == token.IMPORT { 447 var newSpecs []ast.Spec 448 for _, spec := range gd.Specs { 449 if imp, ok := spec.(*ast.ImportSpec); ok { 450 var name string 451 if imp.Name != nil { 452 name = imp.Name.Name 453 } else { 454 name = imp.Path.Value 455 } 456 if usage, exists := importsUsed[name]; !exists || usage.used { 457 newSpecs = append(newSpecs, spec) 458 } 459 } 460 } 461 gd.Specs = newSpecs 462 463 newImportKeys := make([]string, 0, len(importMap)) 464 for k := range importMap { 465 newImportKeys = append(newImportKeys, k) 466 } 467 slices.Sort(newImportKeys) 468 469 for _, path := range newImportKeys { 470 gd.Specs = append(gd.Specs, &ast.ImportSpec{ 471 Name: &ast.Ident{ 472 Name: importMap[path], 473 }, 474 Path: &ast.BasicLit{ 475 Kind: token.STRING, 476 Value: path, 477 }, 478 }) 479 } 480 } 481 } 482 return r.dumpTransformed(fset, node, filePath) 483 } 484 485 func (r *Resolver) dumpTransformed(fset *token.FileSet, node *ast.File, filePath string) error { 486 // dump the transformed resolver file 487 adjustFunctionDocs(node) 488 outFile, err := r.fs.Create(filepath.Clean(filePath)) 489 if err != nil { 490 return errors.Wrap(err, "failed to open the resolver file for writing the transformed AST") 491 } 492 defer func() { _ = outFile.Close() }() 493 494 // write the modified AST back to the resolver file 495 return errors.Wrap(format.Node(outFile, fset, node), "failed to dump the transformed AST back into the resolver file") 496 } 497 498 func adjustFunctionDocs(node *ast.File) { 499 node.Decls[1].(*ast.FuncDecl).Doc.List[0].Slash = node.Decls[1].(*ast.FuncDecl).Name.Pos() 500 } 501 502 func insertStatements(stmts []ast.Stmt, block *ast.BlockStmt, assign *ast.AssignStmt) bool { 503 astutil.Apply(block, nil, func(c *astutil.Cursor) bool { 504 n := c.Node() 505 if n != assign { 506 return true 507 } 508 c.Replace(&ast.BlockStmt{ 509 List: append(stmts, assign), 510 }) 511 return false 512 }) 513 return true 514 } 515 516 func addMRVariableDeclarations(f *ast.File) map[string]string { 517 // prepare the first variable declaration: 518 // `var m xpresource.Managed` 519 varDecl1 := &ast.GenDecl{ 520 Tok: token.VAR, 521 Specs: []ast.Spec{ 522 &ast.ValueSpec{ 523 Names: []*ast.Ident{ast.NewIdent("m")}, 524 Type: &ast.SelectorExpr{ 525 X: ast.NewIdent("xpresource"), 526 Sel: ast.NewIdent("Managed"), 527 }, 528 }, 529 }, 530 } 531 532 // prepare the second variable declaration: 533 // `var l xpresource.ManagedList` 534 varDecl2 := &ast.GenDecl{ 535 Tok: token.VAR, 536 Specs: []ast.Spec{ 537 &ast.ValueSpec{ 538 Names: []*ast.Ident{ast.NewIdent("l")}, 539 Type: &ast.SelectorExpr{ 540 X: ast.NewIdent("xpresource"), 541 Sel: ast.NewIdent("ManagedList"), 542 }, 543 }, 544 }, 545 } 546 547 ast.Inspect(f, func(n ast.Node) bool { 548 fn, ok := n.(*ast.FuncDecl) 549 if !ok { 550 return true 551 } 552 553 if fn.Name.Name == "ResolveReferences" && len(fn.Recv.List) > 0 { 554 fn.Body.List = append([]ast.Stmt{ 555 &ast.DeclStmt{Decl: varDecl1}, 556 &ast.DeclStmt{Decl: varDecl2}, 557 }, fn.Body.List...) 558 } 559 return true 560 }) 561 return map[string]string{ 562 `"github.com/crossplane/crossplane-runtime/pkg/resource"`: "xpresource", 563 } 564 } 565 566 func getManagedResourceStatements(group, version, kind, listKind, apiResolverPackage string) (map[string]string, []ast.Stmt) { 567 // prepare the assignment statement: 568 // `m, l, err = apisresolver.GetManagedResource("group", "version", "kind", "listKind")` 569 assignStmt := &ast.AssignStmt{ 570 Lhs: []ast.Expr{ 571 ast.NewIdent("m"), 572 ast.NewIdent("l"), 573 ast.NewIdent("err"), 574 }, 575 Tok: token.ASSIGN, 576 Rhs: []ast.Expr{ 577 &ast.CallExpr{ 578 Fun: &ast.SelectorExpr{ 579 X: ast.NewIdent("apisresolver"), 580 Sel: ast.NewIdent("GetManagedResource"), 581 }, 582 Args: []ast.Expr{ 583 &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf(`"%s"`, group)}, 584 &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf(`"%s"`, version)}, 585 &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf(`"%s"`, kind)}, 586 &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf(`"%s"`, listKind)}, 587 }, 588 }, 589 }, 590 } 591 592 // prepare the if statement: 593 // ``` 594 // if err != nil { 595 // return errors.Wrap(err, "failed to get the reference target managed resource and its list for reference resolution") 596 // } 597 // ``` 598 ifStmt := &ast.IfStmt{ 599 Cond: &ast.BinaryExpr{ 600 X: ast.NewIdent("err"), 601 Op: token.NEQ, 602 Y: &ast.Ident{Name: "nil"}, 603 }, 604 Body: &ast.BlockStmt{ 605 List: []ast.Stmt{ 606 &ast.ReturnStmt{ 607 Results: []ast.Expr{ 608 &ast.CallExpr{ 609 Fun: &ast.SelectorExpr{ 610 X: ast.NewIdent("errors"), 611 Sel: ast.NewIdent("Wrap"), 612 }, 613 Args: []ast.Expr{ 614 ast.NewIdent("err"), 615 &ast.BasicLit{Kind: token.STRING, Value: `"failed to get the reference target managed resource and its list for reference resolution"`}, 616 }, 617 }, 618 }, 619 }, 620 }, 621 }, 622 } 623 return map[string]string{ 624 // TODO: we may need to parameterize the import alias in the future, if 625 // any provider that uses the transformer has an import alias collision 626 // which is not very likely. 627 fmt.Sprintf(`"%s"`, apiResolverPackage): "apisresolver", 628 }, []ast.Stmt{assignStmt, ifStmt} 629 } 630 631 func (r *Resolver) overrideGroupName(group string) string { 632 g, ok := r.apiGroupOverrides[group] 633 // we need to allow overrides with an empty string 634 if !ok { 635 return group 636 } 637 return g 638 }