Skip to content

feature: Generate Code for resource adoption by annotation #558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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