Skip to content

Commit 943efb0

Browse files
authored
Merge pull request #47 from jaypipes/field-config-nested
allow "nested fields" to be configured
2 parents 94186d9 + 9e2458d commit 943efb0

File tree

6 files changed

+231
-9
lines changed

6 files changed

+231
-9
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ require (
1717
github.com/stretchr/testify v1.7.0
1818
golang.org/x/mod v0.4.1
1919
k8s.io/apimachinery v0.20.1
20+
k8s.io/utils v0.0.0-20201110183641-67b214c5f920
2021
)

pkg/generate/generator.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ func (g *Generator) GetCRDs() ([]*ackmodel.CRD, error) {
226226
sort.Slice(crds, func(i, j int) bool {
227227
return crds[i].Names.Camel < crds[j].Names.Camel
228228
})
229+
// This is the place that we build out the CRD.Fields map with
230+
// `pkg/model.Field` objects that represent the non-top-level Spec and
231+
// Status fields.
232+
g.processNestedFields(crds)
229233
g.crds = crds
230234
return crds, nil
231235
}
@@ -401,6 +405,145 @@ func (g *Generator) GetTypeDefs() ([]*ackmodel.TypeDef, map[string]string, error
401405
return tdefs, timports, nil
402406
}
403407

408+
// processNestedFields is responsible for walking all of the CRDs' Spec and
409+
// Status fields' Shape objects and adding `pkg/model.Field` objects for all
410+
// nested fields along with that `Field`'s `Config` object that allows us to
411+
// determine if the TypeDef associated with that nested field should have its
412+
// data type overridden (e.g. for SecretKeyReferences)
413+
func (g *Generator) processNestedFields(crds []*ackmodel.CRD) {
414+
for _, crd := range crds {
415+
for _, field := range crd.SpecFields {
416+
g.processNestedField(crd, field)
417+
}
418+
for _, field := range crd.StatusFields {
419+
g.processNestedField(crd, field)
420+
}
421+
}
422+
}
423+
424+
// processNestedField processes any nested fields (non-scalar fields associated
425+
// with the Spec and Status objects)
426+
func (g *Generator) processNestedField(
427+
crd *ackmodel.CRD,
428+
field *ackmodel.Field,
429+
) {
430+
if field.ShapeRef == nil && (field.FieldConfig == nil || !field.FieldConfig.IsAttribute) {
431+
fmt.Printf(
432+
"WARNING: Field %s:%s has nil ShapeRef and is not defined as an Attribute-based Field!\n",
433+
crd.Names.Original,
434+
field.Names.Original,
435+
)
436+
return
437+
}
438+
if field.ShapeRef != nil {
439+
fieldShape := field.ShapeRef.Shape
440+
fieldType := fieldShape.Type
441+
switch fieldType {
442+
case "structure":
443+
g.processNestedStructField(crd, field.Path+".", field)
444+
case "list":
445+
g.processNestedListField(crd, field.Path+"..", field)
446+
case "map":
447+
g.processNestedMapField(crd, field.Path+"..", field)
448+
}
449+
}
450+
// TODO(jaypipes): Handle Attribute-based fields...
451+
}
452+
453+
// processNestedStructField recurses through the members of a nested field that
454+
// is a struct type and adds any Field objects to the supplied CRD.
455+
func (g *Generator) processNestedStructField(
456+
crd *ackmodel.CRD,
457+
baseFieldPath string,
458+
baseField *ackmodel.Field,
459+
) {
460+
fieldConfigs := crd.Config().ResourceFields(crd.Names.Original)
461+
baseFieldShape := baseField.ShapeRef.Shape
462+
for memberName, memberRef := range baseFieldShape.MemberRefs {
463+
memberNames := names.New(memberName)
464+
memberShape := memberRef.Shape
465+
memberShapeType := memberShape.Type
466+
fieldPath := baseFieldPath + memberNames.Camel
467+
fieldConfig := fieldConfigs[fieldPath]
468+
field := ackmodel.NewField(crd, fieldPath, memberNames, memberRef, fieldConfig)
469+
switch memberShapeType {
470+
case "structure":
471+
g.processNestedStructField(crd, fieldPath+".", field)
472+
case "list":
473+
g.processNestedListField(crd, fieldPath+"..", field)
474+
case "map":
475+
g.processNestedMapField(crd, fieldPath+"..", field)
476+
}
477+
crd.Fields[fieldPath] = field
478+
}
479+
}
480+
481+
// processNestedListField recurses through the members of a nested field that
482+
// is a list type that has a struct element type and adds any Field objects to
483+
// the supplied CRD.
484+
func (g *Generator) processNestedListField(
485+
crd *ackmodel.CRD,
486+
baseFieldPath string,
487+
baseField *ackmodel.Field,
488+
) {
489+
baseFieldShape := baseField.ShapeRef.Shape
490+
elementFieldShape := baseFieldShape.MemberRef.Shape
491+
if elementFieldShape.Type != "structure" {
492+
return
493+
}
494+
fieldConfigs := crd.Config().ResourceFields(crd.Names.Original)
495+
for memberName, memberRef := range elementFieldShape.MemberRefs {
496+
memberNames := names.New(memberName)
497+
memberShape := memberRef.Shape
498+
memberShapeType := memberShape.Type
499+
fieldPath := baseFieldPath + memberNames.Camel
500+
fieldConfig := fieldConfigs[fieldPath]
501+
field := ackmodel.NewField(crd, fieldPath, memberNames, memberRef, fieldConfig)
502+
switch memberShapeType {
503+
case "structure":
504+
g.processNestedStructField(crd, fieldPath+".", field)
505+
case "list":
506+
g.processNestedListField(crd, fieldPath+"..", field)
507+
case "map":
508+
g.processNestedMapField(crd, fieldPath+"..", field)
509+
}
510+
crd.Fields[fieldPath] = field
511+
}
512+
}
513+
514+
// processNestedMapField recurses through the members of a nested field that
515+
// is a map type that has a struct value type and adds any Field objects to
516+
// the supplied CRD.
517+
func (g *Generator) processNestedMapField(
518+
crd *ackmodel.CRD,
519+
baseFieldPath string,
520+
baseField *ackmodel.Field,
521+
) {
522+
baseFieldShape := baseField.ShapeRef.Shape
523+
valueFieldShape := baseFieldShape.ValueRef.Shape
524+
if valueFieldShape.Type != "structure" {
525+
return
526+
}
527+
fieldConfigs := crd.Config().ResourceFields(crd.Names.Original)
528+
for memberName, memberRef := range valueFieldShape.MemberRefs {
529+
memberNames := names.New(memberName)
530+
memberShape := memberRef.Shape
531+
memberShapeType := memberShape.Type
532+
fieldPath := baseFieldPath + memberNames.Camel
533+
fieldConfig := fieldConfigs[fieldPath]
534+
field := ackmodel.NewField(crd, fieldPath, memberNames, memberRef, fieldConfig)
535+
switch memberShapeType {
536+
case "structure":
537+
g.processNestedStructField(crd, fieldPath+".", field)
538+
case "list":
539+
g.processNestedListField(crd, fieldPath+"..", field)
540+
case "map":
541+
g.processNestedMapField(crd, fieldPath+"..", field)
542+
}
543+
crd.Fields[fieldPath] = field
544+
}
545+
}
546+
404547
// GetEnumDefs returns a slice of pointers to `ackmodel.EnumDef` structs which
405548
// represent string fields whose value is constrained to one or more specific
406549
// string values.

