github.com/crossplane/upjet@v1.3.0/pkg/config/externalname.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  	"bytes"
     9  	"context"
    10  	"regexp"
    11  	"strings"
    12  	"text/template"
    13  	"text/template/parse"
    14  
    15  	"github.com/crossplane/crossplane-runtime/pkg/errors"
    16  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    17  )
    18  
    19  const (
    20  	errIDNotFoundInTFState = "id does not exist in tfstate"
    21  )
    22  
    23  var (
    24  	externalNameRegex = regexp.MustCompile(`{{\ *\.external_name\b\ *}}`)
    25  )
    26  
    27  var (
    28  	// NameAsIdentifier uses "name" field in the arguments as the identifier of
    29  	// the resource.
    30  	NameAsIdentifier = ExternalName{
    31  		SetIdentifierArgumentFn: func(base map[string]any, name string) {
    32  			base["name"] = name
    33  		},
    34  		GetExternalNameFn: IDAsExternalName,
    35  		GetIDFn:           ExternalNameAsID,
    36  		OmittedFields: []string{
    37  			"name",
    38  			"name_prefix",
    39  		},
    40  	}
    41  
    42  	// IdentifierFromProvider is used in resources whose identifier is assigned by
    43  	// the remote client, such as AWS VPC where it gets an identifier like
    44  	// vpc-2213das instead of letting user choose a name.
    45  	IdentifierFromProvider = ExternalName{
    46  		SetIdentifierArgumentFn: NopSetIdentifierArgument,
    47  		GetExternalNameFn:       IDAsExternalName,
    48  		GetIDFn:                 ExternalNameAsID,
    49  		DisableNameInitializer:  true,
    50  	}
    51  
    52  	parameterPattern = regexp.MustCompile(`{{\s*\.parameters\.([^\s}]+)\s*}}`)
    53  )
    54  
    55  // ParameterAsIdentifier uses the given field name in the arguments as the
    56  // identifier of the resource.
    57  func ParameterAsIdentifier(param string) ExternalName {
    58  	e := NameAsIdentifier
    59  	e.SetIdentifierArgumentFn = func(base map[string]any, name string) {
    60  		base[param] = name
    61  	}
    62  	e.OmittedFields = []string{
    63  		param,
    64  		param + "_prefix",
    65  	}
    66  	e.IdentifierFields = []string{param}
    67  	return e
    68  }
    69  
    70  // TemplatedStringAsIdentifier accepts a template as the shape of the Terraform
    71  // ID and lets you provide a field path for the argument you're using as external
    72  // name. The available variables you can use in the template are as follows:
    73  //
    74  // parameters: A tree of parameters that you'd normally see in a Terraform HCL
    75  // file. You can use TF registry documentation of given resource to
    76  // see what's available.
    77  //
    78  // setup.configuration: The Terraform configuration object of the provider. You can
    79  // take a look at the TF registry provider configuration object
    80  // to see what's available. Not to be confused with ProviderConfig
    81  // custom resource of the Crossplane provider.
    82  //
    83  // setup.client_metadata: The Terraform client metadata available for the provider,
    84  // such as the AWS account ID for the AWS provider.
    85  //
    86  // external_name: The value of external name annotation of the custom resource.
    87  // It is required to use this as part of the template.
    88  //
    89  // The following template functions are available:
    90  //
    91  // ToLower: Converts the contents of the pipeline to lower-case
    92  //
    93  // ToUpper: Converts the contents of the pipeline to upper-case
    94  //
    95  // Please note that it's currently *not* possible to use
    96  // the template functions on the .external_name template variable.
    97  // Example usages:
    98  //
    99  // TemplatedStringAsIdentifier("index_name", "/subscriptions/{{ .setup.configuration.subscription }}/{{ .external_name }}")
   100  //
   101  // TemplatedStringAsIdentifier("index_name", "/resource/{{ .external_name }}/static")
   102  //
   103  // TemplatedStringAsIdentifier("index_name", "{{ .parameters.cluster_id }}:{{ .parameters.node_id }}:{{ .external_name }}")
   104  //
   105  // TemplatedStringAsIdentifier("", "arn:aws:network-firewall:{{ .setup.configuration.region }}:{{ .setup.client_metadata.account_id }}:{{ .parameters.type | ToLower }}-rulegroup/{{ .external_name }}")
   106  func TemplatedStringAsIdentifier(nameFieldPath, tmpl string) ExternalName {
   107  	t, err := template.New("getid").Funcs(template.FuncMap{
   108  		"ToLower": strings.ToLower,
   109  		"ToUpper": strings.ToUpper,
   110  	}).Parse(tmpl)
   111  	if err != nil {
   112  		panic(errors.Wrap(err, "cannot parse template"))
   113  	}
   114  
   115  	// Note(turkenh): If a parameter is used in the external name template,
   116  	// it is an identifier field.
   117  	var identifierFields []string
   118  	for _, node := range t.Root.Nodes {
   119  		if node.Type() == parse.NodeAction {
   120  			match := parameterPattern.FindStringSubmatch(node.String())
   121  			if len(match) == 2 {
   122  				identifierFields = append(identifierFields, match[1])
   123  			}
   124  		}
   125  	}
   126  	return ExternalName{
   127  		SetIdentifierArgumentFn: func(base map[string]any, externalName string) {
   128  			if nameFieldPath == "" {
   129  				return
   130  			}
   131  			// TODO(muvaf): Return error in this function? Not returning error
   132  			// is a valid option since the schemas are static so we'd get the
   133  			// panic right when you create a resource. It's not generation-time
   134  			// error though.
   135  			if err := fieldpath.Pave(base).SetString(nameFieldPath, externalName); err != nil {
   136  				panic(errors.Wrapf(err, "cannot set %s to fieldpath %s", externalName, nameFieldPath))
   137  			}
   138  		},
   139  		OmittedFields: []string{
   140  			nameFieldPath,
   141  			nameFieldPath + "_prefix",
   142  		},
   143  		GetIDFn: func(ctx context.Context, externalName string, parameters map[string]any, setup map[string]any) (string, error) {
   144  			o := map[string]any{
   145  				"external_name": externalName,
   146  				"parameters":    parameters,
   147  				"setup":         setup,
   148  			}
   149  			b := bytes.Buffer{}
   150  			if err := t.Execute(&b, o); err != nil {
   151  				return "", errors.Wrap(err, "cannot execute template")
   152  			}
   153  			return b.String(), nil
   154  		},
   155  		GetExternalNameFn: func(tfstate map[string]any) (string, error) {
   156  			id, ok := tfstate["id"]
   157  			if !ok {
   158  				return "", errors.New(errIDNotFoundInTFState)
   159  			}
   160  			return GetExternalNameFromTemplated(tmpl, id.(string))
   161  		},
   162  		IdentifierFields: identifierFields,
   163  	}
   164  }
   165  
   166  // GetExternalNameFromTemplated takes a Terraform ID and the template it's produced
   167  // from and reverse it to get the external name. For example, you can supply
   168  // "/subscription/{{ .paramters.some }}/{{ .external_name }}" with
   169  // "/subscription/someval/myname" and get "myname" returned.
   170  func GetExternalNameFromTemplated(tmpl, val string) (string, error) { //nolint:gocyclo
   171  	// gocyclo: I couldn't find any more room.
   172  	loc := externalNameRegex.FindStringIndex(tmpl)
   173  	// A template without external name usage.
   174  	if loc == nil {
   175  		return val, nil
   176  	}
   177  	leftIndex := loc[0]
   178  	rightIndex := loc[1]
   179  
   180  	leftSeparator := ""
   181  	if leftIndex > 0 {
   182  		leftSeparator = string(tmpl[leftIndex-1])
   183  	}
   184  	rightSeparator := ""
   185  	if rightIndex < len(tmpl) {
   186  		rightSeparator = string(tmpl[rightIndex])
   187  	}
   188  
   189  	switch {
   190  	// {{ .external_name }}
   191  	case leftSeparator == "" && rightSeparator == "":
   192  		return val, nil
   193  	// {{ .external_name }}/someother
   194  	case leftSeparator == "" && rightSeparator != "":
   195  		return strings.Split(val, rightSeparator)[0], nil
   196  	// /another/{{ .external_name }}/someother
   197  	case leftSeparator != "" && rightSeparator != "":
   198  		leftSeparatorCount := strings.Count(tmpl[:leftIndex+1], leftSeparator)
   199  		// ["", "another","myname/someother"]
   200  		separatedLeft := strings.SplitAfterN(val, leftSeparator, leftSeparatorCount+1)
   201  		// myname/someother
   202  		rightString := separatedLeft[len(separatedLeft)-1]
   203  		// myname
   204  		return strings.Split(rightString, rightSeparator)[0], nil
   205  	// /another/{{ .external_name }}
   206  	case leftSeparator != "" && rightSeparator == "":
   207  		separated := strings.Split(val, leftSeparator)
   208  		return separated[len(separated)-1], nil
   209  	}
   210  	return "", errors.Errorf("unhandled case with template %s and value %s", tmpl, val)
   211  }
   212  
   213  // ExternalNameFrom is an ExternalName configuration which uses a parent
   214  // configuration as its base and modifies any of the GetIDFn,
   215  // GetExternalNameFn or SetIdentifierArgumentsFn. This enables us to reuse
   216  // the existing ExternalName configurations with modifications in their
   217  // behaviors via compositions.
   218  type ExternalNameFrom struct {
   219  	ExternalName
   220  	getIDFn                 func(GetIDFn, context.Context, string, map[string]any, map[string]any) (string, error)
   221  	getExternalNameFn       func(GetExternalNameFn, map[string]any) (string, error)
   222  	setIdentifierArgumentFn func(SetIdentifierArgumentsFn, map[string]any, string)
   223  }
   224  
   225  // ExternalNameFromOption is an option that modifies the behavior of an
   226  // ExternalNameFrom external-name configuration.
   227  type ExternalNameFromOption func(from *ExternalNameFrom)
   228  
   229  // WithGetIDFn sets the GetIDFn for the ExternalNameFrom configuration.
   230  // The function parameter fn receives the parent ExternalName's GetIDFn, and
   231  // implementations may invoke the parent's GetIDFn via this
   232  // parameter. For the description of the rest of the parameters and return
   233  // values, please see the documentation of GetIDFn.
   234  func WithGetIDFn(fn func(fn GetIDFn, ctx context.Context, externalName string, parameters map[string]any, terraformProviderConfig map[string]any) (string, error)) ExternalNameFromOption {
   235  	return func(ec *ExternalNameFrom) {
   236  		ec.getIDFn = fn
   237  	}
   238  }
   239  
   240  // WithGetExternalNameFn sets the GetExternalNameFn for the ExternalNameFrom
   241  // configuration. The function parameter fn receives the parent ExternalName's
   242  // GetExternalNameFn, and implementations may invoke the parent's
   243  // GetExternalNameFn via this parameter. For the description of the rest
   244  // of the parameters and return values, please see the documentation of
   245  // GetExternalNameFn.
   246  func WithGetExternalNameFn(fn func(fn GetExternalNameFn, tfstate map[string]any) (string, error)) ExternalNameFromOption {
   247  	return func(ec *ExternalNameFrom) {
   248  		ec.getExternalNameFn = fn
   249  	}
   250  }
   251  
   252  // WithSetIdentifierArgumentsFn sets the SetIdentifierArgumentsFn for the
   253  // ExternalNameFrom configuration. The function parameter fn receives the
   254  // parent ExternalName's SetIdentifierArgumentsFn, and implementations may
   255  // invoke the parent's SetIdentifierArgumentsFn via this
   256  // parameter. For the description of the rest of the parameters and return
   257  // values, please see the documentation of SetIdentifierArgumentsFn.
   258  func WithSetIdentifierArgumentsFn(fn func(fn SetIdentifierArgumentsFn, base map[string]any, externalName string)) ExternalNameFromOption {
   259  	return func(ec *ExternalNameFrom) {
   260  		ec.setIdentifierArgumentFn = fn
   261  	}
   262  }
   263  
   264  // NewExternalNameFrom initializes a new ExternalNameFrom with the given parent
   265  // and with the given options. An example configuration that uses a
   266  // TemplatedStringAsIdentifier as its parent (base) and sets a default value
   267  // for the external-name if the external-name is yet not populated is as
   268  // follows:
   269  //
   270  // config.NewExternalNameFrom(config.TemplatedStringAsIdentifier("", "{{ .parameters.type }}/{{ .setup.client_metadata.account_id }}/{{ .external_name }}"),
   271  //
   272  //	config.WithGetIDFn(func(fn config.GetIDFn, ctx context.Context, externalName string, parameters map[string]any, terraformProviderConfig map[string]any) (string, error) {
   273  //		if externalName == "" {
   274  //			externalName = "some random string"
   275  //		}
   276  //		return fn(ctx, externalName, parameters, terraformProviderConfig)
   277  //	}))
   278  func NewExternalNameFrom(parent ExternalName, opts ...ExternalNameFromOption) ExternalName {
   279  	ec := &ExternalNameFrom{}
   280  	for _, o := range opts {
   281  		o(ec)
   282  	}
   283  
   284  	ec.ExternalName.GetIDFn = func(ctx context.Context, externalName string, parameters map[string]any, terraformProviderConfig map[string]any) (string, error) {
   285  		if ec.getIDFn == nil {
   286  			return parent.GetIDFn(ctx, externalName, parameters, terraformProviderConfig)
   287  		}
   288  		return ec.getIDFn(parent.GetIDFn, ctx, externalName, parameters, terraformProviderConfig)
   289  	}
   290  	ec.ExternalName.GetExternalNameFn = func(tfstate map[string]any) (string, error) {
   291  		if ec.getExternalNameFn == nil {
   292  			return parent.GetExternalNameFn(tfstate)
   293  		}
   294  		return ec.getExternalNameFn(parent.GetExternalNameFn, tfstate)
   295  	}
   296  	ec.ExternalName.SetIdentifierArgumentFn = func(base map[string]any, externalName string) {
   297  		if ec.setIdentifierArgumentFn == nil {
   298  			parent.SetIdentifierArgumentFn(base, externalName)
   299  			return
   300  		}
   301  		ec.setIdentifierArgumentFn(parent.SetIdentifierArgumentFn, base, externalName)
   302  	}
   303  	return ec.ExternalName
   304  }