From ef68c5bffe7ac913fd98fe70eda387859af32796 Mon Sep 17 00:00:00 2001
From: Julian Sommer <115516918+juliansommer@users.noreply.github.com>
Date: Thu, 26 Mar 2026 13:46:46 +1000
Subject: [PATCH] feat: DescribeInstanceCreditSpecifications
---
docs/COMMANDS.md | 2 ++
spinifex/gateway/ec2.go | 3 ++
.../DescribeInstanceCreditSpecifications.go | 31 +++++++++++++++++++
3 files changed, 36 insertions(+)
create mode 100644 spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications.go
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index 11decc713..bd11b3d13 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -89,6 +89,8 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
| `describe-instance-types` | `--filters` (capacity filter only) | `--instance-types`, `--max-results`, `--next-token`, `--dry-run`, all other filters | None | Gateway fans out NATS `ec2.DescribeInstanceTypes` to all nodes → each daemon reports supported types (t3.micro/small/medium/large) with vCPU/memory specs → gateway deduplicates and returns | 1. List all instance types
2. Filter by specific type
3. Filter with `capacity=true` shows available slots
4. Verify vCPU/memory specs match hardware | **DONE** |
| `modify-instance-attribute` | `--instance-id`, `--instance-type`, `--user-data` | `--disable-api-termination`, `--ebs-optimized`, `--source-dest-check`, `--instance-initiated-shutdown-behavior`, `--block-device-mappings`, `--groups`, `--ena-support`, `--sriov-net-support` | Instance must be stopped (in NATS KV) | Gateway validates input (exactly one attribute per call, instance ID format) → NATS `ec2.ModifyInstanceAttribute` with `spinifex-workers` queue group → daemon loads stopped instance from JetStream KV → applies attribute change → writes back to KV → returns `{}` on success. **InstanceType**: updates vm.InstanceType, Config, and Instance fields; clears StateReason (enables recovery from instance-type-missing bug). **UserData**: stores decoded content in vm.UserData and re-encodes to base64 for RunInstancesInput (cloud-init on next start). No instance type pre-validation (matches AWS — invalid types accepted, fail at StartInstances time). | 1. Change instance type while stopped
2. Change user data while stopped
3. Modify running instance (error: NotFound — running instances not in KV)
4. Instance not found (error: InvalidInstanceID.NotFound)
5. Instance not stopped (error: IncorrectInstanceState)
6. Invalid instance type accepted (fails on start with InsufficientInstanceCapacity)
7. StateReason cleared on type change (recovery from capacity-unavailable)
8. Missing/malformed instance ID (error: InvalidInstanceID.Malformed)
9. No attribute set (error: InvalidParameterValue)
10. Multiple attributes in one call (error: InvalidParameterValue) | **DONE** |
| `get-console-output` | `--instance-id` | `--latest` (always returns latest), `--dry-run` | Instance must be running on a node | Gateway sends NATS `ec2.{instanceId}.GetConsoleOutput` (per-instance topic, routed to owning node) → daemon reads console log file from disk → returns last 64KB base64-encoded with timestamp. Always available regardless of serial console access setting (matches AWS behavior). | 1. Get output from running instance
2. Empty log file returns empty output
3. Instance not found (error: InvalidInstanceID.NotFound) | **DONE** |
+| `describe-instance-attribute` | `--instance-id`, `--attribute` (instanceType, userData, instanceInitiatedShutdownBehavior, disableApiTermination, disableApiStop, ebsOptimized, enaSupport, sourceDestCheck, rootDeviceName, kernel, ramdisk) | `--dry-run` | Instance must exist (running or stopped) | Gateway validates input → NATS `ec2.DescribeInstanceAttribute` with `spinifex-workers` queue group → daemon checks running instances first (`d.Instances.VMS`), then stopped instances in JetStream KV → returns single attribute per call (matches AWS behavior). Stored attributes (`instanceType`, `userData`) return real values; unstored attributes return AWS defaults (`instanceInitiatedShutdownBehavior`=stop, `disableApiTermination`=false, etc.) | 1. Get instanceType from running instance
2. Get userData from stopped instance
3. Get default disableApiTermination
4. Invalid attribute name (error)
5. Instance not found (error: InvalidInstanceID.NotFound) | **DONE** |
+| `describe-instance-credit-specifications` | `--instance-ids` | `--filters`, `--max-results`, `--dry-run` | None | Gateway-only stub — returns `CpuCredits: "standard"` for each requested instance ID. No daemon round-trip. T-series credit mode is not persisted. | 1. Get credit spec for T-series instance
2. Multiple instance IDs | **DONE** |
| `monitor-instances` | — | `--instance-ids` | Instance must exist | Enable basic monitoring (CPU, disk, network) → store metrics in NATS KV → return monitoring state | 1. Enable monitoring
2. Verify monitoring state in describe-instances | **NOT STARTED** |
| `unmonitor-instances` | — | `--instance-ids` | Instance must exist | Disable detailed monitoring → revert to basic monitoring → return monitoring state | 1. Disable monitoring
2. Verify monitoring state reverts in describe-instances | **NOT STARTED** |
diff --git a/spinifex/gateway/ec2.go b/spinifex/gateway/ec2.go
index f016d611f..f1e156888 100644
--- a/spinifex/gateway/ec2.go
+++ b/spinifex/gateway/ec2.go
@@ -86,6 +86,9 @@ var ec2Actions = map[string]EC2Handler{
"DescribeInstanceAttribute": ec2Handler(func(input *ec2.DescribeInstanceAttributeInput, gw *GatewayConfig, accountID string) (any, error) {
return gateway_ec2_instance.DescribeInstanceAttribute(input, gw.NATSConn, accountID)
}),
+ "DescribeInstanceCreditSpecifications": ec2Handler(func(input *ec2.DescribeInstanceCreditSpecificationsInput, gw *GatewayConfig, accountID string) (any, error) {
+ return gateway_ec2_instance.DescribeInstanceCreditSpecifications(input)
+ }),
"CreateKeyPair": ec2Handler(func(input *ec2.CreateKeyPairInput, gw *GatewayConfig, accountID string) (any, error) {
return gateway_ec2_key.CreateKeyPair(input, gw.NATSConn, accountID)
}),
diff --git a/spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications.go b/spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications.go
new file mode 100644
index 000000000..3d154bb95
--- /dev/null
+++ b/spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications.go
@@ -0,0 +1,31 @@
+package gateway_ec2_instance
+
+import (
+ "errors"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/service/ec2"
+ "github.com/mulgadc/spinifex/spinifex/awserrors"
+)
+
+// DescribeInstanceCreditSpecifications returns CPU credit specifications for T-series instances.
+// Stub: always returns "standard" mode for each requested instance.
+func DescribeInstanceCreditSpecifications(input *ec2.DescribeInstanceCreditSpecificationsInput) (*ec2.DescribeInstanceCreditSpecificationsOutput, error) {
+ if input == nil {
+ return nil, errors.New(awserrors.ErrorInvalidParameterValue)
+ }
+
+ var specs []*ec2.InstanceCreditSpecification
+ for _, id := range input.InstanceIds {
+ if id != nil && *id != "" {
+ specs = append(specs, &ec2.InstanceCreditSpecification{
+ InstanceId: id,
+ CpuCredits: aws.String("standard"),
+ })
+ }
+ }
+
+ return &ec2.DescribeInstanceCreditSpecificationsOutput{
+ InstanceCreditSpecifications: specs,
+ }, nil
+}