feat: DescribeInstanceAttribute (#92)

This commit is contained in:
Julian Sommer
2026-03-26 13:39:45 +10:00
committed by GitHub
parent d025cab50e
commit 64c7305b37
7 changed files with 665 additions and 1 deletions
+1
View File
@@ -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"},
+117
View File
@@ -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 {
+367
View File
@@ -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
+3
View File
@@ -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())
}
+1 -1
View File
@@ -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",