Skip to content

Commit 00e6463

Browse files
committed
feature: Generate Code for resource adoption by annotation
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. `post_set_resource_identifiers` to
1 parent 9715a2a commit 00e6463

File tree

3 files changed

+342
-0
lines changed

3 files changed

+342
-0
lines changed

pkg/generate/ack/controller.go

Lines changed: 3 additions & 0 deletions
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

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,27 @@ func identifierNameOrIDGuardConstructor(
751751
return out
752752
}
753753

754+
// identifierNameOrIDGuardConstructor returns Go code representing a nil-guard
755+
// and returns a `MissingNameIdentifier` error:
756+
//
757+
// if fields[${requiredField}] == "" {
758+
// return ackerrors.MissingNameIdentifier
759+
// }
760+
func requiredFieldGuardContructor(
761+
// String representing the name of the identifier that should have the
762+
// `NameOrID` pointer defined
763+
sourceVarName string,
764+
// Number of levels of indentation to use
765+
indentLevel int,
766+
) string {
767+
indent := strings.Repeat("\t", indentLevel)
768+
out := fmt.Sprintf("%stmp, ok := %s[\"arn\"]\n", indent, sourceVarName)
769+
out += fmt.Sprintf("%sif !ok {\n", indent)
770+
out += fmt.Sprintf("%s\treturn ackerrors.MissingNameIdentifier\n", indent)
771+
out += fmt.Sprintf("%s}\n", indent)
772+
return out
773+
}
774+
754775
// SetResourceGetAttributes returns the Go code that sets the Status fields
755776
// from the Output shape returned from a resource's GetAttributes operation.
756777
//
@@ -1101,6 +1122,250 @@ func SetResourceIdentifiers(
11011122
return primaryKeyConditionalOut + primaryKeyOut + additionalKeyOut
11021123
}
11031124

