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 +}