Skip to content

Commit e7fe58d

Browse files
committed
feat(instance): improve instance resource handling and updates
- 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.
1 parent cb12d94 commit e7fe58d

File tree

8 files changed

+181
-10
lines changed

8 files changed

+181
-10
lines changed
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2025-09-19T17:24:00Z"
3-
build_hash: 6b4211163dcc34776b01da9a18217bac0f4103fd
4-
go_version: go1.24.6
5-
version: v0.52.0
2+
build_date: "2025-09-24T22:10:16Z"
3+
build_hash: 5bf1e456e1dfc638d47ab492376335f528c0f455
4+
go_version: go1.24.5
5+
version: v0.52.0-1-g5bf1e45
66
api_directory_checksum: b32f97274be98ca3f4cf5cbf559258210c872946
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.32.6
99
generator_config_info:
10-
file_checksum: 381d3f31a88cd00e07717b8957ffb5141218130a
10+
file_checksum: c4d680200af90f87dd7578bb694e23381253a180
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/generator.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ resources:
364364
update_operation:
365365
custom_method_name: customUpdateDHCPOptions
366366
Instance:
367+
exceptions:
368+
errors:
369+
404:
370+
code: InvalidInstanceID.NotFound
367371
fields:
368372
HibernationOptions:
369373
late_initialize: {}

generator.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ resources:
364364
update_operation:
365365
custom_method_name: customUpdateDHCPOptions
366366
Instance:
367+
exceptions:
368+
errors:
369+
404:
370+
code: InvalidInstanceID.NotFound
367371
fields:
368372
HibernationOptions:
369373
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)