From e31e52338044cf49950b763873d91caf45ac8c1b Mon Sep 17 00:00:00 2001
From: Julian Sommer <115516918+juliansommer@users.noreply.github.com>
Date: Mon, 9 Feb 2026 10:19:33 +1100
Subject: [PATCH] feat: describe-account-attributes (#26)
---
TODO.md | 10 ++
hive/gateway/ec2.go | 23 +++++
.../ec2/account/DescribeAccountAttributes.go | 93 +++++++++++++++++++
3 files changed, 126 insertions(+)
create mode 100644 hive/gateway/ec2/account/DescribeAccountAttributes.go
diff --git a/TODO.md b/TODO.md
index 3419f5c36..af560843b 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,5 +1,9 @@
# Hive Development Roadmap
+## Known Bugs
+
+- **QueryParamsToStruct does not match all AWS query param key formats**: The parser tries the Go field name (e.g. `AttributeNames.1`) and the SDK `locationName` tag (e.g. `attributeName.1`), but the AWS CLI sends PascalCase singular form (e.g. `AttributeName.1`). This causes list filters like `--attribute-names` on `describe-account-attributes` to be silently ignored. Affects any SDK field where the Go name, locationName, and wire format all differ. Fix in `hive/awsec2query/parser.go`.
+
## Update Jan 2026
### Big rocks:
@@ -137,6 +141,12 @@ Instance `dmesg`:
| `describe-regions` | *(returns configured region, endpoint, opt-in status from config — input params ignored)* | `--region-names`, `--filters`, `--all-regions`, `--dry-run` | None | Return configured region from hive init config with Endpoint and OptInStatus → local-only response, no NATS | 1. List all regions
2. Filter by region name
3. Verify endpoint URL returned
4. Verify OptInStatus returned | **DONE** |
| `describe-availability-zones` | *(returns configured AZ, region, zone ID, state, opt-in status from config — input params ignored)* | `--zone-names`, `--filters`, `--all-availability-zones` | None | Return configured AZ from hive init config with zone ID, state, group name, network border group → local-only response, no NATS | 1. List all AZs
2. Filter by zone name
3. Verify zone state is available
4. Verify region name matches config | **DONE** |
+### EC2 - Account Attributes
+
+| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
+|---------|-------------------|---------------|---------------|-------------|------------|--------|
+| `describe-account-attributes` | *(returns all 6 hardcoded attributes — input filter parsed but not applied due to QueryParamsToStruct bug, see Known Bugs)* | `--attribute-names` (filter not working) | None | Gateway parses input → returns static account attributes: supported-platforms=VPC, default-vpc=none, max-instances=100, vpc-max-security-groups-per-interface=5, max-elastic-ips=5, vpc-max-elastic-ips=20 → local-only response, no NATS | 1. List all account attributes
2. Filter by attribute name (blocked by parser bug)
3. Verify all 6 attributes returned with correct values | **DONE** |
+
### EC2 - VPC Networking (Core)
Requires Open vSwitch (`apt install openvswitch-switch openvswitch-common`). Single AZ for Hive v1.
diff --git a/hive/gateway/ec2.go b/hive/gateway/ec2.go
index 7a6941f06..e2c2215fd 100644
--- a/hive/gateway/ec2.go
+++ b/hive/gateway/ec2.go
@@ -7,6 +7,7 @@ import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/gofiber/fiber/v2"
"github.com/mulgadc/hive/hive/awsec2query"
+ gateway_ec2_account "github.com/mulgadc/hive/hive/gateway/ec2/account"
gateway_ec2_image "github.com/mulgadc/hive/hive/gateway/ec2/image"
gateway_ec2_instance "github.com/mulgadc/hive/hive/gateway/ec2/instance"
gateway_ec2_key "github.com/mulgadc/hive/hive/gateway/ec2/key"
@@ -479,6 +480,28 @@ func (gw *GatewayConfig) EC2_Request(ctx *fiber.Ctx) error {
return errors.New("failed to marshal response to XML")
}
+ case "DescribeAccountAttributes":
+ var input = &ec2.DescribeAccountAttributesInput{}
+ err = awsec2query.QueryParamsToStruct(queryArgs, input)
+
+ if err != nil {
+ return err
+ }
+
+ output, err := gateway_ec2_account.DescribeAccountAttributes(input)
+
+ if err != nil {
+ return err
+ }
+
+ // Convert to XML
+ payload := utils.GenerateXMLPayload("DescribeAccountAttributesResponse", output)
+ xmlOutput, err = utils.MarshalToXML(payload)
+
+ if err != nil {
+ return errors.New("failed to marshal response to XML")
+ }
+
default:
err = errors.New("InvalidAction")
}
diff --git a/hive/gateway/ec2/account/DescribeAccountAttributes.go b/hive/gateway/ec2/account/DescribeAccountAttributes.go
new file mode 100644
index 000000000..a301e5c65
--- /dev/null
+++ b/hive/gateway/ec2/account/DescribeAccountAttributes.go
@@ -0,0 +1,93 @@
+package gateway_ec2_account
+
+import (
+ "log/slog"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/service/ec2"
+)
+
+// DescribeAccountAttributes returns static account attributes for the Hive platform
+func DescribeAccountAttributes(input *ec2.DescribeAccountAttributesInput) (*ec2.DescribeAccountAttributesOutput, error) {
+ slog.Debug("DescribeAccountAttributes called")
+
+ // Build set of requested attribute names for filtering
+ requestedAttrs := make(map[string]bool)
+ for _, name := range input.AttributeNames {
+ if name != nil {
+ requestedAttrs[*name] = true
+ }
+ }
+
+ // If no specific attributes requested, return all
+ returnAll := len(requestedAttrs) == 0
+
+ var accountAttributes []*ec2.AccountAttribute
+
+ // supported-platforms - VPC only (no EC2-Classic)
+ if returnAll || requestedAttrs["supported-platforms"] {
+ accountAttributes = append(accountAttributes, &ec2.AccountAttribute{
+ AttributeName: aws.String("supported-platforms"),
+ AttributeValues: []*ec2.AccountAttributeValue{
+ {AttributeValue: aws.String("VPC")},
+ },
+ })
+ }
+
+ // default-vpc
+ if returnAll || requestedAttrs["default-vpc"] {
+ accountAttributes = append(accountAttributes, &ec2.AccountAttribute{
+ AttributeName: aws.String("default-vpc"),
+ AttributeValues: []*ec2.AccountAttributeValue{
+ {AttributeValue: aws.String("none")},
+ },
+ })
+ }
+
+ // max-instances
+ if returnAll || requestedAttrs["max-instances"] {
+ accountAttributes = append(accountAttributes, &ec2.AccountAttribute{
+ AttributeName: aws.String("max-instances"),
+ AttributeValues: []*ec2.AccountAttributeValue{
+ {AttributeValue: aws.String("100")},
+ },
+ })
+ }
+
+ // vpc-max-security-groups-per-interface
+ if returnAll || requestedAttrs["vpc-max-security-groups-per-interface"] {
+ accountAttributes = append(accountAttributes, &ec2.AccountAttribute{
+ AttributeName: aws.String("vpc-max-security-groups-per-interface"),
+ AttributeValues: []*ec2.AccountAttributeValue{
+ {AttributeValue: aws.String("5")},
+ },
+ })
+ }
+
+ // max-elastic-ips
+ if returnAll || requestedAttrs["max-elastic-ips"] {
+ accountAttributes = append(accountAttributes, &ec2.AccountAttribute{
+ AttributeName: aws.String("max-elastic-ips"),
+ AttributeValues: []*ec2.AccountAttributeValue{
+ {AttributeValue: aws.String("5")},
+ },
+ })
+ }
+
+ // vpc-max-elastic-ips
+ if returnAll || requestedAttrs["vpc-max-elastic-ips"] {
+ accountAttributes = append(accountAttributes, &ec2.AccountAttribute{
+ AttributeName: aws.String("vpc-max-elastic-ips"),
+ AttributeValues: []*ec2.AccountAttributeValue{
+ {AttributeValue: aws.String("20")},
+ },
+ })
+ }
+
+ output := &ec2.DescribeAccountAttributesOutput{
+ AccountAttributes: accountAttributes,
+ }
+
+ slog.Info("DescribeAccountAttributes completed", "attributeCount", len(accountAttributes))
+ return output, nil
+}