Skip to content

Commit 79311e5

Browse files
authored
code generation changes for resolving referenced resources (#246)
Issue #, if available: aws-controllers-k8s/community#1063 Description of changes: * Code generation changes for supporting resource reference * Corresponding runtime PR aws-controllers-k8s/runtime#62 * Generated code can be seen in `pkg/generate/code/resource_reference_test.go` file By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 82c294c commit 79311e5

File tree

14 files changed

+566
-4
lines changed

14 files changed

+566
-4
lines changed

pkg/generate/ack/controller.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var (
4242
controllerIncludePaths = []string{
4343
"config/controller/kustomization_def.yaml.tpl",
4444
"boilerplate.go.tpl",
45+
"pkg/resource/references_read_referenced_resource.go.tpl",
4546
"pkg/resource/sdk_find_read_one.go.tpl",
4647
"pkg/resource/sdk_find_get_attributes.go.tpl",
4748
"pkg/resource/sdk_find_read_many.go.tpl",
@@ -149,6 +150,12 @@ var (
149150
"GoCodeIncompleteLateInitialization": func(r *ackmodel.CRD, resVarName string, indentLevel int) string {
150151
return code.IncompleteLateInitialization(r.Config(), r, resVarName, indentLevel)
151152
},
153+
"GoCodeReferencesValidation": func(r *ackmodel.CRD, sourceVarName string, referenceFieldSuffix string, indentLevel int) string {
154+
return code.ReferenceFieldsValidation(r, sourceVarName, referenceFieldSuffix, indentLevel)
155+
},
156+
"GoCodeContainsReferences": func(r *ackmodel.CRD, sourceVarName string) string {
157+
return code.ReferenceFieldsPresent(r, sourceVarName)
158+
},
152159
}
153160
)
154161

@@ -195,6 +202,7 @@ func Controller(
195202
"identifiers.go.tpl",
196203
"manager.go.tpl",
197204
"manager_factory.go.tpl",
205+
"references.go.tpl",
198206
"resource.go.tpl",
199207
"sdk.go.tpl",
200208
}

pkg/generate/ack/runtime_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2222
k8srt "k8s.io/apimachinery/pkg/runtime"
23+
"sigs.k8s.io/controller-runtime/pkg/client"
2324

2425
"github.com/aws/aws-sdk-go/aws/session"
2526
"github.com/go-logr/logr"
@@ -121,6 +122,10 @@ func (frm *fakeRM) LateInitialize(context.Context, acktypes.AWSResource) (acktyp
121122
return nil, nil
122123
}
123124

125+
func (frm *fakeRM) ResolveReferences(context.Context, client.Reader, acktypes.AWSResource) (acktypes.AWSResource, error) {
126+
return nil, nil
127+
}
128+
124129
// This test is mostly just a hack to introduce a Go module dependency between
125130
// the ACK runtime library and the code generator. The code generator doesn't
126131
// actually depend on Go code in the ACK runtime, but it *produces* templated

pkg/generate/code/compare.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ func CompareResource(
128128
cfg.PrefixConfig.SpecField+"."+specField.Names.Camel, ".",
129129
)
130130

131+
// Use reflect.DeepEqual for comparing Reference fields because
132+
// some of reference fields are list of pointer to structs and
133+
// DeepEqual is easy way to compare them
134+
if specField.IsReference() {
135+
out += fmt.Sprintf("%sif !reflect.DeepEqual(%s, %s) {\n",
136+
indent, firstResAdaptedVarName, secondResAdaptedVarName)
137+
out += fmt.Sprintf("%s\t%s.Add(\"%s\", %s, %s)\n", indent,
138+
deltaVarName, fieldPath, firstResAdaptedVarName,
139+
secondResAdaptedVarName)
140+
out += fmt.Sprintf("%s}\n", indent)
141+
continue
142+
}
131143
memberShapeRef := specField.ShapeRef
132144
memberShape := memberShapeRef.Shape
133145

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package code
15+
16+
import (
17+
"fmt"
18+
"strings"
19+
20+
"github.com/aws-controllers-k8s/code-generator/pkg/model"
21+
)
22+
23+
// ReferenceFieldsValidation produces the go code to validate reference field and
24+
// corresponding identifier field.
25+
// Sample code:
26+
// if ko.Spec.APIIDRef != nil && ko.Spec.APIID != nil {
27+
// return ackerr.ResourceReferenceAndIDNotSupportedFor("APIID", "APIIDRef")
28+
// }
29+
// if ko.Spec.APIIDRef == nil && ko.Spec.APIID == nil {
30+
// return ackerr.ResourceReferenceOrIDRequiredFor("APIID", "APIIDRef")
31+
// }
32+
func ReferenceFieldsValidation(
33+
crd *model.CRD,
34+
sourceVarName string,
35+
referenceFieldSuffix string,
36+
indentLevel int,
37+
) string {
38+
out := ""
39+
for _, field := range crd.Fields {
40+
if field.HasReference() {
41+
indent := strings.Repeat("\t", indentLevel)
42+
// Validation to make sure both target field and reference are
43+
// not present at the same time in desired resource
44+
out += fmt.Sprintf("%sif %s.Spec.%s%s != nil"+
45+
" && %s.Spec.%s != nil {\n", indent, sourceVarName, field.Names.Camel,
46+
referenceFieldSuffix, sourceVarName, field.Names.Camel)
47+
out += fmt.Sprintf("%s\treturn "+
48+
"ackerr.ResourceReferenceAndIDNotSupportedFor(\"%s\", \"%s%s\")\n",
49+
indent,field.Names.Camel, field.Names.Camel, referenceFieldSuffix)
50+
out += fmt.Sprintf("%s}\n", indent)
51+
52+
// If the field is required, make sure either Ref or original
53+
// field is present in the resource
54+
if field.IsRequired() {
55+
out += fmt.Sprintf("%sif %s.Spec.%s%s == nil &&"+
56+
" %s.Spec.%s == nil {\n", indent, sourceVarName,
57+
field.Names.Camel, referenceFieldSuffix, sourceVarName,
58+
field.Names.Camel)
59+
out += fmt.Sprintf("%s\treturn "+
60+
"ackerr.ResourceReferenceOrIDRequiredFor(\"%s\", \"%s%s\")\n",
61+
indent, field.Names.Camel, field.Names.Camel,
62+
referenceFieldSuffix)
63+
out += fmt.Sprintf("%s}\n", indent)
64+
}
65+
}
66+
}
67+
return out
68+
}
69+
70+
// ReferenceFieldsPresent produces go code(logical condition) for finding whether
71+
// a non-nil reference field is present in a resource. This checks helps in deciding
72+
// whether ACK.ReferencesResolved condition should be added to resource status
73+
// Sample Code:
74+
// return false || ko.Spec.APIIDRef != nil
75+
func ReferenceFieldsPresent(
76+
crd *model.CRD,
77+
sourceVarName string,
78+
) string {
79+
out := "false"
80+
for _, field := range crd.Fields {
81+
if field.IsReference() {
82+
out += fmt.Sprintf(" || %s.Spec.%s != nil", sourceVarName,
83+
field.Names.Camel)
84+
}
85+
}
86+
return out
87+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package code_test
15+
16+
import (
17+
"testing"
18+
19+
"github.com/aws-controllers-k8s/code-generator/pkg/generate/code"
20+
"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func Test_ReferenceFieldsValidation_NoReferenceConfig(t *testing.T) {
26+
assert := assert.New(t)
27+
require := require.New(t)
28+
29+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
30+
&testutil.TestingModelOptions{
31+
GeneratorConfigFile: "generator-with-reference.yaml",
32+
})
33+
34+
crd := testutil.GetCRDByName(t, g, "Api")
35+
require.NotNil(crd)
36+
expected := ""
37+
assert.Equal(expected, code.ReferenceFieldsValidation(crd, "ko", "Ref", 1))
38+
}
39+
40+
func Test_ReferenceFieldsValidation_SingleReference(t *testing.T) {
41+
assert := assert.New(t)
42+
require := require.New(t)
43+
44+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
45+
&testutil.TestingModelOptions{
46+
GeneratorConfigFile: "generator-with-reference.yaml",
47+
})
48+
49+
crd := testutil.GetCRDByName(t, g, "Integration")
50+
require.NotNil(crd)
51+
expected :=
52+
` if ko.Spec.APIIDRef != nil && ko.Spec.APIID != nil {
53+
return ackerr.ResourceReferenceAndIDNotSupportedFor("APIID", "APIIDRef")
54+
}
55+
if ko.Spec.APIIDRef == nil && ko.Spec.APIID == nil {
56+
return ackerr.ResourceReferenceOrIDRequiredFor("APIID", "APIIDRef")
57+
}
58+
`
59+
assert.Equal(expected, code.ReferenceFieldsValidation(crd, "ko", "Ref", 1))
60+
}
61+
62+
func Test_ReferenceFieldsValidation_SliceOfReferences(t *testing.T) {
63+
assert := assert.New(t)
64+
require := require.New(t)
65+
66+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
67+
&testutil.TestingModelOptions{
68+
GeneratorConfigFile: "generator-with-reference.yaml",
69+
})
70+
71+
//NOTE: For the moment, we are substituting SecurityGroupId with ApiId
72+
// just to test code generation for slices of reference
73+
crd := testutil.GetCRDByName(t, g, "VpcLink")
74+
require.NotNil(crd)
75+
expected :=
76+
` if ko.Spec.SecurityGroupIDsRef != nil && ko.Spec.SecurityGroupIDs != nil {
77+
return ackerr.ResourceReferenceAndIDNotSupportedFor("SecurityGroupIDs", "SecurityGroupIDsRef")
78+
}
79+
`
80+
assert.Equal(expected, code.ReferenceFieldsValidation(crd, "ko", "Ref", 1))
81+
}
82+
83+
func Test_ReferenceFieldsPresent_NoReferenceConfig(t *testing.T) {
84+
assert := assert.New(t)
85+
require := require.New(t)
86+
87+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
88+
&testutil.TestingModelOptions{
89+
GeneratorConfigFile: "generator-with-reference.yaml",
90+
})
91+
92+
crd := testutil.GetCRDByName(t, g, "Api")
93+
require.NotNil(crd)
94+
expected := "false"
95+
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
96+
}
97+
98+
func Test_ReferenceFieldsPresent_SingleReference(t *testing.T) {
99+
assert := assert.New(t)
100+
require := require.New(t)
101+
102+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
103+
&testutil.TestingModelOptions{
104+
GeneratorConfigFile: "generator-with-reference.yaml",
105+
})
106+
107+
crd := testutil.GetCRDByName(t, g, "Integration")
108+
require.NotNil(crd)
109+
expected := "false || ko.Spec.APIIDRef != nil"
110+
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
111+
}
112+
113+
func Test_ReferenceFieldsPresent_SliceOfReferences(t *testing.T) {
114+
assert := assert.New(t)
115+
require := require.New(t)
116+
117+
g := testutil.NewModelForServiceWithOptions(t, "apigatewayv2",
118+
&testutil.TestingModelOptions{
119+
GeneratorConfigFile: "generator-with-reference.yaml",
120+
})
121+
122+
//NOTE: For the moment, we are substituting SecurityGroupId with ApiId
123+
// just to test code generation for slices of reference
124+
crd := testutil.GetCRDByName(t, g, "VpcLink")
125+
require.NotNil(crd)
126+
expected := "false || ko.Spec.SecurityGroupIDsRef != nil"
127+
assert.Equal(expected, code.ReferenceFieldsPresent(crd, "ko"))
128+
}

