Skip to content

Commit 56e81eb

Browse files
authored
feat(instance): improve instance resource handling and updates (#291)
Issue [#1709](aws-controllers-k8s/community#1709) Description of changes: - Add InvalidInstanceID.NotFound error code for 404 exceptions - Implement instance state handling and requeue logic - Add support for modifying instance attributes (security groups, API termination, etc.) - Add helper functions for instance state checks and field updates - Update E2E tests to verify instance updates and security group modifications - Set additional fields like SecurityGroupIDs and Monitoring in instance spec - Improve instance syncing and status conditions This commit enhances the EC2 instance resource controller by adding better error handling, state management, and support for modifying various instance attributes. It also includes improvements to E2E tests and resource syncing. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent d4da136 commit 56e81eb

File tree

7 files changed

+176
-5
lines changed

7 files changed

+176
-5
lines changed

apis/v1alpha1/generator.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ resources:
362362
update_operation:
363363
custom_method_name: customUpdateDHCPOptions
364364
Instance:
365+
exceptions:
366+
errors:
367+
404:
368+
code: InvalidInstanceID.NotFound
365369
fields:
366370
HibernationOptions:
367371
late_initialize: {}

generator.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ resources:
362362
update_operation:
363363
custom_method_name: customUpdateDHCPOptions
364364
Instance:
365+
exceptions:
366+
errors:
367+
404:
368+
code: InvalidInstanceID.NotFound
365369
fields:
366370
HibernationOptions:
367371
late_initialize: {}

pkg/resource/instance/hooks.go

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,24 @@ package instance
1616
import (
1717
"context"
1818
"errors"
19+
"fmt"
20+
"time"
1921

2022
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
23+
ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue"
2124
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
25+
"github.com/aws/aws-sdk-go-v2/aws"
2226
svcsdk "github.com/aws/aws-sdk-go-v2/service/ec2"
2327
svcsdktypes "github.com/aws/aws-sdk-go-v2/service/ec2/types"
2428

29+
"github.com/aws-controllers-k8s/ec2-controller/apis/v1alpha1"
2530
"github.com/aws-controllers-k8s/ec2-controller/pkg/tags"
2631
)
2732

33+
const (
34+
requeueUntilReadyDuration = 10 * time.Second
35+
)
36+
2837
// addInstanceIDsToTerminateRequest populates the list of InstanceIDs
2938
// in the TerminateInstances request with the resource's InstanceID
3039
// Return error to indicate to callers that the resource is not yet created.
@@ -45,27 +54,123 @@ func (rm *resourceManager) customUpdateInstance(
4554
) (updated *resource, err error) {
4655
rlog := ackrtlog.FromContext(ctx)
4756
exit := rlog.Trace("rm.customUpdateInstance")
48-
defer exit(err)
57+
defer func() { exit(err) }()
4958

5059
// Default `updated` to `desired` because it is likely
5160
// EC2 `modify` APIs do NOT return output, only errors.
5261
// If the `modify` calls (i.e. `sync`) do NOT return
5362
// an error, then the update was successful and desired.Spec
5463
// (now updated.Spec) reflects the latest resource state.
5564
updated = rm.concreteResource(desired.DeepCopy())
65+
updated.SetStatus(latest)
5666

5767
if delta.DifferentAt("Spec.Tags") {
5868
if err := tags.Sync(
5969
ctx, rm.sdkapi, rm.metrics, *latest.ko.Status.InstanceID,
6070
desired.ko.Spec.Tags, latest.ko.Spec.Tags,
6171
); err != nil {
62-
return nil, err
72+
return updated, err
6373
}
6474
}
6575

76+
if !delta.DifferentExcept("Spec.Tags") {
77+
return updated, nil
78+
}
79+
80+
if !isRunning(updated.ko) {
81+
return updated, ackrequeue.NeededAfter(
82+
fmt.Errorf("requeuing until state is %s or %s", svcsdktypes.InstanceStateNameRunning, svcsdktypes.InstanceStateNameStopped),
83+
requeueUntilReadyDuration,
84+
)
85+
}
86+
87+
err = rm.modifyInstanceAttributes(ctx, delta, desired, latest)
88+
if err != nil {
89+
return updated, err
90+
}
91+
6692
return updated, nil
6793
}
6894

95+
func (rm *resourceManager) modifyInstanceAttributes(ctx context.Context, delta *ackcompare.Delta, desired, latest *resource) (err error) {
96+
rlog := ackrtlog.FromContext(ctx)
97+
exit := rlog.Trace("rm.modifyInstanceAttributes")
98+
defer func() { exit(err) }()
99+
input := &svcsdk.ModifyInstanceAttributeInput{
100+
InstanceId: latest.ko.Status.InstanceID,
101+
}
102+
// we can only update one attribute at a time
103+
if delta.DifferentAt("Spec.DisableAPITermination") {
104+
input.DisableApiTermination = &svcsdktypes.AttributeBooleanValue{Value: desired.ko.Spec.DisableAPITermination}
105+
} else if delta.DifferentAt("Spec.InstanceType") {
106+
input.InstanceType = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.InstanceType}
107+
} else if delta.DifferentAt("Spec.KernelID") {
108+
input.Kernel = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.KernelID}
109+
} else if delta.DifferentAt("Spec.RAMDiskID") {
110+
input.Ramdisk = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.RAMDiskID}
111+
} else if delta.DifferentAt("Spec.InstanceInitiatedShutdownBehavior") {
112+
input.InstanceInitiatedShutdownBehavior = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.InstanceInitiatedShutdownBehavior}
113+
} else if delta.DifferentAt("Spec.UserData") {
114+
input.UserData = &svcsdktypes.BlobAttributeValue{Value: []byte(aws.ToString(desired.ko.Spec.UserData))}
115+
} else if delta.DifferentAt("Spec.EBSOptimized") {
116+
input.EbsOptimized = &svcsdktypes.AttributeBooleanValue{Value: desired.ko.Spec.EBSOptimized}
117+
} else if delta.DifferentAt("Spec.DisableAPIStop") {
118+
input.DisableApiStop = &svcsdktypes.AttributeBooleanValue{Value: desired.ko.Spec.DisableAPIStop}
119+
} else if delta.DifferentAt("Spec.SecurityGroupIDs") {
120+
input.Groups = aws.ToStringSlice(desired.ko.Spec.SecurityGroupIDs)
121+
} else {
122+
input = nil
123+
}
124+
125+
if input != nil {
126+
_, err = rm.sdkapi.ModifyInstanceAttribute(ctx, input)
127+
rm.metrics.RecordAPICall("UPDATE", "ModifyInstanceAttribute", err)
128+
if err != nil {
129+
return err
130+
}
131+
return fmt.Errorf("requeuing until all fields are updated")
132+
}
133+
return nil
134+
}
135+
136+
func isRunning(ko *v1alpha1.Instance) bool {
137+
if ko.Status.State == nil || ko.Status.State.Name == nil {
138+
return false
139+
}
140+
141+
// NOTE: (michaelhtm) We will count `stopped` as running for now.
142+
// TODO: expose annotation to allow users to start/stop instances
143+
return *ko.Status.State.Name == string(svcsdktypes.InstanceStateNameRunning) ||
144+
*ko.Status.State.Name == string(svcsdktypes.InstanceStateNameStopped)
145+
}
146+
147+
// needsRestart checks if the Instance is terminated (deleted)
148+
func needsRestart(ko *v1alpha1.Instance) bool {
149+
if ko.Status.State == nil || ko.Status.State.Name == nil {
150+
return false
151+
}
152+
153+
return *ko.Status.State.Name == string(svcsdktypes.InstanceStateNameTerminated)
154+
}
155+
156+
157+
func setAdditionalFields(instance svcsdktypes.Instance, ko *v1alpha1.Instance) {
158+
ko.Spec.SecurityGroupIDs = []*string{}
159+
for _, group := range instance.SecurityGroups {
160+
ko.Spec.SecurityGroupIDs = append(ko.Spec.SecurityGroupIDs, group.GroupId)
161+
}
162+
163+
if monitoring := instance.Monitoring; monitoring != nil {
164+
switch monitoring.State {
165+
case svcsdktypes.MonitoringStateDisabled, svcsdktypes.MonitoringStateDisabling:
166+
ko.Spec.Monitoring = &v1alpha1.RunInstancesMonitoringEnabled{Enabled: aws.Bool(false)}
167+
168+
case svcsdktypes.MonitoringStateEnabled, svcsdktypes.MonitoringStatePending:
169+
ko.Spec.Monitoring = &v1alpha1.RunInstancesMonitoringEnabled{Enabled: aws.Bool(true)}
170+
}
171+
}
172+
}
173+
69174
var computeTagsDelta = tags.ComputeTagsDelta
70175

