Skip to content

Commit

Permalink
feature: Generate Code for resource adoption by annotation (#558)
Browse files Browse the repository at this point in the history
Issue #, if available:

Description of changes:
These changes introduce a new generated function in all controllers 
that attempts to populate the resource spec/status with fields defined 
by the user. 

This change will require for developers to add hooks for this function to 
all controllers that already have a `SetResourceIdentifiers` hook.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • Loading branch information
michaelhtm authored Dec 2, 2024
1 parent 9715a2a commit 8f0f520
Show file tree
Hide file tree
Showing 6 changed files with 418 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.22.4

require (
github.com/aws-controllers-k8s/pkg v0.0.15
github.com/aws-controllers-k8s/runtime v0.39.0
github.com/aws-controllers-k8s/runtime v0.39.1-0.20241202082353-a6b0014a8130
github.com/aws/aws-sdk-go v1.49.0
github.com/dlclark/regexp2 v1.10.0 // indirect
// pin to v0.1.1 due to release problem with v0.1.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ github.com/aws-controllers-k8s/pkg v0.0.15 h1:C1pnD/aDqJsU9oYf5upHkpSc+Hv4JQVtkd
github.com/aws-controllers-k8s/pkg v0.0.15/go.mod h1:VvdjLWmR6IJ3KU8KByKiq/lJE8M+ur2piXysXKTGUS0=
github.com/aws-controllers-k8s/runtime v0.39.0 h1:IgOXluSzvb4UcDr9eU7SPw5MJnL7kt5R6DuF5Qu9zVQ=
github.com/aws-controllers-k8s/runtime v0.39.0/go.mod h1:G07g26y1cxyZO6Ngp+LwXf03CqFyLNL7os4Py4IdyGY=
github.com/aws-controllers-k8s/runtime v0.39.1-0.20241202082353-a6b0014a8130 h1:EoXYRrpBX2hi5B1IawKr2LJTsVsreHsJdxULLlMNO9U=
github.com/aws-controllers-k8s/runtime v0.39.1-0.20241202082353-a6b0014a8130/go.mod h1:G07g26y1cxyZO6Ngp+LwXf03CqFyLNL7os4Py4IdyGY=
github.com/aws/aws-sdk-go v1.49.0 h1:g9BkW1fo9GqKfwg2+zCD+TW/D36Ux+vtfJ8guF4AYmY=
github.com/aws/aws-sdk-go v1.49.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
Expand Down
3 changes: 3 additions & 0 deletions pkg/generate/ack/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ var (
"GoCodeSetResourceIdentifiers": func(r *ackmodel.CRD, sourceVarName string, targetVarName string, indentLevel int) string {
return code.SetResourceIdentifiers(r.Config(), r, sourceVarName, targetVarName, indentLevel)
},
"GoCodePopulateResourceFromAnnotation": func(r *ackmodel.CRD, sourceVarName string, targetVarName string, indentLevel int) string {
return code.PopulateResourceFromAnnotation(r.Config(), r, sourceVarName, targetVarName, indentLevel)
},
"GoCodeFindLateInitializedFieldNames": func(r *ackmodel.CRD, resVarName string, indentLevel int) string {
return code.FindLateInitializedFieldNames(r.Config(), r, resVarName, indentLevel)
},
Expand Down
327 changes: 327 additions & 0 deletions pkg/generate/code/set_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,30 @@ func identifierNameOrIDGuardConstructor(
return out
}

// requiredFieldGuardContructor returns Go code checking if user provided
// the required field for a read, or returning an error here
// and returns a `MissingNameIdentifier` error:
//
// if fields[${requiredField}] == "" {
// return ackerrors.MissingNameIdentifier
// }
func requiredFieldGuardContructor(
// String representing the fields map that contains the required
// fields for adoption
sourceVarName string,
// String representing the name of the required field
requiredField string,
// Number of levels of indentation to use
indentLevel int,
) string {
indent := strings.Repeat("\t", indentLevel)
out := fmt.Sprintf("%stmp, ok := %s[\"%s\"]\n", indent, sourceVarName, requiredField)
out += fmt.Sprintf("%sif !ok {\n", indent)
out += fmt.Sprintf("%s\treturn ackerrors.MissingNameIdentifier\n", indent)
out += fmt.Sprintf("%s}\n", indent)
return out
}

// SetResourceGetAttributes returns the Go code that sets the Status fields
// from the Output shape returned from a resource's GetAttributes operation.
//
Expand Down Expand Up @@ -1101,6 +1125,243 @@ func SetResourceIdentifiers(
return primaryKeyConditionalOut + primaryKeyOut + additionalKeyOut
}

// PopulateResourceFromAnnotation returns the Go code that sets an empty CR object with
// Spec and Status field values that correspond to the primary identifier (be
// that an ARN, ID or Name) and any other "additional keys" required for the AWS
// service to uniquely identify the object.
//
// The method will attempt to look for the field denoted with a value of true
// for `is_primary_key`, or will use the ARN if the resource has a value of true
// for `is_arn_primary_key`. Otherwise, the method will attempt to use the
// `ReadOne` operation, if present, falling back to using `ReadMany`.
// If it detects the operation uses an ARN to identify the resource it will read
// it from the metadata status field. Otherwise it will use any field with a
// name that matches the primary identifier from the operation, pulling from
// top-level spec or status fields.
//
// An example of code with no additional keys:
//
// ```
// tmp, ok := field["brokerID"]
// if !ok {
// return ackerrors.MissingNameIdentifier
// }
// r.ko.Status.BrokerID = &tmp
//
// ```
//
// An example of code with additional keys:
//
// ```
//
// tmp, ok := field["resourceID"]
// if !ok {
// return ackerrors.MissingNameIdentifier
// }
//
// r.ko.Spec.ResourceID = &tmp
//
// f0, f0ok := fields["scalableDimension"]
//
// if f0ok {
// r.ko.Spec.ScalableDimension = &f0
// }
//
// f1, f1ok := fields["serviceNamespace"]
//
// if f1ok {
// r.ko.Spec.ServiceNamespace = &f1
// }
//
// ```
// An example of code that uses the ARN:
//
// ```
// tmpArn, ok := field["arn"]
// if !ok {
// return ackerrors.MissingNameIdentifier
// }
// if r.ko.Status.ACKResourceMetadata == nil {
// r.ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{}
// }
// arn := ackv1alpha1.AWSResourceName(tmp)
//
// r.ko.Status.ACKResourceMetadata.ARN = &arn
//
// f0, f0ok := fields["modelPackageName"]
//
// if f0ok {
// r.ko.Spec.ModelPackageName = &f0
// }
//
// ```
func PopulateResourceFromAnnotation(
cfg *ackgenconfig.Config,
r *model.CRD,
// String representing the name of the variable that we will grab the Input
// shape from. This will likely be "fields" since in the templates that
// call this method, the "source variable" is the CRD struct which is used
// to populate the target variable, which is the struct of unique
// identifiers
sourceVarName string,
// String representing the name of the variable that we will be **setting**
// with values we get from the Output shape. This will likely be
// "r.ko" since that is the name of the "target variable" that the
// templates that call this method use for the Input shape.
targetVarName string,
// Number of levels of indentation to use
indentLevel int,
) string {
op := r.Ops.ReadOne
if op == nil {
switch {
case r.Ops.GetAttributes != nil:
// If single lookups can only be done with GetAttributes
op = r.Ops.GetAttributes
case r.Ops.ReadMany != nil:
// If single lookups can only be done using ReadMany
op = r.Ops.ReadMany
default:
return ""
}
}
inputShape := op.InputRef.Shape
if inputShape == nil {
return ""
}

primaryKeyOut := ""
additionalKeyOut := "\n"

indent := strings.Repeat("\t", indentLevel)
arnOut := "\n"
out := "\n"
// Check if the CRD defines the primary keys
primaryKeyConditionalOut := "\n"
primaryKeyConditionalOut += requiredFieldGuardContructor(sourceVarName, "arn", indentLevel)
arnOut += ackResourceMetadataGuardConstructor(fmt.Sprintf("%s.Status", targetVarName), indentLevel)
arnOut += fmt.Sprintf(
"%sarn := ackv1alpha1.AWSResourceName(tmp)\n",
indent,
)
arnOut += fmt.Sprintf(
"%s%s.Status.ACKResourceMetadata.ARN = &arn\n",
indent, targetVarName,
)
if r.IsARNPrimaryKey() {
return primaryKeyConditionalOut + arnOut
}
primaryField, err := r.GetPrimaryKeyField()
if err != nil {
panic(err)
}

var primaryCRField, primaryShapeField string
isPrimarySet := primaryField != nil
if isPrimarySet {
memberPath, _ := findFieldInCR(cfg, r, primaryField.Names.Original)
primaryKeyOut += requiredFieldGuardContructor(sourceVarName, primaryField.Names.CamelLower, indentLevel)
targetVarPath := fmt.Sprintf("%s%s", targetVarName, memberPath)
primaryKeyOut += setResourceIdentifierPrimaryIdentifierAnn(cfg, r,
primaryField,
targetVarPath,
sourceVarName,
indentLevel,
)
} else {
primaryCRField, primaryShapeField = FindPrimaryIdentifierFieldNames(cfg, r, op)
if primaryShapeField == PrimaryIdentifierARNOverride {
return primaryKeyConditionalOut + arnOut
}
}

paginatorFieldLookup := []string{
"NextToken",
"MaxResults",
}


for memberIndex, memberName := range inputShape.MemberNames() {
if util.InStrings(memberName, paginatorFieldLookup) {
continue
}

inputShapeRef := inputShape.MemberRefs[memberName]
inputMemberShape := inputShapeRef.Shape

// Only strings and list of strings are currently accepted as valid
// inputs for additional key fields
if inputMemberShape.Type != "string" &&
(inputMemberShape.Type != "list" ||
inputMemberShape.MemberRef.Shape.Type != "string") {
continue
}

if r.IsSecretField(memberName) {
// Secrets cannot be used as fields in identifiers
continue
}

if r.IsPrimaryARNField(memberName) {
continue
}

// Handles field renames, if applicable
fieldName := cfg.GetResourceFieldName(
r.Names.Original,
op.ExportedName,
memberName,
)

// Check to see if we've already set the field as the primary identifier
if isPrimarySet && fieldName == primaryField.Names.Camel {
continue
}

isPrimaryIdentifier := fieldName == primaryShapeField

searchField := ""
if isPrimaryIdentifier {
searchField = primaryCRField
} else {
searchField = fieldName
}

memberPath, targetField := findFieldInCR(cfg, r, searchField)
if targetField == nil || (isPrimarySet && targetField == primaryField) {
continue
}

switch targetField.ShapeRef.Shape.Type {
case "list", "structure", "map":
panic("primary identifier '" + targetField.Path + "' must be a scalar type since NameOrID is a string")
default:
break
}

targetVarPath := fmt.Sprintf("%s%s", targetVarName, memberPath)
if isPrimaryIdentifier {
primaryKeyOut += requiredFieldGuardContructor(sourceVarName, targetField.Names.CamelLower, indentLevel)
primaryKeyOut += setResourceIdentifierPrimaryIdentifierAnn(cfg, r,
targetField,
targetVarPath,
sourceVarName,
indentLevel)
} else {
additionalKeyOut += setResourceIdentifierAdditionalKeyAnn(
cfg, r,
memberIndex,
targetField,
targetVarPath,
sourceVarName,
names.New(fieldName).CamelLower,
indentLevel)
}
}

return out + primaryKeyOut + additionalKeyOut
}

// findFieldInCR will search for a given field, by its name, in a CR and returns
// the member path and Field type if one is found.
func findFieldInCR(
Expand Down Expand Up @@ -1152,6 +1413,34 @@ func setResourceIdentifierPrimaryIdentifier(
)
}

// AnotherOne returns a string of Go code that sets
// the primary identifier Spec or Status field on a given resource to the value
// in the identifier `NameOrID` field:
//
// r.ko.Status.BrokerID = &identifier.NameOrID
func setResourceIdentifierPrimaryIdentifierAnn(
cfg *ackgenconfig.Config,
r *model.CRD,
// The field that will be set on the target variable
targetField *model.Field,
// The variable name that we want to set a value to
targetVarName string,
// The struct or struct field that we access our source value from
sourceVarName string,
// Number of levels of indentation to use
indentLevel int,
) string {
adaptedMemberPath := fmt.Sprintf("&tmp")
qualifiedTargetVar := fmt.Sprintf("%s.%s", targetVarName, targetField.Path)

return setResourceForScalar(
qualifiedTargetVar,
adaptedMemberPath,
targetField.ShapeRef,
indentLevel,
)
}

// setResourceIdentifierAdditionalKey returns a string of Go code that sets a
// Spec or Status field on a given resource to the value in the identifier's
// `AdditionalKeys` mapping:
Expand Down Expand Up @@ -1199,6 +1488,44 @@ func setResourceIdentifierAdditionalKey(
return additionalKeyOut
}

func setResourceIdentifierAdditionalKeyAnn(
cfg *ackgenconfig.Config,
r *model.CRD,
fieldIndex int,
// The field that will be set on the target variable
targetField *model.Field,
// The variable name that we want to set a value to
targetVarName string,
// The struct or struct field that we access our source value from
sourceVarName string,
// The key in the `AdditionalKeys` map storing the source variable
sourceVarKey string,
// Number of levels of indentation to use
indentLevel int,
) string {
indent := strings.Repeat("\t", indentLevel)

additionalKeyOut := ""

fieldIndexName := fmt.Sprintf("f%d", fieldIndex)
sourceAdaptedVarName := fmt.Sprintf("%s[\"%s\"]", sourceVarName, sourceVarKey)

// TODO(RedbackThomson): If the identifiers don't exist, we should be
// throwing an error accessible to the user
additionalKeyOut += fmt.Sprintf("%s%s, %sok := %s\n", indent, fieldIndexName, fieldIndexName, sourceAdaptedVarName)
additionalKeyOut += fmt.Sprintf("%sif %sok {\n", indent, fieldIndexName)
qualifiedTargetVar := fmt.Sprintf("%s.%s", targetVarName, targetField.Path)
additionalKeyOut += setResourceForScalar(
qualifiedTargetVar,
fmt.Sprintf("&%s", fieldIndexName),
targetField.ShapeRef,
indentLevel+1,
)
additionalKeyOut += fmt.Sprintf("%s}\n", indent)

return additionalKeyOut
}

// setResourceForContainer returns a string of Go code that sets the value of a
// target variable to that of a source variable. When the source variable type
// is a map, struct or slice type, then this function is called recursively on
Expand Down
Loading

0 comments on commit 8f0f520

Please sign in to comment.