Skip to content

Commit 8a55c53

Browse files
authored
Support for nested resource references (#314)
Issue #, if available: aws-controllers-k8s/community#1212 This PR aims to add support for adding ResourceReferences for nested fields. I have tested these changes locally by generating and compiling *eks-controller* and *apigatewayv2-controller*, and also added unit tests. In a nut-shell, this PR mainly updates the reference resolution to use `field.Path` instead of `field.Names` with proper nil checks. This PR also updates the TypeDefs to add new *AWSResourceReference* attribute, instead of only supporting Reference fields at top-level. More detailed desciption about the changes is present below. Note: I have broken all the changes into multiple commits and i recommend to review this PR commit-by-commit. : ) Description of changes: * Add new reusable go code generator method for producing Nil check logical condition on a *fieldPath* * Wrap the *ReferenceFieldValidation* with proper Nil check for a fieldPath and correctly use *field.Path* for accessing the value instead of `field.Names.Camel` * Include Nil checks inside `ReferenceFieldPresent` method for the `field.Path` and capture the ReferenceFieldPresent condition for each field inside parentheses to make it more readable. * Make the ResourceFieldConfig lookup case-insensitive for better developer experience . This will allow developers to not worry about case-sensitivity when creating FieldConfig keys inside `generator.yaml` * Update `crd.HasReference` method to work for nested fields as well. * Update *TypeDef* generation inside `model.go` to also include new *AWSResourceReference* attribute if a nested field *ReferencesConfig* is present. * Update `references.go.tpl` to correctly use *field.Path* instead of *field.Names* to resolve references. * Update `references.go.tpl` to support string type-alias by performing type-casting before resolving a reference.(issue# aws-controllers-k8s/community#1213) By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent f09c4e3 commit 8a55c53

File tree

16 files changed

+468
-40
lines changed

16 files changed

+468
-40
lines changed

pkg/generate/ack/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ var (
158158
"GoCodeContainsReferences": func(r *ackmodel.CRD, sourceVarName string) string {
159159
return code.ReferenceFieldsPresent(r, sourceVarName)
160160
},
161+
"CheckNilFieldPath": func(f *ackmodel.Field, sourceVarName string) string {
162+
return code.CheckNilFieldPath(f, sourceVarName)
163+
},
161164
}
162165
)
163166

pkg/generate/code/check.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
2121

22+
"github.com/aws-controllers-k8s/code-generator/pkg/fieldpath"
2223
ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/generate/config"
2324
"github.com/aws-controllers-k8s/code-generator/pkg/model"
2425
)
@@ -192,3 +193,26 @@ func checkRequiredFieldsMissingFromShapeReadMany(
192193
result = fmt.Sprintf("%s == nil", resVarPath)
193194
return fmt.Sprintf("%sreturn %s\n", indent, result)
194195
}
196+
197+
// CheckNilFieldPath returns the condition statement for Nil check
198+
// on a field path. This nil check on field path is useful to avoid
199+
// nil pointer panics when accessing a field value.
200+
//
201+
// This function only outputs the logical condition and not the "if" block
202+
// so that the output can be reused in many templates, where
203+
// logic inside "if" block can be different.
204+
//
205+
// Example Output for fieldpath "JWTConfiguration.Issuer.SomeField" is
206+
// "ko.Spec.JWTConfiguration == nil || ko.Spec.JWTConfiguration.Issuer == nil"
207+
func CheckNilFieldPath(field *model.Field, sourceVarName string) string {
208+
out := ""
209+
fp := fieldpath.FromString(field.Path)
210+
// remove fieldName from fieldPath before adding nil checks
211+
fp.Pop()
212+
fieldNamePrefix := ""
213+
for fp.Size() > 0 {
214+
fieldNamePrefix = fmt.Sprintf("%s.%s", fieldNamePrefix, fp.PopFront())
215+
out += fmt.Sprintf(" || %s%s == nil", sourceVarName, fieldNamePrefix)
216+
}
217+
return strings.TrimPrefix(out, " || ")
218+
}

