Skip to content

Commit 3c89a32

Browse files
Refactor reference resolution into Go code (#417)
Continuation of #401 Refactors the resolveReferenceFor<Field> Go template code into direct Go code. This way we can be more intelligent about nested field references and support unit testing its output. Breaking change: The previous Go template produced valid, but ultimately faulty, code for references within lists of structs. These are not supported (see aws-controllers-k8s/community#1291). As such, this update produces a panic when it detects those types of references. Co-authored-by: Amine <[email protected]>
1 parent e0eac49 commit 3c89a32

File tree

7 files changed

+410
-112
lines changed

7 files changed

+410
-112
lines changed

pkg/generate/ack/controller.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ var (
6262
"Dereference": func(s *string) string {
6363
return *s
6464
},
65+
"AddToMap": func(m map[string]interface{}, k string, v interface{}) map[string]interface{} {
66+
if len(m) == 0 {
67+
m = make(map[string]interface{})
68+
}
69+
m[k] = v
70+
return m
71+
},
72+
"Nil": func() interface{} {
73+
return nil
74+
},
6575
"ResourceExceptionCode": func(r *ackmodel.CRD, httpStatusCode int) string {
6676
return r.ExceptionCode(httpStatusCode)
6777
},
@@ -183,6 +193,9 @@ var (
183193
return code.InitializeNestedStructField(r, sourceVarName, f,
184194
apiPkgImportName, indentLevel)
185195
},
196+
"GoCodeResolveReference": func(f *ackmodel.Field, sourceVarName string, indentLevel int) string {
197+
return code.ResolveReferencesForField(f, sourceVarName, indentLevel)
198+
},
186199
}
187200
)
188201

pkg/generate/code/resource_reference.go

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func ReferenceFieldsValidation(
9898
out += fmt.Sprintf("%sif %s.%s != nil"+
9999
" && %s.%s != nil {\n", fIndent, pathVarPrefix, field.GetReferenceFieldName().Camel, pathVarPrefix, field.Names.Camel)
100100
out += fmt.Sprintf("%s\treturn "+
101-
"ackerr.ResourceReferenceAndIDNotSupportedFor(\"%s\", \"%s\")\n",
101+
"ackerr.ResourceReferenceAndIDNotSupportedFor(%q, %q)\n",
102102
fIndent, field.Path, field.ReferenceFieldPath())
103103

104104
// Close out all the curly braces with proper indentation
@@ -117,7 +117,7 @@ func ReferenceFieldsValidation(
117117
" %s.%s == nil {\n", fIndent, pathVarPrefix,
118118
field.ReferenceFieldPath(), pathVarPrefix, field.Path)
119119
out += fmt.Sprintf("%s\treturn "+
120-
"ackerr.ResourceReferenceOrIDRequiredFor(\"%s\", \"%s\")\n",
120+
"ackerr.ResourceReferenceOrIDRequiredFor(%q, %q)\n",
121121
fIndent, field.Path, field.ReferenceFieldPath())
122122
out += fmt.Sprintf("%s}\n", fIndent)
123123
}
@@ -185,6 +185,122 @@ func ReferenceFieldsPresent(
185185
return iteratorsOut + returnOut
186186
}
187187