pkg/generate/config/field.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,31 @@ type LateInitializeConfig struct {
267267
MaxBackoffSeconds int `json:"max_backoff_seconds"`
268268
}
269269

270+
// ReferencesConfig contains the instructions for how to add the referenced resource
271+
// configuration for a field.
272+
// Example:
273+
// ```
274+
// Integration:
275+
// fields:
276+
// ApiId:
277+
// references:
278+
// resource: API
279+
// path: Status.APIID
280+
//```
281+
// The above configuration will result in generation of a new field 'APIIDRef'
282+
// of type 'AWSResourceReference' for ApiGatewayv2-Integration crd.
283+
// When 'APIIDRef' field is present in custom resource manifest, reconciler will
284+
// read the referred 'API' resource and copy the value from 'Status.APIID' in
285+
// 'Integration' resource's 'APIID' field
286+
type ReferencesConfig struct {
287+
// Resource mentions the K8s resource which is read to resolve the
288+
// reference
289+
Resource string `json:"resource"`
290+
// Path refers to the the path of field which should be copied
291+
// to resolve the reference
292+
Path string `json:"path"`
293+
}
294+
270295
// FieldConfig contains instructions to the code generator about how
271296
// to interpret the value of an Attribute and how to map it to a CRD's Spec or
272297
// Status field
@@ -325,4 +350,7 @@ type FieldConfig struct {
325350
// Late Initialize instructs the code generator how to handle the late initialization
326351
// of the field.
327352
LateInitialize *LateInitializeConfig `json:"late_initialize,omitempty"`
353+
// References instructs the code generator how to refer this field from
354+
// other custom resource
355+
References *ReferencesConfig `json:"references,omitempty"`
328356
}

