Skip to content

Commit d2ebb1c

Browse files
authored
Generate resource IsSynced functions (#247)
Issue: Generate ACK.ResourceSynced handlers #1064 Currently all the controllers contains custom code that checks whether a resource is synced or not. This code is mostly used by generic hooks that sets the `ACK.ResourceSynced` to its correct value. [Generic example hook](https://github.com/aws-controllers-k8s/rds-controller/blob/main/templates/hooks/db_cluster/sdk_read_many_post_set_output.go.tpl) This patch adds a new configuration option that will allow controller maintainers to generate `resource.IsSynced`functions based on some conditional. configuration example: ```yaml resources: Function: synced: when: - path: Status.State is: AVAILABLE - path: Status.State in: - AVAILABLE - ACTIVE ``` generated code example: ```go // IsSynced returns true if the supplied resource is synced. func (r *resource) IsSynced() bool { if *r.ko.Status.State != "AVAILABLE" { return false } candidates1 := []string{"AVAILABLE", "ACTIVE"} if !ackutil.InStrings(*r.ko.Status.State, candidates1) { return false } return true } ``` By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent cccec82 commit d2ebb1c

File tree

8 files changed

+367
-0
lines changed

8 files changed

+367
-0
lines changed

pkg/fieldpath/path.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ func (p *Path) Empty() bool {
118118
return len(p.parts) == 0
119119
}
120120

121+
// Size returns the Path number of parts
122+
func (p *Path) Size() int {
123+
return len(p.parts)
124+
}
125+
121126
// ShapeRef returns an aws-sdk-go ShapeRef within the supplied ShapeRef that
122127
// matches the Path. Returns nil if no matching ShapeRef could be found.
123128
//

pkg/generate/ack/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ var (
120120
"GoCodeCompare": func(r *ackmodel.CRD, deltaVarName string, sourceVarName string, targetVarName string, indentLevel int) string {
121121
return code.CompareResource(r.Config(), r, deltaVarName, sourceVarName, targetVarName, indentLevel)
122122
},
123+
"GoCodeIsSynced": func(r *ackmodel.CRD, resVarName string, indentLevel int) string {
124+
return code.ResourceIsSynced(r.Config(), r, resVarName, indentLevel)
125+
},
123126
"GoCodeCompareStruct": func(r *ackmodel.CRD, shape *awssdkmodel.Shape, deltaVarName string, sourceVarName string, targetVarName string, fieldPath string, indentLevel int) string {
124127
return code.CompareStruct(r.Config(), r, nil, shape, deltaVarName, sourceVarName, targetVarName, fieldPath, indentLevel)
125128
},

pkg/generate/code/synced.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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/fieldpath"
21+
ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/generate/config"
22+
"github.com/aws-controllers-k8s/code-generator/pkg/model"
23+
24+
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
25+
)
26+
27+
// ResourceIsSynced returns the Go code that verifies whether a resource is synced or
28+
// not. This code is generated using ack-generate configuration.
29+
// See ack-generate/pkg/config.SyncedConfiguration.
30+
//
31+
// Sample output:
32+
//
33+
// candidates0 := []string{"AVAILABLE", "ACTIVE"}
34+
// if !ackutil.InStrings(*r.ko.Status.TableStatus, candidates0) {
35+
// return false, nil
36+
// }
37+
// if r.ko.Spec.ProvisionedThroughput == nil {
38+
// return false, nil
39+
// }
40+
// if r.ko.Spec.ProvisionedThroughput.ReadCapacityUnits == nil {
41+
// return false, nil
42+
// }
43+
// candidates1 := []int{0, 10}
44+
// if !ackutil.InStrings(*r.ko.Spec.ProvisionedThroughput.ReadCapacityUnits, candidates1) {
45+
// return false, nil
46+
// }
47+
// candidates2 := []int{0}
48+
// if !ackutil.InStrings(*r.ko.Status.ItemCount, candidates2) {
49+
// return false, nil
50+
// }
51+
func ResourceIsSynced(
52+
cfg *ackgenconfig.Config,
53+
r *model.CRD,
54+
// String
55+
resVarName string,
56+
// Number of levels of indentation to use
57+
indentLevel int,
58+
) string {
59+
out := "\n"
60+
resConfig, ok := cfg.ResourceConfig(r.Names.Original)
61+
if !ok || resConfig.Synced == nil || len(resConfig.Synced.When) == 0 {
62+
return out
63+
}
64+
65+
for _, condition := range resConfig.Synced.When {
66+
if condition.Path == nil || *condition.Path == "" {
67+
panic("Received an empty sync condition path. 'SyncCondition.Path' must be provided.")
68+
}
69+
if len(condition.In) == 0 {
70+
panic("'SyncCondition.In' must be provided.")
71+
}
72+
fp := fieldpath.FromString(*condition.Path)
73+
field, err := getTopLevelField(r, *condition.Path)
74+
if err != nil {
75+
msg := fmt.Sprintf("cannot find top level field of path '%s': %v", *condition.Path, err)
76+
panic(msg)
77+
}
78+
candidatesVarName := fmt.Sprintf("%sCandidates", field.Names.CamelLower)
79+
if fp.Size() == 2 {
80+
out += scalarFieldEqual(resVarName, candidatesVarName, field.ShapeRef.GoTypeElem(), condition)
81+
} else {
82+
out += fieldPathSafeEqual(resVarName, candidatesVarName, field, condition)
83+
}
84+
}
85+
86+
return out
87+
}
88+
89+
func getTopLevelField(r *model.CRD, fieldPath string) (*model.Field, error) {
90+
fp := fieldpath.FromString(fieldPath)
91+
if fp.Size() < 2 {
92+
return nil, fmt.Errorf("fieldPath must contain at least two elements, received: %s", fieldPath)
93+
}
94+
95+
head := fp.PopFront()
96+
fieldName := fp.PopFront()
97+
switch head {
98+
case "Spec":
99+
field, ok := r.Fields[fieldName]
100+
if !ok {
101+
return nil, fmt.Errorf("field not found in Spec: %v", fieldName)
102+
}
103+
return field, nil
104+
case "Status":
105+
field, ok := r.Fields[fieldName]
106+
if !ok {
107+
return nil, fmt.Errorf("field not found in Status: %v", fieldName)
108+
}
109+
return field, nil
110+
default:
111+
return nil, fmt.Errorf("fieldPath must start with 'Spec' or 'Status', received: %v", head)
112+
}
113+
}
114+
115+
// scalarFieldEqual returns Go code that compares a scalar field to a given set of values.
116+
func scalarFieldEqual(resVarName string, candidatesVarName string, goType string, condition ackgenconfig.SyncedCondition) string {
117+
out := ""
118+
fieldPath := fmt.Sprintf("%s.%s", resVarName, *condition.Path)
119+
120+
valuesSlice := ""
121+
switch goType {
122+
case "string":
123+
// []string{"AVAILABLE", "ACTIVE"}
124+
valuesSlice = fmt.Sprintf("[]string{\"%s\"}", strings.Join(condition.In, "\", \""))
125+
case "int64", "PositiveLongObject", "Long":
126+
// []int64{1, 2}
127+
valuesSlice = fmt.Sprintf("[]int{%s}", strings.Join(condition.In, ", "))
128+
case "bool":
129+
// []bool{false}
130+
valuesSlice = fmt.Sprintf("[]bool{%s}", condition.In)
131+
default:
132+
panic("not supported type " + goType)
133+
}
134+
135+
// candidates1 := []string{"AVAILABLE", "ACTIVE"}
136+
out += fmt.Sprintf(
137+
"\t%s := %v\n",
138+
candidatesVarName,
139+
valuesSlice,
140+
)
141+
// if !ackutil.InStrings(*r.ko.Status.State, candidates1) {
142+
out += fmt.Sprintf(
143+
"\tif !ackutil.InStrings(*%s, %s) {\n",
144+
fieldPath,
145+
candidatesVarName,
146+
)
147+
148+
// return false, nil
149+
out += "\t\treturn false, nil\n"
150+
// }
151+
out += "\t}\n"
152+
return out
153+
}
154+
155+
// fieldPathSafeEqual returns go code that safely compares a resource field to value
156+
func fieldPathSafeEqual(
157+
resVarName string,
158+
candidatesVarName string,
159+
field *model.Field,
160+
condition ackgenconfig.SyncedCondition,
161+
) string {
162+
out := ""
163+
rootPath := fmt.Sprintf("%s.%s", resVarName, strings.Split(*condition.Path, ".")[0])
164+
knownShapesPath := strings.Join(strings.Split(*condition.Path, ".")[1:], ".")
165+
166+
fp := fieldpath.FromString(knownShapesPath)
167+
shapes := fp.IterShapeRefs(field.ShapeRef)
168+
169+
subFieldPath := rootPath
170+
for index, shape := range shapes {
171+
if index == len(shapes)-1 {
172+
// Some aws-sdk-go scalar shapes don't contain the real name of a shape
173+
// In this case we use the full path given in condition.Path
174+
subFieldPath = fmt.Sprintf("%s.%s", resVarName, *condition.Path)
175+
} else {
176+
subFieldPath += "." + shape.Shape.ShapeName
177+
}
178+
// if r.ko.Spec.ProvisionedThroughput == nil
179+
out += fmt.Sprintf("\tif %s == nil {\n", subFieldPath)
180+
// return false, nil
181+
out += "\t\treturn false, nil\n"
182+
// }
183+
out += "\t}\n"
184+
}
185+
out += scalarFieldEqual(resVarName, candidatesVarName, shapes[len(shapes)-1].GoTypeElem(), condition)
186+
return out
187+
}
188+
189+
func fieldPathContainsMapOrArray(fieldPath string, shapeRef *awssdkmodel.ShapeRef) bool {
190+
c := fieldpath.FromString(fieldPath)
191+
sr := c.ShapeRef(shapeRef)
192+
193+
if sr == nil {
194+
return false
195+
}
196+
if sr.ShapeName == "map" || sr.ShapeName == "list" {
197+
return true
198+
}
199+
if sr.ShapeName == "structure" {
200+
fieldName := c.PopFront()
201+
return fieldPathContainsMapOrArray(c.Copy().At(1), sr.Shape.MemberRefs[fieldName])
202+
}
203+
return false
204+
}

pkg/generate/code/synced_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
22+
"github.com/aws-controllers-k8s/code-generator/pkg/generate/code"
23+
"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
24+
)
25+
26+
func TestSyncedLambdaFunction(t *testing.T) {
27+
assert := assert.New(t)
28+
require := require.New(t)
29+
30+
g := testutil.NewModelForService(t, "lambda")
31+
32+
crd := testutil.GetCRDByName(t, g, "Function")
33+
require.NotNil(crd)
34+
35+
expectedSyncedConditions := `
36+
stateCandidates := []string{"AVAILABLE", "ACTIVE"}
37+
if !ackutil.InStrings(*r.ko.Status.State, stateCandidates) {
38+
return false, nil
39+
}
40+
lastUpdateStatusCandidates := []string{"AVAILABLE", "ACTIVE"}
41+
if !ackutil.InStrings(*r.ko.Status.LastUpdateStatus, lastUpdateStatusCandidates) {
42+
return false, nil
43+
}
44+
codeSizeCandidates := []int{1, 2}
45+
if !ackutil.InStrings(*r.ko.Status.CodeSize, codeSizeCandidates) {
46+
return false, nil
47+
}
48+
`
49+
assert.Equal(
50+
expectedSyncedConditions,
51+
code.ResourceIsSynced(
52+
crd.Config(), crd, "r.ko", 1,
53+
),
54+
)
55+
}
56+
57+
func TestSyncedDynamodbTable(t *testing.T) {
58+
assert := assert.New(t)
59+
require := require.New(t)
60+
61+
g := testutil.NewModelForService(t, "dynamodb")
62+
63+
crd := testutil.GetCRDByName(t, g, "Table")
64+
require.NotNil(crd)
65+
66+
expectedSyncedConditions := `
67+
tableStatusCandidates := []string{"AVAILABLE", "ACTIVE"}
68+
if !ackutil.InStrings(*r.ko.Status.TableStatus, tableStatusCandidates) {
69+
return false, nil
70+
}
71+
if r.ko.Spec.ProvisionedThroughput == nil {
72+
return false, nil
73+
}
74+
if r.ko.Spec.ProvisionedThroughput.ReadCapacityUnits == nil {
75+
return false, nil
76+
}
77+
provisionedThroughputCandidates := []int{0, 10}
78+
if !ackutil.InStrings(*r.ko.Spec.ProvisionedThroughput.ReadCapacityUnits, provisionedThroughputCandidates) {
79+
return false, nil
80+
}
81+
itemCountCandidates := []int{0}
82+
if !ackutil.InStrings(*r.ko.Status.ItemCount, itemCountCandidates) {
83+
return false, nil
84+
}
85+
`
86+
assert.Equal(
87+
expectedSyncedConditions,
88+
code.ResourceIsSynced(
89+
crd.Config(), crd, "r.ko", 1,
90+
),
91+
)
92+
}