1125+
// PopulateResourceFromAnnotation returns the Go code that sets an empty CR object with
1126+
// Spec and Status field values that correspond to the primary identifier (be
1127+
// that an ARN, ID or Name) and any other "additional keys" required for the AWS
1128+
// service to uniquely identify the object.
1129+
//
1130+
// The method will attempt to look for the field denoted with a value of true
1131+
// for `is_primary_key`, or will use the ARN if the resource has a value of true
1132+
// for `is_arn_primary_key`. Otherwise, the method will attempt to use the
1133+
// `ReadOne` operation, if present, falling back to using `ReadMany`.
1134+
// If it detects the operation uses an ARN to identify the resource it will read
1135+
// it from the metadata status field. Otherwise it will use any field with a
1136+
// name that matches the primary identifier from the operation, pulling from
1137+
// top-level spec or status fields.
1138+
//
1139+
// An example of code with no additional keys:
1140+
//
1141+
// ```
1142+
// tmp, ok := field["brokerID"]
1143+
// if !ok {
1144+
// return ackerrors.MissingNameIdentifier
1145+
// }
1146+
// r.ko.Status.BrokerID = &tmp
1147+
//
1148+
// ```
1149+
//
1150+
// An example of code with additional keys:
1151+
//
1152+
// ```
1153+
//
1154+
// tmp, ok := field["resourceID"]
1155+
// if !ok {
1156+
// return ackerrors.MissingNameIdentifier
1157+
// }
1158+
//
1159+
// r.ko.Spec.ResourceID = &tmp
1160+
//
1161+
// f0, f0ok := fields["scalableDimension"]
1162+
//
1163+
// if f0ok {
1164+
// r.ko.Spec.ScalableDimension = &f0
1165+
// }
1166+
//
1167+
// f1, f1ok := fields["serviceNamespace"]
1168+
//
1169+
// if f1ok {
1170+
// r.ko.Spec.ServiceNamespace = &f1
1171+
// }
1172+
//
1173+
// ```
1174+
// An example of code that uses the ARN:
1175+
//
1176+
// ```
1177+
// tmpArn, ok := field["arn"]
1178+
// if !ok {
1179+
// return ackerrors.MissingNameIdentifier
1180+
// }
1181+
// if r.ko.Status.ACKResourceMetadata == nil {
1182+
// r.ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{}
1183+
// }
1184+
// arn := ackv1alpha1.AWSResourceName(tmp)
1185+
//
1186+
// r.ko.Status.ACKResourceMetadata.ARN = &arn
1187+
//
1188+
// f0, f0ok := fields["modelPackageName"]
1189+
//
1190+
// if f0ok {
1191+
// r.ko.Spec.ModelPackageName = &f0
1192+
// }
1193+
//
1194+
// ```
1195+
func PopulateResourceFromAnnotation(
1196+
cfg *ackgenconfig.Config,
1197+
r *model.CRD,
1198+
// String representing the name of the variable that we will grab the Input
1199+
// shape from. This will likely be "fields" since in the templates that
1200+
// call this method, the "source variable" is the CRD struct which is used
1201+
// to populate the target variable, which is the struct of unique
1202+
// identifiers
1203+
sourceVarName string,
1204+
// String representing the name of the variable that we will be **setting**
1205+
// with values we get from the Output shape. This will likely be
1206+
// "r.ko" since that is the name of the "target variable" that the
1207+
// templates that call this method use for the Input shape.
1208+
targetVarName string,
1209+
// Number of levels of indentation to use
1210+
indentLevel int,
1211+
) string {
1212+
op := r.Ops.ReadOne
1213+
if op == nil {
1214+
switch {
1215+
case r.Ops.GetAttributes != nil:
1216+
// If single lookups can only be done with GetAttributes
1217+
op = r.Ops.GetAttributes
1218+
case r.Ops.ReadMany != nil:
1219+
// If single lookups can only be done using ReadMany
1220+
op = r.Ops.ReadMany
1221+
default:
1222+
return ""
1223+
}
1224+
}
1225+
inputShape := op.InputRef.Shape
1226+
if inputShape == nil {
1227+
return ""
1228+
}
1229+
1230+
primaryKeyOut := ""
1231+
additionalKeyOut := "\n"
1232+
1233+
indent := strings.Repeat("\t", indentLevel)
1234+
arnOut := "\n"
1235+
out := "\n"
1236+
// Check if the CRD defines the primary keys
1237+
primaryKeyConditionalOut := "\n"
1238+
primaryKeyConditionalOut += requiredFieldGuardContructor(sourceVarName, indentLevel)
1239+
arnOut += ackResourceMetadataGuardConstructor(fmt.Sprintf("%s.Status", targetVarName), indentLevel)
1240+
arnOut += fmt.Sprintf(
1241+
"%sarn := ackv1alpha1.AWSResourceName(tmp)\n",
1242+
indent,
1243+
)
1244+
arnOut += fmt.Sprintf(
1245+
"%s%s.Status.ACKResourceMetadata.ARN = &arn",
1246+
indent, targetVarName,
1247+
)
1248+
if r.IsARNPrimaryKey() {
1249+
return primaryKeyConditionalOut + arnOut
1250+
}
1251+
primaryField, err := r.GetPrimaryKeyField()
1252+
if err != nil {
1253+
panic(err)
1254+
}
1255+
1256+
var primaryCRField, primaryShapeField string
1257+
isPrimarySet := primaryField != nil
1258+
if isPrimarySet {
1259+
memberPath, _ := findFieldInCR(cfg, r, primaryField.Names.Original)
1260+
targetVarPath := fmt.Sprintf("%s%s", targetVarName, memberPath)
1261+
primaryKeyOut += fmt.Sprintf("%stmp, ok := %s[\"%s\"]\n", indent, sourceVarName, primaryField.Names.CamelLower)
1262+
primaryKeyOut += fmt.Sprintf("%sif !ok {\n", indent)
1263+
primaryKeyOut += fmt.Sprintf("%s\treturn ackerrors.MissingNameIdentifier\n", indent)
1264+
primaryKeyOut += fmt.Sprintf("%s}\n", indent)
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+
targetVarPath := fmt.Sprintf("%s%s", targetVarName, memberPath)
1345+
primaryKeyOut += fmt.Sprintf("%stmp, ok := %s[\"%s\"]\n", indent, sourceVarName, targetField.Names.CamelLower)
1346+
primaryKeyOut += fmt.Sprintf("%sif !ok {\n", indent)
1347+
primaryKeyOut += fmt.Sprintf("%s\treturn ackerrors.MissingNameIdentifier\n", indent)
1348+
primaryKeyOut += fmt.Sprintf("%s}\n", indent)
1349+
primaryKeyOut += setResourceIdentifierPrimaryIdentifierAnn(cfg, r,
1350+
targetField,
1351+
targetVarPath,
1352+
sourceVarName,
1353+
indentLevel)
1354+
} else {
1355+
additionalKeyOut += setResourceIdentifierAdditionalKeyAnn(
1356+
cfg, r,
1357+
memberIndex,
1358+
targetField,
1359+
targetVarPath,
1360+
sourceVarName,
1361+
names.New(fieldName).CamelLower,
1362+
indentLevel)
1363+
}
1364+
}
1365+
1366+
return out + primaryKeyOut + additionalKeyOut
1367+
}
1368+
11041369
// findFieldInCR will search for a given field, by its name, in a CR and returns
11051370
// the member path and Field type if one is found.
11061371
func findFieldInCR(
@@ -1152,6 +1417,34 @@ func setResourceIdentifierPrimaryIdentifier(
11521417
)
11531418
}
11541419

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

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

templates/pkg/resource/resource.go.tpl

Lines changed: 8 additions & 0 deletions
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)