Skip to content

Commit c6efa6a

Browse files
Add support for references within lists of structs (#332)
Issue #, if available: aws-controllers-k8s/community#1256 Description of changes: This pull request adds support for references that occur within lists of structs. It updates the generated `references.go` files in each controller to now iterate through the structs to check for the existence of references and again for resolving their individual references. Rather than creating a new slice of structs with resolved references, it instead resolves the references in place - updating the base field value and leaving the reference untouched. This pull request has been tested against the APIGatewayv2 and EC2 resource reference tests, as well as manually against the EC2 changes that inspired the original issue. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 52584bb commit c6efa6a

File tree

8 files changed

+556
-36
lines changed

8 files changed

+556
-36
lines changed

pkg/generate/ack/controller.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ package ack
1515

1616
import (
1717
"path/filepath"
18+
"sort"
1819
"strings"
1920
ttpl "text/template"
2021

2122
ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/config"
23+
"github.com/aws-controllers-k8s/code-generator/pkg/fieldpath"
2224
"github.com/aws-controllers-k8s/code-generator/pkg/generate/code"
2325
"github.com/aws-controllers-k8s/code-generator/pkg/generate/templateset"
2426
"github.com/aws-controllers-k8s/code-generator/pkg/model"
@@ -60,6 +62,9 @@ var (
6062
"ResourceExceptionCode": func(r *ackmodel.CRD, httpStatusCode int) string {
6163
return r.ExceptionCode(httpStatusCode)
6264
},
65+
"ConstructFieldPath": func(targetFieldPath string) *fieldpath.Path {
66+
return fieldpath.FromString(targetFieldPath)
67+
},
6368
"GoCodeSetExceptionMessageCheck": func(r *ackmodel.CRD, httpStatusCode int) string {
6469
return code.CheckExceptionMessage(r.Config(), r, httpStatusCode)
6570
},
@@ -259,6 +264,7 @@ func Controller(
259264
for serviceName := range referencedServiceNamesMap {
260265
referencedServiceNames = append(referencedServiceNames, serviceName)
261266
}
267+
sort.Strings(referencedServiceNames)
262268
cmdVars := &templateCmdVars{
263269
metaVars,
264270
snakeCasedCRDNames,

pkg/generate/code/resource_reference.go

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,26 @@ import (
2222
)
2323

2424
// ReferenceFieldsValidation produces the go code to validate reference field and
25-
// corresponding identifier field.
25+
// corresponding identifier field. Iterates through all references within
26+
// slices, if necessary.
27+
// for _, iter0 := range ko.Spec.Routes {
28+
// if iter0.GatewayRef != nil && iter0.GatewayID != nil {
29+
// return ackerr.ResourceReferenceAndIDNotSupportedFor("Routes.GatewayID", "Routes.GatewayRef")
30+
// }
31+
// }
2632
// Sample code:
2733
// if ko.Spec.APIRef != nil && ko.Spec.APIID != nil {
28-
// return ackerr.ResourceReferenceAndIDNotSupportedFor("APIID", "APIRef")
29-
// }
30-
// if ko.Spec.APIRef == nil && ko.Spec.APIID == nil {
31-
// return ackerr.ResourceReferenceOrIDRequiredFor("APIID", "APIRef")
32-
// }
34+
// return ackerr.ResourceReferenceAndIDNotSupportedFor("APIID", "APIRef")
35+
// }
36+
// if ko.Spec.APIRef == nil && ko.Spec.APIID == nil {
37+
// return ackerr.ResourceReferenceOrIDRequiredFor("APIID", "APIRef")
38+
// }
3339
func ReferenceFieldsValidation(
3440
crd *model.CRD,
3541
sourceVarName string,
3642
indentLevel int,
3743
) string {
3844
out := ""
39-
fieldAccessPrefix := fmt.Sprintf("%s%s", sourceVarName,
40-
crd.Config().PrefixConfig.SpecField)
4145
// Sorted fieldnames are used for consistent code-generation
4246
for _, fieldName := range crd.SortedFieldNames() {
4347
field := crd.Fields[fieldName]
@@ -47,20 +51,48 @@ func ReferenceFieldsValidation(
4751
fp := fieldpath.FromString(field.Path)
4852
// remove fieldName from fieldPath before adding nil checks
4953
fp.Pop()
50-
fieldNamePrefix := crd.Config().PrefixConfig.SpecField
54+
55+
// prefix of the field path for referencing in the model
56+
fieldNamePrefix := ""
57+
// prefix of the field path for the generated code
58+
pathVarPrefix := fmt.Sprintf("%s%s", sourceVarName, crd.Config().PrefixConfig.SpecField)
59+
5160
// this loop outputs a nil-guard for each level of nested field path
61+
// or an iterator for any level that is a slice
62+
fieldDepth := 0
5263
for fp.Size() > 0 {
5364
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)
65+
currentField := fp.PopFront()
66+
67+
if fieldNamePrefix == "" {
68+
fieldNamePrefix = currentField
69+
} else {
70+
fieldNamePrefix = fmt.Sprintf("%s.%s", fieldNamePrefix, currentField)
71+
}
72+
pathVarPrefix = fmt.Sprintf("%s.%s", pathVarPrefix, currentField)
73+
74+
fieldConfig, ok := crd.Fields[fieldNamePrefix]
75+
if !ok {
76+
panic(fmt.Sprintf("CRD %s has no Field with path %s", crd.Kind, fieldNamePrefix))
77+
}
78+
79+
if fieldConfig.ShapeRef.Shape.Type == "list" {
80+
out += fmt.Sprintf("%sfor _, iter%d := range %s {\n", fIndent, fieldDepth, pathVarPrefix)
81+
// reset the path variable name
82+
pathVarPrefix = fmt.Sprintf("iter%d", fieldDepth)
83+
} else {
84+
out += fmt.Sprintf("%sif %s != nil {\n", fIndent, pathVarPrefix)
85+
}
86+
5687
fIndentLevel++
88+
fieldDepth++
5789
}
90+
5891
fIndent = strings.Repeat("\t", fIndentLevel)
5992
// Validation to make sure both target field and reference are
6093
// not present at the same time in desired resource
6194
out += fmt.Sprintf("%sif %s.%s != nil"+
62-
" && %s.%s != nil {\n", fIndent, fieldAccessPrefix,
63-
field.ReferenceFieldPath(), fieldAccessPrefix, field.Path)
95+
" && %s.%s != nil {\n", fIndent, pathVarPrefix, field.GetReferenceFieldName().Camel, pathVarPrefix, field.Names.Camel)
6496
out += fmt.Sprintf("%s\treturn "+
6597
"ackerr.ResourceReferenceAndIDNotSupportedFor(\"%s\", \"%s\")\n",
6698
fIndent, field.Path, field.ReferenceFieldPath())
@@ -78,8 +110,8 @@ func ReferenceFieldsValidation(
78110
// field is present in the resource
79111
if field.IsRequired() {
80112
out += fmt.Sprintf("%sif %s.%s == nil &&"+
81-
" %s.%s == nil {\n", fIndent, fieldAccessPrefix,
82-
field.ReferenceFieldPath(), fieldAccessPrefix, field.Path)
113+
" %s.%s == nil {\n", fIndent, pathVarPrefix,
114+
field.ReferenceFieldPath(), pathVarPrefix, field.Path)
83115
out += fmt.Sprintf("%s\treturn "+
84116
"ackerr.ResourceReferenceOrIDRequiredFor(\"%s\", \"%s\")\n",
85117
fIndent, field.Path, field.ReferenceFieldPath())
@@ -94,32 +126,65 @@ func ReferenceFieldsValidation(
94126
// a non-nil reference field is present in a resource. This checks helps in deciding
95127
// whether ACK.ReferencesResolved condition should be added to resource status
96128
// Sample Code:
129+
// if ko.Spec.Routes != nil {
130+
// for _, iter35 := range ko.Spec.Routes {
131+
// if iter35.GatewayRef != nil {
132+
// return true
133+
// }
134+
// }
135+
// }
97136
// return false || (ko.Spec.APIRef != nil)
98137
func ReferenceFieldsPresent(
99138
crd *model.CRD,
100139
sourceVarName string,
101140
) string {
102-
out := "false"
141+
iteratorsOut := ""
142+
returnOut := "return false"
103143
fieldAccessPrefix := fmt.Sprintf("%s%s", sourceVarName,
104144
crd.Config().PrefixConfig.SpecField)
105145
// Sorted fieldnames are used for consistent code-generation
106-
for _, fieldName := range crd.SortedFieldNames() {
146+
for fieldIndex, fieldName := range crd.SortedFieldNames() {
107147
field := crd.Fields[fieldName]
108148
if field.HasReference() {
109-
out += " || ("
110149
fp := fieldpath.FromString(field.Path)
111150
// remove fieldName from fieldPath before adding nil checks
112151
// for nested fieldPath
113152
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)
153+
154+
// Determine whether the field is nested
155+
if fp.Size() > 0 {
156+
// Determine whether the field is inside a slice
157+
parentField, ok := crd.Fields[fp.String()]
158+
if !ok {
159+
panic(fmt.Sprintf("CRD %s has no Field with path %s", crd.Kind, fp.String()))
160+
}
161+
162+
if parentField.ShapeRef.Shape.Type == "list" {
163+
iteratorsOut += fmt.Sprintf("if %s {\n", nestedStructNilCheck(*fp.Copy(), fieldAccessPrefix))
164+
iteratorsOut += fmt.Sprintf("\tfor _, iter%d := range %s.%s {\n", fieldIndex, fieldAccessPrefix, parentField.Path)
165+
iteratorsOut += fmt.Sprintf("\t\tif iter%d.%s != nil {\n", fieldIndex, field.GetReferenceFieldName().Camel)
166+
iteratorsOut += fmt.Sprintf("\t\t\treturn true\n")
167+
iteratorsOut += fmt.Sprintf("\t\t}\n")
168+
iteratorsOut += fmt.Sprintf("\t}\n")
169+
iteratorsOut += fmt.Sprintf("}\n")
170+
continue
171+
}
118172
}
119-
out += fmt.Sprintf("%s.%s != nil", fieldAccessPrefix,
173+
174+
nilCheck := nestedStructNilCheck(*fp.Copy(), fieldAccessPrefix) + " && " + fmt.Sprintf("%s.%s != nil", fieldAccessPrefix,
120175
field.ReferenceFieldPath())
121-
out += ")"
176+
returnOut += " || (" + strings.TrimPrefix(nilCheck, " && ") + ")"
122177
}
123178
}
124-
return out
179+
return iteratorsOut + returnOut
180+
}
181+
182+
func nestedStructNilCheck(path fieldpath.Path, fieldAccessPrefix string) string {
183+
out := ""
184+
fieldNamePrefix := ""
185+
for path.Size() > 0 {
186+
fieldNamePrefix = fmt.Sprintf("%s.%s", fieldNamePrefix, path.PopFront())
187+
out += fmt.Sprintf("%s%s != nil && ", fieldAccessPrefix, fieldNamePrefix)
188+
}
189+
return strings.TrimSuffix(out, " && ")
125190
}

pkg/generate/code/resource_reference_test.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func Test_ReferenceFieldsPresent_NoReferenceConfig(t *testing.T) {
118118

119119
crd := testutil.GetCRDByName(t, g, "Api")
120120
require.NotNil(crd)
121-
expected := "false"
121+
expected := "return false"
122122
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
123123
}
124124

@@ -133,7 +133,7 @@ func Test_ReferenceFieldsPresent_SingleReference(t *testing.T) {
133133

134134
crd := testutil.GetCRDByName(t, g, "Integration")
135135
require.NotNil(crd)
136-
expected := "false || (ko.Spec.APIRef != nil)"
136+
expected := "return false || (ko.Spec.APIRef != nil)"
137137
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
138138
}
139139

@@ -150,7 +150,7 @@ func Test_ReferenceFieldsPresent_SliceOfReferences(t *testing.T) {
150150
// just to test code generation for slices of reference
151151
crd := testutil.GetCRDByName(t, g, "VpcLink")
152152
require.NotNil(crd)
153-
expected := "false || (ko.Spec.SecurityGroupRefs != nil) || (ko.Spec.SubnetRefs != nil)"
153+
expected := "return false || (ko.Spec.SecurityGroupRefs != nil) || (ko.Spec.SubnetRefs != nil)"
154154
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
155155
}
156156

@@ -165,6 +165,36 @@ func Test_ReferenceFieldsPresent_NestedReference(t *testing.T) {
165165

166166
crd := testutil.GetCRDByName(t, g, "Authorizer")
167167
require.NotNil(crd)
168-
expected := "false || (ko.Spec.JWTConfiguration != nil && ko.Spec.JWTConfiguration.IssuerRef != nil)"
168+
expected := "return false || (ko.Spec.JWTConfiguration != nil && ko.Spec.JWTConfiguration.IssuerRef != nil)"
169+
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
170+
}
171+
172+
func Test_ReferenceFieldsPresent_NestedSliceOfStructsReference(t *testing.T) {
173+
assert := assert.New(t)
174+
require := require.New(t)
175+
176+
g := testutil.NewModelForServiceWithOptions(t, "ec2",
177+
&testutil.TestingModelOptions{
178+
GeneratorConfigFile: "generator-with-nested-reference.yaml",
179+
})
180+
181+
crd := testutil.GetCRDByName(t, g, "RouteTable")
182+
require.NotNil(crd)
183+
expected :=
184+
`if ko.Spec.Routes != nil {
185+
for _, iter32 := range ko.Spec.Routes {
186+
if iter32.GatewayRef != nil {
187+
return true
188+
}
189+
}
190+
}
191+
if ko.Spec.Routes != nil {
192+
for _, iter35 := range ko.Spec.Routes {
193+
if iter35.NATGatewayRef != nil {
194+
return true
195+
}
196+
}
197+
}
198+
return false || (ko.Spec.VPCRef != nil)`
169199
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
170200
}

pkg/model/crd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@ func (r *CRD) ReferencedServiceNames() (serviceNames []string) {
703703
for serviceName, _ := range serviceNamesMap {
704704
serviceNames = append(serviceNames, serviceName)
705705
}
706+
sort.Strings(serviceNames)
706707
return serviceNames
707708
}
708709

pkg/model/model.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,16 @@ func updateTypeDefAttributeWithReference(fieldPath string, tdefs []*TypeDef, crd
498498
// the beginning of field path and leave rest of nested member names as is.
499499
// Ex: ResourcesVpcConfig.SecurityGroupIDs will become VPCConfigRequest.SecurityGroupIDs
500500
// for Cluster resource in eks-controller.
501-
specFieldShapeName := specField.ShapeRef.ShapeName
501+
specFieldShapeRef := specField.ShapeRef
502+
specFieldShapeName := specFieldShapeRef.ShapeName
503+
switch shapeType := specFieldShapeRef.Shape.Type; shapeType {
504+
case "list":
505+
specFieldShapeName = specField.ShapeRef.Shape.MemberRef.ShapeName
506+
specFieldShapeRef = &specField.ShapeRef.Shape.MemberRef
507+
case "map":
508+
specFieldShapeName = specField.ShapeRef.Shape.ValueRef.ShapeName
509+
specFieldShapeRef = &specField.ShapeRef.Shape.ValueRef
510+
}
502511
fieldShapePath := strings.Replace(fieldPath, specFieldName, specFieldShapeName, 1)
503512
fsp := ackfp.FromString(fieldShapePath)
504513

@@ -509,7 +518,7 @@ func updateTypeDefAttributeWithReference(fieldPath string, tdefs []*TypeDef, crd
509518
// "fieldName" as attribute. To add a corresponding reference for "fieldName"
510519
// , we will add new attribute in TypeDef for "parentFieldName".
511520
parentFieldName := fsp.Back()
512-
parentFieldShapeRef := fsp.ShapeRef(specField.ShapeRef)
521+
parentFieldShapeRef := fsp.ShapeRef(specFieldShapeRef)
513522
if parentFieldShapeRef == nil {
514523
panic(fmt.Sprintf("Unable to find a shape member with name %s"+
515524
" to add a reference for %s", parentFieldName, fieldPath))

pkg/model/model_ec2_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
package model_test
1515

1616
import (
17+
"strings"
1718
"testing"
1819

20+
"github.com/aws-controllers-k8s/code-generator/pkg/model"
21+
1922
"github.com/stretchr/testify/assert"
2023
"github.com/stretchr/testify/require"
2124

@@ -152,3 +155,31 @@ func TestEC2_Volume(t *testing.T) {
152155
// field
153156
assert.NotNil(testutil.GetTypeDefByName(t, g, "VolumeAttachment"))
154157
}
158+
159+
func TestEC2_NestedReference(t *testing.T) {
160+
assert := assert.New(t)
161+
162+
g := testutil.NewModelForServiceWithOptions(t, "ec2", &testutil.TestingModelOptions{
163+
GeneratorConfigFile: "generator-with-nested-reference.yaml",
164+
})
165+
166+
tds, err := g.GetTypeDefs()
167+
assert.Nil(err)
168+
assert.NotNil(tds)
169+
170+
var createRouteInputTD *model.TypeDef
171+
172+
for _, td := range tds {
173+
if td != nil && strings.EqualFold(td.Names.Original, "createRouteInput") {
174+
createRouteInputTD = td
175+
break
176+
}
177+
}
178+
assert.NotNil(t, createRouteInputTD)
179+
gatewayIdAttr := createRouteInputTD.GetAttribute("GatewayId")
180+
gatewayRefAttr := createRouteInputTD.GetAttribute("GatewayRef")
181+
182+
assert.Equal("GatewayID", gatewayIdAttr.Names.Camel)
183+
assert.Equal("GatewayRef", gatewayRefAttr.Names.Camel)
184+
assert.Equal("*ackv1alpha1.AWSResourceReferenceWrapper", gatewayRefAttr.GoType)
185+
}

0 commit comments

Comments
 (0)