pkg/generate/config/resource.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type ResourceConfig struct {
3333
// the code generator about a custom callback hooks that should be injected
3434
// into the resource's manager or SDK binding code.
3535
Hooks map[string]*HooksConfig `json:"hooks"`
36+
// Synced contains instructions for the code generator to generate Go code
37+
// that verifies whether a resource is synced or not.
38+
Synced *SyncedConfig `json:"synced"`
3639
// Renames identifies fields in Operations that should be renamed.
3740
Renames *RenamesConfig `json:"renames,omitempty"`
3841
// ListOperation contains instructions for the code generator to generate
@@ -87,6 +90,23 @@ type ResourceConfig struct {
8790
IsARNPrimaryKey bool `json:"is_arn_primary_key"`
8891
}
8992

93+
// SyncedConfig instructs the code generator on how to generate functions that checks
94+
// whether a resource was synced or not.
95+
type SyncedConfig struct {
96+
// When is a list of conditions that should be satisfied in order to tell whether a
97+
// a resource was synced or not.
98+
When []SyncedCondition `json:"when"`
99+
}
100+
101+
// SyncedCondition represent one of the unique condition that should be fullfiled in
102+
// order to assert whether a resource is synced.
103+
type SyncedCondition struct {
104+
// Path of the field. e.g Status.Processing
105+
Path *string `json:"path"`
106+
// In contains a list of possible values `Path` should be equal to.
107+
In []string `json:"in"`
108+
}
109+
90110
// HooksConfig instructs the code generator how to inject custom callback hooks
91111
// at various places in the resource manager and SDK linkage code.
92112
//

pkg/testdata/models/apis/dynamodb/0000-00-00/generator.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ resources:
44
errors:
55
404:
66
code: ResourceNotFoundException
7+
synced:
8+
when:
9+
- path: Status.TableStatus
10+
in:
11+
- AVAILABLE
12+
- ACTIVE
13+
- path: Spec.ProvisionedThroughput.ReadCapacityUnits
14+
in:
15+
- 0
16+
- 10
17+
- path: Status.ItemCount
18+
in:
19+
- 0
720
operations:
821
DescribeBackup:
922
# DescribeBackupOutput is an unsual shape because it contains information for

pkg/testdata/models/apis/lambda/0000-00-00/generator.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,17 @@ resources:
1111
from:
1212
operation: GetFunction
1313
path: Code.RepositoryType
14+
synced:
15+
when:
16+
- path: Status.State
17+
in:
18+
- AVAILABLE
19+
- ACTIVE
20+
- path: Status.LastUpdateStatus
21+
in:
22+
- AVAILABLE
23+
- ACTIVE
24+
- path: Status.CodeSize
25+
in:
26+
- 1
27+
- 2

0 commit comments

Comments
 (0)