diff --git a/gateway/configs/config.yaml b/gateway/configs/config.yaml index dfeded80e..a38647e58 100644 --- a/gateway/configs/config.yaml +++ b/gateway/configs/config.yaml @@ -51,6 +51,14 @@ gateway_controller: router: # Gateway host for incoming requests gateway_host: "*" + # Event Gateway (WebSub Hub) configuration + event_gateway: + enabled: true + websub_hub_url: "http://host.docker.internal" + websub_hub_port: 9098 + router_host: "localhost" + websub_hub_listener_port: 8083 + timeout_seconds: 10 # Access logs configuration access_logs: diff --git a/gateway/examples/websubhub.yaml b/gateway/examples/websubhub.yaml index 60024cf3f..d0629f6de 100644 --- a/gateway/examples/websubhub.yaml +++ b/gateway/examples/websubhub.yaml @@ -1,32 +1,41 @@ -# -------------------------------------------------------------------- -# Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). -# -# WSO2 LLC. licenses this file to you under the Apache License, -# Version 2.0 (the "License"); you may not use this file except -# in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# -------------------------------------------------------------------- - apiVersion: gateway.api-platform.wso2.com/v1alpha1 -kind: async/websub +kind: WebSubApi metadata: - name: websub-api-v1.0 + name: WebSubHub-API spec: displayName: WebSub-API-New-API version: v1.0 context: /websubnewapi - servers: - - url: http://host.docker.internal:9098 - protocol: websub + vhosts: + main: "*" channels: - path: /pull-requests + subscribe: + policies: + - name: ModifyHeaders + version: v1.0.0 + enabled: true + params: + requestHeaders: + - action: SET + name: operation-level-req-header + value: hello + responseHeaders: + - action: SET + name: operation-level-res-header + value: world - path: /issues + subscribe: + policies: + - name: ModifyHeaders + version: v1.0.0 + enabled: true + params: + requestHeaders: + - action: SET + name: operation-level-req-header + value: hello + responseHeaders: + - action: SET + name: operation-level-res-header + value: world \ No newline at end of file diff --git a/gateway/gateway-controller/api/openapi.yaml b/gateway/gateway-controller/api/openapi.yaml index 52c797998..71ba38cce 100644 --- a/gateway/gateway-controller/api/openapi.yaml +++ b/gateway/gateway-controller/api/openapi.yaml @@ -1750,9 +1750,7 @@ components: example: RestApi enum: - RestApi - - async/websub - - async/websocket - - async/sse + - WebSubApi metadata: $ref: "#/components/schemas/Metadata" spec: @@ -1779,7 +1777,6 @@ components: - context - upstream - operations - # - kind properties: displayName: type: string @@ -1964,20 +1961,13 @@ components: WebhookAPIData: type: object required: - # - kind - - name + - displayName - version - context - - servers + - upstream - channels properties: - # kind: - # type: string - # description: API type - # example: async/websub - # enum: - # - async/websub - name: + displayName: type: string description: Human-readable API name (must be URL-friendly - only letters, numbers, spaces, hyphens, underscores, and dots allowed) minLength: 1 @@ -1996,12 +1986,30 @@ components: minLength: 1 maxLength: 200 example: /weather - servers: - type: array - description: List of backend service URLs (for REST APIs) or event hub URLs (for async APIs) - minItems: 1 - items: - $ref: '#/components/schemas/Server' + upstream: + type: object + description: API-level upstream configuration + properties: + main: + $ref: "#/components/schemas/Upstream" + sandbox: + $ref: "#/components/schemas/Upstream" + vhosts: + type: object + required: + - main + description: Custom virtual hosts/domains for the API + properties: + main: + type: string + description: Custom virtual host/domain for production traffic + pattern: '^[a-zA-Z0-9\.\-]+$' + example: api.example.com + sandbox: + type: string + description: Custom virtual host/domain for sandbox traffic + pattern: '^[a-zA-Z0-9\.\-]+$' + example: sandbox-api.example.com channels: type: array description: List of operations - HTTP operations for REST APIs or event/topic operations for async APIs @@ -2101,6 +2109,11 @@ components: summary: type: string example: Receive issue events. + policies: + type: array + description: List of policies applied only to this operation (overrides or adds to API-level policies) + items: + $ref: "#/components/schemas/Policy" message: $ref: '#/components/schemas/ChannelMessage' publish: @@ -2110,6 +2123,11 @@ components: summary: type: string example: Publish issue events. + policies: + type: array + description: List of policies applied only to this operation (overrides or adds to API-level policies) + items: + $ref: "#/components/schemas/Policy" message: $ref: '#/components/schemas/ChannelMessage' parameters: @@ -2128,11 +2146,6 @@ components: type: object description: Protocol-specific channel bindings (arbitrary key/value structure). additionalProperties: true - policies: - type: array - description: List of policies applied only to this operation (overrides or adds to API-level policies) - items: - $ref: '#/components/schemas/Policy' # Message schema used within Channel publish/subscribe operations. ChannelMessage: diff --git a/gateway/gateway-controller/pkg/api/generated/generated.go b/gateway/gateway-controller/pkg/api/generated/generated.go index efe3b0e7b..a22d934a0 100644 --- a/gateway/gateway-controller/pkg/api/generated/generated.go +++ b/gateway/gateway-controller/pkg/api/generated/generated.go @@ -28,10 +28,8 @@ const ( // Defines values for APIConfigurationKind. const ( - Asyncsse APIConfigurationKind = "async/sse" - Asyncwebsocket APIConfigurationKind = "async/websocket" - Asyncwebsub APIConfigurationKind = "async/websub" - RestApi APIConfigurationKind = "RestApi" + RestApi APIConfigurationKind = "RestApi" + WebSubApi APIConfigurationKind = "WebSubApi" ) // Defines values for APIDetailResponseApiMetadataStatus. @@ -228,15 +226,6 @@ const ( PUT RouteExceptionMethods = "PUT" ) -// Defines values for ServerProtocol. -const ( - Kafka ServerProtocol = "kafka" - Mqtt ServerProtocol = "mqtt" - Sse ServerProtocol = "sse" - Websocket ServerProtocol = "websocket" - Websub ServerProtocol = "websub" -) - // Defines values for UpstreamAuthAuthType. const ( ApiKey UpstreamAuthAuthType = "api-key" @@ -549,21 +538,24 @@ type Channel struct { // Path Channel path or topic identifier relative to API context. Path string `json:"path" yaml:"path"` - // Policies List of policies applied only to this operation (overrides or adds to API-level policies) - Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` - // Publish Producer (send) operation definition. Publish *struct { // Message Event/message definition transported over a channel. Message *ChannelMessage `json:"message,omitempty" yaml:"message,omitempty"` - Summary *string `json:"summary,omitempty" yaml:"summary,omitempty"` + + // Policies List of policies applied only to this operation (overrides or adds to API-level policies) + Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` + Summary *string `json:"summary,omitempty" yaml:"summary,omitempty"` } `json:"publish,omitempty" yaml:"publish,omitempty"` // Subscribe Consumer (receive) operation definition. Subscribe *struct { // Message Event/message definition transported over a channel. Message *ChannelMessage `json:"message,omitempty" yaml:"message,omitempty"` - Summary *string `json:"summary,omitempty" yaml:"summary,omitempty"` + + // Policies List of policies applied only to this operation (overrides or adds to API-level policies) + Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` + Summary *string `json:"summary,omitempty" yaml:"summary,omitempty"` } `json:"subscribe,omitempty" yaml:"subscribe,omitempty"` } @@ -1219,42 +1211,6 @@ type RouteException struct { // RouteExceptionMethods defines model for RouteException.Methods. type RouteExceptionMethods string -// Server Server definition for async or WebSub APIs. -type Server struct { - // Bindings Protocol-specific server bindings (arbitrary key/value structure). - Bindings *map[string]interface{} `json:"bindings,omitempty" yaml:"bindings,omitempty"` - - // Description Human-readable description of this server. - Description *string `json:"description,omitempty" yaml:"description,omitempty"` - - // Protocol Transport protocol used by the server. - Protocol ServerProtocol `json:"protocol" yaml:"protocol"` - - // ProtocolVersion Version of the selected protocol (if applicable). - ProtocolVersion *string `json:"protocolVersion,omitempty" yaml:"protocolVersion,omitempty"` - - // Security Security requirements for this server (each item maps scheme name to optional scopes array). - Security *[]map[string][]string `json:"security,omitempty" yaml:"security,omitempty"` - - // Url Base URL or connection string for the server (variables may be denoted by {name}). - Url string `json:"url" yaml:"url"` - - // Variables Templated variables contained in the server URL. - Variables *map[string]struct { - // Default Default value for the variable. - Default string `json:"default" yaml:"default"` - - // Description Description of the variable. - Description *string `json:"description,omitempty" yaml:"description,omitempty"` - - // Enum Allowed values. - Enum *[]string `json:"enum,omitempty" yaml:"enum,omitempty"` - } `json:"variables,omitempty" yaml:"variables,omitempty"` -} - -// ServerProtocol Transport protocol used by the server. -type ServerProtocol string - // Upstream Upstream backend configuration (single target or reference) type Upstream struct { // Ref Reference to a predefined upstreamDefinition @@ -1300,17 +1256,32 @@ type WebhookAPIData struct { // Context Base path for all API routes (must start with /, no trailing slash) Context string `json:"context" yaml:"context"` - // Name Human-readable API name (must be URL-friendly - only letters, numbers, spaces, hyphens, underscores, and dots allowed) - Name string `json:"name" yaml:"name"` + // DisplayName Human-readable API name (must be URL-friendly - only letters, numbers, spaces, hyphens, underscores, and dots allowed) + DisplayName string `json:"displayName" yaml:"displayName"` // Policies List of API-level policies applied to all operations unless overridden Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` - // Servers List of backend service URLs (for REST APIs) or event hub URLs (for async APIs) - Servers []Server `json:"servers" yaml:"servers"` + // Upstream API-level upstream configuration + Upstream struct { + // Main Upstream backend configuration (single target or reference) + Main *Upstream `json:"main,omitempty" yaml:"main,omitempty"` + + // Sandbox Upstream backend configuration (single target or reference) + Sandbox *Upstream `json:"sandbox,omitempty" yaml:"sandbox,omitempty"` + } `json:"upstream" yaml:"upstream"` // Version Semantic version of the API Version string `json:"version" yaml:"version"` + + // Vhosts Custom virtual hosts/domains for the API + Vhosts *struct { + // Main Custom virtual host/domain for production traffic + Main string `json:"main" yaml:"main"` + + // Sandbox Custom virtual host/domain for sandbox traffic + Sandbox *string `json:"sandbox,omitempty" yaml:"sandbox,omitempty"` + } `json:"vhosts,omitempty" yaml:"vhosts,omitempty"` } // ListAPIsParams defines parameters for ListAPIs. @@ -2922,185 +2893,180 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "Ccj6qrlL4fby9r8DAAD//5Ch1MOuNwEA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/gateway/gateway-controller/pkg/api/handlers/handlers.go b/gateway/gateway-controller/pkg/api/handlers/handlers.go index d0a18a9c0..4ac695126 100644 --- a/gateway/gateway-controller/pkg/api/handlers/handlers.go +++ b/gateway/gateway-controller/pkg/api/handlers/handlers.go @@ -466,7 +466,7 @@ func (s *APIServer) GetAPIById(c *gin.Context, id string) { return } - if cfg.Kind != string(api.RestApi) && cfg.Kind != string(api.Asyncwebsub) { + if cfg.Kind != string(api.RestApi) && cfg.Kind != string(api.WebSubApi) { log.Warn("Configuration kind mismatch", zap.String("expected", "RestApi or async/websub"), zap.String("actual", cfg.Kind), @@ -552,6 +552,35 @@ func (s *APIServer) UpdateAPI(c *gin.Context, id string) { } } + if apiConfig.Kind == api.WebSubApi { + webhookData, err := apiConfig.Spec.AsWebhookAPIData() + if err != nil { + log.Error("Failed to parse configuration", zap.Error(err)) + c.JSON(http.StatusBadRequest, api.ErrorResponse{ + Status: "error", + Message: "Failed to parse configuration", + }) + return + } + // Ensure an upstream main exists for async/websub configs so downstream + // logic can safely rely on the field being present. Create an empty + // upstream if it is missing and save it back into the union spec. + if webhookData.Upstream.Main == nil { + url := fmt.Sprintf("%s:%d", s.routerConfig.EventGateway.WebSubHubURL, s.routerConfig.EventGateway.WebSubHubPort) + webhookData.Upstream.Main = &api.Upstream{ + Url: &url, + } + if err := apiConfig.Spec.FromWebhookAPIData(webhookData); err != nil { + log.Error("Failed to parse configuration", zap.Error(err)) + c.JSON(http.StatusBadRequest, api.ErrorResponse{ + Status: "error", + Message: "Error while processing configuration", + }) + return + } + } + } + // Validate configuration validationErrors := s.validator.Validate(&apiConfig) if len(validationErrors) > 0 { @@ -606,7 +635,7 @@ func (s *APIServer) UpdateAPI(c *gin.Context, id string) { existing.DeployedAt = nil existing.DeployedVersion = 0 - if apiConfig.Kind == api.Asyncwebsub { + if apiConfig.Kind == api.WebSubApi { topicsToRegister, topicsToUnregister := s.deploymentService.GetTopicsForUpdate(*existing) // TODO: Pre configure the dynamic forward proxy rules for event gw // This was communication bridge will be created on the gw startup @@ -627,7 +656,9 @@ func (s *APIServer) UpdateAPI(c *gin.Context, id string) { childWg.Add(1) go func(topic string) { defer childWg.Done() - if err := s.deploymentService.RegisterTopicWithHub(s.httpClient, topic, "localhost", 8083, log); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.routerConfig.EventGateway.TimeoutSeconds)*time.Second) + defer cancel() + if err := s.deploymentService.RegisterTopicWithHub(ctx, s.httpClient, topic, s.routerConfig.EventGateway.RouterHost, s.routerConfig.EventGateway.WebSubHubListenerPort, log); err != nil { log.Error("Failed to register topic with WebSubHub", zap.Error(err), zap.String("topic", topic), @@ -654,7 +685,9 @@ func (s *APIServer) UpdateAPI(c *gin.Context, id string) { childWg.Add(1) go func(topic string) { defer childWg.Done() - if err := s.deploymentService.UnregisterTopicWithHub(s.httpClient, topic, "localhost", 8083, log); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.routerConfig.EventGateway.TimeoutSeconds)*time.Second) + defer cancel() + if err := s.deploymentService.UnregisterTopicWithHub(ctx, s.httpClient, topic, s.routerConfig.EventGateway.RouterHost, s.routerConfig.EventGateway.WebSubHubListenerPort, log); err != nil { log.Error("Failed to deregister topic from WebSubHub", zap.Error(err), zap.String("topic", topic), @@ -728,7 +761,7 @@ func (s *APIServer) UpdateAPI(c *gin.Context, id string) { // Update xDS snapshot asynchronously go func() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.routerConfig.EventGateway.TimeoutSeconds)*time.Second) defer cancel() if err := s.snapshotManager.UpdateSnapshot(ctx, correlationID); err != nil { @@ -870,7 +903,7 @@ func (s *APIServer) DeleteAPI(c *gin.Context, id string) { } } - if cfg.Configuration.Kind == api.Asyncwebsub { + if cfg.Configuration.Kind == api.WebSubApi { topicsToUnregister := s.deploymentService.GetTopicsForDelete(*cfg) var deregErrs int32 @@ -886,7 +919,9 @@ func (s *APIServer) DeleteAPI(c *gin.Context, id string) { childWg.Add(1) go func(topic string) { defer childWg.Done() - if err := s.deploymentService.UnregisterTopicWithHub(s.httpClient, topic, "localhost", 8083, log); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.routerConfig.EventGateway.TimeoutSeconds)*time.Second) + defer cancel() + if err := s.deploymentService.UnregisterTopicWithHub(ctx, s.httpClient, topic, s.routerConfig.EventGateway.RouterHost, s.routerConfig.EventGateway.WebSubHubListenerPort, log); err != nil { log.Error("Failed to deregister topic from WebSubHub", zap.Error(err), zap.String("topic", topic), @@ -1655,7 +1690,7 @@ func (s *APIServer) buildStoredPolicyFromAPI(cfg *models.StoredConfig) *models.S routes := make([]policyenginev1.PolicyChain, 0) switch apiCfg.Kind { - case api.Asyncwebsub: + case api.WebSubApi: // Build routes with merged policies apiData, err := apiCfg.Spec.AsWebhookAPIData() if err != nil { @@ -1665,13 +1700,13 @@ func (s *APIServer) buildStoredPolicyFromAPI(cfg *models.StoredConfig) *models.S for _, ch := range apiData.Channels { var finalPolicies []policyenginev1.PolicyInstance - if ch.Policies != nil && len(*ch.Policies) > 0 { + if ch.Subscribe.Policies != nil && len(*ch.Subscribe.Policies) > 0 { // Operation has policies: use operation policy order as authoritative // This allows operations to reorder, override, or extend API-level policies - finalPolicies = make([]policyenginev1.PolicyInstance, 0, len(*ch.Policies)) + finalPolicies = make([]policyenginev1.PolicyInstance, 0, len(*ch.Subscribe.Policies)) addedNames := make(map[string]struct{}) - for _, opPolicy := range *ch.Policies { + for _, opPolicy := range *ch.Subscribe.Policies { finalPolicies = append(finalPolicies, convertAPIPolicy(opPolicy)) addedNames[opPolicy.Name] = struct{}{} } @@ -2698,4 +2733,4 @@ func (s *APIServer) populatePropsForSystemPolicies(srcConfig any, props map[stri } } } -} \ No newline at end of file +} diff --git a/gateway/gateway-controller/pkg/config/api_validator.go b/gateway/gateway-controller/pkg/config/api_validator.go index c96737bbc..031c3cda7 100644 --- a/gateway/gateway-controller/pkg/config/api_validator.go +++ b/gateway/gateway-controller/pkg/config/api_validator.go @@ -85,10 +85,10 @@ func (v *APIValidator) validateAPIConfiguration(config *api.APIConfiguration) [] } // Validate kind - if config.Kind != api.RestApi && config.Kind != api.Asyncwebsub { + if config.Kind != api.RestApi && config.Kind != api.WebSubApi { errors = append(errors, ValidationError{ Field: "kind", - Message: "Unsupported API kind (only 'RestApi' and 'async/websub' are supported)", + Message: "Unsupported API kind (only 'RestApi' and 'WebSubApi' are supported)", }) } @@ -104,7 +104,7 @@ func (v *APIValidator) validateAPIConfiguration(config *api.APIConfiguration) [] // Validate data section errors = append(errors, v.validateRestData(&spec)...) } - case api.Asyncwebsub: + case api.WebSubApi: spec, err := config.Spec.AsWebhookAPIData() if err != nil { errors = append(errors, ValidationError{ @@ -258,22 +258,22 @@ func (v *APIValidator) validateRestData(spec *api.APIConfigData) []ValidationErr return errors } -// validateAsyncData validates the data section of the configuration for RestApi kind +// validateAsyncData validates the data section of the configuration for http/rest kind func (v *APIValidator) validateAsyncData(spec *api.WebhookAPIData) []ValidationError { var errors []ValidationError // Validate name - if spec.Name == "" { + if spec.DisplayName == "" { errors = append(errors, ValidationError{ Field: "spec.name", Message: "API name is required", }) - } else if len(spec.Name) > 100 { + } else if len(spec.DisplayName) > 100 { errors = append(errors, ValidationError{ Field: "spec.name", Message: "API name must be 1-100 characters", }) - } else if !v.urlFriendlyNameRegex.MatchString(spec.Name) { + } else if !v.urlFriendlyNameRegex.MatchString(spec.DisplayName) { errors = append(errors, ValidationError{ Field: "spec.name", Message: "API name must be URL-friendly (only letters, numbers, spaces, hyphens, underscores, and dots allowed)", @@ -296,8 +296,13 @@ func (v *APIValidator) validateAsyncData(spec *api.WebhookAPIData) []ValidationE // Validate context errors = append(errors, v.validateContext(spec.Context)...) - // Validate upstream - errors = append(errors, v.validateServer(spec.Servers)...) + // Validate upstream if not WebSub + if spec.Upstream.Main != nil { + errors = append(errors, v.validateUpstream("main", spec.Upstream.Main)...) + } + if spec.Upstream.Sandbox != nil { + errors = append(errors, v.validateUpstream("sandbox", spec.Upstream.Sandbox)...) + } // Validate operations errors = append(errors, v.validateChannels(spec.Channels)...) @@ -305,57 +310,6 @@ func (v *APIValidator) validateAsyncData(spec *api.WebhookAPIData) []ValidationE return errors } -// validateServer validates the server configuration -func (v *APIValidator) validateServer(server []api.Server) []ValidationError { - var errors []ValidationError - - if len(server) == 0 { - errors = append(errors, ValidationError{ - Field: "data.upstream", - Message: "At least one upstream URL is required", - }) - return errors - } - - for i, up := range server { - if up.Url == "" { - errors = append(errors, ValidationError{ - Field: fmt.Sprintf("data.upstream[%d].url", i), - Message: "Upstream URL is required", - }) - continue - } - - // Validate URL format - parsedURL, err := url.Parse(up.Url) - if err != nil { - errors = append(errors, ValidationError{ - Field: fmt.Sprintf("data.upstream[%d].url", i), - Message: fmt.Sprintf("Invalid URL format: %v", err), - }) - continue - } - - // Ensure scheme is http or https - if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { - errors = append(errors, ValidationError{ - Field: fmt.Sprintf("data.upstream[%d].url", i), - Message: "Upstream URL must use http or https scheme", - }) - } - - // Ensure host is present - if parsedURL.Host == "" { - errors = append(errors, ValidationError{ - Field: fmt.Sprintf("data.upstream[%d].url", i), - Message: "Upstream URL must include a host", - }) - } - } - - return errors -} - // validateChannels validates the channels configuration func (v *APIValidator) validateChannels(channels []api.Channel) []ValidationError { var errors []ValidationError diff --git a/gateway/gateway-controller/pkg/config/config.go b/gateway/gateway-controller/pkg/config/config.go index f093bc6c7..d3e4c8649 100644 --- a/gateway/gateway-controller/pkg/config/config.go +++ b/gateway/gateway-controller/pkg/config/config.go @@ -20,6 +20,7 @@ package config import ( "fmt" + "net/url" "strings" "time" @@ -195,9 +196,12 @@ type RouterConfig struct { // EventGatewayConfig holds event gateway specific configurations type EventGatewayConfig struct { - Enabled bool `koanf:"enabled"` - WebSubHubURL string `koanf:"websub_hub_url"` - WebSubHubPort int `koanf:"websub_hub_port"` + Enabled bool `koanf:"enabled"` + WebSubHubURL string `koanf:"websub_hub_url"` + WebSubHubPort int `koanf:"websub_hub_port"` + RouterHost string `koanf:"router_host"` + WebSubHubListenerPort int `koanf:"websub_hub_listener_port"` + TimeoutSeconds int `koanf:"timeout_seconds"` } // DownstreamTLS holds downstream (listener) TLS configuration @@ -391,9 +395,11 @@ func defaultConfig() *Config { }, Router: RouterConfig{ EventGateway: EventGatewayConfig{ - Enabled: true, - WebSubHubURL: "http://host.docker.internal", - WebSubHubPort: 9098, + Enabled: true, + WebSubHubURL: "http://host.docker.internal", + WebSubHubPort: 9098, + RouterHost: "localhost", + WebSubHubListenerPort: 8083, }, AccessLogs: AccessLogsConfig{ Enabled: true, @@ -633,6 +639,13 @@ func (c *Config) Validate() error { } } + // Validate event gateway configuration if enabled + if c.GatewayController.Router.EventGateway.Enabled { + if err := c.validateEventGatewayConfig(); err != nil { + return err + } + } + // Validate control plane configuration if err := c.validateControlPlaneConfig(); err != nil { return err @@ -670,6 +683,27 @@ func (c *Config) Validate() error { return nil } +func (c *Config) validateEventGatewayConfig() error { + if c.GatewayController.Router.EventGateway.WebSubHubPort < 1 || c.GatewayController.Router.EventGateway.WebSubHubPort > 65535 { + return fmt.Errorf("router.event_gateway.websub_hub_port must be between 1 and 65535, got: %d", c.GatewayController.Router.EventGateway.WebSubHubPort) + } + if c.GatewayController.Router.EventGateway.WebSubHubListenerPort < 1 || c.GatewayController.Router.EventGateway.WebSubHubListenerPort > 65535 { + return fmt.Errorf("router.event_gateway.websub_hub_listener_port must be between 1 and 65535, got: %d", c.GatewayController.Router.EventGateway.WebSubHubListenerPort) + } + + // Validate WebSubHubURL if provided - must be a valid http(s) URL + if strings.TrimSpace(c.GatewayController.Router.EventGateway.WebSubHubURL) != "" { + u, err := url.Parse(c.GatewayController.Router.EventGateway.WebSubHubURL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") { + return fmt.Errorf("router.event_gateway.websub_hub_url must be a valid URL with http or https scheme, got: %s", c.GatewayController.Router.EventGateway.WebSubHubURL) + } + } + if c.GatewayController.Router.EventGateway.TimeoutSeconds <= 0 { + return fmt.Errorf("router.event_gateway.timeout_seconds must be positive, got: %d", c.GatewayController.Router.EventGateway.TimeoutSeconds) + } + return nil +} + // validateControlPlaneConfig validates the control plane configuration func (c *Config) validateControlPlaneConfig() error { // Host validation - required if control plane is configured diff --git a/gateway/gateway-controller/pkg/config/validator_test.go b/gateway/gateway-controller/pkg/config/validator_test.go index c0fafd6d1..8e8936ffb 100644 --- a/gateway/gateway-controller/pkg/config/validator_test.go +++ b/gateway/gateway-controller/pkg/config/validator_test.go @@ -232,3 +232,44 @@ func TestValidateAuthConfig_BothAuthEnabled(t *testing.T) { err := config.validateAuthConfig() assert.NoError(t, err) } + +func TestValidateEventGWConfig_Enabled(t *testing.T) { + // Test that validation passes when event gateway is enabled with valid config + config := &Config{ + GatewayController: GatewayController{ + Router: RouterConfig{ + EventGateway: EventGatewayConfig{ + Enabled: true, + WebSubHubURL: "http://example.com", + WebSubHubPort: 9098, + WebSubHubListenerPort: 8083, + TimeoutSeconds: 10, + }, + }, + }, + } + + err := config.validateEventGatewayConfig() + assert.NoError(t, err) +} + +func TestValidateWebSubURLConfig_WithoutSchema(t *testing.T) { + // Test that validation fails when there's no scheme in WebSubHubURL + config := &Config{ + GatewayController: GatewayController{ + Router: RouterConfig{ + EventGateway: EventGatewayConfig{ + Enabled: true, + WebSubHubURL: "example.com", + WebSubHubPort: 9098, + WebSubHubListenerPort: 8083, + TimeoutSeconds: 10, + }, + }, + }, + } + + err := config.validateEventGatewayConfig() + assert.Error(t, err) + assert.Contains(t, err.Error(), "http or https scheme") +} diff --git a/gateway/gateway-controller/pkg/constants/constants.go b/gateway/gateway-controller/pkg/constants/constants.go index dcd840dfc..a8c705fc9 100644 --- a/gateway/gateway-controller/pkg/constants/constants.go +++ b/gateway/gateway-controller/pkg/constants/constants.go @@ -95,6 +95,8 @@ const ( BASE_PATH = "/" WILD_CARD = "*" + WEBSUBHUB_INTERNAL_CLUSTER_NAME = "WEBSUBHUB_INTERNAL_CLUSTER" + // LLM Transformer constants UPSTREAM_AUTH_APIKEY_POLICY_NAME = "modify-headers" UPSTREAM_AUTH_APIKEY_POLICY_VERSION = "v0.1.0" diff --git a/gateway/gateway-controller/pkg/models/stored_config.go b/gateway/gateway-controller/pkg/models/stored_config.go index 644f996b1..bc8015a49 100644 --- a/gateway/gateway-controller/pkg/models/stored_config.go +++ b/gateway/gateway-controller/pkg/models/stored_config.go @@ -49,12 +49,12 @@ type StoredConfig struct { // GetCompositeKey returns the composite key "displayName:version" for indexing func (c *StoredConfig) GetCompositeKey() string { - if c.Configuration.Kind == api.Asyncwebsub { + if c.Configuration.Kind == api.WebSubApi { asyncData, err := c.Configuration.Spec.AsWebhookAPIData() if err != nil { return "" } - return fmt.Sprintf("%s:%s", asyncData.Name, asyncData.Version) + return fmt.Sprintf("%s:%s", asyncData.DisplayName, asyncData.Version) } configData, err := c.Configuration.Spec.AsAPIConfigData() if err != nil { @@ -65,12 +65,12 @@ func (c *StoredConfig) GetCompositeKey() string { // GetDisplayName returns the API display name func (c *StoredConfig) GetDisplayName() string { - if c.Configuration.Kind == api.Asyncwebsub { + if c.Configuration.Kind == api.WebSubApi { asyncData, err := c.Configuration.Spec.AsWebhookAPIData() if err != nil { return "" } - return asyncData.Name + return asyncData.DisplayName } configData, err := c.Configuration.Spec.AsAPIConfigData() if err != nil { @@ -86,7 +86,7 @@ func (c *StoredConfig) GetHandle() string { // GetVersion returns the API version func (c *StoredConfig) GetVersion() string { - if c.Configuration.Kind == api.Asyncwebsub { + if c.Configuration.Kind == api.WebSubApi { asyncData, err := c.Configuration.Spec.AsWebhookAPIData() if err != nil { return "" @@ -102,7 +102,7 @@ func (c *StoredConfig) GetVersion() string { // GetContext returns the API context path func (c *StoredConfig) GetContext() string { - if c.Configuration.Kind == api.Asyncwebsub { + if c.Configuration.Kind == api.WebSubApi { asyncData, err := c.Configuration.Spec.AsWebhookAPIData() if err != nil { return "" diff --git a/gateway/gateway-controller/pkg/storage/memory.go b/gateway/gateway-controller/pkg/storage/memory.go index 5971445d0..3be7f1e79 100644 --- a/gateway/gateway-controller/pkg/storage/memory.go +++ b/gateway/gateway-controller/pkg/storage/memory.go @@ -80,7 +80,7 @@ func (cs *ConfigStore) Add(cfg *models.StoredConfig) error { cs.handle[handle] = cfg.ID cs.nameVersion[key] = cfg.ID - if cfg.Configuration.Kind == api.Asyncwebsub { + if cfg.Configuration.Kind == api.WebSubApi { err := cs.updateTopics(cfg) if err != nil { return err @@ -127,7 +127,7 @@ func (cs *ConfigStore) Update(cfg *models.StoredConfig) error { cs.nameVersion[newKey] = cfg.ID } - if cfg.Configuration.Kind == api.Asyncwebsub { + if cfg.Configuration.Kind == api.WebSubApi { err := cs.updateTopics(cfg) if err != nil { return err @@ -149,7 +149,7 @@ func (cs *ConfigStore) updateTopics(cfg *models.StoredConfig) error { apiTopicsPerRevision := make(map[string]bool) for _, topic := range asyncData.Channels { - name := strings.TrimPrefix(asyncData.Name, "/") + name := strings.TrimPrefix(asyncData.DisplayName, "/") context := strings.TrimPrefix(asyncData.Context, "/") version := strings.TrimPrefix(asyncData.Version, "/") path := strings.TrimPrefix(topic.Path, "/") @@ -179,7 +179,7 @@ func (cs *ConfigStore) Delete(id string) error { key := cfg.GetCompositeKey() handle := cfg.GetHandle() - if cfg.Configuration.Kind == api.Asyncwebsub { + if cfg.Configuration.Kind == api.WebSubApi { cs.TopicManager.RemoveAllForConfig(cfg.ID) } delete(cs.handle, handle) diff --git a/gateway/gateway-controller/pkg/utils/api_deployment.go b/gateway/gateway-controller/pkg/utils/api_deployment.go index 46af53f27..2867b5ae3 100644 --- a/gateway/gateway-controller/pkg/utils/api_deployment.go +++ b/gateway/gateway-controller/pkg/utils/api_deployment.go @@ -97,19 +97,31 @@ func (s *APIDeploymentService) DeployAPIConfiguration(params APIDeploymentParams switch apiConfig.Kind { case api.RestApi: apiData, err := apiConfig.Spec.AsAPIConfigData() - fmt.Println("APIData: ", apiData) if err != nil { return nil, fmt.Errorf("failed to parse REST API data: %w", err) } apiName = apiData.DisplayName apiVersion = apiData.Version - case api.Asyncwebsub: + case api.WebSubApi: webhookData, err := apiConfig.Spec.AsWebhookAPIData() if err != nil { return nil, fmt.Errorf("failed to parse WebSub API data: %w", err) } - apiName = webhookData.Name + apiName = webhookData.DisplayName apiVersion = webhookData.Version + + // Ensure an upstream main exists for async/websub configs so downstream + // logic can safely rely on the field being present. Create an empty + // upstream if it is missing and save it back into the union spec. + if webhookData.Upstream.Main == nil { + url := fmt.Sprintf("%s:%d", s.routerConfig.EventGateway.WebSubHubURL, s.routerConfig.EventGateway.WebSubHubPort) + webhookData.Upstream.Main = &api.Upstream{ + Url: &url, + } + if err := apiConfig.Spec.FromWebhookAPIData(webhookData); err != nil { + return nil, fmt.Errorf("failed to write updated webhook spec: %w", err) + } + } } // Validate configuration @@ -164,7 +176,7 @@ func (s *APIDeploymentService) DeployAPIConfiguration(params APIDeploymentParams DeployedVersion: 0, } - if apiConfig.Kind == api.Asyncwebsub { + if apiConfig.Kind == api.WebSubApi { topicsToRegister, topicsToUnregister := s.GetTopicsForUpdate(*storedCfg) // TODO: Pre configure the dynamic forward proxy rules for event gw // This was communication bridge will be created on the gw startup @@ -184,7 +196,10 @@ func (s *APIDeploymentService) DeployAPIConfiguration(params APIDeploymentParams childWg.Add(1) go func(topic string) { defer childWg.Done() - if err := s.RegisterTopicWithHub(s.httpClient, topic, "localhost", 8083, params.Logger); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.routerConfig.EventGateway.TimeoutSeconds)*time.Second) + defer cancel() + + if err := s.RegisterTopicWithHub(ctx, s.httpClient, topic, s.routerConfig.EventGateway.RouterHost, s.routerConfig.EventGateway.WebSubHubListenerPort, params.Logger); err != nil { params.Logger.Error("Failed to register topic with WebSubHub", zap.Error(err), zap.String("topic", topic), @@ -212,7 +227,10 @@ func (s *APIDeploymentService) DeployAPIConfiguration(params APIDeploymentParams childWg.Add(1) go func(topic string) { defer childWg.Done() - if err := s.UnregisterTopicWithHub(s.httpClient, topic, "localhost", 8083, params.Logger); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.routerConfig.EventGateway.TimeoutSeconds)*time.Second) + defer cancel() + + if err := s.UnregisterTopicWithHub(ctx, s.httpClient, topic, s.routerConfig.EventGateway.RouterHost, s.routerConfig.EventGateway.WebSubHubListenerPort, params.Logger); err != nil { params.Logger.Error("Failed to deregister topic from WebSubHub", zap.Error(err), zap.String("topic", topic), @@ -301,7 +319,7 @@ func (s *APIDeploymentService) GetTopicsForUpdate(apiConfig models.StoredConfig) for _, topic := range asyncData.Channels { // Remove leading '/' from name, context, version and topic path if present - name := strings.TrimPrefix(asyncData.Name, "/") + name := strings.TrimPrefix(asyncData.DisplayName, "/") context := strings.TrimPrefix(asyncData.Context, "/") version := strings.TrimPrefix(asyncData.Version, "/") path := strings.TrimPrefix(topic.Path, "/") @@ -421,17 +439,17 @@ func (s *APIDeploymentService) updateExistingConfig(newConfig *models.StoredConf } // RegisterTopicWithHub registers a topic with the WebSubHub -func (s *APIDeploymentService) RegisterTopicWithHub(httpClient *http.Client, topic, webSubHubHost string, webSubPort int, logger *zap.Logger) error { - return s.sendTopicRequestToHub(httpClient, topic, "register", webSubHubHost, webSubPort, logger) +func (s *APIDeploymentService) RegisterTopicWithHub(ctx context.Context, httpClient *http.Client, topic, webSubHubHost string, webSubPort int, logger *zap.Logger) error { + return s.sendTopicRequestToHub(ctx, httpClient, topic, "register", webSubHubHost, webSubPort, logger) } // UnregisterTopicWithHub unregisters a topic from the WebSubHub -func (s *APIDeploymentService) UnregisterTopicWithHub(httpClient *http.Client, topic, webSubHubHost string, webSubPort int, logger *zap.Logger) error { - return s.sendTopicRequestToHub(httpClient, topic, "deregister", webSubHubHost, webSubPort, logger) +func (s *APIDeploymentService) UnregisterTopicWithHub(ctx context.Context, httpClient *http.Client, topic, webSubHubHost string, webSubPort int, logger *zap.Logger) error { + return s.sendTopicRequestToHub(ctx, httpClient, topic, "deregister", webSubHubHost, webSubPort, logger) } // sendTopicRequestToHub sends a topic registration/unregistration request to the WebSubHub -func (s *APIDeploymentService) sendTopicRequestToHub(httpClient *http.Client, topic string, mode string, webSubHubHost string, webSubPort int, logger *zap.Logger) error { +func (s *APIDeploymentService) sendTopicRequestToHub(ctx context.Context, httpClient *http.Client, topic string, mode string, webSubHubHost string, webSubPort int, logger *zap.Logger) error { // Prepare form data formData := url.Values{} formData.Set("hub.mode", mode) @@ -447,7 +465,7 @@ func (s *APIDeploymentService) sendTopicRequestToHub(httpClient *http.Client, to for attempt := 0; attempt <= maxRetries; attempt++ { // Encode form values so special characters in hub.topic are properly percent-encoded - req, err := http.NewRequest("POST", targetURL, strings.NewReader(formData.Encode())) + req, err := http.NewRequestWithContext(ctx, "POST", targetURL, strings.NewReader(formData.Encode())) if err != nil { return fmt.Errorf("failed to create HTTP request: %w", err) } @@ -455,6 +473,12 @@ func (s *APIDeploymentService) sendTopicRequestToHub(httpClient *http.Client, to resp, err := httpClient.Do(req) if err != nil { + // If the context was cancelled or deadline exceeded, surface that + select { + case <-ctx.Done(): + return fmt.Errorf("request canceled: %w", ctx.Err()) + default: + } return fmt.Errorf("failed to send HTTP request: %w", err) } @@ -467,7 +491,7 @@ func (s *APIDeploymentService) sendTopicRequestToHub(httpClient *http.Client, to zap.String("topic", topic), zap.String("mode", mode), zap.Int("status", resp.StatusCode)) - err = nil + lastStatus = 0 return } @@ -481,7 +505,11 @@ func (s *APIDeploymentService) sendTopicRequestToHub(httpClient *http.Client, to // Retry only on 404 or 503, up to maxRetries if (lastStatus == http.StatusNotFound || lastStatus == http.StatusServiceUnavailable) && attempt < maxRetries { - time.Sleep(backoff) + select { + case <-ctx.Done(): + return fmt.Errorf("request canceled: %w", ctx.Err()) + case <-time.After(backoff): + } backoff *= 2 lastStatus = 0 continue diff --git a/gateway/gateway-controller/pkg/utils/helpers.go b/gateway/gateway-controller/pkg/utils/helpers.go index 20c3bb745..52d86d343 100644 --- a/gateway/gateway-controller/pkg/utils/helpers.go +++ b/gateway/gateway-controller/pkg/utils/helpers.go @@ -18,12 +18,12 @@ func ExtractNameVersion(cfg api.APIConfiguration) (string, string, error) { } return d.DisplayName, d.Version, nil } - if cfg.Kind == api.Asyncwebsub { + if cfg.Kind == api.WebSubApi { d, err := cfg.Spec.AsWebhookAPIData() if err != nil { return "", "", fmt.Errorf("failed to parse async/websub api config data: %w", err) } - return d.Name, d.Version, nil + return d.DisplayName, d.Version, nil } return "", "", fmt.Errorf("unsupported api kind: %s", cfg.Kind) } diff --git a/gateway/gateway-controller/pkg/utils/websub_topic_registration_test.go b/gateway/gateway-controller/pkg/utils/websub_topic_registration_test.go index 2297d9fa1..544c54fa2 100644 --- a/gateway/gateway-controller/pkg/utils/websub_topic_registration_test.go +++ b/gateway/gateway-controller/pkg/utils/websub_topic_registration_test.go @@ -21,15 +21,16 @@ func TestDeployAPIConfigurationWebSubKindTopicRegistration(t *testing.T) { service := NewAPIDeploymentService(configStore, db, snapshotManager, validator, nil) // Inline YAML config similar to websubhub.yaml - yamlConfig := `kind: async/websub -version: api-platform.wso2.com/v1 -spec: + yamlConfig := `kind: WebSubApi +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +metadata: name: testapi +spec: + displayName: testapi context: /test version: v1 - servers: - - url: "http://host.docker.internal:9098" - protocol: websub + vhosts: + main: "*" channels: - path: /topic1 - path: /topic2 @@ -66,15 +67,16 @@ func TestDeployAPIConfigurationWebSubKindRevisionDeployment(t *testing.T) { service := NewAPIDeploymentService(configStore, nil, nil, validator, nil) // Inline YAML config similar to websubhub.yaml - yamlConfig := `kind: async/websub -version: api-platform.wso2.com/v1 -spec: + yamlConfig := `kind: WebSubApi +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +metadata: name: testapi +spec: + displayName: testapi context: /test version: v1 - servers: - - url: "http://host.docker.internal:9098" - protocol: websub + vhosts: + main: "*" channels: - path: /topic1 - path: /topic2 @@ -105,15 +107,16 @@ spec: assert.True(t, configStore.TopicManager.IsTopicExist(cfg.ID, "testapi_test_v1_topic2")) // Second deployment with topic2 removed -> should deregister topic2 - yamlConfig2 := `kind: async/websub -version: api-platform.wso2.com/v1 -spec: + yamlConfig2 := `kind: WebSubApi +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +metadata: name: testapi +spec: + displayName: testapi context: /test version: v1 - servers: - - url: "http://host.docker.internal:9098" - protocol: websub + vhosts: + main: "*" channels: - path: /topic1 ` @@ -148,36 +151,34 @@ func TestTopicRegistrationForConcurrentAPIConfigs(t *testing.T) { service := NewAPIDeploymentService(configStore, nil, nil, validator, nil) // Two different API YAMLs - yamlA := `kind: async/websub -apiVersion: api-platform.wso2.com/v1 + yamlA := `kind: WebSubApi +apiVersion: gateway.api-platform.wso2.com/v1alpha1 metadata: - name: apiA + name: testapiA spec: - context: /a - name: apiA # TODO: (renuka) This should be displayName + displayName: testapiA + context: /testA version: v1 - servers: - - url: "http://host.docker.internal:9098" - protocol: websub + vhosts: + main: "*" channels: - - path: /t1 - - path: /t2 + - path: /topic1 + - path: /topic2 ` - yamlB := `kind: async/websub -apiVersion: api-platform.wso2.com/v1 + yamlB := `kind: WebSubApi +apiVersion: gateway.api-platform.wso2.com/v1alpha1 metadata: - name: apiB + name: testapiB spec: - context: /b - name: apiB # TODO: (renuka) This should be displayName + displayName: testapiB + context: /testB version: v1 - servers: - - url: "http://host.docker.internal:9098" - protocol: websub + vhosts: + main: "*" channels: - - path: /t3 - - path: /t4 + - path: /topic3 + - path: /topic4 ` var apiCfgA, apiCfgB api.APIConfiguration @@ -238,12 +239,12 @@ spec: } // Verify topics for cfgA - assert.True(t, configStore.TopicManager.IsTopicExist(cfgA.ID, "apiA_a_v1_t1")) - assert.True(t, configStore.TopicManager.IsTopicExist(cfgA.ID, "apiA_a_v1_t2")) + assert.True(t, configStore.TopicManager.IsTopicExist(cfgA.ID, "testapiA_testA_v1_topic1")) + assert.True(t, configStore.TopicManager.IsTopicExist(cfgA.ID, "testapiA_testA_v1_topic2")) // Verify topics for cfgB - assert.True(t, configStore.TopicManager.IsTopicExist(cfgB.ID, "apiB_b_v1_t3")) - assert.True(t, configStore.TopicManager.IsTopicExist(cfgB.ID, "apiB_b_v1_t4")) + assert.True(t, configStore.TopicManager.IsTopicExist(cfgB.ID, "testapiB_testB_v1_topic3")) + assert.True(t, configStore.TopicManager.IsTopicExist(cfgB.ID, "testapiB_testB_v1_topic4")) } func TestTopicDeregistrationOnConfigDeletion(t *testing.T) { @@ -252,15 +253,16 @@ func TestTopicDeregistrationOnConfigDeletion(t *testing.T) { service := NewAPIDeploymentService(configStore, nil, nil, validator, nil) // Inline YAML config similar to websubhub.yaml - yamlConfig := `kind: async/websub -version: api-platform.wso2.com/v1 -spec: + yamlConfig := `kind: WebSubApi +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +metadata: name: testapi +spec: + displayName: testapi context: /test version: v1 - servers: - - url: "http://host.docker.internal:9098" - protocol: websub + vhosts: + main: "*" channels: - path: /topic1 - path: /topic2 diff --git a/gateway/gateway-controller/pkg/xds/translator.go b/gateway/gateway-controller/pkg/xds/translator.go index 74d410e94..b8ce6041b 100644 --- a/gateway/gateway-controller/pkg/xds/translator.go +++ b/gateway/gateway-controller/pkg/xds/translator.go @@ -67,8 +67,8 @@ import ( const ( DynamicForwardProxyClusterName = "dynamic-forward-proxy-cluster" ExternalProcessorGRPCServiceClusterName = "ext-processor-grpc-service" - WebSubHubInternalClusterName = "websubhub-internal-cluster" OTELCollectorClusterName = "otel_collector" + WebSubHubInternalClusterName = "WEBSUBHUB_INTERNAL_CLUSTER" ) // Translator converts API configurations to Envoy xDS resources @@ -163,7 +163,7 @@ func (t *Translator) TranslateConfigs( var routesList []*route.Route var clusterList []*cluster.Cluster var err error - if cfg.Configuration.Kind == api.Asyncwebsub { + if cfg.Configuration.Kind == api.WebSubApi { routesList, clusterList, err = t.translateAsyncAPIConfig(cfg) if err != nil { log.Error("Failed to translate config", @@ -295,7 +295,7 @@ func (t *Translator) TranslateConfigs( parsedURL.Scheme = "http" } - websubhubCluster := t.createCluster(WebSubHubInternalClusterName, parsedURL, nil) + websubhubCluster := t.createCluster(constants.WEBSUBHUB_INTERNAL_CLUSTER_NAME, parsedURL, nil) clusters = append(clusters, websubhubCluster) websubInternalListener, err := t.createListenerForWebSubHub() if err != nil { @@ -344,30 +344,51 @@ func (t *Translator) TranslateConfigs( func (t *Translator) translateAsyncAPIConfig(cfg *models.StoredConfig) ([]*route.Route, []*cluster.Cluster, error) { apiData, err := cfg.Configuration.Spec.AsWebhookAPIData() if err != nil { - return nil, nil, fmt.Errorf("failed to parse webhook API data: %w", err) + return nil, nil, fmt.Errorf("failed to parse WebSub config data: %w", err) } - t.logger.Info("Started translating routes for WebSub API") + clusters := []*cluster.Cluster{} + + mainClusterName := constants.WEBSUBHUB_INTERNAL_CLUSTER_NAME + parsedMainURL, err := url.Parse(t.routerConfig.EventGateway.WebSubHubURL) + if err != nil { + return nil, nil, fmt.Errorf("invalid upstream URL: %w", err) + } + if parsedMainURL.Path == "" { + parsedMainURL.Path = "/hub" + } - // Create routes for each operation + // Create routes for each operation (default to main cluster) routesList := make([]*route.Route, 0) + mainRoutesList := make([]*route.Route, 0) + + // Determine effective vhosts (fallback to global router defaults when not provided) + effectiveMainVHost := t.config.GatewayController.Router.VHosts.Main.Default + if apiData.Vhosts != nil { + if strings.TrimSpace(apiData.Vhosts.Main) != "" { + effectiveMainVHost = apiData.Vhosts.Main + } + } + for _, ch := range apiData.Channels { - // Ensure channel path starts with '/' chPath := ch.Path if !strings.HasPrefix(chPath, "/") { chPath = "/" + chPath } - // Always route accepts a POST request for WebSubHub calls - r := t.createRoutePerTopic("POST", apiData.Context, apiData.Version, WebSubHubInternalClusterName, chPath) - routesList = append(routesList, r) + // Use mainClusterName by default; path rewrite based on main upstream path + r := t.createRoutePerTopic(apiData.DisplayName, apiData.Version, apiData.Context, "POST", chPath, + mainClusterName, effectiveMainVHost) + mainRoutesList = append(mainRoutesList, r) } + routesList = append(routesList, mainRoutesList...) - return routesList, nil, nil + return routesList, clusters, nil } // translateAPIConfig translates a single API configuration func (t *Translator) translateAPIConfig(cfg *models.StoredConfig) ([]*route.Route, []*cluster.Cluster, error) { apiData, err := cfg.Configuration.Spec.AsAPIConfigData() + cfg.GetContext() if err != nil { return nil, nil, fmt.Errorf("failed to parse API config data: %w", err) } @@ -588,7 +609,7 @@ func (t *Translator) createListenerForWebSubHub() (*listener.Listener, error) { routeConfig := &route.RouteConfiguration{ Name: "websubhub-internal-route", VirtualHosts: []*route.VirtualHost{{ - Name: "websubhub-internal", + Name: "WEBSUBHUB_INTERNAL_VHOST", Domains: []string{"*"}, Routes: []*route.Route{{ Match: &route.RouteMatch{PathSpecifier: &route.RouteMatch_Path{Path: "/websubhub/operations"}}, @@ -668,8 +689,8 @@ func (t *Translator) createDynamicFwdListenerForWebSubHub() (*listener.Listener, dynamicForwardProxyRouteConfig := &route.RouteConfiguration{ Name: "dynamic-forward-proxy-routing", VirtualHosts: []*route.VirtualHost{{ - Name: "all-domains", - Domains: []string{"*"}, // this should be websubhub domains + Name: "DYNAMIXC_FORWARD_PROXY_VHOST_WEBSUBHUB", + Domains: []string{t.routerConfig.EventGateway.WebSubHubURL}, // this should be websubhub domains Routes: []*route.Route{{ Match: &route.RouteMatch{PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}}, Action: &route.Route_Route{Route: &route.RouteAction{ @@ -721,10 +742,9 @@ func (t *Translator) createDynamicFwdListenerForWebSubHub() (*listener.Listener, } httpConnManager := &hcm.HttpConnectionManager{ - StatPrefix: "WEBSUBHUB_INBOUND_8082", - CodecType: hcm.HttpConnectionManager_AUTO, - GenerateRequestId: wrapperspb.Bool(true), - RouteSpecifier: &hcm.HttpConnectionManager_RouteConfig{RouteConfig: dynamicForwardProxyRouteConfig}, + StatPrefix: "WEBSUBHUB_INBOUND_8082_LISTENER", + CodecType: hcm.HttpConnectionManager_AUTO, + RouteSpecifier: &hcm.HttpConnectionManager_RouteConfig{RouteConfig: dynamicForwardProxyRouteConfig}, HttpFilters: []*hcm.HttpFilter{ { // dynamic forward proxy filter Name: "envoy.filters.http.dynamic_forward_proxy", @@ -931,9 +951,10 @@ func (t *Translator) createRoute(apiId, apiName, apiVersion, context, method, pa } // createRoutePerTopic creates a route for an operation -func (t *Translator) createRoutePerTopic(method, context, apiVersion, clusterName, path string) *route.Route { +func (t *Translator) createRoutePerTopic(apiName, apiVersion, context, method, path, clusterName, vhost string) *route.Route { + routeName := GenerateRouteName(method, context, apiVersion, path, vhost) r := &route.Route{ - Name: GenerateRouteName(method, context, apiVersion, path, t.routerConfig.GatewayHost), + Name: routeName, Match: &route.RouteMatch{ Headers: []*route.HeaderMatcher{{ Name: ":method", @@ -955,6 +976,21 @@ func (t *Translator) createRoutePerTopic(method, context, apiVersion, clusterNam }, } + // Attach dynamic metadata for downstream correlation (policies, logging, tracing) + metaMap := map[string]interface{}{ + "route_name": routeName, + "api_name": apiName, + "api_version": apiVersion, + "api_context": context, + "path": path, + "method": method, + } + if metaStruct, err := structpb.NewStruct(metaMap); err == nil { + r.Metadata = &core.Metadata{FilterMetadata: map[string]*structpb.Struct{ + "wso2.route": metaStruct, + }} + } + r.Match.PathSpecifier = &route.RouteMatch_Path{ Path: ConstructFullPath(context, apiVersion, path), } diff --git a/gateway/it/test-config.yaml b/gateway/it/test-config.yaml index 655586e3d..f3a0725cf 100644 --- a/gateway/it/test-config.yaml +++ b/gateway/it/test-config.yaml @@ -51,7 +51,14 @@ gateway_controller: router: # Gateway host for incoming requests gateway_host: "*" - + # Event Gateway (WebSub Hub) configuration + event_gateway: + enabled: true + websub_hub_url: "http://host.docker.internal" + websub_hub_port: 9098 + router_host: "localhost" + websub_hub_listener_port: 8083 + timeout_seconds: 10 # Access logs configuration access_logs: # Enable or disable access logs diff --git a/go.work.sum b/go.work.sum index 2e509818c..b115f9b06 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1080,6 +1080,8 @@ github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -1287,6 +1289,7 @@ github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03D github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= @@ -1352,6 +1355,7 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= @@ -1370,6 +1374,7 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -1412,6 +1417,7 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -1431,8 +1437,10 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-dap v0.12.0 h1:rVcjv3SyMIrpaOoTAdFDyHs99CwVOItIJGKLQFQhNeM= github.com/google/go-dap v0.12.0/go.mod h1:tNjCASCm5cqePi/RVXXWEVqtnNLV1KTWtYOqu6rZNzc= github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= @@ -1550,6 +1558,7 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwn github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= @@ -1897,6 +1906,7 @@ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzL github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= @@ -1923,6 +1933,8 @@ github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= @@ -1933,6 +1945,7 @@ github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9 github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= @@ -2043,6 +2056,7 @@ github.com/twmb/murmur3 v1.1.3/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq github.com/twpayne/go-kml/v3 v3.2.1 h1:xkTIJ7KMnHGKpHGf30e4XS3UT8o/5jD62hmdGJPf7Io= github.com/twpayne/go-kml/v3 v3.2.1/go.mod h1:lPWoJR3nQAdePBy3SrnniLdBLVQX0hlxrcziCx9XgT0= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= @@ -2256,6 +2270,8 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= @@ -2267,9 +2283,11 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -2308,6 +2326,7 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2396,6 +2415,7 @@ golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -2419,6 +2439,7 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -2476,6 +2497,7 @@ golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2547,6 +2569,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2572,6 +2595,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2583,6 +2607,7 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2640,6 +2665,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= @@ -2647,6 +2673,7 @@ golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsO golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -2686,6 +2713,7 @@ golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -2713,6 +2741,7 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2794,6 +2823,7 @@ golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2971,6 +3001,7 @@ google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b h1:kqShdsddZrS6q+DGBCA73CzHsKDu5vW4qw78tFnbVvY= google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:gw1DtiPCt5uh/HV9STVEeaO00S5ATsJiJ2LsZV8lcDI= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -3273,6 +3304,7 @@ google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5 google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -3294,6 +3326,7 @@ google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=