Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow "nested fields" to be configured #47

Merged
merged 1 commit into from
Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ require (
github.com/stretchr/testify v1.7.0
golang.org/x/mod v0.4.1
k8s.io/apimachinery v0.20.1
k8s.io/utils v0.0.0-20201110183641-67b214c5f920
)
143 changes: 143 additions & 0 deletions pkg/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ func (g *Generator) GetCRDs() ([]*ackmodel.CRD, error) {
sort.Slice(crds, func(i, j int) bool {
return crds[i].Names.Camel < crds[j].Names.Camel
})
// This is the place that we build out the CRD.Fields map with
// `pkg/model.Field` objects that represent the non-top-level Spec and
// Status fields.
g.processNestedFields(crds)
g.crds = crds
return crds, nil
}
Expand Down Expand Up @@ -401,6 +405,145 @@ func (g *Generator) GetTypeDefs() ([]*ackmodel.TypeDef, map[string]string, error
return tdefs, timports, nil
}

// processNestedFields is responsible for walking all of the CRDs' Spec and
// Status fields' Shape objects and adding `pkg/model.Field` objects for all
// nested fields along with that `Field`'s `Config` object that allows us to
// determine if the TypeDef associated with that nested field should have its
// data type overridden (e.g. for SecretKeyReferences)
func (g *Generator) processNestedFields(crds []*ackmodel.CRD) {
for _, crd := range crds {
for _, field := range crd.SpecFields {
g.processNestedField(crd, field)
}
for _, field := range crd.StatusFields {
g.processNestedField(crd, field)
}
}
}

// processNestedField processes any nested fields (non-scalar fields associated
// with the Spec and Status objects)
func (g *Generator) processNestedField(
crd *ackmodel.CRD,
field *ackmodel.Field,
) {
if field.ShapeRef == nil && (field.FieldConfig == nil || !field.FieldConfig.IsAttribute) {
fmt.Printf(
"WARNING: Field %s:%s has nil ShapeRef and is not defined as an Attribute-based Field!\n",
crd.Names.Original,
field.Names.Original,
)
return
}
if field.ShapeRef != nil {
fieldShape := field.ShapeRef.Shape
fieldType := fieldShape.Type
switch fieldType {
case "structure":
g.processNestedStructField(crd, field.Path+".", field)
case "list":
g.processNestedListField(crd, field.Path+"..", field)
case "map":
g.processNestedMapField(crd, field.Path+"..", field)
}
}
// TODO(jaypipes): Handle Attribute-based fields...
}

// processNestedStructField recurses through the members of a nested field that
// is a struct type and adds any Field objects to the supplied CRD.
func (g *Generator) processNestedStructField(
crd *ackmodel.CRD,
baseFieldPath string,
baseField *ackmodel.Field,
) {
fieldConfigs := crd.Config().ResourceFields(crd.Names.Original)
baseFieldShape := baseField.ShapeRef.Shape
for memberName, memberRef := range baseFieldShape.MemberRefs {
memberNames := names.New(memberName)
memberShape := memberRef.Shape
memberShapeType := memberShape.Type
fieldPath := baseFieldPath + memberNames.Camel
fieldConfig := fieldConfigs[fieldPath]
field := ackmodel.NewField(crd, fieldPath, memberNames, memberRef, fieldConfig)
switch memberShapeType {
case "structure":
g.processNestedStructField(crd, fieldPath+".", field)
case "list":
g.processNestedListField(crd, fieldPath+"..", field)
case "map":
g.processNestedMapField(crd, fieldPath+"..", field)
}
crd.Fields[fieldPath] = field
}
}

