Skip to content

Commit 8f0f520

Browse files
authoredDec 2, 2024
feature: Generate Code for resource adoption by annotation (#558)
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.
1 parent 9715a2a commit 8f0f520

File tree

6 files changed

+418
-1
lines changed

6 files changed

+418
-1
lines changed
 

‎go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ toolchain go1.22.4
66

77
require (
88
github.com/aws-controllers-k8s/pkg v0.0.15
9-
github.com/aws-controllers-k8s/runtime v0.39.0
9+
github.com/aws-controllers-k8s/runtime v0.39.1-0.20241202082353-a6b0014a8130
1010
github.com/aws/aws-sdk-go v1.49.0
1111
github.com/dlclark/regexp2 v1.10.0 // indirect
1212
// pin to v0.1.1 due to release problem with v0.1.2

‎go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ github.com/aws-controllers-k8s/pkg v0.0.15 h1:C1pnD/aDqJsU9oYf5upHkpSc+Hv4JQVtkd
7575
github.com/aws-controllers-k8s/pkg v0.0.15/go.mod h1:VvdjLWmR6IJ3KU8KByKiq/lJE8M+ur2piXysXKTGUS0=
7676
github.com/aws-controllers-k8s/runtime v0.39.0 h1:IgOXluSzvb4UcDr9eU7SPw5MJnL7kt5R6DuF5Qu9zVQ=
7777
github.com/aws-controllers-k8s/runtime v0.39.0/go.mod h1:G07g26y1cxyZO6Ngp+LwXf03CqFyLNL7os4Py4IdyGY=
78+
github.com/aws-controllers-k8s/runtime v0.39.1-0.20241202082353-a6b0014a8130 h1:EoXYRrpBX2hi5B1IawKr2LJTsVsreHsJdxULLlMNO9U=
79+
github.com/aws-controllers-k8s/runtime v0.39.1-0.20241202082353-a6b0014a8130/go.mod h1:G07g26y1cxyZO6Ngp+LwXf03CqFyLNL7os4Py4IdyGY=
7880
github.com/aws/aws-sdk-go v1.49.0 h1:g9BkW1fo9GqKfwg2+zCD+TW/D36Ux+vtfJ8guF4AYmY=
7981
github.com/aws/aws-sdk-go v1.49.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
8082
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=

‎pkg/generate/ack/controller.go

+3
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ var (
167167
"GoCodeSetResourceIdentifiers": func(r *ackmodel.CRD, sourceVarName string, targetVarName string, indentLevel int) string {
168168
return code.SetResourceIdentifiers(r.Config(), r, sourceVarName, targetVarName, indentLevel)
169169
},
170+
"GoCodePopulateResourceFromAnnotation": func(r *ackmodel.CRD, sourceVarName string, targetVarName string, indentLevel int) string {
171+
return code.PopulateResourceFromAnnotation(r.Config(), r, sourceVarName, targetVarName, indentLevel)
172+
},
170173
"GoCodeFindLateInitializedFieldNames": func(r *ackmodel.CRD, resVarName string, indentLevel int) string {
171174
return code.FindLateInitializedFieldNames(r.Config(), r, resVarName, indentLevel)
172175
},

‎pkg/generate/code/set_resource.go

+327
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,30 @@ func identifierNameOrIDGuardConstructor(
751751
return out
752752
}
753753

754+
// requiredFieldGuardContructor returns Go code checking if user provided
755+
// the required field for a read, or returning an error here
756+
// and returns a `MissingNameIdentifier` error:
757+
//
758+
// if fields[${requiredField}] == "" {
759+
// return ackerrors.MissingNameIdentifier
760+
// }
761+
func requiredFieldGuardContructor(
762+
// String representing the fields map that contains the required
763+
// fields for adoption
764+
sourceVarName string,
765+
// String representing the name of the required field
766+
requiredField string,
767+
// Number of levels of indentation to use
768+
indentLevel int,
769+
) string {
770+
indent := strings.Repeat("\t", indentLevel)
771+
out := fmt.Sprintf("%stmp, ok := %s[\"%s\"]\n", indent, sourceVarName, requiredField)
772+
out += fmt.Sprintf("%sif !ok {\n", indent)
773+
out += fmt.Sprintf("%s\treturn ackerrors.MissingNameIdentifier\n", indent)
774+
out += fmt.Sprintf("%s}\n", indent)
775+
return out
776+
}
777+
754778
// SetResourceGetAttributes returns the Go code that sets the Status fields
755779
// from the Output shape returned from a resource's GetAttributes operation.
756780
//
@@ -1101,6 +1125,243 @@ func SetResourceIdentifiers(
11011125
return primaryKeyConditionalOut + primaryKeyOut + additionalKeyOut
11021126
}
11031127

1128+
// PopulateResourceFromAnnotation returns the Go code that sets an empty CR object with
1129+
// Spec and Status field values that correspond to the primary identifier (be
1130+
// that an ARN, ID or Name) and any other "additional keys" required for the AWS
1131+
// service to uniquely identify the object.
1132+
//
1133+
// The method will attempt to look for the field denoted with a value of true
1134+
// for `is_primary_key`, or will use the ARN if the resource has a value of true
1135+
// for `is_arn_primary_key`. Otherwise, the method will attempt to use the
1136+
// `ReadOne` operation, if present, falling back to using `ReadMany`.
1137+
// If it detects the operation uses an ARN to identify the resource it will read
1138+
// it from the metadata status field. Otherwise it will use any field with a
1139+
// name that matches the primary identifier from the operation, pulling from
1140+
// top-level spec or status fields.
1141+
//
1142+
// An example of code with no additional keys:
1143+
//
1144+
// ```
1145+
// tmp, ok := field["brokerID"]
1146+
// if !ok {
1147+
// return ackerrors.MissingNameIdentifier
1148+
// }
1149+
// r.ko.Status.BrokerID = &tmp
1150+
//
1151+
// ```
1152+
//
1153+
// An example of code with additional keys:
1154+
//
1155+
// ```
1156+
//
1157+
// tmp, ok := field["resourceID"]
1158+
// if !ok {
1159+
// return ackerrors.MissingNameIdentifier
1160+
// }
1161+
//
1162+
// r.ko.Spec.ResourceID = &tmp
1163+
//
1164+
// f0, f0ok := fields["scalableDimension"]
1165+
//
1166+
// if f0ok {
1167+
// r.ko.Spec.ScalableDimension = &f0
1168+
// }
1169+
//
1170+
// f1, f1ok := fields["serviceNamespace"]
1171+
//
1172+
// if f1ok {
1173+
// r.ko.Spec.ServiceNamespace = &f1
1174+
// }
1175+
//
1176+
// ```
1177+
// An example of code that uses the ARN:
1178+
//
1179+
// ```
1180+
// tmpArn, ok := field["arn"]
1181+
// if !ok {
1182+
// return ackerrors.MissingNameIdentifier
1183+
// }
1184+
// if r.ko.Status.ACKResourceMetadata == nil {
1185+
// r.ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{}
1186+
// }
1187+
// arn := ackv1alpha1.AWSResourceName(tmp)
1188+
//
1189+
// r.ko.Status.ACKResourceMetadata.ARN = &arn
1190+
//
1191+
// f0, f0ok := fields["modelPackageName"]
1192+
//
1193+
// if f0ok {
1194+
// r.ko.Spec.ModelPackageName = &f0
1195+
// }
1196+
//
1197+
// ```
1198+
func PopulateResourceFromAnnotation(
1199+
cfg *ackgenconfig.Config,
1200+
r *model.CRD,
1201+
// String representing the name of the variable that we will grab the Input
1202+
// shape from. This will likely be "fields" since in the templates that
1203+
// call this method, the "source variable" is the CRD struct which is used
1204+
// to populate the target variable, which is the struct of unique
1205+
// identifiers
1206+
sourceVarName string,
1207+
// String representing the name of the variable that we will be **setting**
1208+
// with values we get from the Output shape. This will likely be
1209+
// "r.ko" since that is the name of the "target variable" that the
1210+
// templates that call this method use for the Input shape.
1211+
targetVarName string,
1212+
// Number of levels of indentation to use
1213+
indentLevel int,
1214+
) string {
1215+
op := r.Ops.ReadOne
1216+
if op == nil {
1217+
switch {
1218+
case r.Ops.GetAttributes != nil:
1219+
// If single lookups can only be done with GetAttributes
1220+
op = r.Ops.GetAttributes
1221+
case r.Ops.ReadMany != nil:
1222+
// If single lookups can only be done using ReadMany
1223+
op = r.Ops.ReadMany
1224+
default:
1225+
return ""
1226+
}
1227+
}
1228+
inputShape := op.InputRef.Shape
1229+
if inputShape == nil {
1230+
return ""
1231+
}
1232+
1233+
primaryKeyOut := ""
1234+
additionalKeyOut := "\n"
1235+
1236+
indent := strings.Repeat("\t", indentLevel)
1237+
arnOut := "\n"
1238+
out := "\n"
1239+
// Check if the CRD defines the primary keys
1240+
primaryKeyConditionalOut := "\n"
1241+
primaryKeyConditionalOut += requiredFieldGuardContructor(sourceVarName, "arn", indentLevel)
1242+
arnOut += ackResourceMetadataGuardConstructor(fmt.Sprintf("%s.Status", targetVarName), indentLevel)
1243+
arnOut += fmt.Sprintf(
1244+
"%sarn := ackv1alpha1.AWSResourceName(tmp)\n",
1245+
indent,
1246+
)
1247+
arnOut += fmt.Sprintf(
1248+
"%s%s.Status.ACKResourceMetadata.ARN = &arn\n",
1249+
indent, targetVarName,
1250+
)
1251+
if r.IsARNPrimaryKey() {
1252+
return primaryKeyConditionalOut + arnOut
1253+
}
1254+
primaryField, err := r.GetPrimaryKeyField()
1255+
if err != nil {
1256+
panic(err)
1257+
}
1258+
1259+
var primaryCRField, primaryShapeField string
1260+
isPrimarySet := primaryField != nil
1261+
if isPrimarySet {
1262+
memberPath, _ := findFieldInCR(cfg, r, primaryField.Names.Original)
1263+
primaryKeyOut += requiredFieldGuardContructor(sourceVarName, primaryField.Names.CamelLower, indentLevel)
1264+
targetVarPath := fmt.Sprintf("%s%s", targetVarName, memberPath)
1265+
primaryKeyOut += setResourceIdentifierPrimaryIdentifierAnn(cfg, r,
1266+
primaryField,
1267+
targetVarPath,
1268+
sourceVarName,
1269+
indentLevel,
1270+
)
1271+
} else {
1272+
primaryCRField, primaryShapeField = FindPrimaryIdentifierFieldNames(cfg, r, op)
1273+
if primaryShapeField == PrimaryIdentifierARNOverride {
1274+
return primaryKeyConditionalOut + arnOut
1275+
}
1276+
}
1277+
1278+
paginatorFieldLookup := []string{
1279+
"NextToken",
1280+
"MaxResults",
1281+
}
1282+
1283+
1284+
for memberIndex, memberName := range inputShape.MemberNames() {
1285+
if util.InStrings(memberName, paginatorFieldLookup) {
1286+
continue
1287+
}
1288+
1289+
inputShapeRef := inputShape.MemberRefs[memberName]
1290+
inputMemberShape := inputShapeRef.Shape
1291+
1292+
// Only strings and list of strings are currently accepted as valid
1293+
// inputs for additional key fields
1294+
if inputMemberShape.Type != "string" &&
1295+
(inputMemberShape.Type != "list" ||
1296+
inputMemberShape.MemberRef.Shape.Type != "string") {
1297+
continue
1298+
}
1299+
1300+
if r.IsSecretField(memberName) {
1301+
// Secrets cannot be used as fields in identifiers
1302+
continue
1303+
}
1304+
1305+
if r.IsPrimaryARNField(memberName) {
1306+
continue
1307+
}
1308+
1309+
// Handles field renames, if applicable
1310+
fieldName := cfg.GetResourceFieldName(
1311+
r.Names.Original,
1312+
op.ExportedName,
1313+
memberName,
1314+
)
1315+
1316+
// Check to see if we've already set the field as the primary identifier
1317+
if isPrimarySet && fieldName == primaryField.Names.Camel {
1318+
continue
1319+
}
1320+
1321+
isPrimaryIdentifier := fieldName == primaryShapeField
1322+
1323+
searchField := ""
1324+
if isPrimaryIdentifier {
1325+
searchField = primaryCRField
1326+
} else {
1327+
searchField = fieldName
1328+
}
1329+
1330+
memberPath, targetField := findFieldInCR(cfg, r, searchField)
1331+
if targetField == nil || (isPrimarySet && targetField == primaryField) {
1332+
continue
1333+
}
1334+
1335+
switch targetField.ShapeRef.Shape.Type {
1336+
case "list", "structure", "map":
1337+
panic("primary identifier '" + targetField.Path + "' must be a scalar type since NameOrID is a string")
1338+
default:
1339+
break
1340+
}
1341+
1342+
targetVarPath := fmt.Sprintf("%s%s", targetVarName, memberPath)
1343+
if isPrimaryIdentifier {
1344+
primaryKeyOut += requiredFieldGuardContructor(sourceVarName, targetField.Names.CamelLower, indentLevel)
1345+
primaryKeyOut += setResourceIdentifierPrimaryIdentifierAnn(cfg, r,
1346+
targetField,
1347+
targetVarPath,
1348+
sourceVarName,
1349+
indentLevel)
1350+
} else {
1351+
additionalKeyOut += setResourceIdentifierAdditionalKeyAnn(
1352+
cfg, r,
1353+
memberIndex,
1354+
targetField,
1355+
targetVarPath,
1356+
sourceVarName,
1357+
names.New(fieldName).CamelLower,
1358+
indentLevel)
1359+
}
1360+
}
1361+
1362+
return out + primaryKeyOut + additionalKeyOut
1363+
}
1364+
11041365
// findFieldInCR will search for a given field, by its name, in a CR and returns
11051366
// the member path and Field type if one is found.
11061367
func findFieldInCR(
@@ -1152,6 +1413,34 @@ func setResourceIdentifierPrimaryIdentifier(
11521413
)
11531414
}
11541415

1416+
// AnotherOne returns a string of Go code that sets
1417+
// the primary identifier Spec or Status field on a given resource to the value
1418+
// in the identifier `NameOrID` field:
1419+
//
1420+
// r.ko.Status.BrokerID = &identifier.NameOrID
1421+
func setResourceIdentifierPrimaryIdentifierAnn(
1422+
cfg *ackgenconfig.Config,
1423+
r *model.CRD,
1424+
// The field that will be set on the target variable
1425+
targetField *model.Field,
1426+
// The variable name that we want to set a value to
1427+
targetVarName string,
1428+
// The struct or struct field that we access our source value from
1429+
sourceVarName string,
1430+
// Number of levels of indentation to use
1431+
indentLevel int,
1432+
) string {
1433+
adaptedMemberPath := fmt.Sprintf("&tmp")
1434+
qualifiedTargetVar := fmt.Sprintf("%s.%s", targetVarName, targetField.Path)
1435+
1436+
return setResourceForScalar(
1437+
qualifiedTargetVar,
1438+
adaptedMemberPath,
1439+
targetField.ShapeRef,
1440+
indentLevel,
1441+
)
1442+
}
1443+
11551444
// setResourceIdentifierAdditionalKey returns a string of Go code that sets a
11561445
// Spec or Status field on a given resource to the value in the identifier's
11571446
// `AdditionalKeys` mapping:
@@ -1199,6 +1488,44 @@ func setResourceIdentifierAdditionalKey(
11991488
return additionalKeyOut
12001489
}
12011490

1491+
func setResourceIdentifierAdditionalKeyAnn(
1492+
cfg *ackgenconfig.Config,
1493+
r *model.CRD,
1494+
fieldIndex int,
1495+
// The field that will be set on the target variable
1496+
targetField *model.Field,
1497+
// The variable name that we want to set a value to
1498+
targetVarName string,
1499+
// The struct or struct field that we access our source value from
1500+
sourceVarName string,
1501+
// The key in the `AdditionalKeys` map storing the source variable
1502+
sourceVarKey string,
1503+
// Number of levels of indentation to use
1504+
indentLevel int,
1505+
) string {
1506+
indent := strings.Repeat("\t", indentLevel)
1507+
1508+
additionalKeyOut := ""
1509+
1510+
fieldIndexName := fmt.Sprintf("f%d", fieldIndex)
1511+
sourceAdaptedVarName := fmt.Sprintf("%s[\"%s\"]", sourceVarName, sourceVarKey)
1512+
1513+
// TODO(RedbackThomson): If the identifiers don't exist, we should be
1514+
// throwing an error accessible to the user
1515+
additionalKeyOut += fmt.Sprintf("%s%s, %sok := %s\n", indent, fieldIndexName, fieldIndexName, sourceAdaptedVarName)
1516+
additionalKeyOut += fmt.Sprintf("%sif %sok {\n", indent, fieldIndexName)
1517+
qualifiedTargetVar := fmt.Sprintf("%s.%s", targetVarName, targetField.Path)
1518+
additionalKeyOut += setResourceForScalar(
1519+
qualifiedTargetVar,
1520+
fmt.Sprintf("&%s", fieldIndexName),
1521+
targetField.ShapeRef,
1522+
indentLevel+1,
1523+
)
1524+
additionalKeyOut += fmt.Sprintf("%s}\n", indent)
1525+
1526+
return additionalKeyOut
1527+
}
1528+
12021529
// setResourceForContainer returns a string of Go code that sets the value of a
12031530
// target variable to that of a source variable. When the source variable type
12041531
// is a map, struct or slice type, then this function is called recursively on

‎pkg/generate/code/set_resource_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -3147,6 +3147,83 @@ func TestSetResource_EC2_SecurityGroups_SetResourceIdentifiers(t *testing.T) {
31473147
)
31483148
}
31493149

3150+
func TestSetResource_EKS_Cluster_PopulateResourceFromAnnotation(t *testing.T) {
3151+
assert := assert.New(t)
3152+
require := require.New(t)
3153+
3154+
g := testutil.NewModelForService(t, "eks")
3155+
3156+
crd := testutil.GetCRDByName(t, g, "Cluster")
3157+
require.NotNil(crd)
3158+
3159+
expected := `
3160+
tmp, ok := fields["name"]
3161+
if !ok {
3162+
return ackerrors.MissingNameIdentifier
3163+
}
3164+
r.ko.Spec.Name = &tmp
3165+
3166+
`
3167+
assert.Equal(
3168+
expected,
3169+
code.PopulateResourceFromAnnotation(crd.Config(), crd, "fields", "r.ko", 1),
3170+
)
3171+
}
3172+
3173+
func TestSetResource_SageMaker_ModelPackage_PopulateResourceFromAnnotation(t *testing.T) {
3174+
assert := assert.New(t)
3175+
require := require.New(t)
3176+
3177+
g := testutil.NewModelForService(t, "sagemaker")
3178+
3179+
crd := testutil.GetCRDByName(t, g, "ModelPackage")
3180+
require.NotNil(crd)
3181+
3182+
expected := `
3183+
tmp, ok := identifier["arn"]
3184+
if !ok {
3185+
return ackerrors.MissingNameIdentifier
3186+
}
3187+
3188+
if r.ko.Status.ACKResourceMetadata == nil {
3189+
r.ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{}
3190+
}
3191+
arn := ackv1alpha1.AWSResourceName(tmp)
3192+
r.ko.Status.ACKResourceMetadata.ARN = &arn
3193+
`
3194+
assert.Equal(
3195+
expected,
3196+
code.PopulateResourceFromAnnotation(crd.Config(), crd, "identifier", "r.ko", 1),
3197+
)
3198+
}
3199+
3200+
func TestSetResource_APIGWV2_ApiMapping_PopulateResourceFromAnnotation(t *testing.T) {
3201+
assert := assert.New(t)
3202+
require := require.New(t)
3203+
3204+
g := testutil.NewModelForService(t, "apigatewayv2")
3205+
3206+
crd := testutil.GetCRDByName(t, g, "ApiMapping")
3207+
require.NotNil(crd)
3208+
3209+
expected := `
3210+
tmp, ok := fields["apiMappingID"]
3211+
if !ok {
3212+
return ackerrors.MissingNameIdentifier
3213+
}
3214+
r.ko.Status.APIMappingID = &tmp
3215+
3216+
f1, f1ok := fields["domainName"]
3217+
if f1ok {
3218+
r.ko.Spec.DomainName = &f1
3219+
}
3220+
`
3221+
assert.Equal(
3222+
expected,
3223+
code.PopulateResourceFromAnnotation(crd.Config(), crd, "fields", "r.ko", 1),
3224+
)
3225+
}
3226+
31503227
func TestSetResource_IAM_Role_NestedSetConfig(t *testing.T) {
31513228
assert := assert.New(t)
31523229
require := require.New(t)

‎templates/pkg/resource/resource.go.tpl

+8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error
8282
return nil
8383
}
8484

85+
// PopulateResourceFromAnnotation populates the fields passed from adoption annotation
86+
//
87+
func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error {
88+
{{- GoCodePopulateResourceFromAnnotation .CRD "fields" "r.ko" 1}}
89+
return nil
90+
}
91+
92+
8593
// DeepCopy will return a copy of the resource
8694
func (r *resource) DeepCopy() acktypes.AWSResource {
8795
koCopy := r.ko.DeepCopy()

0 commit comments

Comments
 (0)
Please sign in to comment.