Skip to content

Commit 32cbc57

Browse files
authored
Add updateable and deletable status guards for resources (#685)
Issue #, if available: Description of changes: Add generator.yaml configuration and template functions that allow service controllers to declare which status field values permit update and delete operations. When a resource is not in an allowed state, the generated code requeues the operation with a configurable delay instead of attempting the API call. New config blocks `updateable` and `deletable` on ResourceConfig accept a list of status field path + allowed-values conditions and an optional requeue_after_seconds override (default 30s). Template functions GoCodeResourceIsUpdateable and GoCodeResourceIsDeletable are injected into sdk_update, sdk_update_custom, sdk_update_set_attributes, and sdk_delete templates respectively. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent a8dd015 commit 32cbc57

13 files changed

Lines changed: 614 additions & 0 deletions

pkg/config/resource.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ type ResourceConfig struct {
4040
// Synced contains instructions for the code generator to generate Go code
4141
// that verifies whether a resource is synced or not.
4242
Synced *SyncedConfig `json:"synced"`
43+
// Updateable contains instructions for the code generator to generate
44+
// guard code that checks whether a resource can be updated based on its
45+
// current status. If the resource is not in an allowed state, the update
46+
// operation is requeued.
47+
Updateable *UpdateableConfig `json:"updateable,omitempty"`
48+
// Deletable contains instructions for the code generator to generate
49+
// guard code that checks whether a resource can be deleted based on its
50+
// current status. If the resource is not in an allowed state, the delete
51+
// operation is requeued.
52+
Deletable *DeletableConfig `json:"deletable,omitempty"`
4353
// Renames identifies fields in Operations that should be renamed.
4454
Renames *RenamesConfig `json:"renames,omitempty"`
4555
// ListOperation contains instructions for the code generator to generate
@@ -160,6 +170,42 @@ type SyncedCondition struct {
160170
In []string `json:"in"`
161171
}
162172

173+
// StatusCondition represents a single field condition for updateable/deletable
174+
// guards. It uses the same path+in pattern as SyncedCondition but is a
175+
// separate type to allow future divergence (e.g. requeue_after_seconds).
176+
type StatusCondition struct {
177+
// Path of the field. e.g. Status.Status
178+
Path *string `json:"path"`
179+
// In contains the list of values the field must be IN for the operation
180+
// to proceed. If the field value is NOT in this list, the operation is
181+
// requeued.
182+
In []string `json:"in"`
183+
}
184+
185+
// UpdateableConfig instructs the code generator on how to generate guard code
186+
// that checks whether a resource can be updated based on its current status.
187+
type UpdateableConfig struct {
188+
// When is a list of conditions. ALL conditions must be satisfied for the
189+
// resource to be considered updateable. If any condition is not met, the
190+
// update is requeued.
191+
When []StatusCondition `json:"when"`
192+
// RequeueAfterSeconds is the delay in seconds before the requeue.
193+
// Defaults to 30.
194+
RequeueAfterSeconds *int `json:"requeue_after_seconds,omitempty"`
195+
}
196+
197+
// DeletableConfig instructs the code generator on how to generate guard code
198+
// that checks whether a resource can be deleted based on its current status.
199+
type DeletableConfig struct {
200+
// When is a list of conditions. ALL conditions must be satisfied for the
201+
// resource to be considered deletable. If any condition is not met, the
202+
// delete is requeued.
203+
When []StatusCondition `json:"when"`
204+
// RequeueAfterSeconds is the delay in seconds before the requeue.
205+
// Defaults to 30.
206+
RequeueAfterSeconds *int `json:"requeue_after_seconds,omitempty"`
207+
}
208+
163209
// HooksConfig instructs the code generator how to inject custom callback hooks
164210
// at various places in the resource manager and SDK linkage code.
165211
//

pkg/generate/ack/controller.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ var (
148148
"GoCodeIsSynced": func(r *ackmodel.CRD, resVarName string, indentLevel int) (string, error) {
149149
return code.ResourceIsSynced(r.Config(), r, resVarName, indentLevel)
150150
},
151+
"GoCodeResourceIsUpdateable": func(r *ackmodel.CRD, resVarName string, indentLevel int) (string, error) {
152+
return code.ResourceIsUpdateable(r.Config(), r, resVarName, indentLevel)
153+
},
154+
"GoCodeResourceIsDeletable": func(r *ackmodel.CRD, resVarName string, indentLevel int) (string, error) {
155+
return code.ResourceIsDeletable(r.Config(), r, resVarName, indentLevel)
156+
},
151157
"GoCodeCompareStruct": func(r *ackmodel.CRD, shape *awssdkmodel.Shape, deltaVarName string, sourceVarName string, targetVarName string, fieldPath string, indentLevel int) (string, error) {
152158
return code.CompareStruct(r.Config(), r, nil, shape, deltaVarName, sourceVarName, targetVarName, fieldPath, indentLevel)
153159
},
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/config"
21+
"github.com/aws-controllers-k8s/code-generator/pkg/model"
22+
)
23+
24+
// defaultRequeueAfterSeconds is the default delay in seconds before a requeue
25+
// when a resource is not in an allowed state for update or delete.
26+
const defaultRequeueAfterSeconds = 30
27+
28+
// ResourceIsUpdateable returns Go code that checks whether a resource can be
29+
// updated based on its current status. If the resource is NOT updateable, the
30+
// generated code returns ackrequeue.NeededAfter.
31+
//
32+
// This follows the same pattern as ResourceIsSynced in synced.go.
33+
//
34+
// Sample output:
35+
//
36+
// if latest.ko.Status.Status != nil {
37+
// if !ackutil.InStrings(*latest.ko.Status.Status, []string{"ACTIVE", "AVAILABLE"}) {
38+
// return nil, ackrequeue.NeededAfter(
39+
// fmt.Errorf("resource is in %s state, cannot be updated",
40+
// *latest.ko.Status.Status),
41+
// time.Duration(30)*time.Second,
42+
// )
43+
// }
44+
// }
45+
func ResourceIsUpdateable(
46+
cfg *ackgenconfig.Config,
47+
r *model.CRD,
48+
// resource variable name — "latest" for sdkUpdate
49+
resVarName string,
50+
// Number of levels of indentation to use
51+
indentLevel int,
52+
) (string, error) {
53+
return resourceIsGuarded(cfg, r, resVarName, indentLevel, "updateable", "updated")
54+
}
55+
56+
// ResourceIsDeletable returns Go code that checks whether a resource can be
57+
// deleted based on its current status. If the resource is NOT deletable, the
58+
// generated code returns ackrequeue.NeededAfter.
59+
//
60+
// Sample output:
61+
//
62+
// if r.ko.Status.Status != nil {
63+
// if !ackutil.InStrings(*r.ko.Status.Status, []string{"ACTIVE", "AVAILABLE", "FAILED"}) {
64+
// return nil, ackrequeue.NeededAfter(
65+
// fmt.Errorf("resource is in %s state, cannot be deleted",
66+
// *r.ko.Status.Status),
67+
// time.Duration(30)*time.Second,
68+
// )
69+
// }
70+
// }
71+
func ResourceIsDeletable(
72+
cfg *ackgenconfig.Config,
73+
r *model.CRD,
74+
// resource variable name — "r" for sdkDelete
75+
resVarName string,
76+
// Number of levels of indentation to use
77+
indentLevel int,
78+
) (string, error) {
79+
return resourceIsGuarded(cfg, r, resVarName, indentLevel, "deletable", "deleted")
80+
}
81+
82+
// resourceIsGuarded is the shared implementation for ResourceIsUpdateable and
83+
// ResourceIsDeletable. It reads the appropriate config block and generates
84+
// guard code for each condition.
85+
func resourceIsGuarded(
86+
cfg *ackgenconfig.Config,
87+
r *model.CRD,
88+
resVarName string,
89+
indentLevel int,
90+
// configKey is "updateable" or "deletable"
91+
configKey string,
92+
// opVerb is "updated" or "deleted" — used in the error message
93+
opVerb string,
94+
) (string, error) {
95+
out := ""
96+
resConfig := cfg.GetResourceConfig(r.Names.Original)
97+
if resConfig == nil {
98+
return out, nil
99+
}
100+
101+
var conditions []ackgenconfig.StatusCondition
102+
var requeueSeconds int
103+
104+
switch configKey {
105+
case "updateable":
106+
if resConfig.Updateable == nil || len(resConfig.Updateable.When) == 0 {
107+
return out, nil
108+
}
109+
conditions = resConfig.Updateable.When
110+
requeueSeconds = defaultRequeueAfterSeconds
111+
if resConfig.Updateable.RequeueAfterSeconds != nil &&
112+
*resConfig.Updateable.RequeueAfterSeconds > 0 {
113+
requeueSeconds = *resConfig.Updateable.RequeueAfterSeconds
114+
}
115+
case "deletable":
116+
if resConfig.Deletable == nil || len(resConfig.Deletable.When) == 0 {
117+
return out, nil
118+
}
119+
conditions = resConfig.Deletable.When
120+
requeueSeconds = defaultRequeueAfterSeconds
121+
if resConfig.Deletable.RequeueAfterSeconds != nil &&
122+
*resConfig.Deletable.RequeueAfterSeconds > 0 {
123+
requeueSeconds = *resConfig.Deletable.RequeueAfterSeconds
124+
}
125+
default:
126+
return "", fmt.Errorf("unknown config key %q", configKey)
127+
}
128+
129+
for _, condCfg := range conditions {
130+
if condCfg.Path == nil || *condCfg.Path == "" {
131+
return "", fmt.Errorf(
132+
"resource %q: %s.when condition has empty path",
133+
r.Names.Original, configKey,
134+
)
135+
}
136+
if len(condCfg.In) == 0 {
137+
return "", fmt.Errorf(
138+
"resource %q, path %q: %s.when condition 'in' must not be empty",
139+
r.Names.Original, *condCfg.Path, configKey,
140+
)
141+
}
142+
143+
_, err := getTopLevelField(r, *condCfg.Path)
144+
if err != nil {
145+
return "", fmt.Errorf(
146+
"resource %q: cannot find field for path %q: %w",
147+
r.Names.Original, *condCfg.Path, err,
148+
)
149+
}
150+
151+
out += renderGuardBlock(
152+
resVarName, *condCfg.Path, condCfg.In,
153+
requeueSeconds, opVerb, indentLevel,
154+
)
155+
}
156+
157+
return out, nil
158+
}
159+
160+
// renderGuardBlock produces the Go source code for a single condition check.
161+
// It generates a nil check on the field pointer, then an InStrings check
162+
// against the allowed values, returning ackrequeue.NeededAfter if the value
163+
// is not in the allowed set.
164+
func renderGuardBlock(
165+
resVarName string,
166+
fieldPath string,
167+
allowedValues []string,
168+
requeueSeconds int,
169+
opVerb string,
170+
indentLevel int,
171+
) string {
172+
indent := strings.Repeat("\t", indentLevel)
173+
fullPath := fmt.Sprintf("%s.ko.%s", resVarName, fieldPath)
174+
175+
valuesSlice := fmt.Sprintf(`[]string{"%s"}`, strings.Join(allowedValues, `", "`))
176+
177+
out := ""
178+
out += fmt.Sprintf("%sif %s != nil {\n", indent, fullPath)
179+
out += fmt.Sprintf("%s\tif !ackutil.InStrings(*%s, %s) {\n", indent, fullPath, valuesSlice)
180+
out += fmt.Sprintf("%s\t\treturn nil, ackrequeue.NeededAfter(\n", indent)
181+
out += fmt.Sprintf("%s\t\t\tfmt.Errorf(\"resource is in %%s state, cannot be %s\",\n", indent, opVerb)
182+
out += fmt.Sprintf("%s\t\t\t\t*%s),\n", indent, fullPath)
183+
out += fmt.Sprintf("%s\t\t\ttime.Duration(%d)*time.Second,\n", indent, requeueSeconds)
184+
out += fmt.Sprintf("%s\t\t)\n", indent)
185+
out += fmt.Sprintf("%s\t}\n", indent)
186+
out += fmt.Sprintf("%s}\n", indent)
187+
return out
188+
}

0 commit comments

Comments
 (0)