Skip to content

Commit 7445de3

Browse files
authored
Support of injecting fields in existing structures: CustomNestedFields (#462)
**Issue #** This issue arises from the code changes in lambda-controller for[ PR #88](aws-controllers-k8s/lambda-controller#88) **Summary** Support of injecting fields in existing structures: CustomNestedFields **Description** The code in this PR helps in accepting a custom nestedField defined using `type` as a part of its parentField. In the issue mentioned above when we inject a new field `S3SHA256` into `Code` struct, instead of nesting it under `Code` the generator created a separate field named `CodeS3SHA256`. The new field `S3SHA256` is defined like this in `generator.yaml` file: ``` Function: fields: Code.S3SHA256: type: string ``` After applying the changes in this PR, the field `S3SHA256` is properly injected as a part of `Code` struct. ``` type FunctionCode struct { ImageURI *string `json:"imageURI,omitempty"` S3Bucket *string `json:"s3Bucket,omitempty"` S3BucketRef *ackv1alpha1.AWSResourceReferenceWrapper `json:"s3BucketRef,omitempty"` S3Key *string `json:"s3Key,omitempty"` S3ObjectVersion *string `json:"s3ObjectVersion,omitempty"` S3SHA256 *string `json:"s3SHA256,omitempty"` ZipFile []byte `json:"zipFile,omitempty"` } ``` This PR contains code implementation to inject Nested fields into the Spec or Status struct and has respective model-test to validate the changes. **Acknowledgment** By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 892f29d commit 7445de3

File tree

5 files changed

+212
-4
lines changed

5 files changed

+212
-4
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
.idea
55
/docs/site
66
bin
7-
build
7+
build

pkg/model/crd.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,87 @@ func (r *CRD) addAdditionalPrinterColumns(additionalColumns []*ackgenconfig.Addi
626626
}
627627
}
628628

629+
// checkSpecOrStatus checks whether the new nested field is for Spec or Status struct
630+
// and returns the top level field accordingly
631+
func (crd *CRD) checkSpecOrStatus(
632+
field string,
633+
) (*Field, bool) {
634+
fSpec, okSpec := crd.SpecFields[field]
635+
if okSpec {
636+
return fSpec, okSpec
637+
}
638+
fStatus, okStatus := crd.StatusFields[field]
639+
if okStatus {
640+
return fStatus, okStatus
641+
}
642+
return nil, false
643+
}
644+
645+
// addCustomNestedFields injects the provided customNestedFields into the
646+
// Spec or Status struct as a nested field. The customNestedFields are
647+
// identified by the field path. The field path is a dot separated string
648+
// that represents the path to the nested field. For example, if we want to
649+
// inject a field called "Password" into the "User" struct, the field path
650+
// would be "User.Password". The field path can be as deep as needed.
651+
func (crd *CRD) addCustomNestedFields(customNestedFields map[string]*ackgenconfig.FieldConfig) {
652+
// We have collected all the nested fields in `customNestedFields` map and now we can process
653+
// and validate that they indeed are inject-able (i.e. the parent field is of type struct)
654+
// and inject them into the Spec or Status struct.
655+
for customNestedField, customNestedFieldConfig := range customNestedFields {
656+
fieldParts := strings.Split(customNestedField, ".")
657+
// we know that the length of fieldParts is at least 2
658+
// it is safe to access the first element.
659+
topLevelField := fieldParts[0]
660+
661+
f, ok := crd.checkSpecOrStatus(topLevelField)
662+
663+
if ok && f.ShapeRef.Shape.Type != "structure" {
664+
// We need to panic here because the user is providing wrong configuration.
665+
msg := fmt.Sprintf("Expected parent field to be of type structure, but found %s", f.ShapeRef.Shape.Type)
666+
panic(msg)
667+
}
668+
669+
// If the provided top level field is not in the crd.SpecFields or crd.StatusFields...
670+
if !ok {
671+
// We need to panic here because the user is providing wrong configuration.
672+
msg := fmt.Sprintf("Expected top level field %s to be present in Spec or Status", topLevelField)
673+
panic(msg)
674+
}
675+
676+
// We will have to keep track of the previous field in the path
677+
// to check it's member fields.
678+
parentField := f
679+
680+
// loop over the all left fieldParts except the last one
681+
for _, currentFieldName := range fieldParts[1 : len(fieldParts)-1] {
682+
// Check if parentField contains current field
683+
currentField, ok := parentField.MemberFields[currentFieldName]
684+
if !ok || currentField.ShapeRef.Shape.Type != "structure" {
685+
// Check if the field exists AND is of type structure
686+
msg := fmt.Sprintf("Cannot inject field, %s member doesn't exist or isn't a structure", currentFieldName)
687+
panic(msg)
688+
}
689+
parentField = currentField
690+
}
691+
692+
// arriving here means that successfully walked the path and
693+
// parentField is the parent of the new field.
694+
695+
// the last part is the field name
696+
fieldName := fieldParts[len(fieldParts)-1]
697+
typeOverride := customNestedFieldConfig.Type
698+
shapeRef := crd.sdkAPI.GetShapeRefFromType(*typeOverride)
699+
700+
// Create a new field with the provided field name and shapeRef
701+
newCustomNestedField := NewField(crd, fieldName, names.New(fieldName), shapeRef, customNestedFieldConfig)
702+
703+
// Add the new field to the parentField
704+
parentField.MemberFields[fieldName] = newCustomNestedField
705+
// Add the new field to the parentField's shapeRef
706+
parentField.ShapeRef.Shape.MemberRefs[fieldName] = crd.sdkAPI.GetShapeRefFromType(*customNestedFieldConfig.Type)
707+
}
708+
}
709+
629710
// ReconcileRequeuOnSuccessSeconds returns the duration after which to requeue
630711
// the custom resource as int
631712
func (r *CRD) ReconcileRequeuOnSuccessSeconds() int {

pkg/model/model.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,16 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
136136
crd.AddSpecField(memberNames, memberShapeRef)
137137
}
138138

