Skip to content

Commit 955d10c

Browse files
Add cluster update support (#14)
Description of changes: Adds hooks for checking cluster status, and gating update or delete operations unless cluster is in `ACTIVE` status. Adds custom update logic for the `UpdateClusterVersion` and `UpdateClusterConfig` operations. Note: Update operations are asynchronous for EKS clusters, but can only be made one at a time. At the end of any update call, we forcefully set the synced condition to false and requeue with a 15 second duration. This gives the server time to receive and start the async update request, start the update and set the cluster status to `UPDATING` before we requeue and call read one. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 547bf9f commit 955d10c

13 files changed

+515
-21
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2021-11-17T18:42:03Z"
2+
build_date: "2022-01-05T23:22:05Z"
33
build_hash: 966e9a9ac6dfb4bbc2d3ded1972ce2b706391d44
44
go_version: go1.17.1
55
version: v0.15.2
6-
api_directory_checksum: 4c0c0a0fc33dd3b9ac0d5c6adbce3eb818ec0502
6+
api_directory_checksum: 18423a5aa90c547e267c460f25ce08e5c0b4aaa6
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.38.67
99
generator_config_info:
10-
file_checksum: 4878e381dd85bb88d0691255f3359da6775fc169
10+
file_checksum: f94cfb6bf61dca6a809c8d7cf0dfc1edc561add6
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/generator.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ resources:
4646
- MissingAction
4747
- MissingParameter
4848
- ValidationError
49+
hooks:
50+
sdk_create_post_set_output:
51+
template_path: hooks/cluster/sdk_create_post_set_output.go.tpl
52+
sdk_read_one_post_set_output:
53+
template_path: hooks/cluster/sdk_read_one_post_set_output.go.tpl
54+
sdk_delete_pre_build_request:
55+
template_path: hooks/cluster/sdk_delete_pre_build_request.go.tpl
56+
sdk_file_end:
57+
template_path: hooks/cluster/sdk_file_end.go.tpl
58+
update_operation:
59+
custom_method_name: customUpdate
4960
FargateProfile:
5061
renames:
5162
operations:

apis/v1alpha1/zz_generated.deepcopy.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

generator.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ resources:
4646
- MissingAction
4747
- MissingParameter
4848
- ValidationError
49+
hooks:
50+
sdk_create_post_set_output:
51+
template_path: hooks/cluster/sdk_create_post_set_output.go.tpl
52+
sdk_read_one_post_set_output:
53+
template_path: hooks/cluster/sdk_read_one_post_set_output.go.tpl
54+
sdk_delete_pre_build_request:
55+
template_path: hooks/cluster/sdk_delete_pre_build_request.go.tpl
56+
sdk_file_end:
57+
template_path: hooks/cluster/sdk_file_end.go.tpl
58+
update_operation:
59+
custom_method_name: customUpdate
4960
FargateProfile:
5061
renames:
5162
operations:

pkg/resource/cluster/hook.go

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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 cluster
15+
16+
import (
17+
"context"
18+
"errors"
19+
"fmt"
20+
"time"
21+
22+
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
23+
ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition"
24+
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
25+
ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue"
26+
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
27+
svcsdk "github.com/aws/aws-sdk-go/service/eks"
28+
corev1 "k8s.io/api/core/v1"
29+
)
30+
31+
const (
32+
LoggingNoChangesError = "No changes needed for the logging config provided"
33+
)
34+
35+
// Taken from the list of cluster statuses on the boto3 documentation
36+
// https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/eks.html#EKS.Client.describe_cluster
37+
const (
38+
StatusCreating = "CREATING"
39+
StatusActive = "ACTIVE"
40+
StatusDeleting = "DELETING"
41+
StatusFailed = "FAILED"
42+
StatusUpdating = "UPDATING"
43+
StatusPending = "PENDING"
44+
)
45+
46+
var (
47+
// TerminalStatuses are the status strings that are terminal states for a
48+
// cluster.
49+
TerminalStatuses = []string{
50+
StatusDeleting,
51+
StatusFailed,
52+
}
53+
)
54+
55+
var (
56+
requeueWaitWhileDeleting = ackrequeue.NeededAfter(
57+
errors.New(
58+
fmt.Sprintf("Cluster in '%s' state, cannot be modified or deleted.", StatusDeleting),
59+
),
60+
ackrequeue.DefaultRequeueAfterDuration,
61+
)
62+
RequeueAfterUpdateDuration = 15 * time.Second
63+
)
64+
65+
// requeueWaitUntilCanModify returns a `ackrequeue.RequeueNeededAfter` struct
66+
// explaining the cluster cannot be modified until it reaches an active status.
67+
func requeueWaitUntilCanModify(r *resource) *ackrequeue.RequeueNeededAfter {
68+
if r.ko.Status.Status == nil {
69+
return nil
70+
}
71+
status := *r.ko.Status.Status
72+
msg := fmt.Sprintf(
73+
"Cluster in '%s' state, cannot be modified until '%s'.",
74+
status, StatusActive,
75+
)
76+
return ackrequeue.NeededAfter(
77+
errors.New(msg),
78+
ackrequeue.DefaultRequeueAfterDuration,
79+
)
80+
}
81+
82+
// requeueAfterAsyncUpdate returns a `ackrequeue.RequeueNeededAfter` struct
83+
// explaining the cluster cannot be modified until after the asynchronous update
84+
// has (first, started and then) completed and the cluster reaches an active
85+
// status.
86+
func requeueAfterAsyncUpdate() *ackrequeue.RequeueNeededAfter {
87+
msg := fmt.Sprintf(
88+
"Cluster has started asynchronously updating, cannot be modified until '%s'.",
89+
StatusActive,
90+
)
91+
return ackrequeue.NeededAfter(
92+
errors.New(msg),
93+
RequeueAfterUpdateDuration,
94+
)
95+
}
96+
97+
// clusterHasTerminalStatus returns whether the supplied cluster is in a
98+
// terminal state
99+
func clusterHasTerminalStatus(r *resource) bool {
100+
if r.ko.Status.Status == nil {
101+
return false
102+
}
103+
cs := *r.ko.Status.Status
104+
for _, s := range TerminalStatuses {
105+
if cs == s {
106+
return true
107+
}
108+
}
109+
return false
110+
}
111+
112+
// clusterActive returns true if the supplied cluster is in an active status
113+
func clusterActive(r *resource) bool {
114+
if r.ko.Status.Status == nil {
115+
return false
116+
}
117+
cs := *r.ko.Status.Status
118+
return cs == StatusActive
119+
}
120+
121+
// clusterCreating returns true if the supplied cluster is in the process of
122+
// being created
123+
func clusterCreating(r *resource) bool {
124+
if r.ko.Status.Status == nil {
125+
return false
126+
}
127+
cs := *r.ko.Status.Status
128+
return cs == StatusCreating
129+
}
130+
131+
// clusterDeleting returns true if the supplied cluster is in the process of
132+
// being deleted
133+
func clusterDeleting(r *resource) bool {
134+
if r.ko.Status.Status == nil {
135+
return false
136+
}
137+
cs := *r.ko.Status.Status
138+
return cs == StatusDeleting
139+
}
140+
141+
// returnClusterUpdating will set synced to false on the resource and
142+
// return an async requeue error to signify that the resource should be
143+
// forcefully requeued in order to pick up the 'UPDATING' status.
144+
func returnClusterUpdating(r *resource) (*resource, error) {
145+
msg := "Cluster is currently being updated"
146+
ackcondition.SetSynced(r, corev1.ConditionFalse, &msg, nil)
147+
return r, requeueAfterAsyncUpdate()
148+
}
149+
150+
func (rm *resourceManager) customUpdate(
151+
ctx context.Context,
152+
desired *resource,
153+
latest *resource,
154+
delta *ackcompare.Delta,
155+
) (updated *resource, err error) {
156+
rlog := ackrtlog.FromContext(ctx)
157+
exit := rlog.Trace("rm.customUpdate")
158+
defer exit(err)
159+
160+
if clusterDeleting(latest) {
161+
msg := "Cluster is currently being deleted"
162+
ackcondition.SetSynced(desired, corev1.ConditionFalse, &msg, nil)
163+
return desired, requeueWaitWhileDeleting
164+
}
165+
if !clusterActive(latest) {
166+
msg := "Cluster is in '" + *latest.ko.Status.Status + "' status"
167+
ackcondition.SetSynced(desired, corev1.ConditionFalse, &msg, nil)
168+
if clusterHasTerminalStatus(latest) {
169+
ackcondition.SetTerminal(desired, corev1.ConditionTrue, &msg, nil)
170+
return desired, nil
171+
}
172+
return desired, requeueWaitUntilCanModify(latest)
173+
}
174+
175+
// Merge in the information we read from the API call above to the copy of
176+
// the original Kubernetes object we passed to the function
177+
ko := desired.ko.DeepCopy()
178+
179+
// None of these methods modify the status, so we should return the latest
180+
// status as given by the ReadOne
181+
ko.Status = latest.ko.Status
182+
183+
if delta.DifferentAt("Spec.Logging") {
184+
if err := rm.updateConfigLogging(ctx, desired); err != nil {
185+
awserr, ok := ackerr.AWSError(err)
186+
187+
// The API responds with an error if there were no changes applied
188+
if !ok || awserr.Message() != LoggingNoChangesError {
189+
return nil, err
190+
}
191+
}
192+
return returnClusterUpdating(desired)
193+
}
194+
if delta.DifferentAt("Spec.ResourcesVPCConfig") {
195+
if err := rm.updateConfigResourcesVPCConfig(ctx, desired); err != nil {
196+
return nil, err
197+
}
198+
return returnClusterUpdating(desired)
199+
}
200+
if delta.DifferentAt("Spec.Version") {
201+
if err := rm.updateVersion(ctx, desired); err != nil {
202+
return nil, err
203+
}
204+
return returnClusterUpdating(desired)
205+
}
206+
207+
rm.setStatusDefaults(ko)
208+
return &resource{ko}, nil
209+
}
210+
211+
func (rm *resourceManager) updateVersion(
212+
ctx context.Context,
213+
r *resource,
214+
) (err error) {
215+
rlog := ackrtlog.FromContext(ctx)
216+
exit := rlog.Trace("rm.updateVersion")
217+
defer exit(err)
218+
input := &svcsdk.UpdateClusterVersionInput{
219+
Name: r.ko.Spec.Name,
220+
Version: r.ko.Spec.Version,
221+
}
222+
223+
_, err = rm.sdkapi.UpdateClusterVersionWithContext(ctx, input)
224+
rm.metrics.RecordAPICall("UPDATE", "UpdateClusterVersion", err)
225+
if err != nil {
226+
return err
227+
}
228+
229+
return nil
230+
}
231+
232+
func (rm *resourceManager) updateConfigLogging(
233+
ctx context.Context,
234+
r *resource,
235+
) (err error) {
236+
rlog := ackrtlog.FromContext(ctx)
237+
exit := rlog.Trace("rm.updateConfigLogging")
238+
defer exit(err)
239+
input := &svcsdk.UpdateClusterConfigInput{
240+
Name: r.ko.Spec.Name,
241+
Logging: rm.newLogging(r),
242+
}
243+
244+
_, err = rm.sdkapi.UpdateClusterConfigWithContext(ctx, input)
245+
rm.metrics.RecordAPICall("UPDATE", "UpdateClusterConfig", err)
246+
if err != nil {
247+
return err
248+
}
249+
250+
return nil
251+
}
252+
253+
func (rm *resourceManager) updateConfigResourcesVPCConfig(
254+
ctx context.Context,
255+
r *resource,
256+
) (err error) {
257+
rlog := ackrtlog.FromContext(ctx)
258+
exit := rlog.Trace("rm.updateConfigResourcesVPCConfig")
259+
defer exit(err)
260+
input := &svcsdk.UpdateClusterConfigInput{
261+
Name: r.ko.Spec.Name,
262+
ResourcesVpcConfig: rm.newVpcConfigRequest(r),
263+
}
264+
265+
// From the EKS documentation:
266+
// "You can't update the subnets or security group IDs for an existing
267+
// cluster."
268+
input.ResourcesVpcConfig.SetSubnetIds(nil)
269+
input.ResourcesVpcConfig.SetSecurityGroupIds(nil)
270+
271+
_, err = rm.sdkapi.UpdateClusterConfigWithContext(ctx, input)
272+
rm.metrics.RecordAPICall("UPDATE", "UpdateClusterConfig", err)
273+
if err != nil {
274+
return err
275+
}
276+
277+
return nil
278+
}

0 commit comments

Comments
 (0)