Skip to content

Commit 316cbcb

Browse files
Generate resource identifiers from ReadOne or ReadMany (#105)
Issue #, if available: aws-controllers-k8s/community#842 Description of changes: This pull request introduces a smarter system for detecting resource primary identifiers by using the fields from the `ReadOne` (or optionally `ReadMany`) operations. It detects if a field is attempting to use the resource ARN and will set the status metadata to the corresponding identifier element. Otherwise, it will search through each of the required fields in the operation to attempt to identify the "primary" identifier field (such as `*Name`, `Name`, `*ID`). All other fields in the operation can be configured through the use of the new `AdditionalKeys` map (https://github.com/aws-controllers-k8s/runtime/pull/13/files). Optionally, the primary identifier field can be configured in the generator under `operations.<operation>.primary_identifier_field_name` if one cannot be (or is incorrectly) identified. Example output from `applicationautoscaling` `ScalingPolicy`: ```golang func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error { if identifier.NameOrID == nil { return ackerrors.MissingNameIdentifier } r.ko.Spec.ResourceID = identifier.NameOrID f0, f0ok := identifier.AdditionalKeys["scalableDimension"] if f0ok { r.ko.Spec.ScalableDimension = f0 } f1, f1ok := identifier.AdditionalKeys["serviceNamespace"] if f1ok { r.ko.Spec.ServiceNamespace = f1 } return nil } ``` Example output from `mq` `Broker`: ```golang func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error { if identifier.NameOrID == nil { return ackerrors.MissingNameIdentifier } r.ko.Status.BrokerID = identifier.NameOrID return nil } ``` By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 9fec5f0 commit 316cbcb

File tree

7 files changed

+284
-10
lines changed

7 files changed

+284
-10
lines changed

pkg/generate/ack/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ var (
109109
"GoCodeRequiredFieldsMissingFromSetAttributesInput": func(r *ackmodel.CRD, koVarName string, indentLevel int) string {
110110
return code.CheckRequiredFieldsMissingFromShape(r, ackmodel.OpTypeSetAttributes, koVarName, indentLevel)
111111
},
112+
"GoCodeSetResourceIdentifiers": func(r *ackmodel.CRD, sourceVarName string, targetVarName string, indentLevel int) string {
113+
return code.SetResourceIdentifiers(r.Config(), r, sourceVarName, targetVarName, indentLevel)
114+
},
112115
}
113116
)
114117

pkg/generate/code/set_resource.go

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,6 @@ func SetResource(
317317
return out
318318
}
319319

