github.com/crossplane/upjet@v1.3.0/pkg/registry/reference/resolver.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package reference 6 7 import ( 8 "fmt" 9 "regexp" 10 "strconv" 11 "strings" 12 13 "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 14 "github.com/pkg/errors" 15 16 "github.com/crossplane/upjet/pkg/config" 17 "github.com/crossplane/upjet/pkg/registry" 18 "github.com/crossplane/upjet/pkg/resource/json" 19 ) 20 21 const ( 22 // Wildcard denotes a wildcard resource name 23 Wildcard = "*" 24 ) 25 26 var ( 27 // ReRef represents a regular expression for Terraform resource references 28 // in scraped HCL example manifests. 29 ReRef = regexp.MustCompile(`\${(.+)}`) 30 ) 31 32 // Parts represents the components (resource name, example name & 33 // attribute name) parsed from an HCL reference. 34 type Parts struct { 35 Resource string 36 ExampleName string 37 Attribute string 38 } 39 40 // MatchRefParts parses a Parts from an HCL reference string 41 func MatchRefParts(ref string) *Parts { 42 g := ReRef.FindStringSubmatch(ref) 43 if len(g) != 2 { 44 return nil 45 } 46 return getRefParts(g[1]) 47 } 48 49 func getRefParts(ref string) *Parts { 50 parts := strings.Split(ref, ".") 51 // expected reference format is <resource type>.<resource name>.<field name> 52 if len(parts) < 3 { 53 return nil 54 } 55 return &Parts{ 56 Resource: parts[0], 57 ExampleName: parts[1], 58 Attribute: strings.Join(parts[2:], "."), 59 } 60 } 61 62 // GetResourceName returns the resource name or the wildcard 63 // for this Parts. 64 func (parts Parts) GetResourceName(wildcardName bool) string { 65 name := parts.ExampleName 66 if wildcardName || len(name) == 0 { 67 name = Wildcard 68 } 69 return fmt.Sprintf("%s.%s", parts.Resource, name) 70 } 71 72 // NewRefParts initializes a new Parts from the specified 73 // resource and example names. 74 func NewRefParts(resource, exampleName string) Parts { 75 return Parts{ 76 Resource: resource, 77 ExampleName: exampleName, 78 } 79 } 80 81 // NewRefPartsFromResourceName initializes a new Parts 82 // from the specified <resource name>.<example name> 83 // string. 84 func NewRefPartsFromResourceName(rn string) Parts { 85 parts := strings.Split(rn, ".") 86 return Parts{ 87 Resource: parts[0], 88 ExampleName: parts[1], 89 } 90 } 91 92 // PavedWithManifest represents an example manifest with a fieldpath.Paved 93 type PavedWithManifest struct { 94 Paved *fieldpath.Paved 95 ManifestPath string 96 ParamsPrefix []string 97 refsResolved bool 98 Config *config.Resource 99 Group string 100 Version string 101 ExampleName string 102 } 103 104 // ResolutionContext represents a reference resolution context where 105 // wildcard or named references are used. 106 type ResolutionContext struct { 107 WildcardNames bool 108 Context map[string]*PavedWithManifest 109 } 110 111 func paveExampleManifest(m string) (*PavedWithManifest, error) { 112 var exampleParams map[string]any 113 if err := json.TFParser.Unmarshal([]byte(m), &exampleParams); err != nil { 114 return nil, errors.Wrapf(err, "cannot unmarshal example manifest: %s", m) 115 } 116 return &PavedWithManifest{ 117 Paved: fieldpath.Pave(exampleParams), 118 }, nil 119 } 120 121 // ResolveReferencesOfPaved resolves references of a PavedWithManifest 122 // in the given resolution context. 123 func (rr *Injector) ResolveReferencesOfPaved(pm *PavedWithManifest, resolutionContext *ResolutionContext) error { 124 if pm.refsResolved { 125 return nil 126 } 127 pm.refsResolved = true 128 return errors.Wrap(rr.resolveReferences(pm.Paved.UnstructuredContent(), resolutionContext), "failed to resolve references of paved") 129 } 130 131 func (rr *Injector) resolveReferences(params map[string]any, resolutionContext *ResolutionContext) error { //nolint:gocyclo 132 for paramName, paramValue := range params { 133 switch t := paramValue.(type) { 134 case map[string]any: 135 if err := rr.resolveReferences(t, resolutionContext); err != nil { 136 return err 137 } 138 139 case []any: 140 for _, e := range t { 141 eM, ok := e.(map[string]any) 142 if !ok { 143 continue 144 } 145 if err := rr.resolveReferences(eM, resolutionContext); err != nil { 146 return err 147 } 148 } 149 150 case string: 151 parts := MatchRefParts(t) 152 if parts == nil { 153 continue 154 } 155 pm := resolutionContext.Context[parts.GetResourceName(resolutionContext.WildcardNames)] 156 if pm == nil || pm.Paved == nil { 157 continue 158 } 159 if err := rr.ResolveReferencesOfPaved(pm, resolutionContext); err != nil { 160 return errors.Wrapf(err, "cannot recursively resolve references for %q", parts.Resource) 161 } 162 pathStr := strings.Join(append(pm.ParamsPrefix, parts.Attribute), ".") 163 s, err := pm.Paved.GetString(convertTFPathToFieldPath(pathStr)) 164 if fieldpath.IsNotFound(err) { 165 continue 166 } 167 if err != nil { 168 return errors.Wrapf(err, "cannot get string value from paved: %s", pathStr) 169 } 170 params[paramName] = s 171 } 172 } 173 return nil 174 } 175 176 func convertTFPathToFieldPath(path string) string { 177 segments := strings.Split(path, ".") 178 result := make([]string, 0, len(segments)) 179 for i, p := range segments { 180 d, err := strconv.Atoi(p) 181 switch { 182 case err != nil: 183 result = append(result, p) 184 185 case i > 0: 186 result[i-1] = fmt.Sprintf("%s[%d]", result[i-1], d) 187 } 188 } 189 return strings.Join(result, ".") 190 } 191 192 // PrepareLocalResolutionContext returns a ResolutionContext that can be used 193 // for resolving references between a target resource and its dependencies 194 // that are exemplified together with the resource in Terraform registry. 195 func PrepareLocalResolutionContext(exampleMeta registry.ResourceExample, rootName string) (*ResolutionContext, error) { 196 context := make(map[string]*PavedWithManifest, len(exampleMeta.Dependencies)+1) 197 var err error 198 for rn, m := range exampleMeta.Dependencies { 199 // <Terraform resource>.<example name> 200 context[rn], err = paveExampleManifest(m) 201 if err != nil { 202 return nil, errors.Wrapf(err, "cannot pave example manifest for resource: %s", rn) 203 } 204 } 205 context[rootName], err = paveExampleManifest(exampleMeta.Manifest) 206 if err != nil { 207 return nil, errors.Wrapf(err, "cannot pave example manifest for resource: %s", rootName) 208 } 209 return &ResolutionContext{ 210 WildcardNames: false, 211 Context: context, 212 }, nil 213 }