mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
feat: ELBv2 attributes API + DescribeTags (#128)
* feat: add attributes map to LB and TG records with defaults
Replace CrossZoneEnabled bool with Attributes map[string]string on
LoadBalancerRecord and TargetGroupRecord. Add DefaultLoadBalancerAttributes()
and DefaultTargetGroupAttributes() functions. ALBs seed cross-zone enabled
at create time.
* feat: add 4 attributes methods to ELBv2Service interface
* feat: implement ELBv2 attributes service methods and NATS wrappers
Add ModifyTargetGroupAttributes, DescribeTargetGroupAttributes,
ModifyLoadBalancerAttributes, and DescribeLoadBalancerAttributes
to service_impl.go with proper ARN validation and default merging.
Add corresponding NATS request wrappers in service_nats.go.
* feat: add gateway handlers for ELBv2 attributes API endpoints
Add ModifyTargetGroupAttributes, DescribeTargetGroupAttributes,
ModifyLoadBalancerAttributes, and DescribeLoadBalancerAttributes
gateway handlers with input validation and action map registration.
* feat: add daemon handlers & NATS subscriptions for elbv2 attributes
Add 4 handler functions in daemon_handlers_elbv2.go and 4 NATS
subscriptions in daemon.go for ModifyTargetGroupAttributes,
DescribeTargetGroupAttributes, ModifyLoadBalancerAttributes, and
DescribeLoadBalancerAttributes.
* fix: add missing accountID authorization check to ELBv2 attribute handlers
* fix: guard nil attributes and log skips in ELBv2 Modify*Attributes
ModifyTargetGroupAttributes and ModifyLoadBalancerAttributes silently
dropped attributes with nil Key or Value, and would panic on a nil
slice element when dereferencing attr.Key. Add an explicit nil-element
guard and log a warning whenever an attribute is skipped, so callers
and operators can see divergence between the submitted payload and the
persisted state.
* fix: sort ELBv2 attribute responses by key for deterministic output
DescribeTargetGroupAttributes and DescribeLoadBalancerAttributes iterated
a map and appended to a slice, producing random attribute order on each
call. Sort keys before building the response so Terraform plans stay
stable and snapshot tests can rely on consistent ordering.
Closes mulga-910
* feat: add ELBv2 DescribeTags handler (mulga-919)
Required by the Terraform AWS provider's post-create refresh on
aws_lb, aws_lb_target_group, and aws_lb_listener. Without it, the
gateway returns InvalidAction and apply fails after the resources
are already created.
Dispatches by ARN resource segment (loadbalancer/targetgroup/listener)
to the existing record stores. Enforces account scoping via per-type
not-found errors to avoid cross-account existence leaks. Listeners
return an empty Tags slice since ListenerRecord doesn't store tags yet
— that's the exact case the provider hits on every untagged listener
refresh. Output is sorted by key for deterministic responses.
* refactor: parameterize DefaultLoadBalancerAttributes by lbType (mulga-915)
Cross-zone default was split across three places: CreateLoadBalancer seeded
{load_balancing.cross_zone.enabled: true} for ALBs, DefaultLoadBalancerAttributes
returned false unconditionally, and tests asserted both sides. Make
DefaultLoadBalancerAttributes(lbType) the single source of truth: ALBs get
"true", NLBs (and any other type) get "false". CreateLoadBalancer no longer
seeds attributes — DescribeLoadBalancerAttributes derives per-type defaults
from lb.Type on read. Updated TestCreateLoadBalancer_CrossZoneAttributes to
assert via the Describe handler instead of poking stored record state.
* perf: direct KV lookup for ELBv2 *ByArn finders (mulga-913)
GetLoadBalancerByArn and GetTargetGroupByArn used to call ListLoadBalancers /
ListTargetGroups and linearly scan, doing one NATS round-trip per record in
the bucket. Terraform hits Describe*Attributes on every plan/refresh — a
20-ALB / 40-TG workspace was doing ~60 calls × ~60 records = ~3600 KV gets
per refresh where ~60 would suffice. The LB/TG ARN's final path segment IS
the KV key used by GetLoadBalancer(lbID)/GetTargetGroup(tgID), so parse the
short ID from the ARN and do an O(1) lookup, verifying the record's stored
ARN matches as defence-in-depth against any stale-key weirdness.
* fix: reject ELBv2 ModifyAttributes calls where every entry is invalid (mulga-911)
When every submitted attribute was rejected by the nil-guard (nil element,
nil Key, or nil Value), Modify{TargetGroup,LoadBalancer}Attributes still
called PutTargetGroup/PutLoadBalancer with an unchanged record and returned
a successful ModifyOutput with empty Attributes. Only a slog.Warn marked
the skip — the caller saw 200 OK and moved on. Return
ErrorInvalidParameterValue when len(input.Attributes) > 0 and every entry
was rejected, so the caller learns that nothing landed.
* fix: reject unknown ELBv2 ModifyAttributes keys with ValidationError (mulga-912)
ModifyTargetGroupAttributes and ModifyLoadBalancerAttributes had no whitelist
check — any caller-supplied key (typo like 'stickness.enabled', deprecated
keys, or cross-product keys sent to the wrong resource) was persisted verbatim
and merrily echoed back by Describe. AWS rejects unknown keys with
ValidationError; match that so Terraform surfaces the typo at plan time.
Validate each key against DefaultTargetGroupAttributes() /
DefaultLoadBalancerAttributes(lb.Type) and return ErrorValidationError on the
first unknown key, logging both the arn and the offending key name.
* perf: skip no-op Put in ELBv2 ModifyAttributes (mulga-914)
The unconditional PutTargetGroup/PutLoadBalancer fired on every Modify call,
even when the caller sent an empty attribute list or when every submitted
value already matched the stored value. Terraform re-applies the same
attribute set on every drift check, so the steady-state path was doing a
pointless NATS/KV round-trip per call and unnecessarily expanding the
read-modify-write race window.
Track a dirty flag per Modify call: compare each submitted key's value
against tg.Attributes/lb.Attributes and only write to the map (and allocate
the map lazily) when it actually differs. Skip the Put entirely if nothing
changed. Added regression tests that capture the KV entry revision before
and after a no-op modify to prove the write was skipped.
* test: assert exact ModifyAttributes echoed key/value pairs (mulga-917)
Both round-trip tests used assert.Len(t, modOut.Attributes, 2) and never
checked that the returned Attributes actually contained the submitted
keys/values. A regression that pointed at the wrong source, dropped Value,
or returned a copy of defaults would have passed the length check
unchallenged. Build a modMap from the echoed response and assert each
expected Key/Value pair explicitly.
* test: cover sequential ModifyAttributes merge and same-key overwrite (mulga-916)
All existing round-trip tests did one Modify then Describe. No test covered:
Modify {A}, Modify {B}, Describe, assert BOTH keys present; or Modify
overwriting the same key on a second call. A future refactor that replaced
tg.Attributes instead of mutating in place would pass every existing test
but silently wipe previous attributes on each Modify — the most likely
real-world ALB/TG bug. Add sequential-merge tests for both TG and LB
handlers that exercise multi-key merge, same-key overwrite, and
cross-survival of unrelated keys.
This commit is contained in:
@@ -589,6 +589,12 @@ All ELBv2 resources are stored in the `spinifex-elbv2` JetStream KV bucket. Key
|
||||
| `delete-listener` | `--listener-arn` | `--dry-run` | Listener must exist | Gateway validates ListenerArn (required) → NATS `elbv2.DeleteListener` → daemon verifies listener exists and belongs to account → deletes from KV → fetches parent LB → calls `pushConfig(lb)` to regenerate HAProxy config without this listener → returns success | 1. Delete listener<br>2. Not found (ListenerNotFound) | **DONE** |
|
||||
| `describe-listeners` | `--load-balancer-arn`, `--listener-arns` | `--page-size`, `--marker`, `--dry-run` | None | NATS `elbv2.DescribeListeners` → daemon lists listeners → if LoadBalancerArn specified, filters to that LB → applies ARN filter → account isolation → returns Listener list | 1. Filter by LB ARN<br>2. Account isolation | **DONE** |
|
||||
|
||||
#### ELBv2 - Tags
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `describe-tags` | `--resource-arns` (loadbalancer, targetgroup, listener) | — | Resource(s) must exist | Gateway validates non-empty ResourceArns → NATS `elbv2.DescribeTags` → daemon parses each ARN's resource segment to dispatch by type → looks up record (LB/TG/listener) → enforces account isolation (cross-account → not-found) → returns TagDescriptions in input order with sorted-by-key Tag list. Listener records don't store tags yet, so they always return an empty Tags slice (matches AWS behaviour for untagged resources). Required by Terraform AWS provider post-create refresh on `aws_lb`, `aws_lb_target_group`, and `aws_lb_listener`. | 1. LB tags round-trip<br>2. TG tags round-trip<br>3. Listener returns empty Tags (no error)<br>4. Multiple ARNs in one call<br>5. Unknown LB/TG/listener (per-type not-found)<br>6. Cross-account ARN (not-found, no leak)<br>7. Invalid/non-ELBv2 ARN (InvalidParameterValue)<br>8. Empty/nil ResourceArns (MissingParameter)<br>9. Untagged LB returns empty Tags | **DONE** |
|
||||
|
||||
#### ELBv2 - Not Implemented
|
||||
|
||||
| Feature | Description | Priority | Status |
|
||||
|
||||
@@ -491,8 +491,13 @@ func (d *Daemon) subscribeAll() error {
|
||||
{"elbv2.CreateListener", d.handleELBv2CreateListener, "spinifex-workers"},
|
||||
{"elbv2.DeleteListener", d.handleELBv2DeleteListener, "spinifex-workers"},
|
||||
{"elbv2.DescribeListeners", d.handleELBv2DescribeListeners, "spinifex-workers"},
|
||||
{"elbv2.DescribeTags", d.handleELBv2DescribeTags, "spinifex-workers"},
|
||||
{"elbv2.LBAgentHeartbeat", d.handleELBv2LBAgentHeartbeat, "spinifex-workers"},
|
||||
{"elbv2.GetLBConfig", d.handleELBv2GetLBConfig, "spinifex-workers"},
|
||||
{"elbv2.ModifyTargetGroupAttributes", d.handleELBv2ModifyTargetGroupAttributes, "spinifex-workers"},
|
||||
{"elbv2.DescribeTargetGroupAttributes", d.handleELBv2DescribeTargetGroupAttributes, "spinifex-workers"},
|
||||
{"elbv2.ModifyLoadBalancerAttributes", d.handleELBv2ModifyLoadBalancerAttributes, "spinifex-workers"},
|
||||
{"elbv2.DescribeLoadBalancerAttributes", d.handleELBv2DescribeLoadBalancerAttributes, "spinifex-workers"},
|
||||
{fmt.Sprintf("spinifex.admin.%s.health", d.node), d.handleHealthCheck, ""},
|
||||
{"spinifex.nodes.discover", d.handleNodeDiscover, ""},
|
||||
{"spinifex.node.status", d.handleNodeStatus, ""},
|
||||
|
||||
@@ -50,6 +50,10 @@ func (d *Daemon) handleELBv2DescribeListeners(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.DescribeListeners)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2DescribeTags(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.DescribeTags)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2LBAgentHeartbeat(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.LBAgentHeartbeat)
|
||||
}
|
||||
@@ -57,3 +61,19 @@ func (d *Daemon) handleELBv2LBAgentHeartbeat(msg *nats.Msg) {
|
||||
func (d *Daemon) handleELBv2GetLBConfig(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.GetLBConfig)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2ModifyTargetGroupAttributes(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.ModifyTargetGroupAttributes)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2DescribeTargetGroupAttributes(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.DescribeTargetGroupAttributes)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2ModifyLoadBalancerAttributes(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.ModifyLoadBalancerAttributes)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2DescribeLoadBalancerAttributes(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.DescribeLoadBalancerAttributes)
|
||||
}
|
||||
|
||||
@@ -76,12 +76,27 @@ var elbv2Actions = map[string]ELBv2Handler{
|
||||
"DescribeListeners": elbv2Handler(func(input *elbv2.DescribeListenersInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.DescribeListeners(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeTags": elbv2Handler(func(input *elbv2.DescribeTagsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.DescribeTags(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"LBAgentHeartbeat": elbv2Handler(func(input *handlers_elbv2.LBAgentHeartbeatInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.LBAgentHeartbeat(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"GetLBConfig": elbv2Handler(func(input *handlers_elbv2.GetLBConfigInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.GetLBConfig(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"ModifyTargetGroupAttributes": elbv2Handler(func(input *elbv2.ModifyTargetGroupAttributesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.ModifyTargetGroupAttributes(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeTargetGroupAttributes": elbv2Handler(func(input *elbv2.DescribeTargetGroupAttributesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.DescribeTargetGroupAttributes(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"ModifyLoadBalancerAttributes": elbv2Handler(func(input *elbv2.ModifyLoadBalancerAttributesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.ModifyLoadBalancerAttributes(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeLoadBalancerAttributes": elbv2Handler(func(input *elbv2.DescribeLoadBalancerAttributesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.DescribeLoadBalancerAttributes(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
}
|
||||
|
||||
func (gw *GatewayConfig) ELBv2_Request(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// DescribeLoadBalancerAttributes handles the ELBv2 DescribeLoadBalancerAttributes API call.
|
||||
func DescribeLoadBalancerAttributes(input *elbv2.DescribeLoadBalancerAttributesInput, natsConn *nats.Conn, accountID string) (elbv2.DescribeLoadBalancerAttributesOutput, error) {
|
||||
var output elbv2.DescribeLoadBalancerAttributesOutput
|
||||
|
||||
if input == nil {
|
||||
return output, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if input.LoadBalancerArn == nil || *input.LoadBalancerArn == "" {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.DescribeLoadBalancerAttributes(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// DescribeTags handles the ELBv2 DescribeTags API call. Returns the tags for
|
||||
// each ELBv2 resource (load balancer, target group, listener) named in the
|
||||
// input ResourceArns. Untagged resources still produce a TagDescription with
|
||||
// an empty Tags slice — this matches AWS behaviour and is what the Terraform
|
||||
// AWS provider expects during post-create refresh.
|
||||
func DescribeTags(input *elbv2.DescribeTagsInput, natsConn *nats.Conn, accountID string) (elbv2.DescribeTagsOutput, error) {
|
||||
var output elbv2.DescribeTagsOutput
|
||||
|
||||
if input == nil || len(input.ResourceArns) == 0 {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.DescribeTags(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// DescribeTargetGroupAttributes handles the ELBv2 DescribeTargetGroupAttributes API call.
|
||||
func DescribeTargetGroupAttributes(input *elbv2.DescribeTargetGroupAttributesInput, natsConn *nats.Conn, accountID string) (elbv2.DescribeTargetGroupAttributesOutput, error) {
|
||||
var output elbv2.DescribeTargetGroupAttributesOutput
|
||||
|
||||
if input == nil {
|
||||
return output, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if input.TargetGroupArn == nil || *input.TargetGroupArn == "" {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.DescribeTargetGroupAttributes(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// ModifyLoadBalancerAttributes handles the ELBv2 ModifyLoadBalancerAttributes API call.
|
||||
func ModifyLoadBalancerAttributes(input *elbv2.ModifyLoadBalancerAttributesInput, natsConn *nats.Conn, accountID string) (elbv2.ModifyLoadBalancerAttributesOutput, error) {
|
||||
var output elbv2.ModifyLoadBalancerAttributesOutput
|
||||
|
||||
if input == nil {
|
||||
return output, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if input.LoadBalancerArn == nil || *input.LoadBalancerArn == "" {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
if len(input.Attributes) == 0 {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.ModifyLoadBalancerAttributes(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// ModifyTargetGroupAttributes handles the ELBv2 ModifyTargetGroupAttributes API call.
|
||||
func ModifyTargetGroupAttributes(input *elbv2.ModifyTargetGroupAttributesInput, natsConn *nats.Conn, accountID string) (elbv2.ModifyTargetGroupAttributesOutput, error) {
|
||||
var output elbv2.ModifyTargetGroupAttributesOutput
|
||||
|
||||
if input == nil {
|
||||
return output, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if input.TargetGroupArn == nil || *input.TargetGroupArn == "" {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
if len(input.Attributes) == 0 {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.ModifyTargetGroupAttributes(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -144,3 +144,85 @@ func TestDescribeListeners_NilInput(t *testing.T) {
|
||||
_, err := DescribeListeners(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestModifyTargetGroupAttributes_NilInput(t *testing.T) {
|
||||
_, err := ModifyTargetGroupAttributes(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestModifyTargetGroupAttributes_MissingArn(t *testing.T) {
|
||||
_, err := ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestModifyTargetGroupAttributes_EmptyArn(t *testing.T) {
|
||||
_, err := ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: aws.String(""),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestModifyTargetGroupAttributes_MissingAttributes(t *testing.T) {
|
||||
_, err := ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-tg/abc"),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeTargetGroupAttributes_NilInput(t *testing.T) {
|
||||
_, err := DescribeTargetGroupAttributes(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDescribeTargetGroupAttributes_MissingArn(t *testing.T) {
|
||||
_, err := DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeTargetGroupAttributes_EmptyArn(t *testing.T) {
|
||||
_, err := DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: aws.String(""),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestModifyLoadBalancerAttributes_NilInput(t *testing.T) {
|
||||
_, err := ModifyLoadBalancerAttributes(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestModifyLoadBalancerAttributes_MissingArn(t *testing.T) {
|
||||
_, err := ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestModifyLoadBalancerAttributes_EmptyArn(t *testing.T) {
|
||||
_, err := ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: aws.String(""),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestModifyLoadBalancerAttributes_MissingAttributes(t *testing.T) {
|
||||
_, err := ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/abc"),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_NilInput(t *testing.T) {
|
||||
_, err := DescribeLoadBalancerAttributes(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_MissingArn(t *testing.T) {
|
||||
_, err := DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_EmptyArn(t *testing.T) {
|
||||
_, err := DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: aws.String(""),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,13 @@ func TestELBv2ActionsMap_AllActionsRegistered(t *testing.T) {
|
||||
"CreateListener",
|
||||
"DeleteListener",
|
||||
"DescribeListeners",
|
||||
"DescribeTags",
|
||||
"LBAgentHeartbeat",
|
||||
"GetLBConfig",
|
||||
"ModifyTargetGroupAttributes",
|
||||
"DescribeTargetGroupAttributes",
|
||||
"ModifyLoadBalancerAttributes",
|
||||
"DescribeLoadBalancerAttributes",
|
||||
}
|
||||
|
||||
for _, action := range expectedActions {
|
||||
|
||||
@@ -7,10 +7,14 @@ type ELBv2Service interface {
|
||||
CreateLoadBalancer(input *elbv2.CreateLoadBalancerInput, accountID string) (*elbv2.CreateLoadBalancerOutput, error)
|
||||
DeleteLoadBalancer(input *elbv2.DeleteLoadBalancerInput, accountID string) (*elbv2.DeleteLoadBalancerOutput, error)
|
||||
DescribeLoadBalancers(input *elbv2.DescribeLoadBalancersInput, accountID string) (*elbv2.DescribeLoadBalancersOutput, error)
|
||||
ModifyLoadBalancerAttributes(input *elbv2.ModifyLoadBalancerAttributesInput, accountID string) (*elbv2.ModifyLoadBalancerAttributesOutput, error)
|
||||
DescribeLoadBalancerAttributes(input *elbv2.DescribeLoadBalancerAttributesInput, accountID string) (*elbv2.DescribeLoadBalancerAttributesOutput, error)
|
||||
|
||||
CreateTargetGroup(input *elbv2.CreateTargetGroupInput, accountID string) (*elbv2.CreateTargetGroupOutput, error)
|
||||
DeleteTargetGroup(input *elbv2.DeleteTargetGroupInput, accountID string) (*elbv2.DeleteTargetGroupOutput, error)
|
||||
DescribeTargetGroups(input *elbv2.DescribeTargetGroupsInput, accountID string) (*elbv2.DescribeTargetGroupsOutput, error)
|
||||
ModifyTargetGroupAttributes(input *elbv2.ModifyTargetGroupAttributesInput, accountID string) (*elbv2.ModifyTargetGroupAttributesOutput, error)
|
||||
DescribeTargetGroupAttributes(input *elbv2.DescribeTargetGroupAttributesInput, accountID string) (*elbv2.DescribeTargetGroupAttributesOutput, error)
|
||||
|
||||
RegisterTargets(input *elbv2.RegisterTargetsInput, accountID string) (*elbv2.RegisterTargetsOutput, error)
|
||||
DeregisterTargets(input *elbv2.DeregisterTargetsInput, accountID string) (*elbv2.DeregisterTargetsOutput, error)
|
||||
@@ -20,6 +24,8 @@ type ELBv2Service interface {
|
||||
DeleteListener(input *elbv2.DeleteListenerInput, accountID string) (*elbv2.DeleteListenerOutput, error)
|
||||
DescribeListeners(input *elbv2.DescribeListenersInput, accountID string) (*elbv2.DescribeListenersOutput, error)
|
||||
|
||||
DescribeTags(input *elbv2.DescribeTagsInput, accountID string) (*elbv2.DescribeTagsOutput, error)
|
||||
|
||||
LBAgentHeartbeat(input *LBAgentHeartbeatInput, accountID string) (*LBAgentHeartbeatOutput, error)
|
||||
GetLBConfig(input *GetLBConfigInput, accountID string) (*GetLBConfigOutput, error)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -425,6 +428,37 @@ func buildListenerArn(region, accountID, lbName, lbID, listenerID, lbType string
|
||||
return fmt.Sprintf("arn:aws:elasticloadbalancing:%s:%s:listener/%s/%s/%s/%s", region, accountID, lbArnPathSegment(lbType), lbName, lbID, listenerID)
|
||||
}
|
||||
|
||||
// ELBv2 resource types as they appear in the resource segment of an ARN.
|
||||
const (
|
||||
elbv2ResourceLoadBalancer = "loadbalancer"
|
||||
elbv2ResourceTargetGroup = "targetgroup"
|
||||
elbv2ResourceListener = "listener"
|
||||
)
|
||||
|
||||
// elbv2ResourceTypeFromArn extracts the resource type from an ELBv2 ARN.
|
||||
// ELBv2 ARNs have the form
|
||||
// "arn:aws:elasticloadbalancing:{region}:{account}:{type}/...". Returns one
|
||||
// of "loadbalancer", "targetgroup", "listener" — anything else yields
|
||||
// ErrorInvalidParameterValue so callers can surface a proper API error.
|
||||
func elbv2ResourceTypeFromArn(arn string) (string, error) {
|
||||
parts := strings.SplitN(arn, ":", 6)
|
||||
if len(parts) < 6 || parts[0] != "arn" || parts[2] != "elasticloadbalancing" {
|
||||
return "", errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
resourceSegment := parts[5]
|
||||
slash := strings.Index(resourceSegment, "/")
|
||||
if slash <= 0 {
|
||||
return "", errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
resourceType := resourceSegment[:slash]
|
||||
switch resourceType {
|
||||
case elbv2ResourceLoadBalancer, elbv2ResourceTargetGroup, elbv2ResourceListener:
|
||||
return resourceType, nil
|
||||
default:
|
||||
return "", errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
}
|
||||
|
||||
// isCompatibleProtocol checks whether a listener protocol is compatible with a
|
||||
// target group protocol. ALB: HTTP/HTTPS listeners can forward to HTTP or HTTPS
|
||||
// TGs. NLB: TCP→TCP, UDP→UDP, TLS→TCP or TLS, TCP_UDP→TCP_UDP.
|
||||
@@ -632,28 +666,30 @@ func (s *ELBv2ServiceImpl) CreateLoadBalancer(input *elbv2.CreateLoadBalancerInp
|
||||
state = StateFailed
|
||||
}
|
||||
|
||||
// Attributes are intentionally left nil — DescribeLoadBalancerAttributes
|
||||
// derives per-type defaults from lb.Type via DefaultLoadBalancerAttributes.
|
||||
// Callers that want a non-default value must call ModifyLoadBalancerAttributes.
|
||||
record := &LoadBalancerRecord{
|
||||
LoadBalancerArn: lbArn,
|
||||
LoadBalancerID: lbID,
|
||||
DNSName: dnsName,
|
||||
Name: name,
|
||||
Scheme: scheme,
|
||||
Type: lbType,
|
||||
CrossZoneEnabled: lbType == LoadBalancerTypeApplication,
|
||||
State: state,
|
||||
VpcId: vpcID,
|
||||
SecurityGroups: securityGroups,
|
||||
Subnets: subnets,
|
||||
AvailZones: availZones,
|
||||
ENIs: eniIDs,
|
||||
InstanceID: albInstanceID,
|
||||
VPCIP: albVPCIP,
|
||||
HostPorts: hostPorts,
|
||||
IPAddressType: IPAddressTypeIPv4,
|
||||
NodeID: s.nodeID,
|
||||
Tags: tags,
|
||||
AccountID: accountID,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
LoadBalancerArn: lbArn,
|
||||
LoadBalancerID: lbID,
|
||||
DNSName: dnsName,
|
||||
Name: name,
|
||||
Scheme: scheme,
|
||||
Type: lbType,
|
||||
State: state,
|
||||
VpcId: vpcID,
|
||||
SecurityGroups: securityGroups,
|
||||
Subnets: subnets,
|
||||
AvailZones: availZones,
|
||||
ENIs: eniIDs,
|
||||
InstanceID: albInstanceID,
|
||||
VPCIP: albVPCIP,
|
||||
HostPorts: hostPorts,
|
||||
IPAddressType: IPAddressTypeIPv4,
|
||||
NodeID: s.nodeID,
|
||||
Tags: tags,
|
||||
AccountID: accountID,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
if err := s.store.PutLoadBalancer(record); err != nil {
|
||||
@@ -1335,8 +1371,314 @@ func (s *ELBv2ServiceImpl) DescribeListeners(input *elbv2.DescribeListenersInput
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- Tag operations ---
|
||||
|
||||
// DescribeTags returns tags for one or more ELBv2 resources (load balancers,
|
||||
// target groups, listeners). Tag data is read from the existing record stores
|
||||
// — Spinifex doesn't have a separate tag KV. Listeners currently never store
|
||||
// tags, so they always return an empty Tags slice (matches AWS behaviour for
|
||||
// untagged resources). Cross-account or unknown ARNs return the per-resource
|
||||
// not-found error so existence isn't leaked across accounts.
|
||||
func (s *ELBv2ServiceImpl) DescribeTags(input *elbv2.DescribeTagsInput, accountID string) (*elbv2.DescribeTagsOutput, error) {
|
||||
if input == nil || len(input.ResourceArns) == 0 {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
descriptions := make([]*elbv2.TagDescription, 0, len(input.ResourceArns))
|
||||
for _, arnPtr := range input.ResourceArns {
|
||||
if arnPtr == nil || *arnPtr == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
arn := *arnPtr
|
||||
|
||||
resourceType, err := elbv2ResourceTypeFromArn(arn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
tags map[string]string
|
||||
ownerAccount string
|
||||
notFoundError string
|
||||
found bool
|
||||
)
|
||||
switch resourceType {
|
||||
case elbv2ResourceLoadBalancer:
|
||||
notFoundError = awserrors.ErrorELBv2LoadBalancerNotFound
|
||||
lb, lbErr := s.store.GetLoadBalancerByArn(arn)
|
||||
if lbErr != nil {
|
||||
slog.Error("DescribeTags: failed to get LB", "arn", arn, "err", lbErr)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if lb != nil {
|
||||
found = true
|
||||
tags = lb.Tags
|
||||
ownerAccount = lb.AccountID
|
||||
}
|
||||
case elbv2ResourceTargetGroup:
|
||||
notFoundError = awserrors.ErrorELBv2TargetGroupNotFound
|
||||
tg, tgErr := s.store.GetTargetGroupByArn(arn)
|
||||
if tgErr != nil {
|
||||
slog.Error("DescribeTags: failed to get target group", "arn", arn, "err", tgErr)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if tg != nil {
|
||||
found = true
|
||||
tags = tg.Tags
|
||||
ownerAccount = tg.AccountID
|
||||
}
|
||||
case elbv2ResourceListener:
|
||||
notFoundError = awserrors.ErrorELBv2ListenerNotFound
|
||||
l, lErr := s.store.GetListenerByArn(arn)
|
||||
if lErr != nil {
|
||||
slog.Error("DescribeTags: failed to get listener", "arn", arn, "err", lErr)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if l != nil {
|
||||
found = true
|
||||
ownerAccount = l.AccountID
|
||||
// Listeners don't store tags yet — leave map nil.
|
||||
}
|
||||
}
|
||||
|
||||
if !found || ownerAccount != accountID {
|
||||
return nil, errors.New(notFoundError)
|
||||
}
|
||||
|
||||
descriptions = append(descriptions, &elbv2.TagDescription{
|
||||
ResourceArn: aws.String(arn),
|
||||
Tags: tagsMapToSDK(tags),
|
||||
})
|
||||
}
|
||||
|
||||
return &elbv2.DescribeTagsOutput{
|
||||
TagDescriptions: descriptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// tagsMapToSDK converts a tag map into the SDK Tag slice with deterministic
|
||||
// (sorted-by-key) ordering. Returns nil for empty/nil input so the response
|
||||
// matches AWS for untagged resources.
|
||||
func tagsMapToSDK(tags map[string]string) []*elbv2.Tag {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(tags))
|
||||
for k := range tags {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
out := make([]*elbv2.Tag, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
out = append(out, &elbv2.Tag{Key: aws.String(k), Value: aws.String(tags[k])})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// --- SDK type conversion helpers ---
|
||||
|
||||
func (s *ELBv2ServiceImpl) ModifyTargetGroupAttributes(input *elbv2.ModifyTargetGroupAttributesInput, accountID string) (*elbv2.ModifyTargetGroupAttributesOutput, error) {
|
||||
if input.TargetGroupArn == nil || *input.TargetGroupArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
tg, err := s.store.GetTargetGroupByArn(*input.TargetGroupArn)
|
||||
if err != nil {
|
||||
slog.Error("ModifyTargetGroupAttributes: failed to get TG", "arn", *input.TargetGroupArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if tg == nil || tg.AccountID != accountID {
|
||||
return nil, errors.New(awserrors.ErrorELBv2TargetGroupNotFound)
|
||||
}
|
||||
|
||||
knownTGAttrs := DefaultTargetGroupAttributes()
|
||||
var submitted []*elbv2.TargetGroupAttribute
|
||||
dirty := false
|
||||
for _, attr := range input.Attributes {
|
||||
if attr == nil {
|
||||
slog.Warn("ModifyTargetGroupAttributes: skipping nil attribute element", "arn", *input.TargetGroupArn)
|
||||
continue
|
||||
}
|
||||
if attr.Key == nil || attr.Value == nil {
|
||||
slog.Warn("ModifyTargetGroupAttributes: skipping attribute with nil Key or Value", "arn", *input.TargetGroupArn)
|
||||
continue
|
||||
}
|
||||
if _, ok := knownTGAttrs[*attr.Key]; !ok {
|
||||
slog.Warn("ModifyTargetGroupAttributes: rejecting unknown attribute key", "arn", *input.TargetGroupArn, "key", *attr.Key)
|
||||
return nil, errors.New(awserrors.ErrorValidationError)
|
||||
}
|
||||
submitted = append(submitted, &elbv2.TargetGroupAttribute{
|
||||
Key: attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
existing, exists := tg.Attributes[*attr.Key]
|
||||
if exists && existing == *attr.Value {
|
||||
continue // value already matches stored — no mutation needed
|
||||
}
|
||||
if tg.Attributes == nil {
|
||||
tg.Attributes = make(map[string]string)
|
||||
}
|
||||
tg.Attributes[*attr.Key] = *attr.Value
|
||||
dirty = true
|
||||
}
|
||||
|
||||
// If the caller sent attributes but every single one was rejected by the
|
||||
// nil guard above, surface that as an error instead of silently returning
|
||||
// a successful empty response — otherwise the caller thinks the write
|
||||
// landed when nothing was actually applied.
|
||||
if len(input.Attributes) > 0 && len(submitted) == 0 {
|
||||
slog.Warn("ModifyTargetGroupAttributes: all submitted attributes were invalid", "arn", *input.TargetGroupArn, "submitted_count", len(input.Attributes))
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
// Skip the NATS/KV write when nothing changed. Terraform re-applies the same
|
||||
// attribute set on every drift check, so this kills ~all Modify traffic
|
||||
// during steady state and narrows the read-modify-write race window.
|
||||
if dirty {
|
||||
if err := s.store.PutTargetGroup(tg); err != nil {
|
||||
slog.Error("ModifyTargetGroupAttributes: failed to persist TG", "arn", *input.TargetGroupArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
}
|
||||
|
||||
return &elbv2.ModifyTargetGroupAttributesOutput{
|
||||
Attributes: submitted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ELBv2ServiceImpl) DescribeTargetGroupAttributes(input *elbv2.DescribeTargetGroupAttributesInput, accountID string) (*elbv2.DescribeTargetGroupAttributesOutput, error) {
|
||||
if input.TargetGroupArn == nil || *input.TargetGroupArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
tg, err := s.store.GetTargetGroupByArn(*input.TargetGroupArn)
|
||||
if err != nil {
|
||||
slog.Error("DescribeTargetGroupAttributes: failed to get TG", "arn", *input.TargetGroupArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if tg == nil || tg.AccountID != accountID {
|
||||
return nil, errors.New(awserrors.ErrorELBv2TargetGroupNotFound)
|
||||
}
|
||||
|
||||
merged := DefaultTargetGroupAttributes()
|
||||
maps.Copy(merged, tg.Attributes)
|
||||
|
||||
// Sort keys for deterministic output — Terraform diffs and snapshot tests
|
||||
// depend on stable attribute ordering.
|
||||
keys := slices.Sorted(maps.Keys(merged))
|
||||
attrs := make([]*elbv2.TargetGroupAttribute, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
attrs = append(attrs, &elbv2.TargetGroupAttribute{
|
||||
Key: aws.String(k),
|
||||
Value: aws.String(merged[k]),
|
||||
})
|
||||
}
|
||||
|
||||
return &elbv2.DescribeTargetGroupAttributesOutput{
|
||||
Attributes: attrs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ELBv2ServiceImpl) ModifyLoadBalancerAttributes(input *elbv2.ModifyLoadBalancerAttributesInput, accountID string) (*elbv2.ModifyLoadBalancerAttributesOutput, error) {
|
||||
if input.LoadBalancerArn == nil || *input.LoadBalancerArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
lb, err := s.store.GetLoadBalancerByArn(*input.LoadBalancerArn)
|
||||
if err != nil {
|
||||
slog.Error("ModifyLoadBalancerAttributes: failed to get LB", "arn", *input.LoadBalancerArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if lb == nil || lb.AccountID != accountID {
|
||||
return nil, errors.New(awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
knownLBAttrs := DefaultLoadBalancerAttributes(lb.Type)
|
||||
var submitted []*elbv2.LoadBalancerAttribute
|
||||
dirty := false
|
||||
for _, attr := range input.Attributes {
|
||||
if attr == nil {
|
||||
slog.Warn("ModifyLoadBalancerAttributes: skipping nil attribute element", "arn", *input.LoadBalancerArn)
|
||||
continue
|
||||
}
|
||||
if attr.Key == nil || attr.Value == nil {
|
||||
slog.Warn("ModifyLoadBalancerAttributes: skipping attribute with nil Key or Value", "arn", *input.LoadBalancerArn)
|
||||
continue
|
||||
}
|
||||
if _, ok := knownLBAttrs[*attr.Key]; !ok {
|
||||
slog.Warn("ModifyLoadBalancerAttributes: rejecting unknown attribute key", "arn", *input.LoadBalancerArn, "key", *attr.Key, "lbType", lb.Type)
|
||||
return nil, errors.New(awserrors.ErrorValidationError)
|
||||
}
|
||||
submitted = append(submitted, &elbv2.LoadBalancerAttribute{
|
||||
Key: attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
existing, exists := lb.Attributes[*attr.Key]
|
||||
if exists && existing == *attr.Value {
|
||||
continue
|
||||
}
|
||||
if lb.Attributes == nil {
|
||||
lb.Attributes = make(map[string]string)
|
||||
}
|
||||
lb.Attributes[*attr.Key] = *attr.Value
|
||||
dirty = true
|
||||
}
|
||||
|
||||
// If the caller sent attributes but every single one was rejected by the
|
||||
// nil guard above, surface that as an error instead of silently returning
|
||||
// a successful empty response.
|
||||
if len(input.Attributes) > 0 && len(submitted) == 0 {
|
||||
slog.Warn("ModifyLoadBalancerAttributes: all submitted attributes were invalid", "arn", *input.LoadBalancerArn, "submitted_count", len(input.Attributes))
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
// Skip the NATS/KV write when nothing changed. See
|
||||
// ModifyTargetGroupAttributes for the Terraform-drift-check motivation.
|
||||
if dirty {
|
||||
if err := s.store.PutLoadBalancer(lb); err != nil {
|
||||
slog.Error("ModifyLoadBalancerAttributes: failed to persist LB", "arn", *input.LoadBalancerArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
}
|
||||
|
||||
return &elbv2.ModifyLoadBalancerAttributesOutput{
|
||||
Attributes: submitted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ELBv2ServiceImpl) DescribeLoadBalancerAttributes(input *elbv2.DescribeLoadBalancerAttributesInput, accountID string) (*elbv2.DescribeLoadBalancerAttributesOutput, error) {
|
||||
if input.LoadBalancerArn == nil || *input.LoadBalancerArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
lb, err := s.store.GetLoadBalancerByArn(*input.LoadBalancerArn)
|
||||
if err != nil {
|
||||
slog.Error("DescribeLoadBalancerAttributes: failed to get LB", "arn", *input.LoadBalancerArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if lb == nil || lb.AccountID != accountID {
|
||||
return nil, errors.New(awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
merged := DefaultLoadBalancerAttributes(lb.Type)
|
||||
maps.Copy(merged, lb.Attributes)
|
||||
|
||||
// Sort keys for deterministic output — Terraform diffs and snapshot tests
|
||||
// depend on stable attribute ordering.
|
||||
keys := slices.Sorted(maps.Keys(merged))
|
||||
attrs := make([]*elbv2.LoadBalancerAttribute, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
attrs = append(attrs, &elbv2.LoadBalancerAttribute{
|
||||
Key: aws.String(k),
|
||||
Value: aws.String(merged[k]),
|
||||
})
|
||||
}
|
||||
|
||||
return &elbv2.DescribeLoadBalancerAttributesOutput{
|
||||
Attributes: attrs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ELBv2ServiceImpl) lbRecordToSDK(r *LoadBalancerRecord) *elbv2.LoadBalancer {
|
||||
lb := &elbv2.LoadBalancer{
|
||||
LoadBalancerArn: aws.String(r.LoadBalancerArn),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package handlers_elbv2
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/mulgadc/spinifex/spinifex/testutil"
|
||||
"github.com/mulgadc/spinifex/spinifex/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -139,30 +141,47 @@ func TestCreateLoadBalancer_NetworkType_RejectsSecurityGroups(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "InvalidConfigurationRequest")
|
||||
}
|
||||
|
||||
func TestCreateLoadBalancer_NetworkType_CrossZoneDisabled(t *testing.T) {
|
||||
func TestCreateLoadBalancer_CrossZoneAttributes(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
out, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
// NLB: no stored attributes, Describe should return default cross-zone=false.
|
||||
nlbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("nlb-cz"),
|
||||
Type: aws.String("network"),
|
||||
}, testAccountID)
|
||||
|
||||
require.NoError(t, err)
|
||||
// Verify the stored record has CrossZoneEnabled=false
|
||||
lb, err := svc.store.GetLoadBalancerByName("nlb-cz", testAccountID)
|
||||
nlbRec, err := svc.store.GetLoadBalancerByName("nlb-cz", testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, lb.CrossZoneEnabled)
|
||||
assert.Nil(t, nlbRec.Attributes, "NLB should store nil attributes — defaults come from the handler")
|
||||
|
||||
// ALB should default to CrossZoneEnabled=true
|
||||
_, err = svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
nlbDesc, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: nlbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
nlbAttrs := make(map[string]string)
|
||||
for _, a := range nlbDesc.Attributes {
|
||||
nlbAttrs[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "false", nlbAttrs["load_balancing.cross_zone.enabled"])
|
||||
|
||||
// ALB: no stored attributes either, Describe should return default cross-zone=true.
|
||||
albOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("alb-cz"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
albRec, err := svc.store.GetLoadBalancerByName("alb-cz", testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, albRec.CrossZoneEnabled)
|
||||
assert.Nil(t, albRec.Attributes, "ALB should no longer seed attributes at create time")
|
||||
|
||||
_ = out // suppress unused warning
|
||||
albDesc, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: albOut.LoadBalancers[0].LoadBalancerArn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
albAttrs := make(map[string]string)
|
||||
for _, a := range albDesc.Attributes {
|
||||
albAttrs[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "true", albAttrs["load_balancing.cross_zone.enabled"])
|
||||
}
|
||||
|
||||
func TestCreateLoadBalancer_InvalidType(t *testing.T) {
|
||||
@@ -1668,3 +1687,911 @@ func TestUpdateStoredConfig_MissingTargetGroup(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, stored.ConfigHash)
|
||||
}
|
||||
|
||||
// --- Attribute tests ---
|
||||
|
||||
func TestDescribeTargetGroupAttributes_Defaults(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-attr-defaults")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
defaults := DefaultTargetGroupAttributes()
|
||||
assert.Len(t, out.Attributes, len(defaults))
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range out.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
for k, v := range defaults {
|
||||
assert.Equal(t, v, attrMap[k], "default mismatch for key %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_ALBDefaults(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("alb-attr-defaults")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
defaults := DefaultLoadBalancerAttributes(LoadBalancerTypeApplication)
|
||||
assert.Len(t, out.Attributes, len(defaults))
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range out.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
// ALB default cross-zone is true — comes from the per-type default, not seeding.
|
||||
assert.Equal(t, "true", attrMap["load_balancing.cross_zone.enabled"])
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_NLBDefaults(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("nlb-attr-defaults"),
|
||||
Type: aws.String("network"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range out.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
// NLB should fall through to default false
|
||||
assert.Equal(t, "false", attrMap["load_balancing.cross_zone.enabled"])
|
||||
}
|
||||
|
||||
func TestModifyDescribeTargetGroupAttributes_RoundTrip(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-attr-rt")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := tgOut.TargetGroups[0].TargetGroupArn
|
||||
|
||||
modOut, err := svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("deregistration_delay.timeout_seconds"), Value: aws.String("60")},
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
// Assert the exact echoed key/value pairs, not just the length — a regression
|
||||
// that pointed at the wrong source or dropped Value would pass a length check.
|
||||
require.Len(t, modOut.Attributes, 2)
|
||||
modMap := make(map[string]string, len(modOut.Attributes))
|
||||
for _, a := range modOut.Attributes {
|
||||
require.NotNil(t, a.Key)
|
||||
require.NotNil(t, a.Value)
|
||||
modMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "60", modMap["deregistration_delay.timeout_seconds"])
|
||||
assert.Equal(t, "true", modMap["stickiness.enabled"])
|
||||
|
||||
descOut, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "60", attrMap["deregistration_delay.timeout_seconds"])
|
||||
assert.Equal(t, "true", attrMap["stickiness.enabled"])
|
||||
// Unmodified defaults should still be present
|
||||
assert.Equal(t, "lb_cookie", attrMap["stickiness.type"])
|
||||
}
|
||||
|
||||
func TestModifyDescribeLoadBalancerAttributes_RoundTrip(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-attr-rt")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := lbOut.LoadBalancers[0].LoadBalancerArn
|
||||
|
||||
modOut, err := svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("120")},
|
||||
{Key: aws.String("deletion_protection.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
// Assert the exact echoed key/value pairs, not just the length.
|
||||
require.Len(t, modOut.Attributes, 2)
|
||||
modMap := make(map[string]string, len(modOut.Attributes))
|
||||
for _, a := range modOut.Attributes {
|
||||
require.NotNil(t, a.Key)
|
||||
require.NotNil(t, a.Value)
|
||||
modMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "120", modMap["idle_timeout.timeout_seconds"])
|
||||
assert.Equal(t, "true", modMap["deletion_protection.enabled"])
|
||||
|
||||
descOut, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "120", attrMap["idle_timeout.timeout_seconds"])
|
||||
assert.Equal(t, "true", attrMap["deletion_protection.enabled"])
|
||||
// Unmodified defaults should still be present
|
||||
assert.Equal(t, "true", attrMap["routing.http2.enabled"])
|
||||
}
|
||||
|
||||
func TestModifyTargetGroupAttributes_NotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/missing/tg-missing"),
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2TargetGroupNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeTargetGroupAttributes_NotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/missing/tg-missing"),
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2TargetGroupNotFound)
|
||||
}
|
||||
|
||||
func TestModifyLoadBalancerAttributes_NotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/missing/lb-missing"),
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("30")},
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_NotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/missing/lb-missing"),
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
func TestModifyTargetGroupAttributes_MissingArn(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeTargetGroupAttributes_MissingArn(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestModifyLoadBalancerAttributes_MissingArn(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("30")},
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_MissingArn(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestModifyTargetGroupAttributes_WrongAccount(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-attr-wrong-acct")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, "999999999999")
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2TargetGroupNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeTargetGroupAttributes_WrongAccount(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-desc-attr-wrong-acct")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn,
|
||||
}, "999999999999")
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2TargetGroupNotFound)
|
||||
}
|
||||
|
||||
func TestModifyLoadBalancerAttributes_WrongAccount(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-attr-wrong-acct")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("30")},
|
||||
},
|
||||
}, "999999999999")
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeLoadBalancerAttributes_WrongAccount(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-desc-attr-wrong-acct")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
}, "999999999999")
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
// TestModifyTargetGroupAttributes_SkipsInvalidEntries verifies that nil slice
|
||||
// elements, nil Keys, and nil Values are skipped (with a warning) rather than
|
||||
// panicking or being silently dropped. Valid attributes in the same call must
|
||||
// still be applied.
|
||||
func TestModifyTargetGroupAttributes_SkipsInvalidEntries(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-attr-skip")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := tgOut.TargetGroups[0].TargetGroupArn
|
||||
|
||||
modOut, err := svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
nil, // nil element must not panic
|
||||
{Key: nil, Value: aws.String("v")},
|
||||
{Key: aws.String("k"), Value: nil},
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
// Only the one valid attribute should be returned.
|
||||
require.Len(t, modOut.Attributes, 1)
|
||||
assert.Equal(t, "stickiness.enabled", *modOut.Attributes[0].Key)
|
||||
assert.Equal(t, "true", *modOut.Attributes[0].Value)
|
||||
|
||||
// The valid attribute should have been persisted.
|
||||
descOut, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "true", attrMap["stickiness.enabled"])
|
||||
}
|
||||
|
||||
// TestModifyLoadBalancerAttributes_SkipsInvalidEntries mirrors the TG case for
|
||||
// the LB handler: nil elements and nil Key/Value fields must be skipped, not
|
||||
// panic or swallow valid attributes in the same request.
|
||||
func TestModifyLoadBalancerAttributes_SkipsInvalidEntries(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-attr-skip")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := lbOut.LoadBalancers[0].LoadBalancerArn
|
||||
|
||||
modOut, err := svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
nil, // nil element must not panic
|
||||
{Key: nil, Value: aws.String("v")},
|
||||
{Key: aws.String("k"), Value: nil},
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("75")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, modOut.Attributes, 1)
|
||||
assert.Equal(t, "idle_timeout.timeout_seconds", *modOut.Attributes[0].Key)
|
||||
assert.Equal(t, "75", *modOut.Attributes[0].Value)
|
||||
|
||||
descOut, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "75", attrMap["idle_timeout.timeout_seconds"])
|
||||
}
|
||||
|
||||
// TestModifyTargetGroupAttributes_AllInvalidReturnsError guards against the
|
||||
// silent-success case where every submitted attribute trips the nil guard and
|
||||
// the handler returned 200 OK with an empty response body — the caller would
|
||||
// think the write landed when nothing was actually applied.
|
||||
func TestModifyTargetGroupAttributes_AllInvalidReturnsError(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-all-invalid")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
nil,
|
||||
{Key: nil, Value: aws.String("v")},
|
||||
{Key: aws.String("k"), Value: nil},
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
// TestModifyLoadBalancerAttributes_AllInvalidReturnsError mirrors the TG case.
|
||||
func TestModifyLoadBalancerAttributes_AllInvalidReturnsError(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-all-invalid")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
nil,
|
||||
{Key: nil, Value: aws.String("v")},
|
||||
{Key: aws.String("k"), Value: nil},
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
// TestModifyTargetGroupAttributes_SequentialMerge verifies that successive
|
||||
// Modify calls accumulate keys instead of replacing the entire attribute map.
|
||||
// A future refactor that did `tg.Attributes = newMap` would pass every other
|
||||
// test (single-call round-trip covers the happy path) but silently wipe
|
||||
// previous attributes on every subsequent Modify — the most likely real-world
|
||||
// ALB/TG bug.
|
||||
func TestModifyTargetGroupAttributes_SequentialMerge(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-seq-merge")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := tgOut.TargetGroups[0].TargetGroupArn
|
||||
|
||||
// Call 1: set deregistration_delay.timeout_seconds.
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("deregistration_delay.timeout_seconds"), Value: aws.String("45")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call 2: set a different key; must not wipe the first one.
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
descOut, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "45", attrMap["deregistration_delay.timeout_seconds"], "first-call key must survive second Modify")
|
||||
assert.Equal(t, "true", attrMap["stickiness.enabled"], "second-call key must be present")
|
||||
|
||||
// Call 3: overwrite the same key with a new value.
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("false")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
descOut, err = svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap = make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "false", attrMap["stickiness.enabled"], "same-key overwrite must replace the value")
|
||||
assert.Equal(t, "45", attrMap["deregistration_delay.timeout_seconds"], "unrelated key must still survive")
|
||||
}
|
||||
|
||||
// TestModifyLoadBalancerAttributes_SequentialMerge mirrors the TG case.
|
||||
func TestModifyLoadBalancerAttributes_SequentialMerge(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-seq-merge")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := lbOut.LoadBalancers[0].LoadBalancerArn
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("120")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("deletion_protection.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
descOut, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap := make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "120", attrMap["idle_timeout.timeout_seconds"], "first-call key must survive second Modify")
|
||||
assert.Equal(t, "true", attrMap["deletion_protection.enabled"], "second-call key must be present")
|
||||
|
||||
// Same-key overwrite.
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("90")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
descOut, err = svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
attrMap = make(map[string]string)
|
||||
for _, a := range descOut.Attributes {
|
||||
attrMap[*a.Key] = *a.Value
|
||||
}
|
||||
assert.Equal(t, "90", attrMap["idle_timeout.timeout_seconds"], "same-key overwrite must replace the value")
|
||||
assert.Equal(t, "true", attrMap["deletion_protection.enabled"], "unrelated key must still survive")
|
||||
}
|
||||
|
||||
// TestModifyTargetGroupAttributes_NoopSkipsPersist verifies that a
|
||||
// re-submission of the same attribute values does not bump the underlying KV
|
||||
// revision — Terraform's drift check hits Modify on every apply, so the
|
||||
// steady-state path must be a no-op at the storage layer.
|
||||
func TestModifyTargetGroupAttributes_NoopSkipsPersist(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-noop")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
tgArn := tgOut.TargetGroups[0].TargetGroupArn
|
||||
|
||||
// First modify — must hit the store.
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgArn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Capture KV revision after the first write.
|
||||
tg, err := svc.store.GetTargetGroupByArn(*tgArn)
|
||||
require.NoError(t, err)
|
||||
entry, err := svc.store.kv.Get(KeyPrefixTG + tg.TargetGroupID)
|
||||
require.NoError(t, err)
|
||||
revBefore := entry.Revision()
|
||||
|
||||
// Second modify with identical values — must skip the Put.
|
||||
modOut, err := svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgArn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, modOut.Attributes, 1)
|
||||
|
||||
entry, err = svc.store.kv.Get(KeyPrefixTG + tg.TargetGroupID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, revBefore, entry.Revision(), "identical modify must not increment KV revision")
|
||||
|
||||
// Empty attribute list is also a no-op.
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgArn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
entry, err = svc.store.kv.Get(KeyPrefixTG + tg.TargetGroupID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, revBefore, entry.Revision(), "empty modify must not increment KV revision")
|
||||
}
|
||||
|
||||
// TestModifyLoadBalancerAttributes_NoopSkipsPersist mirrors the TG case.
|
||||
func TestModifyLoadBalancerAttributes_NoopSkipsPersist(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-noop")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
lbArn := lbOut.LoadBalancers[0].LoadBalancerArn
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbArn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("75")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
lb, err := svc.store.GetLoadBalancerByArn(*lbArn)
|
||||
require.NoError(t, err)
|
||||
entry, err := svc.store.kv.Get(KeyPrefixLB + lb.LoadBalancerID)
|
||||
require.NoError(t, err)
|
||||
revBefore := entry.Revision()
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbArn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("75")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
entry, err = svc.store.kv.Get(KeyPrefixLB + lb.LoadBalancerID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, revBefore, entry.Revision(), "identical modify must not increment KV revision")
|
||||
}
|
||||
|
||||
// TestModifyTargetGroupAttributes_RejectsUnknownKey guards against silently
|
||||
// persisting typo'd or cross-product attribute keys. AWS rejects unknown keys
|
||||
// with ValidationError; we must match that so Terraform surfaces the typo at
|
||||
// plan time instead of letting it drift into KV forever.
|
||||
func TestModifyTargetGroupAttributes_RejectsUnknownKey(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-unknown-key")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickness.enabled"), Value: aws.String("true")}, // typo of "stickiness"
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorValidationError)
|
||||
|
||||
// The rejected key must not have been persisted.
|
||||
descOut, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
for _, a := range descOut.Attributes {
|
||||
assert.NotEqual(t, "stickness.enabled", *a.Key, "unknown key must not appear in Describe")
|
||||
}
|
||||
}
|
||||
|
||||
// TestModifyLoadBalancerAttributes_RejectsUnknownKey mirrors the TG case.
|
||||
func TestModifyLoadBalancerAttributes_RejectsUnknownKey(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-unknown-key")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
// Valid TG attribute key sent to LB handler — common cross-product mistake.
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorValidationError)
|
||||
}
|
||||
|
||||
// TestDescribeTargetGroupAttributes_SortedOrder verifies that attributes are
|
||||
// returned in a stable, sorted-by-key order. Go map iteration is randomised,
|
||||
// so without explicit sorting Terraform would see spurious plan diffs between
|
||||
// back-to-back describe calls.
|
||||
func TestDescribeTargetGroupAttributes_SortedOrder(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-attr-sorted")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := tgOut.TargetGroups[0].TargetGroupArn
|
||||
|
||||
// Modify a few attributes to ensure the merged map contains both defaults
|
||||
// and overrides.
|
||||
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
Attributes: []*elbv2.TargetGroupAttribute{
|
||||
{Key: aws.String("stickiness.enabled"), Value: aws.String("true")},
|
||||
{Key: aws.String("deregistration_delay.timeout_seconds"), Value: aws.String("45")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call Describe multiple times; each result must be identical and sorted.
|
||||
var firstKeys []string
|
||||
for i := range 5 {
|
||||
descOut, err := svc.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{
|
||||
TargetGroupArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := make([]string, len(descOut.Attributes))
|
||||
for j, a := range descOut.Attributes {
|
||||
keys[j] = *a.Key
|
||||
}
|
||||
assert.True(t, sort.StringsAreSorted(keys), "attributes must be sorted by key, got %v", keys)
|
||||
if i == 0 {
|
||||
firstKeys = keys
|
||||
} else {
|
||||
assert.Equal(t, firstKeys, keys, "attribute order must be stable across calls")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDescribeLoadBalancerAttributes_SortedOrder mirrors the TG test: LB
|
||||
// describe responses must be sorted and stable across repeated calls.
|
||||
func TestDescribeLoadBalancerAttributes_SortedOrder(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-attr-sorted")}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := lbOut.LoadBalancers[0].LoadBalancerArn
|
||||
|
||||
_, err = svc.ModifyLoadBalancerAttributes(&elbv2.ModifyLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
Attributes: []*elbv2.LoadBalancerAttribute{
|
||||
{Key: aws.String("idle_timeout.timeout_seconds"), Value: aws.String("90")},
|
||||
{Key: aws.String("deletion_protection.enabled"), Value: aws.String("true")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var firstKeys []string
|
||||
for i := range 5 {
|
||||
descOut, err := svc.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
|
||||
LoadBalancerArn: arn,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := make([]string, len(descOut.Attributes))
|
||||
for j, a := range descOut.Attributes {
|
||||
keys[j] = *a.Key
|
||||
}
|
||||
assert.True(t, sort.StringsAreSorted(keys), "attributes must be sorted by key, got %v", keys)
|
||||
if i == 0 {
|
||||
firstKeys = keys
|
||||
} else {
|
||||
assert.Equal(t, firstKeys, keys, "attribute order must be stable across calls")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- DescribeTags tests ---
|
||||
|
||||
func TestDescribeTags_LoadBalancer(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("tags-lb"),
|
||||
Tags: []*elbv2.Tag{
|
||||
{Key: aws.String("Env"), Value: aws.String("prod")},
|
||||
{Key: aws.String("App"), Value: aws.String("nginx")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{lbOut.LoadBalancers[0].LoadBalancerArn},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.TagDescriptions, 1)
|
||||
|
||||
td := out.TagDescriptions[0]
|
||||
assert.Equal(t, *lbOut.LoadBalancers[0].LoadBalancerArn, *td.ResourceArn)
|
||||
require.Len(t, td.Tags, 2)
|
||||
// Sorted by key: App, Env
|
||||
assert.Equal(t, "App", *td.Tags[0].Key)
|
||||
assert.Equal(t, "nginx", *td.Tags[0].Value)
|
||||
assert.Equal(t, "Env", *td.Tags[1].Key)
|
||||
assert.Equal(t, "prod", *td.Tags[1].Value)
|
||||
}
|
||||
|
||||
func TestDescribeTags_TargetGroup(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
tgOut, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{
|
||||
Name: aws.String("tags-tg"),
|
||||
Tags: []*elbv2.Tag{
|
||||
{Key: aws.String("Owner"), Value: aws.String("team-a")},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{tgOut.TargetGroups[0].TargetGroupArn},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.TagDescriptions, 1)
|
||||
require.Len(t, out.TagDescriptions[0].Tags, 1)
|
||||
assert.Equal(t, "Owner", *out.TagDescriptions[0].Tags[0].Key)
|
||||
assert.Equal(t, "team-a", *out.TagDescriptions[0].Tags[0].Value)
|
||||
}
|
||||
|
||||
func TestDescribeTags_Listener_NoTags(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, _ := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("tags-lst-lb")}, testAccountID)
|
||||
tgOut, _ := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tags-lst-tg")}, testAccountID)
|
||||
lstOut, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
Protocol: aws.String("HTTP"),
|
||||
Port: aws.Int64(80),
|
||||
DefaultActions: []*elbv2.Action{
|
||||
{Type: aws.String("forward"), TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn},
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Listeners don't store tags yet — must still return a TagDescription
|
||||
// (with an empty Tags slice), not an error. This is the case the
|
||||
// Terraform AWS provider hits during post-create refresh.
|
||||
out, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{lstOut.Listeners[0].ListenerArn},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.TagDescriptions, 1)
|
||||
assert.Equal(t, *lstOut.Listeners[0].ListenerArn, *out.TagDescriptions[0].ResourceArn)
|
||||
assert.Empty(t, out.TagDescriptions[0].Tags)
|
||||
}
|
||||
|
||||
func TestDescribeTags_MultipleArns(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, _ := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("multi-lb"),
|
||||
Tags: []*elbv2.Tag{{Key: aws.String("Name"), Value: aws.String("lb")}},
|
||||
}, testAccountID)
|
||||
tgOut, _ := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{
|
||||
Name: aws.String("multi-tg"),
|
||||
Tags: []*elbv2.Tag{{Key: aws.String("Name"), Value: aws.String("tg")}},
|
||||
}, testAccountID)
|
||||
lstOut, _ := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
Protocol: aws.String("HTTP"),
|
||||
Port: aws.Int64(80),
|
||||
DefaultActions: []*elbv2.Action{
|
||||
{Type: aws.String("forward"), TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn},
|
||||
},
|
||||
}, testAccountID)
|
||||
|
||||
out, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{
|
||||
lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
tgOut.TargetGroups[0].TargetGroupArn,
|
||||
lstOut.Listeners[0].ListenerArn,
|
||||
},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.TagDescriptions, 3)
|
||||
|
||||
// Order matches input order
|
||||
assert.Equal(t, *lbOut.LoadBalancers[0].LoadBalancerArn, *out.TagDescriptions[0].ResourceArn)
|
||||
assert.Equal(t, *tgOut.TargetGroups[0].TargetGroupArn, *out.TagDescriptions[1].ResourceArn)
|
||||
assert.Equal(t, *lstOut.Listeners[0].ListenerArn, *out.TagDescriptions[2].ResourceArn)
|
||||
|
||||
assert.Equal(t, "lb", *out.TagDescriptions[0].Tags[0].Value)
|
||||
assert.Equal(t, "tg", *out.TagDescriptions[1].Tags[0].Value)
|
||||
assert.Empty(t, out.TagDescriptions[2].Tags)
|
||||
}
|
||||
|
||||
func TestDescribeTags_LoadBalancerNotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{
|
||||
aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/missing/lb-deadbeef"),
|
||||
},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeTags_TargetGroupNotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{
|
||||
aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/missing/tg-deadbeef"),
|
||||
},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2TargetGroupNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeTags_ListenerNotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{
|
||||
aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/missing/lb-x/lst-deadbeef"),
|
||||
},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2ListenerNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeTags_CrossAccount(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("xa-lb"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Same ARN, different account => not-found (no existence leak)
|
||||
_, err = svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{lbOut.LoadBalancers[0].LoadBalancerArn},
|
||||
}, "999999999999")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2LoadBalancerNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeTags_InvalidArn(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
cases := []string{
|
||||
"not-an-arn",
|
||||
"arn:aws:s3:::my-bucket", // wrong service
|
||||
"arn:aws:elasticloadbalancing:us-east-1:123:capacityreservation/abc", // unknown ELBv2 resource type
|
||||
"arn:aws:elasticloadbalancing:us-east-1:123:loadbalancer", // missing slash
|
||||
}
|
||||
for _, arn := range cases {
|
||||
_, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{aws.String(arn)},
|
||||
}, testAccountID)
|
||||
require.Error(t, err, "arn=%q should be rejected", arn)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidParameterValue, "arn=%q", arn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeTags_MissingParameter(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
// Empty ResourceArns
|
||||
_, err := svc.DescribeTags(&elbv2.DescribeTagsInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorMissingParameter)
|
||||
|
||||
// Nil entry inside a non-empty list
|
||||
_, err = svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{nil},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeTags_UntaggedLoadBalancer(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbOut, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("untagged-lb"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Bare LB with no Tags input must still return a TagDescription with
|
||||
// the ARN and an empty/nil Tags slice — exercises the exact path the
|
||||
// failing nginx-alb terraform demo hits.
|
||||
out, err := svc.DescribeTags(&elbv2.DescribeTagsInput{
|
||||
ResourceArns: []*string{lbOut.LoadBalancers[0].LoadBalancerArn},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.TagDescriptions, 1)
|
||||
assert.Equal(t, *lbOut.LoadBalancers[0].LoadBalancerArn, *out.TagDescriptions[0].ResourceArn)
|
||||
assert.Empty(t, out.TagDescriptions[0].Tags)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,26 @@ func (s *NATSELBv2Service) DescribeListeners(input *elbv2.DescribeListenersInput
|
||||
return utils.NATSRequest[elbv2.DescribeListenersOutput](s.natsConn, "elbv2.DescribeListeners", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) DescribeTags(input *elbv2.DescribeTagsInput, accountID string) (*elbv2.DescribeTagsOutput, error) {
|
||||
return utils.NATSRequest[elbv2.DescribeTagsOutput](s.natsConn, "elbv2.DescribeTags", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) ModifyTargetGroupAttributes(input *elbv2.ModifyTargetGroupAttributesInput, accountID string) (*elbv2.ModifyTargetGroupAttributesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.ModifyTargetGroupAttributesOutput](s.natsConn, "elbv2.ModifyTargetGroupAttributes", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) DescribeTargetGroupAttributes(input *elbv2.DescribeTargetGroupAttributesInput, accountID string) (*elbv2.DescribeTargetGroupAttributesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.DescribeTargetGroupAttributesOutput](s.natsConn, "elbv2.DescribeTargetGroupAttributes", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) ModifyLoadBalancerAttributes(input *elbv2.ModifyLoadBalancerAttributesInput, accountID string) (*elbv2.ModifyLoadBalancerAttributesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.ModifyLoadBalancerAttributesOutput](s.natsConn, "elbv2.ModifyLoadBalancerAttributes", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) DescribeLoadBalancerAttributes(input *elbv2.DescribeLoadBalancerAttributesInput, accountID string) (*elbv2.DescribeLoadBalancerAttributesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.DescribeLoadBalancerAttributesOutput](s.natsConn, "elbv2.DescribeLoadBalancerAttributes", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) LBAgentHeartbeat(input *LBAgentHeartbeatInput, accountID string) (*LBAgentHeartbeatOutput, error) {
|
||||
return utils.NATSRequest[LBAgentHeartbeatOutput](s.natsConn, "elbv2.LBAgentHeartbeat", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
@@ -89,18 +89,29 @@ func (s *Store) ListLoadBalancers() ([]*LoadBalancerRecord, error) {
|
||||
return listByPrefix[LoadBalancerRecord](s.kv, KeyPrefixLB)
|
||||
}
|
||||
|
||||
// GetLoadBalancerByArn finds a load balancer by its ARN.
|
||||
// GetLoadBalancerByArn finds a load balancer by its ARN via a direct KV lookup
|
||||
// on the short ID embedded in the ARN's final path segment. Falls back to a
|
||||
// linear scan only if the ARN can't be parsed. Terraform hits Describe*Attributes
|
||||
// on every plan/refresh so this must be O(1), not O(n).
|
||||
func (s *Store) GetLoadBalancerByArn(arn string) (*LoadBalancerRecord, error) {
|
||||
lbs, err := s.ListLoadBalancers()
|
||||
// ELBv2 LB ARN: arn:aws:elasticloadbalancing:{region}:{account}:loadbalancer/{app,net}/{name}/{lbID}
|
||||
idx := strings.LastIndex(arn, "/")
|
||||
if idx < 0 || idx == len(arn)-1 {
|
||||
return nil, nil
|
||||
}
|
||||
lbID := arn[idx+1:]
|
||||
lb, err := s.GetLoadBalancer(lbID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, lb := range lbs {
|
||||
if lb.LoadBalancerArn == arn {
|
||||
return lb, nil
|
||||
}
|
||||
// Defence-in-depth: ensure the record actually belongs to this ARN.
|
||||
// Short IDs are random hex, so collisions are effectively impossible, but
|
||||
// a mismatch here would indicate KV corruption and must not be silently
|
||||
// served as a successful lookup.
|
||||
if lb == nil || lb.LoadBalancerArn != arn {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, nil
|
||||
return lb, nil
|
||||
}
|
||||
|
||||
// GetLoadBalancerByName finds a load balancer by name within an account.
|
||||
@@ -159,18 +170,25 @@ func (s *Store) ListTargetGroups() ([]*TargetGroupRecord, error) {
|
||||
return listByPrefix[TargetGroupRecord](s.kv, KeyPrefixTG)
|
||||
}
|
||||
|
||||
// GetTargetGroupByArn finds a target group by its ARN.
|
||||
// GetTargetGroupByArn finds a target group by its ARN via a direct KV lookup
|
||||
// on the short ID embedded in the ARN's final path segment. See
|
||||
// GetLoadBalancerByArn for the motivation — Terraform's per-plan
|
||||
// DescribeTargetGroupAttributes storm must be O(1).
|
||||
func (s *Store) GetTargetGroupByArn(arn string) (*TargetGroupRecord, error) {
|
||||
tgs, err := s.ListTargetGroups()
|
||||
// ELBv2 TG ARN: arn:aws:elasticloadbalancing:{region}:{account}:targetgroup/{name}/{tgID}
|
||||
idx := strings.LastIndex(arn, "/")
|
||||
if idx < 0 || idx == len(arn)-1 {
|
||||
return nil, nil
|
||||
}
|
||||
tgID := arn[idx+1:]
|
||||
tg, err := s.GetTargetGroup(tgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tg := range tgs {
|
||||
if tg.TargetGroupArn == arn {
|
||||
return tg, nil
|
||||
}
|
||||
if tg == nil || tg.TargetGroupArn != arn {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, nil
|
||||
return tg, nil
|
||||
}
|
||||
|
||||
// TargetGroupsForLB returns only the target groups attached to a load balancer
|
||||
|
||||
@@ -61,30 +61,30 @@ const (
|
||||
|
||||
// LoadBalancerRecord represents a stored load balancer (ALB or NLB).
|
||||
type LoadBalancerRecord struct {
|
||||
LoadBalancerArn string `json:"load_balancer_arn"`
|
||||
LoadBalancerID string `json:"load_balancer_id"` // Short ID (hex suffix)
|
||||
DNSName string `json:"dns_name"`
|
||||
Name string `json:"name"`
|
||||
Scheme string `json:"scheme"` // "internet-facing" or "internal"
|
||||
Type string `json:"type"` // Always "application"
|
||||
State string `json:"state"` // "provisioning", "active", "failed"
|
||||
VpcId string `json:"vpc_id"`
|
||||
SecurityGroups []string `json:"security_groups"`
|
||||
Subnets []string `json:"subnets"`
|
||||
AvailZones []AvailZoneInfo `json:"availability_zones"`
|
||||
ENIs []string `json:"enis,omitempty"` // ENI IDs created for this ALB (internal)
|
||||
InstanceID string `json:"instance_id,omitempty"` // ALB VM instance ID (system-managed)
|
||||
VPCIP string `json:"vpc_ip,omitempty"` // VPC private IP of the ALB VM
|
||||
ConfigText string `json:"config_text,omitempty"` // Pre-computed HAProxy config
|
||||
ConfigHash string `json:"config_hash,omitempty"` // SHA256 of ConfigText
|
||||
LastHeartbeat time.Time `json:"last_heartbeat"` // Last agent heartbeat timestamp
|
||||
HostPorts map[int]int `json:"host_ports,omitempty"` // Dev mode: guest port → host port forwarding
|
||||
NodeID string `json:"node_id"` // Daemon node running this ALB
|
||||
IPAddressType string `json:"ip_address_type"` // "ipv4"
|
||||
CrossZoneEnabled bool `json:"cross_zone_enabled"` // Default false for NLB, true for ALB
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
AccountID string `json:"account_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LoadBalancerArn string `json:"load_balancer_arn"`
|
||||
LoadBalancerID string `json:"load_balancer_id"` // Short ID (hex suffix)
|
||||
DNSName string `json:"dns_name"`
|
||||
Name string `json:"name"`
|
||||
Scheme string `json:"scheme"` // "internet-facing" or "internal"
|
||||
Type string `json:"type"` // Always "application"
|
||||
State string `json:"state"` // "provisioning", "active", "failed"
|
||||
VpcId string `json:"vpc_id"`
|
||||
SecurityGroups []string `json:"security_groups"`
|
||||
Subnets []string `json:"subnets"`
|
||||
AvailZones []AvailZoneInfo `json:"availability_zones"`
|
||||
ENIs []string `json:"enis,omitempty"` // ENI IDs created for this ALB (internal)
|
||||
InstanceID string `json:"instance_id,omitempty"` // ALB VM instance ID (system-managed)
|
||||
VPCIP string `json:"vpc_ip,omitempty"` // VPC private IP of the ALB VM
|
||||
ConfigText string `json:"config_text,omitempty"` // Pre-computed HAProxy config
|
||||
ConfigHash string `json:"config_hash,omitempty"` // SHA256 of ConfigText
|
||||
LastHeartbeat time.Time `json:"last_heartbeat"` // Last agent heartbeat timestamp
|
||||
HostPorts map[int]int `json:"host_ports,omitempty"` // Dev mode: guest port → host port forwarding
|
||||
NodeID string `json:"node_id"` // Daemon node running this ALB
|
||||
IPAddressType string `json:"ip_address_type"` // "ipv4"
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
AccountID string `json:"account_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// AvailZoneInfo tracks subnet-to-AZ mapping for a load balancer.
|
||||
@@ -105,6 +105,7 @@ type TargetGroupRecord struct {
|
||||
TargetType string `json:"target_type"` // "instance" for v1
|
||||
HealthCheck HealthCheckConfig `json:"health_check"`
|
||||
Targets []Target `json:"targets"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
AccountID string `json:"account_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -175,3 +176,40 @@ type ListenerAction struct {
|
||||
Type string `json:"type"` // "forward"
|
||||
TargetGroupArn string `json:"target_group_arn"`
|
||||
}
|
||||
|
||||
// DefaultLoadBalancerAttributes returns the default attribute set for a load
|
||||
// balancer of the given type. ALBs default load_balancing.cross_zone.enabled to
|
||||
// "true"; NLBs (and any unknown type) default it to "false". This is the single
|
||||
// source of truth — CreateLoadBalancer no longer seeds attributes, and
|
||||
// DescribeLoadBalancerAttributes derives the per-type defaults from lb.Type on
|
||||
// read.
|
||||
func DefaultLoadBalancerAttributes(lbType string) map[string]string {
|
||||
crossZone := "false"
|
||||
if lbType == LoadBalancerTypeApplication {
|
||||
crossZone = "true"
|
||||
}
|
||||
return map[string]string{
|
||||
"deletion_protection.enabled": "false",
|
||||
"load_balancing.cross_zone.enabled": crossZone,
|
||||
"access_logs.s3.enabled": "false",
|
||||
"access_logs.s3.bucket": "",
|
||||
"access_logs.s3.prefix": "",
|
||||
"idle_timeout.timeout_seconds": "60",
|
||||
"routing.http.drop_invalid_header_fields.enabled": "false",
|
||||
"routing.http2.enabled": "true",
|
||||
"client_keep_alive.seconds": "3600",
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultTargetGroupAttributes returns the default attribute set for target groups.
|
||||
func DefaultTargetGroupAttributes() map[string]string {
|
||||
return map[string]string{
|
||||
"deregistration_delay.timeout_seconds": "300",
|
||||
"stickiness.enabled": "false",
|
||||
"stickiness.type": "lb_cookie",
|
||||
"stickiness.lb_cookie.duration_seconds": "86400",
|
||||
"load_balancing.cross_zone.enabled": "use_load_balancer_configuration",
|
||||
"load_balancing.algorithm.type": "round_robin",
|
||||
"slow_start.duration_seconds": "0",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user