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  }