320-
321320
func ListMemberNameInReadManyOutput(
322321
r *model.CRD,
323322
) string {
@@ -770,6 +769,205 @@ func SetResourceGetAttributes(
770769
return out
771770
}
772771

772+
// SetResourceIdentifiers returns the Go code that sets an empty CR object with
773+
// Spec and Status field values that correspond to the primary identifier (be
774+
// that an ARN, ID or Name) and any other "additional keys" required for the AWS
775+
// service to uniquely identify the object.
776+
//
777+
// The method will attempt to use the `ReadOne` operation, if present, otherwise
778+
// will fall back to using `ReadMany`. If it detects the operation uses an ARN
779+
// to identify the resource it will read it from the metadata status field.
780+
// Otherwise it will use any fields with a matching name in the operation,
781+
// pulling from spec or status - requiring that exactly one of those fields is
782+
// marked as the "primary" identifier.
783+
//
784+
// An example of code with no additional keys:
785+
//
786+
// ```
787+
// if identifier.NameOrID == nil {
788+
// return ackerrors.MissingNameIdentifier
789+
// }
790+
// r.ko.Status.BrokerID = identifier.NameOrID
791+
// ```
792+
//
793+
// An example of code with additional keys:
794+
//
795+
// ```
796+
// if identifier.NameOrID == nil {
797+
// return ackerrors.MissingNameIdentifier
798+
// }
799+
// r.ko.Spec.ResourceID = identifier.NameOrID
800+
//
801+
// f0, f0ok := identifier.AdditionalKeys["scalableDimension"]
802+
// if f0ok {
803+
// r.ko.Spec.ScalableDimension = f0
804+
// }
805+
// f1, f1ok := identifier.AdditionalKeys["serviceNamespace"]
806+
// if f1ok {
807+
// r.ko.Spec.ServiceNamespace = f1
808+
// }
809+
// ```
810+
func SetResourceIdentifiers(
811+
cfg *ackgenconfig.Config,
812+
r *model.CRD,
813+
// String representing the name of the variable that we will grab the Input
814+
// shape from. This will likely be "identifier" since in the templates that
815+
// call this method, the "source variable" is the CRD struct which is used
816+
// to populate the target variable, which is the struct of unique
817+
// identifiers
818+
sourceVarName string,
819+
// String representing the name of the variable that we will be **setting**
820+
// with values we get from the Output shape. This will likely be
821+
// "r.ko" since that is the name of the "target variable" that the
822+
// templates that call this method use for the Input shape.
823+
targetVarName string,
824+
// Number of levels of indentation to use
825+
indentLevel int,
826+
) string {
827+
op := r.Ops.ReadOne
828+
if op == nil {
829+
if r.Ops.GetAttributes != nil {
830+
// TODO(RedbackThomson): Support attribute maps for resource identifiers
831+
return ""
832+
}
833+
// If single lookups can only be done using ReadMany
834+
op = r.Ops.ReadMany
835+
}
836+
inputShape := op.InputRef.Shape
837+
if inputShape == nil {
838+
return ""
839+
}
840+
841+
primaryKeyOut := "\n"
842+
arnOut := ""
843+
additionalKeyOut := "\n"
844+
845+
indent := strings.Repeat("\t", indentLevel)
846+
847+
primaryKeyOut += fmt.Sprintf("%sif %s.NameOrID == nil {\n", indent, sourceVarName)
848+
primaryKeyOut += fmt.Sprintf("%s\treturn ackerrors.MissingNameIdentifier\n", indent)
849+
primaryKeyOut += fmt.Sprintf("%s}\n", indent)
850+
851+
primaryIdentifier := ""
852+
853+
// Attempt to fetch the primary identifier from the configuration
854+
opConfig, ok := cfg.Operations[op.Name]
855+
if ok {
856+
primaryIdentifier = opConfig.PrimaryIdentifierFieldName
857+
}
858+
859+
// Determine the "primary identifier" based on the names of each field
860+
if primaryIdentifier == "" {
861+
primaryIdentifierLookup := []string{
862+
"Name",
863+
r.Names.Original + "Name",
864+
r.Names.Original + "Id",
865+
}
866+
867+
for _, memberName := range inputShape.MemberNames() {
868+
if util.InStrings(memberName, primaryIdentifierLookup) {
869+
if primaryIdentifier == "" {
870+
primaryIdentifier = memberName
871+
} else {
872+
panic("Found multiple possible primary identifiers for " +
873+
r.Names.Original + ". Set " +
874+
"`primary_identifier_field_name` for the " + op.Name +
875+
" operation in the generator config.")
876+
}
877+
}
878+
}
879+
880+
// Still haven't determined the identifier? Panic
881+
if primaryIdentifier == "" {
882+
panic("Could not find primary identifier for " + r.Names.Original +
883+
". Set `primary_identifier_field_name` for the " + op.Name +
884+
" operation in the generator config.")
885+
}
886+
}
887+
888+
paginatorFieldLookup := []string{
889+
"NextToken",
890+
"MaxResults",
891+
}
892+
893+
additionalKeyCount := 0
894+
for _, memberName := range inputShape.MemberNames() {
895+
if util.InStrings(memberName, paginatorFieldLookup) {
896+
continue
897+
}
898+
899+
memberShapeRef, _ := inputShape.MemberRefs[memberName]
900+
memberShape := memberShapeRef.Shape
901+
902+
// Only strings are currently accepted as valid inputs for
903+
// additional key fields
904+
if memberShape.Type != "string" {
905+
continue
906+
}
907+
908+
if r.IsSecretField(memberName) {
909+
// Secrets cannot be used as fields in identifiers
910+
continue
911+
}
912+
913+
if r.IsPrimaryARNField(memberName) {
914+
// r.ko.Status.ACKResourceMetadata.ARN = identifier.ARN
915+
arnOut += fmt.Sprintf(
916+
"\n%s%s.Status.ACKResourceMetadata.ARN = %s.ARN\n",
917+
indent, targetVarName, sourceVarName,
918+
)
919+
continue
920+
921+
}
922+
923+
isPrimaryIdentifier := memberName == primaryIdentifier
924+
cleanMemberNames := names.New(memberName)
925+
cleanMemberName := cleanMemberNames.Camel
926+
927+
memberPath := ""
928+
_, inSpec := r.SpecFields[memberName]
929+
_, inStatus := r.StatusFields[memberName]
930+
switch {
931+
case inSpec:
932+
memberPath = cfg.PrefixConfig.SpecField
933+
case inStatus:
934+
memberPath = cfg.PrefixConfig.StatusField
935+
case isPrimaryIdentifier:
936+
panic("Primary identifier field '" + memberName + "' cannot be found in either spec or status.")
937+
default:
938+
continue
939+
}
940+
941+
if isPrimaryIdentifier {
942+
// r.ko.Status.BrokerID = identifier.NameOrID
943+
primaryKeyOut += fmt.Sprintf("%s%s%s.%s = %s.NameOrID\n", indent, targetVarName, memberPath, cleanMemberName, sourceVarName)
944+
} else {
945+
// f0, f0ok := identifier.AdditionalKeys["scalableDimension"]
946+
// if f0ok {
947+
// r.ko.Spec.ScalableDimension = f0
948+
// }
949+
950+
fieldIndexName := fmt.Sprintf("f%d", additionalKeyCount)
951+
sourceAdaptedVarName := fmt.Sprintf("%s.AdditionalKeys[\"%s\"]", sourceVarName, cleanMemberNames.CamelLower)
952+
953+
// TODO(RedbackThomson): If the identifiers don't exist, we should be
954+
// throwing an error accessible to the user
955+
additionalKeyOut += fmt.Sprintf("%s%s, %sok := %s\n", indent, fieldIndexName, fieldIndexName, sourceAdaptedVarName)
956+
additionalKeyOut += fmt.Sprintf("%sif %sok {\n", indent, fieldIndexName)
957+
additionalKeyOut += fmt.Sprintf("%s\t%s%s.%s = %s\n", indent, targetVarName, memberPath, cleanMemberName, fieldIndexName)
958+
additionalKeyOut += fmt.Sprintf("%s}\n", indent)
959+
960+
additionalKeyCount++
961+
}
962+
}
963+
964+
// Only use at most one of ARN or nameOrID as primary identifier outputs
965+
if arnOut != "" {
966+
return arnOut + additionalKeyOut
967+
}
968+
return primaryKeyOut + additionalKeyOut
969+
}
970+
773971
// setResourceForContainer returns a string of Go code that sets the value of a
774972
// target variable to that of a source variable. When the source variable type
775973
// is a map, struct or slice type, then this function is called recursively on

pkg/generate/code/set_resource_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,3 +2645,73 @@ func TestGetWrapperOutputShape(t *testing.T) {
26452645
})
26462646
}
26472647
}
2648+
2649+
func TestSetResource_MQ_Broker_SetResourceIdentifiers(t *testing.T) {
2650+
assert := assert.New(t)
2651+
require := require.New(t)
2652+
2653+
g := testutil.NewGeneratorForService(t, "mq")
2654+
2655+
crd := testutil.GetCRDByName(t, g, "Broker")
2656+
require.NotNil(crd)
2657+
2658+
expected := `
2659+
if identifier.NameOrID == nil {
2660+
return ackerrors.MissingNameIdentifier
2661+
}
2662+
r.ko.Status.BrokerID = identifier.NameOrID
2663+
2664+
`
2665+
assert.Equal(
2666+
expected,
2667+
code.SetResourceIdentifiers(crd.Config(), crd, "identifier", "r.ko", 1),
2668+
)
2669+
}
2670+
2671+
func TestSetResource_RDS_DBInstances_SetResourceIdentifiers(t *testing.T) {
2672+
assert := assert.New(t)
2673+
require := require.New(t)
2674+
2675+
g := testutil.NewGeneratorForService(t, "rds")
2676+
2677+
crd := testutil.GetCRDByName(t, g, "DBInstance")
2678+
require.NotNil(crd)
2679+
2680+
expected := `
2681+
if identifier.NameOrID == nil {
2682+
return ackerrors.MissingNameIdentifier
2683+
}
2684+
r.ko.Spec.DBInstanceIdentifier = identifier.NameOrID
2685+
2686+
`
2687+
assert.Equal(
2688+
expected,
2689+
code.SetResourceIdentifiers(crd.Config(), crd, "identifier", "r.ko", 1),
2690+
)
2691+
}
2692+
2693+
func TestSetResource_APIGWV2_ApiMapping_SetResourceIdentifiers(t *testing.T) {
2694+
assert := assert.New(t)
2695+
require := require.New(t)
2696+
2697+
g := testutil.NewGeneratorForService(t, "apigatewayv2")
2698+
2699+
crd := testutil.GetCRDByName(t, g, "ApiMapping")
2700+
require.NotNil(crd)
2701+
2702+
expected := `
2703+
if identifier.NameOrID == nil {
2704+
return ackerrors.MissingNameIdentifier
2705+
}
2706+
r.ko.Status.APIMappingID = identifier.NameOrID
2707+
2708+
f0, f0ok := identifier.AdditionalKeys["domainName"]
2709+
if f0ok {
2710+
r.ko.Spec.DomainName = f0
2711+
}
2712+
`
2713+
assert.Equal(
2714+
expected,
2715+
code.SetResourceIdentifiers(crd.Config(), crd, "identifier", "r.ko", 1),
2716+
)
2717+
}