// processNestedListField recurses through the members of a nested field that
// is a list type that has a struct element type and adds any Field objects to
// the supplied CRD.
func (g *Generator) processNestedListField(
crd *ackmodel.CRD,
baseFieldPath string,
baseField *ackmodel.Field,
) {
baseFieldShape := baseField.ShapeRef.Shape
elementFieldShape := baseFieldShape.MemberRef.Shape
if elementFieldShape.Type != "structure" {
return
}
Comment on lines +491 to +493
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: What does this conditional shield against?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to recurse into list fields when it's a list of strings, for example. Only when it's a list of structs is it necessary to dig deeper into the nesting.

fieldConfigs := crd.Config().ResourceFields(crd.Names.Original)
for memberName, memberRef := range elementFieldShape.MemberRefs {
memberNames := names.New(memberName)
memberShape := memberRef.Shape
memberShapeType := memberShape.Type
fieldPath := baseFieldPath + memberNames.Camel
fieldConfig := fieldConfigs[fieldPath]
field := ackmodel.NewField(crd, fieldPath, memberNames, memberRef, fieldConfig)
switch memberShapeType {
case "structure":
g.processNestedStructField(crd, fieldPath+".", field)
case "list":
g.processNestedListField(crd, fieldPath+"..", field)
case "map":
g.processNestedMapField(crd, fieldPath+"..", field)
}
crd.Fields[fieldPath] = field
}
}

// processNestedMapField recurses through the members of a nested field that
// is a map type that has a struct value type and adds any Field objects to
// the supplied CRD.
func (g *Generator) processNestedMapField(
crd *ackmodel.CRD,
baseFieldPath string,
baseField *ackmodel.Field,
) {
baseFieldShape := baseField.ShapeRef.Shape
valueFieldShape := baseFieldShape.ValueRef.Shape
if valueFieldShape.Type != "structure" {
return
}
fieldConfigs := crd.Config().ResourceFields(crd.Names.Original)
for memberName, memberRef := range valueFieldShape.MemberRefs {
memberNames := names.New(memberName)
memberShape := memberRef.Shape
memberShapeType := memberShape.Type
fieldPath := baseFieldPath + memberNames.Camel
fieldConfig := fieldConfigs[fieldPath]
field := ackmodel.NewField(crd, fieldPath, memberNames, memberRef, fieldConfig)
switch memberShapeType {
case "structure":
g.processNestedStructField(crd, fieldPath+".", field)
case "list":
g.processNestedListField(crd, fieldPath+"..", field)
case "map":
g.processNestedMapField(crd, fieldPath+"..", field)
}
crd.Fields[fieldPath] = field
}
}

// GetEnumDefs returns a slice of pointers to `ackmodel.EnumDef` structs which
// represent string fields whose value is constrained to one or more specific
// string values.
Expand Down
46 changes: 46 additions & 0 deletions pkg/generate/mq_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package generate_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
)

