Skip to content

Commit 60d4776

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 60d4776

File tree

8 files changed

+191
-9
lines changed

8 files changed

+191
-9
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: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,28 @@ 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+
stateRunning = "running"
35+
stateStopped = "stopped"
36+
stateTerminated = "terminated"
37+
38+
requeueUntilReadyDuration = 10 * time.Second
39+
)
40+
2841
// addInstanceIDsToTerminateRequest populates the list of InstanceIDs
2942
// in the TerminateInstances request with the resource's InstanceID
3043
// Return error to indicate to callers that the resource is not yet created.
@@ -53,19 +66,126 @@ func (rm *resourceManager) customUpdateInstance(
5366
// an error, then the update was successful and desired.Spec
5467
// (now updated.Spec) reflects the latest resource state.
5568
updated = rm.concreteResource(desired.DeepCopy())
69+
updated.SetStatus(latest)
5670

5771
if delta.DifferentAt("Spec.Tags") {
5872
if err := tags.Sync(
5973
ctx, rm.sdkapi, rm.metrics, *latest.ko.Status.InstanceID,
6074
desired.ko.Spec.Tags, latest.ko.Spec.Tags,
6175
); err != nil {
62-
return nil, err
76+
return updated, err
6377
}
6478
}
6579

80+
if !delta.DifferentExcept("Spec.Tags") {
81+
return updated, nil
82+
}
83+
84+
if !isRunning(updated.ko) {
85+
return updated, ackrequeue.NeededAfter(
86+
fmt.Errorf("requeuing until state is %s or %s", stateRunning, stateStopped),
87+
requeueUntilReadyDuration,
88+
)
89+
}
90+
91+
err = rm.modifyInstanceAttributes(ctx, delta, desired, latest)
92+
if err != nil {
93+
return updated, err
94+
}
95+
6696
return updated, nil
6797
}
6898

99+
func (rm *resourceManager) modifyInstanceAttributes(ctx context.Context, delta *ackcompare.Delta, desired, latest *resource) (err error) {
100+
rlog := ackrtlog.FromContext(ctx)
101+
exit := rlog.Trace("rm.modifyInstanceAttributes")
102+
defer exit(err)
103+
input := &svcsdk.ModifyInstanceAttributeInput{
104+
InstanceId: latest.ko.Status.InstanceID,
105+
}
106+
if delta.DifferentAt("Spec.DisableAPITermination") {
107+
input.Attribute = "disableApiTermination"
108+
input.DisableApiTermination = &svcsdktypes.AttributeBooleanValue{Value: desired.ko.Spec.DisableAPITermination}
109+
} else if delta.DifferentAt("Spec.InstanceType") {
110+
input.Attribute = "instanceType"
111+
input.InstanceType = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.InstanceType}
112+
} else if delta.DifferentAt("Spec.KernelID") {
113+
input.Attribute = "kernel"
114+
input.Kernel = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.KernelID}
115+
} else if delta.DifferentAt("Spec.RAMDiskID") {
116+
input.Attribute = "ramdisk"
117+
input.Ramdisk = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.RAMDiskID}
118+
} else if delta.DifferentAt("Spec.InstanceInitiatedShutdownBehavior") {
119+
input.Attribute = "instanceInitiatedShutdownBehavior"
120+
input.InstanceInitiatedShutdownBehavior = &svcsdktypes.AttributeValue{Value: desired.ko.Spec.InstanceInitiatedShutdownBehavior}
121+
} else if delta.DifferentAt("Spec.UserData") {
122+
input.Attribute = "userData"
123+
input.UserData = &svcsdktypes.BlobAttributeValue{Value: []byte(stringOrEmpty(desired.ko.Spec.UserData))}
124+
} else if delta.DifferentAt("Spec.EBSOptimized") {
125+
input.Attribute = "ebsOptimized"
126+
input.EbsOptimized = &svcsdktypes.AttributeBooleanValue{Value: desired.ko.Spec.EBSOptimized}
127+
} else if delta.DifferentAt("Spec.DisableAPIStop") {
128+
input.Attribute = "disableApiStop"
129+
input.DisableApiStop = &svcsdktypes.AttributeBooleanValue{Value: desired.ko.Spec.DisableAPIStop}
130+
} else if delta.DifferentAt("Spec.SecurityGroupIDs") {
131+
input.Attribute = "groupSet"
132+
input.Groups = aws.ToStringSlice(desired.ko.Spec.SecurityGroupIDs)
133+
}
134+
135+
if input.Attribute != "" {
136+
_, err := rm.sdkapi.ModifyInstanceAttribute(ctx, input)
137+
rm.metrics.RecordAPICall("UPDATE", "ModifyInstanceAttribute", err)
138+
if err != nil {
139+
return err
140+
}
141+
return fmt.Errorf("requeuing until all fields are updated")
142+
}
143+
return nil
144+
}
145+
146+
func isRunning(ko *v1alpha1.Instance) bool {
147+
if ko.Status.State == nil || ko.Status.State.Name == nil {
148+
return false
149+
}
150+
151+
// NOTE: (michaelhtm) We will count `stopped` as running for now.
152+
// TODO: expose annotation to allow users to start/stop instances
153+
return *ko.Status.State.Name == stateRunning || *ko.Status.State.Name == stateStopped
154+
}
155+
156+
func needsRestart(ko *v1alpha1.Instance) bool {
157+
if ko.Status.State == nil || ko.Status.State.Name == nil {
158+
return false
159+
}
160+
161+
return *ko.Status.State.Name == stateTerminated
162+
}
163+
164+
func stringOrEmpty(s *string) string {
165+
if s == nil {
166+
return ""
167+
}
168+
169+
return *s
170+
}
171+
172+
func setAdditionalFields(instance svcsdktypes.Instance, ko *v1alpha1.Instance) {
173+
ko.Spec.SecurityGroupIDs = []*string{}
174+
for _, group := range instance.SecurityGroups {
175+
ko.Spec.SecurityGroupIDs = append(ko.Spec.SecurityGroupIDs, group.GroupId)
176+
}
177+
178+
if monitoring := instance.Monitoring; monitoring != nil {
179+
switch monitoring.State {
180+
case svcsdktypes.MonitoringStateDisabled, svcsdktypes.MonitoringStateDisabling:
181+
ko.Spec.Monitoring = &v1alpha1.RunInstancesMonitoringEnabled{Enabled: aws.Bool(false)}
182+
183+
case svcsdktypes.MonitoringStateEnabled, svcsdktypes.MonitoringStatePending:
184+
ko.Spec.Monitoring = &v1alpha1.RunInstancesMonitoringEnabled{Enabled: aws.Bool(true)}
185+
}
186+
}
187+
}
188+
69189
var computeTagsDelta = tags.ComputeTagsDelta
70190

71191
// updateTagSpecificationsInCreateRequest adds

pkg/resource/instance/sdk.go

Lines changed: 12 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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
if needsRestart(ko) {
2+
return nil, ackerr.NotFound
3+
}
4+
5+
setAdditionalFields(resp.Reservations[0].Instances[0], ko)
6+
7+
if !isRunning(ko) {
8+
ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, aws.String("waiting for resource to be running"))
9+
}
110

211
toAdd, toDelete := computeTagsDelta(r.ko.Spec.Tags, ko.Spec.Tags)
312
if len(toAdd) == 0 && len(toDelete) == 0 {
413
// if resource's initial tags and response tags are equal,
514
// then assign resource's tags to maintain tag order
615
ko.Spec.Tags = r.ko.Spec.Tags
716
}
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)