Skip to content
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
64 changes: 57 additions & 7 deletions pkg/generate/code/set_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,29 @@ func SetResource(
return ""
}

var err error
// We might be in a "wrapper" shape. Unwrap it to find the real object
// representation for the CRD's createOp. If there is a single member
// shape and that member shape is a structure, unwrap it.
if outputShape.UsedAsOutput && len(outputShape.MemberRefs) == 1 {
for memberName, memberRef := range outputShape.MemberRefs {
if memberRef.Shape.Type == "structure" {
sourceVarName += "." + memberName
outputShape = memberRef.Shape
// representation for the CRD's createOp/DescribeOP.

// Use the wrapper field path if it's given in the ack-generate config file.
wrapperFieldPath := r.GetOutputWrapperFieldPath(op)
if wrapperFieldPath != nil {
outputShape, err = GetWrapperOutputShape(outputShape, *wrapperFieldPath)
if err != nil {
msg := fmt.Sprintf("Unable to unwrap the output shape: %v", err)
panic(msg)
}
sourceVarName += "." + *wrapperFieldPath
} else {
// If the wrapper field path is not specified in the config file and if
// there is a single member shape and that member shape is a structure,
// unwrap it.
if outputShape.UsedAsOutput && len(outputShape.MemberRefs) == 1 {
for memberName, memberRef := range outputShape.MemberRefs {
if memberRef.Shape.Type == "structure" {
sourceVarName += "." + memberName
outputShape = memberRef.Shape
}
}
}
}
Expand Down Expand Up @@ -302,6 +317,41 @@ func SetResource(
return out
}

// GetWrapperOutputShape returns the shape of the last element of a given field
// Path. It carefully unwraps the output shape and verifies that every element
// of the field path exists in their correspanding parent shape and that they are
// structures.
func GetWrapperOutputShape(
shape *awssdkmodel.Shape,
fieldPath string,
) (*awssdkmodel.Shape, error) {
if fieldPath == "" {
return shape, nil
}
fieldPathParts := strings.Split(fieldPath, ".")
for x, wrapperField := range fieldPathParts {
for memberName, memberRef := range shape.MemberRefs {
if memberName == wrapperField {
if memberRef.Shape.Type != "structure" {
// All the mentionned shapes must be structure
return nil, fmt.Errorf(
"Expected SetOutput.WrapperFieldPath to only contain fields of type 'structure'."+
" Found %s of type '%s'",
memberName, memberRef.Shape.Type,
)
}
remainPath := strings.Join(fieldPathParts[x+1:], ".")
return GetWrapperOutputShape(memberRef.Shape, remainPath)
}
}
return nil, fmt.Errorf(
"Incorrect SetOutput.WrapperFieldPath. Could not find %s in Shape %s",
wrapperField, shape.ShapeName,
)
}
return shape, nil
}

func ListMemberNameInReadManyOutput(
r *model.CRD,
) string {
Expand Down
115 changes: 115 additions & 0 deletions pkg/generate/code/set_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package code_test

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -22,6 +23,7 @@ import (
"github.com/aws-controllers-k8s/code-generator/pkg/generate/code"
"github.com/aws-controllers-k8s/code-generator/pkg/model"
"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
)

func TestSetResource_APIGWv2_Route_Create(t *testing.T) {
Expand Down Expand Up @@ -153,6 +155,60 @@ func TestSetResource_APIGWv2_Route_ReadOne(t *testing.T) {
)
}

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

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

crd := testutil.GetCRDByName(t, g, "Backup")
require.NotNil(crd)

expected := `
if ko.Status.ACKResourceMetadata == nil {
ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{}
}
if resp.BackupDescription.BackupDetails.BackupArn != nil {
arn := ackv1alpha1.AWSResourceName(*resp.BackupDescription.BackupDetails.BackupArn)
ko.Status.ACKResourceMetadata.ARN = &arn
}
if resp.BackupDescription.BackupDetails.BackupCreationDateTime != nil {
ko.Status.BackupCreationDateTime = &metav1.Time{*resp.BackupDescription.BackupDetails.BackupCreationDateTime}
} else {
ko.Status.BackupCreationDateTime = nil
}
if resp.BackupDescription.BackupDetails.BackupExpiryDateTime != nil {
ko.Status.BackupExpiryDateTime = &metav1.Time{*resp.BackupDescription.BackupDetails.BackupExpiryDateTime}
} else {
ko.Status.BackupExpiryDateTime = nil
}
if resp.BackupDescription.BackupDetails.BackupName != nil {
ko.Spec.BackupName = resp.BackupDescription.BackupDetails.BackupName
} else {
ko.Spec.BackupName = nil
}
if resp.BackupDescription.BackupDetails.BackupSizeBytes != nil {
ko.Status.BackupSizeBytes = resp.BackupDescription.BackupDetails.BackupSizeBytes
} else {
ko.Status.BackupSizeBytes = nil
}
if resp.BackupDescription.BackupDetails.BackupStatus != nil {
ko.Status.BackupStatus = resp.BackupDescription.BackupDetails.BackupStatus
} else {
ko.Status.BackupStatus = nil
}
if resp.BackupDescription.BackupDetails.BackupType != nil {
ko.Status.BackupType = resp.BackupDescription.BackupDetails.BackupType
} else {
ko.Status.BackupType = nil
}
`
assert.Equal(
expected,
code.SetResource(crd.Config(), crd, model.OpTypeGet, "resp", "ko", 1, true),
)
}

func TestSetResource_CodeDeploy_Deployment_Create(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
Expand Down Expand Up @@ -2530,3 +2586,62 @@ func TestSetResource_RDS_DBSubnetGroup_ReadMany(t *testing.T) {
code.SetResource(crd.Config(), crd, model.OpTypeList, "resp", "ko", 1, false),
)
}

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

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

crd := testutil.GetCRDByName(t, g, "Backup")
require.NotNil(crd)

op := crd.Ops.ReadOne.OutputRef.Shape

type args struct {
outputShape *awssdkmodel.Shape
fieldPath string
}
tests := []struct {
name string
args args
wantErr bool
wantShapeName string
}{
{
name: "incorrect field path: element not found",
args: args{
outputShape: op,
fieldPath: "BackupDescription.Something",
},
wantErr: true,
},
{
name: "incorrect field path: element not of type structure",
args: args{
outputShape: op,
fieldPath: "BackupDescription.BackupArn",
},
wantErr: true,
},
{
name: "correct field path",
args: args{
outputShape: op,
fieldPath: "BackupDescription.BackupDetails",
},
wantErr: false,
wantShapeName: "BackupDetails",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
outputShape, err := code.GetWrapperOutputShape(tt.args.outputShape, tt.args.fieldPath)
if (err != nil) != tt.wantErr {
assert.Fail(fmt.Sprintf("GetWrapperOutputShape() error = %v, wantErr %v", err, tt.wantErr))
} else if !tt.wantErr {
assert.Equal(tt.wantShapeName, outputShape.ShapeName)
}
})
}
}
3 changes: 3 additions & 0 deletions pkg/generate/config/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type OperationConfig struct {
// `resourceManager` struct that will set fields on a `resource` struct
// depending on the output of the operation.
SetOutputCustomMethodName string `json:"set_output_custom_method_name,omitempty"`
// OutputWrapperFieldPath provides the JSON-Path like to the struct field containing
// information that will be merged into a `resource` object.
OutputWrapperFieldPath string `json:"output_wrapper_field_path,omitempty"`
// Override for resource name in case of heuristic failure
// An example of this is correcting stutter when the resource logic doesn't properly determine the resource name
ResourceName string `json:"resource_name"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@ resources:
errors:
404:
code: ResourceNotFoundException
operations:
DescribeBackup:
# DescribeBackupOutput is an unsual shape because it contains information for
# the backup it self (BackupDetails), the table details when the backup was
# created (SourceTableDetails) and the table features (SourceTableFeatureDetails).
# If not specified the code generator will try to determine the wrapper field by
# selecting for the output shape that only have a single member, which is incorrect
# in this case.
output_wrapper_field_path: BackupDescription.BackupDetails
22 changes: 22 additions & 0 deletions pkg/model/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,28 @@ func (r *CRD) SetOutputCustomMethodName(
return &resGenConfig.SetOutputCustomMethodName
}

// GetOutputWrapperFieldPath returns the JSON-Path of the output wrapper field
// as *string for a given operation, if specified in generator config.
func (r *CRD) GetOutputWrapperFieldPath(
op *awssdkmodel.Operation,
) *string {
if op == nil {
return nil
}
if r.cfg == nil {
return nil
}
opConfig, found := r.cfg.Operations[op.Name]
if !found {
return nil
}

if opConfig.OutputWrapperFieldPath == "" {
return nil
}
return &opConfig.OutputWrapperFieldPath
}

// GetCustomImplementation returns custom implementation method name for the
// supplied operation as specified in generator config
func (r *CRD) GetCustomImplementation(
Expand Down