139-
// Now any additional Spec fields that are required from other API
140-
// operations.
139+
// A list of fields that should be processed after gathering
140+
// the Spec and Status top level fields. The customNestedFields will be
141+
// injected into the Spec or Status struct as a nested field.
142+
//
143+
// Note that we could reuse the Field struct here, but we don't because
144+
// we don't need all the fields that the Field struct provides. We only
145+
// need the field path and the FieldConfig. Using Field could lead to
146+
// confusion.
147+
customNestedFields := make(map[string]*ackgenconfig.FieldConfig)
148+
141149
for targetFieldName, fieldConfig := range m.cfg.GetFieldConfigs(crdName) {
142150
if fieldConfig.IsReadOnly {
143151
// It's a Status field...
@@ -176,6 +184,16 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
176184
panic(msg)
177185
}
178186
} else if fieldConfig.Type != nil {
187+
// A nested field will always have a "." in the field path.
188+
// Let's collect those fields and process them after we've
189+
// gathered all the top level fields.
190+
if strings.Contains(targetFieldName, ".") {
191+
// This is a nested field
192+
customNestedFields[targetFieldName] = fieldConfig
193+
continue
194+
}
195+
// If we're here, we have a custom top level field (non-nested).
196+
179197
// We have a custom field that has a type override and has not
180198
// been inferred via the normal Create Input shape or via the
181199
// SourceFieldConfig. Manually construct the field and its
@@ -189,6 +207,7 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
189207

190208
memberNames := names.New(targetFieldName)
191209
crd.AddSpecField(memberNames, memberShapeRef)
210+
192211
}
193212