pkg/generate/code/check_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,22 @@ func TestCheckRequiredFields_StatusField_ReadMany(t *testing.T) {
136136
strings.TrimSpace(gotCode),
137137
)
138138
}
139+
140+
func TestCheckNilFieldPath(t *testing.T) {
141+
// Empty FieldPath
142+
field := model.Field{Path: ""}
143+
assert.Equal(t, "", code.CheckNilFieldPath(&field, "ko.Spec"))
144+
// FieldPath only has fieldName
145+
field.Path = "JWTConfiguration"
146+
assert.Equal(t, "", code.CheckNilFieldPath(&field, "ko.Spec"))
147+
// Nested FieldPath
148+
field.Path = "JWTConfiguration.Issuer"
149+
assert.Equal(t,
150+
"ko.Spec.JWTConfiguration == nil",
151+
code.CheckNilFieldPath(&field, "ko.Spec"))
152+
// Multi Level Nested FieldPath
153+
field.Path = "JWTConfiguration.Issuer.FieldName"
154+
assert.Equal(t,
155+
"ko.Spec.JWTConfiguration == nil || ko.Spec.JWTConfiguration.Issuer == nil",
156+
code.CheckNilFieldPath(&field, "ko.Spec"))
157+
}

pkg/generate/code/resource_reference.go

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"fmt"
1818
"strings"
1919

20+
"github.com/aws-controllers-k8s/code-generator/pkg/fieldpath"
2021
"github.com/aws-controllers-k8s/code-generator/pkg/model"
2122
)
2223