pkg/generate/mq_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 generate_test
15+
16+
import (
17+
"testing"
18+
19+
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
22+
"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
23+
)
24+
25+
func TestMQ_Broker(t *testing.T) {
26+
assert := assert.New(t)
27+
require := require.New(t)
28+
29+
g := testutil.NewGeneratorForService(t, "mq")
30+
31+
crds, err := g.GetCRDs()
32+
require.Nil(err)
33+
34+
crd := getCRDByName("Broker", crds)
35+
require.NotNil(crd)
36+
37+
// We want to verify that the `Password` field of the `Spec.Users` field
38+
// (which is a `[]*User` type) is findable in the CRD's Fields collection
39+
// by the path `Spec.Users..Password` and that the FieldConfig associated
40+
// with this Field is marked as a SecretKeyReference.
41+
passFieldPath := "Users..Password"
42+
passField, found := crd.Fields[passFieldPath]
43+
require.True(found)
44+
require.NotNil(passField.FieldConfig)
45+
assert.True(passField.FieldConfig.IsSecret)
46+
}

pkg/generate/testdata/models/apis/mq/0000-00-00/generator.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ resources:
99
code: if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err }
1010
sdk_delete_pre_build_request:
1111
template_path: sdk_delete_pre_build_request.go.tpl
12+
fields:
13+
Users..Password:
14+
is_secret: true