194213
// Now process the fields that will go into the Status struct. We want
@@ -278,6 +297,16 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
278297
panic(msg)
279298
}
280299
} else if fieldConfig.Type != nil {
300+
// A nested field will always have a "." in the field path.
301+
// Let's collect those fields and process them after we've
302+
// gathered all the top level fields.
303+
if strings.Contains(targetFieldName, ".") {
304+
// This is a nested field
305+
customNestedFields[targetFieldName] = fieldConfig
306+
continue
307+
}
308+
// If we're here, we have a custom top level field (non-nested).
309+
281310
// We have a custom field that has a type override and has not
282311
// been inferred via the normal Create Input shape or via the
283312
// SourceFieldConfig. Manually construct the field and its
@@ -296,7 +325,8 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
296325
// Now add the additional printer columns that have been defined explicitly
297326
// in additional_columns
298327
crd.addAdditionalPrinterColumns(m.cfg.GetAdditionalColumns(crdName))
299-
328+
// Process the custom nested fields
329+
crd.addCustomNestedFields(customNestedFields)
300330
crds = append(crds, crd)
301331
}
302332
sort.Slice(crds, func(i, j int) bool {

pkg/model/model_lambda_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,78 @@ func TestLambda_Function(t *testing.T) {
103103
}
104104
assert.Equal(expStatusFieldCamel, attrCamelNames(statusFields))
105105
}
106+
107+
func TestLambda_customNestedFields_Spec_Depth2(t *testing.T) {
108+
// This test is to check if a custom field
109+
// defined as a nestedField using `type:`,
110+
// is nested properly inside its parentField
111+
112+
assert := assert.New(t)
113+
require := require.New(t)
114+
115+
g := testutil.NewModelForServiceWithOptions(t, "lambda", &testutil.TestingModelOptions{
116+
GeneratorConfigFile: "generator-with-custom-nested-types.yaml",
117+
})
118+
119+
crds, err := g.GetCRDs()
120+
require.Nil(err)
121+
122+
crd := getCRDByName("Function", crds)
123+
require.NotNil(crd)
124+
125+
assert.Contains(crd.SpecFields, "Code")
126+
codeField := crd.SpecFields["Code"]
127+
128+
// Check if Nested Field is inside its Parent Field
129+
assert.Contains(codeField.MemberFields, "S3SHA256")
130+
assert.Contains(codeField.ShapeRef.Shape.MemberRefs, "S3SHA256")
131+
}
132+
func TestLambda_customNestedFields_Spec_Depth3(t *testing.T) {
133+
// This test is to check if a custom field
134+
// defined as a nestedField using `type:`,
135+
// is nested properly inside its parentField
136+
137+
assert := assert.New(t)
138+
require := require.New(t)
139+
140+
g := testutil.NewModelForServiceWithOptions(t, "lambda", &testutil.TestingModelOptions{
141+
GeneratorConfigFile: "generator-with-custom-nested-types.yaml",
142+
})
143+
144+
crds, err := g.GetCRDs()
145+
require.Nil(err)
146+
147+
crd := getCRDByName("EventSourceMapping", crds)
148+
require.NotNil(crd)
149+
150+
assert.Contains(crd.SpecFields, "DestinationConfig")
151+
OnSuccessField := crd.SpecFields["DestinationConfig"].MemberFields["OnSuccess"]
152+
153+
assert.Contains(OnSuccessField.MemberFields, "New")
154+
assert.Contains(OnSuccessField.ShapeRef.Shape.MemberRefs, "New")
155+
}
156+
157+
func TestLambda_customNestedFields_Status_Depth3(t *testing.T) {
158+
// This test is to check if a custom field
159+
// defined as a nestedField using `type:`,
160+
// is nested properly inside its parentField
161+
162+
assert := assert.New(t)
163+
require := require.New(t)
164+
165+
g := testutil.NewModelForServiceWithOptions(t, "lambda", &testutil.TestingModelOptions{
166+
GeneratorConfigFile: "generator-with-custom-nested-types.yaml",
167+
})
168+
169+
crds, err := g.GetCRDs()
170+
require.Nil(err)
171+
172+
crd := getCRDByName("Function", crds)
173+
require.NotNil(crd)
174+
175+
assert.Contains(crd.StatusFields, "ImageConfigResponse")
176+
ErrorField := crd.StatusFields["ImageConfigResponse"].MemberFields["Error"]
177+
178+
assert.Contains(ErrorField.MemberFields, "New")
179+
assert.Contains(ErrorField.ShapeRef.Shape.MemberRefs, "New")
180+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
resources:
2+
Function:
3+
synced:
4+
when:
5+
- path: Status.State
6+
in: [ "Active" ]
7+
fields:
8+
Code.S3SHA256:
9+
type: string
10+
compare:
11+
is_ignored: true
12+
ImageConfigResponse.Error.New:
13+
is_read_only: true
14+
type: string
15+
EventSourceMapping:
16+
fields:
17+
DestinationConfig.OnSuccess.New:
18+
type: string
19+
compare:
20+
is_ignored: true
21+
22+

0 commit comments

Comments
 (0)