mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-21 03:36:35 +00:00
feat: DescribeInstanceAttribute (#92)
This commit is contained in:
@@ -433,6 +433,7 @@ func (d *Daemon) subscribeAll() error {
|
||||
{"ec2.RevokeSecurityGroupIngress", d.handleEC2RevokeSecurityGroupIngress, "spinifex-workers"},
|
||||
{"ec2.RevokeSecurityGroupEgress", d.handleEC2RevokeSecurityGroupEgress, "spinifex-workers"},
|
||||
{"ec2.ModifyInstanceAttribute", d.handleEC2ModifyInstanceAttribute, "spinifex-workers"},
|
||||
{"ec2.DescribeInstanceAttribute", d.handleEC2DescribeInstanceAttribute, "spinifex-workers"},
|
||||
{"ec2.start", d.handleEC2StartStoppedInstance, "spinifex-workers"},
|
||||
{"ec2.terminate", d.handleEC2TerminateStoppedInstance, "spinifex-workers"},
|
||||
{"ec2.DescribeStoppedInstances", d.handleEC2DescribeStoppedInstances, "spinifex-workers"},
|
||||
|
||||
@@ -1218,6 +1218,123 @@ func (d *Daemon) handleEC2ModifyInstanceAttribute(msg *nats.Msg) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleEC2DescribeInstanceAttribute returns a single requested attribute for an instance.
|
||||
// It checks running instances first (in-memory), then falls back to stopped instances in KV.
|
||||
func (d *Daemon) handleEC2DescribeInstanceAttribute(msg *nats.Msg) {
|
||||
var input ec2.DescribeInstanceAttributeInput
|
||||
if err := json.Unmarshal(msg.Data, &input); err != nil {
|
||||
slog.Error("handleEC2DescribeInstanceAttribute: failed to unmarshal request", "err", err)
|
||||
respondWithError(msg, awserrors.ErrorServerInternal)
|
||||
return
|
||||
}
|
||||
|
||||
if input.InstanceId == nil || *input.InstanceId == "" {
|
||||
slog.Error("handleEC2DescribeInstanceAttribute: missing instance_id")
|
||||
respondWithError(msg, awserrors.ErrorMissingParameter)
|
||||
return
|
||||
}
|
||||
if input.Attribute == nil || *input.Attribute == "" {
|
||||
slog.Error("handleEC2DescribeInstanceAttribute: missing attribute")
|
||||
respondWithError(msg, awserrors.ErrorMissingParameter)
|
||||
return
|
||||
}
|
||||
|
||||
instanceID := *input.InstanceId
|
||||
attribute := *input.Attribute
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
// Look up instance: running first, then stopped KV.
|
||||
var instance *vm.VM
|
||||
|
||||
d.Instances.Mu.Lock()
|
||||
if running, ok := d.Instances.VMS[instanceID]; ok {
|
||||
instance = running
|
||||
}
|
||||
d.Instances.Mu.Unlock()
|
||||
|
||||
if instance == nil {
|
||||
if d.jsManager == nil {
|
||||
slog.Error("handleEC2DescribeInstanceAttribute: JetStream not available")
|
||||
respondWithError(msg, awserrors.ErrorServerInternal)
|
||||
return
|
||||
}
|
||||
stopped, err := d.jsManager.LoadStoppedInstance(instanceID)
|
||||
if err != nil {
|
||||
slog.Error("handleEC2DescribeInstanceAttribute: failed to load stopped instance",
|
||||
"instanceId", instanceID, "err", err)
|
||||
respondWithError(msg, awserrors.ErrorServerInternal)
|
||||
return
|
||||
}
|
||||
instance = stopped
|
||||
}
|
||||
|
||||
if instance == nil {
|
||||
slog.Warn("handleEC2DescribeInstanceAttribute: instance not found",
|
||||
"instanceId", instanceID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidInstanceIDNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if !checkInstanceOwnership(msg, instanceID, instance.AccountID) {
|
||||
return
|
||||
}
|
||||
|
||||
output := &ec2.DescribeInstanceAttributeOutput{
|
||||
InstanceId: &instanceID,
|
||||
}
|
||||
|
||||
switch attribute {
|
||||
case ec2.InstanceAttributeNameInstanceType:
|
||||
val := instance.InstanceType
|
||||
output.InstanceType = &ec2.AttributeValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameUserData:
|
||||
val := instance.UserData
|
||||
output.UserData = &ec2.AttributeValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameDisableApiTermination:
|
||||
val := false
|
||||
output.DisableApiTermination = &ec2.AttributeBooleanValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameDisableApiStop:
|
||||
val := false
|
||||
output.DisableApiStop = &ec2.AttributeBooleanValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameInstanceInitiatedShutdownBehavior:
|
||||
val := ec2.ShutdownBehaviorStop
|
||||
output.InstanceInitiatedShutdownBehavior = &ec2.AttributeValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameEbsOptimized:
|
||||
val := false
|
||||
output.EbsOptimized = &ec2.AttributeBooleanValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameEnaSupport:
|
||||
val := true
|
||||
output.EnaSupport = &ec2.AttributeBooleanValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameSourceDestCheck:
|
||||
val := true
|
||||
output.SourceDestCheck = &ec2.AttributeBooleanValue{Value: &val}
|
||||
|
||||
case ec2.InstanceAttributeNameGroupSet:
|
||||
if instance.Instance != nil && len(instance.Instance.SecurityGroups) > 0 {
|
||||
output.Groups = instance.Instance.SecurityGroups
|
||||
} else {
|
||||
output.Groups = []*ec2.GroupIdentifier{}
|
||||
}
|
||||
|
||||
default:
|
||||
slog.Warn("handleEC2DescribeInstanceAttribute: unsupported attribute",
|
||||
"instanceId", instanceID, "attribute", attribute)
|
||||
respondWithError(msg, awserrors.ErrorInvalidParameterValue)
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("handleEC2DescribeInstanceAttribute: completed",
|
||||
"instanceId", instanceID, "attribute", attribute, "accountID", accountID)
|
||||
respondWithJSON(msg, output)
|
||||
}
|
||||
|
||||
// publishNATEvent publishes a NAT lifecycle event (vpc.add-nat or vpc.delete-nat) to NATS.
|
||||
func (d *Daemon) publishNATEvent(topic, vpcId, externalIP, logicalIP, portName, mac string) {
|
||||
if d.natsConn == nil {
|
||||
|
||||
@@ -1918,6 +1918,373 @@ func TestHandleEC2ModifyInstanceAttribute_InvalidJSON(t *testing.T) {
|
||||
assert.Equal(t, "ServerInternal", errResp["Code"])
|
||||
}
|
||||
|
||||
// --- DescribeInstanceAttribute daemon tests ---
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_RunningInstance_InstanceType(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-run-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateRunning,
|
||||
InstanceType: "t3.micro",
|
||||
AccountID: testAccountID,
|
||||
Instance: &ec2.Instance{
|
||||
InstanceId: aws.String(instanceID),
|
||||
InstanceType: aws.String("t3.micro"),
|
||||
},
|
||||
}
|
||||
|
||||
daemon.Instances.Mu.Lock()
|
||||
daemon.Instances.VMS[instanceID] = instance
|
||||
daemon.Instances.Mu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
daemon.Instances.Mu.Lock()
|
||||
delete(daemon.Instances.VMS, instanceID)
|
||||
daemon.Instances.Mu.Unlock()
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DescribeInstanceAttributeOutput
|
||||
err = json.Unmarshal(reply.Data, &output)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, instanceID, *output.InstanceId)
|
||||
require.NotNil(t, output.InstanceType)
|
||||
assert.Equal(t, "t3.micro", *output.InstanceType.Value)
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_StoppedInstance_InstanceType(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-stop-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateStopped,
|
||||
InstanceType: "t3.medium",
|
||||
AccountID: testAccountID,
|
||||
Instance: &ec2.Instance{
|
||||
InstanceId: aws.String(instanceID),
|
||||
InstanceType: aws.String("t3.medium"),
|
||||
},
|
||||
}
|
||||
err = daemon.jsManager.WriteStoppedInstance(instanceID, instance)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = daemon.jsManager.DeleteStoppedInstance(instanceID) })
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DescribeInstanceAttributeOutput
|
||||
err = json.Unmarshal(reply.Data, &output)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, instanceID, *output.InstanceId)
|
||||
require.NotNil(t, output.InstanceType)
|
||||
assert.Equal(t, "t3.medium", *output.InstanceType.Value)
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_UserData(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-ud-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateStopped,
|
||||
InstanceType: "t3.micro",
|
||||
AccountID: testAccountID,
|
||||
UserData: "#!/bin/bash",
|
||||
Instance: &ec2.Instance{
|
||||
InstanceId: aws.String(instanceID),
|
||||
},
|
||||
}
|
||||
err = daemon.jsManager.WriteStoppedInstance(instanceID, instance)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = daemon.jsManager.DeleteStoppedInstance(instanceID) })
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameUserData),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DescribeInstanceAttributeOutput
|
||||
err = json.Unmarshal(reply.Data, &output)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output.UserData)
|
||||
assert.Equal(t, "#!/bin/bash", *output.UserData.Value)
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_DefaultAttribute_DisableApiTermination(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-def-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateStopped,
|
||||
InstanceType: "t3.micro",
|
||||
AccountID: testAccountID,
|
||||
Instance: &ec2.Instance{
|
||||
InstanceId: aws.String(instanceID),
|
||||
},
|
||||
}
|
||||
err = daemon.jsManager.WriteStoppedInstance(instanceID, instance)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = daemon.jsManager.DeleteStoppedInstance(instanceID) })
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameDisableApiTermination),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DescribeInstanceAttributeOutput
|
||||
err = json.Unmarshal(reply.Data, &output)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output.DisableApiTermination)
|
||||
assert.Equal(t, false, *output.DisableApiTermination.Value)
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_DefaultAttribute_ShutdownBehavior(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-shut-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateStopped,
|
||||
InstanceType: "t3.micro",
|
||||
AccountID: testAccountID,
|
||||
Instance: &ec2.Instance{
|
||||
InstanceId: aws.String(instanceID),
|
||||
},
|
||||
}
|
||||
err = daemon.jsManager.WriteStoppedInstance(instanceID, instance)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = daemon.jsManager.DeleteStoppedInstance(instanceID) })
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceInitiatedShutdownBehavior),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DescribeInstanceAttributeOutput
|
||||
err = json.Unmarshal(reply.Data, &output)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output.InstanceInitiatedShutdownBehavior)
|
||||
assert.Equal(t, "stop", *output.InstanceInitiatedShutdownBehavior.Value)
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_GroupSet_WithSecurityGroups(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-gs-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateRunning,
|
||||
InstanceType: "t3.micro",
|
||||
AccountID: testAccountID,
|
||||
Instance: &ec2.Instance{
|
||||
InstanceId: aws.String(instanceID),
|
||||
SecurityGroups: []*ec2.GroupIdentifier{
|
||||
{GroupId: aws.String("sg-111"), GroupName: aws.String("default")},
|
||||
{GroupId: aws.String("sg-222"), GroupName: aws.String("web")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
daemon.Instances.Mu.Lock()
|
||||
daemon.Instances.VMS[instanceID] = instance
|
||||
daemon.Instances.Mu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
daemon.Instances.Mu.Lock()
|
||||
delete(daemon.Instances.VMS, instanceID)
|
||||
daemon.Instances.Mu.Unlock()
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameGroupSet),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DescribeInstanceAttributeOutput
|
||||
err = json.Unmarshal(reply.Data, &output)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, instanceID, *output.InstanceId)
|
||||
require.Len(t, output.Groups, 2)
|
||||
assert.Equal(t, "sg-111", *output.Groups[0].GroupId)
|
||||
assert.Equal(t, "sg-222", *output.Groups[1].GroupId)
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_GroupSet_NilInstance(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-gs-nil-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateStopped,
|
||||
InstanceType: "t3.micro",
|
||||
AccountID: testAccountID,
|
||||
Instance: nil,
|
||||
}
|
||||
err = daemon.jsManager.WriteStoppedInstance(instanceID, instance)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = daemon.jsManager.DeleteStoppedInstance(instanceID) })
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameGroupSet),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DescribeInstanceAttributeOutput
|
||||
err = json.Unmarshal(reply.Data, &output)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, instanceID, *output.InstanceId)
|
||||
require.NotNil(t, output.Groups, "Groups should be empty slice, not nil")
|
||||
assert.Empty(t, output.Groups)
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_InstanceNotFound(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String("i-nonexistent"),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var errResp map[string]any
|
||||
err = json.Unmarshal(reply.Data, &errResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "InvalidInstanceID.NotFound", errResp["Code"])
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_UnsupportedAttribute(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
instanceID := "i-describe-unsup-001"
|
||||
instance := &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateStopped,
|
||||
AccountID: testAccountID,
|
||||
Instance: &ec2.Instance{
|
||||
InstanceId: aws.String(instanceID),
|
||||
},
|
||||
}
|
||||
err = daemon.jsManager.WriteStoppedInstance(instanceID, instance)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = daemon.jsManager.DeleteStoppedInstance(instanceID) })
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameBlockDeviceMapping),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeInstanceAttribute", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var errResp map[string]any
|
||||
err = json.Unmarshal(reply.Data, &errResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "InvalidParameterValue", errResp["Code"])
|
||||
}
|
||||
|
||||
func TestHandleEC2DescribeInstanceAttribute_InvalidJSON(t *testing.T) {
|
||||
natsURL := sharedJSNATSURL
|
||||
|
||||
daemon := createFullTestDaemonWithJetStream(t, natsURL)
|
||||
|
||||
sub, err := daemon.natsConn.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", daemon.handleEC2DescribeInstanceAttribute)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
reply, err := daemon.natsConn.Request("ec2.DescribeInstanceAttribute", []byte(`{invalid`), 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var errResp map[string]any
|
||||
err = json.Unmarshal(reply.Data, &errResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ServerInternal", errResp["Code"])
|
||||
}
|
||||
|
||||
// --- Delegate handler round-trip tests (table-driven) ---
|
||||
// Each of these handlers is a single line delegating to handleNATSRequest.
|
||||
// This test verifies the wiring is correct by sending a NATS request and
|
||||
|
||||
@@ -83,6 +83,9 @@ var ec2Actions = map[string]EC2Handler{
|
||||
"ModifyInstanceAttribute": ec2Handler(func(input *ec2.ModifyInstanceAttributeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.ModifyInstanceAttribute(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeInstanceAttribute": ec2Handler(func(input *ec2.DescribeInstanceAttributeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.DescribeInstanceAttribute(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateKeyPair": ec2Handler(func(input *ec2.CreateKeyPairInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_key.CreateKeyPair(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package gateway_ec2_instance
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/mulgadc/spinifex/spinifex/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// ValidateDescribeInstanceAttributeInput validates the input for DescribeInstanceAttribute.
|
||||
func ValidateDescribeInstanceAttributeInput(input *ec2.DescribeInstanceAttributeInput) error {
|
||||
if input == nil {
|
||||
return errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if input.InstanceId == nil || *input.InstanceId == "" {
|
||||
return errors.New(awserrors.ErrorInvalidInstanceIDMalformed)
|
||||
}
|
||||
if !strings.HasPrefix(*input.InstanceId, "i-") {
|
||||
return errors.New(awserrors.ErrorInvalidInstanceIDMalformed)
|
||||
}
|
||||
if input.Attribute == nil || *input.Attribute == "" {
|
||||
return errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DescribeInstanceAttribute sends a describe-attribute request to the daemon via NATS.
|
||||
func DescribeInstanceAttribute(input *ec2.DescribeInstanceAttributeInput, natsConn *nats.Conn, accountID string) (*ec2.DescribeInstanceAttributeOutput, error) {
|
||||
if err := ValidateDescribeInstanceAttributeInput(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.Info("DescribeInstanceAttribute: Processing request", "instance_id", *input.InstanceId, "attribute", *input.Attribute)
|
||||
|
||||
output, err := utils.NATSRequest[ec2.DescribeInstanceAttributeOutput](natsConn, "ec2.DescribeInstanceAttribute", input, 30*time.Second, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.Info("DescribeInstanceAttribute: Completed successfully", "instance_id", *input.InstanceId)
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package gateway_ec2_instance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// --- Validation tests ---
|
||||
|
||||
func TestValidateDescribeInstanceAttributeInput_NilInput(t *testing.T) {
|
||||
err := ValidateDescribeInstanceAttributeInput(nil)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
}
|
||||
|
||||
func TestValidateDescribeInstanceAttributeInput_MissingInstanceId(t *testing.T) {
|
||||
err := ValidateDescribeInstanceAttributeInput(&ec2.DescribeInstanceAttributeInput{
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidInstanceIDMalformed, err.Error())
|
||||
}
|
||||
|
||||
func TestValidateDescribeInstanceAttributeInput_EmptyInstanceId(t *testing.T) {
|
||||
err := ValidateDescribeInstanceAttributeInput(&ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String(""),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidInstanceIDMalformed, err.Error())
|
||||
}
|
||||
|
||||
func TestValidateDescribeInstanceAttributeInput_BadPrefix(t *testing.T) {
|
||||
err := ValidateDescribeInstanceAttributeInput(&ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String("x-12345"),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidInstanceIDMalformed, err.Error())
|
||||
}
|
||||
|
||||
func TestValidateDescribeInstanceAttributeInput_MissingAttribute(t *testing.T) {
|
||||
err := ValidateDescribeInstanceAttributeInput(&ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String("i-abc123"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorMissingParameter, err.Error())
|
||||
}
|
||||
|
||||
func TestValidateDescribeInstanceAttributeInput_EmptyAttribute(t *testing.T) {
|
||||
err := ValidateDescribeInstanceAttributeInput(&ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String("i-abc123"),
|
||||
Attribute: aws.String(""),
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorMissingParameter, err.Error())
|
||||
}
|
||||
|
||||
func TestValidateDescribeInstanceAttributeInput_Valid(t *testing.T) {
|
||||
err := ValidateDescribeInstanceAttributeInput(&ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String("i-abc123"),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// --- Gateway function tests ---
|
||||
|
||||
func TestDescribeInstanceAttribute_Success(t *testing.T) {
|
||||
_, nc := startTestNATSServer(t)
|
||||
|
||||
nc.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", func(msg *nats.Msg) {
|
||||
var input ec2.DescribeInstanceAttributeInput
|
||||
err := json.Unmarshal(msg.Data, &input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "i-test123", *input.InstanceId)
|
||||
assert.Equal(t, "instanceType", *input.Attribute)
|
||||
|
||||
output := &ec2.DescribeInstanceAttributeOutput{
|
||||
InstanceId: aws.String("i-test123"),
|
||||
InstanceType: &ec2.AttributeValue{Value: aws.String("t3.micro")},
|
||||
}
|
||||
resp, _ := json.Marshal(output)
|
||||
msg.Respond(resp)
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String("i-test123"),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
}
|
||||
|
||||
output, err := DescribeInstanceAttribute(input, nc, "123456789012")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
assert.Equal(t, "i-test123", *output.InstanceId)
|
||||
assert.Equal(t, "t3.micro", *output.InstanceType.Value)
|
||||
}
|
||||
|
||||
func TestDescribeInstanceAttribute_DaemonError(t *testing.T) {
|
||||
_, nc := startTestNATSServer(t)
|
||||
|
||||
nc.QueueSubscribe("ec2.DescribeInstanceAttribute", "spinifex-workers", func(msg *nats.Msg) {
|
||||
msg.Respond([]byte(`{"Code":"InvalidInstanceID.NotFound"}`))
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: aws.String("i-notfound"),
|
||||
Attribute: aws.String(ec2.InstanceAttributeNameInstanceType),
|
||||
}
|
||||
|
||||
_, err := DescribeInstanceAttribute(input, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidInstanceIDNotFound, err.Error())
|
||||
}
|
||||
|
||||
func TestDescribeInstanceAttribute_ValidationFailure(t *testing.T) {
|
||||
_, nc := startTestNATSServer(t)
|
||||
|
||||
_, err := DescribeInstanceAttribute(nil, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
}
|
||||
@@ -1004,7 +1004,7 @@ func TestEC2ActionMapCompleteness(t *testing.T) {
|
||||
expectedActions := []string{
|
||||
"DescribeInstances", "RunInstances", "StartInstances", "StopInstances",
|
||||
"TerminateInstances", "RebootInstances", "DescribeInstanceTypes", "GetConsoleOutput",
|
||||
"ModifyInstanceAttribute",
|
||||
"ModifyInstanceAttribute", "DescribeInstanceAttribute",
|
||||
"CreateKeyPair", "DeleteKeyPair", "DescribeKeyPairs", "ImportKeyPair",
|
||||
"DescribeImages", "CreateImage",
|
||||
"DescribeRegions", "DescribeAvailabilityZones",
|
||||
|
||||
Reference in New Issue
Block a user