pkg/model/crd.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,12 @@ func (r *CRD) HasShapeAsMember(toFind string) bool {
144144
}
145145
}
146146
for _, field := range r.SpecFields {
147-
if shapeHasMember(field.ShapeRef.Shape, toFind) {
147+
if field.ShapeRef != nil && shapeHasMember(field.ShapeRef.Shape, toFind) {
148148
return true
149149
}
150150
}
151151
for _, field := range r.StatusFields {
152-
if shapeHasMember(field.ShapeRef.Shape, toFind) {
152+
if field.ShapeRef != nil && shapeHasMember(field.ShapeRef.Shape, toFind) {
153153
return true
154154
}
155155
}
@@ -757,6 +757,17 @@ func (r *CRD) HasMember(
757757
return inSpec, inStatus
758758
}
759759

760+
// HasReferenceFields returns true if any of the fields in CRD is a reference
761+
// field. Otherwise returns false
762+
func (r *CRD) HasReferenceFields() bool {
763+
for _, field := range r.Fields {
764+
if field.IsReference() {
765+
return true
766+
}
767+
}
768+
return false
769+
}
770+
760771
// NewCRD returns a pointer to a new `ackmodel.CRD` struct that describes a
761772
// single top-level resource in an AWS service API
762773
func NewCRD(

0 commit comments

Comments
 (0)