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 }