188+
// ResolveReferencesForField produces Go code for accessing all references that
189+
// are related to the given concrete field, determining whether its in a valid
190+
// condition and updating the concrete field with the referenced value.
191+
// Sample code (resolving a nested singular reference):
192+
//
193+
// ```
194+
//
195+
// if ko.Spec.APIRef != nil && ko.Spec.APIRef.From != nil {
196+
// arr := ko.Spec.APIRef.From
197+
// if arr == nil || arr.Name == nil || *arr.Name == "" {
198+
// return fmt.Errorf("provided resource reference is nil or empty: APIRef")
199+
// }
200+
// obj := &svcapitypes.API{}
201+
// if err := getReferencedResourceState_API(ctx, apiReader, obj, *arr.Name, namespace); err != nil {
202+
// return err
203+
// }
204+
// ko.Spec.APIID = (*string)(obj.Status.APIID)
205+
// }
206+
//
207+
// ```
208+
func ResolveReferencesForField(field *model.Field, sourceVarName string, indentLevel int) string {
209+
r := field.CRD
210+
fp := fieldpath.FromString(field.Path)
211+
212+
outPrefix := ""
213+
outSuffix := ""
214+
215+
fieldAccessPrefix := fmt.Sprintf("%s%s", sourceVarName, r.Config().PrefixConfig.SpecField)
216+
targetVarName := fmt.Sprintf("%s.%s", fieldAccessPrefix, field.Path)
217+
218+
for idx := 0; idx < fp.Size(); idx++ {
219+
curFP := fp.CopyAt(idx).String()
220+
cur, ok := r.Fields[curFP]
221+
if !ok {
222+
panic(fmt.Sprintf("unable to find field with path %q. crd: %q", curFP, r.Kind))
223+
}
224+
225+
ref := cur.ShapeRef
226+
227+
indent := strings.Repeat("\t", indentLevel+idx)
228+
229+
switch ref.Shape.Type {
230+
case ("structure"):
231+
fieldAccessPrefix = fmt.Sprintf("%s.%s", fieldAccessPrefix, fp.At(idx))
232+
233+
outPrefix += fmt.Sprintf("%sif %s != nil {\n", indent, fieldAccessPrefix)
234+
outSuffix = fmt.Sprintf("%s}\n%s", indent, outSuffix)
235+
case ("list"):
236+
if (fp.Size() - idx) > 1 {
237+
// TODO(nithomso): add support for structs nested within lists
238+
// The logic for structs nested within lists needs to not only
239+
// be added here, but also in a custom patching solution since
240+
// it isn't supported by `StrategicMergePatch`
241+
// see https://github.com/aws-controllers-k8s/community/issues/1291
242+
panic(fmt.Errorf("references within lists inside lists aren't supported. crd: %q, path: %q", r.Kind, field.Path))
243+
}
244+
fieldAccessPrefix = fmt.Sprintf("%s.%s", fieldAccessPrefix, cur.GetReferenceFieldName().Camel)
245+
246+
iterVarName := fmt.Sprintf("iter%d", idx)
247+
aggRefName := fmt.Sprintf("resolved%d", idx)
248+
249+
// base case for references in a list
250+
outPrefix += fmt.Sprintf("%sif len(%s) > 0 {\n", indent, fieldAccessPrefix)
251+
outPrefix += fmt.Sprintf("%s\t%s := %s{}\n", indent, aggRefName, field.GoType)
252+
outPrefix += fmt.Sprintf("%s\tfor _, %s := range %s {\n", indent, iterVarName, fieldAccessPrefix)
253+
254+
fieldAccessPrefix = iterVarName
255+
outPrefix += fmt.Sprintf("%s\t\tarr := %s.From\n", indent, fieldAccessPrefix)
256+
outPrefix += fmt.Sprintf("%s\t\tif arr == nil || arr.Name == nil || *arr.Name == \"\" {\n", indent)
257+
outPrefix += fmt.Sprintf("%s\t\t\treturn fmt.Errorf(\"provided resource reference is nil or empty: %s\")\n", indent, field.ReferenceFieldPath())
258+
outPrefix += fmt.Sprintf("%s\t\t}\n", indent)
259+
260+
outPrefix += getReferencedStateForField(field, indentLevel+idx+1)
261+
262+
outPrefix += fmt.Sprintf("%s\t\t%s = append(%s, (%s)(obj.%s))\n", indent, aggRefName, aggRefName, field.ShapeRef.Shape.MemberRef.GoType(), field.FieldConfig.References.Path)
263+
outPrefix += fmt.Sprintf("%s\t}\n", indent)
264+
outPrefix += fmt.Sprintf("%s\t%s = %s\n", indent, targetVarName, aggRefName)
265+
outPrefix += fmt.Sprintf("%s}\n", indent)
266+
case ("map"):
267+
panic("references cannot be within a map")
268+
default:
269+
// base case for single references
270+
fieldAccessPrefix = fmt.Sprintf("%s.%s", fieldAccessPrefix, cur.GetReferenceFieldName().Camel)
271+
272+
outPrefix += fmt.Sprintf("%sif %s != nil && %s.From != nil {\n", indent, fieldAccessPrefix, fieldAccessPrefix)
273+
outPrefix += fmt.Sprintf("%s\tarr := %s.From\n", indent, fieldAccessPrefix)
274+
outPrefix += fmt.Sprintf("%s\tif arr == nil || arr.Name == nil || *arr.Name == \"\" {\n", indent)
275+
outPrefix += fmt.Sprintf("%s\t\treturn fmt.Errorf(\"provided resource reference is nil or empty: %s\")\n", indent, field.ReferenceFieldPath())
276+
outPrefix += fmt.Sprintf("%s\t}\n", indent)
277+
278+
outPrefix += getReferencedStateForField(field, indentLevel+idx)
279+
280+
outPrefix += fmt.Sprintf("%s\t%s = (%s)(obj.%s)\n", indent, targetVarName, field.GoType, field.FieldConfig.References.Path)
281+
outPrefix += fmt.Sprintf("%s}\n", indent)
282+
}
283+
}
284+
285+
return outPrefix + outSuffix
286+
}
287+
288+
func getReferencedStateForField(field *model.Field, indentLevel int) string {
289+
out := ""
290+
indent := strings.Repeat("\t", indentLevel)
291+
292+
if field.FieldConfig.References.ServiceName == "" {
293+
out += fmt.Sprintf("%s\tobj := &svcapitypes.%s{}\n", indent, field.FieldConfig.References.Resource)
294+
} else {
295+
out += fmt.Sprintf("%s\tobj := &%sapitypes.%s{}\n", indent, field.ReferencedServiceName(), field.FieldConfig.References.Resource)
296+
}
297+
out += fmt.Sprintf("%s\tif err := getReferencedResourceState_%s(ctx, apiReader, obj, *arr.Name, namespace); err != nil {\n", indent, field.FieldConfig.References.Resource)
298+
out += fmt.Sprintf("%s\t\treturn err\n", indent)
299+
out += fmt.Sprintf("%s\t}\n", indent)
300+
301+
return out
302+
}
303+
188304
func nestedStructNilCheck(path fieldpath.Path, fieldAccessPrefix string) string {
189305
out := ""
190306
fieldNamePrefix := ""

pkg/generate/code/resource_reference_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,160 @@ if ko.Spec.Routes != nil {
198198
return false || (ko.Spec.VPCRef != nil)`
199199
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
200200
}
201+
202+
func Test_ResolveReferencesForField_SingleReference(t *testing.T) {
203+
assert := assert.New(t)
204+
require := require.New(t)
205+
206+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
207+
&testutil.TestingModelOptions{
208+
GeneratorConfigFile: "generator-with-reference.yaml",
209+
})
210+
211+
crd := testutil.GetCRDByName(t, g, "Integration")
212+
require.NotNil(crd)
213+
expected :=
214+
` if ko.Spec.APIRef != nil && ko.Spec.APIRef.From != nil {
215+
arr := ko.Spec.APIRef.From
216+
if arr == nil || arr.Name == nil || *arr.Name == "" {
217+
return fmt.Errorf("provided resource reference is nil or empty: APIRef")
218+
}
219+
obj := &svcapitypes.API{}
220+
if err := getReferencedResourceState_API(ctx, apiReader, obj, *arr.Name, namespace); err != nil {
221+
return err
222+
}
223+
ko.Spec.APIID = (*string)(obj.Status.APIID)
224+
}
225+
`
226+
227+
field := crd.Fields["APIID"]
228+
assert.Equal(expected, code.ResolveReferencesForField(field, "ko", 1))
229+
}
230+
231+
func Test_ResolveReferencesForField_ReferencingARN(t *testing.T) {
232+
assert := assert.New(t)
233+
require := require.New(t)
234+
235+
g := testutil.NewModelForServiceWithOptions(t, "iam",
236+
&testutil.TestingModelOptions{
237+
GeneratorConfigFile: "generator.yaml",
238+
})
239+
240+
crd := testutil.GetCRDByName(t, g, "User")
241+
require.NotNil(crd)
242+
expected :=
243+
` if ko.Spec.PermissionsBoundaryRef != nil && ko.Spec.PermissionsBoundaryRef.From != nil {
244+
arr := ko.Spec.PermissionsBoundaryRef.From
245+
if arr == nil || arr.Name == nil || *arr.Name == "" {
246+
return fmt.Errorf("provided resource reference is nil or empty: PermissionsBoundaryRef")
247+
}
248+
obj := &svcapitypes.Policy{}
249+
if err := getReferencedResourceState_Policy(ctx, apiReader, obj, *arr.Name, namespace); err != nil {
250+
return err
251+
}
252+
ko.Spec.PermissionsBoundary = (*string)(obj.Status.ACKResourceMetadata.ARN)
253+
}
254+
`
255+
256+
field := crd.Fields["PermissionsBoundary"]
257+
assert.Equal(expected, code.ResolveReferencesForField(field, "ko", 1))
258+
}
259+
260+
func Test_ResolveReferencesForField_SliceOfReferences(t *testing.T) {
261+
assert := assert.New(t)
262+
require := require.New(t)
263+
264+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
265+
&testutil.TestingModelOptions{
266+
GeneratorConfigFile: "generator-with-reference.yaml",
267+
})
268+
269+
crd := testutil.GetCRDByName(t, g, "VpcLink")
270+
require.NotNil(crd)
271+
expected :=
272+
` if len(ko.Spec.SecurityGroupRefs) > 0 {
273+
resolved0 := []*string{}
274+
for _, iter0 := range ko.Spec.SecurityGroupRefs {
275+
arr := iter0.From
276+
if arr == nil || arr.Name == nil || *arr.Name == "" {
277+
return fmt.Errorf("provided resource reference is nil or empty: SecurityGroupRefs")
278+
}
279+
obj := &ec2apitypes.SecurityGroup{}
280+
if err := getReferencedResourceState_SecurityGroup(ctx, apiReader, obj, *arr.Name, namespace); err != nil {
281+
return err
282+
}
283+
resolved0 = append(resolved0, (*string)(obj.Status.ID))
284+
}
285+
ko.Spec.SecurityGroupIDs = resolved0
286+
}
287+
`
288+
289+
field := crd.Fields["SecurityGroupIDs"]
290+
assert.Equal(expected, code.ResolveReferencesForField(field, "ko", 1))
291+
}
292+
293+
func Test_ResolveReferencesForField_NestedSingleReference(t *testing.T) {
294+
assert := assert.New(t)
295+
require := require.New(t)
296+
297+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
298+
&testutil.TestingModelOptions{
299+
GeneratorConfigFile: "generator-with-nested-reference.yaml",
300+
})
301+
302+
crd := testutil.GetCRDByName(t, g, "Authorizer")
303+
require.NotNil(crd)
304+
expected :=
305+
` if ko.Spec.JWTConfiguration != nil {
306+
if ko.Spec.JWTConfiguration.IssuerRef != nil && ko.Spec.JWTConfiguration.IssuerRef.From != nil {
307+
arr := ko.Spec.JWTConfiguration.IssuerRef.From
308+
if arr == nil || arr.Name == nil || *arr.Name == "" {
309+
return fmt.Errorf("provided resource reference is nil or empty: JWTConfiguration.IssuerRef")
310+
}
311+
obj := &svcapitypes.API{}
312+
if err := getReferencedResourceState_API(ctx, apiReader, obj, *arr.Name, namespace); err != nil {
313+
return err
314+
}
315+
ko.Spec.JWTConfiguration.Issuer = (*string)(obj.Status.APIID)
316+
}
317+
}
318+
`
319+
320+
field := crd.Fields["JWTConfiguration.Issuer"]
321+
assert.Equal(expected, code.ResolveReferencesForField(field, "ko", 1))
322+
}
323+
324+
func Test_ResolveReferencesForField_SingleReference_DeeplyNested(t *testing.T) {
325+
assert := assert.New(t)
326+
require := require.New(t)
327+
328+
g := testutil.NewModelForServiceWithOptions(t, "s3",
329+
&testutil.TestingModelOptions{
330+
GeneratorConfigFile: "generator-with-nested-references.yaml",
331+
})
332+
333+
crd := testutil.GetCRDByName(t, g, "Bucket")
334+
require.NotNil(crd)
335+
336+
// the Go template has the appropriate nil checks to ensure the parent path exists
337+
expected :=
338+
` if ko.Spec.Logging != nil {
339+
if ko.Spec.Logging.LoggingEnabled != nil {
340+
if ko.Spec.Logging.LoggingEnabled.TargetBucketRef != nil && ko.Spec.Logging.LoggingEnabled.TargetBucketRef.From != nil {
341+
arr := ko.Spec.Logging.LoggingEnabled.TargetBucketRef.From
342+
if arr == nil || arr.Name == nil || *arr.Name == "" {
343+
return fmt.Errorf("provided resource reference is nil or empty: Logging.LoggingEnabled.TargetBucketRef")
344+
}
345+
obj := &svcapitypes.Bucket{}
346+
if err := getReferencedResourceState_Bucket(ctx, apiReader, obj, *arr.Name, namespace); err != nil {
347+
return err
348+
}
349+
ko.Spec.Logging.LoggingEnabled.TargetBucket = (*string)(obj.Spec.Name)
350+
}
351+
}
352+
}
353+
`
354+
355+
field := crd.Fields["Logging.LoggingEnabled.TargetBucket"]
356+
assert.Equal(expected, code.ResolveReferencesForField(field, "ko", 1))
357+
}

pkg/testdata/models/apis/iam/0000-00-00/generator.yaml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ ignore:
1212
- SAMLProvider
1313
- ServiceLinkedRole
1414
- ServiceSpecificCredential
15-
- User
15+
#- User
1616
- VirtualMFADevice
1717
resources:
1818
Role:
@@ -43,3 +43,28 @@ resources:
4343
type: "map[string]*bool"
4444
MyCustomInteger:
4545
type: "*int64"
46+
User:
47+
renames:
48+
operations:
49+
CreateUser:
50+
input_fields:
51+
UserName: Name
52+
fields:
53+
PermissionsBoundary:
54+
references:
55+
resource: Policy
56+
path: Status.ACKResourceMetadata.ARN
57+
set:
58+
# The input and output shapes are different...
59+
- from: PermissionsBoundary.PermissionsBoundaryArn
60+
# In order to support attaching zero or more policies to a user, we use
61+
# custom update code path code that uses the Attach/DetachUserPolicy API
62+
# calls to manage the set of PolicyARNs attached to this User.
63+
Policies:
64+
type: "[]*string"
65+
references:
66+
resource: Policy
67+
path: Status.ACKResourceMetadata.ARN
68+
Tags:
69+
compare:
70+
is_ignored: true

0 commit comments

Comments
 (0)