func TestMQ_Broker(t *testing.T) {
assert := assert.New(t)
require := require.New(t)

g := testutil.NewGeneratorForService(t, "mq")

crds, err := g.GetCRDs()
require.Nil(err)

crd := getCRDByName("Broker", crds)
require.NotNil(crd)

// We want to verify that the `Password` field of the `Spec.Users` field
// (which is a `[]*User` type) is findable in the CRD's Fields collection
// by the path `Spec.Users..Password` and that the FieldConfig associated
// with this Field is marked as a SecretKeyReference.
passFieldPath := "Users..Password"
passField, found := crd.Fields[passFieldPath]
require.True(found)
require.NotNil(passField.FieldConfig)
assert.True(passField.FieldConfig.IsSecret)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ resources:
code: if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err }
sdk_delete_pre_build_request:
template_path: sdk_delete_pre_build_request.go.tpl
fields:
Users..Password:
is_secret: true
17 changes: 14 additions & 3 deletions pkg/model/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ type CRD struct {
// field. Note that there are no fields in StatusFields that are also in
// SpecFields.
StatusFields map[string]*Field
// Fields is a map, keyed by the **renamed/normalized field path**, of
// Field objects representing a field in the CRD's Spec or Status objects.
Fields map[string]*Field
// TypeImports is a map, keyed by an import string, with the map value
// being the import alias
TypeImports map[string]string
Expand Down Expand Up @@ -169,13 +172,15 @@ func (r *CRD) AddSpecField(
memberNames names.Names,
shapeRef *awssdkmodel.ShapeRef,
) {
fPath := memberNames.Camel
fConfigs := r.cfg.ResourceFields(r.Names.Original)
fConfig := fConfigs[memberNames.Original]
f := newField(r, memberNames, shapeRef, fConfig)
f := NewField(r, fPath, memberNames, shapeRef, fConfig)
if fConfig != nil && fConfig.IsPrintable {
r.addSpecPrintableColumn(f)
}
r.SpecFields[memberNames.Original] = f
r.Fields[fPath] = f
}

// AddStatusField adds a new Field of a given name and shape into the Status
Expand All @@ -184,13 +189,15 @@ func (r *CRD) AddStatusField(
memberNames names.Names,
shapeRef *awssdkmodel.ShapeRef,
) {
fPath := memberNames.Camel
fConfigs := r.cfg.ResourceFields(r.Names.Original)
fConfig := fConfigs[memberNames.Original]
f := newField(r, memberNames, shapeRef, fConfig)
f := NewField(r, fPath, memberNames, shapeRef, fConfig)
if fConfig != nil && fConfig.IsPrintable {
r.addStatusPrintableColumn(f)
}
r.StatusFields[memberNames.Original] = f
r.Fields[fPath] = f
}

// AddTypeImport adds an entry in the CRD's TypeImports map for an import line
Expand Down Expand Up @@ -254,12 +261,15 @@ func (r *CRD) UnpackAttributes() {
continue
}
fieldNames := names.New(fieldName)
f := newField(r, fieldNames, nil, fieldConfig)
fPath := fieldNames.Camel

f := NewField(r, fPath, fieldNames, nil, fieldConfig)
if !fieldConfig.IsReadOnly {
r.SpecFields[fieldName] = f
} else {
r.StatusFields[fieldName] = f
}
r.Fields[fPath] = f
}
}

Expand Down Expand Up @@ -414,6 +424,7 @@ func NewCRD(
additionalPrinterColumns: make([]*PrinterColumn, 0),
SpecFields: map[string]*Field{},
StatusFields: map[string]*Field{},
Fields: map[string]*Field{},
ShortNames: cfg.ResourceShortNames(kind),
}
}
30 changes: 24 additions & 6 deletions pkg/model/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,27 @@ import (
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
)

// Field represents a single field in the CRD's Spec or Status objects
// Field represents a single field in the CRD's Spec or Status objects. The
// field may be a direct field of the Spec or Status object or may be a field
// of a list or struct-type field of the Spec or Status object. We call these
// latter fields "nested fields" and they are identified by the Field.Path
// attribute.
type Field struct {
CRD *CRD
Names names.Names
GoType string
// CRD is the a pointer to the top-level custom resource definition
// descriptor for the field or field's parent (if a nested field)
CRD *CRD
// Names is a set of normalized names for the field
Names names.Names
// Path is a "field path" that indicates where the field is within the CRD.
// For example "Spec.Name" or "Status.BrokerInstances..Endpoint". Note for
// the latter example, the field path indicates that the field `Endpoint`
// is an attribute of the `Status.BrokerInstances` top-level field and the
// double dot (`..` indicates that BrokerInstances is a list type).
Path string
// GoType is a string containing the Go data type for the field
GoType string
// GoTypeElem indicates the Go data type for the type of list element if
// the field is a list type
GoTypeElem string
GoTypeWithPkgName string
ShapeRef *awssdkmodel.ShapeRef
Expand All @@ -45,9 +61,10 @@ func (f *Field) IsRequired() bool {
return util.InStrings(f.Names.ModelOriginal, f.CRD.Ops.Create.InputRef.Shape.Required)
}

// newField returns a pointer to a new Field object
func newField(
// NewField returns a pointer to a new Field object
func NewField(
crd *CRD,
path string,
fieldNames names.Names,
shapeRef *awssdkmodel.ShapeRef,
cfg *ackgenconfig.FieldConfig,
Expand All @@ -72,6 +89,7 @@ func newField(
return &Field{
CRD: crd,
Names: fieldNames,
Path: path,
ShapeRef: shapeRef,
GoType: gt,
GoTypeElem: gte,
Expand Down