pkg/model/crd.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ type CRD struct {
7878
// field. Note that there are no fields in StatusFields that are also in
7979
// SpecFields.
8080
StatusFields map[string]*Field
81+
// Fields is a map, keyed by the **renamed/normalized field path**, of
82+
// Field objects representing a field in the CRD's Spec or Status objects.
83+
Fields map[string]*Field
8184
// TypeImports is a map, keyed by an import string, with the map value
8285
// being the import alias
8386
TypeImports map[string]string
@@ -169,13 +172,15 @@ func (r *CRD) AddSpecField(
169172
memberNames names.Names,
170173
shapeRef *awssdkmodel.ShapeRef,
171174
) {
175+
fPath := memberNames.Camel
172176
fConfigs := r.cfg.ResourceFields(r.Names.Original)
173177
fConfig := fConfigs[memberNames.Original]
174-
f := newField(r, memberNames, shapeRef, fConfig)
178+
f := NewField(r, fPath, memberNames, shapeRef, fConfig)
175179
if fConfig != nil && fConfig.IsPrintable {
176180
r.addSpecPrintableColumn(f)
177181
}
178182
r.SpecFields[memberNames.Original] = f
183+
r.Fields[fPath] = f
179184
}
180185

181186
// AddStatusField adds a new Field of a given name and shape into the Status
@@ -184,13 +189,15 @@ func (r *CRD) AddStatusField(
184189
memberNames names.Names,
185190
shapeRef *awssdkmodel.ShapeRef,
186191
) {
192+
fPath := memberNames.Camel
187193
fConfigs := r.cfg.ResourceFields(r.Names.Original)
188194
fConfig := fConfigs[memberNames.Original]
189-
f := newField(r, memberNames, shapeRef, fConfig)
195+
f := NewField(r, fPath, memberNames, shapeRef, fConfig)
190196
if fConfig != nil && fConfig.IsPrintable {
191197
r.addStatusPrintableColumn(f)
192198
}
193199
r.StatusFields[memberNames.Original] = f
200+
r.Fields[fPath] = f
194201
}
195202

196203
// AddTypeImport adds an entry in the CRD's TypeImports map for an import line
@@ -254,12 +261,15 @@ func (r *CRD) UnpackAttributes() {
254261
continue
255262
}
256263
fieldNames := names.New(fieldName)
257-
f := newField(r, fieldNames, nil, fieldConfig)
264+
fPath := fieldNames.Camel
265+
266+
f := NewField(r, fPath, fieldNames, nil, fieldConfig)
258267
if !fieldConfig.IsReadOnly {
259268
r.SpecFields[fieldName] = f
260269
} else {
261270
r.StatusFields[fieldName] = f
262271
}
272+
r.Fields[fPath] = f
263273
}
264274
}
265275

@@ -414,6 +424,7 @@ func NewCRD(
414424
additionalPrinterColumns: make([]*PrinterColumn, 0),
415425
SpecFields: map[string]*Field{},
416426
StatusFields: map[string]*Field{},
427+
Fields: map[string]*Field{},
417428
ShortNames: cfg.ResourceShortNames(kind),
418429
}
419430
}

pkg/model/field.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,27 @@ import (
2020
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
2121
)
2222

23-
// Field represents a single field in the CRD's Spec or Status objects
23+
// Field represents a single field in the CRD's Spec or Status objects. The
24+
// field may be a direct field of the Spec or Status object or may be a field
25+
// of a list or struct-type field of the Spec or Status object. We call these
26+
// latter fields "nested fields" and they are identified by the Field.Path
27+
// attribute.
2428
type Field struct {
25-
CRD *CRD
26-
Names names.Names
27-
GoType string
29+
// CRD is the a pointer to the top-level custom resource definition
30+
// descriptor for the field or field's parent (if a nested field)
31+
CRD *CRD
32+
// Names is a set of normalized names for the field
33+
Names names.Names
34+
// Path is a "field path" that indicates where the field is within the CRD.
35+
// For example "Spec.Name" or "Status.BrokerInstances..Endpoint". Note for
36+
// the latter example, the field path indicates that the field `Endpoint`
37+
// is an attribute of the `Status.BrokerInstances` top-level field and the
38+
// double dot (`..` indicates that BrokerInstances is a list type).
39+
Path string
40+
// GoType is a string containing the Go data type for the field
41+
GoType string
42+
// GoTypeElem indicates the Go data type for the type of list element if
43+
// the field is a list type
2844
GoTypeElem string
2945
GoTypeWithPkgName string
3046
ShapeRef *awssdkmodel.ShapeRef
@@ -45,9 +61,10 @@ func (f *Field) IsRequired() bool {
4561
return util.InStrings(f.Names.ModelOriginal, f.CRD.Ops.Create.InputRef.Shape.Required)
4662
}
4763

48-
// newField returns a pointer to a new Field object
49-
func newField(
64+
// NewField returns a pointer to a new Field object
65+
func NewField(
5066
crd *CRD,
67+
path string,
5168
fieldNames names.Names,
5269
shapeRef *awssdkmodel.ShapeRef,
5370
cfg *ackgenconfig.FieldConfig,
@@ -72,6 +89,7 @@ func newField(
7289
return &Field{
7390
CRD: crd,
7491
Names: fieldNames,
92+
Path: path,
7593
ShapeRef: shapeRef,
7694
GoType: gt,
7795
GoTypeElem: gte,

0 commit comments

Comments
 (0)