diff --git a/gateway/docker-compose.yaml b/gateway/docker-compose.yaml index 219465fae..77569863d 100644 --- a/gateway/docker-compose.yaml +++ b/gateway/docker-compose.yaml @@ -36,6 +36,7 @@ services: - APIP_GW_CONTROLLER_METRICS_PORT=9091 - APIP_GW_GATEWAY_REGISTRATION_TOKEN=${GATEWAY_REGISTRATION_TOKEN:-} - APIP_GW_CONTROLPLANE_HOST=${GATEWAY_CONTROLPLANE_HOST:-host.docker.internal:9243} + - APIP_GW_CONTROLPLANE_ON_PREM=${GATEWAY_CONTROLPLANE_ON_PREM:-false} volumes: - controller-data:/app/data - ./configs/config.toml:/etc/gateway-controller/config.toml:ro diff --git a/gateway/gateway-controller/pkg/config/config.go b/gateway/gateway-controller/pkg/config/config.go index e1baf73d1..a9c380ed6 100644 --- a/gateway/gateway-controller/pkg/config/config.go +++ b/gateway/gateway-controller/pkg/config/config.go @@ -369,8 +369,9 @@ type LoggingConfig struct { // ControlPlaneConfig holds control plane connection configuration type ControlPlaneConfig struct { - Host string `koanf:"host"` // Control plane hostname + Host string `koanf:"host"` // Control plane host:port (e.g. platform-api:9243 for cloud; on-prem: apim.example.com:9443 or host.docker.internal:9443) Token string `koanf:"token"` // Registration token (api-key) + OnPrem bool `koanf:"on_prem"` // If true, use on-prem path (/internal/data/v1/ws); else cloud path (/api/internal/v1/ws) ReconnectInitial time.Duration `koanf:"reconnect_initial"` // Initial retry delay ReconnectMax time.Duration `koanf:"reconnect_max"` // Maximum retry delay PollingInterval time.Duration `koanf:"polling_interval"` // Reconciliation polling interval @@ -406,6 +407,8 @@ func LoadConfig(configPath string) (*Config, error) { switch s { case "controlplane_host": return "controller.controlplane.host" + case "controlplane_on_prem": + return "controller.controlplane.on_prem" case "gateway_registration_token": return "controller.controlplane.token" case "reconnect_initial": @@ -519,6 +522,7 @@ func defaultConfig() *Config { ControlPlane: ControlPlaneConfig{ Host: "localhost:9243", Token: "", + OnPrem: false, ReconnectInitial: 1 * time.Second, ReconnectMax: 5 * time.Minute, PollingInterval: 15 * time.Minute, @@ -934,7 +938,7 @@ func (c *Config) validateEventGatewayConfig() error { // validateControlPlaneConfig validates the control plane configuration func (c *Config) validateControlPlaneConfig() error { - // Host validation - required if control plane is configured + // Host is required; all URLs are built from host in code (path depends on on_prem). if c.Controller.ControlPlane.Host == "" { return fmt.Errorf("controlplane.host is required") } diff --git a/gateway/gateway-controller/pkg/controlplane/client.go b/gateway/gateway-controller/pkg/controlplane/client.go index d45ddaf1d..88a5d0093 100644 --- a/gateway/gateway-controller/pkg/controlplane/client.go +++ b/gateway/gateway-controller/pkg/controlplane/client.go @@ -200,7 +200,8 @@ func (c *Client) Start() error { c.logger.Info("Starting control plane client", slog.String("host", c.config.Host), - slog.String("websocket_url", c.getWebSocketURL()), + slog.String("websocket_url", c.getWebSocketConnectURL()), + slog.Bool("on_prem", c.config.OnPrem), ) // Start connection in background @@ -240,7 +241,8 @@ func (c *Client) Connect() error { c.setState(Connecting) c.logger.Info("Connecting to control plane", - slog.String("url", c.getWebSocketURL()), + slog.String("url", c.getWebSocketConnectURL()), + slog.Bool("on_prem", c.config.OnPrem), slog.Int("retry_count", c.state.RetryCount), ) @@ -261,8 +263,8 @@ func (c *Client) Connect() error { headers := http.Header{} headers.Add("api-key", c.config.Token) - // Dial WebSocket - wsURL := c.getWebSocketURL() + "/gateways/connect" + // Dial WebSocket: URL built from host (path depends on OnPrem) + wsURL := c.getWebSocketConnectURL() conn, resp, err := dialer.Dial(wsURL, headers) if err != nil { if resp != nil { @@ -1913,16 +1915,25 @@ func (c *Client) NotifyAPIDeployment(apiID string, apiConfig *models.StoredConfi return c.apiUtilsService.NotifyAPIDeployment(apiID, apiConfig, deploymentID) } -// getWebSocketURL constructs the base WebSocket URL from configuration +// getWebSocketURL constructs the base WebSocket URL from configuration. +// If OnPrem is true, uses on-prem path (/internal/data/v1/ws); otherwise cloud path (/api/internal/v1/ws). func (c *Client) getWebSocketURL() string { - return fmt.Sprintf("wss://%s/api/internal/v1/ws", - c.config.Host, - ) + if c.config.OnPrem { + return fmt.Sprintf("wss://%s/internal/data/v1/ws", c.config.Host) + } + return fmt.Sprintf("wss://%s/api/internal/v1/ws", c.config.Host) +} + +// getWebSocketConnectURL returns the full WebSocket URL for gateway connect. +func (c *Client) getWebSocketConnectURL() string { + return c.getWebSocketURL() + "/gateways/connect" } -// getRestAPIBaseURL constructs the base REST API URL from configuration +// getRestAPIBaseURL constructs the base REST API URL from configuration. +// If OnPrem is true, uses on-prem path (/internal/data/v1); otherwise cloud path (/api/internal/v1). func (c *Client) getRestAPIBaseURL() string { - return fmt.Sprintf("https://%s/api/internal/v1", - c.config.Host, - ) + if c.config.OnPrem { + return fmt.Sprintf("https://%s/internal/data/v1", c.config.Host) + } + return fmt.Sprintf("https://%s/api/internal/v1", c.config.Host) } diff --git a/gateway/gateway-controller/pkg/controlplane/controlplane_test.go b/gateway/gateway-controller/pkg/controlplane/controlplane_test.go index 8c292e650..65dcb7be1 100644 --- a/gateway/gateway-controller/pkg/controlplane/controlplane_test.go +++ b/gateway/gateway-controller/pkg/controlplane/controlplane_test.go @@ -158,6 +158,29 @@ func createTestClient(t *testing.T) *Client { return NewClient(cfg, logger, store, nil, nil, nil, routerConfig, nil, nil, nil, nil, nil, nil, nil) } +func createTestClientWithOnPrem(t *testing.T, onPrem bool) *Client { + t.Helper() + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) + store := storage.NewConfigStore() + + cfg := config.ControlPlaneConfig{ + Host: "control-plane.example.com", + Token: "test-token", + OnPrem: onPrem, + ReconnectInitial: 1 * time.Second, + ReconnectMax: 30 * time.Second, + } + + routerConfig := &config.RouterConfig{ + VHosts: config.VHostsConfig{ + Main: config.VHostEntry{Default: "api.example.com"}, + Sandbox: config.VHostEntry{Default: "sandbox.example.com"}, + }, + } + + return NewClient(cfg, logger, store, nil, nil, nil, routerConfig, nil, nil, nil, nil, nil, nil, nil) +} + func TestNewClient(t *testing.T) { client := createTestClient(t) if client == nil { @@ -223,6 +246,21 @@ func TestClient_getWebSocketURL(t *testing.T) { } } +func TestClient_getWebSocketURL_OnPrem(t *testing.T) { + client := createTestClientWithOnPrem(t, true) + + url := client.getWebSocketURL() + expected := "wss://control-plane.example.com/internal/data/v1/ws" + if url != expected { + t.Errorf("getWebSocketURL() OnPrem = %q, want %q", url, expected) + } + connectURL := client.getWebSocketConnectURL() + expectedConnect := expected + "/gateways/connect" + if connectURL != expectedConnect { + t.Errorf("getWebSocketConnectURL() = %q, want %q", connectURL, expectedConnect) + } +} + func TestClient_getRestAPIBaseURL(t *testing.T) { client := createTestClient(t) @@ -233,6 +271,16 @@ func TestClient_getRestAPIBaseURL(t *testing.T) { } } +func TestClient_getRestAPIBaseURL_OnPrem(t *testing.T) { + client := createTestClientWithOnPrem(t, true) + + url := client.getRestAPIBaseURL() + expected := "https://control-plane.example.com/internal/data/v1" + if url != expected { + t.Errorf("getRestAPIBaseURL() OnPrem = %q, want %q", url, expected) + } +} + func TestClient_isShuttingDown(t *testing.T) { client := createTestClient(t)