diff --git a/docs/configuring-a-resource.md b/docs/configuring-a-resource.md index 6b53189d..5fd95a2d 100644 --- a/docs/configuring-a-resource.md +++ b/docs/configuring-a-resource.md @@ -22,126 +22,239 @@ cloud provider; sometimes it could simply be the name of resource (e.g. VPC id ). This is something specific to resource, and we need some input configuration for terrajet to appropriately generate a resource. -Since Terraform already needs the same identifier to import a resource, most +Since Terraform already needs a similar [identifier to import a resource], most helpful part of resource documentation is the [import section]. -This is [the struct that holds the External Name configuration]: +Terrajet performs some back and forth conversions between Crossplane resource +model and Terraform configuration. We need a custom, per resource configuration +to adapt Crossplane `external name` and Terraform `id`. + +![external name configuration](images/terrajet-externalname.png) + +Here are [the types for the External Name configuration]: ```go +// SetIdentifierArgumentsFn sets the name of the resource in Terraform attributes map, +// i.e. Main HCL file. +type SetIdentifierArgumentsFn func(base map[string]interface{}, externalName string) +// GetExternalNameFn returns the external name extracted from the TF State. +type GetExternalNameFn func(tfstate map[string]interface{}) (string, error) +// GetIDFn returns the ID to be used in TF State file, i.e. "id" field in +// terraform.tfstate. +type GetIDFn func(ctx context.Context, externalName string, parameters map[string]interface{}, providerConfig map[string]interface{}) (string, error) + // ExternalName contains all information that is necessary for naming operations, // such as removal of those fields from spec schema and calling Configure function // to fill attributes with information given in external name. type ExternalName struct { - // SetIdentifierArgumentFn sets the name of the resource in Terraform argument - // map. - SetIdentifierArgumentFn SetIdentifierArgumentFn - - // OmittedFields are the ones you'd like to be removed from the schema since - // they are specified via external name. You can omit only the top level fields. - // No field is omitted by default. - OmittedFields []string - - // DisableNameInitializer allows you to specify whether the name initializer - // that sets external name to metadata.name if none specified should be disabled. - // It needs to be disabled for resources whose external name includes information - // more than the actual name of the resource, like subscription ID or region - // etc. which is unlikely to be included in metadata.name - DisableNameInitializer bool + // SetIdentifierArgumentFn sets the name of the resource in Terraform argument + // map. In many cases, there is a field called "name" in the HCL schema, however, + // there are cases like RDS DB Cluster where the name field in HCL is called + // "cluster_identifier". This function is the place that you can take external + // name and assign it to that specific key for that resource type. + SetIdentifierArgumentFn SetIdentifierArgumentsFn + + // GetExternalNameFn returns the external name extracted from TF State. In most cases, + // "id" field contains all the information you need. You'll need to extract + // the format that is decided for external name annotation to use. + // For example the following is an Azure resource ID: + // /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1 + // The function should return "mygroup1" so that it can be used to set external + // name if it was not set already. + GetExternalNameFn GetExternalNameFn + + // GetIDFn returns the string that will be used as "id" key in TF state. In + // many cases, external name format is the same as "id" but when it is not + // we may need information from other places to construct it. For example, + // the following is an Azure resource ID: + // /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1 + // The function here should use information from supplied arguments to + // construct this ID, i.e. "mygroup1" from external name, subscription ID + // from providerConfig, and others from parameters map if needed. + GetIDFn GetIDFn + + // OmittedFields are the ones you'd like to be removed from the schema since + // they are specified via external name. For example, if you set + // "cluster_identifier" in SetIdentifierArgumentFn, then you need to omit + // that field. + // You can omit only the top level fields. + // No field is omitted by default. + OmittedFields []string + + // DisableNameInitializer allows you to specify whether the name initializer + // that sets external name to metadata.name if none specified should be disabled. + // It needs to be disabled for resources whose external identifier is randomly + // assigned by the provider, like AWS VPC where it gets vpc-21kn123 identifier + // and not let you name it. + DisableNameInitializer bool } ``` Comments explain the purpose of each field but let's clarify further with some -examples. +example cases. -Checking the [import section of aws_vpc], we see that this resource is being -imported with `vpc id`. When we check the [arguments list] and provided -[example usages], it is clear that this **id** is not something that user -provides, rather generated by AWS API. Hence, we need to disable name -initializer, which simply sets the external-name annotation to `metadata.name` -of the resource. +#### Case 1: Name as External Name and Terraform ID + +This is the simplest and most straightforward case with the following +conditions: + +- Terraform resource uses the `name` argument to identify the resources +- Terraform resource can be imported with `name`, i.e. `id`=`name` + +[aws_iam_user] is a good example here. In this case, we can just use the +[NameAsIdentifier] config of Terrajet as follows: ```go -DisableNameInitializer: true +import ( + "github.com/crossplane-contrib/terrajet/pkg/config" + ... +) + +... + p.AddResourceConfigurator("aws_iam_user", func(r *config.Resource) { + r.ExternalName = config.NameAsIdentifier + ... + } ``` -Since we have no related fields in the [arguments list] that could be used to -build the external-name, we don't need to omit any fields (`OmittedFields`) or -need to use external name to set some arguments (`SetIdentifierArgumentFn`). -Hence, we end up the following external name configuration for `aws_vpc` -resource: +There are some resources which fits into this case with an exception by +expecting an argument other than `name` to name/identify a resource, for +example, [bucket] for [aws_s3_bucket] and [cluster_identifier] for +[aws_rds_cluster]. + +Let's check [aws_s3_bucket] further. Reading the [import section of s3 bucket] +we see that bucket is imported with its **name**, however, checking _arguments_ +section we see that this name is provided with the [bucket] argument. We also +notice, there is also another argument as `bucket_prefix` which conflicts with +`bucket` argument. We can just use the [NameAsIdentifier] config, however, we +also need to configure the `bucket` argument with `SetIdentifierArgumentFn` and +also omit `bucket` and `bucket_prefix` arguments from the spec with +`OmittedFields`: ```go -func Configure(p *config.Provider) { - p.AddResourceConfigurator("aws_vpc", func (r *config.Resource) { - r.ExternalName = config.ExternalName{ - // Set to true explicitly since the value is calculated by AWS. - DisableNameInitializer: true, - } - }) -} +import ( + "github.com/crossplane-contrib/terrajet/pkg/config" + ... +) + +... + p.AddResourceConfigurator("aws_s3_bucket", func(r *config.Resource) { + r.ExternalName = config.NameAsIdentifier + r.ExternalName.SetIdentifierArgumentFn = func(base map[string]interface{}, name string) { + base["bucket"] = name + }, + r.ExternalName.OmittedFields: []string{ + "bucket", + "bucket_prefix", + }, + ... + } ``` -And for this specific case, where Provider assigns identifier of the resource -independent of resource specification, Terrajet has a default external name -configuration that is [IdentifierFromProvider] which we can simply use here -doing the same as above: +#### Case 2: Identifier from Provider + +In this case, the (cloud) provider generates an identifier for the resource +independent of what we provided as arguments. + +Checking the [import section of aws_vpc], we see that this resource is being +imported with `vpc id`. When we check the [arguments list] and provided +[example usages], it is clear that this **id** is **not** something that user +provides, rather generated by AWS API. + +Here, we can just use [IdentifierFromProvider] configuration: ```go -func Configure(p *config.Provider) { - p.AddResourceConfigurator("aws_vpc", func (r *config.Resource) { +import ( + "github.com/crossplane-contrib/terrajet/pkg/config" + ... +) + +... + p.AddResourceConfigurator("aws_vpc", func(r *config.Resource) { r.ExternalName = config.IdentifierFromProvider - }) -} + ... + } ``` -Let's check another resource, [aws_s3_bucket] which requires some other -configuration. Reading the [import section of s3 bucket] we see that bucket is -imported with its **name** which is provided with the [bucket] argument. -We can just use the CR name as the bucket name, and we don't have to disable -name initializer as we did above. - -However, since we are using metadata name as `bucket` argument, we need the -following two: +#### Case 3: Terraform ID as a Formatted String -- Fill `bucket` attribute using external-name annotation, so that Terraform - knows the value we want to provide: +For some resources, Terraform uses a formatted string as `id` which include +resource identifier that Crossplane uses as external name but may also contain +some other parameters. - ```go - SetIdentifierArgumentFn: func(base map[string]interface{}, name string) { - base["bucket"] = name - }, - ``` +Most `azurerm` resources fall into this category. Checking the +[import section of azurerm_sql_server], we see that can be imported with an `id` +in the following format: -- Omit `bucket` and `bucket_prefix` from the crd spec, so that we don't have - multiple inputs for the same thing (name of the bucket): - ```go - OmittedFields: []string{ - "bucket", - "bucket_prefix", - }, - ``` +``` +/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/servers/myserver +``` -Hence, we end up the following external name configuration for `aws_s3_bucket` -resource: +To properly set external name for such a resource, we need to configure how to +extract external name from this string (`GetExternalNameFn`) and how to build +this id back (`GetIDFn`). ```go -func Configure(p *config.Provider) { - p.AddResourceConfigurator("aws_s3_bucket", func(r *config.Resource) { - r.ExternalName = config.ExternalName{ - SetIdentifierArgumentFn: func(base map[string]interface{}, name string) { - base["bucket"] = name - }, - OmittedFields: []string{ - "bucket", - "bucket_prefix", - }, - } - }) +import ( + "github.com/crossplane-contrib/terrajet/pkg/config" + ... +) + +func getNameFromFullyQualifiedID(tfstate map[string]interface{}) (string, error) { + id, ok := tfstate["id"] + if !ok { + return "", errors.Errorf(ErrFmtNoAttribute, "id") + } + idStr, ok := id.(string) + if !ok { + return "", errors.Errorf(ErrFmtUnexpectedType, "id") + } + words := strings.Split(idStr, "/") + return words[len(words)-1], nil } + +func getFullyQualifiedIDfunc(ctx context.Context, externalName string, parameters map[string]interface{}, providerConfig map[string]interface{}) (string, error) + subID, ok := providerConfig["subscription_id"] + if !ok { + return "", errors.Errorf(ErrFmtNoAttribute, "subscription_id") + } + subIDStr, ok := subID.(string) + if !ok { + return "", errors.Errorf(ErrFmtUnexpectedType, "subscription_id") + } + rg, ok := parameters["resource_group_name"] + if !ok { + return "", errors.Errorf(ErrFmtNoAttribute, "resource_group_name") + } + rgStr, ok := rg.(string) + if !ok { + return "", errors.Errorf(ErrFmtUnexpectedType, "resource_group_name") + } + + name, ok := parameters["name"] + if !ok { + return "", errors.Errorf(ErrFmtNoAttribute, "name") + } + nameStr, ok := rg.(string) + if !ok { + return "", errors.Errorf(ErrFmtUnexpectedType, "name") + } + + return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Sql/servers/%s", subIDStr, rgStr, nameStr), nil +} + +... + p.AddResourceConfigurator("azurerm_sql_server", func(r *config.Resource) { + r.ExternalName = config.NameAsIdentifier + r.ExternalName.GetExternalNameFn = getNameFromFullyQualifiedID + r.ExternalName.GetIDFn = getFullyQualifiedIDfunc + ... + } ``` -Please note, you can always check resource configurations of existing Providers -as further examples under `config//config.go`. +With this, we have covered most common scenarios for configuring external name. +You can always check resource configurations of existing jet Providers as +further examples under `config//config.go`. ### Cross Resource Referencing @@ -384,16 +497,23 @@ during late-initialization. [Additional Sensitive Fields and Custom Connection Details]: #additional-sensitive-fields-and-custom-connection-details [Late Initialization Behavior]: #late-initialization-behavior [the external name documentation]: https://crossplane.io/docs/v1.4/concepts/managed-resources.html#external-name +[concept to identify a resource]: https://www.terraform.io/docs/glossary#id [import section]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key#import -[the struct that holds the External Name configuration]: https://github.com/crossplane-contrib/terrajet/blob/08e5e93f8a93c6628a4302fb520cd4be4b6cab07/pkg/config/resource.go#L50 +[the types for the External Name configuration]: https://github.com/crossplane/terrajet/blob/2299925ea2541e6a8088ede463cd865bd64eba32/pkg/config/resource.go#L67 +[aws_iam_user]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user +[NameAsIdentifier]: https://github.com/crossplane/terrajet/blob/2299925ea2541e6a8088ede463cd865bd64eba32/pkg/config/defaults.go#L31 +[aws_s3_bucket]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket +[import section of s3 bucket]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#import +[bucket]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#bucket +[cluster_identifier]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster#cluster_identifier +[aws_rds_cluster]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster. [aws_vpc]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc [import section of aws_vpc]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc#import [arguments list]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc#argument-reference [example usages]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc#example-usage -[IdentifierFromProvider]: https://github.com/crossplane-contrib/terrajet/blob/08e5e93f8a93c6628a4302fb520cd4be4b6cab07/pkg/config/defaults.go#L43 -[aws_s3_bucket]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket -[import section of s3 bucket]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#import -[bucket]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#bucket +[IdentifierFromProvider]: https://github.com/crossplane/terrajet/blob/2299925ea2541e6a8088ede463cd865bd64eba32/pkg/config/defaults.go#L46 + +[import section of azurerm_sql_server]: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/sql_server#import [handle dependencies]: https://crossplane.io/docs/v1.4/concepts/managed-resources.html#dependencies [user]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key#user [generate reference resolution methods]: https://github.com/crossplane/crossplane-tools/pull/35 diff --git a/docs/images/terrajet-externalname.excalidraw b/docs/images/terrajet-externalname.excalidraw new file mode 100644 index 00000000..83ace476 --- /dev/null +++ b/docs/images/terrajet-externalname.excalidraw @@ -0,0 +1,592 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "text", + "version": 1021, + "versionNonce": 395519118, + "isDeleted": false, + "id": "KI_GOCk6D3LF7MLJH45YI", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 226.03963385687956, + "y": 523.9467875162761, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 83, + "height": 24, + "seed": 1950342674, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1640766936323, + "fontSize": 20, + "fontFamily": 3, + "text": "main.tf", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "main.tf" + }, + { + "id": "VAQXG49p_GImGlOM2SCX0", + "type": "text", + "x": 224.96400282118054, + "y": 549.7008090549045, + "width": 384, + "height": 57, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1444074126, + "version": 684, + "versionNonce": 1274587406, + "isDeleted": false, + "boundElements": null, + "updated": 1640767175637, + "text": "resource \"azurerm_sql_server\" \"example\" {\n name = \"myserver\"\n}", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 53, + "containerId": null, + "originalText": "resource \"azurerm_sql_server\" \"example\" {\n name = \"myserver\"\n}" + }, + { + "type": "text", + "version": 1152, + "versionNonce": 1514502290, + "isDeleted": false, + "id": "Hg0AUpFh4JZCDuizUHDar", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 217.75595601399732, + "y": 642.5234018961589, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 200, + "height": 24, + "seed": 1578568526, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1640766938710, + "fontSize": 20, + "fontFamily": 3, + "text": "terraform.tfstate", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "terraform.tfstate" + }, + { + "type": "text", + "version": 646, + "versionNonce": 202159826, + "isDeleted": false, + "id": "UaOb_w_qBrxt2QK-tOatR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 217.45820448133696, + "y": 668.8050825330946, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "width": 1350, + "height": 380, + "seed": 2066520210, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1640767175637, + "fontSize": 16, + "fontFamily": 3, + "text": "{\n ...\n \"resources\": [\n {\n ...\n \"type\": \"azurerm_sql_server\",\n \"instances\": [\n {\n \"schema_version\": 1,\n \"attributes\": {\n ...\n \"name\": \"myserver\"\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/servers/myserver\",\n },\n }\n ]\n }\n ]\n}\n", + "baseline": 376, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n ...\n \"resources\": [\n {\n ...\n \"type\": \"azurerm_sql_server\",\n \"instances\": [\n {\n \"schema_version\": 1,\n \"attributes\": {\n ...\n \"name\": \"myserver\"\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/servers/myserver\",\n },\n }\n ]\n }\n ]\n}\n" + }, + { + "type": "text", + "version": 387, + "versionNonce": 2091587342, + "isDeleted": false, + "id": "eRqeEgcdtZItIItZEV1UM", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 221.58283148871618, + "y": 224.56826443142427, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 153, + "height": 24, + "seed": 413876174, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1640767143139, + "fontSize": 20, + "fontFamily": 3, + "text": "Crossplane CR", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Crossplane CR" + }, + { + "type": "text", + "version": 491, + "versionNonce": 1792164370, + "isDeleted": false, + "id": "r6-eRZlL06_WGHzNm_gCJ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 222.51002332899395, + "y": 250.42725965711873, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "width": 450, + "height": 247, + "seed": 176417810, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1640767155602, + "fontSize": 16, + "fontFamily": 3, + "text": "apiVersion: sql.azure.jet.crossplane.io/v1alpha1\nkind: Server\nmetadata:\n name: myserver\n annotations:\n crossplane.io/external-name: myserver\nspec:\n forProvider:\n ...\n resourceGroupNameRef:\n name: myresourcegroup\n providerConfigRef:\n name: example", + "baseline": 243, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "apiVersion: sql.azure.jet.crossplane.io/v1alpha1\nkind: Server\nmetadata:\n name: myserver\n annotations:\n crossplane.io/external-name: myserver\nspec:\n forProvider:\n ...\n resourceGroupNameRef:\n name: myresourcegroup\n providerConfigRef:\n name: example" + }, + { + "id": "5TV10V0jD_m7Ba68DZ2fG", + "type": "text", + "x": -25.893093532985446, + "y": 423.17475721571225, + "width": 282, + "height": 24, + "angle": 1.5908388161377918, + "strokeColor": "#a61e4d", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2019671186, + "version": 877, + "versionNonce": 992954322, + "isDeleted": false, + "boundElements": null, + "updated": 1640767240658, + "text": "SetIdentifierArgumentsFn", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 19, + "containerId": null, + "originalText": "SetIdentifierArgumentsFn" + }, + { + "id": "OWMLmG1qEaTPVjAkQzYVL", + "type": "arrow", + "x": 317.055989583334, + "y": 905.9023904734846, + "width": 274.96063599415095, + "height": 606.400725194113, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 851976526, + "version": 336, + "versionNonce": 116907982, + "isDeleted": false, + "boundElements": null, + "updated": 1640767075144, + "points": [ + [ + 0, + 0 + ], + [ + -245.06941434035298, + 10.584980393955334 + ], + [ + -274.96063599415095, + -595.8157448001576 + ], + [ + -63.10340314582341, + -546.7894346744989 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "snDyjav5O0W8vLbx6tTUK", + "type": "text", + "x": -81.70299614800285, + "y": 622.9935167100699, + "width": 200, + "height": 24, + "angle": 4.663104997457104, + "strokeColor": "#a61e4d", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2145515726, + "version": 750, + "versionNonce": 404848846, + "isDeleted": false, + "boundElements": null, + "updated": 1640767233118, + "text": "GetExternalNameFn", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 19, + "containerId": null, + "originalText": "GetExternalNameFn" + }, + { + "id": "i1IwY99AsyJEAlICn0Q4E", + "type": "arrow", + "x": 614.5095350477438, + "y": 353.70274522569514, + "width": 378.6663818359373, + "height": 523.7147352430555, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 578539726, + "version": 486, + "versionNonce": 2024339342, + "isDeleted": false, + "boundElements": null, + "updated": 1640766615322, + "points": [ + [ + 0, + 0 + ], + [ + 185.72340223524293, + 72.74000379774304 + ], + [ + 378.6663818359373, + 523.7147352430555 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "yp_4lXs-yhQZTlmdYdcmm", + "type": "text", + "x": 865.1228027343753, + "y": 602.6049940321188, + "width": 83, + "height": 24, + "angle": 1.1859009297040224, + "strokeColor": "#a61e4d", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1671514578, + "version": 405, + "versionNonce": 836997070, + "isDeleted": false, + "boundElements": null, + "updated": 1640767210339, + "text": "GetIDFn", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 19, + "containerId": null, + "originalText": "GetIDFn" + }, + { + "id": "nI54fXMCmticLq4TcxXYw", + "type": "line", + "x": 482.2833116319452, + "y": 451.5879109700528, + "width": 382.05634223090266, + "height": 98.87664794921875, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 302988754, + "version": 224, + "versionNonce": 137855630, + "isDeleted": false, + "boundElements": null, + "updated": 1640766644783, + "points": [ + [ + 0, + 0 + ], + [ + 212.16501438174836, + 4.2364501953125 + ], + [ + 382.05634223090266, + 98.87664794921875 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "type": "text", + "version": 624, + "versionNonce": 1256141778, + "isDeleted": false, + "id": "1SkEQGtk7u8GXVU08hI11", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 961.9043240017368, + "y": 382.66367594401095, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 294, + "height": 24, + "seed": 122927890, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "_JhZVDh7aT10eKkF1e-yg", + "type": "arrow" + } + ], + "updated": 1640766685481, + "fontSize": 20, + "fontFamily": 3, + "text": "Crossplane ProviderConfig", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Crossplane ProviderConfig" + }, + { + "id": "ddq0dI6vNmGJ_3KyqgqgU", + "type": "line", + "x": 960.1107313368061, + "y": 396.84781901041737, + "width": 95.15625, + "height": 158.71751573350696, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1018964558, + "version": 101, + "versionNonce": 1480678094, + "isDeleted": false, + "boundElements": null, + "updated": 1640766691678, + "points": [ + [ + 0, + 0 + ], + [ + -60.899929470486086, + -7.0215521918402715 + ], + [ + -95.15625, + 151.69596354166669 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "RCTO4Db2bzTKzfdKziS2N", + "type": "arrow", + "x": 235.94725206163235, + "y": 318.1501736111114, + "width": 138.8999769422743, + "height": 338.50004408094617, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 791766482, + "version": 257, + "versionNonce": 2073707986, + "isDeleted": false, + "boundElements": null, + "updated": 1640766807811, + "points": [ + [ + 0, + 0 + ], + [ + -122.9632568359375, + -62.69946628146704 + ], + [ + -134.64467366536456, + 275.80057779947913 + ], + [ + 4.2553032769097285, + 262.4541558159722 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "2ScnicOW-IBF4SBBG_FEB", + "type": "text", + "x": 1260.7777777777783, + "y": 385.61111111111154, + "width": 245, + "height": 19, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1118441362, + "version": 32, + "versionNonce": 291264334, + "isDeleted": false, + "boundElements": null, + "updated": 1640767175637, + "text": "(e.g. for subscription id)", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 15, + "containerId": null, + "originalText": "(e.g. for subscription id)" + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/docs/images/terrajet-externalname.png b/docs/images/terrajet-externalname.png new file mode 100644 index 00000000..9ace8e55 Binary files /dev/null and b/docs/images/terrajet-externalname.png differ