71176
// updateTagSpecificationsInCreateRequest adds

pkg/resource/instance/sdk.go

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

templates/hooks/instance/sdk_create_post_set_output.go.tpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
setAdditionalFields(resp.Instances[0], ko)
2+
13
toAdd, toDelete := computeTagsDelta(desired.ko.Spec.Tags, ko.Spec.Tags)
24
if len(toAdd) == 0 && len(toDelete) == 0 {
35
// if desired tags and response tags are equal,

templates/hooks/instance/sdk_read_many_post_set_output.go.tpl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1+
// Here we want to check if the instance is terminated(deleted)
2+
// returning NotFound will trigger a create
3+
if needsRestart(ko) {
4+
return nil, ackerr.NotFound
5+
}
6+
7+
setAdditionalFields(resp.Reservations[0].Instances[0], ko)
8+
9+
if !isRunning(ko) {
10+
ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, aws.String("waiting for resource to be running"))
11+
}
112

213
toAdd, toDelete := computeTagsDelta(r.ko.Spec.Tags, ko.Spec.Tags)
314
if len(toAdd) == 0 && len(toDelete) == 0 {
415
// if resource's initial tags and response tags are equal,
516
// then assign resource's tags to maintain tag order
617
ko.Spec.Tags = r.ko.Spec.Tags
718
}
8-

