Understanding generator.yaml configuration
Understanding generator.yaml Configuration
This document describes the various configuration fields in a generator.yaml
file that can be used to control the API inference and code generation for an ACK controller.
We will show examples of configuring specific ACK controllers to highlight various configuration options.
Generate a resource manager package
For this section, we will use ECR as our example service API.
When creating a new ACK controller, after running the controller-bootstrap
program, you will be left with a generator.yaml
that has all inferred API resources ignored.
For the ECR controller, the generator.yaml
file would look like this:
ignore:
resource_names:
- Repository
- PullThroughCacheRule
If we ran make build-controller SERVICE=ecr
with the above generator.yaml
file, we would have some basic directories and files created:
[jaypipes@thelio code-generator]$ make build-controller SERVICE=ecr
building ack-generate ... ok.
==== building ecr-controller ====
Copying common custom resource definitions into ecr
Building Kubernetes API objects for ecr
Generating deepcopy code for ecr
Generating custom resource definitions for ecr
Building service controller for ecr
Generating RBAC manifests for ecr
Running gofmt against generated code for ecr
Updating additional GitHub repository maintenance files
==== building ecr-controller release artifacts ====
Building release artifacts for ecr-v0.0.0-non-release
Generating common custom resource definitions
Generating custom resource definitions for ecr
Generating RBAC manifests for ecr
[jaypipes@thelio ecr-controller]$ tree apis/ config/ pkg/
apis/
└── v1alpha1
├── ack-generate-metadata.yaml
├── doc.go
├── enums.go
├── generator.yaml
├── groupversion_info.go
└── types.go
config/
├── controller
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ └── service.yaml
├── crd
│ ├── common
│ │ ├── bases
│ │ │ ├── services.k8s.aws_adoptedresources.yaml
│ │ │ └── services.k8s.aws_fieldexports.yaml
│ │ └── kustomization.yaml
│ └── kustomization.yaml
├── default
│ └── kustomization.yaml
├── overlays
│ └── namespaced
│ ├── kustomization.yaml
│ ├── role-binding.json
│ └── role.json
└── rbac
├── cluster-role-binding.yaml
├── cluster-role-controller.yaml
├── kustomization.yaml
├── role-reader.yaml
├── role-writer.yaml
└── service-account.yaml
pkg/
├── resource
│ └── registry.go
└── version
└── version.go
11 directories, 25 files
To begin generating a particular resource manager, comment out the name of the resource from the ignore list and run make build-controller SERVICE=$SERVICE
.
ignore:
resource_names:
> #- Repository
- PullThroughCacheRule
After doing so, the resource manager for Repository
resources will have been generated in the ecr-controller
source code repository.
[jaypipes@thelio ecr-controller]$ tree apis/ config/ pkg/
apis/
└── v1alpha1
├── ack-generate-metadata.yaml
├── doc.go
├── enums.go
├── generator.yaml
├── groupversion_info.go
├── repository.go
├── types.go
└── zz_generated.deepcopy.go
config/
├── controller
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ └── service.yaml
├── crd
│ ├── bases
│ │ └── ecr.services.k8s.aws_repositories.yaml
│ ├── common
│ │ ├── bases
│ │ │ ├── services.k8s.aws_adoptedresources.yaml
│ │ │ └── services.k8s.aws_fieldexports.yaml
│ │ └── kustomization.yaml
│ └── kustomization.yaml
├── default
│ └── kustomization.yaml
├── overlays
│ └── namespaced
│ ├── kustomization.yaml
│ ├── role-binding.json
│ └── role.json
└── rbac
├── cluster-role-binding.yaml
├── cluster-role-controller.yaml
├── kustomization.yaml
├── role-reader.yaml
├── role-writer.yaml
└── service-account.yaml
pkg/
├── resource
│ ├── registry.go
│ └── repository
│ ├── delta.go
│ ├── descriptor.go
│ ├── identifiers.go
│ ├── manager_factory.go
│ ├── manager.go
│ ├── references.go
│ ├── resource.go
│ ├── sdk.go
│ └── tags.go
└── version
└── version.go
13 directories, 37 files
Note the new files under pkg/resource/repository/
, apis/v1alpha1/repository.go
and config/crd/bases/
. These files represent the Go type for the generated Repository
custom resource definition (CRD), the resource manager package and the YAML representation for the CRD, respectively.
renames
: Renaming things
Why might we want to rename fields or resources? Generally, there are two reasons for this:
- reducing stutter in the input shape
- correcting instances where a field is named differently in the input and output shapes
The first reason is to reduce “stutter” (or redundancy) in naming. For example, the ECR Repository
resource has a field called RepositoryName
. This field is redundantly named because the resource itself is called Repository
. Every Kubernetes object has a Metadata.Name
field and we like to align resource “name fields” with this simple Name
moniker.
For this example, let’s go ahead and “destutter” the RepositoryName
field. To do this, we use the renames
configuration option, specifying the input and output shapes and their members that we want to rename:
ignore:
resource_names:
#- Repository
- PullThroughCacheRule
resources:
Repository:
> renames:
> operations:
> CreateRepository:
> input_fields:
> RepositoryName: Name
> DeleteRepository:
> input_fields:
> RepositoryName: Name
> DescribeRepositories:
> input_fields:
> RepositoryName: Name
📝 Note that we must tell the code generator which fields to rename in the input shapes for each API operation that the resource manager will call. In the case of ECR
Repository
resources, the resource manager calls theCreateRepository
,DeleteRepository
andDescribeRepositories
API calls and so we need specify theRepositoryName
member field in each of those input shapes should be renamed toName
.
After calling make build-controller SERVICE=ecr
, we see the above generator configuration items produced the following diff:
diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml
index e34e029..f214b43 100755
--- a/apis/v1alpha1/ack-generate-metadata.yaml
+++ b/apis/v1alpha1/ack-generate-metadata.yaml
@@ -1,13 +1,13 @@
ack_generate_info:
- build_date: "2022-11-09T20:15:42Z"
+ build_date: "2022-11-09T20:16:52Z"
build_hash: 5ee0ac052c54f008dff50f6f5ebb73f2cf3a0bd7
go_version: go1.18.1
version: v0.20.1-4-g5ee0ac0
-api_directory_checksum: 0a514bef9cff983f9fe28f080d85725ccf578060
+api_directory_checksum: 84fb59a0991980da922a385f585111a1ff784d82
api_version: v1alpha1
aws_sdk_go_version: v1.44.93
generator_config_info:
- file_checksum: 87446926d73abae9355e6328eb7f8f668b16b18e
+ file_checksum: a383007f82a686dc544879792dde7b091aeededa
original_file_name: generator.yaml
last_modification:
reason: API generation
diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml
index cb7045a..ed0130f 100644
--- a/apis/v1alpha1/generator.yaml
+++ b/apis/v1alpha1/generator.yaml
@@ -2,3 +2,16 @@ ignore:
resource_names:
#- Repository
- PullThroughCacheRule
+resources:
+ Repository:
+ renames:
+ operations:
+ CreateRepository:
+ input_fields:
+ RepositoryName: Name
+ DeleteRepository:
+ input_fields:
+ RepositoryName: Name
+ DescribeRepositories:
+ input_fields:
+ RepositoryName: Name
diff --git a/apis/v1alpha1/repository.go b/apis/v1alpha1/repository.go
index c226d4f..fc6165d 100644
--- a/apis/v1alpha1/repository.go
+++ b/apis/v1alpha1/repository.go
@@ -35,15 +35,15 @@ type RepositorySpec struct {
// be overwritten. If IMMUTABLE is specified, all image tags within the repository
// will be immutable which will prevent them from being overwritten.
ImageTagMutability *string `json:"imageTagMutability,omitempty"`
- // The Amazon Web Services account ID associated with the registry to create
- // the repository. If you do not specify a registry, the default registry is
- // assumed.
- RegistryID *string `json:"registryID,omitempty"`
// The name to use for the repository. The repository name may be specified
// on its own (such as nginx-web-app) or it can be prepended with a namespace
// to group the repository into a category (such as project-a/nginx-web-app).
// +kubebuilder:validation:Required
- RepositoryName *string `json:"repositoryName"`
+ Name *string `json:"name"`
+ // The Amazon Web Services account ID associated with the registry to create
+ // the repository. If you do not specify a registry, the default registry is
+ // assumed.
+ RegistryID *string `json:"registryID,omitempty"`
// The metadata that you apply to the repository to help you categorize and
// organize them. Each tag consists of a key and an optional value, both of
// which you define. Tag keys can have a maximum character length of 128 characters,
diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go
index 93919be..88dd4c0 100644
--- a/apis/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/v1alpha1/zz_generated.deepcopy.go
@@ -421,13 +421,13 @@ func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) {
*out = new(string)
**out = **in
}
- if in.RegistryID != nil {
- in, out := &in.RegistryID, &out.RegistryID
+ if in.Name != nil {
+ in, out := &in.Name, &out.Name
*out = new(string)
**out = **in
}
- if in.RepositoryName != nil {
- in, out := &in.RepositoryName, &out.RepositoryName
+ if in.RegistryID != nil {
+ in, out := &in.RegistryID, &out.RegistryID
*out = new(string)
**out = **in
}
diff --git a/config/crd/bases/ecr.services.k8s.aws_repositories.yaml b/config/crd/bases/ecr.services.k8s.aws_repositories.yaml
index 438785e..9657569 100644
--- a/config/crd/bases/ecr.services.k8s.aws_repositories.yaml
+++ b/config/crd/bases/ecr.services.k8s.aws_repositories.yaml
@@ -61,17 +61,17 @@ spec:
all image tags within the repository will be immutable which will
prevent them from being overwritten.
type: string
- registryID:
- description: The Amazon Web Services account ID associated with the
- registry to create the repository. If you do not specify a registry,
- the default registry is assumed.
- type: string
- repositoryName:
+ name:
description: The name to use for the repository. The repository name
may be specified on its own (such as nginx-web-app) or it can be
prepended with a namespace to group the repository into a category
(such as project-a/nginx-web-app).
type: string
+ registryID:
+ description: The Amazon Web Services account ID associated with the
+ registry to create the repository. If you do not specify a registry,
+ the default registry is assumed.
+ type: string
tags:
description: The metadata that you apply to the repository to help
you categorize and organize them. Each tag consists of a key and
@@ -92,7 +92,7 @@ spec:
type: object
type: array
required:
- - repositoryName
+ - name
type: object
status:
description: RepositoryStatus defines the observed state of Repository
diff --git a/generator.yaml b/generator.yaml
index cb7045a..ed0130f 100644
--- a/generator.yaml
+++ b/generator.yaml
@@ -2,3 +2,16 @@ ignore:
resource_names:
#- Repository
- PullThroughCacheRule
+resources:
+ Repository:
+ renames:
+ operations:
+ CreateRepository:
+ input_fields:
+ RepositoryName: Name
+ DeleteRepository:
+ input_fields:
+ RepositoryName: Name
+ DescribeRepositories:
+ input_fields:
+ RepositoryName: Name
diff --git a/helm/crds/ecr.services.k8s.aws_repositories.yaml b/helm/crds/ecr.services.k8s.aws_repositories.yaml
index 438785e..9657569 100644
--- a/helm/crds/ecr.services.k8s.aws_repositories.yaml
+++ b/helm/crds/ecr.services.k8s.aws_repositories.yaml
@@ -61,17 +61,17 @@ spec:
all image tags within the repository will be immutable which will
prevent them from being overwritten.
type: string
- registryID:
- description: The Amazon Web Services account ID associated with the
- registry to create the repository. If you do not specify a registry,
- the default registry is assumed.
- type: string
- repositoryName:
+ name:
description: The name to use for the repository. The repository name
may be specified on its own (such as nginx-web-app) or it can be
prepended with a namespace to group the repository into a category
(such as project-a/nginx-web-app).
type: string
+ registryID:
+ description: The Amazon Web Services account ID associated with the
+ registry to create the repository. If you do not specify a registry,
+ the default registry is assumed.
+ type: string
tags:
description: The metadata that you apply to the repository to help
you categorize and organize them. Each tag consists of a key and
@@ -92,7 +92,7 @@ spec:
type: object
type: array
required:
- - repositoryName
+ - name
type: object
status:
description: RepositoryStatus defines the observed state of Repository
diff --git a/pkg/resource/repository/delta.go b/pkg/resource/repository/delta.go
index a15d260..57b54df 100644
--- a/pkg/resource/repository/delta.go
+++ b/pkg/resource/repository/delta.go
@@ -77,6 +77,13 @@ func newResourceDelta(
delta.Add("Spec.ImageTagMutability", a.ko.Spec.ImageTagMutability, b.ko.Spec.ImageTagMutability)
}
}
+ if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name) {
+ delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name)
+ } else if a.ko.Spec.Name != nil && b.ko.Spec.Name != nil {
+ if *a.ko.Spec.Name != *b.ko.Spec.Name {
+ delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name)
+ }
+ }
if ackcompare.HasNilDifference(a.ko.Spec.RegistryID, b.ko.Spec.RegistryID) {
delta.Add("Spec.RegistryID", a.ko.Spec.RegistryID, b.ko.Spec.RegistryID)
} else if a.ko.Spec.RegistryID != nil && b.ko.Spec.RegistryID != nil {
@@ -84,13 +91,6 @@ func newResourceDelta(
delta.Add("Spec.RegistryID", a.ko.Spec.RegistryID, b.ko.Spec.RegistryID)
}
}
- if ackcompare.HasNilDifference(a.ko.Spec.RepositoryName, b.ko.Spec.RepositoryName) {
- delta.Add("Spec.RepositoryName", a.ko.Spec.RepositoryName, b.ko.Spec.RepositoryName)
- } else if a.ko.Spec.RepositoryName != nil && b.ko.Spec.RepositoryName != nil {
- if *a.ko.Spec.RepositoryName != *b.ko.Spec.RepositoryName {
- delta.Add("Spec.RepositoryName", a.ko.Spec.RepositoryName, b.ko.Spec.RepositoryName)
- }
- }
if !reflect.DeepEqual(a.ko.Spec.Tags, b.ko.Spec.Tags) {
delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
}
diff --git a/pkg/resource/repository/resource.go b/pkg/resource/repository/resource.go
index e15d755..a2dd27e 100644
--- a/pkg/resource/repository/resource.go
+++ b/pkg/resource/repository/resource.go
@@ -88,7 +88,7 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error
if identifier.NameOrID == "" {
return ackerrors.MissingNameIdentifier
}
- r.ko.Spec.RepositoryName = &identifier.NameOrID
+ r.ko.Spec.Name = &identifier.NameOrID
f2, f2ok := identifier.AdditionalKeys["registryID"]
if f2ok {
diff --git a/pkg/resource/repository/sdk.go b/pkg/resource/repository/sdk.go
index 4366244..61a0053 100644
--- a/pkg/resource/repository/sdk.go
+++ b/pkg/resource/repository/sdk.go
@@ -132,9 +132,9 @@ func (rm *resourceManager) sdkFind(
ko.Status.ACKResourceMetadata.ARN = &tmpARN
}
if elem.RepositoryName != nil {
- ko.Spec.RepositoryName = elem.RepositoryName
+ ko.Spec.Name = elem.RepositoryName
} else {
- ko.Spec.RepositoryName = nil
+ ko.Spec.Name = nil
}
if elem.RepositoryUri != nil {
ko.Status.RepositoryURI = elem.RepositoryUri
@@ -158,7 +158,7 @@ func (rm *resourceManager) sdkFind(
func (rm *resourceManager) requiredFieldsMissingFromReadManyInput(
r *resource,
) bool {
- return r.ko.Spec.RepositoryName == nil
+ return r.ko.Spec.Name == nil
}
@@ -172,9 +172,9 @@ func (rm *resourceManager) newListRequestPayload(
if r.ko.Spec.RegistryID != nil {
res.SetRegistryId(*r.ko.Spec.RegistryID)
}
- if r.ko.Spec.RepositoryName != nil {
+ if r.ko.Spec.Name != nil {
f3 := []*string{}
- f3 = append(f3, r.ko.Spec.RepositoryName)
+ f3 = append(f3, r.ko.Spec.Name)
res.SetRepositoryNames(f3)
}
@@ -253,9 +253,9 @@ func (rm *resourceManager) sdkCreate(
ko.Status.ACKResourceMetadata.ARN = &arn
}
if resp.Repository.RepositoryName != nil {
- ko.Spec.RepositoryName = resp.Repository.RepositoryName
+ ko.Spec.Name = resp.Repository.RepositoryName
} else {
- ko.Spec.RepositoryName = nil
+ ko.Spec.Name = nil
}
if resp.Repository.RepositoryUri != nil {
ko.Status.RepositoryURI = resp.Repository.RepositoryUri
@@ -298,8 +298,8 @@ func (rm *resourceManager) newCreateRequestPayload(
if r.ko.Spec.RegistryID != nil {
res.SetRegistryId(*r.ko.Spec.RegistryID)
}
- if r.ko.Spec.RepositoryName != nil {
- res.SetRepositoryName(*r.ko.Spec.RepositoryName)
+ if r.ko.Spec.Name != nil {
+ res.SetRepositoryName(*r.ko.Spec.Name)
}
if r.ko.Spec.Tags != nil {
f5 := []*svcsdk.Tag{}
@@ -362,8 +362,8 @@ func (rm *resourceManager) newDeleteRequestPayload(
if r.ko.Spec.RegistryID != nil {
res.SetRegistryId(*r.ko.Spec.RegistryID)
}
- if r.ko.Spec.RepositoryName != nil {
- res.SetRepositoryName(*r.ko.Spec.RepositoryName)
+ if r.ko.Spec.Name != nil {
+ res.SetRepositoryName(*r.ko.Spec.Name)
}
return res, nil
You will note that there were changes made to the repository.go
file, the pkg/resource/repository/sdk.go
.
Renaming things with different names in input and output shapes
The second reason we might need to rename a field is when the same field goes by different names in the shapes (i.e., expected syntax) of the input and output. An hypothetical example of this might be a field that is called EnableEncryption
in an input shape and EncryptionEnabled
in an output shape. In order to inform the code generator that these fields are actually the same, we would rename one of the fields to match the other.
[TODO this needs a concrete example of renaming with both input_fields
and output_fields
]
ignore
: Ignoring things
Sometimes you want to instruct the code generator to simply ignore a particular API Operation, or a particular field in an API Shape. See here for a real world motivating example of such a need.
You will use the ignore:
block of configuration options to do this.
To ignore a specific field in an API Shape, you can list the field via fieldpath in the ignore.fieldpaths
configuration option.
An example of this can be found in the S3 controller’s generator.yaml
file:
ignore:
field_paths:
# We cannot support MFA, so if it is set we cannot unset
- "VersioningConfiguration.MFADelete"
# This subfield struct has no members...
- "NotificationConfiguration.EventBridgeConfiguration"
When you specify a field path in ignore.field_paths
, the code generator will skip over that field when inferring custom resource definition Spec
and Status
structures.
Tags
Most resources in AWS service APIs can have one or more tags associated with them. Tags are typically simple string key/value pairs; however, the representation of tags across different AWS service APIs is not consistent. Some APIs use a map[string]string
to represent tags. Others use a []struct{}
where the struct has a Key
and a Value
field. Others use more complex structures.
tags.ignore
: Telling ACK code generator that a resource does not support tags
There are some API resources that do not support tags at all, and we want a way to skip the generation of code that handles tagging for those resources. By default, for all resources, ACK generates some code that handles conversion between the ACK standard representation of tags (i.e., map[string]string
) and the AWS service-specific representation of tags (e.g., []struct{}
, etc).
If you attempt to generate a resource manager for a resource that does not support tags, you will receive an error from the code generator. ECR’s PassThroughCacheRule
is an example of a resource that does not support tags. If we unignore the PassThroughCacheRule
resource in the ECR controller’s generator.yaml
file and regenerate the controller, we will stumble upon this error:
[jaypipes@thelio code-generator]$ make build-controller SERVICE=ecr
building ack-generate ... ok.
==== building ecr-controller ====
Copying common custom resource definitions into ecr
Building Kubernetes API objects for ecr
Generating deepcopy code for ecr
Generating custom resource definitions for ecr
Building service controller for ecr
Error: template: /home/jaypipes/go/src/github.com/aws-controllers-k8s/code-generator/templates/pkg/resource/manager.go.tpl:282:20: executing "/home/jaypipes/go/src/github.com/aws-controllers-k8s/code-generator/templates/pkg/resource/manager.go.tpl" at <.CRD.GetTagField>: error calling GetTagField: tag field path Tags does not exist inside PullThroughCacheRule crd
make: *** [Makefile:41: build-controller] Error 1
To fix this error, we used the tags.ignore
configuration option in generator.yaml
:
ignore:
resource_names:
#- Repository
#- PullThroughCacheRule
resources:
Repository:
renames:
operations:
CreateRepository:
input_fields:
RepositoryName: Name
DeleteRepository:
input_fields:
RepositoryName: Name
DescribeRepositories:
input_fields:
RepositoryName: Name
PullThroughCacheRule:
fields:
ECRRepositoryPrefix:
is_primary_key: true
> tags:
> ignore: true
Resource configuration
Understanding resource-identifying fields
All resources in the AWS world have one or more fields that serve as primary key identifiers. Most people are familiar with the ARN
fields that most modern AWS resources have. However, the ARN
field is not the only field that can serve as a primary key for a resource. ACK’s code generator reads an API model file and attempts to determine which fields on a resource can be used to uniquely identify that resource. Sometimes, though, the code generator needs to be instructed which field or fields comprise this primary key. See below for an example from ECR’s PullThroughCacheRule
.
There are resource-level and field-level configuration options that inform the code generator about identifying fields.
is_arn_primary_key
: Resource-level configuration of identifying fields
The resources[$resource].is_arn_primary_key
configuration option is a boolean, defaulting to false
that instructs the code generator to use the ARN
field when calling the “ReadOne” (i.e., “Describe” or “Get”) operation for that resource. When false
, the code generator will look for “identifier fields” with field names such as ID
or Name
(along with variants that include the resource name as a prefix, e.g., “BucketName”).
Use the is_arn_primary_key=true
configuration option when the resource has no other identifying fields. An example of this is SageMaker’s ModelPackage
resource that has no Name
or ID
field and can only be identified via an ARN
field:
resources:
ModelPackage:
is_arn_primary_key: true
[NOTE(jaypipes): Probably want to reevaluate this particular config option and use the field-centric is_primary_key option instead…]
is_primary_key
: Field-level configuration of identifying fields
Sometimes a resource’s primary key field is non-obvious (like Name
or ID
). Use the resources[$resource]fields[$field].is_primary_key
configuration option to tell the code generator about these fields.
An example here is ECR’s PullThroughCacheRule
resource, which has a primary key field called ECRRepositoryPrefix
:
resources:
PullThroughCacheRule:
fields:
ECRRepositoryPrefix:
is_primary_key: true
[NOTE(jljaco): If we discard is_arn_primary_key
in favor of only is_primary_key
, this sub-section should be moved into the Field Configuration
section]
exceptions
: Correcting exception codes
An ACK controller needs to understand which HTTP exception code means “this resource was not found”; otherwise, the controller’s logic that determines whether to create or update a resource falls apart.
For the majority of AWS service APIs, the ACK code generator can figure out which HTTP exception codes map to which HTTP fault behaviours. However, some AWS service API model definitions do not include exception metadata. Other service API models include straight-up incorrect information that does not match what the actual AWS service returns.
To address these issues, you can use the resources[$resource].exceptions
configuration block.
An example of an API model that does not indicate the exception code representing a resource not found is DynamoDB. When calling DynamoDB’s DescribeTable
API call with a table name that does not exist, you will get back a 400
error code instead of 404
and the exception code string is ResourceNotFoundException
.
To tell the ACK code generator how to deal with this, use the exceptions
configuration option:
resources:
Table:
exceptions:
errors:
404:
code: ResourceNotFoundException
This configuration instructs the code generator to produce code that looks for ResourceNotFoundException
in the error response of the API call and interprets it properly as a 404
or “resource not found” error.
terminal_codes
: Specifying terminal codes to indicate terminal state
An ACK.Terminal
Condition
is placed on a custom resource (inside of its Status
) when the controller realizes that, without the user changing the resource’s Spec
, the resource will not be able to be reconciled (i.e., the desired state will never match the actual state).
When an ACK controller gets a response back from an AWS service containing an error code, the controller evaluates whether that error code should result in the ACK.Terminal
Condition
being placed on the resource. Examples of these “terminal codes” are things such as:
- improper input being supplied
- a duplicate resource already existing
- conflicting input values
AWS service API responses having a 4XX
HTTP status code will have a corresponding exception string code (e.g., InvalidParameterValue
or EntityExistsException
). Use the resources[$resource].exceptions.terminal_codes
configuration option to tell the code generation which of these exception string codes it should consider to be a terminal state for the resource.
Here is an example from the RDS controller, where we indicate the set of exception string code that will set the resource into a terminal state:
resources:
DBCluster:
exceptions:
terminal_codes:
- DBClusterQuotaExceededFault
- DBSubnetGroupDoesNotCoverEnoughAZs
- InsufficientStorageClusterCapacity
- InvalidParameter
- InvalidParameterValue
- InvalidParameterCombination
- InvalidSubnet
- StorageQuotaExceeded
reconcile
: Controlling reconciliation and requeue logic
By default, an ACK controller will requeue a resource for future reconciliation only when the resource is in some transitional state.
For example, when you create an RDS DBInstance
resource, the resource initially goes into a CREATING
transitional state and then eventually will arrive at an AVAILABLE
state. When the RDS controller for ACK initially creates the RDS DBInstance
resource, it calls the RDS CreateDBInstance
API call, sees the state of the DB instance is CREATING
, adds an ACK.ResourceSynced=False
Condition
to the resource and requeues the resource to be processed again in a few seconds.
When the resource is processed in the next reconciliation loop, the controller calls the DescribeDBInstance
API endpoint and checks to see if the DB instance is in the AVAILABLE
state. If it is not, then the controller requeues the resource again. If it is in the AVAILABLE
state, then the controller sets the ACK.ResourceSynced
Condition
to True
, which is the indication to the ACK runtime that the resource should not be requeued.
Sometimes, you may want to have the ACK controller requeue certain resources even after a successful reconciliation loop that leaves the resource in the ACK.ResourceSynced=True
state. If this is the case, you should use the resources[$resource].reconcile.requeue_on_success_seconds
configuration option. The value of this option should be the amount of time (in seconds) after which the reconciler should requeue the resource.
Here is an example of this configuration option as used in the SageMaker controller’s NotebookInstance
resource:
resources:
NotebookInstance:
# Resource state/status can be modified in Sagemaker Console
# Need to reconcile to catch these state/status changes
reconcile:
requeue_on_success_seconds: 60
We set this requeue_on_success_seconds
value to 60
here because the values of various fields in this Sagemaker resource tend to change often and we want the Status
section of our custom resource to contain values that are fresher than the default requeue period (10 hours as of this writing).
Including additional printer columns
TODO(jljaco)
Field configuration
When ack-generate
first infers the definition of a resource from the AWS API model, it collects the various member fields of a resource. This documentation section discusses the configuration options that instruct the code generator about a particular resource field.
is_read_only
: Manually marking a field as belonging to the resource Status
struct
During API inference, ack-generate
automatically determines which fields belong in the custom resource definition’s Spec
or Status
struct. Fields that can be modified by the user go in the Spec
and fields that cannot be modified go in the Status
.
Use the resources[$resource].fields[$field].is_read_only
configuration option to override whether a field should go in the Status
struct.
Here is an example from the Lambda controller’s generator.yaml file that instructs the code generator to treat the LayerStatuses
field as a read-only field (and thus should belong in the Status
struct for the Function resource):
resources:
Function:
fields:
LayerStatuses:
is_read_only: true
Typically, you will see this configuration option used for fields that have two different Go types representing the modifiable version of the field and the non-modifiable version of the field (as is the case for a Lambda Function’s Layers information) or when you need to create a custom field.
is_required
: Marking a field as required
If an AWS API model file marks a particular member field as required, ack-generate
will usually infer that the associated custom resource field is required. Sometimes, however, you may want to override whether or not a field should be required. Use the resources[$resource].fields[$field].is_required
configuration option to do so.
Here is an example from the EC2 controller’s generator.yaml
file that instructs the code generator to treat the Instance custom resource’s MinCount and MaxCount fields as not required, even though the API model definition marks these fields as required in the Create Operation’s Input shape.
NOTE: The reason for this is because the EC2 controller only deals with single Instance resources, not batches of instances
resources:
Instance:
fields:
MaxCount:
is_required: false
MinCount:
is_required: false
type
: controlling a field’s Go type
Use the resources[$resource].fields[$field].type
configuration option to override a field’s Go type. You will typically use this configuration option for custom fields that are not inferred by ack-generate
by looking at the AWS API model definition.
An example of this is the Policies field for a Role custom resource definition in the IAM controller. The IAM controller uses some custom hook code to allow a Kubernetes user to specify one or more Policy ARNs for a Role simply by specifying Spec.Policies
. To define this custom field as a list of string pointers, the IAM controller’s generator.yaml
file uses the following:
resources:
Role:
fields:
# In order to support attaching zero or more policies to a Role, we use
# custom update code path code that uses the Attach/DetachGroupPolicy API
# calls to manage the set of PolicyARNs attached to this Role.
Policies:
type: "[]*string"
compare
: Controlling how a field’s values are compared
Use the resources[$resource].fields[$field].compare
configuration option to control how the value of a field is compared between two resources. This configuration option has two boolean subfields, is_ignored
and nil_equals_zero_value
(TODO(jljaco): nil_equals_zero_value
not yet implemented or used).
is_ignored
: marking a field as ignored
Use the is_ignored
subfield to instruct the code generator to exclude this particular field from automatic value comparisons when building the Delta
struct that compares two resources.
Typically, you will want to mark a field as ignored for comparison operations because the Go type of the field does not natively support deterministic equality operations. For example, a slice of Tag
structs where the code generator does not know how to sort the slice means that the default reflect.DeepEqual
call will produce non-deterministic results. These types of fields you will want to mark with compare.is_ignored: true
and include a custom comparison function using the delta_pre_compare
hook, as this example from the IAM controller’s generator.yaml
does for the Role resource:
Role:
hooks:
delta_pre_compare:
code: compareTags(delta, a, b)
fields:
Tags:
compare:
is_ignored: true
is_immutable
: Mutable vs. immutable fields
Use the resources[$resource].fields[$field].is_immutable
configuration option to mark a field as immutable – meaning the user cannot update the field after initially setting its value.
A good example of the use of is_immutable
comes from the RDS controller’s DBInstance resource’s AvailabilityZone field:
resources:
DBInstance:
fields:
AvailabilityZone:
late_initialize: {}
is_immutable: true
In the case of a DBInstance resource, once the AvailabilityZone field is set by the user, it cannot be modified.
By telling the code generator that this field is immutable, it will generate code in the sdk.go
file that checks for whether a user has modified any immutable fields and set a Condition on the resource if so:
// sdkUpdate patches the supplied resource in the backend AWS service API and
// returns a new resource with updated fields.
func (rm *resourceManager) sdkUpdate(
ctx context.Context,
desired *resource,
latest *resource,
delta *ackcompare.Delta,
) (updated *resource, err error) {
rlog := ackrtlog.FromContext(ctx)
exit := rlog.Trace("rm.sdkUpdate")
defer func() {
exit(err)
}()
if immutableFieldChanges := rm.getImmutableFieldChanges(delta); len(immutableFieldChanges) > 0 {
msg := fmt.Sprintf("Immutable Spec fields have been modified: %s", strings.Join(immutableFieldChanges, ","))
return nil, ackerr.NewTerminalError(fmt.Errorf(msg))
}
...
}
from
: Controlling the source of a field’s definition
During API inference, ack-generate
inspects the AWS service API model definition and discovers resource fields by looking at the Input and Output shapes of the Create
API call for that resource. Members of the Input shape will go in the Spec
and members of the Output shape that are not also in the Input shape will go into the Status
.
This works for a majority of the field definitions, however sometimes you want to “grab a field” from a different location (i.e., other than either the Input or Output shapes of the Create
API call).
Each Resource typically also has a ReadOne
Operation. The ACK service controller will call this ReadOne
Operation to get the latest observed state of a particular resource in the backend AWS API service. The service controller sets the observed Resource’s Spec
and Status
fields from the Output shape of the ReadOne
Operation. The code generator is responsible for producing the Go code that performs these “setter” methods on the Resource.
The way the code generator determines how to set the Spec
or Status
fields from the Output shape’s member fields is by looking at the data type of the Spec
or Status
field with the same name as the Output shape’s member field.
Importantly, in producing this “setter” Go code the code generator assumes that the data types (Go types) in the source (the Output shape’s member field) and target (the Spec or Status field) are the same.
There are some APIs, however, where the Go type of the field in the Create
Operation’s Input shape is actually different from the same-named field in the ReadOne
Operation’s Output shape. A good example of this is the Lambda CreateFunction
API call, which has a Code
member of its Input shape that looks like this:
"Code": {
"ImageUri": "string",
"S3Bucket": "string",
"S3Key": "string",
"S3ObjectVersion": "string",
"ZipFile": blob
},
The GetFunction
API call’s Output shape has a same-named field called Code
in it, but this field looks like this:
"Code": {
"ImageUri": "string",
"Location": "string",
"RepositoryType": "string",
"ResolvedImageUri": "string"
},
This presents a conundrum to the ACK code generator, which, as noted above, assumes the data types of same-named fields in the Create
Operation’s Input shape and ReadOne
Operation’s Output shape are the same.
Use the resources[$resource].fields[$field].from
configuration option to handle these situations.
For the Lambda Function
Resource’s Code
field, we can inform the code generator to create three new Status
fields (read-only) from the Location
, RepositoryType
and ResolvedImageUri
fields in the Code
member of the ReadOne
Operation’s Output shape:
resources:
Function:
fields:
CodeLocation:
is_read_only: true
from:
operation: GetFunction
path: Code.Location
CodeRepositoryType:
is_read_only: true
from:
operation: GetFunction
path: Code.RepositoryType
CodeRegisteredImageURI:
is_read_only: true
from:
operation: GetFunction
path: Code.RegisteredImageUri
NOTE on maintainability: Another way of solving this particular problem would be to use a completely new custom field. However, we only use this as a last resort. The reason why we prefer to use the from:
configuration option is because this approach will adapt over time with changes to the AWS service API model, including documentation about those fields, whereas completely new custom fields will always need to be hand-rolled and no API documentation will be auto-generated for them.
print
: Controlling a field’s output as printer columns in kubectl get
If we want to add one of a Resource’s fields to the output of the kubectl get
command, we can do so by annotating that field’s configuration with a print:
section. An example of this is in the EC2 controller’s ElasticIPAddress
Resource, for which we would like to include the PublicIP
field in the output of kubectl get
:
resources:
...
ElasticIPAddress:
...
fields:
...
> PublicIp:
> print:
> name: PUBLIC-IP
Including this in the field’s configuration will cause the code generator to produce kubebuilder
markers in the appropriate place in its generated code, which will result in the field being included in the kubectl get
output.
NOTE this configuration is used to include printer columns in the output at the level of individual fields. One can also create additional printer columns at the level of Resources.
late_initialize
: Late initialization of a field
Late initialization of a field is a Kubernetes Resource Model concept that allows for a nil-valued field to be defaulted to some value after the resource has been successfully created. This is akin to a database table field’s “default value”.
Late initialized fields are slightly awkward for an ACK controller to handle, primarily because late initialized fields end up being erroneously identified as having different values in the Delta comparisons. The desired state of the field is nil
but the server-side default value of that field is some non-nil
value.
ACK’s code generator can output Go code that handles this server-side defaulting behaviour (we call this “late initialization”). To instruct the code generator to generate late initialization code for a field, use the resources[$resource].fields[$field].late_initialize
configuration option.
A good example of late initialization can be found in the RDS controller. For DBInstance
resources, if the user does not specify an availability zone when creating the DBInstance
, RDS chooses one for the user. To ensure that the RDS controller understands that the AvailabilityZone
field is set to a default value after creation, the following configuration is set in the generator.yaml
:
resources:
DBInstance:
fields:
AvailabilityZone:
> late_initialize: {}
NOTE: the late_initialize:
configuration option is currently a struct with a couple member fields that are not yet implemented (as of Dec 2022), which is why you need to use the {}
notation.
references
: Making a field refer to another Resource
One custom resource can refer to another custom resource using something called Resource References. The Go code that handles the validation and resolution of Resource References is generated by ack-generate
.
Use the resources[$resource].fields[$field].references
configuration option to inform the code generator what kind of Resource a field references and which field within that Resource is the identifier field.
Here is an example from the API Gateway v2 controller that shows an Integration Resource referencing an API and a VPCLink Resource:
resources:
Integration:
fields:
ApiId:
references:
resource: API
path: Status.APIID
ConnectionId:
references:
resource: VPCLink
path: Status.VPCLinkID
After regenerating the controller, the Integration resource will have two new fields, one called APIRef
and another called ConnectionRef
. The Go type of these fields will be a pointer to an AWSResourceReferenceWrapper
:
type IntegrationSpec struct {
APIID *string `json:"apiID,omitempty"`
APIRef *ackv1alpha1.AWSResourceReferenceWrapper `json:"apiRef,omitempty"`
ConnectionID *string `json:"connectionID,omitempty"`
ConnectionRef *ackv1alpha1.AWSResourceReferenceWrapper `json:"connectionRef,omitempty"`
...
}
NOTE: The generated name of the reference fields will always be the field name, stripped of any “Id”, “Name”, or “ARN” suffix, plus “Ref”.
In the above example, both the API and VPCLink Resources are managed by the API Gateway v2 controller. It is possible to reference Resources that are managed by a different ACK controller by specifying the resources[$resource].fields[$field].references.service_name
configuration option, as shown in this example from the RDS controller, which has the DBCluster resource reference the KMS controller’s Key resource:
resources:
DBCluster:
fields:
KmsKeyId:
references:
resource: Key
service_name: kms
path: Status.ACKResourceMetadata.ARN
Adding custom fields
resources[$resource].fields[$field].type
overrides the inferred Go type of a field. This is required for custom fields that are not inferred (either as a Create
Input/Output shape or via the SourceFieldConfig
attribute).
As an example, assume you have a Role
Resource where you want to add a custom Spec
field called Policies
that is a slice of string pointers.
The generator.yaml
file for the IAM controller looks like this:
resources:
Role:
fields:
Policies:
type: []*string
There is no Policies
field in either the CreateRole
Input or Output shapes, therefore in order to create a new custom field, we simply add a Policies
object in the fields
configuration mapping and tell the code generator what Go type this new field will have – in this case, []*string
.
NOTE on maintainability: Use custom fields as a last resort! When you use custom fields, you will not get the benefit of auto-generated documentation for your field like you will with auto-inferred or from:
-configured fields. You will be required to use custom code hooks to populate and set any custom fields.
NOTE: (DEPRECATED) This can also be accomplished by using custom_field:
, however we intend to move away from this approach.
Relevant TODO
for combining CustomShape stuff into type:
override
Custom code hook points
The code generator will generate Go code that implements the aws-sdk-go
SDK “binding” calls. Sometimes you will want to inject bits of custom code at various points in the code generation pipeline.
Custom code hook points do this injection. They should be preferred versus using complete overrides (e.g., resources[$resource].update_operation.custom_method_name
). The reason that custom code hooks are preferred is because you generally want to maximize the amount of generated code and minimize the amount of hand-written code in each controller. [NOTE(jljaco): decide later whether to bother documenting complete overrides via update_operation.custom_method_name
]
The sdk.go
hook points
First, some background. Within the pkg/resources/$resource/sdk.go
file, there are 4 primary resource manager methods that control CRUD operations on a resource:
sdkFind
reads a single resource record from a backend AWS service API, then populates a custom resource representation of that record and returns it back to the reconciler.sdkCreate
takes the desired custom resource state (in theSpec
struct of the CR). It calls AWS service APIs to create the resource in AWS, then sets certain fields on the custom resource’sStatus
struct that represent the latest observed state of that resource.sdkUpdate
takes the desired custom resource state (from theSpec
struct of the CR), the latest observed resource state, and a representation of the differences between those (called aDelta
struct). It calls one or more AWS service APIs to modify a resource’s attributes, then populates the custom resource’sStatus
struct with the latest (post-modification) observed state of the resource.sdkDelete
calls one or more AWS service APIs to delete a resource.
For all 4 of these main ResourceManager methods, there is a consistent code path that looks like this:
- Construct the SDK Input shape. For
sdkFind
andsdkDelete
, this Input shape will contain the identifier of the resource (e.g. anARN
). ForsdkCreate
andsdkUpdate
, this Input shape will also contain various desired state fields for the resource. This is called the “Set SDK” stage and corresponds to code generator functions in code-generator’spkg/generate/code/set_sdk.go
. - Pass the SDK Input shape to the
aws-sdk-go
API method.- For
sdkFind
, this API method will be either theReadOne
operation for the resource (e.g., ECR’sGetRepository
or RDS’sDescribeDBInstance
) or theReadMany
operation (e.g., S3’sListBuckets
or EC2’sDescribeInstances
). - For the
sdkCreate
,sdkUpdate
andsdkDelete
methods, the API operation will correspond to theCreate
,Update
andDelete
operation types.
- For
- Process the error/return code from the API call. If there is an error that appears in the list of Terminal codes (TODO(jljaco) link to docs), then the custom resource will have a Terminal condition applied to it, and a Terminal error is returned to the reconciler. The reconciler will subsequently add a
ACK.Terminal
Condition to the custom resource. - Process the Output shape from the API call. If no error was returned from the API call, the Output shape representing the HTTP response content will then be processed, resulting in fields in either the
Spec
orStatus
of the custom resource being set to the value of matching fields on the Output shape. This is called the “Set Resource” stage and corresponds to code generator functions in code-generator’spkg/generate/code/set_resource.go
.
Along with the above 4 main ResourceManager methods, there are a number of generated helper methods and functions that will:
- create the SDK input shape used when making HTTP requests to AWS APIs
- process responses from those AWS APIs
sdk_*_pre_build_request
: before validation and construction of Input shape
The sdk_*_pre_build_request
hooks are called before the call to construct the Input shape that is used in the API operation and therefore before any call to validate that Input shape.
Use this custom hook point if you want to short-circuit the processing of the resource for some reason OR if you want to process certain resource fields (e.g., Tags) separately from the main resource fields.
Example: Short-circuiting
Here is an example from the DynamoDB controller’s generator.yaml
file that uses a pre_build_request
custom code hook for Table resources:
resources:
Table:
hooks:
sdk_delete_pre_build_request:
template_path: hooks/table/sdk_delete_pre_build_request.go.tpl
As you can see, the hook is for the Delete operation. You can specify the filepath to a template which contains Go code that you wish to inject at this custom hook point. Here is the Go code from that template:
if isTableDeleting(r) {
return nil, requeueWaitWhileDeleting
}
if isTableUpdating(r) {
return nil, requeueWaitWhileUpdating
}
The snippet of Go code above simply requeues the resource to be deleted in the future if the Table is currently either being updated (via UpdateTable
) or deleted (via DeleteTable
).
After running make build-controller
for DynamoDB, the above generator.yaml
configuration and corresponding template file produces the following Go code implementation for sdkDelete
inside of the sdk.go
file for Table resources:
// sdkDelete deletes the supplied resource in the backend AWS service API
func (rm *resourceManager) sdkDelete(
ctx context.Context,
r *resource,
) (latest *resource, err error) {
rlog := ackrtlog.FromContext(ctx)
exit := rlog.Trace("rm.sdkDelete")
defer func() {
exit(err)
}()
> if isTableDeleting(r) {
> return nil, requeueWaitWhileDeleting
> }
> if isTableUpdating(r) {
> return nil, requeueWaitWhileUpdating
> }
input, err := rm.newDeleteRequestPayload(r)
if err != nil {
return nil, err
}
var resp *svcsdk.DeleteTableOutput
_ = resp
resp, err = rm.sdkapi.DeleteTableWithContext(ctx, input)
rm.metrics.RecordAPICall("DELETE", "DeleteTable", err)
return nil, err
}
In the example above, we’ve highlighted the lines (with >
) that were injected into the sdkDelete
method using this custom hook point.
Example: Custom field processing
Another example of a pre_build_request
custom hook comes from the IAM controller’s Role resource and this generator.yaml
snippet:
resources:
Role:
hooks:
sdk_update_pre_build_request:
template_path: hooks/role/sdk_update_pre_build_request.go.tpl
which has the following Go code in the template file:
if delta.DifferentAt("Spec.Policies") {
err = rm.syncPolicies(ctx, desired, latest)
if err != nil {
return nil, err
}
}
if delta.DifferentAt("Spec.Tags") {
err = rm.syncTags(ctx, desired, latest)
if err != nil {
return nil, err
}
}
if delta.DifferentAt("Spec.PermissionsBoundary") {
err = rm.syncRolePermissionsBoundary(ctx, desired)
if err != nil {
return nil, err
}
}
if !delta.DifferentExcept("Spec.Tags", "Spec.Policies", "Spec.PermissionsBoundary") {
return desired, nil
}
What you can see above is the use of the pre_build_request
hook point to update the Role’s policy collection, tag collection, and permissions boundary before calling the UpdateRole
API call. The reason for this is because a Role’s policies, tags, and permissions boundary are set using a different set of AWS API calls.
TOP TIP (1): Note the use of
delta.DifferentAt()
in the code above. This is the recommended best practice for determining whether a particular field at a supplied field path has diverged between the desired and latest observed resource state.
sdk_*_post_build_request
: after construction of Input shape
The post_build_request
hooks are called AFTER the call to construct the Input shape but before the API operation.
Use this custom hook point if you want to add custom validation of the Input shape.
Here’s an example of a post_build_request
custom hook point from the RDS controller’s DBInstance resource:
resources:
DBInstance:
hooks:
sdk_update_post_build_request:
template_path: hooks/db_instance/sdk_update_post_build_request.go.tpl
and here’s the Go code in that template:
// ModifyDBInstance call will return ValidationError when the
// ModifyDBInstanceRequest contains the same DBSubnetGroupName
// as the DBInstance. So, if there is no delta between
// desired and latest for Spec.DBSubnetGroupName, exclude it
// from ModifyDBInstanceRequest
if !delta.DifferentAt("Spec.DBSubnetGroupName") {
input.DBSubnetGroupName = nil
}
// RDS will not compare diff value and accept any modify db call
// for below values, MonitoringInterval, CACertificateIdentifier
// and user master password, NetworkType
// hence if there is no delta between desired
// and latest, exclude it from ModifyDBInstanceRequest
if !delta.DifferentAt("Spec.MonitoringInterval") {
input.MonitoringInterval = nil
}
if !delta.DifferentAt("Spec.CACertificateIdentifier") {
input.CACertificateIdentifier = nil
}
if !delta.DifferentAt("Spec.MasterUserPassword.Name") {
input.MasterUserPassword = nil
}
if !delta.DifferentAt("Spec.NetworkType") {
input.NetworkType = nil
}
// For dbInstance inside dbCluster, it's either aurora or
// multi-az cluster case, in either case, the below params
// are not controlled in instance level.
// hence when DBClusterIdentifier appear, set them to nil
// Please refer to doc : https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DeleteDBInstance.html
if desired.ko.Spec.DBClusterIdentifier != nil {
input.AllocatedStorage = nil
input.BackupRetentionPeriod = nil
input.PreferredBackupWindow = nil
input.DeletionProtection = nil
}
As you can see, we add some custom validation and normalization of the Input shape for a DBInstance before calling the ModifyDBInstance
API call.
TOP TIP (2): Note the verbose usage of nil-checks. This is very important.
aws-sdk-go
does not have automatic protection againstnil
pointer dereferencing. All fields in anaws-sdk-go
shape are pointer types. This means you should always do your own nil-checks when dereferencing any field in any shape.
sdk_*_post_request
: after the API operation
The post_request
hooks are called IMMEDIATELY AFTER the API operation aws-sdk-go
client call. These hooks will have access to a Go variable named resp
that refers to the aws-sdk-go
client response and a Go variable named respErr
that refers to any error returned from the aws-sdk-go
client call.
sdk_*_pre_set_output
: before validation of Output shape
The pre_set_output
hooks are called BEFORE the code that processes the Output shape (the pkg/generate/code.SetOutput
function). These hooks will have access to a Go variable named ko
that represents the concrete Kubernetes CR object that will be returned from the main method (sdkFind
, sdkCreate
, etc). This ko
variable will have been defined immediately before the pre_set_output
hooks as a copy of the resource that is supplied to the main method, like so:
// Merge in the information we read from the API call above to the copy of
// the original Kubernetes object we passed to the function
ko := r.ko.DeepCopy()
sdk_*_post_set_output
: after merging data from API response & k8s object
The post_set_output
hooks are called AFTER the the information from the API call is merged with the copy of the original Kubernetes object. These hooks will have access to the updated Kubernetes object ko
, the response of the API call (and the original Kubernetes CR object if it’s sdkUpdate
).
sdk_file_end
: miscellaneous catch-all
The sdk_file_end
is a generic hook point that occurs outside the scope of any specific AWSResourceManager
method and can be used to place commonly-generated code inside the sdk.go
file.
NOTE(jaypipes): This is the weirdest of the hooks… need to cleanly explain this!
The comparison hook points
delta_pre_compare
: before comparing resources
The delta_pre_compare
hooks are called before the generated code that compares two resources.
NOTE: If you specified that a particular field should have its comparison code ignored, you almost always will want to use a delta_pre_compare
hook to handle the comparison logic for that field. See the example above in the section on “Marking a field as ignored” for an illustration of this.
The canonical example of when and how to use this custom code hook point is for handling the correct comparison of slices of Tag structs.
By default, if the code generator does not know how to generate specialized comparison code for a Go type, it will generate a call to reflect.DeepEqual
for this comparison. However, for some types (e.g., lists-of-structs), reflect.DeepEqual
will return true
even when the only difference between two lists lies in the order by which the structs are sorted. This sort order needs to be ignored in order that the comparison logic properly returns false
for lists-of-structs that are identical regardless of sort order.
The IAM controller’s generator.yaml
file contains this snippet:
Role:
hooks:
delta_pre_compare:
code: compareTags(delta, a, b)
fields:
Tags:
compare:
is_ignored: true
and the delta_pre_compare
hook code is an inline Go code function call to compareTags
. This function is defined in the hooks.go
file for the Role resource and looks like this:
// compareTags is a custom comparison function for comparing lists of Tag
// structs where the order of the structs in the list is not important.
func compareTags(
delta *ackcompare.Delta,
a *resource,
b *resource,
) {
if len(a.ko.Spec.Tags) != len(b.ko.Spec.Tags) {
delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
} else if len(a.ko.Spec.Tags) > 0 {
if !commonutil.EqualTags(a.ko.Spec.Tags, b.ko.Spec.Tags) {
delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
}
}
}
commonutil.EqualTags
properly handles the comparison of lists of Tag structs.
delta_post_compare
: after comparing resources
The delta_post_compare
hooks are called after the generated code that compares two resources.
This hook is not commonly used, since the delta_pre_compare
custom code hook point is generally used to inject custom code for comparing special fields.
However, the delta_post_compare
hook point can be useful if you want to add some code that can post-process the Delta struct after all fields have been compared. For example, if you wanted to output some debugging information about the comparison operations.
The late initialization hook points
late_initialize_pre_read_one
: before the readOne
The late_initialize_pre_read_one
hooks are called before making the readOne
call inside the AWSResourceManager.LateInitialize()
method.
TODO
late_initialize_post_read_one
: after the readOne
The late_initialize_post_read_one
hooks are called after making the readOne
call inside the AWSResourceManager.LateInitialize()
method.
TODO
The reference hook points
references_pre_resolve
: before resolving references
The references_pre_resolve
hook is called before resolving the references for all Reference fields inside the AWSResourceManager.ResolveReferences()
method.
TODO
references_post_resolve
: after resolving references
The references_post_resolve
hook is called after resolving the references for all Reference fields inside the AWSResourceManager.ResolveReferences()
method.
TODO
The tags hook points
ensure_tags
: custom EnsureTags
method
The ensure_tags
hook provides a complete custom implementation for the AWSResourceManager.EnsureTags()
method.
TODO
convert_tags
: custom ToACKTags
and FromACKTags
methods
The convert_tags
hook provides a complete custom implementation for the ToACKTags
and FromACKTags
methods.
TODO
convert_tags_pre_to_ack_tags
: before converting k8s tags to ACK tags
The convert_tags_pre_to_ack_tags
hooks are called before converting the K8s resource tags into ACK tags.
TODO
convert_tags_post_to_ack_tags
: after converting k8s tags to ACK tags
The convert_tags_post_to_ack_tags
hooks are called after converting the K8s resource tags into ACK tags.
TODO
convert_tags_pre_from_ack_tags
: before converting ACK tags to k8s tags
The convert_tags_pre_from_ack_tags
hooks are called before converting the ACK tags into K8s resource tags.
TODO
convert_tags_post_from_ack_tags
: after converting ACK tags to k8s tags
The convert_tags_post_from_ack_tags
hooks are called after converting the ACK tags into K8s resource tags.
TODO
Attribute-based APIs
OMG TODO.