@@ -35,32 +36,54 @@ func ReferenceFieldsValidation(
3536
indentLevel int,
3637
) string {
3738
out := ""
39+
fieldAccessPrefix := fmt.Sprintf("%s%s", sourceVarName,
40+
crd.Config().PrefixConfig.SpecField)
3841
// Sorted fieldnames are used for consistent code-generation
3942
for _, fieldName := range crd.SortedFieldNames() {
4043
field := crd.Fields[fieldName]
44+
var fIndent string
4145
if field.HasReference() {
42-
indent := strings.Repeat("\t", indentLevel)
46+
fIndentLevel := indentLevel
47+
fp := fieldpath.FromString(field.Path)
48+
// remove fieldName from fieldPath before adding nil checks
49+
fp.Pop()
50+
fieldNamePrefix := crd.Config().PrefixConfig.SpecField
51+
// this loop outputs a nil-guard for each level of nested field path
52+
for fp.Size() > 0 {
53+
fIndent = strings.Repeat("\t", fIndentLevel)
54+
fieldNamePrefix = fmt.Sprintf("%s.%s", fieldNamePrefix, fp.PopFront())
55+
out += fmt.Sprintf("%sif %s%s != nil {\n", fIndent, sourceVarName, fieldNamePrefix)
56+
fIndentLevel++
57+
}
58+
fIndent = strings.Repeat("\t", fIndentLevel)
4359
// Validation to make sure both target field and reference are
4460
// not present at the same time in desired resource
45-
out += fmt.Sprintf("%sif %s.Spec.%s != nil"+
46-
" && %s.Spec.%s != nil {\n", indent, sourceVarName,
47-
field.GetReferenceFieldName().Camel, sourceVarName, field.Names.Camel)
61+
out += fmt.Sprintf("%sif %s.%s != nil"+
62+
" && %s.%s != nil {\n", fIndent, fieldAccessPrefix,
63+
field.ReferenceFieldPath(), fieldAccessPrefix, field.Path)
4864
out += fmt.Sprintf("%s\treturn "+
4965
"ackerr.ResourceReferenceAndIDNotSupportedFor(\"%s\", \"%s\")\n",
50-
indent, field.Names.Camel, field.GetReferenceFieldName().Camel)
51-
out += fmt.Sprintf("%s}\n", indent)
66+
fIndent, field.Path, field.ReferenceFieldPath())
67+
68+
// Close out all the curly braces with proper indentation
69+
for fIndentLevel >= indentLevel {
70+
fIndent = strings.Repeat("\t", fIndentLevel)
71+
out += fmt.Sprintf("%s}\n", fIndent)
72+
fIndentLevel--
73+
}
74+
75+
fIndent = strings.Repeat("\t", indentLevel)
5276

5377
// If the field is required, make sure either Ref or original
5478
// field is present in the resource
5579
if field.IsRequired() {
56-
out += fmt.Sprintf("%sif %s.Spec.%s == nil &&"+
57-
" %s.Spec.%s == nil {\n", indent, sourceVarName,
58-
field.GetReferenceFieldName().Camel, sourceVarName,
59-
field.Names.Camel)
80+
out += fmt.Sprintf("%sif %s.%s == nil &&"+
81+
" %s.%s == nil {\n", fIndent, fieldAccessPrefix,
82+
field.ReferenceFieldPath(), fieldAccessPrefix, field.Path)
6083
out += fmt.Sprintf("%s\treturn "+
6184
"ackerr.ResourceReferenceOrIDRequiredFor(\"%s\", \"%s\")\n",
62-
indent, field.Names.Camel, field.GetReferenceFieldName().Camel)
63-
out += fmt.Sprintf("%s}\n", indent)
85+
fIndent, field.Path, field.ReferenceFieldPath())
86+
out += fmt.Sprintf("%s}\n", fIndent)
6487
}
6588
}
6689
}
@@ -71,18 +94,31 @@ func ReferenceFieldsValidation(
7194
// a non-nil reference field is present in a resource. This checks helps in deciding
7295
// whether ACK.ReferencesResolved condition should be added to resource status
7396
// Sample Code:
74-
// return false || ko.Spec.APIRef != nil
97+
// return false || (ko.Spec.APIRef != nil)
7598
func ReferenceFieldsPresent(
7699
crd *model.CRD,
77100
sourceVarName string,
78101
) string {
79102
out := "false"
103+
fieldAccessPrefix := fmt.Sprintf("%s%s", sourceVarName,
104+
crd.Config().PrefixConfig.SpecField)
80105
// Sorted fieldnames are used for consistent code-generation
81106
for _, fieldName := range crd.SortedFieldNames() {
82107
field := crd.Fields[fieldName]
83-
if field.IsReference() {
84-
out += fmt.Sprintf(" || %s.Spec.%s != nil", sourceVarName,
85-
field.Names.Camel)
108+
if field.HasReference() {
109+
out += " || ("
110+
fp := fieldpath.FromString(field.Path)
111+
// remove fieldName from fieldPath before adding nil checks
112+
// for nested fieldPath
113+
fp.Pop()
114+
fieldNamePrefix := ""
115+
for fp.Size() > 0 {
116+
fieldNamePrefix = fmt.Sprintf("%s.%s", fieldNamePrefix, fp.PopFront())
117+
out += fmt.Sprintf("%s%s != nil && ", fieldAccessPrefix, fieldNamePrefix)
118+
}
119+
out += fmt.Sprintf("%s.%s != nil", fieldAccessPrefix,
120+
field.ReferenceFieldPath())
121+
out += ")"
86122
}
87123
}
88124
return out

pkg/generate/code/resource_reference_test.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,27 @@ func Test_ReferenceFieldsValidation_SliceOfReferences(t *testing.T) {
8686
assert.Equal(expected, code.ReferenceFieldsValidation(crd, "ko", 1))
8787
}
8888

89+
func Test_ReferenceFieldsValidation_NestedReference(t *testing.T) {
90+
assert := assert.New(t)
91+
require := require.New(t)
92+
93+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
94+
&testutil.TestingModelOptions{
95+
GeneratorConfigFile: "generator-with-nested-reference.yaml",
96+
})
97+
98+
crd := testutil.GetCRDByName(t, g, "Authorizer")
99+
require.NotNil(crd)
100+
expected :=
101+
` if ko.Spec.JWTConfiguration != nil {
102+
if ko.Spec.JWTConfiguration.IssuerRef != nil && ko.Spec.JWTConfiguration.Issuer != nil {
103+
return ackerr.ResourceReferenceAndIDNotSupportedFor("JWTConfiguration.Issuer", "JWTConfiguration.IssuerRef")
104+
}
105+
}
106+
`
107+
assert.Equal(expected, code.ReferenceFieldsValidation(crd, "ko", 1))
108+
}
109+
89110
func Test_ReferenceFieldsPresent_NoReferenceConfig(t *testing.T) {
90111
assert := assert.New(t)
91112
require := require.New(t)
@@ -112,7 +133,7 @@ func Test_ReferenceFieldsPresent_SingleReference(t *testing.T) {
112133

113134
crd := testutil.GetCRDByName(t, g, "Integration")
114135
require.NotNil(crd)
115-
expected := "false || ko.Spec.APIRef != nil"
136+
expected := "false || (ko.Spec.APIRef != nil)"
116137
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
117138
}
118139

@@ -129,6 +150,21 @@ func Test_ReferenceFieldsPresent_SliceOfReferences(t *testing.T) {
129150
// just to test code generation for slices of reference
130151
crd := testutil.GetCRDByName(t, g, "VpcLink")
131152
require.NotNil(crd)
132-
expected := "false || ko.Spec.SecurityGroupRefs != nil || ko.Spec.SubnetRefs != nil"
153+
expected := "false || (ko.Spec.SecurityGroupRefs != nil) || (ko.Spec.SubnetRefs != nil)"
154+
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
155+
}
156+
157+
func Test_ReferenceFieldsPresent_NestedReference(t *testing.T) {
158+
assert := assert.New(t)
159+
require := require.New(t)
160+
161+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
162+
&testutil.TestingModelOptions{
163+
GeneratorConfigFile: "generator-with-nested-reference.yaml",
164+
})
165+
166+
crd := testutil.GetCRDByName(t, g, "Authorizer")
167+
require.NotNil(crd)
168+
expected := "false || (ko.Spec.JWTConfiguration != nil && ko.Spec.JWTConfiguration.IssuerRef != nil)"
133169
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
134170
}

pkg/generate/config/resource.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
package config
1515

1616
import (
17+
"strings"
18+
1719
"github.com/aws-controllers-k8s/code-generator/pkg/util"
1820
)
1921

@@ -392,6 +394,30 @@ func (c *Config) ResourceFields(resourceName string) map[string]*FieldConfig {
392394
return resourceConfig.Fields
393395
}
394396

397+
// ResourceFieldByPath returns the FieldConfig for a field from
398+
// "resourceName" crd, where field.Path matches the passed "fieldPath" parameter.
399+
// This method performs the case-insensitive resource and fieldPath lookup.
400+
func (c *Config) ResourceFieldByPath(resourceName string, fieldPath string) *FieldConfig {
401+
var resourceConfig ResourceConfig
402+
if c == nil {
403+
return nil
404+
}
405+
406+
for resName, resConfig := range c.Resources {
407+
if strings.EqualFold(resName, resourceName) {
408+
resourceConfig = resConfig
409+
break
410+
}
411+
}
412+
413+
for fPath, fConfig := range resourceConfig.Fields {
414+
if strings.EqualFold(fPath, fieldPath) {
415+
return fConfig
416+
}
417+
}
418+
return nil
419+
}
420+
395421
// GetCompareIgnoredFields returns the list of field path to ignore when
396422
// comparing two differnt objects
397423
func (c *Config) GetCompareIgnoredFields(resName string) []string {

pkg/model/crd.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,7 @@ func (r *CRD) AddSpecField(
199199
shapeRef *awssdkmodel.ShapeRef,
200200
) {
201201
fPath := memberNames.Camel
202-
fConfigs := r.cfg.ResourceFields(r.Names.Original)
203-
fConfig := fConfigs[memberNames.Original]
202+
fConfig := r.cfg.ResourceFieldByPath(r.Names.Original, fPath)
204203
f := NewField(r, fPath, memberNames, shapeRef, fConfig)
205204
if fConfig != nil && fConfig.Print != nil {
206205
r.addSpecPrintableColumn(f)
@@ -225,8 +224,7 @@ func (r *CRD) AddStatusField(
225224
shapeRef *awssdkmodel.ShapeRef,
226225
) {
227226
fPath := memberNames.Camel
228-
fConfigs := r.cfg.ResourceFields(r.Names.Original)
229-
fConfig := fConfigs[memberNames.Original]
227+
fConfig := r.cfg.ResourceFieldByPath(r.Names.Original, fPath)
230228
f := NewField(r, fPath, memberNames, shapeRef, fConfig)
231229
if fConfig != nil && fConfig.Print != nil {
232230
r.addStatusPrintableColumn(f)
@@ -787,7 +785,7 @@ func (r *CRD) HasMember(
787785
// field. Otherwise returns false
788786
func (r *CRD) HasReferenceFields() bool {
789787
for _, field := range r.Fields {
790-
if field.IsReference() {
788+
if field.HasReference() {
791789
return true
792790
}
793791
}

pkg/model/field.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,21 @@ func (f *Field) ReferencedResourceNamePlural() string {
178178
return referencedResourceName
179179
}
180180

181+
// ReferenceFieldPath returns the fieldPath for the corresponding
182+
// Reference field. It replaces the fieldName with ReferenceFieldName
183+
// at the end of fieldPath
184+
func (f *Field) ReferenceFieldPath() string {
185+
fieldPathPrefix := strings.TrimSuffix(f.Path, f.Names.Camel)
186+
return fmt.Sprintf("%s%s", fieldPathPrefix, f.GetReferenceFieldName().Camel)
187+
}
188+
189+
// FieldPathWithUnderscore replaces the period in fieldPath with
190+
// underscore. This method is useful for generating go method
191+
// name from the fieldPath.
192+
func (f *Field) FieldPathWithUnderscore() string {
193+
return strings.ReplaceAll(f.Path, ".", "_")
194+
}
195+
181196
// NewReferenceField returns a pointer to a new Field object.
182197
// The go-type of field is either slice of '*AWSResourceReferenceWrapper' or
183198
// '*AWSResourceReferenceWrapper' depending on whether 'shapeRef' parameter

pkg/model/field_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,35 @@ func TestGetReferenceFieldName(t *testing.T) {
195195
assert.Equal(tc.expectedReferenceFieldName, referenceFieldName, msg)
196196
}
197197
}
198+
199+
func TestReferenceFieldPath(t *testing.T) {
200+
assert := assert.New(t)
201+
202+
field := model.Field{}
203+
//Non nested fieldPath
204+
field.Path = "MyField"
205+
field.Names = names.New("MyField")
206+
assert.Equal("MyFieldRef", field.ReferenceFieldPath())
207+
208+
// Nested fieldPath
209+
field.Path = "subPathA.subPathB.MyField"
210+
field.Names = names.New("MyField")
211+
assert.Equal("subPathA.subPathB.MyFieldRef", field.ReferenceFieldPath())
212+
}
213+
214+
func TestFieldPathWithUnderscore(t *testing.T) {
215+
assert := assert.New(t)
216+
217+
field := model.Field{}
218+
//Empty fieldPath
219+
field.Path = ""
220+
assert.Equal("", field.FieldPathWithUnderscore())
221+
222+
//Non nested fieldPath
223+
field.Path = "MyField"
224+
assert.Equal("MyField", field.FieldPathWithUnderscore())
225+
226+
// Nested fieldPath
227+
field.Path = "subPathA.subPathB.MyField"
228+
assert.Equal("subPathA_subPathB_MyField", field.FieldPathWithUnderscore())
229+
}

0 commit comments

Comments
 (0)