test/e2e/tests/test_instance.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def instance(ec2_client):
135135
@service_marker
136136
@pytest.mark.canary
137137
class TestInstance:
138-
def test_create_delete(self, ec2_client, instance):
138+
def test_crud(self, ec2_client, instance):
139139
(ref, cr) = instance
140140
resource_id = cr["status"]["instanceID"]
141141

@@ -156,6 +156,39 @@ def test_create_delete(self, ec2_client, instance):
156156
t['Value'] == INSTANCE_TAG_VAL):
157157
tag_present = True
158158
assert tag_present
159+
160+
# Check resource synced successfully
161+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
162+
163+
# Ensure instance is running
164+
cr = k8s.get_resource(ref)
165+
assert 'status' in cr
166+
assert 'state' in cr['status']
167+
assert 'name' in cr['status']['state']
168+
assert cr['status']['state']['name'] == 'running'
169+
170+
# Update Instance securityGroupID
171+
test_vpc = get_bootstrap_resources().SharedTestVPC
172+
updates = {
173+
"spec": {
174+
"securityGroupIDs": [test_vpc.security_group.group_id]
175+
}
176+
}
177+
k8s.patch_custom_resource(ref, updates)
178+
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
179+
180+
# Check resource synced successfully
181+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
182+
183+
# Check Instance updated value
184+
instance = get_instance(ec2_client, resource_id)
185+
assert instance is not None
186+
assert 'SecurityGroups' in instance
187+
foundSecurityGroup = False
188+
for group in instance['SecurityGroups']:
189+
if group['GroupId'] == test_vpc.security_group.group_id:
190+
foundSecurityGroup = True
191+
assert foundSecurityGroup
159192

160193
# Delete k8s resource
161194
_, deleted = k8s.delete_custom_resource(ref, 2, 5)

0 commit comments

Comments
 (0)