Skip to content

Commit 64436ce

Browse files
authored
Implement ReadMany for CheckRequiredFieldsMissingFromShape (#155)
Issue #, if available: [#890](aws-controllers-k8s/community#890) Description of changes: * Adds checkRequiredFieldsMissingFromShapeReadMany to `check.go` to handle ReadMany operations without a corresponding ReadOne. * The new method will **require resource identifier field** should there exist an *identifier* or *identifierS* field in the ReadMany operation. * Eventually `newListRequestPayload` will use this identifier field to populate its request in order to guarantee the ReadMany operation returns the desired resource. * ex: DescribeVpcs has a VpcIds field. Therefore, Vpc shape should require VpcId, which is part of its status field. * Updated tests and templates Testing: * `make test` ✅ * `make build-controller` ✅ ``` func (rm *resourceManager) sdkFind( ctx context.Context, r *resource, ) (latest *resource, err error) { rlog := ackrtlog.FromContext(ctx) exit := rlog.Trace("rm.sdkFind") defer exit(err) // If any required fields in the input shape are missing, AWS resource is // not created yet. Return NotFound here to indicate to callers that the // resource isn't yet created. if rm.requiredFieldsMissingFromReadManyInput(r) { return nil, ackerr.NotFound } input, err := rm.newListRequestPayload(r) ... // requiredFieldsMissingFromReadManyInput returns true if there are any fields // for the ReadMany Input shape that are required but not present in the // resource's Spec or Status func (rm *resourceManager) requiredFieldsMissingFromReadManyInput( r *resource, ) bool { return r.ko.Status.VPCID == nil } ``` By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent db2aa58 commit 64436ce

File tree

5 files changed

+133
-11
lines changed

5 files changed

+133
-11
lines changed

pkg/generate/ack/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ var (
110110
"GoCodeRequiredFieldsMissingFromReadOneInput": func(r *ackmodel.CRD, koVarName string, indentLevel int) string {
111111
return code.CheckRequiredFieldsMissingFromShape(r, ackmodel.OpTypeGet, koVarName, indentLevel)
112112
},
113+
"GoCodeRequiredFieldsMissingFromReadManyInput": func(r *ackmodel.CRD, koVarName string, indentLevel int) string {
114+
return code.CheckRequiredFieldsMissingFromShape(r, ackmodel.OpTypeList, koVarName, indentLevel)
115+
},
113116
"GoCodeRequiredFieldsMissingFromGetAttributesInput": func(r *ackmodel.CRD, koVarName string, indentLevel int) string {
114117
return code.CheckRequiredFieldsMissingFromShape(r, ackmodel.OpTypeGetAttributes, koVarName, indentLevel)
115118
},

pkg/generate/code/check.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919

2020
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
21+
"github.com/gertd/go-pluralize"
2122

2223
ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/generate/config"
2324
"github.com/aws-controllers-k8s/code-generator/pkg/model"
@@ -74,6 +75,10 @@ func CheckRequiredFieldsMissingFromShape(
7475
switch opType {
7576
case model.OpTypeGet:
7677
op = r.Ops.ReadOne
78+
case model.OpTypeList:
79+
op = r.Ops.ReadMany
80+
return checkRequiredFieldsMissingFromShapeReadMany(
81+
r, koVarName, indentLevel, op, op.InputRef.Shape)
7782
case model.OpTypeGetAttributes:
7883
op = r.Ops.GetAttributes
7984
case model.OpTypeSetAttributes:
@@ -152,6 +157,74 @@ func checkRequiredFieldsMissingFromShape(
152157
return fmt.Sprintf("%sreturn %s\n", indent, missingCondition)
153158
}
154159

160+
// checkRequiredFieldsMissingFromShapeReadMany is a special-case handling
161+
// of those APIs where there is no ReadOne operation and instead the only way to
162+
// grab information for a single object is to call the ReadMany/List operation
163+
// with one of more filtering fields-- specifically identifier(s). This method
164+
// locates an identifier field in the shape that can be populated with an
165+
// identifier value from the CR.
166+
//
167+
//
168+
// As an example, DescribeVpcs EC2 API call doesn't have a ReadOne operation or
169+
// required fields. However, the input shape has a VpcIds field which can be
170+
// populated using a VpcId, a field in the VPC CR's Status. Therefore, require
171+
// the VpcId field to be present to ensure the returned array from the API call
172+
// consists only of the desired Vpc.
173+
//
174+
// Sample Output:
175+
//
176+
// return r.ko.Status.VPCID == nil
177+
func checkRequiredFieldsMissingFromShapeReadMany(
178+
r *model.CRD,
179+
koVarName string,
180+
indentLevel int,
181+
op *awssdkmodel.Operation,
182+
shape *awssdkmodel.Shape,
183+
) string {
184+
indent := strings.Repeat("\t", indentLevel)
185+
result := fmt.Sprintf("%sreturn false", indent)
186+
187+
shapeIdentifiers := FindIdentifiersInShape(r, shape)
188+
crIdentifiers := FindIdentifiersInCRD(r)
189+
if len(shapeIdentifiers) == 0 || len(crIdentifiers) == 0 {
190+
return result
191+
}
192+
193+
pluralize := pluralize.NewClient()
194+
reqIdentifier := ""
195+
for _, si := range shapeIdentifiers {
196+
for _, ci := range crIdentifiers {
197+
if strings.EqualFold(pluralize.Singular(si),
198+
pluralize.Singular(ci)) {
199+
// The CRD identifiers being used for comparison reflect the
200+
// *original* field names in the API model shape.
201+
// Field renames are handled below in the call to
202+
// getSanitizedMemberPath.
203+
if reqIdentifier == "" {
204+
reqIdentifier = ci
205+
} else {
206+
// If there are multiple identifiers, then prioritize the
207+
// 'Id' identifier. Checking 'Id' to determine resource
208+
// creation should be safe as the field is usually
209+
// present in CR.Status.
210+
if !strings.HasSuffix(reqIdentifier, "Id") ||
211+
!strings.HasSuffix(reqIdentifier, "Ids") {
212+
reqIdentifier = ci
213+
}
214+
}
215+
}
216+
}
217+
}
218+
219+
resVarPath, err := getSanitizedMemberPath(reqIdentifier, r, op, koVarName)
220+
if err != nil {
221+
return result
222+
}
223+
224+
result = fmt.Sprintf("%s == nil", resVarPath)
225+
return fmt.Sprintf("%sreturn %s\n", indent, result)
226+
}
227+
155228
// getSanitizedMemberPath takes a shape member field, checks for renames, checks
156229
// for existence in Spec and Status, then constructs and returns the var path.
157230
// Returns error if memberName is not present in either Spec or Status.

pkg/generate/code/check_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,24 @@ func TestCheckRequiredFields_RenamedSpecField(t *testing.T) {
115115
strings.TrimSpace(gotCode),
116116
)
117117
}
118+
119+
func TestCheckRequiredFields_StatusField_ReadMany(t *testing.T) {
120+
assert := assert.New(t)
121+
require := require.New(t)
122+
123+
g := testutil.NewModelForService(t, "ec2")
124+
125+
crd := testutil.GetCRDByName(t, g, "Vpc")
126+
require.NotNil(crd)
127+
128+
expRequiredFieldsCode := `
129+
return r.ko.Status.VPCID == nil
130+
`
131+
gotCode := code.CheckRequiredFieldsMissingFromShape(
132+
crd, model.OpTypeList, "r.ko", 1,
133+
)
134+
assert.Equal(
135+
strings.TrimSpace(expRequiredFieldsCode),
136+
strings.TrimSpace(gotCode),
137+
)
138+
}

pkg/generate/code/common.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,24 @@ import (
2121
)
2222

2323
// FindIdentifiersInShape returns the identifier fields of a given shape which
24-
// can be singular or plural. Errors iff multiple identifier fields detected
25-
// in the shape.
24+
// can be singular or plural.
2625
func FindIdentifiersInShape(
2726
r *model.CRD,
2827
shape *awssdkmodel.Shape) []string {
28+
var identifiers []string
29+
if r == nil || shape == nil {
30+
return identifiers
31+
}
2932
identifierLookup := []string{
3033
"Id",
3134
"Ids",
35+
r.Names.Original + "Id",
36+
r.Names.Original + "Ids",
3237
"Name",
3338
"Names",
3439
r.Names.Original + "Name",
3540
r.Names.Original + "Names",
36-
r.Names.Original + "Id",
37-
r.Names.Original + "Ids",
3841
}
39-
var identifiers []string
4042

4143
for _, memberName := range shape.MemberNames() {
4244
if util.InStrings(memberName, identifierLookup) {
@@ -47,22 +49,25 @@ func FindIdentifiersInShape(
4749
return identifiers
4850
}
4951

50-
// FindIdentifiersInCRD returns the identifier field of a given CRD which
51-
// can be singular or plural. Errors iff multiple identifier fields detected
52-
// in the CRD.
52+
// FindIdentifiersInCRD returns the identifier fields of a given CRD which
53+
// can be singular or plural. Note, these fields will be the *original* field
54+
// names from the API model shape, not renamed field names.
5355
func FindIdentifiersInCRD(
5456
r *model.CRD) []string {
57+
var identifiers []string
58+
if r == nil {
59+
return identifiers
60+
}
5561
identifierLookup := []string{
5662
"Id",
5763
"Ids",
64+
r.Names.Original + "Id",
65+
r.Names.Original + "Ids",
5866
"Name",
5967
"Names",
6068
r.Names.Original + "Name",
6169
r.Names.Original + "Names",
62-
r.Names.Original + "Id",
63-
r.Names.Original + "Ids",
6470
}
65-
var identifiers []string
6671

6772
for _, id := range identifierLookup {
6873
_, found := r.SpecFields[id]

templates/pkg/resource/sdk_find_read_many.go.tpl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ func (rm *resourceManager) sdkFind(
1010
{{- if $hookCode := Hook .CRD "sdk_read_many_pre_build_request" }}
1111
{{ $hookCode }}
1212
{{- end }}
13+
// If any required fields in the input shape are missing, AWS resource is
14+
// not created yet. Return NotFound here to indicate to callers that the
15+
// resource isn't yet created.
16+
if rm.requiredFieldsMissingFromReadManyInput(r) {
17+
return nil, ackerr.NotFound
18+
}
19+
1320
input, err := rm.newListRequestPayload(r)
1421
if err != nil {
1522
return nil, err
@@ -51,6 +58,19 @@ func (rm *resourceManager) sdkFind(
5158
return &resource{ko}, nil
5259
}
5360

61+
// requiredFieldsMissingFromReadManyInput returns true if there are any fields
62+
// for the ReadMany Input shape that are required but not present in the
63+
// resource's Spec or Status
64+
func (rm *resourceManager) requiredFieldsMissingFromReadManyInput(
65+
r *resource,
66+
) bool {
67+
{{- if $customCheckMethod := .CRD.GetCustomCheckRequiredFieldsMissingMethod .CRD.Ops.ReadMany }}
68+
return rm.{{ $customCheckMethod }}(r)
69+
{{- else }}
70+
{{ GoCodeRequiredFieldsMissingFromReadManyInput .CRD "r.ko" 1 }}
71+
{{- end }}
72+
}
73+
5474
// newListRequestPayload returns SDK-specific struct for the HTTP request
5575
// payload of the List API call for the resource
5676
func (rm *resourceManager) newListRequestPayload(

0 commit comments

Comments
 (0)