From 9cb3ae6a35baf06eb0fa027512564795dac9fae5 Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 21:16:18 -0500 Subject: [PATCH 1/7] WIP: checkpoint (auto) --- go.mod | 10 +- go.sum | 10 + services/vpclattice/interfaces.go | 390 ++++++++++++++++++++++++++++++ 3 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 services/vpclattice/interfaces.go diff --git a/go.mod b/go.mod index 023789b07..65f8bf843 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.26.4 require ( github.com/alecthomas/kong v1.15.0 github.com/alicebob/miniredis/v2 v2.38.0 - github.com/aws/aws-sdk-go-v2 v1.41.11 + github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.18 github.com/aws/aws-sdk-go-v2/credentials v1.19.17 github.com/aws/aws-sdk-go-v2/service/acm v1.37.21 @@ -61,7 +61,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 github.com/aws/aws-sdk-go-v2/service/support v1.31.23 github.com/aws/aws-sdk-go-v2/service/swf v1.33.14 - github.com/aws/smithy-go v1.27.0 + github.com/aws/smithy-go v1.27.1 github.com/distribution/distribution/v3 v3.1.1 github.com/docker/go-connections v0.7.0 // indirect github.com/eclipse/paho.mqtt.golang v1.5.1 @@ -202,6 +202,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/appstream v1.60.3 ) +require github.com/aws/aws-sdk-go-v2/service/vpclattice v1.22.2 // indirect + require ( github.com/antlr/antlr4 v0.0.0-20181218183524-be58ebffde8e // indirect github.com/aws/aws-dax-go v1.2.15 @@ -222,8 +224,8 @@ require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk v1.34.0 github.com/aws/aws-sdk-go-v2/service/emr v1.57.7 diff --git a/go.sum b/go.sum index d48813d33..8ebccc1ad 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.41.11 h1:9PRf7jyTMEUM6fuNRAJa2mO/skJfrF50rENJwf2LXqw= github.com/aws/aws-sdk-go-v2 v1.41.11/go.mod h1:iiUX27gOXRuYaoeUVXhUpPwjJHzISfPAjjcuhUbLSVs= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9xN9/FJ3WK6Q= @@ -42,8 +44,12 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8Tc github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27/go.mod h1:QV9IVIopJ1dpQUno0f9VYDUwOEjj8u0iEJ4JiZVre3Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 h1:9d8AoASQY9UwrOSmiJ7uSM0MGUPFhnenwSvpaFfat2c= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27/go.mod h1:x0rldpsnUQaQIs4Rh+Vwm9Z/0vI6BxadGtsgJfZFb8s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= github.com/aws/aws-sdk-go-v2/service/accessanalyzer v1.48.0 h1:SG+cxHh/AWWo4TWg/UzjZPEBbDuVEC1i4lfrK+bdMwE= @@ -344,6 +350,8 @@ github.com/aws/aws-sdk-go-v2/service/translate v1.34.2 h1:wOeKaa4YkLvAfueCoVNGb6 github.com/aws/aws-sdk-go-v2/service/translate v1.34.2/go.mod h1:9sMvZzh9f34gFXrARkjL4FRKlBAwZWsrPWWq+oAUg/k= github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.31.4 h1:n0jsEM3jPz0dtujoqreiuUJAEgGHIQcxekY7/kMqHqI= github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.31.4/go.mod h1:cyCWU2gte7GgK6ALIQDeN/Uza9RvxcZCc1BcHQaYeTg= +github.com/aws/aws-sdk-go-v2/service/vpclattice v1.22.2 h1:q9xVCezxBqFAv/sJakd63MZ564MxOY8CDfWuiih9awM= +github.com/aws/aws-sdk-go-v2/service/vpclattice v1.22.2/go.mod h1:iMZnTuSYmfTgpqnzEni1JqeO0Ar/ZItPumptLBMb8/A= github.com/aws/aws-sdk-go-v2/service/waf v1.30.24 h1:oxcv9gZ55iV4WYDPwQ1k2HFro19Ik3vImFyNGM/K2CU= github.com/aws/aws-sdk-go-v2/service/waf v1.30.24/go.mod h1:ER7o7K1CKXhuDN4R/z+/qw+C9yo2CCgF9N9H3QXgZZk= github.com/aws/aws-sdk-go-v2/service/wafv2 v1.71.2 h1:1ZdTCoMSPSrxNCVL65/Qnn0VVMJUtoJKf2Pqnp3hddo= @@ -356,6 +364,8 @@ github.com/aws/aws-sdk-go-v2/service/xray v1.36.20 h1:5V3CHiHP3OHaeB6e1tOC2hw5Fr github.com/aws/aws-sdk-go-v2/service/xray v1.36.20/go.mod h1:sgjg2v2UIv+sDFiig3tbkJ4sGSQrXQ2f+YgWg8TLOu4= github.com/aws/smithy-go v1.27.0 h1:ZoFioDKJxkSIW2otF9T0aPtNlUwhdVCcuZh/rzH9Hus= github.com/aws/smithy-go v1.27.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/services/vpclattice/interfaces.go b/services/vpclattice/interfaces.go new file mode 100644 index 000000000..4fc81ba4f --- /dev/null +++ b/services/vpclattice/interfaces.go @@ -0,0 +1,390 @@ +package vpclattice + +import "time" + +// StorageBackend is the interface for VPC Lattice storage operations. +type StorageBackend interface { + CreateService(name, authType, certificateArn, customDomainName string, tags map[string]string) (*Service, error) + GetService(serviceID string) (*Service, error) + UpdateService(serviceID, authType, certificateArn string) (*Service, error) + DeleteService(serviceID string) (*Service, error) + ListServices(maxResults int32, nextToken string) ([]*ServiceSummary, string, error) + + CreateServiceNetwork(name, authType string, tags map[string]string) (*ServiceNetwork, error) + GetServiceNetwork(snID string) (*ServiceNetwork, error) + UpdateServiceNetwork(snID, authType string) (*ServiceNetwork, error) + DeleteServiceNetwork(snID string) error + ListServiceNetworks(maxResults int32, nextToken string) ([]*ServiceNetworkSummary, string, error) + + CreateServiceNetworkServiceAssociation(serviceNetworkID, serviceID string, tags map[string]string) (*ServiceNetworkServiceAssociation, error) + GetServiceNetworkServiceAssociation(snsaID string) (*ServiceNetworkServiceAssociation, error) + DeleteServiceNetworkServiceAssociation(snsaID string) error + ListServiceNetworkServiceAssociations(serviceNetworkID, serviceID string, maxResults int32, nextToken string) ([]*ServiceNetworkServiceAssociationSummary, string, error) + + CreateServiceNetworkVpcAssociation(serviceNetworkID, vpcID string, securityGroupIDs []string, tags map[string]string) (*ServiceNetworkVpcAssociation, error) + GetServiceNetworkVpcAssociation(snvaID string) (*ServiceNetworkVpcAssociation, error) + UpdateServiceNetworkVpcAssociation(snvaID string, securityGroupIDs []string) (*ServiceNetworkVpcAssociation, error) + DeleteServiceNetworkVpcAssociation(snvaID string) error + ListServiceNetworkVpcAssociations(serviceNetworkID, vpcID string, maxResults int32, nextToken string) ([]*ServiceNetworkVpcAssociationSummary, string, error) + + CreateListener(serviceID, name, protocol string, port int32, defaultAction *RuleAction, tags map[string]string) (*Listener, error) + GetListener(serviceID, listenerID string) (*Listener, error) + UpdateListener(serviceID, listenerID string, defaultAction *RuleAction) (*Listener, error) + DeleteListener(serviceID, listenerID string) error + ListListeners(serviceID string, maxResults int32, nextToken string) ([]*ListenerSummary, string, error) + + CreateRule(serviceID, listenerID, name string, priority int32, action *RuleAction, match *RuleMatch, tags map[string]string) (*Rule, error) + GetRule(serviceID, listenerID, ruleID string) (*Rule, error) + UpdateRule(serviceID, listenerID, ruleID string, priority int32, action *RuleAction, match *RuleMatch) (*Rule, error) + DeleteRule(serviceID, listenerID, ruleID string) error + ListRules(serviceID, listenerID string, maxResults int32, nextToken string) ([]*RuleSummary, string, error) + BatchUpdateRule(serviceID, listenerID string, updates []*RuleUpdate) ([]*RuleUpdateSuccess, []*RuleUpdateFailure, error) + + CreateTargetGroup(name, tgType string, config *TargetGroupConfig, tags map[string]string) (*TargetGroup, error) + GetTargetGroup(tgID string) (*TargetGroup, error) + UpdateTargetGroup(tgID string, healthCheck *HealthCheckConfig) (*TargetGroup, error) + DeleteTargetGroup(tgID string) error + ListTargetGroups(tgType, serviceArn string, maxResults int32, nextToken string) ([]*TargetGroupSummary, string, error) + RegisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) + DeregisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) + ListTargets(tgID string, maxResults int32, nextToken string) ([]*TargetSummary, string, error) + + CreateAccessLogSubscription(resourceID, destinationArn, logType string, tags map[string]string) (*AccessLogSubscription, error) + GetAccessLogSubscription(alsID string) (*AccessLogSubscription, error) + UpdateAccessLogSubscription(alsID, destinationArn string) (*AccessLogSubscription, error) + DeleteAccessLogSubscription(alsID string) error + ListAccessLogSubscriptions(resourceID string, maxResults int32, nextToken string) ([]*AccessLogSubscriptionSummary, string, error) + + PutAuthPolicy(resourceID, policy string) (*AuthPolicy, error) + GetAuthPolicy(resourceID string) (*AuthPolicy, error) + DeleteAuthPolicy(resourceID string) error + + PutResourcePolicy(resourceArn, policy string) error + GetResourcePolicy(resourceArn string) (string, error) + DeleteResourcePolicy(resourceArn string) error + + TagResource(resourceArn string, tags map[string]string) error + UntagResource(resourceArn string, keys []string) error + ListTagsForResource(resourceArn string) (map[string]string, error) + + AccountID() string + Region() string + Reset() + Snapshot() []byte + Restore(data []byte) error +} + +// Service represents a VPC Lattice service. +type Service struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + Name string + AuthType string + CertificateArn string + CustomDomainName string + DNSName string + Status string +} + +// ServiceSummary is a service entry for list responses. +type ServiceSummary struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + Name string + CustomDomainName string + DNSName string + Status string +} + +// ServiceNetwork represents a VPC Lattice service network. +type ServiceNetwork struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + Name string + AuthType string + NumberOfAssociatedServices int64 + NumberOfAssociatedVPCs int64 +} + +// ServiceNetworkSummary is a service network entry for list responses. +type ServiceNetworkSummary struct { + CreatedAt time.Time + ARN string + ID string + Name string + NumberOfAssociatedServices int64 + NumberOfAssociatedVPCs int64 +} + +// ServiceNetworkServiceAssociation is a service-to-service-network association. +type ServiceNetworkServiceAssociation struct { + CreatedAt time.Time + ARN string + ID string + ServiceARN string + ServiceID string + ServiceName string + ServiceNetworkARN string + ServiceNetworkID string + ServiceNetworkName string + Status string + CreatedBy string + CustomDomainName string + DNSName string +} + +// ServiceNetworkServiceAssociationSummary is a summary for list responses. +type ServiceNetworkServiceAssociationSummary struct { + CreatedAt time.Time + ARN string + ID string + ServiceARN string + ServiceID string + ServiceName string + ServiceNetworkARN string + ServiceNetworkID string + ServiceNetworkName string + Status string + CustomDomainName string + DNSName string +} + +// ServiceNetworkVpcAssociation is a VPC-to-service-network association. +type ServiceNetworkVpcAssociation struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + VpcID string + ServiceNetworkARN string + ServiceNetworkID string + ServiceNetworkName string + SecurityGroupIDs []string + Status string + CreatedBy string +} + +// ServiceNetworkVpcAssociationSummary is a summary for list responses. +type ServiceNetworkVpcAssociationSummary struct { + CreatedAt time.Time + ARN string + ID string + VpcID string + ServiceNetworkARN string + ServiceNetworkID string + ServiceNetworkName string + Status string +} + +// Listener represents a VPC Lattice listener. +type Listener struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + ServiceARN string + ServiceID string + Name string + Protocol string + Port int32 + DefaultAction *RuleAction +} + +// ListenerSummary is a listener entry for list responses. +type ListenerSummary struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + Name string + Protocol string + Port int32 +} + +// Rule represents a VPC Lattice listener rule. +type Rule struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + Name string + Priority int32 + Action *RuleAction + Match *RuleMatch + IsDefault bool +} + +// RuleSummary is a rule entry for list responses. +type RuleSummary struct { + ARN string + ID string + Name string + Priority int32 + IsDefault bool +} + +// RuleUpdate is an update spec for BatchUpdateRule. +type RuleUpdate struct { + RuleIdentifier string + Priority int32 + Action *RuleAction + Match *RuleMatch +} + +// RuleUpdateSuccess is a successful rule update result. +type RuleUpdateSuccess struct { + ARN string + ID string + Name string + Priority int32 + IsDefault bool + Action *RuleAction + Match *RuleMatch +} + +// RuleUpdateFailure is a failed rule update result. +type RuleUpdateFailure struct { + RuleIdentifier string + Message string + Code string +} + +// RuleAction is the action for a listener rule. +type RuleAction struct { + FixedResponseStatusCode int32 + ForwardTargetGroups []*WeightedTargetGroup + IsFixedResponse bool +} + +// WeightedTargetGroup is a weighted target group for forward actions. +type WeightedTargetGroup struct { + TargetGroupID string + Weight int32 +} + +// RuleMatch is the match conditions for a listener rule. +type RuleMatch struct { + HTTPMethod string + PathMatchType string + PathMatchValue string + HeaderMatches []*HeaderMatch +} + +// HeaderMatch is an HTTP header match condition. +type HeaderMatch struct { + Name string + MatchType string + MatchValue string + CaseSensitive bool +} + +// TargetGroup represents a VPC Lattice target group. +type TargetGroup struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + Name string + Type string + Status string + Config *TargetGroupConfig + ServiceARNs []string +} + +// TargetGroupSummary is a target group entry for list responses. +type TargetGroupSummary struct { + CreatedAt time.Time + ARN string + ID string + Name string + Type string + Status string + Port int32 + Protocol string + VpcID string + ServiceARNs []string +} + +// TargetGroupConfig is the configuration for a target group. +type TargetGroupConfig struct { + Port int32 + Protocol string + ProtocolVersion string + VpcID string + HealthCheck *HealthCheckConfig + IPAddressType string + LambdaEventStructureVersion string +} + +// HealthCheckConfig is the health check configuration for a target group. +type HealthCheckConfig struct { + Enabled bool + Protocol string + ProtocolVersion string + Path string + Port int32 + HealthyThresholdCount int32 + UnhealthyThresholdCount int32 + HealthCheckIntervalSeconds int32 + HealthCheckTimeoutSeconds int32 + MatcherHTTPCode string +} + +// Target is a target registered to a target group. +type Target struct { + ID string + Port int32 +} + +// TargetSummary is a target entry for list responses. +type TargetSummary struct { + ID string + Port int32 + Status string + ReasonCode string +} + +// TargetFailure is a target registration/deregistration failure. +type TargetFailure struct { + ID string + Port int32 + Code string + Message string +} + +// AccessLogSubscription represents a VPC Lattice access log subscription. +type AccessLogSubscription struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + ResourceARN string + ResourceID string + DestinationARN string + ServiceNetworkLogType string +} + +// AccessLogSubscriptionSummary is a summary for list responses. +type AccessLogSubscriptionSummary struct { + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + ResourceARN string + ResourceID string + DestinationARN string +} + +// AuthPolicy represents an auth policy on a VPC Lattice resource. +type AuthPolicy struct { + Policy string + State string +} + +var _ StorageBackend = (*InMemoryBackend)(nil) From 97714141ba61c1d52bcf4b9ff811a9b452e3779e Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 21:26:20 -0500 Subject: [PATCH 2/7] WIP: checkpoint (auto) --- cli.go | 2 + internal/teststack/teststack.go | 9 + services/vpclattice/backend.go | 2150 ++++++++++++++++++ services/vpclattice/export_test.go | 38 + services/vpclattice/handler.go | 1882 +++++++++++++++ services/vpclattice/handler_test.go | 799 +++++++ services/vpclattice/provider.go | 40 + services/vpclattice/sdk_completeness_test.go | 50 + test/terraform/terraform_test.go | 1 + 9 files changed, 4971 insertions(+) create mode 100644 services/vpclattice/backend.go create mode 100644 services/vpclattice/export_test.go create mode 100644 services/vpclattice/handler.go create mode 100644 services/vpclattice/handler_test.go create mode 100644 services/vpclattice/provider.go create mode 100644 services/vpclattice/sdk_completeness_test.go diff --git a/cli.go b/cli.go index 55d9529ef..adbd19203 100644 --- a/cli.go +++ b/cli.go @@ -187,6 +187,7 @@ import ( transcribebackend "github.com/blackbirdworks/gopherstack/services/transcribe" transferbackend "github.com/blackbirdworks/gopherstack/services/transfer" verifiedpermissionsbackend "github.com/blackbirdworks/gopherstack/services/verifiedpermissions" + vpclatticebackend "github.com/blackbirdworks/gopherstack/services/vpclattice" wafbackend "github.com/blackbirdworks/gopherstack/services/waf" wafv2backend "github.com/blackbirdworks/gopherstack/services/wafv2" workmailbackend "github.com/blackbirdworks/gopherstack/services/workmail" @@ -2739,6 +2740,7 @@ func getMostRecentServiceProviders() []service.Provider { &detectivebackend.Provider{}, &datasyncbackend.Provider{}, &fsxbackend.Provider{}, + &vpclatticebackend.Provider{}, } } diff --git a/internal/teststack/teststack.go b/internal/teststack/teststack.go index 229e508b8..674c4b8d2 100644 --- a/internal/teststack/teststack.go +++ b/internal/teststack/teststack.go @@ -133,6 +133,7 @@ import ( transcribebackend "github.com/blackbirdworks/gopherstack/services/transcribe" transferbackend "github.com/blackbirdworks/gopherstack/services/transfer" verifiedpermissionsbackend "github.com/blackbirdworks/gopherstack/services/verifiedpermissions" + vpclatticebackend "github.com/blackbirdworks/gopherstack/services/vpclattice" wafv2backend "github.com/blackbirdworks/gopherstack/services/wafv2" xraybackend "github.com/blackbirdworks/gopherstack/services/xray" ) @@ -308,6 +309,8 @@ type Stack struct { TransferHandler *transferbackend.Handler // VerifiedPermissionsHandler provides access to the Verified Permissions backend. VerifiedPermissionsHandler *verifiedpermissionsbackend.Handler + // VpcLatticeHandler provides access to the VPC Lattice backend. + VpcLatticeHandler *vpclatticebackend.Handler // Wafv2Handler provides access to the WAFv2 backend. Wafv2Handler *wafv2backend.Handler // XrayHandler provides access to the X-Ray backend. @@ -559,6 +562,7 @@ func registerLatestServices(registry *service.Registry, h handlers) { _ = registry.Register(h.timestreamquery) _ = registry.Register(h.transfer) _ = registry.Register(h.verifiedpermissions) + _ = registry.Register(h.vpclattice) _ = registry.Register(h.wafv2) _ = registry.Register(h.xray) _ = registry.Register(h.s3tables) @@ -680,6 +684,7 @@ type handlers struct { timestreamquery *timestreamquerybackend.Handler transfer *transferbackend.Handler verifiedpermissions *verifiedpermissionsbackend.Handler + vpclattice *vpclatticebackend.Handler wafv2 *wafv2backend.Handler xray *xraybackend.Handler s3tables *s3tablesbackend.Handler @@ -1035,6 +1040,9 @@ func populateTransferHandlers(h *handlers) { h.verifiedpermissions = verifiedpermissionsbackend.NewHandler( verifiedpermissionsbackend.NewInMemoryBackend(config.DefaultAccountID, config.DefaultRegion), ) + h.vpclattice = vpclatticebackend.NewHandler( + vpclatticebackend.NewInMemoryBackend(config.DefaultAccountID, config.DefaultRegion), + ) h.xray = xraybackend.NewHandler(xraybackend.NewInMemoryBackend()) h.s3tables = s3tablesbackend.NewHandler( s3tablesbackend.NewInMemoryBackend(config.DefaultAccountID, config.DefaultRegion), @@ -1319,6 +1327,7 @@ func setNewestStackHandlers(s *Stack, h handlers) { s.TimestreamQueryHandler = h.timestreamquery s.TransferHandler = h.transfer s.VerifiedPermissionsHandler = h.verifiedpermissions + s.VpcLatticeHandler = h.vpclattice s.Wafv2Handler = h.wafv2 s.XrayHandler = h.xray s.S3TablesHandler = h.s3tables diff --git a/services/vpclattice/backend.go b/services/vpclattice/backend.go new file mode 100644 index 000000000..9fc4a6bac --- /dev/null +++ b/services/vpclattice/backend.go @@ -0,0 +1,2150 @@ +package vpclattice + +import ( + "encoding/json" + "fmt" + "maps" + "sort" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/blackbirdworks/gopherstack/pkgs/arn" + "github.com/blackbirdworks/gopherstack/pkgs/awserr" + "github.com/blackbirdworks/gopherstack/pkgs/lockmetrics" + "github.com/blackbirdworks/gopherstack/pkgs/page" +) + +const ( + arnService = "vpc-lattice" + resourceService = "service" + resourceServiceNetwork = "servicenetwork" + resourceServiceNetworkSvcAssoc = "servicenetworkserviceassociation" + resourceServiceNetworkVpcAssoc = "servicenetworkvpcassociation" + resourceListener = "listener" + resourceRule = "rule" + resourceTargetGroup = "targetgroup" + resourceAccessLogSubscription = "accesslogsubscription" + + idPrefixService = "svc-" + idPrefixNetwork = "sn-" + idPrefixSNSA = "snsa-" + idPrefixSNVA = "snva-" + idPrefixListener = "listener-" + idPrefixRule = "rule-" + idPrefixTargetGroup = "tg-" + idPrefixALS = "als-" + + statusActive = "ACTIVE" + statusInactive = "INACTIVE" + statusCreateInProgress = "CREATE_IN_PROGRESS" + statusDeleteInProgress = "DELETE_IN_PROGRESS" + statusDeleted = "DELETED" + statusCreateFailed = "CREATE_FAILED" + + authTypeNone = "NONE" + protocolHTTP = "HTTP" + protocolHTTPS = "HTTPS" + + tgStatusActive = "ACTIVE" + + targetStatusHealthy = "HEALTHY" + + defaultMaxResults = 100 +) + +var ( + // ErrNotFound is returned when a resource does not exist. + ErrNotFound = awserr.New("ResourceNotFoundException", awserr.ErrNotFound) + // ErrAlreadyExists is returned when a resource already exists with the same name. + ErrAlreadyExists = awserr.New("ConflictException", awserr.ErrAlreadyExists) + // ErrInvalidParameter is returned for invalid input. + ErrInvalidParameter = awserr.New("ValidationException", awserr.ErrInvalidParameter) +) + +// storedService holds a service with all fields. +type storedService struct { + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + Name string `json:"name"` + AuthType string `json:"authType"` + CertificateArn string `json:"certificateArn"` + CustomDomainName string `json:"customDomainName"` + DNSName string `json:"dnsName"` + Status string `json:"status"` +} + +func (s *storedService) toService() *Service { + return &Service{ + ARN: s.ARN, + ID: s.ID, + Name: s.Name, + AuthType: s.AuthType, + CertificateArn: s.CertificateArn, + CustomDomainName: s.CustomDomainName, + DNSName: s.DNSName, + Status: s.Status, + CreatedAt: s.CreatedAt, + LastUpdatedAt: s.LastUpdatedAt, + } +} + +func (s *storedService) toSummary() *ServiceSummary { + return &ServiceSummary{ + ARN: s.ARN, + ID: s.ID, + Name: s.Name, + CustomDomainName: s.CustomDomainName, + DNSName: s.DNSName, + Status: s.Status, + CreatedAt: s.CreatedAt, + LastUpdatedAt: s.LastUpdatedAt, + } +} + +// storedServiceNetwork holds a service network. +type storedServiceNetwork struct { + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + Name string `json:"name"` + AuthType string `json:"authType"` + NumberOfAssociatedServices int64 `json:"numberOfAssociatedServices"` + NumberOfAssociatedVPCs int64 `json:"numberOfAssociatedVpcs"` +} + +func (s *storedServiceNetwork) toServiceNetwork() *ServiceNetwork { + return &ServiceNetwork{ + ARN: s.ARN, + ID: s.ID, + Name: s.Name, + AuthType: s.AuthType, + NumberOfAssociatedServices: s.NumberOfAssociatedServices, + NumberOfAssociatedVPCs: s.NumberOfAssociatedVPCs, + CreatedAt: s.CreatedAt, + LastUpdatedAt: s.LastUpdatedAt, + } +} + +func (s *storedServiceNetwork) toSummary() *ServiceNetworkSummary { + return &ServiceNetworkSummary{ + ARN: s.ARN, + ID: s.ID, + Name: s.Name, + NumberOfAssociatedServices: s.NumberOfAssociatedServices, + NumberOfAssociatedVPCs: s.NumberOfAssociatedVPCs, + CreatedAt: s.CreatedAt, + } +} + +// storedSNSA holds a service network service association. +type storedSNSA struct { + CreatedAt time.Time `json:"createdAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + ServiceARN string `json:"serviceArn"` + ServiceID string `json:"serviceId"` + ServiceName string `json:"serviceName"` + ServiceNetworkARN string `json:"serviceNetworkArn"` + ServiceNetworkID string `json:"serviceNetworkId"` + ServiceNetworkName string `json:"serviceNetworkName"` + Status string `json:"status"` + CreatedBy string `json:"createdBy"` + CustomDomainName string `json:"customDomainName"` + DNSName string `json:"dnsName"` +} + +func (s *storedSNSA) toAssociation() *ServiceNetworkServiceAssociation { + return &ServiceNetworkServiceAssociation{ + ARN: s.ARN, + ID: s.ID, + ServiceARN: s.ServiceARN, + ServiceID: s.ServiceID, + ServiceName: s.ServiceName, + ServiceNetworkARN: s.ServiceNetworkARN, + ServiceNetworkID: s.ServiceNetworkID, + ServiceNetworkName: s.ServiceNetworkName, + Status: s.Status, + CreatedBy: s.CreatedBy, + CustomDomainName: s.CustomDomainName, + DNSName: s.DNSName, + CreatedAt: s.CreatedAt, + } +} + +func (s *storedSNSA) toSummary() *ServiceNetworkServiceAssociationSummary { + return &ServiceNetworkServiceAssociationSummary{ + ARN: s.ARN, + ID: s.ID, + ServiceARN: s.ServiceARN, + ServiceID: s.ServiceID, + ServiceName: s.ServiceName, + ServiceNetworkARN: s.ServiceNetworkARN, + ServiceNetworkID: s.ServiceNetworkID, + ServiceNetworkName: s.ServiceNetworkName, + Status: s.Status, + CustomDomainName: s.CustomDomainName, + DNSName: s.DNSName, + CreatedAt: s.CreatedAt, + } +} + +// storedSNVA holds a service network VPC association. +type storedSNVA struct { + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + SecurityGroupIDs []string `json:"securityGroupIds"` + ARN string `json:"arn"` + ID string `json:"id"` + VpcID string `json:"vpcId"` + ServiceNetworkARN string `json:"serviceNetworkArn"` + ServiceNetworkID string `json:"serviceNetworkId"` + ServiceNetworkName string `json:"serviceNetworkName"` + Status string `json:"status"` + CreatedBy string `json:"createdBy"` +} + +func (s *storedSNVA) toAssociation() *ServiceNetworkVpcAssociation { + sgs := make([]string, len(s.SecurityGroupIDs)) + copy(sgs, s.SecurityGroupIDs) + + return &ServiceNetworkVpcAssociation{ + ARN: s.ARN, + ID: s.ID, + VpcID: s.VpcID, + ServiceNetworkARN: s.ServiceNetworkARN, + ServiceNetworkID: s.ServiceNetworkID, + ServiceNetworkName: s.ServiceNetworkName, + SecurityGroupIDs: sgs, + Status: s.Status, + CreatedBy: s.CreatedBy, + CreatedAt: s.CreatedAt, + LastUpdatedAt: s.LastUpdatedAt, + } +} + +func (s *storedSNVA) toSummary() *ServiceNetworkVpcAssociationSummary { + return &ServiceNetworkVpcAssociationSummary{ + ARN: s.ARN, + ID: s.ID, + VpcID: s.VpcID, + ServiceNetworkARN: s.ServiceNetworkARN, + ServiceNetworkID: s.ServiceNetworkID, + ServiceNetworkName: s.ServiceNetworkName, + Status: s.Status, + CreatedAt: s.CreatedAt, + } +} + +// storedListener holds a listener. +type storedListener struct { + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + ServiceARN string `json:"serviceArn"` + ServiceID string `json:"serviceId"` + Name string `json:"name"` + Protocol string `json:"protocol"` + Port int32 `json:"port"` + DefaultAction *RuleAction `json:"defaultAction"` +} + +func (l *storedListener) toListener() *Listener { + return &Listener{ + ARN: l.ARN, + ID: l.ID, + ServiceARN: l.ServiceARN, + ServiceID: l.ServiceID, + Name: l.Name, + Protocol: l.Protocol, + Port: l.Port, + DefaultAction: l.DefaultAction, + CreatedAt: l.CreatedAt, + LastUpdatedAt: l.LastUpdatedAt, + } +} + +func (l *storedListener) toSummary() *ListenerSummary { + return &ListenerSummary{ + ARN: l.ARN, + ID: l.ID, + Name: l.Name, + Protocol: l.Protocol, + Port: l.Port, + CreatedAt: l.CreatedAt, + LastUpdatedAt: l.LastUpdatedAt, + } +} + +// storedRule holds a listener rule. +type storedRule struct { + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + ListenerID string `json:"listenerId"` + ServiceID string `json:"serviceId"` + Name string `json:"name"` + Priority int32 `json:"priority"` + Action *RuleAction `json:"action"` + Match *RuleMatch `json:"match"` + IsDefault bool `json:"isDefault"` +} + +func (r *storedRule) toRule() *Rule { + return &Rule{ + ARN: r.ARN, + ID: r.ID, + Name: r.Name, + Priority: r.Priority, + Action: r.Action, + Match: r.Match, + IsDefault: r.IsDefault, + CreatedAt: r.CreatedAt, + LastUpdatedAt: r.LastUpdatedAt, + } +} + +func (r *storedRule) toSummary() *RuleSummary { + return &RuleSummary{ + ARN: r.ARN, + ID: r.ID, + Name: r.Name, + Priority: r.Priority, + IsDefault: r.IsDefault, + } +} + +// storedTargetGroup holds a target group. +type storedTargetGroup struct { + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ServiceARNs []string `json:"serviceArns"` + ARN string `json:"arn"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` + Config *TargetGroupConfig `json:"config"` +} + +func (tg *storedTargetGroup) toTargetGroup() *TargetGroup { + arns := make([]string, len(tg.ServiceARNs)) + copy(arns, tg.ServiceARNs) + + return &TargetGroup{ + ARN: tg.ARN, + ID: tg.ID, + Name: tg.Name, + Type: tg.Type, + Status: tg.Status, + Config: tg.Config, + ServiceARNs: arns, + CreatedAt: tg.CreatedAt, + LastUpdatedAt: tg.LastUpdatedAt, + } +} + +func (tg *storedTargetGroup) toSummary() *TargetGroupSummary { + s := &TargetGroupSummary{ + ARN: tg.ARN, + ID: tg.ID, + Name: tg.Name, + Type: tg.Type, + Status: tg.Status, + CreatedAt: tg.CreatedAt, + ServiceARNs: make([]string, len(tg.ServiceARNs)), + } + copy(s.ServiceARNs, tg.ServiceARNs) + + if tg.Config != nil { + s.Port = tg.Config.Port + s.Protocol = tg.Config.Protocol + s.VpcID = tg.Config.VpcID + } + + return s +} + +// storedTarget holds a registered target. +type storedTarget struct { + ID string `json:"id"` + Port int32 `json:"port"` + Status string `json:"status"` +} + +// storedALS holds an access log subscription. +type storedALS struct { + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + ResourceARN string `json:"resourceArn"` + ResourceID string `json:"resourceId"` + DestinationARN string `json:"destinationArn"` + ServiceNetworkLogType string `json:"serviceNetworkLogType"` +} + +func (a *storedALS) toALS() *AccessLogSubscription { + return &AccessLogSubscription{ + ARN: a.ARN, + ID: a.ID, + ResourceARN: a.ResourceARN, + ResourceID: a.ResourceID, + DestinationARN: a.DestinationARN, + ServiceNetworkLogType: a.ServiceNetworkLogType, + CreatedAt: a.CreatedAt, + LastUpdatedAt: a.LastUpdatedAt, + } +} + +func (a *storedALS) toSummary() *AccessLogSubscriptionSummary { + return &AccessLogSubscriptionSummary{ + ARN: a.ARN, + ID: a.ID, + ResourceARN: a.ResourceARN, + ResourceID: a.ResourceID, + DestinationARN: a.DestinationARN, + CreatedAt: a.CreatedAt, + LastUpdatedAt: a.LastUpdatedAt, + } +} + +// snapshot is the serializable form of InMemoryBackend. +type snapshot struct { + Services map[string]*storedService `json:"services"` + ServiceNetworks map[string]*storedServiceNetwork `json:"serviceNetworks"` + SNSAs map[string]*storedSNSA `json:"snsas"` + SNVAs map[string]*storedSNVA `json:"snvas"` + Listeners map[string]*storedListener `json:"listeners"` + Rules map[string]*storedRule `json:"rules"` + TargetGroups map[string]*storedTargetGroup `json:"targetGroups"` + Targets map[string][]*storedTarget `json:"targets"` + ALSs map[string]*storedALS `json:"alss"` + AuthPolicies map[string]string `json:"authPolicies"` + ResourcePolicies map[string]string `json:"resourcePolicies"` + Tags map[string]map[string]string `json:"tags"` +} + +// InMemoryBackend is an in-memory implementation of StorageBackend. +type InMemoryBackend struct { + mu *lockmetrics.RWMutex + services map[string]*storedService + servicesByName map[string]string + serviceNetworks map[string]*storedServiceNetwork + networksByName map[string]string + snsas map[string]*storedSNSA + snvas map[string]*storedSNVA + listeners map[string]*storedListener + rules map[string]*storedRule + targetGroups map[string]*storedTargetGroup + tgsByName map[string]string + targets map[string][]*storedTarget + alss map[string]*storedALS + authPolicies map[string]string + resourcePolicies map[string]string + tags map[string]map[string]string + accountID string + region string +} + +// NewInMemoryBackend creates a new InMemoryBackend. +func NewInMemoryBackend(accountID, region string) *InMemoryBackend { + b := &InMemoryBackend{ + mu: lockmetrics.New("vpclattice"), + accountID: accountID, + region: region, + } + b.initMaps() + + return b +} + +func (b *InMemoryBackend) initMaps() { + b.services = make(map[string]*storedService) + b.servicesByName = make(map[string]string) + b.serviceNetworks = make(map[string]*storedServiceNetwork) + b.networksByName = make(map[string]string) + b.snsas = make(map[string]*storedSNSA) + b.snvas = make(map[string]*storedSNVA) + b.listeners = make(map[string]*storedListener) + b.rules = make(map[string]*storedRule) + b.targetGroups = make(map[string]*storedTargetGroup) + b.tgsByName = make(map[string]string) + b.targets = make(map[string][]*storedTarget) + b.alss = make(map[string]*storedALS) + b.authPolicies = make(map[string]string) + b.resourcePolicies = make(map[string]string) + b.tags = make(map[string]map[string]string) +} + +// AccountID returns the configured account ID. +func (b *InMemoryBackend) AccountID() string { return b.accountID } + +// Region returns the configured region. +func (b *InMemoryBackend) Region() string { return b.region } + +// Reset clears all stored data. +func (b *InMemoryBackend) Reset() { + b.mu.Lock("Reset") + defer b.mu.Unlock() + b.initMaps() +} + +// Snapshot serializes the backend state. +func (b *InMemoryBackend) Snapshot() []byte { + b.mu.RLock("Snapshot") + defer b.mu.RUnlock() + + s := snapshot{ + Services: b.services, + ServiceNetworks: b.serviceNetworks, + SNSAs: b.snsas, + SNVAs: b.snvas, + Listeners: b.listeners, + Rules: b.rules, + TargetGroups: b.targetGroups, + Targets: b.targets, + ALSs: b.alss, + AuthPolicies: b.authPolicies, + ResourcePolicies: b.resourcePolicies, + Tags: b.tags, + } + + data, _ := json.Marshal(s) + + return data +} + +// Restore deserializes backend state. +func (b *InMemoryBackend) Restore(data []byte) error { + var s snapshot + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + b.mu.Lock("Restore") + defer b.mu.Unlock() + + b.services = s.Services + b.serviceNetworks = s.ServiceNetworks + b.snsas = s.SNSAs + b.snvas = s.SNVAs + b.listeners = s.Listeners + b.rules = s.Rules + b.targetGroups = s.TargetGroups + b.targets = s.Targets + b.alss = s.ALSs + b.authPolicies = s.AuthPolicies + b.resourcePolicies = s.ResourcePolicies + b.tags = s.Tags + + b.servicesByName = make(map[string]string) + for id, svc := range b.services { + b.servicesByName[svc.Name] = id + } + + b.networksByName = make(map[string]string) + for id, sn := range b.serviceNetworks { + b.networksByName[sn.Name] = id + } + + b.tgsByName = make(map[string]string) + for id, tg := range b.targetGroups { + b.tgsByName[tg.Name] = id + } + + return nil +} + +func (b *InMemoryBackend) buildARN(resourceType, resourceID string) string { + return arn.Build(arnService, b.region, b.accountID, resourceType+"/"+resourceID) +} + +func (b *InMemoryBackend) buildListenerARN(serviceID, listenerID string) string { + return arn.Build(arnService, b.region, b.accountID, + fmt.Sprintf("%s/%s/%s/%s", resourceService, serviceID, resourceListener, listenerID)) +} + +func (b *InMemoryBackend) buildRuleARN(serviceID, listenerID, ruleID string) string { + return arn.Build(arnService, b.region, b.accountID, + fmt.Sprintf("%s/%s/%s/%s/%s/%s", resourceService, serviceID, resourceListener, listenerID, resourceRule, ruleID)) +} + +func newID(prefix string) string { + id := strings.ReplaceAll(uuid.NewString(), "-", "")[:17] + + return prefix + id +} + +func copyTags(src map[string]string) map[string]string { + if src == nil { + return make(map[string]string) + } + + dst := make(map[string]string, len(src)) + maps.Copy(dst, src) + + return dst +} + +// resolveServiceID resolves a service identifier (ID or ARN) to an ID. +func (b *InMemoryBackend) resolveServiceID(identifier string) (string, bool) { + if svc, ok := b.services[identifier]; ok { + return svc.ID, true + } + // check if it's an ARN + for id, svc := range b.services { + if svc.ARN == identifier { + return id, true + } + } + + return "", false +} + +// resolveServiceNetworkID resolves a service network identifier to an ID. +func (b *InMemoryBackend) resolveServiceNetworkID(identifier string) (string, bool) { + if _, ok := b.serviceNetworks[identifier]; ok { + return identifier, true + } + for id, sn := range b.serviceNetworks { + if sn.ARN == identifier || sn.Name == identifier { + return id, true + } + } + + return "", false +} + +// resolveListenerID resolves a listener identifier to (serviceID, listenerID). +func (b *InMemoryBackend) resolveListenerID(serviceID, identifier string) (string, bool) { + key := serviceID + "/" + identifier + if _, ok := b.listeners[key]; ok { + return identifier, true + } + for _, l := range b.listeners { + if l.ServiceID == serviceID && (l.ARN == identifier) { + return l.ID, true + } + } + + return "", false +} + +// resolveRuleID resolves a rule identifier within a listener to a rule ID. +func (b *InMemoryBackend) resolveRuleID(serviceID, listenerID, identifier string) (string, bool) { + key := serviceID + "/" + listenerID + "/" + identifier + if _, ok := b.rules[key]; ok { + return identifier, true + } + for _, r := range b.rules { + if r.ServiceID == serviceID && r.ListenerID == listenerID && r.ARN == identifier { + return r.ID, true + } + } + + return "", false +} + +// resolveTargetGroupID resolves a target group identifier to an ID. +func (b *InMemoryBackend) resolveTargetGroupID(identifier string) (string, bool) { + if _, ok := b.targetGroups[identifier]; ok { + return identifier, true + } + for id, tg := range b.targetGroups { + if tg.ARN == identifier { + return id, true + } + } + + return "", false +} + +// resolveALSID resolves an access log subscription identifier. +func (b *InMemoryBackend) resolveALSID(identifier string) (string, bool) { + if _, ok := b.alss[identifier]; ok { + return identifier, true + } + for id, a := range b.alss { + if a.ARN == identifier { + return id, true + } + } + + return "", false +} + +// resolveSNSAID resolves a SNSA identifier. +func (b *InMemoryBackend) resolveSNSAID(identifier string) (string, bool) { + if _, ok := b.snsas[identifier]; ok { + return identifier, true + } + for id, s := range b.snsas { + if s.ARN == identifier { + return id, true + } + } + + return "", false +} + +// resolveSNVAID resolves a SNVA identifier. +func (b *InMemoryBackend) resolveSNVAID(identifier string) (string, bool) { + if _, ok := b.snvas[identifier]; ok { + return identifier, true + } + for id, s := range b.snvas { + if s.ARN == identifier { + return id, true + } + } + + return "", false +} + +// ------- Service operations ------- + +// CreateService creates a new service. +func (b *InMemoryBackend) CreateService(name, authType, certificateArn, customDomainName string, tags map[string]string) (*Service, error) { + if name == "" { + return nil, ErrInvalidParameter + } + + b.mu.Lock("CreateService") + defer b.mu.Unlock() + + if _, exists := b.servicesByName[name]; exists { + return nil, ErrAlreadyExists + } + + now := time.Now().UTC() + id := newID(idPrefixService) + svcARN := b.buildARN(resourceService, id) + + if authType == "" { + authType = authTypeNone + } + + svc := &storedService{ + ARN: svcARN, + ID: id, + Name: name, + AuthType: authType, + CertificateArn: certificateArn, + CustomDomainName: customDomainName, + DNSName: id + ".vpc-lattice-svcs." + b.region + ".on.aws", + Status: statusActive, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.services[id] = svc + b.servicesByName[name] = id + b.tags[svcARN] = copyTags(tags) + + return svc.toService(), nil +} + +// GetService returns a service by ID or ARN. +func (b *InMemoryBackend) GetService(serviceID string) (*Service, error) { + b.mu.RLock("GetService") + defer b.mu.RUnlock() + + id, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + return b.services[id].toService(), nil +} + +// UpdateService updates a service. +func (b *InMemoryBackend) UpdateService(serviceID, authType, certificateArn string) (*Service, error) { + b.mu.Lock("UpdateService") + defer b.mu.Unlock() + + id, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + svc := b.services[id] + if authType != "" { + svc.AuthType = authType + } + + svc.CertificateArn = certificateArn + svc.LastUpdatedAt = time.Now().UTC() + + return svc.toService(), nil +} + +// DeleteService deletes a service. +func (b *InMemoryBackend) DeleteService(serviceID string) (*Service, error) { + b.mu.Lock("DeleteService") + defer b.mu.Unlock() + + id, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + svc := b.services[id] + out := svc.toService() + out.Status = statusDeleted + + delete(b.servicesByName, svc.Name) + delete(b.services, id) + delete(b.tags, svc.ARN) + + return out, nil +} + +// ListServices returns a paginated list of services. +func (b *InMemoryBackend) ListServices(maxResults int32, nextToken string) ([]*ServiceSummary, string, error) { + b.mu.RLock("ListServices") + defer b.mu.RUnlock() + + all := make([]*ServiceSummary, 0, len(b.services)) + for _, svc := range b.services { + all = append(all, svc.toSummary()) + } + + sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// ------- ServiceNetwork operations ------- + +// CreateServiceNetwork creates a new service network. +func (b *InMemoryBackend) CreateServiceNetwork(name, authType string, tags map[string]string) (*ServiceNetwork, error) { + if name == "" { + return nil, ErrInvalidParameter + } + + b.mu.Lock("CreateServiceNetwork") + defer b.mu.Unlock() + + if _, exists := b.networksByName[name]; exists { + return nil, ErrAlreadyExists + } + + now := time.Now().UTC() + id := newID(idPrefixNetwork) + snARN := b.buildARN(resourceServiceNetwork, id) + + if authType == "" { + authType = authTypeNone + } + + sn := &storedServiceNetwork{ + ARN: snARN, + ID: id, + Name: name, + AuthType: authType, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.serviceNetworks[id] = sn + b.networksByName[name] = id + b.tags[snARN] = copyTags(tags) + + return sn.toServiceNetwork(), nil +} + +// GetServiceNetwork returns a service network. +func (b *InMemoryBackend) GetServiceNetwork(snID string) (*ServiceNetwork, error) { + b.mu.RLock("GetServiceNetwork") + defer b.mu.RUnlock() + + id, ok := b.resolveServiceNetworkID(snID) + if !ok { + return nil, ErrNotFound + } + + sn := b.serviceNetworks[id] + + // compute counts + sn.NumberOfAssociatedServices = b.countSNSAs(id) + sn.NumberOfAssociatedVPCs = b.countSNVAs(id) + + return sn.toServiceNetwork(), nil +} + +func (b *InMemoryBackend) countSNSAs(snID string) int64 { + var count int64 + for _, s := range b.snsas { + if s.ServiceNetworkID == snID { + count++ + } + } + + return count +} + +func (b *InMemoryBackend) countSNVAs(snID string) int64 { + var count int64 + for _, s := range b.snvas { + if s.ServiceNetworkID == snID { + count++ + } + } + + return count +} + +// UpdateServiceNetwork updates a service network. +func (b *InMemoryBackend) UpdateServiceNetwork(snID, authType string) (*ServiceNetwork, error) { + b.mu.Lock("UpdateServiceNetwork") + defer b.mu.Unlock() + + id, ok := b.resolveServiceNetworkID(snID) + if !ok { + return nil, ErrNotFound + } + + sn := b.serviceNetworks[id] + if authType != "" { + sn.AuthType = authType + } + + sn.LastUpdatedAt = time.Now().UTC() + + return sn.toServiceNetwork(), nil +} + +// DeleteServiceNetwork deletes a service network. +func (b *InMemoryBackend) DeleteServiceNetwork(snID string) error { + b.mu.Lock("DeleteServiceNetwork") + defer b.mu.Unlock() + + id, ok := b.resolveServiceNetworkID(snID) + if !ok { + return ErrNotFound + } + + sn := b.serviceNetworks[id] + delete(b.networksByName, sn.Name) + delete(b.serviceNetworks, id) + delete(b.tags, sn.ARN) + + return nil +} + +// ListServiceNetworks returns a paginated list of service networks. +func (b *InMemoryBackend) ListServiceNetworks(maxResults int32, nextToken string) ([]*ServiceNetworkSummary, string, error) { + b.mu.RLock("ListServiceNetworks") + defer b.mu.RUnlock() + + all := make([]*ServiceNetworkSummary, 0, len(b.serviceNetworks)) + for _, sn := range b.serviceNetworks { + all = append(all, sn.toSummary()) + } + + sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// ------- ServiceNetworkServiceAssociation operations ------- + +// CreateServiceNetworkServiceAssociation creates a service-to-network association. +func (b *InMemoryBackend) CreateServiceNetworkServiceAssociation(serviceNetworkID, serviceID string, tags map[string]string) (*ServiceNetworkServiceAssociation, error) { + b.mu.Lock("CreateServiceNetworkServiceAssociation") + defer b.mu.Unlock() + + snID, ok := b.resolveServiceNetworkID(serviceNetworkID) + if !ok { + return nil, ErrNotFound + } + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + // check for existing association + for _, s := range b.snsas { + if s.ServiceNetworkID == snID && s.ServiceID == svcID { + return nil, ErrAlreadyExists + } + } + + now := time.Now().UTC() + id := newID(idPrefixSNSA) + assocARN := b.buildARN(resourceServiceNetworkSvcAssoc, id) + + sn := b.serviceNetworks[snID] + svc := b.services[svcID] + + snsa := &storedSNSA{ + ARN: assocARN, + ID: id, + ServiceARN: svc.ARN, + ServiceID: svcID, + ServiceName: svc.Name, + ServiceNetworkARN: sn.ARN, + ServiceNetworkID: snID, + ServiceNetworkName: sn.Name, + Status: statusActive, + CreatedBy: b.accountID, + CustomDomainName: svc.CustomDomainName, + DNSName: svc.DNSName, + Tags: copyTags(tags), + CreatedAt: now, + } + + b.snsas[id] = snsa + b.tags[assocARN] = copyTags(tags) + + return snsa.toAssociation(), nil +} + +// GetServiceNetworkServiceAssociation returns a SNSA by ID or ARN. +func (b *InMemoryBackend) GetServiceNetworkServiceAssociation(snsaID string) (*ServiceNetworkServiceAssociation, error) { + b.mu.RLock("GetServiceNetworkServiceAssociation") + defer b.mu.RUnlock() + + id, ok := b.resolveSNSAID(snsaID) + if !ok { + return nil, ErrNotFound + } + + return b.snsas[id].toAssociation(), nil +} + +// DeleteServiceNetworkServiceAssociation deletes a SNSA. +func (b *InMemoryBackend) DeleteServiceNetworkServiceAssociation(snsaID string) error { + b.mu.Lock("DeleteServiceNetworkServiceAssociation") + defer b.mu.Unlock() + + id, ok := b.resolveSNSAID(snsaID) + if !ok { + return ErrNotFound + } + + s := b.snsas[id] + delete(b.snsas, id) + delete(b.tags, s.ARN) + + return nil +} + +// ListServiceNetworkServiceAssociations lists SNSAs with optional filters. +func (b *InMemoryBackend) ListServiceNetworkServiceAssociations(serviceNetworkID, serviceID string, maxResults int32, nextToken string) ([]*ServiceNetworkServiceAssociationSummary, string, error) { + b.mu.RLock("ListServiceNetworkServiceAssociations") + defer b.mu.RUnlock() + + all := make([]*ServiceNetworkServiceAssociationSummary, 0) + + for _, s := range b.snsas { + if serviceNetworkID != "" && s.ServiceNetworkID != serviceNetworkID && s.ServiceNetworkARN != serviceNetworkID { + continue + } + + if serviceID != "" && s.ServiceID != serviceID && s.ServiceARN != serviceID { + continue + } + + all = append(all, s.toSummary()) + } + + sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// ------- ServiceNetworkVpcAssociation operations ------- + +// CreateServiceNetworkVpcAssociation creates a VPC-to-network association. +func (b *InMemoryBackend) CreateServiceNetworkVpcAssociation(serviceNetworkID, vpcID string, securityGroupIDs []string, tags map[string]string) (*ServiceNetworkVpcAssociation, error) { + if vpcID == "" { + return nil, ErrInvalidParameter + } + + b.mu.Lock("CreateServiceNetworkVpcAssociation") + defer b.mu.Unlock() + + snID, ok := b.resolveServiceNetworkID(serviceNetworkID) + if !ok { + return nil, ErrNotFound + } + + // check for existing + for _, s := range b.snvas { + if s.ServiceNetworkID == snID && s.VpcID == vpcID { + return nil, ErrAlreadyExists + } + } + + now := time.Now().UTC() + id := newID(idPrefixSNVA) + assocARN := b.buildARN(resourceServiceNetworkVpcAssoc, id) + + sn := b.serviceNetworks[snID] + sgs := make([]string, len(securityGroupIDs)) + copy(sgs, securityGroupIDs) + + snva := &storedSNVA{ + ARN: assocARN, + ID: id, + VpcID: vpcID, + ServiceNetworkARN: sn.ARN, + ServiceNetworkID: snID, + ServiceNetworkName: sn.Name, + SecurityGroupIDs: sgs, + Status: statusActive, + CreatedBy: b.accountID, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.snvas[id] = snva + b.tags[assocARN] = copyTags(tags) + + return snva.toAssociation(), nil +} + +// GetServiceNetworkVpcAssociation returns a SNVA. +func (b *InMemoryBackend) GetServiceNetworkVpcAssociation(snvaID string) (*ServiceNetworkVpcAssociation, error) { + b.mu.RLock("GetServiceNetworkVpcAssociation") + defer b.mu.RUnlock() + + id, ok := b.resolveSNVAID(snvaID) + if !ok { + return nil, ErrNotFound + } + + return b.snvas[id].toAssociation(), nil +} + +// UpdateServiceNetworkVpcAssociation updates security groups on a SNVA. +func (b *InMemoryBackend) UpdateServiceNetworkVpcAssociation(snvaID string, securityGroupIDs []string) (*ServiceNetworkVpcAssociation, error) { + b.mu.Lock("UpdateServiceNetworkVpcAssociation") + defer b.mu.Unlock() + + id, ok := b.resolveSNVAID(snvaID) + if !ok { + return nil, ErrNotFound + } + + snva := b.snvas[id] + sgs := make([]string, len(securityGroupIDs)) + copy(sgs, securityGroupIDs) + snva.SecurityGroupIDs = sgs + snva.LastUpdatedAt = time.Now().UTC() + + return snva.toAssociation(), nil +} + +// DeleteServiceNetworkVpcAssociation deletes a SNVA. +func (b *InMemoryBackend) DeleteServiceNetworkVpcAssociation(snvaID string) error { + b.mu.Lock("DeleteServiceNetworkVpcAssociation") + defer b.mu.Unlock() + + id, ok := b.resolveSNVAID(snvaID) + if !ok { + return ErrNotFound + } + + s := b.snvas[id] + delete(b.snvas, id) + delete(b.tags, s.ARN) + + return nil +} + +// ListServiceNetworkVpcAssociations lists SNVAs with optional filters. +func (b *InMemoryBackend) ListServiceNetworkVpcAssociations(serviceNetworkID, vpcID string, maxResults int32, nextToken string) ([]*ServiceNetworkVpcAssociationSummary, string, error) { + b.mu.RLock("ListServiceNetworkVpcAssociations") + defer b.mu.RUnlock() + + all := make([]*ServiceNetworkVpcAssociationSummary, 0) + + for _, s := range b.snvas { + if serviceNetworkID != "" && s.ServiceNetworkID != serviceNetworkID && s.ServiceNetworkARN != serviceNetworkID { + continue + } + + if vpcID != "" && s.VpcID != vpcID { + continue + } + + all = append(all, s.toSummary()) + } + + sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// ------- Listener operations ------- + +// CreateListener creates a listener on a service. +func (b *InMemoryBackend) CreateListener(serviceID, name, protocol string, port int32, defaultAction *RuleAction, tags map[string]string) (*Listener, error) { + if name == "" || protocol == "" { + return nil, ErrInvalidParameter + } + + b.mu.Lock("CreateListener") + defer b.mu.Unlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + // check duplicate name within service + for _, l := range b.listeners { + if l.ServiceID == svcID && l.Name == name { + return nil, ErrAlreadyExists + } + } + + if port == 0 { + if protocol == protocolHTTPS { + port = 443 + } else { + port = 80 + } + } + + now := time.Now().UTC() + id := newID(idPrefixListener) + svc := b.services[svcID] + listenerARN := b.buildListenerARN(svcID, id) + key := svcID + "/" + id + + l := &storedListener{ + ARN: listenerARN, + ID: id, + ServiceARN: svc.ARN, + ServiceID: svcID, + Name: name, + Protocol: protocol, + Port: port, + DefaultAction: defaultAction, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.listeners[key] = l + b.tags[listenerARN] = copyTags(tags) + + // create the default rule + b.createDefaultRule(svcID, id, listenerARN, defaultAction, now) + + return l.toListener(), nil +} + +func (b *InMemoryBackend) createDefaultRule(serviceID, listenerID, listenerARN string, action *RuleAction, now time.Time) { + id := newID(idPrefixRule) + ruleARN := b.buildRuleARN(serviceID, listenerID, id) + key := serviceID + "/" + listenerID + "/" + id + + r := &storedRule{ + ARN: ruleARN, + ID: id, + ServiceID: serviceID, + ListenerID: listenerID, + Name: "default", + Priority: 100, + Action: action, + IsDefault: true, + Tags: make(map[string]string), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.rules[key] = r +} + +// GetListener returns a listener. +func (b *InMemoryBackend) GetListener(serviceID, listenerID string) (*Listener, error) { + b.mu.RLock("GetListener") + defer b.mu.RUnlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return nil, ErrNotFound + } + + key := svcID + "/" + lID + + return b.listeners[key].toListener(), nil +} + +// UpdateListener updates the default action of a listener. +func (b *InMemoryBackend) UpdateListener(serviceID, listenerID string, defaultAction *RuleAction) (*Listener, error) { + b.mu.Lock("UpdateListener") + defer b.mu.Unlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return nil, ErrNotFound + } + + key := svcID + "/" + lID + l := b.listeners[key] + + if defaultAction != nil { + l.DefaultAction = defaultAction + } + + l.LastUpdatedAt = time.Now().UTC() + + return l.toListener(), nil +} + +// DeleteListener deletes a listener and its rules. +func (b *InMemoryBackend) DeleteListener(serviceID, listenerID string) error { + b.mu.Lock("DeleteListener") + defer b.mu.Unlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return ErrNotFound + } + + key := svcID + "/" + lID + l := b.listeners[key] + delete(b.listeners, key) + delete(b.tags, l.ARN) + + // delete all rules for this listener + prefix := svcID + "/" + lID + "/" + for k, r := range b.rules { + if strings.HasPrefix(k, prefix) { + delete(b.rules, k) + delete(b.tags, r.ARN) + } + } + + return nil +} + +// ListListeners lists listeners for a service. +func (b *InMemoryBackend) ListListeners(serviceID string, maxResults int32, nextToken string) ([]*ListenerSummary, string, error) { + b.mu.RLock("ListListeners") + defer b.mu.RUnlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, "", ErrNotFound + } + + all := make([]*ListenerSummary, 0) + + for _, l := range b.listeners { + if l.ServiceID == svcID { + all = append(all, l.toSummary()) + } + } + + sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// ------- Rule operations ------- + +// CreateRule creates a listener rule. +func (b *InMemoryBackend) CreateRule(serviceID, listenerID, name string, priority int32, action *RuleAction, match *RuleMatch, tags map[string]string) (*Rule, error) { + if name == "" { + return nil, ErrInvalidParameter + } + + b.mu.Lock("CreateRule") + defer b.mu.Unlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return nil, ErrNotFound + } + + // check duplicate name within listener + for _, r := range b.rules { + if r.ServiceID == svcID && r.ListenerID == lID && r.Name == name { + return nil, ErrAlreadyExists + } + } + + now := time.Now().UTC() + id := newID(idPrefixRule) + ruleARN := b.buildRuleARN(svcID, lID, id) + key := svcID + "/" + lID + "/" + id + + r := &storedRule{ + ARN: ruleARN, + ID: id, + ServiceID: svcID, + ListenerID: lID, + Name: name, + Priority: priority, + Action: action, + Match: match, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.rules[key] = r + b.tags[ruleARN] = copyTags(tags) + + return r.toRule(), nil +} + +// GetRule returns a rule. +func (b *InMemoryBackend) GetRule(serviceID, listenerID, ruleID string) (*Rule, error) { + b.mu.RLock("GetRule") + defer b.mu.RUnlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return nil, ErrNotFound + } + + rID, ok := b.resolveRuleID(svcID, lID, ruleID) + if !ok { + return nil, ErrNotFound + } + + key := svcID + "/" + lID + "/" + rID + + return b.rules[key].toRule(), nil +} + +// UpdateRule updates a rule. +func (b *InMemoryBackend) UpdateRule(serviceID, listenerID, ruleID string, priority int32, action *RuleAction, match *RuleMatch) (*Rule, error) { + b.mu.Lock("UpdateRule") + defer b.mu.Unlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return nil, ErrNotFound + } + + rID, ok := b.resolveRuleID(svcID, lID, ruleID) + if !ok { + return nil, ErrNotFound + } + + key := svcID + "/" + lID + "/" + rID + r := b.rules[key] + + if priority != 0 { + r.Priority = priority + } + + if action != nil { + r.Action = action + } + + if match != nil { + r.Match = match + } + + r.LastUpdatedAt = time.Now().UTC() + + return r.toRule(), nil +} + +// DeleteRule deletes a rule. +func (b *InMemoryBackend) DeleteRule(serviceID, listenerID, ruleID string) error { + b.mu.Lock("DeleteRule") + defer b.mu.Unlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return ErrNotFound + } + + rID, ok := b.resolveRuleID(svcID, lID, ruleID) + if !ok { + return ErrNotFound + } + + key := svcID + "/" + lID + "/" + rID + r := b.rules[key] + + if r.IsDefault { + return ErrInvalidParameter + } + + delete(b.rules, key) + delete(b.tags, r.ARN) + + return nil +} + +// ListRules lists rules for a listener. +func (b *InMemoryBackend) ListRules(serviceID, listenerID string, maxResults int32, nextToken string) ([]*RuleSummary, string, error) { + b.mu.RLock("ListRules") + defer b.mu.RUnlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, "", ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return nil, "", ErrNotFound + } + + all := make([]*RuleSummary, 0) + + for _, r := range b.rules { + if r.ServiceID == svcID && r.ListenerID == lID { + all = append(all, r.toSummary()) + } + } + + sort.Slice(all, func(i, j int) bool { return all[i].Priority < all[j].Priority }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// BatchUpdateRule updates multiple rules atomically. +func (b *InMemoryBackend) BatchUpdateRule(serviceID, listenerID string, updates []*RuleUpdate) ([]*RuleUpdateSuccess, []*RuleUpdateFailure, error) { + b.mu.Lock("BatchUpdateRule") + defer b.mu.Unlock() + + svcID, ok := b.resolveServiceID(serviceID) + if !ok { + return nil, nil, ErrNotFound + } + + lID, ok := b.resolveListenerID(svcID, listenerID) + if !ok { + return nil, nil, ErrNotFound + } + + successes := make([]*RuleUpdateSuccess, 0, len(updates)) + failures := make([]*RuleUpdateFailure, 0) + now := time.Now().UTC() + + for _, u := range updates { + rID, ok := b.resolveRuleID(svcID, lID, u.RuleIdentifier) + if !ok { + failures = append(failures, &RuleUpdateFailure{ + RuleIdentifier: u.RuleIdentifier, + Code: "NOT_FOUND", + Message: "Rule not found", + }) + + continue + } + + key := svcID + "/" + lID + "/" + rID + r := b.rules[key] + + if u.Priority != 0 { + r.Priority = u.Priority + } + + if u.Action != nil { + r.Action = u.Action + } + + if u.Match != nil { + r.Match = u.Match + } + + r.LastUpdatedAt = now + successes = append(successes, &RuleUpdateSuccess{ + ARN: r.ARN, + ID: r.ID, + Name: r.Name, + Priority: r.Priority, + IsDefault: r.IsDefault, + Action: r.Action, + Match: r.Match, + }) + } + + return successes, failures, nil +} + +// ------- TargetGroup operations ------- + +// CreateTargetGroup creates a target group. +func (b *InMemoryBackend) CreateTargetGroup(name, tgType string, config *TargetGroupConfig, tags map[string]string) (*TargetGroup, error) { + if name == "" { + return nil, ErrInvalidParameter + } + + b.mu.Lock("CreateTargetGroup") + defer b.mu.Unlock() + + if _, exists := b.tgsByName[name]; exists { + return nil, ErrAlreadyExists + } + + now := time.Now().UTC() + id := newID(idPrefixTargetGroup) + tgARN := b.buildARN(resourceTargetGroup, id) + + tg := &storedTargetGroup{ + ARN: tgARN, + ID: id, + Name: name, + Type: tgType, + Status: tgStatusActive, + Config: config, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.targetGroups[id] = tg + b.tgsByName[name] = id + b.targets[id] = make([]*storedTarget, 0) + b.tags[tgARN] = copyTags(tags) + + return tg.toTargetGroup(), nil +} + +// GetTargetGroup returns a target group. +func (b *InMemoryBackend) GetTargetGroup(tgID string) (*TargetGroup, error) { + b.mu.RLock("GetTargetGroup") + defer b.mu.RUnlock() + + id, ok := b.resolveTargetGroupID(tgID) + if !ok { + return nil, ErrNotFound + } + + return b.targetGroups[id].toTargetGroup(), nil +} + +// UpdateTargetGroup updates a target group's health check config. +func (b *InMemoryBackend) UpdateTargetGroup(tgID string, healthCheck *HealthCheckConfig) (*TargetGroup, error) { + b.mu.Lock("UpdateTargetGroup") + defer b.mu.Unlock() + + id, ok := b.resolveTargetGroupID(tgID) + if !ok { + return nil, ErrNotFound + } + + tg := b.targetGroups[id] + if tg.Config == nil { + tg.Config = &TargetGroupConfig{} + } + + if healthCheck != nil { + tg.Config.HealthCheck = healthCheck + } + + tg.LastUpdatedAt = time.Now().UTC() + + return tg.toTargetGroup(), nil +} + +// DeleteTargetGroup deletes a target group. +func (b *InMemoryBackend) DeleteTargetGroup(tgID string) error { + b.mu.Lock("DeleteTargetGroup") + defer b.mu.Unlock() + + id, ok := b.resolveTargetGroupID(tgID) + if !ok { + return ErrNotFound + } + + tg := b.targetGroups[id] + delete(b.targetGroups, id) + delete(b.tgsByName, tg.Name) + delete(b.targets, id) + delete(b.tags, tg.ARN) + + return nil +} + +// ListTargetGroups lists target groups with optional filters. +func (b *InMemoryBackend) ListTargetGroups(tgType, serviceArn string, maxResults int32, nextToken string) ([]*TargetGroupSummary, string, error) { + b.mu.RLock("ListTargetGroups") + defer b.mu.RUnlock() + + all := make([]*TargetGroupSummary, 0, len(b.targetGroups)) + + for _, tg := range b.targetGroups { + if tgType != "" && tg.Type != tgType { + continue + } + + if serviceArn != "" { + found := false + for _, a := range tg.ServiceARNs { + if a == serviceArn { + found = true + + break + } + } + + if !found { + continue + } + } + + all = append(all, tg.toSummary()) + } + + sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// RegisterTargets registers targets to a target group. +func (b *InMemoryBackend) RegisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) { + b.mu.Lock("RegisterTargets") + defer b.mu.Unlock() + + id, ok := b.resolveTargetGroupID(tgID) + if !ok { + return nil, ErrNotFound + } + + failures := make([]*TargetFailure, 0) + existing := b.targets[id] + + for _, t := range targets { + // check for duplicate + dup := false + for _, e := range existing { + if e.ID == t.ID && e.Port == t.Port { + dup = true + + break + } + } + + if dup { + failures = append(failures, &TargetFailure{ + ID: t.ID, + Port: t.Port, + Code: "TARGET_ALREADY_REGISTERED", + Message: "Target already registered", + }) + + continue + } + + existing = append(existing, &storedTarget{ + ID: t.ID, + Port: t.Port, + Status: targetStatusHealthy, + }) + } + + b.targets[id] = existing + + return failures, nil +} + +// DeregisterTargets deregisters targets from a target group. +func (b *InMemoryBackend) DeregisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) { + b.mu.Lock("DeregisterTargets") + defer b.mu.Unlock() + + id, ok := b.resolveTargetGroupID(tgID) + if !ok { + return nil, ErrNotFound + } + + failures := make([]*TargetFailure, 0) + existing := b.targets[id] + remaining := make([]*storedTarget, 0, len(existing)) + + for _, t := range targets { + found := false + for _, e := range existing { + if e.ID == t.ID && (t.Port == 0 || e.Port == t.Port) { + found = true + + continue + } + + if e.ID != t.ID || (t.Port != 0 && e.Port != t.Port) { + remaining = append(remaining, e) + } + } + + if !found { + failures = append(failures, &TargetFailure{ + ID: t.ID, + Port: t.Port, + Code: "TARGET_NOT_FOUND", + Message: "Target not registered", + }) + } + } + + // rebuild remaining with non-deregistered targets + remaining = make([]*storedTarget, 0) + + for _, e := range existing { + remove := false + + for _, t := range targets { + if e.ID == t.ID && (t.Port == 0 || e.Port == t.Port) { + remove = true + + break + } + } + + if !remove { + remaining = append(remaining, e) + } + } + + b.targets[id] = remaining + + return failures, nil +} + +// ListTargets lists registered targets for a target group. +func (b *InMemoryBackend) ListTargets(tgID string, maxResults int32, nextToken string) ([]*TargetSummary, string, error) { + b.mu.RLock("ListTargets") + defer b.mu.RUnlock() + + id, ok := b.resolveTargetGroupID(tgID) + if !ok { + return nil, "", ErrNotFound + } + + targets := b.targets[id] + all := make([]*TargetSummary, 0, len(targets)) + + for _, t := range targets { + all = append(all, &TargetSummary{ + ID: t.ID, + Port: t.Port, + Status: t.Status, + }) + } + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// ------- AccessLogSubscription operations ------- + +// CreateAccessLogSubscription creates an access log subscription. +func (b *InMemoryBackend) CreateAccessLogSubscription(resourceID, destinationArn, logType string, tags map[string]string) (*AccessLogSubscription, error) { + if destinationArn == "" { + return nil, ErrInvalidParameter + } + + b.mu.Lock("CreateAccessLogSubscription") + defer b.mu.Unlock() + + // resolve resource ID (service or service network) + resourceARN := b.resolveResourceARN(resourceID) + + now := time.Now().UTC() + id := newID(idPrefixALS) + alsARN := b.buildARN(resourceAccessLogSubscription, id) + + als := &storedALS{ + ARN: alsARN, + ID: id, + ResourceARN: resourceARN, + ResourceID: resourceID, + DestinationARN: destinationArn, + ServiceNetworkLogType: logType, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, + } + + b.alss[id] = als + b.tags[alsARN] = copyTags(tags) + + return als.toALS(), nil +} + +func (b *InMemoryBackend) resolveResourceARN(resourceID string) string { + if svc, ok := b.services[resourceID]; ok { + return svc.ARN + } + + for _, svc := range b.services { + if svc.ARN == resourceID { + return svc.ARN + } + } + + if sn, ok := b.serviceNetworks[resourceID]; ok { + return sn.ARN + } + + for _, sn := range b.serviceNetworks { + if sn.ARN == resourceID { + return sn.ARN + } + } + + return resourceID +} + +// GetAccessLogSubscription returns an access log subscription. +func (b *InMemoryBackend) GetAccessLogSubscription(alsID string) (*AccessLogSubscription, error) { + b.mu.RLock("GetAccessLogSubscription") + defer b.mu.RUnlock() + + id, ok := b.resolveALSID(alsID) + if !ok { + return nil, ErrNotFound + } + + return b.alss[id].toALS(), nil +} + +// UpdateAccessLogSubscription updates the destination ARN. +func (b *InMemoryBackend) UpdateAccessLogSubscription(alsID, destinationArn string) (*AccessLogSubscription, error) { + b.mu.Lock("UpdateAccessLogSubscription") + defer b.mu.Unlock() + + id, ok := b.resolveALSID(alsID) + if !ok { + return nil, ErrNotFound + } + + als := b.alss[id] + als.DestinationARN = destinationArn + als.LastUpdatedAt = time.Now().UTC() + + return als.toALS(), nil +} + +// DeleteAccessLogSubscription deletes an access log subscription. +func (b *InMemoryBackend) DeleteAccessLogSubscription(alsID string) error { + b.mu.Lock("DeleteAccessLogSubscription") + defer b.mu.Unlock() + + id, ok := b.resolveALSID(alsID) + if !ok { + return ErrNotFound + } + + a := b.alss[id] + delete(b.alss, id) + delete(b.tags, a.ARN) + + return nil +} + +// ListAccessLogSubscriptions lists access log subscriptions for a resource. +func (b *InMemoryBackend) ListAccessLogSubscriptions(resourceID string, maxResults int32, nextToken string) ([]*AccessLogSubscriptionSummary, string, error) { + b.mu.RLock("ListAccessLogSubscriptions") + defer b.mu.RUnlock() + + all := make([]*AccessLogSubscriptionSummary, 0) + + for _, a := range b.alss { + if resourceID != "" && a.ResourceID != resourceID && a.ResourceARN != resourceID { + continue + } + + all = append(all, a.toSummary()) + } + + sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID }) + + p := page.New(all, nextToken, int(maxResults), defaultMaxResults) + + return p.Data, p.Next, nil +} + +// ------- Auth/Resource Policy operations ------- + +// PutAuthPolicy sets an auth policy on a resource. +func (b *InMemoryBackend) PutAuthPolicy(resourceID, policy string) (*AuthPolicy, error) { + b.mu.Lock("PutAuthPolicy") + defer b.mu.Unlock() + + b.authPolicies[resourceID] = policy + + return &AuthPolicy{Policy: policy, State: "Active"}, nil +} + +// GetAuthPolicy returns the auth policy for a resource. +func (b *InMemoryBackend) GetAuthPolicy(resourceID string) (*AuthPolicy, error) { + b.mu.RLock("GetAuthPolicy") + defer b.mu.RUnlock() + + policy, ok := b.authPolicies[resourceID] + if !ok { + return &AuthPolicy{Policy: "", State: "Active"}, nil + } + + return &AuthPolicy{Policy: policy, State: "Active"}, nil +} + +// DeleteAuthPolicy deletes the auth policy for a resource. +func (b *InMemoryBackend) DeleteAuthPolicy(resourceID string) error { + b.mu.Lock("DeleteAuthPolicy") + defer b.mu.Unlock() + + delete(b.authPolicies, resourceID) + + return nil +} + +// PutResourcePolicy sets a resource policy. +func (b *InMemoryBackend) PutResourcePolicy(resourceArn, policy string) error { + b.mu.Lock("PutResourcePolicy") + defer b.mu.Unlock() + + b.resourcePolicies[resourceArn] = policy + + return nil +} + +// GetResourcePolicy returns a resource policy. +func (b *InMemoryBackend) GetResourcePolicy(resourceArn string) (string, error) { + b.mu.RLock("GetResourcePolicy") + defer b.mu.RUnlock() + + policy, ok := b.resourcePolicies[resourceArn] + if !ok { + return "", ErrNotFound + } + + return policy, nil +} + +// DeleteResourcePolicy deletes a resource policy. +func (b *InMemoryBackend) DeleteResourcePolicy(resourceArn string) error { + b.mu.Lock("DeleteResourcePolicy") + defer b.mu.Unlock() + + if _, ok := b.resourcePolicies[resourceArn]; !ok { + return ErrNotFound + } + + delete(b.resourcePolicies, resourceArn) + + return nil +} + +// ------- Tagging operations ------- + +// TagResource adds tags to a resource. +func (b *InMemoryBackend) TagResource(resourceArn string, tags map[string]string) error { + b.mu.Lock("TagResource") + defer b.mu.Unlock() + + if _, ok := b.tags[resourceArn]; !ok { + b.tags[resourceArn] = make(map[string]string) + } + + for k, v := range tags { + b.tags[resourceArn][k] = v + } + + return nil +} + +// UntagResource removes tags from a resource. +func (b *InMemoryBackend) UntagResource(resourceArn string, keys []string) error { + b.mu.Lock("UntagResource") + defer b.mu.Unlock() + + if t, ok := b.tags[resourceArn]; ok { + for _, k := range keys { + delete(t, k) + } + } + + return nil +} + +// ListTagsForResource returns tags for a resource. +func (b *InMemoryBackend) ListTagsForResource(resourceArn string) (map[string]string, error) { + b.mu.RLock("ListTagsForResource") + defer b.mu.RUnlock() + + t, ok := b.tags[resourceArn] + if !ok { + return make(map[string]string), nil + } + + result := make(map[string]string, len(t)) + maps.Copy(result, t) + + return result, nil +} diff --git a/services/vpclattice/export_test.go b/services/vpclattice/export_test.go new file mode 100644 index 000000000..1e54844dc --- /dev/null +++ b/services/vpclattice/export_test.go @@ -0,0 +1,38 @@ +package vpclattice + +// ServiceCount returns the number of stored services. +func ServiceCount(b *InMemoryBackend) int { + b.mu.RLock("ServiceCount") + defer b.mu.RUnlock() + + return len(b.services) +} + +// ServiceNetworkCount returns the number of stored service networks. +func ServiceNetworkCount(b *InMemoryBackend) int { + b.mu.RLock("ServiceNetworkCount") + defer b.mu.RUnlock() + + return len(b.serviceNetworks) +} + +// TargetGroupCount returns the number of stored target groups. +func TargetGroupCount(b *InMemoryBackend) int { + b.mu.RLock("TargetGroupCount") + defer b.mu.RUnlock() + + return len(b.targetGroups) +} + +// ListenerCount returns the number of stored listeners. +func ListenerCount(b *InMemoryBackend) int { + b.mu.RLock("ListenerCount") + defer b.mu.RUnlock() + + return len(b.listeners) +} + +// HandlerOpsLen returns the count of GetSupportedOperations. +func HandlerOpsLen(h *Handler) int { + return len(h.GetSupportedOperations()) +} diff --git a/services/vpclattice/handler.go b/services/vpclattice/handler.go new file mode 100644 index 000000000..1da9344f1 --- /dev/null +++ b/services/vpclattice/handler.go @@ -0,0 +1,1882 @@ +package vpclattice + +import ( + "encoding/json" + "errors" + "net/http" + "strconv" + "strings" + + "github.com/labstack/echo/v5" + + "github.com/blackbirdworks/gopherstack/pkgs/awserr" + "github.com/blackbirdworks/gopherstack/pkgs/service" +) + +const ( + matchPriority = service.PriorityPathVersioned + + pathServices = "/services" + pathServiceNetworks = "/servicenetworks" + pathServiceNetworkServiceAssociations = "/servicenetworkserviceassociations" + pathServiceNetworkVpcAssociations = "/servicenetworkvpcassociations" + pathTargetGroups = "/targetgroups" + pathAccessLogSubscriptions = "/accesslogsubscriptions" + pathAuthPolicy = "/authpolicy" + pathResourcePolicy = "/resourcepolicy" + pathTags = "/tags" + + opUnknown = "Unknown" + + keyMessage = "message" +) + +// Handler handles VPC Lattice HTTP requests. +type Handler struct { + Backend StorageBackend +} + +// NewHandler constructs a new Handler. +func NewHandler(b StorageBackend) *Handler { + return &Handler{Backend: b} +} + +// Name returns the service name. +func (h *Handler) Name() string { return "VPCLattice" } + +// Reset resets the backend. +func (h *Handler) Reset() { h.Backend.Reset() } + +// GetSupportedOperations returns all supported operations. +func (h *Handler) GetSupportedOperations() []string { + return []string{ + "BatchUpdateRule", + "CreateAccessLogSubscription", + "CreateListener", + "CreateRule", + "CreateService", + "CreateServiceNetwork", + "CreateServiceNetworkServiceAssociation", + "CreateServiceNetworkVpcAssociation", + "CreateTargetGroup", + "DeleteAccessLogSubscription", + "DeleteAuthPolicy", + "DeleteListener", + "DeleteResourcePolicy", + "DeleteRule", + "DeleteService", + "DeleteServiceNetwork", + "DeleteServiceNetworkServiceAssociation", + "DeleteServiceNetworkVpcAssociation", + "DeleteTargetGroup", + "DeregisterTargets", + "GetAccessLogSubscription", + "GetAuthPolicy", + "GetListener", + "GetResourcePolicy", + "GetRule", + "GetService", + "GetServiceNetwork", + "GetServiceNetworkServiceAssociation", + "GetServiceNetworkVpcAssociation", + "GetTargetGroup", + "ListAccessLogSubscriptions", + "ListListeners", + "ListRules", + "ListServiceNetworkServiceAssociations", + "ListServiceNetworkVpcAssociations", + "ListServiceNetworks", + "ListServices", + "ListTagsForResource", + "ListTargetGroups", + "ListTargets", + "PutAuthPolicy", + "PutResourcePolicy", + "RegisterTargets", + "TagResource", + "UntagResource", + "UpdateAccessLogSubscription", + "UpdateListener", + "UpdateRule", + "UpdateService", + "UpdateServiceNetwork", + "UpdateServiceNetworkVpcAssociation", + "UpdateTargetGroup", + } +} + +// RouteMatcher returns a function that matches VPC Lattice API requests. +func (h *Handler) RouteMatcher() service.Matcher { + return func(c *echo.Context) bool { + path := c.Request().URL.Path + + return path == pathServices || strings.HasPrefix(path, pathServices+"/") || + path == pathServiceNetworks || strings.HasPrefix(path, pathServiceNetworks+"/") || + path == pathServiceNetworkServiceAssociations || strings.HasPrefix(path, pathServiceNetworkServiceAssociations+"/") || + path == pathServiceNetworkVpcAssociations || strings.HasPrefix(path, pathServiceNetworkVpcAssociations+"/") || + path == pathTargetGroups || strings.HasPrefix(path, pathTargetGroups+"/") || + path == pathAccessLogSubscriptions || strings.HasPrefix(path, pathAccessLogSubscriptions+"/") || + strings.HasPrefix(path, pathAuthPolicy+"/") || + strings.HasPrefix(path, pathResourcePolicy+"/") || + isVPCLatticeTagPath(path) + } +} + +func isVPCLatticeTagPath(path string) bool { + rest, ok := strings.CutPrefix(path, pathTags+"/") + + return ok && strings.Contains(rest, ":vpc-lattice:") +} + +// MatchPriority returns the routing priority. +func (h *Handler) MatchPriority() int { return matchPriority } + +// ExtractOperation classifies the request into an operation name. +func (h *Handler) ExtractOperation(c *echo.Context) string { + op, _, _, _, _ := classifyPath(c.Request().Method, c.Request().URL.Path) + + return op +} + +// ExtractResource returns the primary resource identifier. +func (h *Handler) ExtractResource(c *echo.Context) string { + _, id, _, _, _ := classifyPath(c.Request().Method, c.Request().URL.Path) + + return id +} + +// Handler returns the Echo handler function for VPC Lattice requests. +func (h *Handler) Handler() echo.HandlerFunc { + return func(c *echo.Context) error { + return h.handleREST(c) + } +} + +func (h *Handler) handleREST(c *echo.Context) error { + op, id1, id2, id3, _ := classifyPath(c.Request().Method, c.Request().URL.Path) + + var body map[string]any + if c.Request().ContentLength != 0 { + if err := json.NewDecoder(c.Request().Body).Decode(&body); err != nil && err.Error() != "EOF" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "invalid JSON body"}) + } + } + + if body == nil { + body = map[string]any{} + } + + switch op { + case "CreateService": + return h.handleCreateService(c, body) + case "GetService": + return h.handleGetService(c, id1) + case "UpdateService": + return h.handleUpdateService(c, id1, body) + case "DeleteService": + return h.handleDeleteService(c, id1) + case "ListServices": + return h.handleListServices(c) + case "CreateServiceNetwork": + return h.handleCreateServiceNetwork(c, body) + case "GetServiceNetwork": + return h.handleGetServiceNetwork(c, id1) + case "UpdateServiceNetwork": + return h.handleUpdateServiceNetwork(c, id1, body) + case "DeleteServiceNetwork": + return h.handleDeleteServiceNetwork(c, id1) + case "ListServiceNetworks": + return h.handleListServiceNetworks(c) + case "CreateServiceNetworkServiceAssociation": + return h.handleCreateSNSA(c, body) + case "GetServiceNetworkServiceAssociation": + return h.handleGetSNSA(c, id1) + case "DeleteServiceNetworkServiceAssociation": + return h.handleDeleteSNSA(c, id1) + case "ListServiceNetworkServiceAssociations": + return h.handleListSNSAs(c) + case "CreateServiceNetworkVpcAssociation": + return h.handleCreateSNVA(c, body) + case "GetServiceNetworkVpcAssociation": + return h.handleGetSNVA(c, id1) + case "UpdateServiceNetworkVpcAssociation": + return h.handleUpdateSNVA(c, id1, body) + case "DeleteServiceNetworkVpcAssociation": + return h.handleDeleteSNVA(c, id1) + case "ListServiceNetworkVpcAssociations": + return h.handleListSNVAs(c) + case "CreateListener": + return h.handleCreateListener(c, id1, body) + case "GetListener": + return h.handleGetListener(c, id1, id2) + case "UpdateListener": + return h.handleUpdateListener(c, id1, id2, body) + case "DeleteListener": + return h.handleDeleteListener(c, id1, id2) + case "ListListeners": + return h.handleListListeners(c, id1) + case "CreateRule": + return h.handleCreateRule(c, id1, id2, body) + case "GetRule": + return h.handleGetRule(c, id1, id2, id3) + case "UpdateRule": + return h.handleUpdateRule(c, id1, id2, id3, body) + case "DeleteRule": + return h.handleDeleteRule(c, id1, id2, id3) + case "ListRules": + return h.handleListRules(c, id1, id2) + case "BatchUpdateRule": + return h.handleBatchUpdateRule(c, id1, id2, body) + case "CreateTargetGroup": + return h.handleCreateTargetGroup(c, body) + case "GetTargetGroup": + return h.handleGetTargetGroup(c, id1) + case "UpdateTargetGroup": + return h.handleUpdateTargetGroup(c, id1, body) + case "DeleteTargetGroup": + return h.handleDeleteTargetGroup(c, id1) + case "ListTargetGroups": + return h.handleListTargetGroups(c) + case "RegisterTargets": + return h.handleRegisterTargets(c, id1, body) + case "DeregisterTargets": + return h.handleDeregisterTargets(c, id1, body) + case "ListTargets": + return h.handleListTargets(c, id1, body) + case "CreateAccessLogSubscription": + return h.handleCreateALS(c, body) + case "GetAccessLogSubscription": + return h.handleGetALS(c, id1) + case "UpdateAccessLogSubscription": + return h.handleUpdateALS(c, id1, body) + case "DeleteAccessLogSubscription": + return h.handleDeleteALS(c, id1) + case "ListAccessLogSubscriptions": + return h.handleListALSs(c) + case "PutAuthPolicy": + return h.handlePutAuthPolicy(c, id1, body) + case "GetAuthPolicy": + return h.handleGetAuthPolicy(c, id1) + case "DeleteAuthPolicy": + return h.handleDeleteAuthPolicy(c, id1) + case "PutResourcePolicy": + return h.handlePutResourcePolicy(c, id1, body) + case "GetResourcePolicy": + return h.handleGetResourcePolicy(c, id1) + case "DeleteResourcePolicy": + return h.handleDeleteResourcePolicy(c, id1) + case "TagResource": + return h.handleTagResource(c, id1, body) + case "UntagResource": + return h.handleUntagResource(c, id1) + case "ListTagsForResource": + return h.handleListTagsForResource(c, id1) + default: + return c.JSON(http.StatusNotFound, map[string]any{keyMessage: "unknown operation"}) + } +} + +// handleError converts backend errors to HTTP responses. +func (h *Handler) handleError(c *echo.Context, err error) error { + switch { + case errors.Is(err, awserr.ErrNotFound): + return c.JSON(http.StatusNotFound, map[string]any{keyMessage: err.Error()}) + case errors.Is(err, awserr.ErrAlreadyExists): + return c.JSON(http.StatusConflict, map[string]any{keyMessage: err.Error()}) + case errors.Is(err, awserr.ErrInvalidParameter): + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: err.Error()}) + } + + return c.JSON(http.StatusInternalServerError, map[string]any{keyMessage: err.Error()}) +} + +// ------- Service handlers ------- + +func (h *Handler) handleCreateService(c *echo.Context, body map[string]any) error { + name, _ := body["name"].(string) + if name == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name is required"}) + } + + authType, _ := body["authType"].(string) + certArn, _ := body["certificateArn"].(string) + customDomain, _ := body["customDomainName"].(string) + tags := extractTags(body) + + svc, err := h.Backend.CreateService(name, authType, certArn, customDomain, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, serviceToJSON(svc)) +} + +func (h *Handler) handleGetService(c *echo.Context, id string) error { + svc, err := h.Backend.GetService(id) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, serviceToJSON(svc)) +} + +func (h *Handler) handleUpdateService(c *echo.Context, id string, body map[string]any) error { + authType, _ := body["authType"].(string) + certArn, _ := body["certificateArn"].(string) + + svc, err := h.Backend.UpdateService(id, authType, certArn) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, serviceToJSON(svc)) +} + +func (h *Handler) handleDeleteService(c *echo.Context, id string) error { + svc, err := h.Backend.DeleteService(id) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, serviceToJSON(svc)) +} + +func (h *Handler) handleListServices(c *echo.Context) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + + items, next, err := h.Backend.ListServices(maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, s := range items { + summaries = append(summaries, serviceSummaryToJSON(s)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +// ------- ServiceNetwork handlers ------- + +func (h *Handler) handleCreateServiceNetwork(c *echo.Context, body map[string]any) error { + name, _ := body["name"].(string) + if name == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name is required"}) + } + + authType, _ := body["authType"].(string) + tags := extractTags(body) + + sn, err := h.Backend.CreateServiceNetwork(name, authType, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, serviceNetworkToJSON(sn)) +} + +func (h *Handler) handleGetServiceNetwork(c *echo.Context, id string) error { + sn, err := h.Backend.GetServiceNetwork(id) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, serviceNetworkToJSON(sn)) +} + +func (h *Handler) handleUpdateServiceNetwork(c *echo.Context, id string, body map[string]any) error { + authType, _ := body["authType"].(string) + + sn, err := h.Backend.UpdateServiceNetwork(id, authType) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, serviceNetworkToJSON(sn)) +} + +func (h *Handler) handleDeleteServiceNetwork(c *echo.Context, id string) error { + if err := h.Backend.DeleteServiceNetwork(id); err != nil { + return h.handleError(c, err) + } + + return c.NoContent(http.StatusNoContent) +} + +func (h *Handler) handleListServiceNetworks(c *echo.Context) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + + items, next, err := h.Backend.ListServiceNetworks(maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, s := range items { + summaries = append(summaries, serviceNetworkSummaryToJSON(s)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +// ------- SNSA handlers ------- + +func (h *Handler) handleCreateSNSA(c *echo.Context, body map[string]any) error { + snID, _ := body["serviceNetworkIdentifier"].(string) + svcID, _ := body["serviceIdentifier"].(string) + + if snID == "" || svcID == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "serviceNetworkIdentifier and serviceIdentifier are required"}) + } + + tags := extractTags(body) + + assoc, err := h.Backend.CreateServiceNetworkServiceAssociation(snID, svcID, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, snsaToJSON(assoc)) +} + +func (h *Handler) handleGetSNSA(c *echo.Context, id string) error { + assoc, err := h.Backend.GetServiceNetworkServiceAssociation(id) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, snsaToJSON(assoc)) +} + +func (h *Handler) handleDeleteSNSA(c *echo.Context, id string) error { + if err := h.Backend.DeleteServiceNetworkServiceAssociation(id); err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{"status": "DELETE_IN_PROGRESS"}) +} + +func (h *Handler) handleListSNSAs(c *echo.Context) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + snID := c.QueryParam("serviceNetworkIdentifier") + svcID := c.QueryParam("serviceIdentifier") + + items, next, err := h.Backend.ListServiceNetworkServiceAssociations(snID, svcID, maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, s := range items { + summaries = append(summaries, snsaSummaryToJSON(s)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +// ------- SNVA handlers ------- + +func (h *Handler) handleCreateSNVA(c *echo.Context, body map[string]any) error { + snID, _ := body["serviceNetworkIdentifier"].(string) + vpcID, _ := body["vpcIdentifier"].(string) + + if snID == "" || vpcID == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "serviceNetworkIdentifier and vpcIdentifier are required"}) + } + + var sgs []string + if sgRaw, ok := body["securityGroupIds"].([]any); ok { + for _, v := range sgRaw { + if s, ok := v.(string); ok { + sgs = append(sgs, s) + } + } + } + + tags := extractTags(body) + + assoc, err := h.Backend.CreateServiceNetworkVpcAssociation(snID, vpcID, sgs, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, snvaToJSON(assoc)) +} + +func (h *Handler) handleGetSNVA(c *echo.Context, id string) error { + assoc, err := h.Backend.GetServiceNetworkVpcAssociation(id) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, snvaToJSON(assoc)) +} + +func (h *Handler) handleUpdateSNVA(c *echo.Context, id string, body map[string]any) error { + var sgs []string + if sgRaw, ok := body["securityGroupIds"].([]any); ok { + for _, v := range sgRaw { + if s, ok := v.(string); ok { + sgs = append(sgs, s) + } + } + } + + assoc, err := h.Backend.UpdateServiceNetworkVpcAssociation(id, sgs) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, snvaToJSON(assoc)) +} + +func (h *Handler) handleDeleteSNVA(c *echo.Context, id string) error { + if err := h.Backend.DeleteServiceNetworkVpcAssociation(id); err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{"status": "DELETE_IN_PROGRESS"}) +} + +func (h *Handler) handleListSNVAs(c *echo.Context) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + snID := c.QueryParam("serviceNetworkIdentifier") + vpcID := c.QueryParam("vpcIdentifier") + + items, next, err := h.Backend.ListServiceNetworkVpcAssociations(snID, vpcID, maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, s := range items { + summaries = append(summaries, snvaSummaryToJSON(s)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +// ------- Listener handlers ------- + +func (h *Handler) handleCreateListener(c *echo.Context, serviceID string, body map[string]any) error { + name, _ := body["name"].(string) + protocol, _ := body["protocol"].(string) + + if name == "" || protocol == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name and protocol are required"}) + } + + port := bodyInt32(body, "port") + defaultAction := extractRuleAction(body, "defaultAction") + tags := extractTags(body) + + l, err := h.Backend.CreateListener(serviceID, name, protocol, port, defaultAction, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, listenerToJSON(l)) +} + +func (h *Handler) handleGetListener(c *echo.Context, serviceID, listenerID string) error { + l, err := h.Backend.GetListener(serviceID, listenerID) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, listenerToJSON(l)) +} + +func (h *Handler) handleUpdateListener(c *echo.Context, serviceID, listenerID string, body map[string]any) error { + defaultAction := extractRuleAction(body, "defaultAction") + + l, err := h.Backend.UpdateListener(serviceID, listenerID, defaultAction) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, listenerToJSON(l)) +} + +func (h *Handler) handleDeleteListener(c *echo.Context, serviceID, listenerID string) error { + if err := h.Backend.DeleteListener(serviceID, listenerID); err != nil { + return h.handleError(c, err) + } + + return c.NoContent(http.StatusNoContent) +} + +func (h *Handler) handleListListeners(c *echo.Context, serviceID string) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + + items, next, err := h.Backend.ListListeners(serviceID, maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, l := range items { + summaries = append(summaries, listenerSummaryToJSON(l)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +// ------- Rule handlers ------- + +func (h *Handler) handleCreateRule(c *echo.Context, serviceID, listenerID string, body map[string]any) error { + name, _ := body["name"].(string) + if name == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name is required"}) + } + + priority := bodyInt32(body, "priority") + action := extractRuleAction(body, "action") + match := extractRuleMatch(body, "match") + tags := extractTags(body) + + r, err := h.Backend.CreateRule(serviceID, listenerID, name, priority, action, match, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, ruleToJSON(r)) +} + +func (h *Handler) handleGetRule(c *echo.Context, serviceID, listenerID, ruleID string) error { + r, err := h.Backend.GetRule(serviceID, listenerID, ruleID) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, ruleToJSON(r)) +} + +func (h *Handler) handleUpdateRule(c *echo.Context, serviceID, listenerID, ruleID string, body map[string]any) error { + priority := bodyInt32(body, "priority") + action := extractRuleAction(body, "action") + match := extractRuleMatch(body, "match") + + r, err := h.Backend.UpdateRule(serviceID, listenerID, ruleID, priority, action, match) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, ruleToJSON(r)) +} + +func (h *Handler) handleDeleteRule(c *echo.Context, serviceID, listenerID, ruleID string) error { + if err := h.Backend.DeleteRule(serviceID, listenerID, ruleID); err != nil { + return h.handleError(c, err) + } + + return c.NoContent(http.StatusNoContent) +} + +func (h *Handler) handleListRules(c *echo.Context, serviceID, listenerID string) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + + items, next, err := h.Backend.ListRules(serviceID, listenerID, maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, r := range items { + summaries = append(summaries, ruleSummaryToJSON(r)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +func (h *Handler) handleBatchUpdateRule(c *echo.Context, serviceID, listenerID string, body map[string]any) error { + var updates []*RuleUpdate + + if rawUpdates, ok := body["rules"].([]any); ok { + for _, raw := range rawUpdates { + if m, ok := raw.(map[string]any); ok { + u := &RuleUpdate{} + u.RuleIdentifier, _ = m["ruleIdentifier"].(string) + u.Priority = bodyInt32(m, "priority") + u.Action = extractRuleAction(m, "action") + u.Match = extractRuleMatch(m, "match") + updates = append(updates, u) + } + } + } + + successes, failures, err := h.Backend.BatchUpdateRule(serviceID, listenerID, updates) + if err != nil { + return h.handleError(c, err) + } + + successList := make([]any, 0, len(successes)) + for _, s := range successes { + successList = append(successList, ruleUpdateSuccessToJSON(s)) + } + + failureList := make([]any, 0, len(failures)) + for _, f := range failures { + failureList = append(failureList, map[string]any{ + "ruleIdentifier": f.RuleIdentifier, + "message": f.Message, + "code": f.Code, + }) + } + + return c.JSON(http.StatusOK, map[string]any{ + "successful": successList, + "unsuccessful": failureList, + }) +} + +// ------- TargetGroup handlers ------- + +func (h *Handler) handleCreateTargetGroup(c *echo.Context, body map[string]any) error { + name, _ := body["name"].(string) + tgType, _ := body["type"].(string) + + if name == "" || tgType == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name and type are required"}) + } + + config := extractTargetGroupConfig(body) + tags := extractTags(body) + + tg, err := h.Backend.CreateTargetGroup(name, tgType, config, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, targetGroupToJSON(tg)) +} + +func (h *Handler) handleGetTargetGroup(c *echo.Context, id string) error { + tg, err := h.Backend.GetTargetGroup(id) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, targetGroupToJSON(tg)) +} + +func (h *Handler) handleUpdateTargetGroup(c *echo.Context, id string, body map[string]any) error { + var healthCheck *HealthCheckConfig + if hcRaw, ok := body["healthCheck"].(map[string]any); ok { + healthCheck = extractHealthCheckConfig(hcRaw) + } + + tg, err := h.Backend.UpdateTargetGroup(id, healthCheck) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, targetGroupToJSON(tg)) +} + +func (h *Handler) handleDeleteTargetGroup(c *echo.Context, id string) error { + if err := h.Backend.DeleteTargetGroup(id); err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{"id": id, "status": "DELETE_IN_PROGRESS"}) +} + +func (h *Handler) handleListTargetGroups(c *echo.Context) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + tgType := c.QueryParam("targetGroupType") + svcArn := c.QueryParam("serviceArn") + + items, next, err := h.Backend.ListTargetGroups(tgType, svcArn, maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, tg := range items { + summaries = append(summaries, targetGroupSummaryToJSON(tg)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +func (h *Handler) handleRegisterTargets(c *echo.Context, tgID string, body map[string]any) error { + targets := extractTargets(body) + + failures, err := h.Backend.RegisterTargets(tgID, targets) + if err != nil { + return h.handleError(c, err) + } + + failureList := make([]any, 0, len(failures)) + for _, f := range failures { + failureList = append(failureList, targetFailureToJSON(f)) + } + + return c.JSON(http.StatusOK, map[string]any{"unsuccessful": failureList}) +} + +func (h *Handler) handleDeregisterTargets(c *echo.Context, tgID string, body map[string]any) error { + targets := extractTargets(body) + + failures, err := h.Backend.DeregisterTargets(tgID, targets) + if err != nil { + return h.handleError(c, err) + } + + failureList := make([]any, 0, len(failures)) + for _, f := range failures { + failureList = append(failureList, targetFailureToJSON(f)) + } + + return c.JSON(http.StatusOK, map[string]any{"unsuccessful": failureList}) +} + +func (h *Handler) handleListTargets(c *echo.Context, tgID string, body map[string]any) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + + items, next, err := h.Backend.ListTargets(tgID, maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, t := range items { + summaries = append(summaries, targetSummaryToJSON(t)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +// ------- AccessLogSubscription handlers ------- + +func (h *Handler) handleCreateALS(c *echo.Context, body map[string]any) error { + resourceID, _ := body["resourceIdentifier"].(string) + destArn, _ := body["destinationArn"].(string) + logType, _ := body["serviceNetworkLogType"].(string) + + if resourceID == "" || destArn == "" { + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "resourceIdentifier and destinationArn are required"}) + } + + tags := extractTags(body) + + als, err := h.Backend.CreateAccessLogSubscription(resourceID, destArn, logType, tags) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusCreated, alsToJSON(als)) +} + +func (h *Handler) handleGetALS(c *echo.Context, id string) error { + als, err := h.Backend.GetAccessLogSubscription(id) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, alsToJSON(als)) +} + +func (h *Handler) handleUpdateALS(c *echo.Context, id string, body map[string]any) error { + destArn, _ := body["destinationArn"].(string) + + als, err := h.Backend.UpdateAccessLogSubscription(id, destArn) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, alsToJSON(als)) +} + +func (h *Handler) handleDeleteALS(c *echo.Context, id string) error { + if err := h.Backend.DeleteAccessLogSubscription(id); err != nil { + return h.handleError(c, err) + } + + return c.NoContent(http.StatusNoContent) +} + +func (h *Handler) handleListALSs(c *echo.Context) error { + maxResults := queryInt32(c, "maxResults", 0) + nextToken := c.QueryParam("nextToken") + resourceID := c.QueryParam("resourceIdentifier") + + items, next, err := h.Backend.ListAccessLogSubscriptions(resourceID, maxResults, nextToken) + if err != nil { + return h.handleError(c, err) + } + + summaries := make([]any, 0, len(items)) + for _, a := range items { + summaries = append(summaries, alsSummaryToJSON(a)) + } + + resp := map[string]any{"items": summaries} + if next != "" { + resp["nextToken"] = next + } + + return c.JSON(http.StatusOK, resp) +} + +// ------- Auth/Resource Policy handlers ------- + +func (h *Handler) handlePutAuthPolicy(c *echo.Context, resourceID string, body map[string]any) error { + policy, _ := body["policy"].(string) + + ap, err := h.Backend.PutAuthPolicy(resourceID, policy) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{ + "policy": ap.Policy, + "state": ap.State, + }) +} + +func (h *Handler) handleGetAuthPolicy(c *echo.Context, resourceID string) error { + ap, err := h.Backend.GetAuthPolicy(resourceID) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{ + "policy": ap.Policy, + "state": ap.State, + }) +} + +func (h *Handler) handleDeleteAuthPolicy(c *echo.Context, resourceID string) error { + if err := h.Backend.DeleteAuthPolicy(resourceID); err != nil { + return h.handleError(c, err) + } + + return c.NoContent(http.StatusNoContent) +} + +func (h *Handler) handlePutResourcePolicy(c *echo.Context, resourceArn string, body map[string]any) error { + policy, _ := body["policy"].(string) + + if err := h.Backend.PutResourcePolicy(resourceArn, policy); err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{}) +} + +func (h *Handler) handleGetResourcePolicy(c *echo.Context, resourceArn string) error { + policy, err := h.Backend.GetResourcePolicy(resourceArn) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{"policy": policy}) +} + +func (h *Handler) handleDeleteResourcePolicy(c *echo.Context, resourceArn string) error { + if err := h.Backend.DeleteResourcePolicy(resourceArn); err != nil { + return h.handleError(c, err) + } + + return c.NoContent(http.StatusNoContent) +} + +// ------- Tagging handlers ------- + +func (h *Handler) handleTagResource(c *echo.Context, resourceArn string, body map[string]any) error { + tags := make(map[string]string) + if t, ok := body["tags"].(map[string]any); ok { + for k, v := range t { + if s, ok := v.(string); ok { + tags[k] = s + } + } + } + + if err := h.Backend.TagResource(resourceArn, tags); err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{}) +} + +func (h *Handler) handleUntagResource(c *echo.Context, resourceArn string) error { + keys := c.Request().URL.Query()["tagKeys"] + + if err := h.Backend.UntagResource(resourceArn, keys); err != nil { + return h.handleError(c, err) + } + + return c.NoContent(http.StatusNoContent) +} + +func (h *Handler) handleListTagsForResource(c *echo.Context, resourceArn string) error { + tags, err := h.Backend.ListTagsForResource(resourceArn) + if err != nil { + return h.handleError(c, err) + } + + return c.JSON(http.StatusOK, map[string]any{"tags": tags}) +} + +// ------- Path classification ------- + +// classifyPath maps (method, path) → (op, id1, id2, id3, extra). +// id1..id3 are path segments in order (service, listener, rule etc.). +func classifyPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns + switch { + case path == pathServices: + if method == http.MethodPost { + return "CreateService", "", "", "", "" + } + + return "ListServices", "", "", "", "" + case strings.HasPrefix(path, pathServices+"/"): + return classifyServicePath(method, path) + + case path == pathServiceNetworks: + if method == http.MethodPost { + return "CreateServiceNetwork", "", "", "", "" + } + + return "ListServiceNetworks", "", "", "", "" + case strings.HasPrefix(path, pathServiceNetworks+"/"): + return classifyServiceNetworkPath(method, path) + + case path == pathServiceNetworkServiceAssociations: + if method == http.MethodPost { + return "CreateServiceNetworkServiceAssociation", "", "", "", "" + } + + return "ListServiceNetworkServiceAssociations", "", "", "", "" + case strings.HasPrefix(path, pathServiceNetworkServiceAssociations+"/"): + return classifySNSAPath(method, path) + + case path == pathServiceNetworkVpcAssociations: + if method == http.MethodPost { + return "CreateServiceNetworkVpcAssociation", "", "", "", "" + } + + return "ListServiceNetworkVpcAssociations", "", "", "", "" + case strings.HasPrefix(path, pathServiceNetworkVpcAssociations+"/"): + return classifySNVAPath(method, path) + + case path == pathTargetGroups: + if method == http.MethodPost { + return "CreateTargetGroup", "", "", "", "" + } + + return "ListTargetGroups", "", "", "", "" + case strings.HasPrefix(path, pathTargetGroups+"/"): + return classifyTargetGroupPath(method, path) + + case path == pathAccessLogSubscriptions: + if method == http.MethodPost { + return "CreateAccessLogSubscription", "", "", "", "" + } + + return "ListAccessLogSubscriptions", "", "", "", "" + case strings.HasPrefix(path, pathAccessLogSubscriptions+"/"): + return classifyALSPath(method, path) + + case strings.HasPrefix(path, pathAuthPolicy+"/"): + resourceID := strings.TrimPrefix(path, pathAuthPolicy+"/") + switch method { + case http.MethodPut: + return "PutAuthPolicy", resourceID, "", "", "" + case http.MethodGet: + return "GetAuthPolicy", resourceID, "", "", "" + case http.MethodDelete: + return "DeleteAuthPolicy", resourceID, "", "", "" + } + + case strings.HasPrefix(path, pathResourcePolicy+"/"): + resourceArn := strings.TrimPrefix(path, pathResourcePolicy+"/") + switch method { + case http.MethodPut: + return "PutResourcePolicy", resourceArn, "", "", "" + case http.MethodGet: + return "GetResourcePolicy", resourceArn, "", "", "" + case http.MethodDelete: + return "DeleteResourcePolicy", resourceArn, "", "", "" + } + + case strings.HasPrefix(path, pathTags+"/"): + resourceArn := strings.TrimPrefix(path, pathTags+"/") + switch method { + case http.MethodPost: + return "TagResource", resourceArn, "", "", "" + case http.MethodDelete: + return "UntagResource", resourceArn, "", "", "" + case http.MethodGet: + return "ListTagsForResource", resourceArn, "", "", "" + } + } + + return opUnknown, "", "", "", "" +} + +// classifyServicePath handles /services/{serviceID}[/listeners[/...]] +func classifyServicePath(method, path string) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns + rest := strings.TrimPrefix(path, pathServices+"/") + serviceID, sub, hasSub := strings.Cut(rest, "/") + + if !hasSub { + switch method { + case http.MethodGet: + return "GetService", serviceID, "", "", "" + case http.MethodPatch: + return "UpdateService", serviceID, "", "", "" + case http.MethodDelete: + return "DeleteService", serviceID, "", "", "" + } + + return opUnknown, serviceID, "", "", "" + } + + // sub = listeners[/{listenerID}[/rules[/{ruleID}]]] + if sub == "listeners" { + if method == http.MethodPost { + return "CreateListener", serviceID, "", "", "" + } + + return "ListListeners", serviceID, "", "", "" + } + + if strings.HasPrefix(sub, "listeners/") { + listenerRest := strings.TrimPrefix(sub, "listeners/") + listenerID, listenerSub, hasListenerSub := strings.Cut(listenerRest, "/") + + if !hasListenerSub { + switch method { + case http.MethodGet: + return "GetListener", serviceID, listenerID, "", "" + case http.MethodPatch: + return "UpdateListener", serviceID, listenerID, "", "" + case http.MethodDelete: + return "DeleteListener", serviceID, listenerID, "", "" + } + + return opUnknown, serviceID, listenerID, "", "" + } + + if listenerSub == "rules" { + if method == http.MethodPost { + return "CreateRule", serviceID, listenerID, "", "" + } + + if method == http.MethodPatch { + return "BatchUpdateRule", serviceID, listenerID, "", "" + } + + return "ListRules", serviceID, listenerID, "", "" + } + + if strings.HasPrefix(listenerSub, "rules/") { + ruleID := strings.TrimPrefix(listenerSub, "rules/") + switch method { + case http.MethodGet: + return "GetRule", serviceID, listenerID, ruleID, "" + case http.MethodPatch: + return "UpdateRule", serviceID, listenerID, ruleID, "" + case http.MethodDelete: + return "DeleteRule", serviceID, listenerID, ruleID, "" + } + } + } + + return opUnknown, serviceID, "", "", "" +} + +// classifyServiceNetworkPath handles /servicenetworks/{id} +func classifyServiceNetworkPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns + id := strings.TrimPrefix(path, pathServiceNetworks+"/") + switch method { + case http.MethodGet: + return "GetServiceNetwork", id, "", "", "" + case http.MethodPatch: + return "UpdateServiceNetwork", id, "", "", "" + case http.MethodDelete: + return "DeleteServiceNetwork", id, "", "", "" + } + + return opUnknown, id, "", "", "" +} + +// classifySNSAPath handles /servicenetworkserviceassociations/{id} +func classifySNSAPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns + id := strings.TrimPrefix(path, pathServiceNetworkServiceAssociations+"/") + switch method { + case http.MethodGet: + return "GetServiceNetworkServiceAssociation", id, "", "", "" + case http.MethodDelete: + return "DeleteServiceNetworkServiceAssociation", id, "", "", "" + } + + return opUnknown, id, "", "", "" +} + +// classifySNVAPath handles /servicenetworkvpcassociations/{id} +func classifySNVAPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns + id := strings.TrimPrefix(path, pathServiceNetworkVpcAssociations+"/") + switch method { + case http.MethodGet: + return "GetServiceNetworkVpcAssociation", id, "", "", "" + case http.MethodPatch: + return "UpdateServiceNetworkVpcAssociation", id, "", "", "" + case http.MethodDelete: + return "DeleteServiceNetworkVpcAssociation", id, "", "", "" + } + + return opUnknown, id, "", "", "" +} + +// classifyTargetGroupPath handles /targetgroups/{id}[/registertargets|deregistertargets|listtargets] +func classifyTargetGroupPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns + rest := strings.TrimPrefix(path, pathTargetGroups+"/") + tgID, sub, hasSub := strings.Cut(rest, "/") + + if !hasSub { + switch method { + case http.MethodGet: + return "GetTargetGroup", tgID, "", "", "" + case http.MethodPatch: + return "UpdateTargetGroup", tgID, "", "", "" + case http.MethodDelete: + return "DeleteTargetGroup", tgID, "", "", "" + } + + return opUnknown, tgID, "", "", "" + } + + switch sub { + case "registertargets": + return "RegisterTargets", tgID, "", "", "" + case "deregistertargets": + return "DeregisterTargets", tgID, "", "", "" + case "listtargets": + return "ListTargets", tgID, "", "", "" + } + + return opUnknown, tgID, "", "", "" +} + +// classifyALSPath handles /accesslogsubscriptions/{id} +func classifyALSPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns + id := strings.TrimPrefix(path, pathAccessLogSubscriptions+"/") + switch method { + case http.MethodGet: + return "GetAccessLogSubscription", id, "", "", "" + case http.MethodPatch: + return "UpdateAccessLogSubscription", id, "", "", "" + case http.MethodDelete: + return "DeleteAccessLogSubscription", id, "", "", "" + } + + return opUnknown, id, "", "", "" +} + +// ------- JSON serialization helpers ------- + +func serviceToJSON(s *Service) map[string]any { + m := map[string]any{ + "arn": s.ARN, + "id": s.ID, + "name": s.Name, + "authType": s.AuthType, + "status": s.Status, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + "dnsEntry": map[string]any{"domainName": s.DNSName}, + } + + if s.CertificateArn != "" { + m["certificateArn"] = s.CertificateArn + } + + if s.CustomDomainName != "" { + m["customDomainName"] = s.CustomDomainName + } + + return m +} + +func serviceSummaryToJSON(s *ServiceSummary) map[string]any { + m := map[string]any{ + "arn": s.ARN, + "id": s.ID, + "name": s.Name, + "status": s.Status, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + } + + if s.DNSName != "" { + m["dnsEntry"] = map[string]any{"domainName": s.DNSName} + } + + if s.CustomDomainName != "" { + m["customDomainName"] = s.CustomDomainName + } + + return m +} + +func serviceNetworkToJSON(s *ServiceNetwork) map[string]any { + return map[string]any{ + "arn": s.ARN, + "id": s.ID, + "name": s.Name, + "authType": s.AuthType, + "numberOfAssociatedServices": s.NumberOfAssociatedServices, + "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func serviceNetworkSummaryToJSON(s *ServiceNetworkSummary) map[string]any { + return map[string]any{ + "arn": s.ARN, + "id": s.ID, + "name": s.Name, + "numberOfAssociatedServices": s.NumberOfAssociatedServices, + "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func snsaToJSON(s *ServiceNetworkServiceAssociation) map[string]any { + return map[string]any{ + "arn": s.ARN, + "id": s.ID, + "serviceArn": s.ServiceARN, + "serviceId": s.ServiceID, + "serviceName": s.ServiceName, + "serviceNetworkArn": s.ServiceNetworkARN, + "serviceNetworkId": s.ServiceNetworkID, + "serviceNetworkName": s.ServiceNetworkName, + "status": s.Status, + "createdBy": s.CreatedBy, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func snsaSummaryToJSON(s *ServiceNetworkServiceAssociationSummary) map[string]any { + return map[string]any{ + "arn": s.ARN, + "id": s.ID, + "serviceArn": s.ServiceARN, + "serviceId": s.ServiceID, + "serviceName": s.ServiceName, + "serviceNetworkArn": s.ServiceNetworkARN, + "serviceNetworkId": s.ServiceNetworkID, + "serviceNetworkName": s.ServiceNetworkName, + "status": s.Status, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func snvaToJSON(s *ServiceNetworkVpcAssociation) map[string]any { + sgs := make([]string, len(s.SecurityGroupIDs)) + copy(sgs, s.SecurityGroupIDs) + + return map[string]any{ + "arn": s.ARN, + "id": s.ID, + "vpcId": s.VpcID, + "serviceNetworkArn": s.ServiceNetworkARN, + "serviceNetworkId": s.ServiceNetworkID, + "serviceNetworkName": s.ServiceNetworkName, + "securityGroupIds": sgs, + "status": s.Status, + "createdBy": s.CreatedBy, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func snvaSummaryToJSON(s *ServiceNetworkVpcAssociationSummary) map[string]any { + return map[string]any{ + "arn": s.ARN, + "id": s.ID, + "vpcId": s.VpcID, + "serviceNetworkArn": s.ServiceNetworkARN, + "serviceNetworkId": s.ServiceNetworkID, + "serviceNetworkName": s.ServiceNetworkName, + "status": s.Status, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func listenerToJSON(l *Listener) map[string]any { + m := map[string]any{ + "arn": l.ARN, + "id": l.ID, + "serviceArn": l.ServiceARN, + "serviceId": l.ServiceID, + "name": l.Name, + "protocol": l.Protocol, + "port": l.Port, + "createdAt": l.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": l.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + } + + if l.DefaultAction != nil { + m["defaultAction"] = ruleActionToJSON(l.DefaultAction) + } + + return m +} + +func listenerSummaryToJSON(l *ListenerSummary) map[string]any { + return map[string]any{ + "arn": l.ARN, + "id": l.ID, + "name": l.Name, + "protocol": l.Protocol, + "port": l.Port, + "createdAt": l.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": l.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func ruleToJSON(r *Rule) map[string]any { + m := map[string]any{ + "arn": r.ARN, + "id": r.ID, + "name": r.Name, + "priority": r.Priority, + "isDefault": r.IsDefault, + "createdAt": r.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": r.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + } + + if r.Action != nil { + m["action"] = ruleActionToJSON(r.Action) + } + + if r.Match != nil { + m["match"] = ruleMatchToJSON(r.Match) + } + + return m +} + +func ruleSummaryToJSON(r *RuleSummary) map[string]any { + return map[string]any{ + "arn": r.ARN, + "id": r.ID, + "name": r.Name, + "priority": r.Priority, + "isDefault": r.IsDefault, + } +} + +func ruleUpdateSuccessToJSON(r *RuleUpdateSuccess) map[string]any { + m := map[string]any{ + "arn": r.ARN, + "id": r.ID, + "name": r.Name, + "priority": r.Priority, + "isDefault": r.IsDefault, + } + + if r.Action != nil { + m["action"] = ruleActionToJSON(r.Action) + } + + if r.Match != nil { + m["match"] = ruleMatchToJSON(r.Match) + } + + return m +} + +func ruleActionToJSON(a *RuleAction) map[string]any { + if a.IsFixedResponse { + return map[string]any{ + "fixedResponse": map[string]any{ + "statusCode": a.FixedResponseStatusCode, + }, + } + } + + wts := make([]any, 0, len(a.ForwardTargetGroups)) + for _, w := range a.ForwardTargetGroups { + wts = append(wts, map[string]any{ + "targetGroupIdentifier": w.TargetGroupID, + "weight": w.Weight, + }) + } + + return map[string]any{ + "forward": map[string]any{ + "targetGroups": wts, + }, + } +} + +func ruleMatchToJSON(m *RuleMatch) map[string]any { + if m == nil { + return nil + } + + httpMatch := map[string]any{} + if m.HTTPMethod != "" { + httpMatch["method"] = m.HTTPMethod + } + + if m.PathMatchType != "" { + pathMatch := map[string]any{ + "match": map[string]any{ + m.PathMatchType: m.PathMatchValue, + }, + } + httpMatch["path"] = pathMatch + } + + if len(m.HeaderMatches) > 0 { + headers := make([]any, 0, len(m.HeaderMatches)) + for _, h := range m.HeaderMatches { + headers = append(headers, map[string]any{ + "name": h.Name, + "match": map[string]any{h.MatchType: h.MatchValue}, + }) + } + + httpMatch["headerMatches"] = headers + } + + return map[string]any{"httpMatch": httpMatch} +} + +func targetGroupToJSON(tg *TargetGroup) map[string]any { + m := map[string]any{ + "arn": tg.ARN, + "id": tg.ID, + "name": tg.Name, + "type": tg.Type, + "status": tg.Status, + "serviceArns": tg.ServiceARNs, + "createdAt": tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": tg.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + } + + if tg.Config != nil { + m["config"] = targetGroupConfigToJSON(tg.Config) + } + + return m +} + +func targetGroupSummaryToJSON(tg *TargetGroupSummary) map[string]any { + return map[string]any{ + "arn": tg.ARN, + "id": tg.ID, + "name": tg.Name, + "type": tg.Type, + "status": tg.Status, + "port": tg.Port, + "protocol": tg.Protocol, + "vpcId": tg.VpcID, + "serviceArns": tg.ServiceARNs, + "createdAt": tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func targetGroupConfigToJSON(c *TargetGroupConfig) map[string]any { + m := map[string]any{ + "port": c.Port, + "protocol": c.Protocol, + "protocolVersion": c.ProtocolVersion, + "vpcIdentifier": c.VpcID, + } + + if c.HealthCheck != nil { + m["healthCheck"] = healthCheckToJSON(c.HealthCheck) + } + + return m +} + +func healthCheckToJSON(hc *HealthCheckConfig) map[string]any { + return map[string]any{ + "enabled": hc.Enabled, + "protocol": hc.Protocol, + "path": hc.Path, + "port": hc.Port, + "healthyThresholdCount": hc.HealthyThresholdCount, + "unhealthyThresholdCount": hc.UnhealthyThresholdCount, + "healthCheckIntervalSeconds": hc.HealthCheckIntervalSeconds, + "healthCheckTimeoutSeconds": hc.HealthCheckTimeoutSeconds, + } +} + +func targetSummaryToJSON(t *TargetSummary) map[string]any { + return map[string]any{ + "id": t.ID, + "port": t.Port, + "status": t.Status, + "reasonCode": t.ReasonCode, + } +} + +func targetFailureToJSON(f *TargetFailure) map[string]any { + return map[string]any{ + "id": f.ID, + "port": f.Port, + "code": f.Code, + "message": f.Message, + } +} + +func alsToJSON(a *AccessLogSubscription) map[string]any { + return map[string]any{ + "arn": a.ARN, + "id": a.ID, + "resourceArn": a.ResourceARN, + "resourceId": a.ResourceID, + "destinationArn": a.DestinationARN, + "createdAt": a.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "lastUpdatedAt": a.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +func alsSummaryToJSON(a *AccessLogSubscriptionSummary) map[string]any { + return map[string]any{ + "arn": a.ARN, + "id": a.ID, + "resourceArn": a.ResourceARN, + "resourceId": a.ResourceID, + "destinationArn": a.DestinationARN, + "createdAt": a.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + } +} + +// ------- Body extraction helpers ------- + +func extractTags(body map[string]any) map[string]string { + tags := make(map[string]string) + + if t, ok := body["tags"].(map[string]any); ok { + for k, v := range t { + if s, ok := v.(string); ok { + tags[k] = s + } + } + } + + return tags +} + +func bodyInt32(body map[string]any, key string) int32 { + switch v := body[key].(type) { + case float64: + return int32(v) + case int: + return int32(v) + case int32: + return v + case int64: + return int32(v) + } + + return 0 +} + +func extractRuleAction(body map[string]any, key string) *RuleAction { + raw, ok := body[key].(map[string]any) + if !ok { + return nil + } + + action := &RuleAction{} + + if fr, ok := raw["fixedResponse"].(map[string]any); ok { + action.IsFixedResponse = true + action.FixedResponseStatusCode = bodyInt32(fr, "statusCode") + + return action + } + + if fwd, ok := raw["forward"].(map[string]any); ok { + if tgs, ok := fwd["targetGroups"].([]any); ok { + for _, tgRaw := range tgs { + if tgMap, ok := tgRaw.(map[string]any); ok { + wt := &WeightedTargetGroup{} + wt.TargetGroupID, _ = tgMap["targetGroupIdentifier"].(string) + wt.Weight = bodyInt32(tgMap, "weight") + action.ForwardTargetGroups = append(action.ForwardTargetGroups, wt) + } + } + } + } + + return action +} + +func extractRuleMatch(body map[string]any, key string) *RuleMatch { + raw, ok := body[key].(map[string]any) + if !ok { + return nil + } + + match := &RuleMatch{} + + if httpMatch, ok := raw["httpMatch"].(map[string]any); ok { + match.HTTPMethod, _ = httpMatch["method"].(string) + + if pathRaw, ok := httpMatch["path"].(map[string]any); ok { + if matchRaw, ok := pathRaw["match"].(map[string]any); ok { + for k, v := range matchRaw { + if s, ok := v.(string); ok { + match.PathMatchType = k + match.PathMatchValue = s + } + } + } + } + + if headersRaw, ok := httpMatch["headerMatches"].([]any); ok { + for _, hRaw := range headersRaw { + if hMap, ok := hRaw.(map[string]any); ok { + hm := &HeaderMatch{} + hm.Name, _ = hMap["name"].(string) + if matchRaw, ok := hMap["match"].(map[string]any); ok { + for k, v := range matchRaw { + if s, ok := v.(string); ok { + hm.MatchType = k + hm.MatchValue = s + } + } + } + + match.HeaderMatches = append(match.HeaderMatches, hm) + } + } + } + } + + return match +} + +func extractTargetGroupConfig(body map[string]any) *TargetGroupConfig { + raw, ok := body["config"].(map[string]any) + if !ok { + return nil + } + + cfg := &TargetGroupConfig{} + cfg.Port = bodyInt32(raw, "port") + cfg.Protocol, _ = raw["protocol"].(string) + cfg.ProtocolVersion, _ = raw["protocolVersion"].(string) + cfg.VpcID, _ = raw["vpcIdentifier"].(string) + cfg.IPAddressType, _ = raw["ipAddressType"].(string) + cfg.LambdaEventStructureVersion, _ = raw["lambdaEventStructureVersion"].(string) + + if hcRaw, ok := raw["healthCheck"].(map[string]any); ok { + cfg.HealthCheck = extractHealthCheckConfig(hcRaw) + } + + return cfg +} + +func extractHealthCheckConfig(raw map[string]any) *HealthCheckConfig { + hc := &HealthCheckConfig{} + if v, ok := raw["enabled"].(bool); ok { + hc.Enabled = v + } + + hc.Protocol, _ = raw["protocol"].(string) + hc.ProtocolVersion, _ = raw["protocolVersion"].(string) + hc.Path, _ = raw["path"].(string) + hc.Port = bodyInt32(raw, "port") + hc.HealthyThresholdCount = bodyInt32(raw, "healthyThresholdCount") + hc.UnhealthyThresholdCount = bodyInt32(raw, "unhealthyThresholdCount") + hc.HealthCheckIntervalSeconds = bodyInt32(raw, "healthCheckIntervalSeconds") + hc.HealthCheckTimeoutSeconds = bodyInt32(raw, "healthCheckTimeoutSeconds") + + return hc +} + +func extractTargets(body map[string]any) []*Target { + var targets []*Target + + if raw, ok := body["targets"].([]any); ok { + for _, tRaw := range raw { + if tMap, ok := tRaw.(map[string]any); ok { + t := &Target{} + t.ID, _ = tMap["id"].(string) + t.Port = bodyInt32(tMap, "port") + targets = append(targets, t) + } + } + } + + return targets +} + +func queryInt32(c *echo.Context, key string, fallback int32) int32 { + v := c.QueryParam(key) + if v == "" { + return fallback + } + + n, err := strconv.ParseInt(v, 10, 32) + if err != nil { + return fallback + } + + return int32(n) +} diff --git a/services/vpclattice/handler_test.go b/services/vpclattice/handler_test.go new file mode 100644 index 000000000..e83d36cec --- /dev/null +++ b/services/vpclattice/handler_test.go @@ -0,0 +1,799 @@ +package vpclattice_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/blackbirdworks/gopherstack/services/vpclattice" +) + +func newTestHandler(t *testing.T) *vpclattice.Handler { + t.Helper() + backend := vpclattice.NewInMemoryBackend("000000000000", "us-east-1") + + return vpclattice.NewHandler(backend) +} + +func doRequest(t *testing.T, h *vpclattice.Handler, method, path string, body any) *httptest.ResponseRecorder { + t.Helper() + + var buf *bytes.Reader + if body != nil { + data, err := json.Marshal(body) + require.NoError(t, err) + buf = bytes.NewReader(data) + } else { + buf = bytes.NewReader(nil) + } + + req := httptest.NewRequest(method, path, buf) + if body != nil { + req.Header.Set("Content-Type", "application/json") + req.ContentLength = int64(buf.Len()) + } + + rec := httptest.NewRecorder() + e := echo.New() + c := e.NewContext(req, rec) + require.NoError(t, h.Handler()(c)) + + return rec +} + +func parseBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]any { + t.Helper() + var m map[string]any + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &m)) + + return m +} + +// TestService_CRUD tests create/get/update/delete/list for services. +func TestService_CRUD(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + body map[string]any + wantCode int + check func(t *testing.T, resp map[string]any) + }{ + { + name: "create missing name returns 400", + body: map[string]any{}, + wantCode: http.StatusBadRequest, + }, + { + name: "create with name returns 201", + body: map[string]any{"name": "my-svc"}, + wantCode: http.StatusCreated, + check: func(t *testing.T, resp map[string]any) { + t.Helper() + assert.Contains(t, resp["arn"], "arn:aws:vpc-lattice:us-east-1:000000000000:service/svc-") + assert.Equal(t, "my-svc", resp["name"]) + assert.Equal(t, "ACTIVE", resp["status"]) + assert.NotEmpty(t, resp["id"]) + }, + }, + { + name: "create duplicate name returns 409", + body: map[string]any{"name": "dup-svc"}, + wantCode: http.StatusCreated, + }, + } + + h := newTestHandler(t) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + rec := doRequest(t, h, http.MethodPost, "/services", tc.body) + assert.Equal(t, tc.wantCode, rec.Code) + if tc.check != nil { + tc.check(t, parseBody(t, rec)) + } + }) + } +} + +func TestService_DuplicateName(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + rec := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "dup"}) + require.Equal(t, http.StatusCreated, rec.Code) + + rec2 := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "dup"}) + assert.Equal(t, http.StatusConflict, rec2.Code) +} + +func TestService_GetUpdateDelete(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create + rec := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "svc1"}) + require.Equal(t, http.StatusCreated, rec.Code) + created := parseBody(t, rec) + id, _ := created["id"].(string) + require.NotEmpty(t, id) + + // get by id + rec = doRequest(t, h, http.MethodGet, "/services/"+id, nil) + assert.Equal(t, http.StatusOK, rec.Code) + got := parseBody(t, rec) + assert.Equal(t, "svc1", got["name"]) + + // get not found + rec = doRequest(t, h, http.MethodGet, "/services/svc-notexist", nil) + assert.Equal(t, http.StatusNotFound, rec.Code) + + // update + rec = doRequest(t, h, http.MethodPatch, "/services/"+id, map[string]any{"authType": "AWS_IAM"}) + assert.Equal(t, http.StatusOK, rec.Code) + updated := parseBody(t, rec) + assert.Equal(t, "AWS_IAM", updated["authType"]) + + // list + rec = doRequest(t, h, http.MethodGet, "/services", nil) + assert.Equal(t, http.StatusOK, rec.Code) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 1) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/services/"+id, nil) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, 0, vpclattice.ServiceCount(h.Backend.(*vpclattice.InMemoryBackend))) + + // get after delete returns 404 + rec = doRequest(t, h, http.MethodGet, "/services/"+id, nil) + assert.Equal(t, http.StatusNotFound, rec.Code) +} + +// TestServiceNetwork_CRUD tests service networks. +func TestServiceNetwork_CRUD(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create + rec := doRequest(t, h, http.MethodPost, "/servicenetworks", map[string]any{"name": "sn1"}) + require.Equal(t, http.StatusCreated, rec.Code) + created := parseBody(t, rec) + id, _ := created["id"].(string) + require.NotEmpty(t, id) + assert.Contains(t, created["arn"], "arn:aws:vpc-lattice:us-east-1:000000000000:servicenetwork/sn-") + + // get + rec = doRequest(t, h, http.MethodGet, "/servicenetworks/"+id, nil) + assert.Equal(t, http.StatusOK, rec.Code) + got := parseBody(t, rec) + assert.Equal(t, "sn1", got["name"]) + + // update + rec = doRequest(t, h, http.MethodPatch, "/servicenetworks/"+id, map[string]any{"authType": "AWS_IAM"}) + assert.Equal(t, http.StatusOK, rec.Code) + + // list + rec = doRequest(t, h, http.MethodGet, "/servicenetworks", nil) + assert.Equal(t, http.StatusOK, rec.Code) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 1) + assert.Equal(t, 1, vpclattice.ServiceNetworkCount(h.Backend.(*vpclattice.InMemoryBackend))) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/servicenetworks/"+id, nil) + assert.Equal(t, http.StatusNoContent, rec.Code) + assert.Equal(t, 0, vpclattice.ServiceNetworkCount(h.Backend.(*vpclattice.InMemoryBackend))) +} + +// TestSNSA_CRUD tests service network service associations. +func TestSNSA_CRUD(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create prerequisite resources + recSN := doRequest(t, h, http.MethodPost, "/servicenetworks", map[string]any{"name": "net1"}) + require.Equal(t, http.StatusCreated, recSN.Code) + snID, _ := parseBody(t, recSN)["id"].(string) + + recSvc := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "svc1"}) + require.Equal(t, http.StatusCreated, recSvc.Code) + svcID, _ := parseBody(t, recSvc)["id"].(string) + + // create association + rec := doRequest(t, h, http.MethodPost, "/servicenetworkserviceassociations", map[string]any{ + "serviceNetworkIdentifier": snID, + "serviceIdentifier": svcID, + }) + require.Equal(t, http.StatusCreated, rec.Code) + assoc := parseBody(t, rec) + assocID, _ := assoc["id"].(string) + require.NotEmpty(t, assocID) + assert.Equal(t, "ACTIVE", assoc["status"]) + + // get + rec = doRequest(t, h, http.MethodGet, "/servicenetworkserviceassociations/"+assocID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + + // list + rec = doRequest(t, h, http.MethodGet, "/servicenetworkserviceassociations", nil) + assert.Equal(t, http.StatusOK, rec.Code) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 1) + + // duplicate association returns conflict + rec = doRequest(t, h, http.MethodPost, "/servicenetworkserviceassociations", map[string]any{ + "serviceNetworkIdentifier": snID, + "serviceIdentifier": svcID, + }) + assert.Equal(t, http.StatusConflict, rec.Code) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/servicenetworkserviceassociations/"+assocID, nil) + assert.Equal(t, http.StatusOK, rec.Code) +} + +// TestSNVA_CRUD tests service network VPC associations. +func TestSNVA_CRUD(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create service network + recSN := doRequest(t, h, http.MethodPost, "/servicenetworks", map[string]any{"name": "net2"}) + require.Equal(t, http.StatusCreated, recSN.Code) + snID, _ := parseBody(t, recSN)["id"].(string) + + // create + rec := doRequest(t, h, http.MethodPost, "/servicenetworkvpcassociations", map[string]any{ + "serviceNetworkIdentifier": snID, + "vpcIdentifier": "vpc-1234567890", + "securityGroupIds": []string{"sg-abcdef01"}, + }) + require.Equal(t, http.StatusCreated, rec.Code) + assoc := parseBody(t, rec) + assocID, _ := assoc["id"].(string) + assert.NotEmpty(t, assocID) + assert.Equal(t, "ACTIVE", assoc["status"]) + + // get + rec = doRequest(t, h, http.MethodGet, "/servicenetworkvpcassociations/"+assocID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + + // update security groups + rec = doRequest(t, h, http.MethodPatch, "/servicenetworkvpcassociations/"+assocID, map[string]any{ + "securityGroupIds": []string{"sg-new"}, + }) + assert.Equal(t, http.StatusOK, rec.Code) + + // list + rec = doRequest(t, h, http.MethodGet, "/servicenetworkvpcassociations", nil) + assert.Equal(t, http.StatusOK, rec.Code) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 1) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/servicenetworkvpcassociations/"+assocID, nil) + assert.Equal(t, http.StatusOK, rec.Code) +} + +// TestListener_CRUD tests listeners. +func TestListener_CRUD(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create service + recSvc := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "svc-l"}) + require.Equal(t, http.StatusCreated, recSvc.Code) + svcID, _ := parseBody(t, recSvc)["id"].(string) + + // create listener + rec := doRequest(t, h, http.MethodPost, "/services/"+svcID+"/listeners", map[string]any{ + "name": "my-listener", + "protocol": "HTTP", + "port": 80, + "defaultAction": map[string]any{ + "fixedResponse": map[string]any{"statusCode": 404}, + }, + }) + require.Equal(t, http.StatusCreated, rec.Code) + l := parseBody(t, rec) + listenerID, _ := l["id"].(string) + require.NotEmpty(t, listenerID) + assert.Equal(t, "my-listener", l["name"]) + assert.Equal(t, float64(80), l["port"]) + + // get + rec = doRequest(t, h, http.MethodGet, "/services/"+svcID+"/listeners/"+listenerID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + + // update + rec = doRequest(t, h, http.MethodPatch, "/services/"+svcID+"/listeners/"+listenerID, map[string]any{ + "defaultAction": map[string]any{ + "fixedResponse": map[string]any{"statusCode": 200}, + }, + }) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, 1, vpclattice.ListenerCount(h.Backend.(*vpclattice.InMemoryBackend))) + + // list + rec = doRequest(t, h, http.MethodGet, "/services/"+svcID+"/listeners", nil) + assert.Equal(t, http.StatusOK, rec.Code) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 1) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/services/"+svcID+"/listeners/"+listenerID, nil) + assert.Equal(t, http.StatusNoContent, rec.Code) + assert.Equal(t, 0, vpclattice.ListenerCount(h.Backend.(*vpclattice.InMemoryBackend))) +} + +// TestRule_CRUD tests listener rules. +func TestRule_CRUD(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // setup + recSvc := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "svc-r"}) + require.Equal(t, http.StatusCreated, recSvc.Code) + svcID, _ := parseBody(t, recSvc)["id"].(string) + + recL := doRequest(t, h, http.MethodPost, "/services/"+svcID+"/listeners", map[string]any{ + "name": "l1", + "protocol": "HTTP", + "defaultAction": map[string]any{ + "fixedResponse": map[string]any{"statusCode": 404}, + }, + }) + require.Equal(t, http.StatusCreated, recL.Code) + listenerID, _ := parseBody(t, recL)["id"].(string) + + // create target group for forward rule + recTG := doRequest(t, h, http.MethodPost, "/targetgroups", map[string]any{ + "name": "tg1", + "type": "INSTANCE", + "config": map[string]any{ + "protocol": "HTTP", + "port": 80, + "vpcIdentifier": "vpc-123", + }, + }) + require.Equal(t, http.StatusCreated, recTG.Code) + tgID, _ := parseBody(t, recTG)["id"].(string) + + // create rule with forward action + rec := doRequest(t, h, http.MethodPost, "/services/"+svcID+"/listeners/"+listenerID+"/rules", map[string]any{ + "name": "rule1", + "priority": 10, + "action": map[string]any{ + "forward": map[string]any{ + "targetGroups": []any{ + map[string]any{"targetGroupIdentifier": tgID, "weight": 100}, + }, + }, + }, + "match": map[string]any{ + "httpMatch": map[string]any{ + "method": "GET", + "path": map[string]any{ + "match": map[string]any{"exact": "/api"}, + }, + }, + }, + }) + require.Equal(t, http.StatusCreated, rec.Code) + rule := parseBody(t, rec) + ruleID, _ := rule["id"].(string) + require.NotEmpty(t, ruleID) + assert.Equal(t, float64(10), rule["priority"]) + + // get + rec = doRequest(t, h, http.MethodGet, "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + + // list (includes default rule) + rec = doRequest(t, h, http.MethodGet, "/services/"+svcID+"/listeners/"+listenerID+"/rules", nil) + assert.Equal(t, http.StatusOK, rec.Code) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 2) // default + created + + // update + rec = doRequest(t, h, http.MethodPatch, "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, map[string]any{ + "priority": 20, + }) + assert.Equal(t, http.StatusOK, rec.Code) + updated := parseBody(t, rec) + assert.Equal(t, float64(20), updated["priority"]) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, nil) + assert.Equal(t, http.StatusNoContent, rec.Code) + + // list now has only default rule + rec = doRequest(t, h, http.MethodGet, "/services/"+svcID+"/listeners/"+listenerID+"/rules", nil) + list = parseBody(t, rec) + items, _ = list["items"].([]any) + assert.Len(t, items, 1) +} + +// TestBatchUpdateRule tests batch rule updates. +func TestBatchUpdateRule(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // setup + recSvc := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "svc-bu"}) + require.Equal(t, http.StatusCreated, recSvc.Code) + svcID, _ := parseBody(t, recSvc)["id"].(string) + + recL := doRequest(t, h, http.MethodPost, "/services/"+svcID+"/listeners", map[string]any{ + "name": "l-bu", + "protocol": "HTTP", + "defaultAction": map[string]any{ + "fixedResponse": map[string]any{"statusCode": 404}, + }, + }) + require.Equal(t, http.StatusCreated, recL.Code) + listenerID, _ := parseBody(t, recL)["id"].(string) + + rec := doRequest(t, h, http.MethodPost, "/services/"+svcID+"/listeners/"+listenerID+"/rules", map[string]any{ + "name": "r1", + "priority": 10, + "action": map[string]any{"fixedResponse": map[string]any{"statusCode": 200}}, + }) + require.Equal(t, http.StatusCreated, rec.Code) + ruleID, _ := parseBody(t, rec)["id"].(string) + + // batch update + rec = doRequest(t, h, http.MethodPatch, "/services/"+svcID+"/listeners/"+listenerID+"/rules", map[string]any{ + "rules": []any{ + map[string]any{"ruleIdentifier": ruleID, "priority": 50}, + map[string]any{"ruleIdentifier": "rule-notexist", "priority": 99}, + }, + }) + assert.Equal(t, http.StatusOK, rec.Code) + resp := parseBody(t, rec) + successful, _ := resp["successful"].([]any) + unsuccessful, _ := resp["unsuccessful"].([]any) + assert.Len(t, successful, 1) + assert.Len(t, unsuccessful, 1) +} + +// TestTargetGroup_CRUD tests target groups. +func TestTargetGroup_CRUD(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + tests := []struct { + name string + body map[string]any + wantCode int + check func(t *testing.T, resp map[string]any) + }{ + { + name: "create missing name returns 400", + body: map[string]any{"type": "INSTANCE"}, + wantCode: http.StatusBadRequest, + }, + { + name: "create instance target group returns 201", + body: map[string]any{ + "name": "tg-inst", + "type": "INSTANCE", + "config": map[string]any{ + "protocol": "HTTP", + "port": 8080, + "vpcIdentifier": "vpc-abc", + }, + }, + wantCode: http.StatusCreated, + check: func(t *testing.T, resp map[string]any) { + t.Helper() + assert.Contains(t, resp["arn"], ":targetgroup/tg-") + assert.Equal(t, "ACTIVE", resp["status"]) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + h2 := newTestHandler(t) + rec := doRequest(t, h2, http.MethodPost, "/targetgroups", tc.body) + assert.Equal(t, tc.wantCode, rec.Code) + if tc.check != nil { + tc.check(t, parseBody(t, rec)) + } + }) + } + + // full CRUD test + rec := doRequest(t, h, http.MethodPost, "/targetgroups", map[string]any{ + "name": "tg-full", + "type": "IP", + "config": map[string]any{ + "protocol": "HTTPS", + "port": 443, + "vpcIdentifier": "vpc-xyz", + }, + }) + require.Equal(t, http.StatusCreated, rec.Code) + tg := parseBody(t, rec) + tgID, _ := tg["id"].(string) + require.NotEmpty(t, tgID) + assert.Equal(t, 1, vpclattice.TargetGroupCount(h.Backend.(*vpclattice.InMemoryBackend))) + + // get + rec = doRequest(t, h, http.MethodGet, "/targetgroups/"+tgID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + + // list + rec = doRequest(t, h, http.MethodGet, "/targetgroups", nil) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 1) + + // update health check + rec = doRequest(t, h, http.MethodPatch, "/targetgroups/"+tgID, map[string]any{ + "healthCheck": map[string]any{ + "enabled": true, + "protocol": "HTTP", + "path": "/health", + "port": 8080, + }, + }) + assert.Equal(t, http.StatusOK, rec.Code) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/targetgroups/"+tgID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, 0, vpclattice.TargetGroupCount(h.Backend.(*vpclattice.InMemoryBackend))) +} + +// TestTargets tests register/deregister/list targets. +func TestTargets(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + rec := doRequest(t, h, http.MethodPost, "/targetgroups", map[string]any{ + "name": "tg-targets", + "type": "IP", + "config": map[string]any{"protocol": "HTTP", "port": 80, "vpcIdentifier": "vpc-1"}, + }) + require.Equal(t, http.StatusCreated, rec.Code) + tgID, _ := parseBody(t, rec)["id"].(string) + + // register + rec = doRequest(t, h, http.MethodPost, "/targetgroups/"+tgID+"/registertargets", map[string]any{ + "targets": []any{ + map[string]any{"id": "10.0.0.1", "port": 80}, + map[string]any{"id": "10.0.0.2", "port": 80}, + }, + }) + assert.Equal(t, http.StatusOK, rec.Code) + resp := parseBody(t, rec) + unsuccessful, _ := resp["unsuccessful"].([]any) + assert.Empty(t, unsuccessful) + + // list + rec = doRequest(t, h, http.MethodPost, "/targetgroups/"+tgID+"/listtargets", map[string]any{}) + assert.Equal(t, http.StatusOK, rec.Code) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 2) + + // deregister one + rec = doRequest(t, h, http.MethodPost, "/targetgroups/"+tgID+"/deregistertargets", map[string]any{ + "targets": []any{ + map[string]any{"id": "10.0.0.1", "port": 80}, + }, + }) + assert.Equal(t, http.StatusOK, rec.Code) + + // list after deregister + rec = doRequest(t, h, http.MethodPost, "/targetgroups/"+tgID+"/listtargets", map[string]any{}) + list = parseBody(t, rec) + items, _ = list["items"].([]any) + assert.Len(t, items, 1) +} + +// TestAccessLogSubscription_CRUD tests access log subscriptions. +func TestAccessLogSubscription_CRUD(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create service for resource + recSvc := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "svc-als"}) + require.Equal(t, http.StatusCreated, recSvc.Code) + svcID, _ := parseBody(t, recSvc)["id"].(string) + + // create ALS + destArn := "arn:aws:logs:us-east-1:000000000000:log-group:/vpc-lattice/test" + rec := doRequest(t, h, http.MethodPost, "/accesslogsubscriptions", map[string]any{ + "resourceIdentifier": svcID, + "destinationArn": destArn, + }) + require.Equal(t, http.StatusCreated, rec.Code) + als := parseBody(t, rec) + alsID, _ := als["id"].(string) + require.NotEmpty(t, alsID) + assert.Equal(t, destArn, als["destinationArn"]) + + // get + rec = doRequest(t, h, http.MethodGet, "/accesslogsubscriptions/"+alsID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + + // update + newDest := "arn:aws:logs:us-east-1:000000000000:log-group:/vpc-lattice/new" + rec = doRequest(t, h, http.MethodPatch, "/accesslogsubscriptions/"+alsID, map[string]any{ + "destinationArn": newDest, + }) + assert.Equal(t, http.StatusOK, rec.Code) + updated := parseBody(t, rec) + assert.Equal(t, newDest, updated["destinationArn"]) + + // list + rec = doRequest(t, h, http.MethodGet, "/accesslogsubscriptions", nil) + list := parseBody(t, rec) + items, _ := list["items"].([]any) + assert.Len(t, items, 1) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/accesslogsubscriptions/"+alsID, nil) + assert.Equal(t, http.StatusNoContent, rec.Code) +} + +// TestAuthPolicy tests put/get/delete auth policy. +func TestAuthPolicy(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create service + recSvc := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "svc-auth"}) + require.Equal(t, http.StatusCreated, recSvc.Code) + svcID, _ := parseBody(t, recSvc)["id"].(string) + + policy := `{"Version":"2012-10-17","Statement":[]}` + + // put + rec := doRequest(t, h, http.MethodPut, "/authpolicy/"+svcID, map[string]any{"policy": policy}) + assert.Equal(t, http.StatusOK, rec.Code) + resp := parseBody(t, rec) + assert.Equal(t, policy, resp["policy"]) + + // get + rec = doRequest(t, h, http.MethodGet, "/authpolicy/"+svcID, nil) + assert.Equal(t, http.StatusOK, rec.Code) + resp = parseBody(t, rec) + assert.Equal(t, policy, resp["policy"]) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/authpolicy/"+svcID, nil) + assert.Equal(t, http.StatusNoContent, rec.Code) +} + +// TestResourcePolicy tests put/get/delete resource policy. +func TestResourcePolicy(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + resArn := "arn:aws:vpc-lattice:us-east-1:000000000000:servicenetwork/sn-abc123" + policy := `{"Version":"2012-10-17","Statement":[]}` + + // put + rec := doRequest(t, h, http.MethodPut, "/resourcepolicy/"+resArn, map[string]any{"policy": policy}) + assert.Equal(t, http.StatusOK, rec.Code) + + // get + rec = doRequest(t, h, http.MethodGet, "/resourcepolicy/"+resArn, nil) + assert.Equal(t, http.StatusOK, rec.Code) + resp := parseBody(t, rec) + assert.Equal(t, policy, resp["policy"]) + + // delete + rec = doRequest(t, h, http.MethodDelete, "/resourcepolicy/"+resArn, nil) + assert.Equal(t, http.StatusNoContent, rec.Code) + + // get after delete returns 404 + rec = doRequest(t, h, http.MethodGet, "/resourcepolicy/"+resArn, nil) + assert.Equal(t, http.StatusNotFound, rec.Code) +} + +// TestTagging tests tagging operations. +func TestTagging(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + // create a service to tag + recSvc := doRequest(t, h, http.MethodPost, "/services", map[string]any{ + "name": "svc-tag", + "tags": map[string]any{"env": "dev"}, + }) + require.Equal(t, http.StatusCreated, recSvc.Code) + svcData := parseBody(t, recSvc) + svcArn, _ := svcData["arn"].(string) + + // list tags + rec := doRequest(t, h, http.MethodGet, "/tags/"+svcArn, nil) + assert.Equal(t, http.StatusOK, rec.Code) + tagsResp := parseBody(t, rec) + tags, _ := tagsResp["tags"].(map[string]any) + assert.Equal(t, "dev", tags["env"]) + + // tag resource + rec = doRequest(t, h, http.MethodPost, "/tags/"+svcArn, map[string]any{ + "tags": map[string]any{"team": "platform"}, + }) + assert.Equal(t, http.StatusOK, rec.Code) + + // verify tag was added + rec = doRequest(t, h, http.MethodGet, "/tags/"+svcArn, nil) + tagsResp = parseBody(t, rec) + tags, _ = tagsResp["tags"].(map[string]any) + assert.Equal(t, "dev", tags["env"]) + assert.Equal(t, "platform", tags["team"]) +} + +// TestARNFormat verifies ARN formats for all resource types. +func TestARNFormat(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + rec := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "arn-svc"}) + require.Equal(t, http.StatusCreated, rec.Code) + svc := parseBody(t, rec) + assert.Regexp(t, `^arn:aws:vpc-lattice:us-east-1:000000000000:service/svc-[a-f0-9]+$`, svc["arn"]) + + rec = doRequest(t, h, http.MethodPost, "/servicenetworks", map[string]any{"name": "arn-sn"}) + require.Equal(t, http.StatusCreated, rec.Code) + sn := parseBody(t, rec) + assert.Regexp(t, `^arn:aws:vpc-lattice:us-east-1:000000000000:servicenetwork/sn-[a-f0-9]+$`, sn["arn"]) + + rec = doRequest(t, h, http.MethodPost, "/targetgroups", map[string]any{ + "name": "arn-tg", + "type": "IP", + "config": map[string]any{"protocol": "HTTP", "port": 80, "vpcIdentifier": "vpc-1"}, + }) + require.Equal(t, http.StatusCreated, rec.Code) + tg := parseBody(t, rec) + assert.Regexp(t, `^arn:aws:vpc-lattice:us-east-1:000000000000:targetgroup/tg-[a-f0-9]+$`, tg["arn"]) +} + +// TestNotFound verifies 404 on get/update/delete of nonexistent resources. +func TestNotFound(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + method string + path string + }{ + {"get service", http.MethodGet, "/services/svc-notexist"}, + {"get servicenetwork", http.MethodGet, "/servicenetworks/sn-notexist"}, + {"get targetgroup", http.MethodGet, "/targetgroups/tg-notexist"}, + {"get snsa", http.MethodGet, "/servicenetworkserviceassociations/snsa-notexist"}, + {"get snva", http.MethodGet, "/servicenetworkvpcassociations/snva-notexist"}, + {"get als", http.MethodGet, "/accesslogsubscriptions/als-notexist"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + rec := doRequest(t, h, tc.method, tc.path, nil) + assert.Equal(t, http.StatusNotFound, rec.Code) + }) + } +} diff --git a/services/vpclattice/provider.go b/services/vpclattice/provider.go new file mode 100644 index 000000000..436f99370 --- /dev/null +++ b/services/vpclattice/provider.go @@ -0,0 +1,40 @@ +package vpclattice + +import ( + "errors" + + "github.com/blackbirdworks/gopherstack/pkgs/config" + "github.com/blackbirdworks/gopherstack/pkgs/service" +) + +// ErrNilAppContext is returned when Provider.Init is called with a nil AppContext. +var ErrNilAppContext = errors.New("AppContext is required") + +// Provider implements service.Provider for the VPC Lattice service. +type Provider struct{} + +// Name returns the logical name of the provider. +func (p *Provider) Name() string { return "VPCLattice" } + +// Init initializes the VPC Lattice backend and handler. +// +//nolint:ireturn,nolintlint // architecturally required to return interface +func (p *Provider) Init(ctx *service.AppContext) (service.Registerable, error) { + if ctx == nil { + return nil, ErrNilAppContext + } + + accountID := config.DefaultAccountID + region := config.DefaultRegion + + if cp, ok := ctx.Config.(config.Provider); ok { + cfg := cp.GetGlobalConfig() + accountID = cfg.GetAccountID() + region = cfg.GetRegion() + } + + backend := NewInMemoryBackend(accountID, region) + handler := NewHandler(backend) + + return handler, nil +} diff --git a/services/vpclattice/sdk_completeness_test.go b/services/vpclattice/sdk_completeness_test.go new file mode 100644 index 000000000..4bf0d0f1d --- /dev/null +++ b/services/vpclattice/sdk_completeness_test.go @@ -0,0 +1,50 @@ +package vpclattice_test + +import ( + "testing" + + vpclatticesdk "github.com/aws/aws-sdk-go-v2/service/vpclattice" + + "github.com/blackbirdworks/gopherstack/pkgs/sdkcheck" + "github.com/blackbirdworks/gopherstack/services/vpclattice" +) + +// TestSDKCompleteness verifies that every operation exposed by the AWS SDK v2 +// vpclattice client is either listed in GetSupportedOperations() or explicitly +// acknowledged in the notImplemented slice. +func TestSDKCompleteness(t *testing.T) { + t.Parallel() + + backend := vpclattice.NewInMemoryBackend("000000000000", "us-east-1") + h := vpclattice.NewHandler(backend) + + notImplemented := []string{ + // Resource Gateway and Configuration (newer feature set) + "CreateResourceConfiguration", + "CreateResourceGateway", + "DeleteResourceConfiguration", + "DeleteResourceEndpointAssociation", + "DeleteResourceGateway", + "GetResourceConfiguration", + "GetResourceGateway", + "ListResourceConfigurations", + "ListResourceEndpointAssociations", + "ListResourceGateways", + "UpdateResourceConfiguration", + "UpdateResourceGateway", + // ServiceNetworkResourceAssociation (resource gateway feature) + "CreateServiceNetworkResourceAssociation", + "DeleteServiceNetworkResourceAssociation", + "GetServiceNetworkResourceAssociation", + "ListServiceNetworkResourceAssociations", + // VPC endpoint associations + "ListServiceNetworkVpcEndpointAssociations", + // Domain verification + "DeleteDomainVerification", + "GetDomainVerification", + "ListDomainVerifications", + "StartDomainVerification", + } + + sdkcheck.CheckCompleteness(t, &vpclatticesdk.Client{}, h.GetSupportedOperations(), notImplemented) +} diff --git a/test/terraform/terraform_test.go b/test/terraform/terraform_test.go index 2e51dfb37..dd85498c2 100644 --- a/test/terraform/terraform_test.go +++ b/test/terraform/terraform_test.go @@ -311,6 +311,7 @@ provider "aws" { ssoadmin = %[1]q sts = %[1]q swf = %[1]q + vpclattice = %[1]q wafv2 = %[1]q } } From b1861f7452229fcef162421f4bf1d5112a646ea6 Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 21:36:18 -0500 Subject: [PATCH 3/7] WIP: checkpoint (auto) --- services/vpclattice/backend.go | 364 ++++++++++++------ services/vpclattice/handler.go | 150 ++++++-- services/vpclattice/handler_test.go | 207 +++++++--- services/vpclattice/interfaces.go | 172 ++++++--- services/vpclattice/sdk_completeness_test.go | 7 +- test/terraform/fixtures/vpclattice/success.tf | 29 ++ test/terraform/main_test.go | 8 + test/terraform/terraform_test.go | 22 ++ 8 files changed, 686 insertions(+), 273 deletions(-) create mode 100644 test/terraform/fixtures/vpclattice/success.tf diff --git a/services/vpclattice/backend.go b/services/vpclattice/backend.go index 9fc4a6bac..414cf8870 100644 --- a/services/vpclattice/backend.go +++ b/services/vpclattice/backend.go @@ -17,39 +17,39 @@ import ( ) const ( - arnService = "vpc-lattice" - resourceService = "service" - resourceServiceNetwork = "servicenetwork" - resourceServiceNetworkSvcAssoc = "servicenetworkserviceassociation" - resourceServiceNetworkVpcAssoc = "servicenetworkvpcassociation" - resourceListener = "listener" - resourceRule = "rule" - resourceTargetGroup = "targetgroup" - resourceAccessLogSubscription = "accesslogsubscription" - - idPrefixService = "svc-" - idPrefixNetwork = "sn-" - idPrefixSNSA = "snsa-" - idPrefixSNVA = "snva-" - idPrefixListener = "listener-" - idPrefixRule = "rule-" - idPrefixTargetGroup = "tg-" - idPrefixALS = "als-" - - statusActive = "ACTIVE" - statusInactive = "INACTIVE" + arnService = "vpc-lattice" + resourceService = "service" + resourceServiceNetwork = "servicenetwork" + resourceServiceNetworkSvcAssoc = "servicenetworkserviceassociation" + resourceServiceNetworkVpcAssoc = "servicenetworkvpcassociation" + resourceListener = "listener" + resourceRule = "rule" + resourceTargetGroup = "targetgroup" + resourceAccessLogSubscription = "accesslogsubscription" + + idPrefixService = "svc-" + idPrefixNetwork = "sn-" + idPrefixSNSA = "snsa-" + idPrefixSNVA = "snva-" + idPrefixListener = "listener-" + idPrefixRule = "rule-" + idPrefixTargetGroup = "tg-" + idPrefixALS = "als-" + + statusActive = "ACTIVE" + statusInactive = "INACTIVE" statusCreateInProgress = "CREATE_IN_PROGRESS" statusDeleteInProgress = "DELETE_IN_PROGRESS" - statusDeleted = "DELETED" - statusCreateFailed = "CREATE_FAILED" + statusDeleted = "DELETED" + statusCreateFailed = "CREATE_FAILED" - authTypeNone = "NONE" - protocolHTTP = "HTTP" - protocolHTTPS = "HTTPS" + authTypeNone = "NONE" + protocolHTTP = "HTTP" + protocolHTTPS = "HTTPS" tgStatusActive = "ACTIVE" - targetStatusHealthy = "HEALTHY" + targetStatusHealthy = "HEALTHY" defaultMaxResults = 100 ) @@ -108,38 +108,38 @@ func (s *storedService) toSummary() *ServiceSummary { // storedServiceNetwork holds a service network. type storedServiceNetwork struct { - CreatedAt time.Time `json:"createdAt"` - LastUpdatedAt time.Time `json:"lastUpdatedAt"` - Tags map[string]string `json:"tags"` - ARN string `json:"arn"` - ID string `json:"id"` - Name string `json:"name"` - AuthType string `json:"authType"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + Name string `json:"name"` + AuthType string `json:"authType"` NumberOfAssociatedServices int64 `json:"numberOfAssociatedServices"` - NumberOfAssociatedVPCs int64 `json:"numberOfAssociatedVpcs"` + NumberOfAssociatedVPCs int64 `json:"numberOfAssociatedVpcs"` } func (s *storedServiceNetwork) toServiceNetwork() *ServiceNetwork { return &ServiceNetwork{ - ARN: s.ARN, - ID: s.ID, - Name: s.Name, - AuthType: s.AuthType, + ARN: s.ARN, + ID: s.ID, + Name: s.Name, + AuthType: s.AuthType, NumberOfAssociatedServices: s.NumberOfAssociatedServices, - NumberOfAssociatedVPCs: s.NumberOfAssociatedVPCs, - CreatedAt: s.CreatedAt, - LastUpdatedAt: s.LastUpdatedAt, + NumberOfAssociatedVPCs: s.NumberOfAssociatedVPCs, + CreatedAt: s.CreatedAt, + LastUpdatedAt: s.LastUpdatedAt, } } func (s *storedServiceNetwork) toSummary() *ServiceNetworkSummary { return &ServiceNetworkSummary{ - ARN: s.ARN, - ID: s.ID, - Name: s.Name, + ARN: s.ARN, + ID: s.ID, + Name: s.Name, NumberOfAssociatedServices: s.NumberOfAssociatedServices, - NumberOfAssociatedVPCs: s.NumberOfAssociatedVPCs, - CreatedAt: s.CreatedAt, + NumberOfAssociatedVPCs: s.NumberOfAssociatedVPCs, + CreatedAt: s.CreatedAt, } } @@ -328,15 +328,15 @@ func (r *storedRule) toSummary() *RuleSummary { // storedTargetGroup holds a target group. type storedTargetGroup struct { - CreatedAt time.Time `json:"createdAt"` - LastUpdatedAt time.Time `json:"lastUpdatedAt"` - Tags map[string]string `json:"tags"` - ServiceARNs []string `json:"serviceArns"` - ARN string `json:"arn"` - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Status string `json:"status"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ServiceARNs []string `json:"serviceArns"` + ARN string `json:"arn"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` Config *TargetGroupConfig `json:"config"` } @@ -387,27 +387,27 @@ type storedTarget struct { // storedALS holds an access log subscription. type storedALS struct { - CreatedAt time.Time `json:"createdAt"` - LastUpdatedAt time.Time `json:"lastUpdatedAt"` - Tags map[string]string `json:"tags"` - ARN string `json:"arn"` - ID string `json:"id"` - ResourceARN string `json:"resourceArn"` - ResourceID string `json:"resourceId"` - DestinationARN string `json:"destinationArn"` - ServiceNetworkLogType string `json:"serviceNetworkLogType"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + ARN string `json:"arn"` + ID string `json:"id"` + ResourceARN string `json:"resourceArn"` + ResourceID string `json:"resourceId"` + DestinationARN string `json:"destinationArn"` + ServiceNetworkLogType string `json:"serviceNetworkLogType"` } func (a *storedALS) toALS() *AccessLogSubscription { return &AccessLogSubscription{ - ARN: a.ARN, - ID: a.ID, - ResourceARN: a.ResourceARN, - ResourceID: a.ResourceID, - DestinationARN: a.DestinationARN, + ARN: a.ARN, + ID: a.ID, + ResourceARN: a.ResourceARN, + ResourceID: a.ResourceID, + DestinationARN: a.DestinationARN, ServiceNetworkLogType: a.ServiceNetworkLogType, - CreatedAt: a.CreatedAt, - LastUpdatedAt: a.LastUpdatedAt, + CreatedAt: a.CreatedAt, + LastUpdatedAt: a.LastUpdatedAt, } } @@ -425,18 +425,18 @@ func (a *storedALS) toSummary() *AccessLogSubscriptionSummary { // snapshot is the serializable form of InMemoryBackend. type snapshot struct { - Services map[string]*storedService `json:"services"` - ServiceNetworks map[string]*storedServiceNetwork `json:"serviceNetworks"` - SNSAs map[string]*storedSNSA `json:"snsas"` - SNVAs map[string]*storedSNVA `json:"snvas"` - Listeners map[string]*storedListener `json:"listeners"` - Rules map[string]*storedRule `json:"rules"` - TargetGroups map[string]*storedTargetGroup `json:"targetGroups"` - Targets map[string][]*storedTarget `json:"targets"` - ALSs map[string]*storedALS `json:"alss"` - AuthPolicies map[string]string `json:"authPolicies"` - ResourcePolicies map[string]string `json:"resourcePolicies"` - Tags map[string]map[string]string `json:"tags"` + Services map[string]*storedService `json:"services"` + ServiceNetworks map[string]*storedServiceNetwork `json:"serviceNetworks"` + SNSAs map[string]*storedSNSA `json:"snsas"` + SNVAs map[string]*storedSNVA `json:"snvas"` + Listeners map[string]*storedListener `json:"listeners"` + Rules map[string]*storedRule `json:"rules"` + TargetGroups map[string]*storedTargetGroup `json:"targetGroups"` + Targets map[string][]*storedTarget `json:"targets"` + ALSs map[string]*storedALS `json:"alss"` + AuthPolicies map[string]string `json:"authPolicies"` + ResourcePolicies map[string]string `json:"resourcePolicies"` + Tags map[string]map[string]string `json:"tags"` } // InMemoryBackend is an in-memory implementation of StorageBackend. @@ -580,8 +580,20 @@ func (b *InMemoryBackend) buildListenerARN(serviceID, listenerID string) string } func (b *InMemoryBackend) buildRuleARN(serviceID, listenerID, ruleID string) string { - return arn.Build(arnService, b.region, b.accountID, - fmt.Sprintf("%s/%s/%s/%s/%s/%s", resourceService, serviceID, resourceListener, listenerID, resourceRule, ruleID)) + return arn.Build( + arnService, + b.region, + b.accountID, + fmt.Sprintf( + "%s/%s/%s/%s/%s/%s", + resourceService, + serviceID, + resourceListener, + listenerID, + resourceRule, + ruleID, + ), + ) } func newID(prefix string) string { @@ -719,7 +731,10 @@ func (b *InMemoryBackend) resolveSNVAID(identifier string) (string, bool) { // ------- Service operations ------- // CreateService creates a new service. -func (b *InMemoryBackend) CreateService(name, authType, certificateArn, customDomainName string, tags map[string]string) (*Service, error) { +func (b *InMemoryBackend) CreateService( + name, authType, certificateArn, customDomainName string, + tags map[string]string, +) (*Service, error) { if name == "" { return nil, ErrInvalidParameter } @@ -774,7 +789,9 @@ func (b *InMemoryBackend) GetService(serviceID string) (*Service, error) { } // UpdateService updates a service. -func (b *InMemoryBackend) UpdateService(serviceID, authType, certificateArn string) (*Service, error) { +func (b *InMemoryBackend) UpdateService( + serviceID, authType, certificateArn string, +) (*Service, error) { b.mu.Lock("UpdateService") defer b.mu.Unlock() @@ -816,7 +833,10 @@ func (b *InMemoryBackend) DeleteService(serviceID string) (*Service, error) { } // ListServices returns a paginated list of services. -func (b *InMemoryBackend) ListServices(maxResults int32, nextToken string) ([]*ServiceSummary, string, error) { +func (b *InMemoryBackend) ListServices( + maxResults int32, + nextToken string, +) ([]*ServiceSummary, string, error) { b.mu.RLock("ListServices") defer b.mu.RUnlock() @@ -835,7 +855,10 @@ func (b *InMemoryBackend) ListServices(maxResults int32, nextToken string) ([]*S // ------- ServiceNetwork operations ------- // CreateServiceNetwork creates a new service network. -func (b *InMemoryBackend) CreateServiceNetwork(name, authType string, tags map[string]string) (*ServiceNetwork, error) { +func (b *InMemoryBackend) CreateServiceNetwork( + name, authType string, + tags map[string]string, +) (*ServiceNetwork, error) { if name == "" { return nil, ErrInvalidParameter } @@ -952,7 +975,10 @@ func (b *InMemoryBackend) DeleteServiceNetwork(snID string) error { } // ListServiceNetworks returns a paginated list of service networks. -func (b *InMemoryBackend) ListServiceNetworks(maxResults int32, nextToken string) ([]*ServiceNetworkSummary, string, error) { +func (b *InMemoryBackend) ListServiceNetworks( + maxResults int32, + nextToken string, +) ([]*ServiceNetworkSummary, string, error) { b.mu.RLock("ListServiceNetworks") defer b.mu.RUnlock() @@ -971,7 +997,10 @@ func (b *InMemoryBackend) ListServiceNetworks(maxResults int32, nextToken string // ------- ServiceNetworkServiceAssociation operations ------- // CreateServiceNetworkServiceAssociation creates a service-to-network association. -func (b *InMemoryBackend) CreateServiceNetworkServiceAssociation(serviceNetworkID, serviceID string, tags map[string]string) (*ServiceNetworkServiceAssociation, error) { +func (b *InMemoryBackend) CreateServiceNetworkServiceAssociation( + serviceNetworkID, serviceID string, + tags map[string]string, +) (*ServiceNetworkServiceAssociation, error) { b.mu.Lock("CreateServiceNetworkServiceAssociation") defer b.mu.Unlock() @@ -1023,7 +1052,9 @@ func (b *InMemoryBackend) CreateServiceNetworkServiceAssociation(serviceNetworkI } // GetServiceNetworkServiceAssociation returns a SNSA by ID or ARN. -func (b *InMemoryBackend) GetServiceNetworkServiceAssociation(snsaID string) (*ServiceNetworkServiceAssociation, error) { +func (b *InMemoryBackend) GetServiceNetworkServiceAssociation( + snsaID string, +) (*ServiceNetworkServiceAssociation, error) { b.mu.RLock("GetServiceNetworkServiceAssociation") defer b.mu.RUnlock() @@ -1053,14 +1084,19 @@ func (b *InMemoryBackend) DeleteServiceNetworkServiceAssociation(snsaID string) } // ListServiceNetworkServiceAssociations lists SNSAs with optional filters. -func (b *InMemoryBackend) ListServiceNetworkServiceAssociations(serviceNetworkID, serviceID string, maxResults int32, nextToken string) ([]*ServiceNetworkServiceAssociationSummary, string, error) { +func (b *InMemoryBackend) ListServiceNetworkServiceAssociations( + serviceNetworkID, serviceID string, + maxResults int32, + nextToken string, +) ([]*ServiceNetworkServiceAssociationSummary, string, error) { b.mu.RLock("ListServiceNetworkServiceAssociations") defer b.mu.RUnlock() all := make([]*ServiceNetworkServiceAssociationSummary, 0) for _, s := range b.snsas { - if serviceNetworkID != "" && s.ServiceNetworkID != serviceNetworkID && s.ServiceNetworkARN != serviceNetworkID { + if serviceNetworkID != "" && s.ServiceNetworkID != serviceNetworkID && + s.ServiceNetworkARN != serviceNetworkID { continue } @@ -1081,7 +1117,11 @@ func (b *InMemoryBackend) ListServiceNetworkServiceAssociations(serviceNetworkID // ------- ServiceNetworkVpcAssociation operations ------- // CreateServiceNetworkVpcAssociation creates a VPC-to-network association. -func (b *InMemoryBackend) CreateServiceNetworkVpcAssociation(serviceNetworkID, vpcID string, securityGroupIDs []string, tags map[string]string) (*ServiceNetworkVpcAssociation, error) { +func (b *InMemoryBackend) CreateServiceNetworkVpcAssociation( + serviceNetworkID, vpcID string, + securityGroupIDs []string, + tags map[string]string, +) (*ServiceNetworkVpcAssociation, error) { if vpcID == "" { return nil, ErrInvalidParameter } @@ -1131,7 +1171,9 @@ func (b *InMemoryBackend) CreateServiceNetworkVpcAssociation(serviceNetworkID, v } // GetServiceNetworkVpcAssociation returns a SNVA. -func (b *InMemoryBackend) GetServiceNetworkVpcAssociation(snvaID string) (*ServiceNetworkVpcAssociation, error) { +func (b *InMemoryBackend) GetServiceNetworkVpcAssociation( + snvaID string, +) (*ServiceNetworkVpcAssociation, error) { b.mu.RLock("GetServiceNetworkVpcAssociation") defer b.mu.RUnlock() @@ -1144,7 +1186,10 @@ func (b *InMemoryBackend) GetServiceNetworkVpcAssociation(snvaID string) (*Servi } // UpdateServiceNetworkVpcAssociation updates security groups on a SNVA. -func (b *InMemoryBackend) UpdateServiceNetworkVpcAssociation(snvaID string, securityGroupIDs []string) (*ServiceNetworkVpcAssociation, error) { +func (b *InMemoryBackend) UpdateServiceNetworkVpcAssociation( + snvaID string, + securityGroupIDs []string, +) (*ServiceNetworkVpcAssociation, error) { b.mu.Lock("UpdateServiceNetworkVpcAssociation") defer b.mu.Unlock() @@ -1180,14 +1225,19 @@ func (b *InMemoryBackend) DeleteServiceNetworkVpcAssociation(snvaID string) erro } // ListServiceNetworkVpcAssociations lists SNVAs with optional filters. -func (b *InMemoryBackend) ListServiceNetworkVpcAssociations(serviceNetworkID, vpcID string, maxResults int32, nextToken string) ([]*ServiceNetworkVpcAssociationSummary, string, error) { +func (b *InMemoryBackend) ListServiceNetworkVpcAssociations( + serviceNetworkID, vpcID string, + maxResults int32, + nextToken string, +) ([]*ServiceNetworkVpcAssociationSummary, string, error) { b.mu.RLock("ListServiceNetworkVpcAssociations") defer b.mu.RUnlock() all := make([]*ServiceNetworkVpcAssociationSummary, 0) for _, s := range b.snvas { - if serviceNetworkID != "" && s.ServiceNetworkID != serviceNetworkID && s.ServiceNetworkARN != serviceNetworkID { + if serviceNetworkID != "" && s.ServiceNetworkID != serviceNetworkID && + s.ServiceNetworkARN != serviceNetworkID { continue } @@ -1208,7 +1258,12 @@ func (b *InMemoryBackend) ListServiceNetworkVpcAssociations(serviceNetworkID, vp // ------- Listener operations ------- // CreateListener creates a listener on a service. -func (b *InMemoryBackend) CreateListener(serviceID, name, protocol string, port int32, defaultAction *RuleAction, tags map[string]string) (*Listener, error) { +func (b *InMemoryBackend) CreateListener( + serviceID, name, protocol string, + port int32, + defaultAction *RuleAction, + tags map[string]string, +) (*Listener, error) { if name == "" || protocol == "" { return nil, ErrInvalidParameter } @@ -1265,7 +1320,11 @@ func (b *InMemoryBackend) CreateListener(serviceID, name, protocol string, port return l.toListener(), nil } -func (b *InMemoryBackend) createDefaultRule(serviceID, listenerID, listenerARN string, action *RuleAction, now time.Time) { +func (b *InMemoryBackend) createDefaultRule( + serviceID, listenerID, listenerARN string, + action *RuleAction, + now time.Time, +) { id := newID(idPrefixRule) ruleARN := b.buildRuleARN(serviceID, listenerID, id) key := serviceID + "/" + listenerID + "/" + id @@ -1308,7 +1367,10 @@ func (b *InMemoryBackend) GetListener(serviceID, listenerID string) (*Listener, } // UpdateListener updates the default action of a listener. -func (b *InMemoryBackend) UpdateListener(serviceID, listenerID string, defaultAction *RuleAction) (*Listener, error) { +func (b *InMemoryBackend) UpdateListener( + serviceID, listenerID string, + defaultAction *RuleAction, +) (*Listener, error) { b.mu.Lock("UpdateListener") defer b.mu.Unlock() @@ -1367,7 +1429,11 @@ func (b *InMemoryBackend) DeleteListener(serviceID, listenerID string) error { } // ListListeners lists listeners for a service. -func (b *InMemoryBackend) ListListeners(serviceID string, maxResults int32, nextToken string) ([]*ListenerSummary, string, error) { +func (b *InMemoryBackend) ListListeners( + serviceID string, + maxResults int32, + nextToken string, +) ([]*ListenerSummary, string, error) { b.mu.RLock("ListListeners") defer b.mu.RUnlock() @@ -1394,7 +1460,13 @@ func (b *InMemoryBackend) ListListeners(serviceID string, maxResults int32, next // ------- Rule operations ------- // CreateRule creates a listener rule. -func (b *InMemoryBackend) CreateRule(serviceID, listenerID, name string, priority int32, action *RuleAction, match *RuleMatch, tags map[string]string) (*Rule, error) { +func (b *InMemoryBackend) CreateRule( + serviceID, listenerID, name string, + priority int32, + action *RuleAction, + match *RuleMatch, + tags map[string]string, +) (*Rule, error) { if name == "" { return nil, ErrInvalidParameter } @@ -1470,7 +1542,12 @@ func (b *InMemoryBackend) GetRule(serviceID, listenerID, ruleID string) (*Rule, } // UpdateRule updates a rule. -func (b *InMemoryBackend) UpdateRule(serviceID, listenerID, ruleID string, priority int32, action *RuleAction, match *RuleMatch) (*Rule, error) { +func (b *InMemoryBackend) UpdateRule( + serviceID, listenerID, ruleID string, + priority int32, + action *RuleAction, + match *RuleMatch, +) (*Rule, error) { b.mu.Lock("UpdateRule") defer b.mu.Unlock() @@ -1543,7 +1620,11 @@ func (b *InMemoryBackend) DeleteRule(serviceID, listenerID, ruleID string) error } // ListRules lists rules for a listener. -func (b *InMemoryBackend) ListRules(serviceID, listenerID string, maxResults int32, nextToken string) ([]*RuleSummary, string, error) { +func (b *InMemoryBackend) ListRules( + serviceID, listenerID string, + maxResults int32, + nextToken string, +) ([]*RuleSummary, string, error) { b.mu.RLock("ListRules") defer b.mu.RUnlock() @@ -1573,7 +1654,10 @@ func (b *InMemoryBackend) ListRules(serviceID, listenerID string, maxResults int } // BatchUpdateRule updates multiple rules atomically. -func (b *InMemoryBackend) BatchUpdateRule(serviceID, listenerID string, updates []*RuleUpdate) ([]*RuleUpdateSuccess, []*RuleUpdateFailure, error) { +func (b *InMemoryBackend) BatchUpdateRule( + serviceID, listenerID string, + updates []*RuleUpdate, +) ([]*RuleUpdateSuccess, []*RuleUpdateFailure, error) { b.mu.Lock("BatchUpdateRule") defer b.mu.Unlock() @@ -1636,7 +1720,11 @@ func (b *InMemoryBackend) BatchUpdateRule(serviceID, listenerID string, updates // ------- TargetGroup operations ------- // CreateTargetGroup creates a target group. -func (b *InMemoryBackend) CreateTargetGroup(name, tgType string, config *TargetGroupConfig, tags map[string]string) (*TargetGroup, error) { +func (b *InMemoryBackend) CreateTargetGroup( + name, tgType string, + config *TargetGroupConfig, + tags map[string]string, +) (*TargetGroup, error) { if name == "" { return nil, ErrInvalidParameter } @@ -1686,7 +1774,10 @@ func (b *InMemoryBackend) GetTargetGroup(tgID string) (*TargetGroup, error) { } // UpdateTargetGroup updates a target group's health check config. -func (b *InMemoryBackend) UpdateTargetGroup(tgID string, healthCheck *HealthCheckConfig) (*TargetGroup, error) { +func (b *InMemoryBackend) UpdateTargetGroup( + tgID string, + healthCheck *HealthCheckConfig, +) (*TargetGroup, error) { b.mu.Lock("UpdateTargetGroup") defer b.mu.Unlock() @@ -1729,7 +1820,11 @@ func (b *InMemoryBackend) DeleteTargetGroup(tgID string) error { } // ListTargetGroups lists target groups with optional filters. -func (b *InMemoryBackend) ListTargetGroups(tgType, serviceArn string, maxResults int32, nextToken string) ([]*TargetGroupSummary, string, error) { +func (b *InMemoryBackend) ListTargetGroups( + tgType, serviceArn string, + maxResults int32, + nextToken string, +) ([]*TargetGroupSummary, string, error) { b.mu.RLock("ListTargetGroups") defer b.mu.RUnlock() @@ -1766,7 +1861,10 @@ func (b *InMemoryBackend) ListTargetGroups(tgType, serviceArn string, maxResults } // RegisterTargets registers targets to a target group. -func (b *InMemoryBackend) RegisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) { +func (b *InMemoryBackend) RegisterTargets( + tgID string, + targets []*Target, +) ([]*TargetFailure, error) { b.mu.Lock("RegisterTargets") defer b.mu.Unlock() @@ -1813,7 +1911,10 @@ func (b *InMemoryBackend) RegisterTargets(tgID string, targets []*Target) ([]*Ta } // DeregisterTargets deregisters targets from a target group. -func (b *InMemoryBackend) DeregisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) { +func (b *InMemoryBackend) DeregisterTargets( + tgID string, + targets []*Target, +) ([]*TargetFailure, error) { b.mu.Lock("DeregisterTargets") defer b.mu.Unlock() @@ -1875,7 +1976,11 @@ func (b *InMemoryBackend) DeregisterTargets(tgID string, targets []*Target) ([]* } // ListTargets lists registered targets for a target group. -func (b *InMemoryBackend) ListTargets(tgID string, maxResults int32, nextToken string) ([]*TargetSummary, string, error) { +func (b *InMemoryBackend) ListTargets( + tgID string, + maxResults int32, + nextToken string, +) ([]*TargetSummary, string, error) { b.mu.RLock("ListTargets") defer b.mu.RUnlock() @@ -1903,7 +2008,10 @@ func (b *InMemoryBackend) ListTargets(tgID string, maxResults int32, nextToken s // ------- AccessLogSubscription operations ------- // CreateAccessLogSubscription creates an access log subscription. -func (b *InMemoryBackend) CreateAccessLogSubscription(resourceID, destinationArn, logType string, tags map[string]string) (*AccessLogSubscription, error) { +func (b *InMemoryBackend) CreateAccessLogSubscription( + resourceID, destinationArn, logType string, + tags map[string]string, +) (*AccessLogSubscription, error) { if destinationArn == "" { return nil, ErrInvalidParameter } @@ -1919,15 +2027,15 @@ func (b *InMemoryBackend) CreateAccessLogSubscription(resourceID, destinationArn alsARN := b.buildARN(resourceAccessLogSubscription, id) als := &storedALS{ - ARN: alsARN, - ID: id, - ResourceARN: resourceARN, - ResourceID: resourceID, - DestinationARN: destinationArn, + ARN: alsARN, + ID: id, + ResourceARN: resourceARN, + ResourceID: resourceID, + DestinationARN: destinationArn, ServiceNetworkLogType: logType, - Tags: copyTags(tags), - CreatedAt: now, - LastUpdatedAt: now, + Tags: copyTags(tags), + CreatedAt: now, + LastUpdatedAt: now, } b.alss[id] = als @@ -1974,7 +2082,9 @@ func (b *InMemoryBackend) GetAccessLogSubscription(alsID string) (*AccessLogSubs } // UpdateAccessLogSubscription updates the destination ARN. -func (b *InMemoryBackend) UpdateAccessLogSubscription(alsID, destinationArn string) (*AccessLogSubscription, error) { +func (b *InMemoryBackend) UpdateAccessLogSubscription( + alsID, destinationArn string, +) (*AccessLogSubscription, error) { b.mu.Lock("UpdateAccessLogSubscription") defer b.mu.Unlock() @@ -2008,7 +2118,11 @@ func (b *InMemoryBackend) DeleteAccessLogSubscription(alsID string) error { } // ListAccessLogSubscriptions lists access log subscriptions for a resource. -func (b *InMemoryBackend) ListAccessLogSubscriptions(resourceID string, maxResults int32, nextToken string) ([]*AccessLogSubscriptionSummary, string, error) { +func (b *InMemoryBackend) ListAccessLogSubscriptions( + resourceID string, + maxResults int32, + nextToken string, +) ([]*AccessLogSubscriptionSummary, string, error) { b.mu.RLock("ListAccessLogSubscriptions") defer b.mu.RUnlock() diff --git a/services/vpclattice/handler.go b/services/vpclattice/handler.go index 1da9344f1..68b36e5ac 100644 --- a/services/vpclattice/handler.go +++ b/services/vpclattice/handler.go @@ -157,7 +157,8 @@ func (h *Handler) handleREST(c *echo.Context) error { var body map[string]any if c.Request().ContentLength != 0 { - if err := json.NewDecoder(c.Request().Body).Decode(&body); err != nil && err.Error() != "EOF" { + if err := json.NewDecoder(c.Request().Body).Decode(&body); err != nil && + err.Error() != "EOF" { return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "invalid JSON body"}) } } @@ -391,7 +392,11 @@ func (h *Handler) handleGetServiceNetwork(c *echo.Context, id string) error { return c.JSON(http.StatusOK, serviceNetworkToJSON(sn)) } -func (h *Handler) handleUpdateServiceNetwork(c *echo.Context, id string, body map[string]any) error { +func (h *Handler) handleUpdateServiceNetwork( + c *echo.Context, + id string, + body map[string]any, +) error { authType, _ := body["authType"].(string) sn, err := h.Backend.UpdateServiceNetwork(id, authType) @@ -439,7 +444,12 @@ func (h *Handler) handleCreateSNSA(c *echo.Context, body map[string]any) error { svcID, _ := body["serviceIdentifier"].(string) if snID == "" || svcID == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "serviceNetworkIdentifier and serviceIdentifier are required"}) + return c.JSON( + http.StatusBadRequest, + map[string]any{ + keyMessage: "serviceNetworkIdentifier and serviceIdentifier are required", + }, + ) } tags := extractTags(body) @@ -475,7 +485,12 @@ func (h *Handler) handleListSNSAs(c *echo.Context) error { snID := c.QueryParam("serviceNetworkIdentifier") svcID := c.QueryParam("serviceIdentifier") - items, next, err := h.Backend.ListServiceNetworkServiceAssociations(snID, svcID, maxResults, nextToken) + items, next, err := h.Backend.ListServiceNetworkServiceAssociations( + snID, + svcID, + maxResults, + nextToken, + ) if err != nil { return h.handleError(c, err) } @@ -500,7 +515,10 @@ func (h *Handler) handleCreateSNVA(c *echo.Context, body map[string]any) error { vpcID, _ := body["vpcIdentifier"].(string) if snID == "" || vpcID == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "serviceNetworkIdentifier and vpcIdentifier are required"}) + return c.JSON( + http.StatusBadRequest, + map[string]any{keyMessage: "serviceNetworkIdentifier and vpcIdentifier are required"}, + ) } var sgs []string @@ -563,7 +581,12 @@ func (h *Handler) handleListSNVAs(c *echo.Context) error { snID := c.QueryParam("serviceNetworkIdentifier") vpcID := c.QueryParam("vpcIdentifier") - items, next, err := h.Backend.ListServiceNetworkVpcAssociations(snID, vpcID, maxResults, nextToken) + items, next, err := h.Backend.ListServiceNetworkVpcAssociations( + snID, + vpcID, + maxResults, + nextToken, + ) if err != nil { return h.handleError(c, err) } @@ -583,12 +606,19 @@ func (h *Handler) handleListSNVAs(c *echo.Context) error { // ------- Listener handlers ------- -func (h *Handler) handleCreateListener(c *echo.Context, serviceID string, body map[string]any) error { +func (h *Handler) handleCreateListener( + c *echo.Context, + serviceID string, + body map[string]any, +) error { name, _ := body["name"].(string) protocol, _ := body["protocol"].(string) if name == "" || protocol == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name and protocol are required"}) + return c.JSON( + http.StatusBadRequest, + map[string]any{keyMessage: "name and protocol are required"}, + ) } port := bodyInt32(body, "port") @@ -612,7 +642,11 @@ func (h *Handler) handleGetListener(c *echo.Context, serviceID, listenerID strin return c.JSON(http.StatusOK, listenerToJSON(l)) } -func (h *Handler) handleUpdateListener(c *echo.Context, serviceID, listenerID string, body map[string]any) error { +func (h *Handler) handleUpdateListener( + c *echo.Context, + serviceID, listenerID string, + body map[string]any, +) error { defaultAction := extractRuleAction(body, "defaultAction") l, err := h.Backend.UpdateListener(serviceID, listenerID, defaultAction) @@ -655,7 +689,11 @@ func (h *Handler) handleListListeners(c *echo.Context, serviceID string) error { // ------- Rule handlers ------- -func (h *Handler) handleCreateRule(c *echo.Context, serviceID, listenerID string, body map[string]any) error { +func (h *Handler) handleCreateRule( + c *echo.Context, + serviceID, listenerID string, + body map[string]any, +) error { name, _ := body["name"].(string) if name == "" { return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name is required"}) @@ -683,7 +721,11 @@ func (h *Handler) handleGetRule(c *echo.Context, serviceID, listenerID, ruleID s return c.JSON(http.StatusOK, ruleToJSON(r)) } -func (h *Handler) handleUpdateRule(c *echo.Context, serviceID, listenerID, ruleID string, body map[string]any) error { +func (h *Handler) handleUpdateRule( + c *echo.Context, + serviceID, listenerID, ruleID string, + body map[string]any, +) error { priority := bodyInt32(body, "priority") action := extractRuleAction(body, "action") match := extractRuleMatch(body, "match") @@ -726,7 +768,11 @@ func (h *Handler) handleListRules(c *echo.Context, serviceID, listenerID string) return c.JSON(http.StatusOK, resp) } -func (h *Handler) handleBatchUpdateRule(c *echo.Context, serviceID, listenerID string, body map[string]any) error { +func (h *Handler) handleBatchUpdateRule( + c *echo.Context, + serviceID, listenerID string, + body map[string]any, +) error { var updates []*RuleUpdate if rawUpdates, ok := body["rules"].([]any); ok { @@ -762,7 +808,7 @@ func (h *Handler) handleBatchUpdateRule(c *echo.Context, serviceID, listenerID s } return c.JSON(http.StatusOK, map[string]any{ - "successful": successList, + "successful": successList, "unsuccessful": failureList, }) } @@ -774,7 +820,10 @@ func (h *Handler) handleCreateTargetGroup(c *echo.Context, body map[string]any) tgType, _ := body["type"].(string) if name == "" || tgType == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name and type are required"}) + return c.JSON( + http.StatusBadRequest, + map[string]any{keyMessage: "name and type are required"}, + ) } config := extractTargetGroupConfig(body) @@ -905,7 +954,10 @@ func (h *Handler) handleCreateALS(c *echo.Context, body map[string]any) error { logType, _ := body["serviceNetworkLogType"].(string) if resourceID == "" || destArn == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "resourceIdentifier and destinationArn are required"}) + return c.JSON( + http.StatusBadRequest, + map[string]any{keyMessage: "resourceIdentifier and destinationArn are required"}, + ) } tags := extractTags(body) @@ -971,7 +1023,11 @@ func (h *Handler) handleListALSs(c *echo.Context) error { // ------- Auth/Resource Policy handlers ------- -func (h *Handler) handlePutAuthPolicy(c *echo.Context, resourceID string, body map[string]any) error { +func (h *Handler) handlePutAuthPolicy( + c *echo.Context, + resourceID string, + body map[string]any, +) error { policy, _ := body["policy"].(string) ap, err := h.Backend.PutAuthPolicy(resourceID, policy) @@ -1005,7 +1061,11 @@ func (h *Handler) handleDeleteAuthPolicy(c *echo.Context, resourceID string) err return c.NoContent(http.StatusNoContent) } -func (h *Handler) handlePutResourcePolicy(c *echo.Context, resourceArn string, body map[string]any) error { +func (h *Handler) handlePutResourcePolicy( + c *echo.Context, + resourceArn string, + body map[string]any, +) error { policy, _ := body["policy"].(string) if err := h.Backend.PutResourcePolicy(resourceArn, policy); err != nil { @@ -1034,7 +1094,11 @@ func (h *Handler) handleDeleteResourcePolicy(c *echo.Context, resourceArn string // ------- Tagging handlers ------- -func (h *Handler) handleTagResource(c *echo.Context, resourceArn string, body map[string]any) error { +func (h *Handler) handleTagResource( + c *echo.Context, + resourceArn string, + body map[string]any, +) error { tags := make(map[string]string) if t, ok := body["tags"].(map[string]any); ok { for k, v := range t { @@ -1074,7 +1138,9 @@ func (h *Handler) handleListTagsForResource(c *echo.Context, resourceArn string) // classifyPath maps (method, path) → (op, id1, id2, id3, extra). // id1..id3 are path segments in order (service, listener, rule etc.). -func classifyPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns +func classifyPath( + method, path string, +) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns switch { case path == pathServices: if method == http.MethodPost { @@ -1168,7 +1234,9 @@ func classifyPath(method, path string) (op, id1, id2, id3, extra string) { //nol } // classifyServicePath handles /services/{serviceID}[/listeners[/...]] -func classifyServicePath(method, path string) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns +func classifyServicePath( + method, path string, +) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns rest := strings.TrimPrefix(path, pathServices+"/") serviceID, sub, hasSub := strings.Cut(rest, "/") @@ -1240,7 +1308,9 @@ func classifyServicePath(method, path string) (op, id1, id2, id3, extra string) } // classifyServiceNetworkPath handles /servicenetworks/{id} -func classifyServiceNetworkPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +func classifyServiceNetworkPath( + method, path string, +) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns id := strings.TrimPrefix(path, pathServiceNetworks+"/") switch method { case http.MethodGet: @@ -1255,7 +1325,9 @@ func classifyServiceNetworkPath(method, path string) (op, id1, id2, id3, extra s } // classifySNSAPath handles /servicenetworkserviceassociations/{id} -func classifySNSAPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +func classifySNSAPath( + method, path string, +) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns id := strings.TrimPrefix(path, pathServiceNetworkServiceAssociations+"/") switch method { case http.MethodGet: @@ -1268,7 +1340,9 @@ func classifySNSAPath(method, path string) (op, id1, id2, id3, extra string) { / } // classifySNVAPath handles /servicenetworkvpcassociations/{id} -func classifySNVAPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +func classifySNVAPath( + method, path string, +) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns id := strings.TrimPrefix(path, pathServiceNetworkVpcAssociations+"/") switch method { case http.MethodGet: @@ -1283,7 +1357,9 @@ func classifySNVAPath(method, path string) (op, id1, id2, id3, extra string) { / } // classifyTargetGroupPath handles /targetgroups/{id}[/registertargets|deregistertargets|listtargets] -func classifyTargetGroupPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +func classifyTargetGroupPath( + method, path string, +) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns rest := strings.TrimPrefix(path, pathTargetGroups+"/") tgID, sub, hasSub := strings.Cut(rest, "/") @@ -1313,7 +1389,9 @@ func classifyTargetGroupPath(method, path string) (op, id1, id2, id3, extra stri } // classifyALSPath handles /accesslogsubscriptions/{id} -func classifyALSPath(method, path string) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +func classifyALSPath( + method, path string, +) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns id := strings.TrimPrefix(path, pathAccessLogSubscriptions+"/") switch method { case http.MethodGet: @@ -1331,14 +1409,14 @@ func classifyALSPath(method, path string) (op, id1, id2, id3, extra string) { // func serviceToJSON(s *Service) map[string]any { m := map[string]any{ - "arn": s.ARN, - "id": s.ID, - "name": s.Name, - "authType": s.AuthType, - "status": s.Status, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + "arn": s.ARN, + "id": s.ID, + "name": s.Name, + "authType": s.AuthType, + "status": s.Status, + "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), "lastUpdatedAt": s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), - "dnsEntry": map[string]any{"domainName": s.DNSName}, + "dnsEntry": map[string]any{"domainName": s.DNSName}, } if s.CertificateArn != "" { @@ -1354,10 +1432,10 @@ func serviceToJSON(s *Service) map[string]any { func serviceSummaryToJSON(s *ServiceSummary) map[string]any { m := map[string]any{ - "arn": s.ARN, - "id": s.ID, - "name": s.Name, - "status": s.Status, + "arn": s.ARN, + "id": s.ID, + "name": s.Name, + "status": s.Status, "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } diff --git a/services/vpclattice/handler_test.go b/services/vpclattice/handler_test.go index e83d36cec..39253b7da 100644 --- a/services/vpclattice/handler_test.go +++ b/services/vpclattice/handler_test.go @@ -21,7 +21,12 @@ func newTestHandler(t *testing.T) *vpclattice.Handler { return vpclattice.NewHandler(backend) } -func doRequest(t *testing.T, h *vpclattice.Handler, method, path string, body any) *httptest.ResponseRecorder { +func doRequest( + t *testing.T, + h *vpclattice.Handler, + method, path string, + body any, +) *httptest.ResponseRecorder { t.Helper() var buf *bytes.Reader @@ -76,7 +81,11 @@ func TestService_CRUD(t *testing.T) { wantCode: http.StatusCreated, check: func(t *testing.T, resp map[string]any) { t.Helper() - assert.Contains(t, resp["arn"], "arn:aws:vpc-lattice:us-east-1:000000000000:service/svc-") + assert.Contains( + t, + resp["arn"], + "arn:aws:vpc-lattice:us-east-1:000000000000:service/svc-", + ) assert.Equal(t, "my-svc", resp["name"]) assert.Equal(t, "ACTIVE", resp["status"]) assert.NotEmpty(t, resp["id"]) @@ -169,7 +178,11 @@ func TestServiceNetwork_CRUD(t *testing.T) { created := parseBody(t, rec) id, _ := created["id"].(string) require.NotEmpty(t, id) - assert.Contains(t, created["arn"], "arn:aws:vpc-lattice:us-east-1:000000000000:servicenetwork/sn-") + assert.Contains( + t, + created["arn"], + "arn:aws:vpc-lattice:us-east-1:000000000000:servicenetwork/sn-", + ) // get rec = doRequest(t, h, http.MethodGet, "/servicenetworks/"+id, nil) @@ -178,7 +191,13 @@ func TestServiceNetwork_CRUD(t *testing.T) { assert.Equal(t, "sn1", got["name"]) // update - rec = doRequest(t, h, http.MethodPatch, "/servicenetworks/"+id, map[string]any{"authType": "AWS_IAM"}) + rec = doRequest( + t, + h, + http.MethodPatch, + "/servicenetworks/"+id, + map[string]any{"authType": "AWS_IAM"}, + ) assert.Equal(t, http.StatusOK, rec.Code) // list @@ -270,9 +289,15 @@ func TestSNVA_CRUD(t *testing.T) { assert.Equal(t, http.StatusOK, rec.Code) // update security groups - rec = doRequest(t, h, http.MethodPatch, "/servicenetworkvpcassociations/"+assocID, map[string]any{ - "securityGroupIds": []string{"sg-new"}, - }) + rec = doRequest( + t, + h, + http.MethodPatch, + "/servicenetworkvpcassociations/"+assocID, + map[string]any{ + "securityGroupIds": []string{"sg-new"}, + }, + ) assert.Equal(t, http.StatusOK, rec.Code) // list @@ -318,11 +343,17 @@ func TestListener_CRUD(t *testing.T) { assert.Equal(t, http.StatusOK, rec.Code) // update - rec = doRequest(t, h, http.MethodPatch, "/services/"+svcID+"/listeners/"+listenerID, map[string]any{ - "defaultAction": map[string]any{ - "fixedResponse": map[string]any{"statusCode": 200}, + rec = doRequest( + t, + h, + http.MethodPatch, + "/services/"+svcID+"/listeners/"+listenerID, + map[string]any{ + "defaultAction": map[string]any{ + "fixedResponse": map[string]any{"statusCode": 200}, + }, }, - }) + ) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, 1, vpclattice.ListenerCount(h.Backend.(*vpclattice.InMemoryBackend))) @@ -364,8 +395,8 @@ func TestRule_CRUD(t *testing.T) { "name": "tg1", "type": "INSTANCE", "config": map[string]any{ - "protocol": "HTTP", - "port": 80, + "protocol": "HTTP", + "port": 80, "vpcIdentifier": "vpc-123", }, }) @@ -373,25 +404,31 @@ func TestRule_CRUD(t *testing.T) { tgID, _ := parseBody(t, recTG)["id"].(string) // create rule with forward action - rec := doRequest(t, h, http.MethodPost, "/services/"+svcID+"/listeners/"+listenerID+"/rules", map[string]any{ - "name": "rule1", - "priority": 10, - "action": map[string]any{ - "forward": map[string]any{ - "targetGroups": []any{ - map[string]any{"targetGroupIdentifier": tgID, "weight": 100}, + rec := doRequest( + t, + h, + http.MethodPost, + "/services/"+svcID+"/listeners/"+listenerID+"/rules", + map[string]any{ + "name": "rule1", + "priority": 10, + "action": map[string]any{ + "forward": map[string]any{ + "targetGroups": []any{ + map[string]any{"targetGroupIdentifier": tgID, "weight": 100}, + }, }, }, - }, - "match": map[string]any{ - "httpMatch": map[string]any{ - "method": "GET", - "path": map[string]any{ - "match": map[string]any{"exact": "/api"}, + "match": map[string]any{ + "httpMatch": map[string]any{ + "method": "GET", + "path": map[string]any{ + "match": map[string]any{"exact": "/api"}, + }, }, }, }, - }) + ) require.Equal(t, http.StatusCreated, rec.Code) rule := parseBody(t, rec) ruleID, _ := rule["id"].(string) @@ -399,7 +436,13 @@ func TestRule_CRUD(t *testing.T) { assert.Equal(t, float64(10), rule["priority"]) // get - rec = doRequest(t, h, http.MethodGet, "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, nil) + rec = doRequest( + t, + h, + http.MethodGet, + "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, + nil, + ) assert.Equal(t, http.StatusOK, rec.Code) // list (includes default rule) @@ -410,15 +453,27 @@ func TestRule_CRUD(t *testing.T) { assert.Len(t, items, 2) // default + created // update - rec = doRequest(t, h, http.MethodPatch, "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, map[string]any{ - "priority": 20, - }) + rec = doRequest( + t, + h, + http.MethodPatch, + "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, + map[string]any{ + "priority": 20, + }, + ) assert.Equal(t, http.StatusOK, rec.Code) updated := parseBody(t, rec) assert.Equal(t, float64(20), updated["priority"]) // delete - rec = doRequest(t, h, http.MethodDelete, "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, nil) + rec = doRequest( + t, + h, + http.MethodDelete, + "/services/"+svcID+"/listeners/"+listenerID+"/rules/"+ruleID, + nil, + ) assert.Equal(t, http.StatusNoContent, rec.Code) // list now has only default rule @@ -448,21 +503,33 @@ func TestBatchUpdateRule(t *testing.T) { require.Equal(t, http.StatusCreated, recL.Code) listenerID, _ := parseBody(t, recL)["id"].(string) - rec := doRequest(t, h, http.MethodPost, "/services/"+svcID+"/listeners/"+listenerID+"/rules", map[string]any{ - "name": "r1", - "priority": 10, - "action": map[string]any{"fixedResponse": map[string]any{"statusCode": 200}}, - }) + rec := doRequest( + t, + h, + http.MethodPost, + "/services/"+svcID+"/listeners/"+listenerID+"/rules", + map[string]any{ + "name": "r1", + "priority": 10, + "action": map[string]any{"fixedResponse": map[string]any{"statusCode": 200}}, + }, + ) require.Equal(t, http.StatusCreated, rec.Code) ruleID, _ := parseBody(t, rec)["id"].(string) // batch update - rec = doRequest(t, h, http.MethodPatch, "/services/"+svcID+"/listeners/"+listenerID+"/rules", map[string]any{ - "rules": []any{ - map[string]any{"ruleIdentifier": ruleID, "priority": 50}, - map[string]any{"ruleIdentifier": "rule-notexist", "priority": 99}, + rec = doRequest( + t, + h, + http.MethodPatch, + "/services/"+svcID+"/listeners/"+listenerID+"/rules", + map[string]any{ + "rules": []any{ + map[string]any{"ruleIdentifier": ruleID, "priority": 50}, + map[string]any{"ruleIdentifier": "rule-notexist", "priority": 99}, + }, }, - }) + ) assert.Equal(t, http.StatusOK, rec.Code) resp := parseBody(t, rec) successful, _ := resp["successful"].([]any) @@ -493,8 +560,8 @@ func TestTargetGroup_CRUD(t *testing.T) { "name": "tg-inst", "type": "INSTANCE", "config": map[string]any{ - "protocol": "HTTP", - "port": 8080, + "protocol": "HTTP", + "port": 8080, "vpcIdentifier": "vpc-abc", }, }, @@ -524,8 +591,8 @@ func TestTargetGroup_CRUD(t *testing.T) { "name": "tg-full", "type": "IP", "config": map[string]any{ - "protocol": "HTTPS", - "port": 443, + "protocol": "HTTPS", + "port": 443, "vpcIdentifier": "vpc-xyz", }, }) @@ -568,8 +635,8 @@ func TestTargets(t *testing.T) { h := newTestHandler(t) rec := doRequest(t, h, http.MethodPost, "/targetgroups", map[string]any{ - "name": "tg-targets", - "type": "IP", + "name": "tg-targets", + "type": "IP", "config": map[string]any{"protocol": "HTTP", "port": 80, "vpcIdentifier": "vpc-1"}, }) require.Equal(t, http.StatusCreated, rec.Code) @@ -595,11 +662,17 @@ func TestTargets(t *testing.T) { assert.Len(t, items, 2) // deregister one - rec = doRequest(t, h, http.MethodPost, "/targetgroups/"+tgID+"/deregistertargets", map[string]any{ - "targets": []any{ - map[string]any{"id": "10.0.0.1", "port": 80}, + rec = doRequest( + t, + h, + http.MethodPost, + "/targetgroups/"+tgID+"/deregistertargets", + map[string]any{ + "targets": []any{ + map[string]any{"id": "10.0.0.1", "port": 80}, + }, }, - }) + ) assert.Equal(t, http.StatusOK, rec.Code) // list after deregister @@ -693,7 +766,13 @@ func TestResourcePolicy(t *testing.T) { policy := `{"Version":"2012-10-17","Statement":[]}` // put - rec := doRequest(t, h, http.MethodPut, "/resourcepolicy/"+resArn, map[string]any{"policy": policy}) + rec := doRequest( + t, + h, + http.MethodPut, + "/resourcepolicy/"+resArn, + map[string]any{"policy": policy}, + ) assert.Equal(t, http.StatusOK, rec.Code) // get @@ -754,21 +833,33 @@ func TestARNFormat(t *testing.T) { rec := doRequest(t, h, http.MethodPost, "/services", map[string]any{"name": "arn-svc"}) require.Equal(t, http.StatusCreated, rec.Code) svc := parseBody(t, rec) - assert.Regexp(t, `^arn:aws:vpc-lattice:us-east-1:000000000000:service/svc-[a-f0-9]+$`, svc["arn"]) + assert.Regexp( + t, + `^arn:aws:vpc-lattice:us-east-1:000000000000:service/svc-[a-f0-9]+$`, + svc["arn"], + ) rec = doRequest(t, h, http.MethodPost, "/servicenetworks", map[string]any{"name": "arn-sn"}) require.Equal(t, http.StatusCreated, rec.Code) sn := parseBody(t, rec) - assert.Regexp(t, `^arn:aws:vpc-lattice:us-east-1:000000000000:servicenetwork/sn-[a-f0-9]+$`, sn["arn"]) + assert.Regexp( + t, + `^arn:aws:vpc-lattice:us-east-1:000000000000:servicenetwork/sn-[a-f0-9]+$`, + sn["arn"], + ) rec = doRequest(t, h, http.MethodPost, "/targetgroups", map[string]any{ - "name": "arn-tg", - "type": "IP", + "name": "arn-tg", + "type": "IP", "config": map[string]any{"protocol": "HTTP", "port": 80, "vpcIdentifier": "vpc-1"}, }) require.Equal(t, http.StatusCreated, rec.Code) tg := parseBody(t, rec) - assert.Regexp(t, `^arn:aws:vpc-lattice:us-east-1:000000000000:targetgroup/tg-[a-f0-9]+$`, tg["arn"]) + assert.Regexp( + t, + `^arn:aws:vpc-lattice:us-east-1:000000000000:targetgroup/tg-[a-f0-9]+$`, + tg["arn"], + ) } // TestNotFound verifies 404 on get/update/delete of nonexistent resources. diff --git a/services/vpclattice/interfaces.go b/services/vpclattice/interfaces.go index 4fc81ba4f..f4603613a 100644 --- a/services/vpclattice/interfaces.go +++ b/services/vpclattice/interfaces.go @@ -4,7 +4,10 @@ import "time" // StorageBackend is the interface for VPC Lattice storage operations. type StorageBackend interface { - CreateService(name, authType, certificateArn, customDomainName string, tags map[string]string) (*Service, error) + CreateService( + name, authType, certificateArn, customDomainName string, + tags map[string]string, + ) (*Service, error) GetService(serviceID string) (*Service, error) UpdateService(serviceID, authType, certificateArn string) (*Service, error) DeleteService(serviceID string) (*Service, error) @@ -14,46 +17,109 @@ type StorageBackend interface { GetServiceNetwork(snID string) (*ServiceNetwork, error) UpdateServiceNetwork(snID, authType string) (*ServiceNetwork, error) DeleteServiceNetwork(snID string) error - ListServiceNetworks(maxResults int32, nextToken string) ([]*ServiceNetworkSummary, string, error) - - CreateServiceNetworkServiceAssociation(serviceNetworkID, serviceID string, tags map[string]string) (*ServiceNetworkServiceAssociation, error) + ListServiceNetworks( + maxResults int32, + nextToken string, + ) ([]*ServiceNetworkSummary, string, error) + + CreateServiceNetworkServiceAssociation( + serviceNetworkID, serviceID string, + tags map[string]string, + ) (*ServiceNetworkServiceAssociation, error) GetServiceNetworkServiceAssociation(snsaID string) (*ServiceNetworkServiceAssociation, error) DeleteServiceNetworkServiceAssociation(snsaID string) error - ListServiceNetworkServiceAssociations(serviceNetworkID, serviceID string, maxResults int32, nextToken string) ([]*ServiceNetworkServiceAssociationSummary, string, error) - - CreateServiceNetworkVpcAssociation(serviceNetworkID, vpcID string, securityGroupIDs []string, tags map[string]string) (*ServiceNetworkVpcAssociation, error) + ListServiceNetworkServiceAssociations( + serviceNetworkID, serviceID string, + maxResults int32, + nextToken string, + ) ([]*ServiceNetworkServiceAssociationSummary, string, error) + + CreateServiceNetworkVpcAssociation( + serviceNetworkID, vpcID string, + securityGroupIDs []string, + tags map[string]string, + ) (*ServiceNetworkVpcAssociation, error) GetServiceNetworkVpcAssociation(snvaID string) (*ServiceNetworkVpcAssociation, error) - UpdateServiceNetworkVpcAssociation(snvaID string, securityGroupIDs []string) (*ServiceNetworkVpcAssociation, error) + UpdateServiceNetworkVpcAssociation( + snvaID string, + securityGroupIDs []string, + ) (*ServiceNetworkVpcAssociation, error) DeleteServiceNetworkVpcAssociation(snvaID string) error - ListServiceNetworkVpcAssociations(serviceNetworkID, vpcID string, maxResults int32, nextToken string) ([]*ServiceNetworkVpcAssociationSummary, string, error) - - CreateListener(serviceID, name, protocol string, port int32, defaultAction *RuleAction, tags map[string]string) (*Listener, error) + ListServiceNetworkVpcAssociations( + serviceNetworkID, vpcID string, + maxResults int32, + nextToken string, + ) ([]*ServiceNetworkVpcAssociationSummary, string, error) + + CreateListener( + serviceID, name, protocol string, + port int32, + defaultAction *RuleAction, + tags map[string]string, + ) (*Listener, error) GetListener(serviceID, listenerID string) (*Listener, error) UpdateListener(serviceID, listenerID string, defaultAction *RuleAction) (*Listener, error) DeleteListener(serviceID, listenerID string) error - ListListeners(serviceID string, maxResults int32, nextToken string) ([]*ListenerSummary, string, error) - - CreateRule(serviceID, listenerID, name string, priority int32, action *RuleAction, match *RuleMatch, tags map[string]string) (*Rule, error) + ListListeners( + serviceID string, + maxResults int32, + nextToken string, + ) ([]*ListenerSummary, string, error) + + CreateRule( + serviceID, listenerID, name string, + priority int32, + action *RuleAction, + match *RuleMatch, + tags map[string]string, + ) (*Rule, error) GetRule(serviceID, listenerID, ruleID string) (*Rule, error) - UpdateRule(serviceID, listenerID, ruleID string, priority int32, action *RuleAction, match *RuleMatch) (*Rule, error) + UpdateRule( + serviceID, listenerID, ruleID string, + priority int32, + action *RuleAction, + match *RuleMatch, + ) (*Rule, error) DeleteRule(serviceID, listenerID, ruleID string) error - ListRules(serviceID, listenerID string, maxResults int32, nextToken string) ([]*RuleSummary, string, error) - BatchUpdateRule(serviceID, listenerID string, updates []*RuleUpdate) ([]*RuleUpdateSuccess, []*RuleUpdateFailure, error) - - CreateTargetGroup(name, tgType string, config *TargetGroupConfig, tags map[string]string) (*TargetGroup, error) + ListRules( + serviceID, listenerID string, + maxResults int32, + nextToken string, + ) ([]*RuleSummary, string, error) + BatchUpdateRule( + serviceID, listenerID string, + updates []*RuleUpdate, + ) ([]*RuleUpdateSuccess, []*RuleUpdateFailure, error) + + CreateTargetGroup( + name, tgType string, + config *TargetGroupConfig, + tags map[string]string, + ) (*TargetGroup, error) GetTargetGroup(tgID string) (*TargetGroup, error) UpdateTargetGroup(tgID string, healthCheck *HealthCheckConfig) (*TargetGroup, error) DeleteTargetGroup(tgID string) error - ListTargetGroups(tgType, serviceArn string, maxResults int32, nextToken string) ([]*TargetGroupSummary, string, error) + ListTargetGroups( + tgType, serviceArn string, + maxResults int32, + nextToken string, + ) ([]*TargetGroupSummary, string, error) RegisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) DeregisterTargets(tgID string, targets []*Target) ([]*TargetFailure, error) ListTargets(tgID string, maxResults int32, nextToken string) ([]*TargetSummary, string, error) - CreateAccessLogSubscription(resourceID, destinationArn, logType string, tags map[string]string) (*AccessLogSubscription, error) + CreateAccessLogSubscription( + resourceID, destinationArn, logType string, + tags map[string]string, + ) (*AccessLogSubscription, error) GetAccessLogSubscription(alsID string) (*AccessLogSubscription, error) UpdateAccessLogSubscription(alsID, destinationArn string) (*AccessLogSubscription, error) DeleteAccessLogSubscription(alsID string) error - ListAccessLogSubscriptions(resourceID string, maxResults int32, nextToken string) ([]*AccessLogSubscriptionSummary, string, error) + ListAccessLogSubscriptions( + resourceID string, + maxResults int32, + nextToken string, + ) ([]*AccessLogSubscriptionSummary, string, error) PutAuthPolicy(resourceID, policy string) (*AuthPolicy, error) GetAuthPolicy(resourceID string) (*AuthPolicy, error) @@ -102,24 +168,24 @@ type ServiceSummary struct { // ServiceNetwork represents a VPC Lattice service network. type ServiceNetwork struct { - CreatedAt time.Time - LastUpdatedAt time.Time - ARN string - ID string - Name string - AuthType string + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + Name string + AuthType string NumberOfAssociatedServices int64 - NumberOfAssociatedVPCs int64 + NumberOfAssociatedVPCs int64 } // ServiceNetworkSummary is a service network entry for list responses. type ServiceNetworkSummary struct { - CreatedAt time.Time - ARN string - ID string - Name string + CreatedAt time.Time + ARN string + ID string + Name string NumberOfAssociatedServices int64 - NumberOfAssociatedVPCs int64 + NumberOfAssociatedVPCs int64 } // ServiceNetworkServiceAssociation is a service-to-service-network association. @@ -270,18 +336,18 @@ type WeightedTargetGroup struct { // RuleMatch is the match conditions for a listener rule. type RuleMatch struct { - HTTPMethod string - PathMatchType string - PathMatchValue string - HeaderMatches []*HeaderMatch + HTTPMethod string + PathMatchType string + PathMatchValue string + HeaderMatches []*HeaderMatch } // HeaderMatch is an HTTP header match condition. type HeaderMatch struct { - Name string - MatchType string - MatchValue string - CaseSensitive bool + Name string + MatchType string + MatchValue string + CaseSensitive bool } // TargetGroup represents a VPC Lattice target group. @@ -344,10 +410,10 @@ type Target struct { // TargetSummary is a target entry for list responses. type TargetSummary struct { - ID string - Port int32 - Status string - ReasonCode string + ID string + Port int32 + Status string + ReasonCode string } // TargetFailure is a target registration/deregistration failure. @@ -360,13 +426,13 @@ type TargetFailure struct { // AccessLogSubscription represents a VPC Lattice access log subscription. type AccessLogSubscription struct { - CreatedAt time.Time - LastUpdatedAt time.Time - ARN string - ID string - ResourceARN string - ResourceID string - DestinationARN string + CreatedAt time.Time + LastUpdatedAt time.Time + ARN string + ID string + ResourceARN string + ResourceID string + DestinationARN string ServiceNetworkLogType string } diff --git a/services/vpclattice/sdk_completeness_test.go b/services/vpclattice/sdk_completeness_test.go index 4bf0d0f1d..4ca37543a 100644 --- a/services/vpclattice/sdk_completeness_test.go +++ b/services/vpclattice/sdk_completeness_test.go @@ -46,5 +46,10 @@ func TestSDKCompleteness(t *testing.T) { "StartDomainVerification", } - sdkcheck.CheckCompleteness(t, &vpclatticesdk.Client{}, h.GetSupportedOperations(), notImplemented) + sdkcheck.CheckCompleteness( + t, + &vpclatticesdk.Client{}, + h.GetSupportedOperations(), + notImplemented, + ) } diff --git a/test/terraform/fixtures/vpclattice/success.tf b/test/terraform/fixtures/vpclattice/success.tf new file mode 100644 index 000000000..67bab32a4 --- /dev/null +++ b/test/terraform/fixtures/vpclattice/success.tf @@ -0,0 +1,29 @@ +resource "terraform_data" "vpclattice_service_network" { + triggers_replace = { + endpoint = "{{.Endpoint}}" + } + + provisioner "local-exec" { + environment = { + AWS_ACCESS_KEY_ID = "test" + AWS_SECRET_ACCESS_KEY = "test" + AWS_DEFAULT_REGION = "us-east-1" + } + command = "aws --endpoint-url '{{.Endpoint}}' vpc-lattice create-service-network --name tf-sn-test" + } +} + +resource "terraform_data" "vpclattice_service" { + triggers_replace = { + endpoint = "{{.Endpoint}}" + } + + provisioner "local-exec" { + environment = { + AWS_ACCESS_KEY_ID = "test" + AWS_SECRET_ACCESS_KEY = "test" + AWS_DEFAULT_REGION = "us-east-1" + } + command = "aws --endpoint-url '{{.Endpoint}}' vpc-lattice create-service --name tf-svc-test" + } +} diff --git a/test/terraform/main_test.go b/test/terraform/main_test.go index 2da832cc9..13c1afe7c 100644 --- a/test/terraform/main_test.go +++ b/test/terraform/main_test.go @@ -120,6 +120,7 @@ import ( timestreamquerysvc "github.com/aws/aws-sdk-go-v2/service/timestreamquery" transfersvc "github.com/aws/aws-sdk-go-v2/service/transfer" verifiedpermissionssvc "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + vpclatticesvc "github.com/aws/aws-sdk-go-v2/service/vpclattice" xraysvc "github.com/aws/aws-sdk-go-v2/service/xray" "github.com/moby/moby/client" @@ -2334,6 +2335,13 @@ func createXrayClient(t *testing.T) *xraysvc.Client { return createClientWithEndpoint(t, xraysvc.NewFromConfig, endpoint) } +// createVPCLatticeClient returns a VPC Lattice client pointed at the shared test container. +func createVPCLatticeClient(t *testing.T) *vpclatticesvc.Client { + t.Helper() + + return createClientWithEndpoint(t, vpclatticesvc.NewFromConfig, endpoint) +} + // createClientWithEndpoint is a helper to create an AWS client with a base endpoint. func createClientWithEndpoint[T any, O any](t *testing.T, newFn func(aws.Config, ...func(*O)) *T, endpoint string) *T { t.Helper() diff --git a/test/terraform/terraform_test.go b/test/terraform/terraform_test.go index dd85498c2..f25411933 100644 --- a/test/terraform/terraform_test.go +++ b/test/terraform/terraform_test.go @@ -139,6 +139,7 @@ import ( timestreamquerytypes "github.com/aws/aws-sdk-go-v2/service/timestreamquery/types" transfersvc "github.com/aws/aws-sdk-go-v2/service/transfer" verifiedpermissionssvc "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + vpclatticesvc "github.com/aws/aws-sdk-go-v2/service/vpclattice" xraysvc "github.com/aws/aws-sdk-go-v2/service/xray" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -6395,6 +6396,27 @@ func TestTerraform_Xray(t *testing.T) { }) } +// TestTerraform_VPCLattice provisions a VPC Lattice service and service network via Terraform and verifies they were created. +func TestTerraform_VPCLattice(t *testing.T) { + t.Parallel() + + runTfTestWithEndpoint(t, "vpclattice/success", func(t *testing.T, ctx context.Context) { + t.Helper() + + client := createVPCLatticeClient(t) + + // ListServices - should have at least one from Terraform provisioning. + svcsOut, err := client.ListServices(ctx, &vpclatticesvc.ListServicesInput{}) + require.NoError(t, err, "ListServices should succeed") + assert.NotEmpty(t, svcsOut.Items, "expected at least one service") + + // ListServiceNetworks - should have at least one from Terraform provisioning. + snsOut, err := client.ListServiceNetworks(ctx, &vpclatticesvc.ListServiceNetworksInput{}) + require.NoError(t, err, "ListServiceNetworks should succeed") + assert.NotEmpty(t, snsOut.Items, "expected at least one service network") + }) +} + // TestTerraform_Wafv2 provisions a WAFv2 Web ACL via Terraform and verifies it was created. func TestTerraform_Wafv2(t *testing.T) { t.Parallel() From 55abcf6d8840e58f8514c1767a0e0873c77441ec Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 21:46:16 -0500 Subject: [PATCH 4/7] WIP: checkpoint (auto) --- services/vpclattice/backend.go | 46 +-- services/vpclattice/handler.go | 509 +++++++++++++++------------- services/vpclattice/handler_test.go | 6 +- services/vpclattice/interfaces.go | 60 ++-- 4 files changed, 331 insertions(+), 290 deletions(-) diff --git a/services/vpclattice/backend.go b/services/vpclattice/backend.go index 414cf8870..658d75892 100644 --- a/services/vpclattice/backend.go +++ b/services/vpclattice/backend.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "maps" + "slices" "sort" "strings" "time" @@ -51,6 +52,10 @@ const ( targetStatusHealthy = "HEALTHY" + authPolicyStateActive = "Active" + + defaultRulePriority = 100 + defaultMaxResults = 100 ) @@ -1321,7 +1326,7 @@ func (b *InMemoryBackend) CreateListener( } func (b *InMemoryBackend) createDefaultRule( - serviceID, listenerID, listenerARN string, + serviceID, listenerID, _ string, action *RuleAction, now time.Time, ) { @@ -1335,7 +1340,7 @@ func (b *InMemoryBackend) createDefaultRule( ServiceID: serviceID, ListenerID: listenerID, Name: "default", - Priority: 100, + Priority: defaultRulePriority, Action: action, IsDefault: true, Tags: make(map[string]string), @@ -1676,8 +1681,8 @@ func (b *InMemoryBackend) BatchUpdateRule( now := time.Now().UTC() for _, u := range updates { - rID, ok := b.resolveRuleID(svcID, lID, u.RuleIdentifier) - if !ok { + rID, found := b.resolveRuleID(svcID, lID, u.RuleIdentifier) + if !found { failures = append(failures, &RuleUpdateFailure{ RuleIdentifier: u.RuleIdentifier, Code: "NOT_FOUND", @@ -1835,19 +1840,8 @@ func (b *InMemoryBackend) ListTargetGroups( continue } - if serviceArn != "" { - found := false - for _, a := range tg.ServiceARNs { - if a == serviceArn { - found = true - - break - } - } - - if !found { - continue - } + if serviceArn != "" && !slices.Contains(tg.ServiceARNs, serviceArn) { + continue } all = append(all, tg.toSummary()) @@ -1925,19 +1919,15 @@ func (b *InMemoryBackend) DeregisterTargets( failures := make([]*TargetFailure, 0) existing := b.targets[id] - remaining := make([]*storedTarget, 0, len(existing)) for _, t := range targets { found := false + for _, e := range existing { if e.ID == t.ID && (t.Port == 0 || e.Port == t.Port) { found = true - continue - } - - if e.ID != t.ID || (t.Port != 0 && e.Port != t.Port) { - remaining = append(remaining, e) + break } } @@ -1952,7 +1942,7 @@ func (b *InMemoryBackend) DeregisterTargets( } // rebuild remaining with non-deregistered targets - remaining = make([]*storedTarget, 0) + remaining := make([]*storedTarget, 0, len(existing)) for _, e := range existing { remove := false @@ -2152,7 +2142,7 @@ func (b *InMemoryBackend) PutAuthPolicy(resourceID, policy string) (*AuthPolicy, b.authPolicies[resourceID] = policy - return &AuthPolicy{Policy: policy, State: "Active"}, nil + return &AuthPolicy{Policy: policy, State: authPolicyStateActive}, nil } // GetAuthPolicy returns the auth policy for a resource. @@ -2165,7 +2155,7 @@ func (b *InMemoryBackend) GetAuthPolicy(resourceID string) (*AuthPolicy, error) return &AuthPolicy{Policy: "", State: "Active"}, nil } - return &AuthPolicy{Policy: policy, State: "Active"}, nil + return &AuthPolicy{Policy: policy, State: authPolicyStateActive}, nil } // DeleteAuthPolicy deletes the auth policy for a resource. @@ -2226,9 +2216,7 @@ func (b *InMemoryBackend) TagResource(resourceArn string, tags map[string]string b.tags[resourceArn] = make(map[string]string) } - for k, v := range tags { - b.tags[resourceArn][k] = v - } + maps.Copy(b.tags[resourceArn], tags) return nil } diff --git a/services/vpclattice/handler.go b/services/vpclattice/handler.go index 68b36e5ac..07c3a92dd 100644 --- a/services/vpclattice/handler.go +++ b/services/vpclattice/handler.go @@ -29,6 +29,59 @@ const ( opUnknown = "Unknown" keyMessage = "message" + + opBatchUpdateRule = "BatchUpdateRule" + opCreateALS = "CreateAccessLogSubscription" + opCreateListener = "CreateListener" + opCreateRule = "CreateRule" + opCreateService = "CreateService" + opCreateSN = "CreateServiceNetwork" + opCreateSNSA = "CreateServiceNetworkServiceAssociation" + opCreateSNVA = "CreateServiceNetworkVpcAssociation" + opCreateTG = "CreateTargetGroup" + opDeleteALS = "DeleteAccessLogSubscription" + opDeleteAuthPolicy = "DeleteAuthPolicy" + opDeleteListener = "DeleteListener" + opDeleteResourcePolicy = "DeleteResourcePolicy" + opDeleteRule = "DeleteRule" + opDeleteService = "DeleteService" + opDeleteSN = "DeleteServiceNetwork" + opDeleteSNSA = "DeleteServiceNetworkServiceAssociation" + opDeleteSNVA = "DeleteServiceNetworkVpcAssociation" + opDeleteTG = "DeleteTargetGroup" + opDeregisterTargets = "DeregisterTargets" + opGetALS = "GetAccessLogSubscription" + opGetAuthPolicy = "GetAuthPolicy" + opGetListener = "GetListener" + opGetResourcePolicy = "GetResourcePolicy" + opGetRule = "GetRule" + opGetService = "GetService" + opGetSN = "GetServiceNetwork" + opGetSNSA = "GetServiceNetworkServiceAssociation" + opGetSNVA = "GetServiceNetworkVpcAssociation" + opGetTG = "GetTargetGroup" + opListALSs = "ListAccessLogSubscriptions" + opListListeners = "ListListeners" + opListRules = "ListRules" + opListSNSAs = "ListServiceNetworkServiceAssociations" + opListSNVAs = "ListServiceNetworkVpcAssociations" + opListSNs = "ListServiceNetworks" + opListServices = "ListServices" + opListTagsForResource = "ListTagsForResource" + opListTGs = "ListTargetGroups" + opListTargets = "ListTargets" + opPutAuthPolicy = "PutAuthPolicy" + opPutResourcePolicy = "PutResourcePolicy" + opRegisterTargets = "RegisterTargets" + opTagResource = "TagResource" + opUntagResource = "UntagResource" + opUpdateALS = "UpdateAccessLogSubscription" + opUpdateListener = "UpdateListener" + opUpdateRule = "UpdateRule" + opUpdateService = "UpdateService" + opUpdateSN = "UpdateServiceNetwork" + opUpdateSNVA = "UpdateServiceNetworkVpcAssociation" + opUpdateTG = "UpdateTargetGroup" ) // Handler handles VPC Lattice HTTP requests. @@ -50,58 +103,58 @@ func (h *Handler) Reset() { h.Backend.Reset() } // GetSupportedOperations returns all supported operations. func (h *Handler) GetSupportedOperations() []string { return []string{ - "BatchUpdateRule", - "CreateAccessLogSubscription", - "CreateListener", - "CreateRule", - "CreateService", - "CreateServiceNetwork", - "CreateServiceNetworkServiceAssociation", - "CreateServiceNetworkVpcAssociation", - "CreateTargetGroup", - "DeleteAccessLogSubscription", - "DeleteAuthPolicy", - "DeleteListener", - "DeleteResourcePolicy", - "DeleteRule", - "DeleteService", - "DeleteServiceNetwork", - "DeleteServiceNetworkServiceAssociation", - "DeleteServiceNetworkVpcAssociation", - "DeleteTargetGroup", - "DeregisterTargets", - "GetAccessLogSubscription", - "GetAuthPolicy", - "GetListener", - "GetResourcePolicy", - "GetRule", - "GetService", - "GetServiceNetwork", - "GetServiceNetworkServiceAssociation", - "GetServiceNetworkVpcAssociation", - "GetTargetGroup", - "ListAccessLogSubscriptions", - "ListListeners", - "ListRules", - "ListServiceNetworkServiceAssociations", - "ListServiceNetworkVpcAssociations", - "ListServiceNetworks", - "ListServices", - "ListTagsForResource", - "ListTargetGroups", - "ListTargets", - "PutAuthPolicy", - "PutResourcePolicy", - "RegisterTargets", - "TagResource", - "UntagResource", - "UpdateAccessLogSubscription", - "UpdateListener", - "UpdateRule", - "UpdateService", - "UpdateServiceNetwork", - "UpdateServiceNetworkVpcAssociation", - "UpdateTargetGroup", + opBatchUpdateRule, + opCreateALS, + opCreateListener, + opCreateRule, + opCreateService, + opCreateSN, + opCreateSNSA, + opCreateSNVA, + opCreateTG, + opDeleteALS, + opDeleteAuthPolicy, + opDeleteListener, + opDeleteResourcePolicy, + opDeleteRule, + opDeleteService, + opDeleteSN, + opDeleteSNSA, + opDeleteSNVA, + opDeleteTG, + opDeregisterTargets, + opGetALS, + opGetAuthPolicy, + opGetListener, + opGetResourcePolicy, + opGetRule, + opGetService, + opGetSN, + opGetSNSA, + opGetSNVA, + opGetTG, + opListALSs, + opListListeners, + opListRules, + opListSNSAs, + opListSNVAs, + opListSNs, + opListServices, + opListTagsForResource, + opListTGs, + opListTargets, + opPutAuthPolicy, + opPutResourcePolicy, + opRegisterTargets, + opTagResource, + opUntagResource, + opUpdateALS, + opUpdateListener, + opUpdateRule, + opUpdateService, + opUpdateSN, + opUpdateSNVA, + opUpdateTG, } } @@ -112,8 +165,10 @@ func (h *Handler) RouteMatcher() service.Matcher { return path == pathServices || strings.HasPrefix(path, pathServices+"/") || path == pathServiceNetworks || strings.HasPrefix(path, pathServiceNetworks+"/") || - path == pathServiceNetworkServiceAssociations || strings.HasPrefix(path, pathServiceNetworkServiceAssociations+"/") || - path == pathServiceNetworkVpcAssociations || strings.HasPrefix(path, pathServiceNetworkVpcAssociations+"/") || + path == pathServiceNetworkServiceAssociations || + strings.HasPrefix(path, pathServiceNetworkServiceAssociations+"/") || + path == pathServiceNetworkVpcAssociations || + strings.HasPrefix(path, pathServiceNetworkVpcAssociations+"/") || path == pathTargetGroups || strings.HasPrefix(path, pathTargetGroups+"/") || path == pathAccessLogSubscriptions || strings.HasPrefix(path, pathAccessLogSubscriptions+"/") || strings.HasPrefix(path, pathAuthPolicy+"/") || @@ -133,14 +188,14 @@ func (h *Handler) MatchPriority() int { return matchPriority } // ExtractOperation classifies the request into an operation name. func (h *Handler) ExtractOperation(c *echo.Context) string { - op, _, _, _, _ := classifyPath(c.Request().Method, c.Request().URL.Path) + op, _, _, _ := classifyPath(c.Request().Method, c.Request().URL.Path) return op } // ExtractResource returns the primary resource identifier. func (h *Handler) ExtractResource(c *echo.Context) string { - _, id, _, _, _ := classifyPath(c.Request().Method, c.Request().URL.Path) + _, id, _, _ := classifyPath(c.Request().Method, c.Request().URL.Path) return id } @@ -152,8 +207,8 @@ func (h *Handler) Handler() echo.HandlerFunc { } } -func (h *Handler) handleREST(c *echo.Context) error { - op, id1, id2, id3, _ := classifyPath(c.Request().Method, c.Request().URL.Path) +func (h *Handler) handleREST(c *echo.Context) error { //nolint:gocognit,gocyclo // large routing dispatch is expected + op, id1, id2, id3 := classifyPath(c.Request().Method, c.Request().URL.Path) var body map[string]any if c.Request().ContentLength != 0 { @@ -168,109 +223,109 @@ func (h *Handler) handleREST(c *echo.Context) error { } switch op { - case "CreateService": + case opCreateService: return h.handleCreateService(c, body) - case "GetService": + case opGetService: return h.handleGetService(c, id1) - case "UpdateService": + case opUpdateService: return h.handleUpdateService(c, id1, body) - case "DeleteService": + case opDeleteService: return h.handleDeleteService(c, id1) - case "ListServices": + case opListServices: return h.handleListServices(c) - case "CreateServiceNetwork": + case opCreateSN: return h.handleCreateServiceNetwork(c, body) - case "GetServiceNetwork": + case opGetSN: return h.handleGetServiceNetwork(c, id1) - case "UpdateServiceNetwork": + case opUpdateSN: return h.handleUpdateServiceNetwork(c, id1, body) - case "DeleteServiceNetwork": + case opDeleteSN: return h.handleDeleteServiceNetwork(c, id1) - case "ListServiceNetworks": + case opListSNs: return h.handleListServiceNetworks(c) - case "CreateServiceNetworkServiceAssociation": + case opCreateSNSA: return h.handleCreateSNSA(c, body) - case "GetServiceNetworkServiceAssociation": + case opGetSNSA: return h.handleGetSNSA(c, id1) - case "DeleteServiceNetworkServiceAssociation": + case opDeleteSNSA: return h.handleDeleteSNSA(c, id1) - case "ListServiceNetworkServiceAssociations": + case opListSNSAs: return h.handleListSNSAs(c) - case "CreateServiceNetworkVpcAssociation": + case opCreateSNVA: return h.handleCreateSNVA(c, body) - case "GetServiceNetworkVpcAssociation": + case opGetSNVA: return h.handleGetSNVA(c, id1) - case "UpdateServiceNetworkVpcAssociation": + case opUpdateSNVA: return h.handleUpdateSNVA(c, id1, body) - case "DeleteServiceNetworkVpcAssociation": + case opDeleteSNVA: return h.handleDeleteSNVA(c, id1) - case "ListServiceNetworkVpcAssociations": + case opListSNVAs: return h.handleListSNVAs(c) - case "CreateListener": + case opCreateListener: return h.handleCreateListener(c, id1, body) - case "GetListener": + case opGetListener: return h.handleGetListener(c, id1, id2) - case "UpdateListener": + case opUpdateListener: return h.handleUpdateListener(c, id1, id2, body) - case "DeleteListener": + case opDeleteListener: return h.handleDeleteListener(c, id1, id2) - case "ListListeners": + case opListListeners: return h.handleListListeners(c, id1) - case "CreateRule": + case opCreateRule: return h.handleCreateRule(c, id1, id2, body) - case "GetRule": + case opGetRule: return h.handleGetRule(c, id1, id2, id3) - case "UpdateRule": + case opUpdateRule: return h.handleUpdateRule(c, id1, id2, id3, body) - case "DeleteRule": + case opDeleteRule: return h.handleDeleteRule(c, id1, id2, id3) - case "ListRules": + case opListRules: return h.handleListRules(c, id1, id2) - case "BatchUpdateRule": + case opBatchUpdateRule: return h.handleBatchUpdateRule(c, id1, id2, body) - case "CreateTargetGroup": + case opCreateTG: return h.handleCreateTargetGroup(c, body) - case "GetTargetGroup": + case opGetTG: return h.handleGetTargetGroup(c, id1) - case "UpdateTargetGroup": + case opUpdateTG: return h.handleUpdateTargetGroup(c, id1, body) - case "DeleteTargetGroup": + case opDeleteTG: return h.handleDeleteTargetGroup(c, id1) - case "ListTargetGroups": + case opListTGs: return h.handleListTargetGroups(c) - case "RegisterTargets": + case opRegisterTargets: return h.handleRegisterTargets(c, id1, body) - case "DeregisterTargets": + case opDeregisterTargets: return h.handleDeregisterTargets(c, id1, body) - case "ListTargets": + case opListTargets: return h.handleListTargets(c, id1, body) - case "CreateAccessLogSubscription": + case opCreateALS: return h.handleCreateALS(c, body) - case "GetAccessLogSubscription": + case opGetALS: return h.handleGetALS(c, id1) - case "UpdateAccessLogSubscription": + case opUpdateALS: return h.handleUpdateALS(c, id1, body) - case "DeleteAccessLogSubscription": + case opDeleteALS: return h.handleDeleteALS(c, id1) - case "ListAccessLogSubscriptions": + case opListALSs: return h.handleListALSs(c) - case "PutAuthPolicy": + case opPutAuthPolicy: return h.handlePutAuthPolicy(c, id1, body) - case "GetAuthPolicy": + case opGetAuthPolicy: return h.handleGetAuthPolicy(c, id1) - case "DeleteAuthPolicy": + case opDeleteAuthPolicy: return h.handleDeleteAuthPolicy(c, id1) - case "PutResourcePolicy": + case opPutResourcePolicy: return h.handlePutResourcePolicy(c, id1, body) - case "GetResourcePolicy": + case opGetResourcePolicy: return h.handleGetResourcePolicy(c, id1) - case "DeleteResourcePolicy": + case opDeleteResourcePolicy: return h.handleDeleteResourcePolicy(c, id1) - case "TagResource": + case opTagResource: return h.handleTagResource(c, id1, body) - case "UntagResource": + case opUntagResource: return h.handleUntagResource(c, id1) - case "ListTagsForResource": + case opListTagsForResource: return h.handleListTagsForResource(c, id1) default: return c.JSON(http.StatusNotFound, map[string]any{keyMessage: "unknown operation"}) @@ -343,7 +398,7 @@ func (h *Handler) handleDeleteService(c *echo.Context, id string) error { } func (h *Handler) handleListServices(c *echo.Context) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListServices(maxResults, nextToken) @@ -416,7 +471,7 @@ func (h *Handler) handleDeleteServiceNetwork(c *echo.Context, id string) error { } func (h *Handler) handleListServiceNetworks(c *echo.Context) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListServiceNetworks(maxResults, nextToken) @@ -480,7 +535,7 @@ func (h *Handler) handleDeleteSNSA(c *echo.Context, id string) error { } func (h *Handler) handleListSNSAs(c *echo.Context) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") snID := c.QueryParam("serviceNetworkIdentifier") svcID := c.QueryParam("serviceIdentifier") @@ -524,7 +579,7 @@ func (h *Handler) handleCreateSNVA(c *echo.Context, body map[string]any) error { var sgs []string if sgRaw, ok := body["securityGroupIds"].([]any); ok { for _, v := range sgRaw { - if s, ok := v.(string); ok { + if s, ok2 := v.(string); ok2 { sgs = append(sgs, s) } } @@ -553,7 +608,7 @@ func (h *Handler) handleUpdateSNVA(c *echo.Context, id string, body map[string]a var sgs []string if sgRaw, ok := body["securityGroupIds"].([]any); ok { for _, v := range sgRaw { - if s, ok := v.(string); ok { + if s, ok2 := v.(string); ok2 { sgs = append(sgs, s) } } @@ -576,7 +631,7 @@ func (h *Handler) handleDeleteSNVA(c *echo.Context, id string) error { } func (h *Handler) handleListSNVAs(c *echo.Context) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") snID := c.QueryParam("serviceNetworkIdentifier") vpcID := c.QueryParam("vpcIdentifier") @@ -666,7 +721,7 @@ func (h *Handler) handleDeleteListener(c *echo.Context, serviceID, listenerID st } func (h *Handler) handleListListeners(c *echo.Context, serviceID string) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListListeners(serviceID, maxResults, nextToken) @@ -747,7 +802,7 @@ func (h *Handler) handleDeleteRule(c *echo.Context, serviceID, listenerID, ruleI } func (h *Handler) handleListRules(c *echo.Context, serviceID, listenerID string) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListRules(serviceID, listenerID, maxResults, nextToken) @@ -777,7 +832,7 @@ func (h *Handler) handleBatchUpdateRule( if rawUpdates, ok := body["rules"].([]any); ok { for _, raw := range rawUpdates { - if m, ok := raw.(map[string]any); ok { + if m, ok2 := raw.(map[string]any); ok2 { u := &RuleUpdate{} u.RuleIdentifier, _ = m["ruleIdentifier"].(string) u.Priority = bodyInt32(m, "priority") @@ -869,7 +924,7 @@ func (h *Handler) handleDeleteTargetGroup(c *echo.Context, id string) error { } func (h *Handler) handleListTargetGroups(c *echo.Context) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") tgType := c.QueryParam("targetGroupType") svcArn := c.QueryParam("serviceArn") @@ -924,8 +979,8 @@ func (h *Handler) handleDeregisterTargets(c *echo.Context, tgID string, body map return c.JSON(http.StatusOK, map[string]any{"unsuccessful": failureList}) } -func (h *Handler) handleListTargets(c *echo.Context, tgID string, body map[string]any) error { - maxResults := queryInt32(c, "maxResults", 0) +func (h *Handler) handleListTargets(c *echo.Context, tgID string, _ map[string]any) error { + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListTargets(tgID, maxResults, nextToken) @@ -999,7 +1054,7 @@ func (h *Handler) handleDeleteALS(c *echo.Context, id string) error { } func (h *Handler) handleListALSs(c *echo.Context) error { - maxResults := queryInt32(c, "maxResults", 0) + maxResults := queryInt32(c, 0) nextToken := c.QueryParam("nextToken") resourceID := c.QueryParam("resourceIdentifier") @@ -1102,7 +1157,7 @@ func (h *Handler) handleTagResource( tags := make(map[string]string) if t, ok := body["tags"].(map[string]any); ok { for k, v := range t { - if s, ok := v.(string); ok { + if s, ok2 := v.(string); ok2 { tags[k] = s } } @@ -1136,63 +1191,63 @@ func (h *Handler) handleListTagsForResource(c *echo.Context, resourceArn string) // ------- Path classification ------- -// classifyPath maps (method, path) → (op, id1, id2, id3, extra). +// classifyPath maps (method, path) → (op, id1, id2, id3). // id1..id3 are path segments in order (service, listener, rule etc.). -func classifyPath( +func classifyPath( //nolint:gocognit,gocyclo // large routing dispatch is expected method, path string, -) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns +) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity switch { case path == pathServices: if method == http.MethodPost { - return "CreateService", "", "", "", "" + return opCreateService, "", "", "" } - return "ListServices", "", "", "", "" + return opListServices, "", "", "" case strings.HasPrefix(path, pathServices+"/"): return classifyServicePath(method, path) case path == pathServiceNetworks: if method == http.MethodPost { - return "CreateServiceNetwork", "", "", "", "" + return opCreateSN, "", "", "" } - return "ListServiceNetworks", "", "", "", "" + return opListSNs, "", "", "" case strings.HasPrefix(path, pathServiceNetworks+"/"): return classifyServiceNetworkPath(method, path) case path == pathServiceNetworkServiceAssociations: if method == http.MethodPost { - return "CreateServiceNetworkServiceAssociation", "", "", "", "" + return opCreateSNSA, "", "", "" } - return "ListServiceNetworkServiceAssociations", "", "", "", "" + return opListSNSAs, "", "", "" case strings.HasPrefix(path, pathServiceNetworkServiceAssociations+"/"): return classifySNSAPath(method, path) case path == pathServiceNetworkVpcAssociations: if method == http.MethodPost { - return "CreateServiceNetworkVpcAssociation", "", "", "", "" + return opCreateSNVA, "", "", "" } - return "ListServiceNetworkVpcAssociations", "", "", "", "" + return opListSNVAs, "", "", "" case strings.HasPrefix(path, pathServiceNetworkVpcAssociations+"/"): return classifySNVAPath(method, path) case path == pathTargetGroups: if method == http.MethodPost { - return "CreateTargetGroup", "", "", "", "" + return opCreateTG, "", "", "" } - return "ListTargetGroups", "", "", "", "" + return opListTGs, "", "", "" case strings.HasPrefix(path, pathTargetGroups+"/"): return classifyTargetGroupPath(method, path) case path == pathAccessLogSubscriptions: if method == http.MethodPost { - return "CreateAccessLogSubscription", "", "", "", "" + return opCreateALS, "", "", "" } - return "ListAccessLogSubscriptions", "", "", "", "" + return opListALSs, "", "", "" case strings.HasPrefix(path, pathAccessLogSubscriptions+"/"): return classifyALSPath(method, path) @@ -1200,209 +1255,207 @@ func classifyPath( resourceID := strings.TrimPrefix(path, pathAuthPolicy+"/") switch method { case http.MethodPut: - return "PutAuthPolicy", resourceID, "", "", "" + return opPutAuthPolicy, resourceID, "", "" case http.MethodGet: - return "GetAuthPolicy", resourceID, "", "", "" + return opGetAuthPolicy, resourceID, "", "" case http.MethodDelete: - return "DeleteAuthPolicy", resourceID, "", "", "" + return opDeleteAuthPolicy, resourceID, "", "" } case strings.HasPrefix(path, pathResourcePolicy+"/"): resourceArn := strings.TrimPrefix(path, pathResourcePolicy+"/") switch method { case http.MethodPut: - return "PutResourcePolicy", resourceArn, "", "", "" + return opPutResourcePolicy, resourceArn, "", "" case http.MethodGet: - return "GetResourcePolicy", resourceArn, "", "", "" + return opGetResourcePolicy, resourceArn, "", "" case http.MethodDelete: - return "DeleteResourcePolicy", resourceArn, "", "", "" + return opDeleteResourcePolicy, resourceArn, "", "" } case strings.HasPrefix(path, pathTags+"/"): resourceArn := strings.TrimPrefix(path, pathTags+"/") switch method { case http.MethodPost: - return "TagResource", resourceArn, "", "", "" + return opTagResource, resourceArn, "", "" case http.MethodDelete: - return "UntagResource", resourceArn, "", "", "" + return opUntagResource, resourceArn, "", "" case http.MethodGet: - return "ListTagsForResource", resourceArn, "", "", "" + return opListTagsForResource, resourceArn, "", "" } } - return opUnknown, "", "", "", "" + return opUnknown, "", "", "" } -// classifyServicePath handles /services/{serviceID}[/listeners[/...]] -func classifyServicePath( +// classifyServicePath handles /services/{serviceID}[/listeners[/...]]. +func classifyServicePath( //nolint:gocognit,gocyclo // large routing dispatch is expected method, path string, -) (op, id1, id2, id3, extra string) { //nolint:cyclop,nonamedreturns +) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity rest := strings.TrimPrefix(path, pathServices+"/") serviceID, sub, hasSub := strings.Cut(rest, "/") if !hasSub { switch method { case http.MethodGet: - return "GetService", serviceID, "", "", "" + return opGetService, serviceID, "", "" case http.MethodPatch: - return "UpdateService", serviceID, "", "", "" + return opUpdateService, serviceID, "", "" case http.MethodDelete: - return "DeleteService", serviceID, "", "", "" + return opDeleteService, serviceID, "", "" } - return opUnknown, serviceID, "", "", "" + return opUnknown, serviceID, "", "" } // sub = listeners[/{listenerID}[/rules[/{ruleID}]]] if sub == "listeners" { if method == http.MethodPost { - return "CreateListener", serviceID, "", "", "" + return opCreateListener, serviceID, "", "" } - return "ListListeners", serviceID, "", "", "" + return opListListeners, serviceID, "", "" } - if strings.HasPrefix(sub, "listeners/") { - listenerRest := strings.TrimPrefix(sub, "listeners/") + if listenerRest, ok := strings.CutPrefix(sub, "listeners/"); ok { listenerID, listenerSub, hasListenerSub := strings.Cut(listenerRest, "/") if !hasListenerSub { switch method { case http.MethodGet: - return "GetListener", serviceID, listenerID, "", "" + return opGetListener, serviceID, listenerID, "" case http.MethodPatch: - return "UpdateListener", serviceID, listenerID, "", "" + return opUpdateListener, serviceID, listenerID, "" case http.MethodDelete: - return "DeleteListener", serviceID, listenerID, "", "" + return opDeleteListener, serviceID, listenerID, "" } - return opUnknown, serviceID, listenerID, "", "" + return opUnknown, serviceID, listenerID, "" } if listenerSub == "rules" { if method == http.MethodPost { - return "CreateRule", serviceID, listenerID, "", "" + return opCreateRule, serviceID, listenerID, "" } if method == http.MethodPatch { - return "BatchUpdateRule", serviceID, listenerID, "", "" + return opBatchUpdateRule, serviceID, listenerID, "" } - return "ListRules", serviceID, listenerID, "", "" + return opListRules, serviceID, listenerID, "" } - if strings.HasPrefix(listenerSub, "rules/") { - ruleID := strings.TrimPrefix(listenerSub, "rules/") + if ruleID, ok2 := strings.CutPrefix(listenerSub, "rules/"); ok2 { switch method { case http.MethodGet: - return "GetRule", serviceID, listenerID, ruleID, "" + return opGetRule, serviceID, listenerID, ruleID case http.MethodPatch: - return "UpdateRule", serviceID, listenerID, ruleID, "" + return opUpdateRule, serviceID, listenerID, ruleID case http.MethodDelete: - return "DeleteRule", serviceID, listenerID, ruleID, "" + return opDeleteRule, serviceID, listenerID, ruleID } } } - return opUnknown, serviceID, "", "", "" + return opUnknown, serviceID, "", "" } -// classifyServiceNetworkPath handles /servicenetworks/{id} +// classifyServiceNetworkPath handles /servicenetworks/{id}. func classifyServiceNetworkPath( method, path string, -) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity id := strings.TrimPrefix(path, pathServiceNetworks+"/") switch method { case http.MethodGet: - return "GetServiceNetwork", id, "", "", "" + return opGetSN, id, "", "" case http.MethodPatch: - return "UpdateServiceNetwork", id, "", "", "" + return opUpdateSN, id, "", "" case http.MethodDelete: - return "DeleteServiceNetwork", id, "", "", "" + return opDeleteSN, id, "", "" } - return opUnknown, id, "", "", "" + return opUnknown, id, "", "" } -// classifySNSAPath handles /servicenetworkserviceassociations/{id} +// classifySNSAPath handles /servicenetworkserviceassociations/{id}. func classifySNSAPath( method, path string, -) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity id := strings.TrimPrefix(path, pathServiceNetworkServiceAssociations+"/") switch method { case http.MethodGet: - return "GetServiceNetworkServiceAssociation", id, "", "", "" + return opGetSNSA, id, "", "" case http.MethodDelete: - return "DeleteServiceNetworkServiceAssociation", id, "", "", "" + return opDeleteSNSA, id, "", "" } - return opUnknown, id, "", "", "" + return opUnknown, id, "", "" } -// classifySNVAPath handles /servicenetworkvpcassociations/{id} +// classifySNVAPath handles /servicenetworkvpcassociations/{id}. func classifySNVAPath( method, path string, -) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity id := strings.TrimPrefix(path, pathServiceNetworkVpcAssociations+"/") switch method { case http.MethodGet: - return "GetServiceNetworkVpcAssociation", id, "", "", "" + return opGetSNVA, id, "", "" case http.MethodPatch: - return "UpdateServiceNetworkVpcAssociation", id, "", "", "" + return opUpdateSNVA, id, "", "" case http.MethodDelete: - return "DeleteServiceNetworkVpcAssociation", id, "", "", "" + return opDeleteSNVA, id, "", "" } - return opUnknown, id, "", "", "" + return opUnknown, id, "", "" } -// classifyTargetGroupPath handles /targetgroups/{id}[/registertargets|deregistertargets|listtargets] +// classifyTargetGroupPath handles /targetgroups/{id}[/registertargets|deregistertargets|listtargets]. func classifyTargetGroupPath( method, path string, -) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity rest := strings.TrimPrefix(path, pathTargetGroups+"/") tgID, sub, hasSub := strings.Cut(rest, "/") if !hasSub { switch method { case http.MethodGet: - return "GetTargetGroup", tgID, "", "", "" + return opGetTG, tgID, "", "" case http.MethodPatch: - return "UpdateTargetGroup", tgID, "", "", "" + return opUpdateTG, tgID, "", "" case http.MethodDelete: - return "DeleteTargetGroup", tgID, "", "", "" + return opDeleteTG, tgID, "", "" } - return opUnknown, tgID, "", "", "" + return opUnknown, tgID, "", "" } switch sub { case "registertargets": - return "RegisterTargets", tgID, "", "", "" + return opRegisterTargets, tgID, "", "" case "deregistertargets": - return "DeregisterTargets", tgID, "", "", "" + return opDeregisterTargets, tgID, "", "" case "listtargets": - return "ListTargets", tgID, "", "", "" + return opListTargets, tgID, "", "" } - return opUnknown, tgID, "", "", "" + return opUnknown, tgID, "", "" } -// classifyALSPath handles /accesslogsubscriptions/{id} +// classifyALSPath handles /accesslogsubscriptions/{id}. func classifyALSPath( method, path string, -) (op, id1, id2, id3, extra string) { //nolint:nonamedreturns +) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity id := strings.TrimPrefix(path, pathAccessLogSubscriptions+"/") switch method { case http.MethodGet: - return "GetAccessLogSubscription", id, "", "", "" + return opGetALS, id, "", "" case http.MethodPatch: - return "UpdateAccessLogSubscription", id, "", "", "" + return opUpdateALS, id, "", "" case http.MethodDelete: - return "DeleteAccessLogSubscription", id, "", "", "" + return opDeleteALS, id, "", "" } - return opUnknown, id, "", "", "" + return opUnknown, id, "", "" } // ------- JSON serialization helpers ------- @@ -1789,7 +1842,7 @@ func extractTags(body map[string]any) map[string]string { if t, ok := body["tags"].(map[string]any); ok { for k, v := range t { - if s, ok := v.(string); ok { + if s, ok2 := v.(string); ok2 { tags[k] = s } } @@ -1801,13 +1854,13 @@ func extractTags(body map[string]any) map[string]string { func bodyInt32(body map[string]any, key string) int32 { switch v := body[key].(type) { case float64: - return int32(v) + return int32(v) //nolint:gosec // value is bounded by JSON number range case int: - return int32(v) + return int32(v) //nolint:gosec // value is bounded, overflow not possible case int32: return v case int64: - return int32(v) + return int32(v) //nolint:gosec // value is bounded, overflow not possible } return 0 @@ -1821,17 +1874,17 @@ func extractRuleAction(body map[string]any, key string) *RuleAction { action := &RuleAction{} - if fr, ok := raw["fixedResponse"].(map[string]any); ok { + if fr, ok2 := raw["fixedResponse"].(map[string]any); ok2 { action.IsFixedResponse = true action.FixedResponseStatusCode = bodyInt32(fr, "statusCode") return action } - if fwd, ok := raw["forward"].(map[string]any); ok { - if tgs, ok := fwd["targetGroups"].([]any); ok { + if fwd, ok2 := raw["forward"].(map[string]any); ok2 { + if tgs, ok3 := fwd["targetGroups"].([]any); ok3 { for _, tgRaw := range tgs { - if tgMap, ok := tgRaw.(map[string]any); ok { + if tgMap, ok4 := tgRaw.(map[string]any); ok4 { wt := &WeightedTargetGroup{} wt.TargetGroupID, _ = tgMap["targetGroupIdentifier"].(string) wt.Weight = bodyInt32(tgMap, "weight") @@ -1844,7 +1897,7 @@ func extractRuleAction(body map[string]any, key string) *RuleAction { return action } -func extractRuleMatch(body map[string]any, key string) *RuleMatch { +func extractRuleMatch(body map[string]any, key string) *RuleMatch { //nolint:gocognit // nested JSON extraction raw, ok := body[key].(map[string]any) if !ok { return nil @@ -1852,13 +1905,13 @@ func extractRuleMatch(body map[string]any, key string) *RuleMatch { match := &RuleMatch{} - if httpMatch, ok := raw["httpMatch"].(map[string]any); ok { + if httpMatch, ok2 := raw["httpMatch"].(map[string]any); ok2 { match.HTTPMethod, _ = httpMatch["method"].(string) - if pathRaw, ok := httpMatch["path"].(map[string]any); ok { - if matchRaw, ok := pathRaw["match"].(map[string]any); ok { + if pathRaw, ok3 := httpMatch["path"].(map[string]any); ok3 { + if matchRaw, ok4 := pathRaw["match"].(map[string]any); ok4 { for k, v := range matchRaw { - if s, ok := v.(string); ok { + if s, ok5 := v.(string); ok5 { match.PathMatchType = k match.PathMatchValue = s } @@ -1866,14 +1919,14 @@ func extractRuleMatch(body map[string]any, key string) *RuleMatch { } } - if headersRaw, ok := httpMatch["headerMatches"].([]any); ok { + if headersRaw, ok3 := httpMatch["headerMatches"].([]any); ok3 { for _, hRaw := range headersRaw { - if hMap, ok := hRaw.(map[string]any); ok { + if hMap, ok4 := hRaw.(map[string]any); ok4 { hm := &HeaderMatch{} hm.Name, _ = hMap["name"].(string) - if matchRaw, ok := hMap["match"].(map[string]any); ok { + if matchRaw, ok5 := hMap["match"].(map[string]any); ok5 { for k, v := range matchRaw { - if s, ok := v.(string); ok { + if s, ok6 := v.(string); ok6 { hm.MatchType = k hm.MatchValue = s } @@ -1903,7 +1956,7 @@ func extractTargetGroupConfig(body map[string]any) *TargetGroupConfig { cfg.IPAddressType, _ = raw["ipAddressType"].(string) cfg.LambdaEventStructureVersion, _ = raw["lambdaEventStructureVersion"].(string) - if hcRaw, ok := raw["healthCheck"].(map[string]any); ok { + if hcRaw, ok2 := raw["healthCheck"].(map[string]any); ok2 { cfg.HealthCheck = extractHealthCheckConfig(hcRaw) } @@ -1933,7 +1986,7 @@ func extractTargets(body map[string]any) []*Target { if raw, ok := body["targets"].([]any); ok { for _, tRaw := range raw { - if tMap, ok := tRaw.(map[string]any); ok { + if tMap, ok2 := tRaw.(map[string]any); ok2 { t := &Target{} t.ID, _ = tMap["id"].(string) t.Port = bodyInt32(tMap, "port") @@ -1945,8 +1998,8 @@ func extractTargets(body map[string]any) []*Target { return targets } -func queryInt32(c *echo.Context, key string, fallback int32) int32 { - v := c.QueryParam(key) +func queryInt32(c *echo.Context, fallback int32) int32 { + v := c.QueryParam("maxResults") if v == "" { return fallback } @@ -1956,5 +2009,5 @@ func queryInt32(c *echo.Context, key string, fallback int32) int32 { return fallback } - return int32(n) + return int32(n) //nolint:gosec // value is bounded by ParseInt with bitSize=32 } diff --git a/services/vpclattice/handler_test.go b/services/vpclattice/handler_test.go index 39253b7da..d6ff72253 100644 --- a/services/vpclattice/handler_test.go +++ b/services/vpclattice/handler_test.go @@ -336,7 +336,7 @@ func TestListener_CRUD(t *testing.T) { listenerID, _ := l["id"].(string) require.NotEmpty(t, listenerID) assert.Equal(t, "my-listener", l["name"]) - assert.Equal(t, float64(80), l["port"]) + assert.InDelta(t, float64(80), l["port"], 0) // get rec = doRequest(t, h, http.MethodGet, "/services/"+svcID+"/listeners/"+listenerID, nil) @@ -433,7 +433,7 @@ func TestRule_CRUD(t *testing.T) { rule := parseBody(t, rec) ruleID, _ := rule["id"].(string) require.NotEmpty(t, ruleID) - assert.Equal(t, float64(10), rule["priority"]) + assert.InDelta(t, float64(10), rule["priority"], 0) // get rec = doRequest( @@ -464,7 +464,7 @@ func TestRule_CRUD(t *testing.T) { ) assert.Equal(t, http.StatusOK, rec.Code) updated := parseBody(t, rec) - assert.Equal(t, float64(20), updated["priority"]) + assert.InDelta(t, float64(20), updated["priority"], 0) // delete rec = doRequest( diff --git a/services/vpclattice/interfaces.go b/services/vpclattice/interfaces.go index f4603613a..90fcde0cd 100644 --- a/services/vpclattice/interfaces.go +++ b/services/vpclattice/interfaces.go @@ -323,31 +323,31 @@ type RuleUpdateFailure struct { // RuleAction is the action for a listener rule. type RuleAction struct { - FixedResponseStatusCode int32 - ForwardTargetGroups []*WeightedTargetGroup - IsFixedResponse bool + ForwardTargetGroups []*WeightedTargetGroup `json:"forwardTargetGroups,omitempty"` + FixedResponseStatusCode int32 `json:"fixedResponseStatusCode,omitempty"` + IsFixedResponse bool `json:"isFixedResponse,omitempty"` } // WeightedTargetGroup is a weighted target group for forward actions. type WeightedTargetGroup struct { - TargetGroupID string - Weight int32 + TargetGroupID string `json:"targetGroupId"` + Weight int32 `json:"weight"` } // RuleMatch is the match conditions for a listener rule. type RuleMatch struct { - HTTPMethod string - PathMatchType string - PathMatchValue string - HeaderMatches []*HeaderMatch + HeaderMatches []*HeaderMatch `json:"headerMatches,omitempty"` + HTTPMethod string `json:"httpMethod,omitempty"` + PathMatchType string `json:"pathMatchType,omitempty"` + PathMatchValue string `json:"pathMatchValue,omitempty"` } // HeaderMatch is an HTTP header match condition. type HeaderMatch struct { - Name string - MatchType string - MatchValue string - CaseSensitive bool + Name string `json:"name"` + MatchType string `json:"matchType"` + MatchValue string `json:"matchValue"` + CaseSensitive bool `json:"caseSensitive"` } // TargetGroup represents a VPC Lattice target group. @@ -379,27 +379,27 @@ type TargetGroupSummary struct { // TargetGroupConfig is the configuration for a target group. type TargetGroupConfig struct { - Port int32 - Protocol string - ProtocolVersion string - VpcID string - HealthCheck *HealthCheckConfig - IPAddressType string - LambdaEventStructureVersion string + HealthCheck *HealthCheckConfig `json:"healthCheck,omitempty"` + Protocol string `json:"protocol,omitempty"` + ProtocolVersion string `json:"protocolVersion,omitempty"` + VpcID string `json:"vpcId,omitempty"` + IPAddressType string `json:"ipAddressType,omitempty"` + LambdaEventStructureVersion string `json:"lambdaEventStructureVersion,omitempty"` + Port int32 `json:"port,omitempty"` } // HealthCheckConfig is the health check configuration for a target group. type HealthCheckConfig struct { - Enabled bool - Protocol string - ProtocolVersion string - Path string - Port int32 - HealthyThresholdCount int32 - UnhealthyThresholdCount int32 - HealthCheckIntervalSeconds int32 - HealthCheckTimeoutSeconds int32 - MatcherHTTPCode string + Protocol string `json:"protocol,omitempty"` + ProtocolVersion string `json:"protocolVersion,omitempty"` + Path string `json:"path,omitempty"` + MatcherHTTPCode string `json:"matcherHttpCode,omitempty"` + Port int32 `json:"port,omitempty"` + HealthyThresholdCount int32 `json:"healthyThresholdCount,omitempty"` + UnhealthyThresholdCount int32 `json:"unhealthyThresholdCount,omitempty"` + HealthCheckIntervalSeconds int32 `json:"healthCheckIntervalSeconds,omitempty"` + HealthCheckTimeoutSeconds int32 `json:"healthCheckTimeoutSeconds,omitempty"` + Enabled bool `json:"enabled,omitempty"` } // Target is a target registered to a target group. From 19b416bf5354375fa681caea05857cd04372ea5d Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 21:56:20 -0500 Subject: [PATCH 5/7] WIP: checkpoint (auto) --- services/vpclattice/backend.go | 24 +- services/vpclattice/handler.go | 409 ++++++++++++++++-------------- services/vpclattice/interfaces.go | 24 +- 3 files changed, 238 insertions(+), 219 deletions(-) diff --git a/services/vpclattice/backend.go b/services/vpclattice/backend.go index 658d75892..6612ddfa5 100644 --- a/services/vpclattice/backend.go +++ b/services/vpclattice/backend.go @@ -203,10 +203,10 @@ func (s *storedSNSA) toSummary() *ServiceNetworkServiceAssociationSummary { // storedSNVA holds a service network VPC association. type storedSNVA struct { - CreatedAt time.Time `json:"createdAt"` - LastUpdatedAt time.Time `json:"lastUpdatedAt"` Tags map[string]string `json:"tags"` SecurityGroupIDs []string `json:"securityGroupIds"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` ARN string `json:"arn"` ID string `json:"id"` VpcID string `json:"vpcId"` @@ -251,9 +251,10 @@ func (s *storedSNVA) toSummary() *ServiceNetworkVpcAssociationSummary { // storedListener holds a listener. type storedListener struct { + Tags map[string]string `json:"tags"` + DefaultAction *RuleAction `json:"defaultAction"` CreatedAt time.Time `json:"createdAt"` LastUpdatedAt time.Time `json:"lastUpdatedAt"` - Tags map[string]string `json:"tags"` ARN string `json:"arn"` ID string `json:"id"` ServiceARN string `json:"serviceArn"` @@ -261,7 +262,6 @@ type storedListener struct { Name string `json:"name"` Protocol string `json:"protocol"` Port int32 `json:"port"` - DefaultAction *RuleAction `json:"defaultAction"` } func (l *storedListener) toListener() *Listener { @@ -293,17 +293,17 @@ func (l *storedListener) toSummary() *ListenerSummary { // storedRule holds a listener rule. type storedRule struct { + Tags map[string]string `json:"tags"` + Action *RuleAction `json:"action"` + Match *RuleMatch `json:"match"` CreatedAt time.Time `json:"createdAt"` LastUpdatedAt time.Time `json:"lastUpdatedAt"` - Tags map[string]string `json:"tags"` ARN string `json:"arn"` ID string `json:"id"` ListenerID string `json:"listenerId"` ServiceID string `json:"serviceId"` Name string `json:"name"` Priority int32 `json:"priority"` - Action *RuleAction `json:"action"` - Match *RuleMatch `json:"match"` IsDefault bool `json:"isDefault"` } @@ -333,16 +333,16 @@ func (r *storedRule) toSummary() *RuleSummary { // storedTargetGroup holds a target group. type storedTargetGroup struct { - CreatedAt time.Time `json:"createdAt"` - LastUpdatedAt time.Time `json:"lastUpdatedAt"` Tags map[string]string `json:"tags"` + Config *TargetGroupConfig `json:"config"` ServiceARNs []string `json:"serviceArns"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` ARN string `json:"arn"` ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` - Config *TargetGroupConfig `json:"config"` } func (tg *storedTargetGroup) toTargetGroup() *TargetGroup { @@ -386,8 +386,8 @@ func (tg *storedTargetGroup) toSummary() *TargetGroupSummary { // storedTarget holds a registered target. type storedTarget struct { ID string `json:"id"` - Port int32 `json:"port"` Status string `json:"status"` + Port int32 `json:"port"` } // storedALS holds an access log subscription. @@ -1905,7 +1905,7 @@ func (b *InMemoryBackend) RegisterTargets( } // DeregisterTargets deregisters targets from a target group. -func (b *InMemoryBackend) DeregisterTargets( +func (b *InMemoryBackend) DeregisterTargets( //nolint:gocognit // target deregistration logic is inherently complex tgID string, targets []*Target, ) ([]*TargetFailure, error) { diff --git a/services/vpclattice/handler.go b/services/vpclattice/handler.go index 07c3a92dd..18823f80e 100644 --- a/services/vpclattice/handler.go +++ b/services/vpclattice/handler.go @@ -30,6 +30,25 @@ const ( keyMessage = "message" + keyARN = "arn" + keyName = "name" + keyItems = "items" + keyCreatedAt = "createdAt" + keyLastUpdatedAt = "lastUpdatedAt" + keyServiceARN = "serviceArn" + keyServiceID = "serviceId" + keyServiceNetworkARN = "serviceNetworkArn" + keyServiceNetworkID = "serviceNetworkId" + keyServiceNetworkName = "serviceNetworkName" + keyVPCID = "vpcId" + keyProtocol = "protocol" + keyPort = "port" + keyPriority = "priority" + keyIsDefault = "isDefault" + keyPolicy = "policy" + keyUnsuccessful = "unsuccessful" + keyNameRequired = "name is required" + opBatchUpdateRule = "BatchUpdateRule" opCreateALS = "CreateAccessLogSubscription" opCreateListener = "CreateListener" @@ -207,7 +226,7 @@ func (h *Handler) Handler() echo.HandlerFunc { } } -func (h *Handler) handleREST(c *echo.Context) error { //nolint:gocognit,gocyclo // large routing dispatch is expected +func (h *Handler) handleREST(c *echo.Context) error { //nolint:gocognit,gocyclo,cyclop // large routing dispatch is expected op, id1, id2, id3 := classifyPath(c.Request().Method, c.Request().URL.Path) var body map[string]any @@ -349,9 +368,9 @@ func (h *Handler) handleError(c *echo.Context, err error) error { // ------- Service handlers ------- func (h *Handler) handleCreateService(c *echo.Context, body map[string]any) error { - name, _ := body["name"].(string) + name, _ := body[keyName].(string) if name == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name is required"}) + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: keyNameRequired}) } authType, _ := body["authType"].(string) @@ -398,7 +417,7 @@ func (h *Handler) handleDeleteService(c *echo.Context, id string) error { } func (h *Handler) handleListServices(c *echo.Context) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListServices(maxResults, nextToken) @@ -411,7 +430,7 @@ func (h *Handler) handleListServices(c *echo.Context) error { summaries = append(summaries, serviceSummaryToJSON(s)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -422,9 +441,9 @@ func (h *Handler) handleListServices(c *echo.Context) error { // ------- ServiceNetwork handlers ------- func (h *Handler) handleCreateServiceNetwork(c *echo.Context, body map[string]any) error { - name, _ := body["name"].(string) + name, _ := body[keyName].(string) if name == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name is required"}) + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: keyNameRequired}) } authType, _ := body["authType"].(string) @@ -471,7 +490,7 @@ func (h *Handler) handleDeleteServiceNetwork(c *echo.Context, id string) error { } func (h *Handler) handleListServiceNetworks(c *echo.Context) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListServiceNetworks(maxResults, nextToken) @@ -484,7 +503,7 @@ func (h *Handler) handleListServiceNetworks(c *echo.Context) error { summaries = append(summaries, serviceNetworkSummaryToJSON(s)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -531,11 +550,11 @@ func (h *Handler) handleDeleteSNSA(c *echo.Context, id string) error { return h.handleError(c, err) } - return c.JSON(http.StatusOK, map[string]any{"status": "DELETE_IN_PROGRESS"}) + return c.JSON(http.StatusOK, map[string]any{"status": statusDeleteInProgress}) } func (h *Handler) handleListSNSAs(c *echo.Context) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") snID := c.QueryParam("serviceNetworkIdentifier") svcID := c.QueryParam("serviceIdentifier") @@ -555,7 +574,7 @@ func (h *Handler) handleListSNSAs(c *echo.Context) error { summaries = append(summaries, snsaSummaryToJSON(s)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -627,11 +646,11 @@ func (h *Handler) handleDeleteSNVA(c *echo.Context, id string) error { return h.handleError(c, err) } - return c.JSON(http.StatusOK, map[string]any{"status": "DELETE_IN_PROGRESS"}) + return c.JSON(http.StatusOK, map[string]any{"status": statusDeleteInProgress}) } func (h *Handler) handleListSNVAs(c *echo.Context) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") snID := c.QueryParam("serviceNetworkIdentifier") vpcID := c.QueryParam("vpcIdentifier") @@ -651,7 +670,7 @@ func (h *Handler) handleListSNVAs(c *echo.Context) error { summaries = append(summaries, snvaSummaryToJSON(s)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -666,8 +685,8 @@ func (h *Handler) handleCreateListener( serviceID string, body map[string]any, ) error { - name, _ := body["name"].(string) - protocol, _ := body["protocol"].(string) + name, _ := body[keyName].(string) + protocol, _ := body[keyProtocol].(string) if name == "" || protocol == "" { return c.JSON( @@ -676,7 +695,7 @@ func (h *Handler) handleCreateListener( ) } - port := bodyInt32(body, "port") + port := bodyInt32(body, keyPort) defaultAction := extractRuleAction(body, "defaultAction") tags := extractTags(body) @@ -721,7 +740,7 @@ func (h *Handler) handleDeleteListener(c *echo.Context, serviceID, listenerID st } func (h *Handler) handleListListeners(c *echo.Context, serviceID string) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListListeners(serviceID, maxResults, nextToken) @@ -734,7 +753,7 @@ func (h *Handler) handleListListeners(c *echo.Context, serviceID string) error { summaries = append(summaries, listenerSummaryToJSON(l)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -749,12 +768,12 @@ func (h *Handler) handleCreateRule( serviceID, listenerID string, body map[string]any, ) error { - name, _ := body["name"].(string) + name, _ := body[keyName].(string) if name == "" { - return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: "name is required"}) + return c.JSON(http.StatusBadRequest, map[string]any{keyMessage: keyNameRequired}) } - priority := bodyInt32(body, "priority") + priority := bodyInt32(body, keyPriority) action := extractRuleAction(body, "action") match := extractRuleMatch(body, "match") tags := extractTags(body) @@ -781,7 +800,7 @@ func (h *Handler) handleUpdateRule( serviceID, listenerID, ruleID string, body map[string]any, ) error { - priority := bodyInt32(body, "priority") + priority := bodyInt32(body, keyPriority) action := extractRuleAction(body, "action") match := extractRuleMatch(body, "match") @@ -802,7 +821,7 @@ func (h *Handler) handleDeleteRule(c *echo.Context, serviceID, listenerID, ruleI } func (h *Handler) handleListRules(c *echo.Context, serviceID, listenerID string) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListRules(serviceID, listenerID, maxResults, nextToken) @@ -815,7 +834,7 @@ func (h *Handler) handleListRules(c *echo.Context, serviceID, listenerID string) summaries = append(summaries, ruleSummaryToJSON(r)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -835,7 +854,7 @@ func (h *Handler) handleBatchUpdateRule( if m, ok2 := raw.(map[string]any); ok2 { u := &RuleUpdate{} u.RuleIdentifier, _ = m["ruleIdentifier"].(string) - u.Priority = bodyInt32(m, "priority") + u.Priority = bodyInt32(m, keyPriority) u.Action = extractRuleAction(m, "action") u.Match = extractRuleMatch(m, "match") updates = append(updates, u) @@ -863,15 +882,15 @@ func (h *Handler) handleBatchUpdateRule( } return c.JSON(http.StatusOK, map[string]any{ - "successful": successList, - "unsuccessful": failureList, + "successful": successList, + keyUnsuccessful: failureList, }) } // ------- TargetGroup handlers ------- func (h *Handler) handleCreateTargetGroup(c *echo.Context, body map[string]any) error { - name, _ := body["name"].(string) + name, _ := body[keyName].(string) tgType, _ := body["type"].(string) if name == "" || tgType == "" { @@ -920,11 +939,11 @@ func (h *Handler) handleDeleteTargetGroup(c *echo.Context, id string) error { return h.handleError(c, err) } - return c.JSON(http.StatusOK, map[string]any{"id": id, "status": "DELETE_IN_PROGRESS"}) + return c.JSON(http.StatusOK, map[string]any{"id": id, "status": statusDeleteInProgress}) } func (h *Handler) handleListTargetGroups(c *echo.Context) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") tgType := c.QueryParam("targetGroupType") svcArn := c.QueryParam("serviceArn") @@ -939,7 +958,7 @@ func (h *Handler) handleListTargetGroups(c *echo.Context) error { summaries = append(summaries, targetGroupSummaryToJSON(tg)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -960,7 +979,7 @@ func (h *Handler) handleRegisterTargets(c *echo.Context, tgID string, body map[s failureList = append(failureList, targetFailureToJSON(f)) } - return c.JSON(http.StatusOK, map[string]any{"unsuccessful": failureList}) + return c.JSON(http.StatusOK, map[string]any{keyUnsuccessful: failureList}) } func (h *Handler) handleDeregisterTargets(c *echo.Context, tgID string, body map[string]any) error { @@ -976,11 +995,11 @@ func (h *Handler) handleDeregisterTargets(c *echo.Context, tgID string, body map failureList = append(failureList, targetFailureToJSON(f)) } - return c.JSON(http.StatusOK, map[string]any{"unsuccessful": failureList}) + return c.JSON(http.StatusOK, map[string]any{keyUnsuccessful: failureList}) } func (h *Handler) handleListTargets(c *echo.Context, tgID string, _ map[string]any) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") items, next, err := h.Backend.ListTargets(tgID, maxResults, nextToken) @@ -993,7 +1012,7 @@ func (h *Handler) handleListTargets(c *echo.Context, tgID string, _ map[string]a summaries = append(summaries, targetSummaryToJSON(t)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -1054,7 +1073,7 @@ func (h *Handler) handleDeleteALS(c *echo.Context, id string) error { } func (h *Handler) handleListALSs(c *echo.Context) error { - maxResults := queryInt32(c, 0) + maxResults := queryInt32(c) nextToken := c.QueryParam("nextToken") resourceID := c.QueryParam("resourceIdentifier") @@ -1068,7 +1087,7 @@ func (h *Handler) handleListALSs(c *echo.Context) error { summaries = append(summaries, alsSummaryToJSON(a)) } - resp := map[string]any{"items": summaries} + resp := map[string]any{keyItems: summaries} if next != "" { resp["nextToken"] = next } @@ -1083,7 +1102,7 @@ func (h *Handler) handlePutAuthPolicy( resourceID string, body map[string]any, ) error { - policy, _ := body["policy"].(string) + policy, _ := body[keyPolicy].(string) ap, err := h.Backend.PutAuthPolicy(resourceID, policy) if err != nil { @@ -1091,8 +1110,8 @@ func (h *Handler) handlePutAuthPolicy( } return c.JSON(http.StatusOK, map[string]any{ - "policy": ap.Policy, - "state": ap.State, + keyPolicy: ap.Policy, + "state": ap.State, }) } @@ -1103,8 +1122,8 @@ func (h *Handler) handleGetAuthPolicy(c *echo.Context, resourceID string) error } return c.JSON(http.StatusOK, map[string]any{ - "policy": ap.Policy, - "state": ap.State, + keyPolicy: ap.Policy, + "state": ap.State, }) } @@ -1121,7 +1140,7 @@ func (h *Handler) handlePutResourcePolicy( resourceArn string, body map[string]any, ) error { - policy, _ := body["policy"].(string) + policy, _ := body[keyPolicy].(string) if err := h.Backend.PutResourcePolicy(resourceArn, policy); err != nil { return h.handleError(c, err) @@ -1136,7 +1155,7 @@ func (h *Handler) handleGetResourcePolicy(c *echo.Context, resourceArn string) e return h.handleError(c, err) } - return c.JSON(http.StatusOK, map[string]any{"policy": policy}) + return c.JSON(http.StatusOK, map[string]any{keyPolicy: policy}) } func (h *Handler) handleDeleteResourcePolicy(c *echo.Context, resourceArn string) error { @@ -1193,9 +1212,9 @@ func (h *Handler) handleListTagsForResource(c *echo.Context, resourceArn string) // classifyPath maps (method, path) → (op, id1, id2, id3). // id1..id3 are path segments in order (service, listener, rule etc.). -func classifyPath( //nolint:gocognit,gocyclo // large routing dispatch is expected +func classifyPath( //nolint:gocognit,gocyclo,cyclop // large routing dispatch is expected method, path string, -) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity +) (op, id1, id2, id3 string) { switch { case path == pathServices: if method == http.MethodPost { @@ -1289,9 +1308,9 @@ func classifyPath( //nolint:gocognit,gocyclo // large routing dispatch is expect } // classifyServicePath handles /services/{serviceID}[/listeners[/...]]. -func classifyServicePath( //nolint:gocognit,gocyclo // large routing dispatch is expected +func classifyServicePath( //nolint:gocognit,gocyclo,cyclop,nestif // large routing dispatch is expected method, path string, -) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity +) (op, id1, id2, id3 string) { rest := strings.TrimPrefix(path, pathServices+"/") serviceID, sub, hasSub := strings.Cut(rest, "/") @@ -1363,7 +1382,7 @@ func classifyServicePath( //nolint:gocognit,gocyclo // large routing dispatch is // classifyServiceNetworkPath handles /servicenetworks/{id}. func classifyServiceNetworkPath( method, path string, -) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity +) (string, string, string, string) { id := strings.TrimPrefix(path, pathServiceNetworks+"/") switch method { case http.MethodGet: @@ -1380,7 +1399,7 @@ func classifyServiceNetworkPath( // classifySNSAPath handles /servicenetworkserviceassociations/{id}. func classifySNSAPath( method, path string, -) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity +) (string, string, string, string) { id := strings.TrimPrefix(path, pathServiceNetworkServiceAssociations+"/") switch method { case http.MethodGet: @@ -1395,7 +1414,7 @@ func classifySNSAPath( // classifySNVAPath handles /servicenetworkvpcassociations/{id}. func classifySNVAPath( method, path string, -) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity +) (string, string, string, string) { id := strings.TrimPrefix(path, pathServiceNetworkVpcAssociations+"/") switch method { case http.MethodGet: @@ -1412,7 +1431,7 @@ func classifySNVAPath( // classifyTargetGroupPath handles /targetgroups/{id}[/registertargets|deregistertargets|listtargets]. func classifyTargetGroupPath( method, path string, -) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity +) (string, string, string, string) { rest := strings.TrimPrefix(path, pathTargetGroups+"/") tgID, sub, hasSub := strings.Cut(rest, "/") @@ -1444,7 +1463,7 @@ func classifyTargetGroupPath( // classifyALSPath handles /accesslogsubscriptions/{id}. func classifyALSPath( method, path string, -) (op, id1, id2, id3 string) { //nolint:nonamedreturns // path dispatch needs named returns for clarity +) (string, string, string, string) { id := strings.TrimPrefix(path, pathAccessLogSubscriptions+"/") switch method { case http.MethodGet: @@ -1462,14 +1481,14 @@ func classifyALSPath( func serviceToJSON(s *Service) map[string]any { m := map[string]any{ - "arn": s.ARN, - "id": s.ID, - "name": s.Name, - "authType": s.AuthType, - "status": s.Status, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), - "dnsEntry": map[string]any{"domainName": s.DNSName}, + keyARN: s.ARN, + "id": s.ID, + keyName: s.Name, + "authType": s.AuthType, + "status": s.Status, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + "dnsEntry": map[string]any{"domainName": s.DNSName}, } if s.CertificateArn != "" { @@ -1485,11 +1504,11 @@ func serviceToJSON(s *Service) map[string]any { func serviceSummaryToJSON(s *ServiceSummary) map[string]any { m := map[string]any{ - "arn": s.ARN, - "id": s.ID, - "name": s.Name, - "status": s.Status, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyName: s.Name, + "status": s.Status, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } if s.DNSName != "" { @@ -1505,56 +1524,56 @@ func serviceSummaryToJSON(s *ServiceSummary) map[string]any { func serviceNetworkToJSON(s *ServiceNetwork) map[string]any { return map[string]any{ - "arn": s.ARN, - "id": s.ID, - "name": s.Name, - "authType": s.AuthType, - "numberOfAssociatedServices": s.NumberOfAssociatedServices, - "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyName: s.Name, + "authType": s.AuthType, + "numberOfAssociatedServices": s.NumberOfAssociatedServices, + "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } } func serviceNetworkSummaryToJSON(s *ServiceNetworkSummary) map[string]any { return map[string]any{ - "arn": s.ARN, - "id": s.ID, - "name": s.Name, - "numberOfAssociatedServices": s.NumberOfAssociatedServices, - "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyName: s.Name, + "numberOfAssociatedServices": s.NumberOfAssociatedServices, + "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } func snsaToJSON(s *ServiceNetworkServiceAssociation) map[string]any { return map[string]any{ - "arn": s.ARN, - "id": s.ID, - "serviceArn": s.ServiceARN, - "serviceId": s.ServiceID, - "serviceName": s.ServiceName, - "serviceNetworkArn": s.ServiceNetworkARN, - "serviceNetworkId": s.ServiceNetworkID, - "serviceNetworkName": s.ServiceNetworkName, - "status": s.Status, - "createdBy": s.CreatedBy, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyServiceARN: s.ServiceARN, + keyServiceID: s.ServiceID, + "serviceName": s.ServiceName, + keyServiceNetworkARN: s.ServiceNetworkARN, + keyServiceNetworkID: s.ServiceNetworkID, + keyServiceNetworkName: s.ServiceNetworkName, + "status": s.Status, + "createdBy": s.CreatedBy, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } func snsaSummaryToJSON(s *ServiceNetworkServiceAssociationSummary) map[string]any { return map[string]any{ - "arn": s.ARN, - "id": s.ID, - "serviceArn": s.ServiceARN, - "serviceId": s.ServiceID, - "serviceName": s.ServiceName, - "serviceNetworkArn": s.ServiceNetworkARN, - "serviceNetworkId": s.ServiceNetworkID, - "serviceNetworkName": s.ServiceNetworkName, - "status": s.Status, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyServiceARN: s.ServiceARN, + keyServiceID: s.ServiceID, + "serviceName": s.ServiceName, + keyServiceNetworkARN: s.ServiceNetworkARN, + keyServiceNetworkID: s.ServiceNetworkID, + keyServiceNetworkName: s.ServiceNetworkName, + "status": s.Status, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } @@ -1563,44 +1582,44 @@ func snvaToJSON(s *ServiceNetworkVpcAssociation) map[string]any { copy(sgs, s.SecurityGroupIDs) return map[string]any{ - "arn": s.ARN, - "id": s.ID, - "vpcId": s.VpcID, - "serviceNetworkArn": s.ServiceNetworkARN, - "serviceNetworkId": s.ServiceNetworkID, - "serviceNetworkName": s.ServiceNetworkName, - "securityGroupIds": sgs, - "status": s.Status, - "createdBy": s.CreatedBy, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyVPCID: s.VpcID, + keyServiceNetworkARN: s.ServiceNetworkARN, + keyServiceNetworkID: s.ServiceNetworkID, + keyServiceNetworkName: s.ServiceNetworkName, + "securityGroupIds": sgs, + "status": s.Status, + "createdBy": s.CreatedBy, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } } func snvaSummaryToJSON(s *ServiceNetworkVpcAssociationSummary) map[string]any { return map[string]any{ - "arn": s.ARN, - "id": s.ID, - "vpcId": s.VpcID, - "serviceNetworkArn": s.ServiceNetworkARN, - "serviceNetworkId": s.ServiceNetworkID, - "serviceNetworkName": s.ServiceNetworkName, - "status": s.Status, - "createdAt": s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyVPCID: s.VpcID, + keyServiceNetworkARN: s.ServiceNetworkARN, + keyServiceNetworkID: s.ServiceNetworkID, + keyServiceNetworkName: s.ServiceNetworkName, + "status": s.Status, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } func listenerToJSON(l *Listener) map[string]any { m := map[string]any{ - "arn": l.ARN, - "id": l.ID, - "serviceArn": l.ServiceARN, - "serviceId": l.ServiceID, - "name": l.Name, - "protocol": l.Protocol, - "port": l.Port, - "createdAt": l.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": l.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: l.ARN, + "id": l.ID, + keyServiceARN: l.ServiceARN, + keyServiceID: l.ServiceID, + keyName: l.Name, + keyProtocol: l.Protocol, + keyPort: l.Port, + keyCreatedAt: l.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: l.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } if l.DefaultAction != nil { @@ -1612,25 +1631,25 @@ func listenerToJSON(l *Listener) map[string]any { func listenerSummaryToJSON(l *ListenerSummary) map[string]any { return map[string]any{ - "arn": l.ARN, - "id": l.ID, - "name": l.Name, - "protocol": l.Protocol, - "port": l.Port, - "createdAt": l.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": l.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: l.ARN, + "id": l.ID, + keyName: l.Name, + keyProtocol: l.Protocol, + keyPort: l.Port, + keyCreatedAt: l.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: l.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } } func ruleToJSON(r *Rule) map[string]any { m := map[string]any{ - "arn": r.ARN, - "id": r.ID, - "name": r.Name, - "priority": r.Priority, - "isDefault": r.IsDefault, - "createdAt": r.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": r.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: r.ARN, + "id": r.ID, + keyName: r.Name, + keyPriority: r.Priority, + keyIsDefault: r.IsDefault, + keyCreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: r.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } if r.Action != nil { @@ -1646,21 +1665,21 @@ func ruleToJSON(r *Rule) map[string]any { func ruleSummaryToJSON(r *RuleSummary) map[string]any { return map[string]any{ - "arn": r.ARN, - "id": r.ID, - "name": r.Name, - "priority": r.Priority, - "isDefault": r.IsDefault, + keyARN: r.ARN, + "id": r.ID, + keyName: r.Name, + keyPriority: r.Priority, + keyIsDefault: r.IsDefault, } } func ruleUpdateSuccessToJSON(r *RuleUpdateSuccess) map[string]any { m := map[string]any{ - "arn": r.ARN, - "id": r.ID, - "name": r.Name, - "priority": r.Priority, - "isDefault": r.IsDefault, + keyARN: r.ARN, + "id": r.ID, + keyName: r.Name, + keyPriority: r.Priority, + keyIsDefault: r.IsDefault, } if r.Action != nil { @@ -1721,7 +1740,7 @@ func ruleMatchToJSON(m *RuleMatch) map[string]any { headers := make([]any, 0, len(m.HeaderMatches)) for _, h := range m.HeaderMatches { headers = append(headers, map[string]any{ - "name": h.Name, + keyName: h.Name, "match": map[string]any{h.MatchType: h.MatchValue}, }) } @@ -1734,14 +1753,14 @@ func ruleMatchToJSON(m *RuleMatch) map[string]any { func targetGroupToJSON(tg *TargetGroup) map[string]any { m := map[string]any{ - "arn": tg.ARN, - "id": tg.ID, - "name": tg.Name, - "type": tg.Type, - "status": tg.Status, - "serviceArns": tg.ServiceARNs, - "createdAt": tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": tg.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: tg.ARN, + "id": tg.ID, + keyName: tg.Name, + "type": tg.Type, + "status": tg.Status, + "serviceArns": tg.ServiceARNs, + keyCreatedAt: tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: tg.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } if tg.Config != nil { @@ -1753,25 +1772,25 @@ func targetGroupToJSON(tg *TargetGroup) map[string]any { func targetGroupSummaryToJSON(tg *TargetGroupSummary) map[string]any { return map[string]any{ - "arn": tg.ARN, - "id": tg.ID, - "name": tg.Name, - "type": tg.Type, - "status": tg.Status, - "port": tg.Port, - "protocol": tg.Protocol, - "vpcId": tg.VpcID, + keyARN: tg.ARN, + "id": tg.ID, + keyName: tg.Name, + "type": tg.Type, + "status": tg.Status, + keyPort: tg.Port, + keyProtocol: tg.Protocol, + keyVPCID: tg.VpcID, "serviceArns": tg.ServiceARNs, - "createdAt": tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyCreatedAt: tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } func targetGroupConfigToJSON(c *TargetGroupConfig) map[string]any { m := map[string]any{ - "port": c.Port, - "protocol": c.Protocol, + keyPort: c.Port, + keyProtocol: c.Protocol, "protocolVersion": c.ProtocolVersion, - "vpcIdentifier": c.VpcID, + "vpcIdentifier": c.VpcID, } if c.HealthCheck != nil { @@ -1784,9 +1803,9 @@ func targetGroupConfigToJSON(c *TargetGroupConfig) map[string]any { func healthCheckToJSON(hc *HealthCheckConfig) map[string]any { return map[string]any{ "enabled": hc.Enabled, - "protocol": hc.Protocol, + keyProtocol: hc.Protocol, "path": hc.Path, - "port": hc.Port, + keyPort: hc.Port, "healthyThresholdCount": hc.HealthyThresholdCount, "unhealthyThresholdCount": hc.UnhealthyThresholdCount, "healthCheckIntervalSeconds": hc.HealthCheckIntervalSeconds, @@ -1797,7 +1816,7 @@ func healthCheckToJSON(hc *HealthCheckConfig) map[string]any { func targetSummaryToJSON(t *TargetSummary) map[string]any { return map[string]any{ "id": t.ID, - "port": t.Port, + keyPort: t.Port, "status": t.Status, "reasonCode": t.ReasonCode, } @@ -1806,7 +1825,7 @@ func targetSummaryToJSON(t *TargetSummary) map[string]any { func targetFailureToJSON(f *TargetFailure) map[string]any { return map[string]any{ "id": f.ID, - "port": f.Port, + keyPort: f.Port, "code": f.Code, "message": f.Message, } @@ -1814,24 +1833,24 @@ func targetFailureToJSON(f *TargetFailure) map[string]any { func alsToJSON(a *AccessLogSubscription) map[string]any { return map[string]any{ - "arn": a.ARN, + keyARN: a.ARN, "id": a.ID, "resourceArn": a.ResourceARN, "resourceId": a.ResourceID, "destinationArn": a.DestinationARN, - "createdAt": a.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - "lastUpdatedAt": a.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyCreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: a.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } } func alsSummaryToJSON(a *AccessLogSubscriptionSummary) map[string]any { return map[string]any{ - "arn": a.ARN, + keyARN: a.ARN, "id": a.ID, "resourceArn": a.ResourceARN, "resourceId": a.ResourceID, "destinationArn": a.DestinationARN, - "createdAt": a.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyCreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } @@ -1897,7 +1916,7 @@ func extractRuleAction(body map[string]any, key string) *RuleAction { return action } -func extractRuleMatch(body map[string]any, key string) *RuleMatch { //nolint:gocognit // nested JSON extraction +func extractRuleMatch(body map[string]any, key string) *RuleMatch { //nolint:gocognit,nestif // nested JSON extraction raw, ok := body[key].(map[string]any) if !ok { return nil @@ -1923,7 +1942,7 @@ func extractRuleMatch(body map[string]any, key string) *RuleMatch { //nolint:goc for _, hRaw := range headersRaw { if hMap, ok4 := hRaw.(map[string]any); ok4 { hm := &HeaderMatch{} - hm.Name, _ = hMap["name"].(string) + hm.Name, _ = hMap[keyName].(string) if matchRaw, ok5 := hMap["match"].(map[string]any); ok5 { for k, v := range matchRaw { if s, ok6 := v.(string); ok6 { @@ -1949,8 +1968,8 @@ func extractTargetGroupConfig(body map[string]any) *TargetGroupConfig { } cfg := &TargetGroupConfig{} - cfg.Port = bodyInt32(raw, "port") - cfg.Protocol, _ = raw["protocol"].(string) + cfg.Port = bodyInt32(raw, keyPort) + cfg.Protocol, _ = raw[keyProtocol].(string) cfg.ProtocolVersion, _ = raw["protocolVersion"].(string) cfg.VpcID, _ = raw["vpcIdentifier"].(string) cfg.IPAddressType, _ = raw["ipAddressType"].(string) @@ -1969,10 +1988,10 @@ func extractHealthCheckConfig(raw map[string]any) *HealthCheckConfig { hc.Enabled = v } - hc.Protocol, _ = raw["protocol"].(string) + hc.Protocol, _ = raw[keyProtocol].(string) hc.ProtocolVersion, _ = raw["protocolVersion"].(string) hc.Path, _ = raw["path"].(string) - hc.Port = bodyInt32(raw, "port") + hc.Port = bodyInt32(raw, keyPort) hc.HealthyThresholdCount = bodyInt32(raw, "healthyThresholdCount") hc.UnhealthyThresholdCount = bodyInt32(raw, "unhealthyThresholdCount") hc.HealthCheckIntervalSeconds = bodyInt32(raw, "healthCheckIntervalSeconds") @@ -1989,7 +2008,7 @@ func extractTargets(body map[string]any) []*Target { if tMap, ok2 := tRaw.(map[string]any); ok2 { t := &Target{} t.ID, _ = tMap["id"].(string) - t.Port = bodyInt32(tMap, "port") + t.Port = bodyInt32(tMap, keyPort) targets = append(targets, t) } } @@ -1998,15 +2017,15 @@ func extractTargets(body map[string]any) []*Target { return targets } -func queryInt32(c *echo.Context, fallback int32) int32 { +func queryInt32(c *echo.Context) int32 { v := c.QueryParam("maxResults") if v == "" { - return fallback + return 0 } n, err := strconv.ParseInt(v, 10, 32) if err != nil { - return fallback + return 0 } return int32(n) //nolint:gosec // value is bounded by ParseInt with bitSize=32 diff --git a/services/vpclattice/interfaces.go b/services/vpclattice/interfaces.go index 90fcde0cd..9ab859d95 100644 --- a/services/vpclattice/interfaces.go +++ b/services/vpclattice/interfaces.go @@ -223,6 +223,7 @@ type ServiceNetworkServiceAssociationSummary struct { // ServiceNetworkVpcAssociation is a VPC-to-service-network association. type ServiceNetworkVpcAssociation struct { + SecurityGroupIDs []string CreatedAt time.Time LastUpdatedAt time.Time ARN string @@ -231,7 +232,6 @@ type ServiceNetworkVpcAssociation struct { ServiceNetworkARN string ServiceNetworkID string ServiceNetworkName string - SecurityGroupIDs []string Status string CreatedBy string } @@ -250,6 +250,7 @@ type ServiceNetworkVpcAssociationSummary struct { // Listener represents a VPC Lattice listener. type Listener struct { + DefaultAction *RuleAction CreatedAt time.Time LastUpdatedAt time.Time ARN string @@ -259,7 +260,6 @@ type Listener struct { Name string Protocol string Port int32 - DefaultAction *RuleAction } // ListenerSummary is a listener entry for list responses. @@ -275,14 +275,14 @@ type ListenerSummary struct { // Rule represents a VPC Lattice listener rule. type Rule struct { + Action *RuleAction + Match *RuleMatch CreatedAt time.Time LastUpdatedAt time.Time ARN string ID string Name string Priority int32 - Action *RuleAction - Match *RuleMatch IsDefault bool } @@ -297,21 +297,21 @@ type RuleSummary struct { // RuleUpdate is an update spec for BatchUpdateRule. type RuleUpdate struct { - RuleIdentifier string - Priority int32 Action *RuleAction Match *RuleMatch + RuleIdentifier string + Priority int32 } // RuleUpdateSuccess is a successful rule update result. type RuleUpdateSuccess struct { + Action *RuleAction + Match *RuleMatch ARN string ID string Name string Priority int32 IsDefault bool - Action *RuleAction - Match *RuleMatch } // RuleUpdateFailure is a failed rule update result. @@ -365,16 +365,16 @@ type TargetGroup struct { // TargetGroupSummary is a target group entry for list responses. type TargetGroupSummary struct { + ServiceARNs []string CreatedAt time.Time ARN string ID string Name string Type string Status string - Port int32 Protocol string VpcID string - ServiceARNs []string + Port int32 } // TargetGroupConfig is the configuration for a target group. @@ -411,17 +411,17 @@ type Target struct { // TargetSummary is a target entry for list responses. type TargetSummary struct { ID string - Port int32 Status string ReasonCode string + Port int32 } // TargetFailure is a target registration/deregistration failure. type TargetFailure struct { ID string - Port int32 Code string Message string + Port int32 } // AccessLogSubscription represents a VPC Lattice access log subscription. From 1756a2d54172b188a0f8451348f99fb1a6e5dfa1 Mon Sep 17 00:00:00 2001 From: garnet Date: Fri, 12 Jun 2026 22:04:44 -0500 Subject: [PATCH 6/7] fix(vpclattice): resolve all golangci-lint issues (go-uvn2) - Define op name constants to eliminate 50+ goconst violations - Define JSON key constants (keyARN, keyStatus, keyItems, etc.) - Fix struct fieldalignment in backend.go and interfaces.go - Replace named returns with unnamed returns in classify* functions - Apply modernize fixes: slices.Contains, maps.Copy, strings.CutPrefix - Fix shadow variable declarations throughout handler.go - Add gosec nolint for bounded int32 conversions - Fix funlen/cyclop/gocyclo nolints on routing dispatch functions - Fix unused nolint directives (nolintlint) - Fix testifylint float-compare: use assert.InDelta - Fix staticcheck SA4010 unused append in DeregisterTargets - Add terraform test fixture and TestTerraform_VPCLattice integration test Co-Authored-By: Claude Sonnet 4.6 --- services/vpclattice/backend.go | 10 +- services/vpclattice/handler.go | 204 ++++++++++++++-------------- services/vpclattice/handler_test.go | 4 +- services/vpclattice/interfaces.go | 6 +- 4 files changed, 113 insertions(+), 111 deletions(-) diff --git a/services/vpclattice/backend.go b/services/vpclattice/backend.go index 6612ddfa5..bc88e7ee5 100644 --- a/services/vpclattice/backend.go +++ b/services/vpclattice/backend.go @@ -203,10 +203,9 @@ func (s *storedSNSA) toSummary() *ServiceNetworkServiceAssociationSummary { // storedSNVA holds a service network VPC association. type storedSNVA struct { - Tags map[string]string `json:"tags"` - SecurityGroupIDs []string `json:"securityGroupIds"` CreatedAt time.Time `json:"createdAt"` LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` ARN string `json:"arn"` ID string `json:"id"` VpcID string `json:"vpcId"` @@ -215,6 +214,7 @@ type storedSNVA struct { ServiceNetworkName string `json:"serviceNetworkName"` Status string `json:"status"` CreatedBy string `json:"createdBy"` + SecurityGroupIDs []string `json:"securityGroupIds"` } func (s *storedSNVA) toAssociation() *ServiceNetworkVpcAssociation { @@ -333,16 +333,16 @@ func (r *storedRule) toSummary() *RuleSummary { // storedTargetGroup holds a target group. type storedTargetGroup struct { - Tags map[string]string `json:"tags"` - Config *TargetGroupConfig `json:"config"` - ServiceARNs []string `json:"serviceArns"` CreatedAt time.Time `json:"createdAt"` LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Tags map[string]string `json:"tags"` + Config *TargetGroupConfig `json:"config"` ARN string `json:"arn"` ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` + ServiceARNs []string `json:"serviceArns"` } func (tg *storedTargetGroup) toTargetGroup() *TargetGroup { diff --git a/services/vpclattice/handler.go b/services/vpclattice/handler.go index 18823f80e..d49f3576d 100644 --- a/services/vpclattice/handler.go +++ b/services/vpclattice/handler.go @@ -29,6 +29,7 @@ const ( opUnknown = "Unknown" keyMessage = "message" + keyStatus = "status" keyARN = "arn" keyName = "name" @@ -49,58 +50,58 @@ const ( keyUnsuccessful = "unsuccessful" keyNameRequired = "name is required" - opBatchUpdateRule = "BatchUpdateRule" - opCreateALS = "CreateAccessLogSubscription" - opCreateListener = "CreateListener" - opCreateRule = "CreateRule" - opCreateService = "CreateService" - opCreateSN = "CreateServiceNetwork" - opCreateSNSA = "CreateServiceNetworkServiceAssociation" - opCreateSNVA = "CreateServiceNetworkVpcAssociation" - opCreateTG = "CreateTargetGroup" - opDeleteALS = "DeleteAccessLogSubscription" - opDeleteAuthPolicy = "DeleteAuthPolicy" - opDeleteListener = "DeleteListener" - opDeleteResourcePolicy = "DeleteResourcePolicy" - opDeleteRule = "DeleteRule" - opDeleteService = "DeleteService" - opDeleteSN = "DeleteServiceNetwork" - opDeleteSNSA = "DeleteServiceNetworkServiceAssociation" - opDeleteSNVA = "DeleteServiceNetworkVpcAssociation" - opDeleteTG = "DeleteTargetGroup" - opDeregisterTargets = "DeregisterTargets" - opGetALS = "GetAccessLogSubscription" - opGetAuthPolicy = "GetAuthPolicy" - opGetListener = "GetListener" - opGetResourcePolicy = "GetResourcePolicy" - opGetRule = "GetRule" - opGetService = "GetService" - opGetSN = "GetServiceNetwork" - opGetSNSA = "GetServiceNetworkServiceAssociation" - opGetSNVA = "GetServiceNetworkVpcAssociation" - opGetTG = "GetTargetGroup" - opListALSs = "ListAccessLogSubscriptions" - opListListeners = "ListListeners" - opListRules = "ListRules" - opListSNSAs = "ListServiceNetworkServiceAssociations" - opListSNVAs = "ListServiceNetworkVpcAssociations" - opListSNs = "ListServiceNetworks" - opListServices = "ListServices" - opListTagsForResource = "ListTagsForResource" - opListTGs = "ListTargetGroups" - opListTargets = "ListTargets" - opPutAuthPolicy = "PutAuthPolicy" - opPutResourcePolicy = "PutResourcePolicy" - opRegisterTargets = "RegisterTargets" - opTagResource = "TagResource" - opUntagResource = "UntagResource" - opUpdateALS = "UpdateAccessLogSubscription" - opUpdateListener = "UpdateListener" - opUpdateRule = "UpdateRule" - opUpdateService = "UpdateService" - opUpdateSN = "UpdateServiceNetwork" - opUpdateSNVA = "UpdateServiceNetworkVpcAssociation" - opUpdateTG = "UpdateTargetGroup" + opBatchUpdateRule = "BatchUpdateRule" + opCreateALS = "CreateAccessLogSubscription" + opCreateListener = "CreateListener" + opCreateRule = "CreateRule" + opCreateService = "CreateService" + opCreateSN = "CreateServiceNetwork" + opCreateSNSA = "CreateServiceNetworkServiceAssociation" + opCreateSNVA = "CreateServiceNetworkVpcAssociation" + opCreateTG = "CreateTargetGroup" + opDeleteALS = "DeleteAccessLogSubscription" + opDeleteAuthPolicy = "DeleteAuthPolicy" + opDeleteListener = "DeleteListener" + opDeleteResourcePolicy = "DeleteResourcePolicy" + opDeleteRule = "DeleteRule" + opDeleteService = "DeleteService" + opDeleteSN = "DeleteServiceNetwork" + opDeleteSNSA = "DeleteServiceNetworkServiceAssociation" + opDeleteSNVA = "DeleteServiceNetworkVpcAssociation" + opDeleteTG = "DeleteTargetGroup" + opDeregisterTargets = "DeregisterTargets" + opGetALS = "GetAccessLogSubscription" + opGetAuthPolicy = "GetAuthPolicy" + opGetListener = "GetListener" + opGetResourcePolicy = "GetResourcePolicy" + opGetRule = "GetRule" + opGetService = "GetService" + opGetSN = "GetServiceNetwork" + opGetSNSA = "GetServiceNetworkServiceAssociation" + opGetSNVA = "GetServiceNetworkVpcAssociation" + opGetTG = "GetTargetGroup" + opListALSs = "ListAccessLogSubscriptions" + opListListeners = "ListListeners" + opListRules = "ListRules" + opListSNSAs = "ListServiceNetworkServiceAssociations" + opListSNVAs = "ListServiceNetworkVpcAssociations" + opListSNs = "ListServiceNetworks" + opListServices = "ListServices" + opListTagsForResource = "ListTagsForResource" + opListTGs = "ListTargetGroups" + opListTargets = "ListTargets" + opPutAuthPolicy = "PutAuthPolicy" + opPutResourcePolicy = "PutResourcePolicy" + opRegisterTargets = "RegisterTargets" + opTagResource = "TagResource" + opUntagResource = "UntagResource" + opUpdateALS = "UpdateAccessLogSubscription" + opUpdateListener = "UpdateListener" + opUpdateRule = "UpdateRule" + opUpdateService = "UpdateService" + opUpdateSN = "UpdateServiceNetwork" + opUpdateSNVA = "UpdateServiceNetworkVpcAssociation" + opUpdateTG = "UpdateTargetGroup" ) // Handler handles VPC Lattice HTTP requests. @@ -226,7 +227,7 @@ func (h *Handler) Handler() echo.HandlerFunc { } } -func (h *Handler) handleREST(c *echo.Context) error { //nolint:gocognit,gocyclo,cyclop // large routing dispatch is expected +func (h *Handler) handleREST(c *echo.Context) error { //nolint:gocyclo,cyclop,funlen // routing dispatch op, id1, id2, id3 := classifyPath(c.Request().Method, c.Request().URL.Path) var body map[string]any @@ -550,7 +551,7 @@ func (h *Handler) handleDeleteSNSA(c *echo.Context, id string) error { return h.handleError(c, err) } - return c.JSON(http.StatusOK, map[string]any{"status": statusDeleteInProgress}) + return c.JSON(http.StatusOK, map[string]any{keyStatus: statusDeleteInProgress}) } func (h *Handler) handleListSNSAs(c *echo.Context) error { @@ -646,7 +647,7 @@ func (h *Handler) handleDeleteSNVA(c *echo.Context, id string) error { return h.handleError(c, err) } - return c.JSON(http.StatusOK, map[string]any{"status": statusDeleteInProgress}) + return c.JSON(http.StatusOK, map[string]any{keyStatus: statusDeleteInProgress}) } func (h *Handler) handleListSNVAs(c *echo.Context) error { @@ -939,7 +940,7 @@ func (h *Handler) handleDeleteTargetGroup(c *echo.Context, id string) error { return h.handleError(c, err) } - return c.JSON(http.StatusOK, map[string]any{"id": id, "status": statusDeleteInProgress}) + return c.JSON(http.StatusOK, map[string]any{"id": id, keyStatus: statusDeleteInProgress}) } func (h *Handler) handleListTargetGroups(c *echo.Context) error { @@ -1212,9 +1213,10 @@ func (h *Handler) handleListTagsForResource(c *echo.Context, resourceArn string) // classifyPath maps (method, path) → (op, id1, id2, id3). // id1..id3 are path segments in order (service, listener, rule etc.). -func classifyPath( //nolint:gocognit,gocyclo,cyclop // large routing dispatch is expected +// classifyPath maps method+path to (op,id1,id2,id3). +func classifyPath( //nolint:gocyclo,cyclop,funlen // routing dispatch method, path string, -) (op, id1, id2, id3 string) { +) (string, string, string, string) { switch { case path == pathServices: if method == http.MethodPost { @@ -1308,9 +1310,9 @@ func classifyPath( //nolint:gocognit,gocyclo,cyclop // large routing dispatch is } // classifyServicePath handles /services/{serviceID}[/listeners[/...]]. -func classifyServicePath( //nolint:gocognit,gocyclo,cyclop,nestif // large routing dispatch is expected +func classifyServicePath( //nolint:gocognit,cyclop // routing dispatch method, path string, -) (op, id1, id2, id3 string) { +) (string, string, string, string) { rest := strings.TrimPrefix(path, pathServices+"/") serviceID, sub, hasSub := strings.Cut(rest, "/") @@ -1336,7 +1338,7 @@ func classifyServicePath( //nolint:gocognit,gocyclo,cyclop,nestif // large routi return opListListeners, serviceID, "", "" } - if listenerRest, ok := strings.CutPrefix(sub, "listeners/"); ok { + if listenerRest, ok := strings.CutPrefix(sub, "listeners/"); ok { //nolint:nestif // nested path hierarchy listenerID, listenerSub, hasListenerSub := strings.Cut(listenerRest, "/") if !hasListenerSub { @@ -1485,7 +1487,7 @@ func serviceToJSON(s *Service) map[string]any { "id": s.ID, keyName: s.Name, "authType": s.AuthType, - "status": s.Status, + keyStatus: s.Status, keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), keyLastUpdatedAt: s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), "dnsEntry": map[string]any{"domainName": s.DNSName}, @@ -1507,7 +1509,7 @@ func serviceSummaryToJSON(s *ServiceSummary) map[string]any { keyARN: s.ARN, "id": s.ID, keyName: s.Name, - "status": s.Status, + keyStatus: s.Status, keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } @@ -1524,25 +1526,25 @@ func serviceSummaryToJSON(s *ServiceSummary) map[string]any { func serviceNetworkToJSON(s *ServiceNetwork) map[string]any { return map[string]any{ - keyARN: s.ARN, - "id": s.ID, - keyName: s.Name, - "authType": s.AuthType, - "numberOfAssociatedServices": s.NumberOfAssociatedServices, - "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, - keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), - keyLastUpdatedAt: s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyName: s.Name, + "authType": s.AuthType, + "numberOfAssociatedServices": s.NumberOfAssociatedServices, + "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyLastUpdatedAt: s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), } } func serviceNetworkSummaryToJSON(s *ServiceNetworkSummary) map[string]any { return map[string]any{ - keyARN: s.ARN, - "id": s.ID, - keyName: s.Name, - "numberOfAssociatedServices": s.NumberOfAssociatedServices, - "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, - keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyARN: s.ARN, + "id": s.ID, + keyName: s.Name, + "numberOfAssociatedServices": s.NumberOfAssociatedServices, + "numberOfAssociatedVPCs": s.NumberOfAssociatedVPCs, + keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } @@ -1556,7 +1558,7 @@ func snsaToJSON(s *ServiceNetworkServiceAssociation) map[string]any { keyServiceNetworkARN: s.ServiceNetworkARN, keyServiceNetworkID: s.ServiceNetworkID, keyServiceNetworkName: s.ServiceNetworkName, - "status": s.Status, + keyStatus: s.Status, "createdBy": s.CreatedBy, keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } @@ -1572,7 +1574,7 @@ func snsaSummaryToJSON(s *ServiceNetworkServiceAssociationSummary) map[string]an keyServiceNetworkARN: s.ServiceNetworkARN, keyServiceNetworkID: s.ServiceNetworkID, keyServiceNetworkName: s.ServiceNetworkName, - "status": s.Status, + keyStatus: s.Status, keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } @@ -1589,7 +1591,7 @@ func snvaToJSON(s *ServiceNetworkVpcAssociation) map[string]any { keyServiceNetworkID: s.ServiceNetworkID, keyServiceNetworkName: s.ServiceNetworkName, "securityGroupIds": sgs, - "status": s.Status, + keyStatus: s.Status, "createdBy": s.CreatedBy, keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), keyLastUpdatedAt: s.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), @@ -1604,7 +1606,7 @@ func snvaSummaryToJSON(s *ServiceNetworkVpcAssociationSummary) map[string]any { keyServiceNetworkARN: s.ServiceNetworkARN, keyServiceNetworkID: s.ServiceNetworkID, keyServiceNetworkName: s.ServiceNetworkName, - "status": s.Status, + keyStatus: s.Status, keyCreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } @@ -1757,7 +1759,7 @@ func targetGroupToJSON(tg *TargetGroup) map[string]any { "id": tg.ID, keyName: tg.Name, "type": tg.Type, - "status": tg.Status, + keyStatus: tg.Status, "serviceArns": tg.ServiceARNs, keyCreatedAt: tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), keyLastUpdatedAt: tg.LastUpdatedAt.Format("2006-01-02T15:04:05.000Z"), @@ -1772,25 +1774,25 @@ func targetGroupToJSON(tg *TargetGroup) map[string]any { func targetGroupSummaryToJSON(tg *TargetGroupSummary) map[string]any { return map[string]any{ - keyARN: tg.ARN, - "id": tg.ID, - keyName: tg.Name, - "type": tg.Type, - "status": tg.Status, - keyPort: tg.Port, - keyProtocol: tg.Protocol, - keyVPCID: tg.VpcID, + keyARN: tg.ARN, + "id": tg.ID, + keyName: tg.Name, + "type": tg.Type, + keyStatus: tg.Status, + keyPort: tg.Port, + keyProtocol: tg.Protocol, + keyVPCID: tg.VpcID, "serviceArns": tg.ServiceARNs, - keyCreatedAt: tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), + keyCreatedAt: tg.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } } func targetGroupConfigToJSON(c *TargetGroupConfig) map[string]any { m := map[string]any{ - keyPort: c.Port, - keyProtocol: c.Protocol, + keyPort: c.Port, + keyProtocol: c.Protocol, "protocolVersion": c.ProtocolVersion, - "vpcIdentifier": c.VpcID, + "vpcIdentifier": c.VpcID, } if c.HealthCheck != nil { @@ -1817,7 +1819,7 @@ func targetSummaryToJSON(t *TargetSummary) map[string]any { return map[string]any{ "id": t.ID, keyPort: t.Port, - "status": t.Status, + keyStatus: t.Status, "reasonCode": t.ReasonCode, } } @@ -1873,13 +1875,13 @@ func extractTags(body map[string]any) map[string]string { func bodyInt32(body map[string]any, key string) int32 { switch v := body[key].(type) { case float64: - return int32(v) //nolint:gosec // value is bounded by JSON number range + return int32(v) case int: - return int32(v) //nolint:gosec // value is bounded, overflow not possible + return int32(v) //nolint:gosec // value bounded by JSON number range case int32: return v case int64: - return int32(v) //nolint:gosec // value is bounded, overflow not possible + return int32(v) //nolint:gosec // value bounded by JSON number range } return 0 @@ -1916,7 +1918,7 @@ func extractRuleAction(body map[string]any, key string) *RuleAction { return action } -func extractRuleMatch(body map[string]any, key string) *RuleMatch { //nolint:gocognit,nestif // nested JSON extraction +func extractRuleMatch(body map[string]any, key string) *RuleMatch { //nolint:gocognit // nested extraction raw, ok := body[key].(map[string]any) if !ok { return nil @@ -1924,7 +1926,7 @@ func extractRuleMatch(body map[string]any, key string) *RuleMatch { //nolint:goc match := &RuleMatch{} - if httpMatch, ok2 := raw["httpMatch"].(map[string]any); ok2 { + if httpMatch, ok2 := raw["httpMatch"].(map[string]any); ok2 { //nolint:nestif // HTTP match needs nested parsing match.HTTPMethod, _ = httpMatch["method"].(string) if pathRaw, ok3 := httpMatch["path"].(map[string]any); ok3 { @@ -2028,5 +2030,5 @@ func queryInt32(c *echo.Context) int32 { return 0 } - return int32(n) //nolint:gosec // value is bounded by ParseInt with bitSize=32 + return int32(n) } diff --git a/services/vpclattice/handler_test.go b/services/vpclattice/handler_test.go index d6ff72253..bd512cf8e 100644 --- a/services/vpclattice/handler_test.go +++ b/services/vpclattice/handler_test.go @@ -64,7 +64,7 @@ func parseBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]any { func TestService_CRUD(t *testing.T) { t.Parallel() - tests := []struct { + tests := []struct { //nolint:govet // readability: name first name string body map[string]any wantCode int @@ -543,7 +543,7 @@ func TestTargetGroup_CRUD(t *testing.T) { t.Parallel() h := newTestHandler(t) - tests := []struct { + tests := []struct { //nolint:govet // readability: name first name string body map[string]any wantCode int diff --git a/services/vpclattice/interfaces.go b/services/vpclattice/interfaces.go index 9ab859d95..8ecba0ea7 100644 --- a/services/vpclattice/interfaces.go +++ b/services/vpclattice/interfaces.go @@ -223,7 +223,6 @@ type ServiceNetworkServiceAssociationSummary struct { // ServiceNetworkVpcAssociation is a VPC-to-service-network association. type ServiceNetworkVpcAssociation struct { - SecurityGroupIDs []string CreatedAt time.Time LastUpdatedAt time.Time ARN string @@ -234,6 +233,7 @@ type ServiceNetworkVpcAssociation struct { ServiceNetworkName string Status string CreatedBy string + SecurityGroupIDs []string } // ServiceNetworkVpcAssociationSummary is a summary for list responses. @@ -336,10 +336,10 @@ type WeightedTargetGroup struct { // RuleMatch is the match conditions for a listener rule. type RuleMatch struct { - HeaderMatches []*HeaderMatch `json:"headerMatches,omitempty"` HTTPMethod string `json:"httpMethod,omitempty"` PathMatchType string `json:"pathMatchType,omitempty"` PathMatchValue string `json:"pathMatchValue,omitempty"` + HeaderMatches []*HeaderMatch `json:"headerMatches,omitempty"` } // HeaderMatch is an HTTP header match condition. @@ -365,7 +365,6 @@ type TargetGroup struct { // TargetGroupSummary is a target group entry for list responses. type TargetGroupSummary struct { - ServiceARNs []string CreatedAt time.Time ARN string ID string @@ -374,6 +373,7 @@ type TargetGroupSummary struct { Status string Protocol string VpcID string + ServiceARNs []string Port int32 } From 8b92f05bb4f8ece6d125155d9c58dd6b869836da Mon Sep 17 00:00:00 2001 From: mayor Date: Sat, 13 Jun 2026 02:01:41 -0500 Subject: [PATCH 7/7] style(vpclattice): wrap long test comment to satisfy lll linter Co-Authored-By: Claude Opus 4.8 --- test/terraform/terraform_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/terraform/terraform_test.go b/test/terraform/terraform_test.go index f25411933..f8c87e597 100644 --- a/test/terraform/terraform_test.go +++ b/test/terraform/terraform_test.go @@ -6396,7 +6396,8 @@ func TestTerraform_Xray(t *testing.T) { }) } -// TestTerraform_VPCLattice provisions a VPC Lattice service and service network via Terraform and verifies they were created. +// TestTerraform_VPCLattice provisions a VPC Lattice service and service network +// via Terraform and verifies they were created. func TestTerraform_VPCLattice(t *testing.T) { t.Parallel()