Skip to content

Latest commit

 

History

History
907 lines (766 loc) · 37 KB

configuring-a-resource.md

File metadata and controls

907 lines (766 loc) · 37 KB

Configuring a resource

Upjet generates as much as it could using the available information in the Terraform resource schema. This includes an XRM-conformant schema of the resource, controller logic, late initialization, sensitive data handling, etc. However, there are still information that requires some input configuration which can be found by checking the Terraform documentation of the resource:

External Name

Crossplane uses an annotation in managed resource CR to identify the external resource which is managed by Crossplane. See the external name documentation for more details. The format and source of the external name depends on the cloud provider; sometimes it could simply be the name of resource (e.g. S3 Bucket), and sometimes it is an auto-generated id by cloud API (e.g. VPC id ). This is something specific to resource, and we need some input configuration for upjet to appropriately generate a resource.

Since Terraform already needs a similar identifier to import a resource, most helpful part of resource documentation is the import section.

Upjet 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 from Terraform id.

Here are the types for the External Name configuration:

// SetIdentifierArgumentsFn sets the name of the resource in Terraform attributes map,
// i.e. Main HCL file.
type SetIdentifierArgumentsFn func(base map[string]any, externalName string)
// GetExternalNameFn returns the external name extracted from the TF State.
type GetExternalNameFn func(tfstate map[string]any) (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]any, providerConfig map[string]any) (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. 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 example cases.

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 Upjet as follows:

import (
 "github.com/crossplane/upjet/pkg/config"
 ...
)

...
    p.AddResourceConfigurator("aws_iam_user", func(r *config.Resource) {
        r.ExternalName = config.NameAsIdentifier
  ...
    }

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:

import (
 "github.com/crossplane/upjet/pkg/config"
 ...
)

...
    p.AddResourceConfigurator("aws_s3_bucket", func(r *config.Resource) {
        r.ExternalName = config.NameAsIdentifier
        r.ExternalName.SetIdentifierArgumentFn = func(base map[string]any, externalName string) {
            base["bucket"] = externalName
        }
        r.ExternalName.OmittedFields = []string{
            "bucket",
            "bucket_prefix",
        }
  ...
    }

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:

import (
 "github.com/crossplane/upjet/pkg/config"
 ...
)

...
    p.AddResourceConfigurator("aws_vpc", func(r *config.Resource) {
        r.ExternalName = config.IdentifierFromProvider
  ...
    }

Case 3: Terraform ID as a Formatted String

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.

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:

/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/servers/myserver

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).

import (
 "github.com/crossplane/upjet/pkg/config"
 ...
)

func getNameFromFullyQualifiedID(tfstate map[string]any) (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]any, providerConfig map[string]any) (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
  ...
    }

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/<group>/config.go in their repositories.

Please see this figure to understand why we really need 3 different functions to configure external names and, it visualizes which is used how: Alt text Note that, initially, GetIDFn will use the external-name annotation to set the terraform.tfstate id and, after that, it uses the terraform.tfstate id to update the external-name annotation. For cases where both values are different, both GetIDFn and GetExternalNameFn must be set in order to have the correct configuration.

Cross Resource Referencing

Crossplane uses cross resource referencing to handle dependencies between managed resources. For example, if you have an IAM User defined as a Crossplane managed resource, and you want to create an Access Key for that user, you would need to refer to the User CR from the Access Key resource. This is handled by cross resource referencing.

See how the user referenced at forProvider.userRef.name field of the Access Key in the following example:

apiVersion: iam.aws.tf.crossplane.io/v1alpha1
kind: User
metadata:
  name: sample-user
spec:
  forProvider: {}
---
apiVersion: iam.aws.tf.crossplane.io/v1alpha1
kind: AccessKey
metadata:
  name: sample-access-key
spec:
  forProvider:
    userRef:
      name: sample-user
  writeConnectionSecretToRef:
    name: sample-access-key-secret
    namespace: crossplane-system

Historically, reference resolution method were written by hand which requires some effort, however, with the latest Crossplane code generation tooling, it is now possible to generate reference resolution methods by just adding some marker on the fields. Now, the only manual step for generating cross resource references is to provide which field of a resource depends on which information (e.g. id, name, arn etc.) from the other.

In Upjet, we have a configuration to provide this information for a field:

// Reference represents the Crossplane options used to generate
// reference resolvers for fields
type Reference struct {
    // Type is the type name of the CRD if it is in the same package or
    // <package-path>.<type-name> if it is in a different package.
    Type string
    // Extractor is the function to be used to extract value from the
    // referenced type. Defaults to getting external name.
    // Optional
    Extractor string
    // RefFieldName is the field name for the Reference field. Defaults to
    // <field-name>Ref or <field-name>Refs.
    // Optional
    RefFieldName string
    // SelectorFieldName is the field name for the Selector field. Defaults to
    // <field-name>Selector.
    // Optional
    SelectorFieldName string
}

For a resource that we want to generate, we need to check its argument list in Terraform documentation and figure out which field needs reference to which resource.

Let's check iam_access_key as an example. In the argument list, we see the user field which requires a reference to a IAM user. So, we need to the following referencing configuration:

func Configure(p *config.Provider) {
    p.AddResourceConfigurator("aws_iam_access_key", func (r *config.Resource) {
        r.References["user"] = config.Reference{
            Type: "User",
        }
    })
}

Please note the value of Type field needs to be a string representing the Go type of the resource. Since, AccessKey and User resources are under the same go package, we don't need to provide the package path. However, this is not always the case and referenced resources might be in different package. In that case, we would need to provide the full path. Referencing to a kms key from aws_ebs_volume resource is a good example here:

func Configure(p *config.Provider) {
 p.AddResourceConfigurator("aws_ebs_volume", func(r *config.Resource) {
  r.References["kms_key_id"] = config.Reference{
   Type: "github.com/crossplane-contrib/provider-tf-aws/apis/kms/v1alpha1.Key",
  }
 })
}

Auto Cross Resource Reference Generation

Cross Resource Referencing is one of the key concepts of the resource configuration. As a very common case, cloud services depend on other cloud services. For example, AWS Subnet resource needs an AWS VPC for creation. So, for creating a Subnet successfully, before you have to create a VPC resource. Please see the Dependencies documentation for more details. And also, for resource configuration-related parts of cross-resource referencing, please see this part of Configuring a Resource documentation.

These documentations focus on the general concepts and manual configurations of Cross Resource References. However, the main topic of this documentation is automatic example&reference generation.

Upjet has a scraper tool for scraping provider metadata from the Terraform Registry. The scraped metadata are:

  • Resource Descriptions
  • Examples of Resources (in HCL format)
  • Field Documentations
  • Import Statements

These are very critical information for our automation processes. We use this scraped metadata in many contexts. For example, field documentation of resources and descriptions are used as Golang comments for schema fields and CRDs.

Another important scraped information is examples of resources. As a part of testing efforts, finding the correct combination of field values is not easy for every scenario. So, having a working example (combination) is very important for easy testing.

At this point, this example that is in HCL format is converted to a Managed Resource manifest, and we can use this manifest in our test efforts.

This is an example from Terraform Registry AWS Ebs Volume resource:

resource "aws_ebs_volume" "example" {
  availability_zone = "us-west-2a"
  size              = 40

  tags = {
    Name = "HelloWorld"
  }
}

resource "aws_ebs_snapshot" "example_snapshot" {
  volume_id = aws_ebs_volume.example.id

  tags = {
    Name = "HelloWorld_snap"
  }
}

The generated example:

apiVersion: ec2.aws.upbound.io/v1beta1
kind: EBSSnapshot
metadata:
  annotations:
    meta.upbound.io/example-id: ec2/v1beta1/ebssnapshot
  labels:
    testing.upbound.io/example-name: example_snapshot
  name: example-snapshot
spec:
  forProvider:
    region: us-west-1
    tags:
      Name: HelloWorld_snap
    volumeIdSelector:
      matchLabels:
        testing.upbound.io/example-name: example

---

apiVersion: ec2.aws.upbound.io/v1beta1
kind: EBSVolume
metadata:
  annotations:
    meta.upbound.io/example-id: ec2/v1beta1/ebssnapshot
  labels:
    testing.upbound.io/example-name: example
  name: example
spec:
  forProvider:
    availabilityZone: us-west-2a
    region: us-west-1
    size: 40
    tags:
      Name: HelloWorld

Here, there are three very important points that scraper makes easy our life:

  • We do not have to find the correct value combinations for fields. So, we can easily use the generated example manifest in our tests.
  • The HCL example was scraped from registry documentation of the aws_ebs_snapshot resource. In the example, you also see the aws_ebs_volume resource manifest because, for the creation of an EBS Snapshot, you need an EBS Volume resource. Thanks to the source Registry, (in many cases, there are the dependent resources of target resources) we can also scrape the dependencies of target resources.
  • The last item is actually what is intended to be explained in this document. For using the Cross Resource References, as I mentioned above, you need to add some references to the resource configuration. But, in many cases, if in the scraped example, the mentioned dependencies are already described you do not have to write explicit references to resource configuration. The Cross Resource Reference generator generates the mentioned references.

Validating the Cross Resource References

As I mentioned, many references are generated from scraped metadata by an auto reference generator. However, there are two cases where we miss generating the references.

The first one is related to some bugs or improvement points in the generator. This means that the generator can handle many references in the scraped examples and generate correctly them. But we cannot say that the ratio is %100. For some cases, the generator cannot generate references although, they are in the scraped example manifests.

The second one is related to the scraped example itself. As I mentioned above, the source of the generator is the scraped example manifest. So, it checks the manifest and tries to generate the found cross-resource references. In some cases, although there are other reference fields, these do not exist in the example manifest. They can only be mentioned in schema/field documentation.

For these types of situations, you must configure cross-resource references explicitly.

Removing Auto-Generated Cross Resource References In Some Corner Cases

In some cases, the generated references can narrow the reference pool covered by the field. For example, X resource has an A field and Y and Z resources can be referenced via this field. However, since the reference to Y is mentioned in the example manifest, this reference field will only be defined over Y. In this case, since the reference pool of the relevant field will be narrowed, it would be more appropriate to delete this reference. For example,

resource "aws_route53_record" "www" {
  zone_id = aws_route53_zone.primary.zone_id
  name    = "example.com"
  type    = "A"

  alias {
    name                   = aws_elb.main.dns_name
    zone_id                = aws_elb.main.zone_id
    evaluate_target_health = true
  }
}

Route53 Record resource’s alias.name field has a reference. In the example, this reference is shown by using the aws_elb resource. However, when we check the field documentation, we see that this field can also be used for reference for other resources:

Alias
Alias records support the following:

name - (Required) DNS domain name for a CloudFront distribution, S3 bucket, ELB, 
or another resource record set in this hosted zone.

Conclusion

As a result, mentioned scraper and example&reference generators are very useful for making easy the test efforts. But using these functionalities, we must be careful to avoid undesired states.

Additional Sensitive Fields and Custom Connection Details

Crossplane stores sensitive information of a managed resource in a Kubernetes secret, together with some additional fields that would help consumption of the resource, a.k.a. connection details.

In Upjet, we already handle sensitive fields that are marked as sensitive in Terraform schema and no further action required for them. Upjet will properly hide these fields from CRD spec and status by converting to a secret reference or storing in connection details secret respectively. However, we still have some custom configuration API that would allow including additional fields into connection details secret no matter they are sensitive or not.

As an example, let's use aws_iam_access_key. Currently, Upjet stores all sensitive fields in Terraform schema as prefixed with attribute., so without any AdditionalConnectionDetailsFn, connection resource will have attribute.id and attribute.secret corresponding to id and secret fields respectively. To see them with more common keys, i.e. aws_access_key_id and aws_secret_access_key, we would need to make the following configuration:

func Configure(p *config.Provider) {
 p.AddResourceConfigurator("aws_iam_access_key", func(r *config.Resource) {
  r.Sensitive.AdditionalConnectionDetailsFn = func(attr map[string]any) (map[string][]byte, error) {
   conn := map[string][]byte{}
   if a, ok := attr["id"].(string); ok {
    conn["aws_access_key_id"] = []byte(a)
   }
   if a, ok := attr["secret"].(string); ok {
    conn["aws_secret_access_key"] = []byte(a)
   }
   return conn, nil
  }
 })
}

This will produce a connection details secret as follows:

apiVersion: v1
data:
  attribute.id: QUtJQVk0QUZUVFNFNDI2TlhKS0I=
  attribute.secret: ABCxyzRedacted==
  attribute.ses_smtp_password_v4: QQ00REDACTED==
  aws_access_key_id: QUtJQVk0QUZUVFNFNDI2TlhKS0I=
  aws_secret_access_key: ABCxyzRedacted==
kind: Secret

Late Initialization Configuration

Late initialization configuration is only required if there are conflicting arguments in terraform resource configuration. Unfortunately, there is no easy way to figure that out without testing the resource, so feel free to skip this configuration at the first place and revisit only if you have errors like below while testing the resource.

observe failed: cannot run refresh: refresh failed: Invalid combination of arguments:
  "address_prefix": only one of `address_prefix,address_prefixes` can be specified, but `address_prefix,address_prefixes` were specified.: File name: main.tf.json

If you would like to have the late-initialization library not to process the address_prefix parameter field, then the following configuration where we specify the parameter field path is sufficient:

func Configure(p *config.Provider) {
 p.AddResourceConfigurator("azurerm_subnet", func(r *config.Resource) {
  r.LateInitializer = config.LateInitializer{
   IgnoredFields: []string{"address_prefix"},
  }
 })
}

Please note that, there could be errors looking slightly different from above, so please consider configuring late initialization behaviour whenever you got some unexpected error starting with observe failed:, once you are sure that you provided all necessary parameters to your resource.

Further details on Late Initialization

Upjet runtime automatically performs late-initialization during an external.Observe call with means of runtime reflection. State of the world observed by Terraform CLI is used to initialize any nil-valued pointer parameters in the managed resource's spec. In most of the cases no custom configuration should be necessary for late-initialization to work. However, there are certain cases where you will want/need to customize late-initialization behaviour. Thus, Upjet provides an extensible late-initialization customization API that controls late-initialization behaviour.

The associated resource struct is defined here as follows:

// LateInitializer represents configurations that control
// late-initialization behaviour
type LateInitializer struct {
    // IgnoredFields are the canonical field names to be skipped during
    // late-initialization
    IgnoredFields []string
}

Currently, it only involves a configuration option to specify certain spec parameters to be ignored during late-initialization. Each element of the LateInitializer.IgnoredFields slice represents the canonical path relative to the parameters struct for the managed resource's Spec using Go type names as path elements. As an example, with the following type definitions:

type Subnet struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`
    Spec              SubnetSpec   `json:"spec"`
    Status            SubnetStatus `json:"status,omitempty"`
}

type SubnetSpec struct {
    ForProvider     SubnetParameters `json:"forProvider"`
    ...
}

type DelegationParameters struct {
    // +kubebuilder:validation:Required
    Name *string `json:"name" tf:"name,omitempty"`
    ...
}

type SubnetParameters struct {
    // +kubebuilder:validation:Optional
    AddressPrefix *string `json:"addressPrefix,omitempty" tf:"address_prefix,omitempty"`
    // +kubebuilder:validation:Optional
    Delegation []DelegationParameters `json:"delegation,omitempty" tf:"delegation,omitempty"`
    ...
}

In most cases, custom late-initialization configuration will not be necessary. However, after generating a new managed resource and observing its behaviour (at runtime), it may turn out that late-initialization behaviour needs customization. For certain resources like the provider-tf-azure's PostgresqlServer resource, we have observed that Terraform state contains values for mutually exclusive parameters, e.g., for PostgresqlServer, both StorageMb and StorageProfile[].StorageMb get late-initialized. Upon next reconciliation, we generate values for both parameters in the Terraform configuration, and although they happen to have the same value, Terraform configuration validation requires them to be mutually exclusive. Currently, we observe this behaviour at runtime, and upon observing that the resource cannot transition to the Ready state and acquires the Terraform validation error message in its status.conditions, we do the LateInitializer.IgnoreFields custom configuration detailed above to skip one of the mutually exclusive fields during late-initialization.

Overriding Terraform Resource Schema

Upjet generates Crossplane resource schemas (CR spec/status) using the Terraform schema of the resource. As of today, Upjet leverages the following attributes in the schema:

  • Type and Elem to identify the type of the field.
  • Sensitive to see if we need to keep it in a Secret instead of CR.
  • Description to add as a description to the field in CRD.
  • Optional and Computed to identify whether the fields go under spec or status:
    • Not Optional & Not Computed => Spec (required)
    • Optional & Not Computed => Spec (optional)
    • Optional & Computed => Spec (optional, to be late-initialized)
    • Not Optional & Computed => Status

Usually, we don't need to make any modifications in the resource schema and resource schema just works as is. However, there could be some rare edge cases like:

  • Field contains sensitive information but not marked as Sensitive or vice versa.
  • An attribute does not make sense to have in CRD schema, like tags_all for jet AWS resources.
  • Moving parameters from Terraform provider config to resources schema to fit Crossplane model, e.g. AWS region parameter is part of provider config in Terraform but Crossplane expects it in CR spec.

Schema of a resource could be overridden as follows:

p.AddResourceConfigurator("aws_autoscaling_group", func(r *config.Resource) {
    // Managed by Attachment resource.
    if s, ok := r.TerraformResource.Schema["load_balancers"]; ok {
        s.Optional = false
        s.Computed = true
    }
    if s, ok := r.TerraformResource.Schema["target_group_arns"]; ok {
        s.Optional = false
        s.Computed = true
    }
})

Initializers

Initializers involve the operations that run before beginning of reconciliation. This configuration option will provide that setting initializers for per resource.

Many resources in aws have tags field in their schema. Also, in Crossplane there is a tagging convention. To implement the tagging convention for jet-aws provider, this initializer configuration support was provided.

There is a common struct (Tagger) in upjet to use the tagging convention:

// Tagger implements the Initialize function to set external tags
type Tagger struct {
 kube      client.Client
 fieldName string
}

// NewTagger returns a Tagger object.
func NewTagger(kube client.Client, fieldName string) *Tagger {
 return &Tagger{kube: kube, fieldName: fieldName}
}

// Initialize is a custom initializer for setting external tags
func (t *Tagger) Initialize(ctx context.Context, mg xpresource.Managed) error {
 paved, err := fieldpath.PaveObject(mg)
 if err != nil {
  return err
 }
 pavedByte, err := setExternalTagsWithPaved(xpresource.GetExternalTags(mg), paved, t.fieldName)
 if err != nil {
  return err
 }
 if err := json.Unmarshal(pavedByte, mg); err != nil {
  return err
 }
 if err := t.kube.Update(ctx, mg); err != nil {
  return err
 }
 return nil
}

As seen above, the Tagger struct accepts a fieldName. This fieldName specifies which value of field to set in the resource's spec. You can use the common Initializer by specifying the field name that points to the external tags in the configured resource.

There is also a default initializer for tagging convention, TagInitializer. It sets the value of fieldName to tags as default:

// TagInitializer returns a tagger to use default tag initializer.
var TagInitializer NewInitializerFn = func(client client.Client) managed.Initializer {
 return NewTagger(client, "tags")
}

In jet-aws provider, as a default process, if a resource has tags field in its schema, then the default initializer (TagInitializer) is added to Initializer list of resource:

// AddExternalTagsField adds ExternalTagsFieldName configuration for resources that have tags field.
func AddExternalTagsField() tjconfig.ResourceOption {
 return func(r *tjconfig.Resource) {
  if s, ok := r.TerraformResource.Schema["tags"]; ok && s.Type == schema.TypeMap {
   r.InitializerFns = append(r.InitializerFns, tjconfig.TagInitializer)
  }
 }
}

However, if the field name that used for the external label is different from the tags, the NewTagger function can be called and the specific fieldName can be passed to this:

r.InitializerFns = append(r.InitializerFns, func(client client.Client) managed.Initializer {
 return tjconfig.NewTagger(client, "example_tags_name")
})

If the above tagging convention logic does not work for you, and you want to use this configuration option for a reason other than tagging convention (for another custom initializer operation), you need to write your own struct in provider and have this struct implement the Initializer function with a custom logic.

This configuration option is set by using the InitializerFns field that is a list of NewInitializerFn:

// NewInitializerFn returns the Initializer with a client.
type NewInitializerFn func(client client.Client) managed.Initializer

Initializer is an interface in crossplane-runtime:

type Initializer interface {
 Initialize(ctx context.Context, mg resource.Managed) error
}

So, an interface must be passed to the related configuration field for adding initializers for a resource.