pkg/generate/config/operation.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ type OperationConfig struct {
3737
// Override for operation type in case of heuristic failure
3838
// An example of this is `Put...` or `Register...` API operations not being correctly classified as `Create` op type
3939
OperationType string `json:"operation_type"`
40+
// PrimaryIdentifierFieldName provides the name of the field that should be
41+
// interpreted as the "primary" identifier field. This field will be used as
42+
// the primary field for resource adoption.
43+
PrimaryIdentifierFieldName string `json:"primary_identifier_field_name,omitempty"`
4044
}
4145

4246
// IsIgnoredOperation returns true if Operation Name is configured to be ignored

pkg/generate/testdata/models/apis/rds/0000-00-00/generator.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
ignore:
22
shape_names:
33
- DBSecurityGroupMembershipList
4+
operations:
5+
DescribeDBInstances:
6+
primary_identifier_field_name: DBInstanceIdentifier
47
resources:
58
DBSubnetGroup:
69
renames:

pkg/model/crd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,10 @@ func (r *CRD) UpdateConditionsCustomMethodName() string {
507507
return resGenConfig.UpdateConditionsCustomMethodName
508508
}
509509

510-
// SpecIdentifierField returns the name of the "Name" or string identifier field in the Spec
510+
// SpecIdentifierField returns the name of the "Name" or string identifier field
511+
// in the Spec.
512+
// This method does not guarantee that the identifier field returned is the
513+
// primary identifier used in any of the `Read*` operations.
511514
func (r *CRD) SpecIdentifierField() *string {
512515
if r.cfg != nil {
513516
rConfig, found := r.cfg.Resources[r.Names.Original]

templates/pkg/resource/resource.go.tpl

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,6 @@ func (r *resource) SetObjectMeta(meta metav1.ObjectMeta) {
7474
// SetIdentifiers sets the Spec or Status field that is referenced as the unique
7575
// resource identifier
7676
func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error {
77-
{{- if $idField := .CRD.SpecIdentifierField }}
78-
if identifier.NameOrID == nil {
79-
return ackerrors.MissingNameIdentifier
80-
}
81-
r.ko.Spec.{{ $idField }} = identifier.NameOrID
82-
{{- else }}
83-
r.ko.Status.ACKResourceMetadata.ARN = identifier.ARN
84-
{{- end }}
77+
{{- GoCodeSetResourceIdentifiers .CRD "identifier" "r.ko" 1}}
8578
return nil
8679
}

0 commit comments

Comments
 (0)