diff --git a/platform-api/src/api/generated.go b/platform-api/src/api/generated.go index 22ebd5d69..eefcf6966 100644 --- a/platform-api/src/api/generated.go +++ b/platform-api/src/api/generated.go @@ -135,6 +135,12 @@ const ( GatewayArtifactTypeRESTAPI GatewayArtifactType = "REST_API" ) +// Defines values for GatewayEndpointProtocol. +const ( + GatewayEndpointProtocolHttps GatewayEndpointProtocol = "https" + GatewayEndpointProtocolWss GatewayEndpointProtocol = "wss" +) + // Defines values for GatewayResponseFunctionalityType. const ( GatewayResponseFunctionalityTypeAi GatewayResponseFunctionalityType = "ai" @@ -453,8 +459,8 @@ const ( // Defines values for WebSubAPITransport. const ( - WebSubAPITransportHttp WebSubAPITransport = "http" - WebSubAPITransportHttps WebSubAPITransport = "https" + Http WebSubAPITransport = "http" + Https WebSubAPITransport = "https" ) // Defines values for WebSubAPIListItemLifeCycleStatus. @@ -857,6 +863,9 @@ type CreateGatewayRequest struct { // DisplayName Human-readable gateway name DisplayName string `binding:"required" json:"displayName" yaml:"displayName"` + // Endpoints List of endpoints (host/protocol/port combinations) exposed by this gateway + Endpoints *[]GatewayEndpoint `json:"endpoints,omitempty" yaml:"endpoints,omitempty"` + // FunctionalityType Type of gateway functionality FunctionalityType CreateGatewayRequestFunctionalityType `binding:"required" json:"functionalityType" yaml:"functionalityType"` @@ -1295,6 +1304,21 @@ type GatewayArtifactListResponse struct { Pagination Pagination `json:"pagination" yaml:"pagination"` } +// GatewayEndpoint defines model for GatewayEndpoint. +type GatewayEndpoint struct { + // Host Hostname for this endpoint + Host string `binding:"required" json:"host" yaml:"host"` + + // Port Port number for this endpoint + Port int32 `binding:"required" json:"port" yaml:"port"` + + // Protocol Protocol for this endpoint + Protocol GatewayEndpointProtocol `binding:"required" json:"protocol" yaml:"protocol"` +} + +// GatewayEndpointProtocol Protocol for this endpoint +type GatewayEndpointProtocol string + // GatewayListResponse defines model for GatewayListResponse. type GatewayListResponse struct { // Count Number of items in current response @@ -1334,6 +1358,9 @@ type GatewayResponse struct { // DisplayName Human-readable gateway name DisplayName *string `json:"displayName,omitempty" yaml:"displayName,omitempty"` + // Endpoints List of endpoints (host/protocol/port combinations) exposed by this gateway + Endpoints *[]GatewayEndpoint `json:"endpoints,omitempty" yaml:"endpoints,omitempty"` + // FunctionalityType Type of gateway functionality FunctionalityType *GatewayResponseFunctionalityType `json:"functionalityType,omitempty" yaml:"functionalityType,omitempty"` @@ -2475,6 +2502,9 @@ type RESTAPIGatewayResponse struct { // DisplayName Human-readable gateway name DisplayName *string `json:"displayName,omitempty" yaml:"displayName,omitempty"` + // Endpoints List of endpoints (host/protocol/port combinations) exposed by this gateway + Endpoints *[]GatewayEndpoint `json:"endpoints,omitempty" yaml:"endpoints,omitempty"` + // FunctionalityType Type of gateway functionality FunctionalityType *RESTAPIGatewayResponseFunctionalityType `json:"functionalityType,omitempty" yaml:"functionalityType,omitempty"` @@ -2913,6 +2943,9 @@ type UpdateGatewayRequest struct { // DisplayName Human-readable gateway name DisplayName *string `json:"displayName,omitempty" yaml:"displayName,omitempty"` + // Endpoints Updated list of endpoints (host/protocol/port combinations) exposed by this gateway + Endpoints *[]GatewayEndpoint `json:"endpoints,omitempty" yaml:"endpoints,omitempty"` + // IsCritical Whether the gateway is critical for production IsCritical *bool `json:"isCritical,omitempty" yaml:"isCritical,omitempty"` diff --git a/platform-api/src/internal/database/schema.postgres.sql b/platform-api/src/internal/database/schema.postgres.sql index 65ab2c96b..f587929ca 100644 --- a/platform-api/src/internal/database/schema.postgres.sql +++ b/platform-api/src/internal/database/schema.postgres.sql @@ -150,6 +150,7 @@ CREATE TABLE IF NOT EXISTS gateways ( description VARCHAR(1023), properties JSONB NOT NULL DEFAULT '{}'::jsonb, vhost VARCHAR(255) NOT NULL, + endpoints JSONB NOT NULL DEFAULT '[]'::jsonb, is_critical BOOLEAN DEFAULT FALSE, gateway_functionality_type VARCHAR(20) DEFAULT 'regular' NOT NULL, is_active BOOLEAN DEFAULT FALSE, diff --git a/platform-api/src/internal/database/schema.sql b/platform-api/src/internal/database/schema.sql index fbf5cc4df..7271ccfb4 100644 --- a/platform-api/src/internal/database/schema.sql +++ b/platform-api/src/internal/database/schema.sql @@ -146,6 +146,7 @@ CREATE TABLE IF NOT EXISTS gateways ( description VARCHAR(1023), properties TEXT NOT NULL DEFAULT '{}', vhost VARCHAR(255) NOT NULL, + endpoints TEXT NOT NULL DEFAULT '[]', is_critical BOOLEAN DEFAULT FALSE, gateway_functionality_type VARCHAR(20) DEFAULT 'regular' NOT NULL, is_active BOOLEAN DEFAULT FALSE, diff --git a/platform-api/src/internal/database/schema.sqlite.sql b/platform-api/src/internal/database/schema.sqlite.sql index 6a5e12bbe..9b6ae29df 100644 --- a/platform-api/src/internal/database/schema.sqlite.sql +++ b/platform-api/src/internal/database/schema.sqlite.sql @@ -149,6 +149,7 @@ CREATE TABLE IF NOT EXISTS gateways ( description VARCHAR(1023), properties TEXT NOT NULL DEFAULT '{}', vhost VARCHAR(255) NOT NULL, + endpoints TEXT NOT NULL DEFAULT '[]', is_critical BOOLEAN DEFAULT FALSE, gateway_functionality_type VARCHAR(20) DEFAULT 'regular' NOT NULL, is_active BOOLEAN DEFAULT FALSE, diff --git a/platform-api/src/internal/handler/gateway.go b/platform-api/src/internal/handler/gateway.go index 4ac7707c7..0978ea8fe 100644 --- a/platform-api/src/internal/handler/gateway.go +++ b/platform-api/src/internal/handler/gateway.go @@ -28,6 +28,7 @@ import ( "strings" "platform-api/src/internal/middleware" + "platform-api/src/internal/model" "platform-api/src/internal/service" "platform-api/src/internal/utils" @@ -105,8 +106,20 @@ func (h *GatewayHandler) CreateGateway(c *gin.Context) { return } + var endpoints []model.GatewayEndpoint + if req.Endpoints != nil { + endpoints = make([]model.GatewayEndpoint, 0, len(*req.Endpoints)) + for _, ep := range *req.Endpoints { + endpoints = append(endpoints, model.GatewayEndpoint{ + Host: ep.Host, + Protocol: string(ep.Protocol), + Port: int(ep.Port), + }) + } + } + gateway, err := h.gatewayService.RegisterGateway(orgId, req.Name, req.DisplayName, description, req.Vhost, - isCritical, functionalityType, version, properties) + isCritical, functionalityType, version, properties, endpoints) if err != nil { errMsg := err.Error() @@ -262,7 +275,20 @@ func (h *GatewayHandler) UpdateGateway(c *gin.Context) { return } - response, err := h.gatewayService.UpdateGateway(gatewayId, orgId, req.Description, req.DisplayName, req.IsCritical, req.Properties) + var endpoints *[]model.GatewayEndpoint + if req.Endpoints != nil { + eps := make([]model.GatewayEndpoint, 0, len(*req.Endpoints)) + for _, ep := range *req.Endpoints { + eps = append(eps, model.GatewayEndpoint{ + Host: ep.Host, + Protocol: string(ep.Protocol), + Port: int(ep.Port), + }) + } + endpoints = &eps + } + + response, err := h.gatewayService.UpdateGateway(gatewayId, orgId, req.Description, req.DisplayName, req.IsCritical, req.Properties, endpoints) if err != nil { if errors.Is(err, constants.ErrGatewayNotFound) { h.slogger.Error("Gateway not found during update", "error", err) diff --git a/platform-api/src/internal/model/gateway.go b/platform-api/src/internal/model/gateway.go index d8cdf01ce..308652bb5 100644 --- a/platform-api/src/internal/model/gateway.go +++ b/platform-api/src/internal/model/gateway.go @@ -21,6 +21,13 @@ import ( "time" ) +// GatewayEndpoint represents a single host/protocol/port combination for a gateway +type GatewayEndpoint struct { + Host string `json:"host"` + Protocol string `json:"protocol"` + Port int `json:"port"` +} + // Gateway represents a registered gateway instance within an organization type Gateway struct { ID string `json:"id" db:"uuid"` @@ -30,12 +37,13 @@ type Gateway struct { Description string `json:"description" db:"description"` Properties map[string]interface{} `json:"properties,omitempty" db:"properties"` Vhost string `json:"vhost" db:"vhost"` - IsCritical bool `json:"isCritical" db:"is_critical"` - FunctionalityType string `json:"functionalityType" db:"gateway_functionality_type"` - Version string `json:"version" db:"version"` - IsActive bool `json:"isActive" db:"is_active"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + Endpoints []GatewayEndpoint `json:"endpoints" db:"endpoints"` + IsCritical bool `json:"isCritical" db:"is_critical"` + FunctionalityType string `json:"functionalityType" db:"gateway_functionality_type"` + Version string `json:"version" db:"version"` + IsActive bool `json:"isActive" db:"is_active"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` } // TableName returns the table name for the Gateway model @@ -74,18 +82,19 @@ func (t *GatewayToken) Revoke() { // APIGatewayWithDetails represents a gateway with its association and deployment details for an API type APIGatewayWithDetails struct { // Gateway information - ID string `json:"id" db:"id"` - OrganizationID string `json:"organizationId" db:"organization_id"` - Name string `json:"name" db:"name"` - DisplayName string `json:"displayName" db:"display_name"` - Description string `json:"description" db:"description"` + ID string `json:"id" db:"id"` + OrganizationID string `json:"organizationId" db:"organization_id"` + Name string `json:"name" db:"name"` + DisplayName string `json:"displayName" db:"display_name"` + Description string `json:"description" db:"description"` Properties map[string]interface{} `json:"properties,omitempty" db:"properties"` - Vhost string `json:"vhost" db:"vhost"` - IsCritical bool `json:"isCritical" db:"is_critical"` - FunctionalityType string `json:"functionalityType" db:"functionality_type"` - IsActive bool `json:"isActive" db:"is_active"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + Vhost string `json:"vhost" db:"vhost"` + Endpoints []GatewayEndpoint `json:"endpoints" db:"endpoints"` + IsCritical bool `json:"isCritical" db:"is_critical"` + FunctionalityType string `json:"functionalityType" db:"functionality_type"` + IsActive bool `json:"isActive" db:"is_active"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` // Association information AssociatedAt time.Time `json:"associatedAt" db:"associated_at"` diff --git a/platform-api/src/internal/repository/gateway.go b/platform-api/src/internal/repository/gateway.go index 224407681..313b87faa 100644 --- a/platform-api/src/internal/repository/gateway.go +++ b/platform-api/src/internal/repository/gateway.go @@ -56,29 +56,36 @@ func (r *GatewayRepo) Create(gateway *model.Gateway) error { propertiesJSON = "{}" } + // Serialize endpoints to JSON + endpointsJSON, err := json.Marshal(gateway.Endpoints) + if err != nil { + return fmt.Errorf("failed to marshal endpoints: %w", err) + } + query := ` - INSERT INTO gateways (uuid, organization_uuid, name, display_name, description, properties, vhost, is_critical, + INSERT INTO gateways (uuid, organization_uuid, name, display_name, description, properties, vhost, endpoints, is_critical, gateway_functionality_type, version, is_active, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` - _, err := r.db.Exec(r.db.Rebind(query), gateway.ID, gateway.OrganizationID, gateway.Name, gateway.DisplayName, - gateway.Description, propertiesJSON, gateway.Vhost, gateway.IsCritical, gateway.FunctionalityType, gateway.Version, - gateway.IsActive, gateway.CreatedAt, gateway.UpdatedAt) + _, err = r.db.Exec(r.db.Rebind(query), gateway.ID, gateway.OrganizationID, gateway.Name, gateway.DisplayName, + gateway.Description, propertiesJSON, gateway.Vhost, string(endpointsJSON), gateway.IsCritical, gateway.FunctionalityType, + gateway.Version, gateway.IsActive, gateway.CreatedAt, gateway.UpdatedAt) return err } // GetByUUID retrieves a gateway by ID func (r *GatewayRepo) GetByUUID(gatewayId string) (*model.Gateway, error) { gateway := &model.Gateway{} - var propertiesJSON string + var propertiesJSON, endpointsJSON string query := ` - SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, is_critical, gateway_functionality_type, version, is_active, - created_at, updated_at + SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, endpoints, is_critical, + gateway_functionality_type, version, is_active, created_at, updated_at FROM gateways WHERE uuid = ? ` err := r.db.QueryRow(r.db.Rebind(query), gatewayId).Scan( - &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, &propertiesJSON, &gateway.Vhost, + &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, + &propertiesJSON, &gateway.Vhost, &endpointsJSON, &gateway.IsCritical, &gateway.FunctionalityType, &gateway.Version, &gateway.IsActive, &gateway.CreatedAt, &gateway.UpdatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -94,14 +101,21 @@ func (r *GatewayRepo) GetByUUID(gatewayId string) (*model.Gateway, error) { } } + // Deserialize endpoints from JSON + if endpointsJSON != "" && endpointsJSON != "[]" { + if err := json.Unmarshal([]byte(endpointsJSON), &gateway.Endpoints); err != nil { + return nil, fmt.Errorf("failed to unmarshal endpoints: %w", err) + } + } + return gateway, nil } // GetByOrganizationID retrieves all gateways for an organization func (r *GatewayRepo) GetByOrganizationID(orgID string) ([]*model.Gateway, error) { query := ` - SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, is_critical, gateway_functionality_type, version, is_active, - created_at, updated_at + SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, endpoints, is_critical, + gateway_functionality_type, version, is_active, created_at, updated_at FROM gateways WHERE organization_uuid = ? ORDER BY created_at DESC @@ -115,9 +129,10 @@ func (r *GatewayRepo) GetByOrganizationID(orgID string) ([]*model.Gateway, error var gateways []*model.Gateway for rows.Next() { gateway := &model.Gateway{} - var propertiesJSON string + var propertiesJSON, endpointsJSON string err := rows.Scan( - &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, &propertiesJSON, &gateway.Vhost, + &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, + &propertiesJSON, &gateway.Vhost, &endpointsJSON, &gateway.IsCritical, &gateway.FunctionalityType, &gateway.Version, &gateway.IsActive, &gateway.CreatedAt, &gateway.UpdatedAt) if err != nil { return nil, err @@ -130,6 +145,13 @@ func (r *GatewayRepo) GetByOrganizationID(orgID string) ([]*model.Gateway, error } } + // Deserialize endpoints from JSON + if endpointsJSON != "" && endpointsJSON != "[]" { + if err := json.Unmarshal([]byte(endpointsJSON), &gateway.Endpoints); err != nil { + return nil, fmt.Errorf("failed to unmarshal endpoints: %w", err) + } + } + gateways = append(gateways, gateway) } return gateways, nil @@ -138,15 +160,16 @@ func (r *GatewayRepo) GetByOrganizationID(orgID string) ([]*model.Gateway, error // GetByNameAndOrgID checks if a gateway with the given name exists within an organization func (r *GatewayRepo) GetByNameAndOrgID(name, orgID string) (*model.Gateway, error) { gateway := &model.Gateway{} - var propertiesJSON string + var propertiesJSON, endpointsJSON string query := ` - SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, is_critical, gateway_functionality_type, version, is_active, - created_at, updated_at + SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, endpoints, is_critical, + gateway_functionality_type, version, is_active, created_at, updated_at FROM gateways WHERE name = ? AND organization_uuid = ? ` err := r.db.QueryRow(r.db.Rebind(query), name, orgID).Scan( - &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, &propertiesJSON, &gateway.Vhost, + &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, + &propertiesJSON, &gateway.Vhost, &endpointsJSON, &gateway.IsCritical, &gateway.FunctionalityType, &gateway.Version, &gateway.IsActive, &gateway.CreatedAt, &gateway.UpdatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -162,14 +185,21 @@ func (r *GatewayRepo) GetByNameAndOrgID(name, orgID string) (*model.Gateway, err } } + // Deserialize endpoints from JSON + if endpointsJSON != "" && endpointsJSON != "[]" { + if err := json.Unmarshal([]byte(endpointsJSON), &gateway.Endpoints); err != nil { + return nil, fmt.Errorf("failed to unmarshal endpoints: %w", err) + } + } + return gateway, nil } // List retrieves all gateways func (r *GatewayRepo) List() ([]*model.Gateway, error) { query := ` - SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, is_critical, gateway_functionality_type, version, is_active, - created_at, updated_at + SELECT uuid, organization_uuid, name, display_name, description, properties, vhost, endpoints, is_critical, + gateway_functionality_type, version, is_active, created_at, updated_at FROM gateways ORDER BY created_at DESC ` @@ -182,9 +212,10 @@ func (r *GatewayRepo) List() ([]*model.Gateway, error) { var gateways []*model.Gateway for rows.Next() { gateway := &model.Gateway{} - var propertiesJSON string + var propertiesJSON, endpointsJSON string err := rows.Scan( - &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, &propertiesJSON, &gateway.Vhost, + &gateway.ID, &gateway.OrganizationID, &gateway.Name, &gateway.DisplayName, &gateway.Description, + &propertiesJSON, &gateway.Vhost, &endpointsJSON, &gateway.IsCritical, &gateway.FunctionalityType, &gateway.Version, &gateway.IsActive, &gateway.CreatedAt, &gateway.UpdatedAt) if err != nil { return nil, err @@ -197,6 +228,13 @@ func (r *GatewayRepo) List() ([]*model.Gateway, error) { } } + // Deserialize endpoints from JSON + if endpointsJSON != "" && endpointsJSON != "[]" { + if err := json.Unmarshal([]byte(endpointsJSON), &gateway.Endpoints); err != nil { + return nil, fmt.Errorf("failed to unmarshal endpoints: %w", err) + } + } + gateways = append(gateways, gateway) } return gateways, nil @@ -254,12 +292,19 @@ func (r *GatewayRepo) UpdateGateway(gateway *model.Gateway) error { propertiesJSON = "{}" } + // Serialize endpoints to JSON + endpointsJSON, err := json.Marshal(gateway.Endpoints) + if err != nil { + return fmt.Errorf("failed to marshal endpoints: %w", err) + } + query := ` UPDATE gateways - SET display_name = ?, description = ?, is_critical = ?, properties = ?, updated_at = ? + SET display_name = ?, description = ?, is_critical = ?, properties = ?, endpoints = ?, updated_at = ? WHERE uuid = ? ` - _, err := r.db.Exec(r.db.Rebind(query), gateway.DisplayName, gateway.Description, gateway.IsCritical, propertiesJSON, gateway.UpdatedAt, gateway.ID) + _, err = r.db.Exec(r.db.Rebind(query), gateway.DisplayName, gateway.Description, gateway.IsCritical, propertiesJSON, + string(endpointsJSON), gateway.UpdatedAt, gateway.ID) return err } diff --git a/platform-api/src/internal/service/gateway.go b/platform-api/src/internal/service/gateway.go index df6182474..a05fd68f1 100644 --- a/platform-api/src/internal/service/gateway.go +++ b/platform-api/src/internal/service/gateway.go @@ -487,7 +487,7 @@ const defaultGatewayVersion = "1.0" // RegisterGateway registers a new gateway with organization validation func (s *GatewayService) RegisterGateway(orgID, name, displayName, description, vhost string, isCritical bool, - functionalityType, version string, properties map[string]interface{}) (*api.GatewayResponse, error) { + functionalityType, version string, properties map[string]interface{}, endpoints []model.GatewayEndpoint) (*api.GatewayResponse, error) { // 1. Validate inputs if err := s.validateGatewayInput(orgID, name, displayName, vhost, functionalityType); err != nil { return nil, err @@ -541,6 +541,7 @@ func (s *GatewayService) RegisterGateway(orgID, name, displayName, description, Description: description, Properties: properties, Vhost: vhost, + Endpoints: endpoints, IsCritical: isCritical, FunctionalityType: functionalityType, Version: version, @@ -618,7 +619,7 @@ func (s *GatewayService) GetGateway(gatewayId, orgId string) (*api.GatewayRespon // UpdateGateway updates gateway details func (s *GatewayService) UpdateGateway(gatewayId, orgId string, description, displayName *string, - isCritical *bool, properties *map[string]interface{}) (*api.GatewayResponse, error) { + isCritical *bool, properties *map[string]interface{}, endpoints *[]model.GatewayEndpoint) (*api.GatewayResponse, error) { // Get existing gateway gateway, err := s.gatewayRepo.GetByUUID(gatewayId) if err != nil { @@ -643,6 +644,9 @@ func (s *GatewayService) UpdateGateway(gatewayId, orgId string, description, dis if properties != nil { gateway.Properties = *properties } + if endpoints != nil { + gateway.Endpoints = *endpoints + } gateway.UpdatedAt = time.Now() err = s.gatewayRepo.UpdateGateway(gateway) @@ -1052,6 +1056,16 @@ func gatewayModelToAPI(gateway *model.Gateway) *api.GatewayResponse { } functionalityType := api.GatewayResponseFunctionalityType(gateway.FunctionalityType) + // Convert model endpoints to API endpoints + apiEndpoints := make([]api.GatewayEndpoint, 0, len(gateway.Endpoints)) + for _, ep := range gateway.Endpoints { + apiEndpoints = append(apiEndpoints, api.GatewayEndpoint{ + Host: ep.Host, + Protocol: api.GatewayEndpointProtocol(ep.Protocol), + Port: int32(ep.Port), + }) + } + return &api.GatewayResponse{ Id: &gatewayID, OrganizationId: &orgID, @@ -1060,6 +1074,7 @@ func gatewayModelToAPI(gateway *model.Gateway) *api.GatewayResponse { Description: utils.StringPtrIfNotEmpty(gateway.Description), Properties: utils.MapPtrIfNotEmpty(gateway.Properties), Vhost: &gateway.Vhost, + Endpoints: &apiEndpoints, IsCritical: &gateway.IsCritical, FunctionalityType: &functionalityType, Version: &gateway.Version, diff --git a/platform-api/src/internal/service/gateway_endpoints_test.go b/platform-api/src/internal/service/gateway_endpoints_test.go new file mode 100644 index 000000000..d9af4e842 --- /dev/null +++ b/platform-api/src/internal/service/gateway_endpoints_test.go @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed 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. + * + */ + +package service + +import ( + "reflect" + "testing" + + "platform-api/src/internal/constants" + "platform-api/src/internal/model" +) + +// TestRegisterGatewayEndpoints covers the two supported CREATE combinations: +// 1. vhost only (endpoints omitted) — endpoints is optional, vhost is required +// 2. both vhost and endpoints present — all values stored and returned +// +// A "single endpoint" sub-case is included alongside the multi-endpoint case. +func TestRegisterGatewayEndpoints(t *testing.T) { + const orgID = "123e4567-e89b-12d3-a456-426614174000" + + newService := func() (*GatewayService, *mockGatewayRepository) { + repo := &mockGatewayRepository{} + svc := &GatewayService{ + gatewayRepo: repo, + orgRepo: &mockOrganizationRepository{org: &model.Organization{ID: orgID}}, + } + return svc, repo + } + + t.Run("vhost only, no endpoints", func(t *testing.T) { + svc, repo := newService() + + response, err := svc.RegisterGateway( + orgID, "gw-vhost-only", "Vhost Only Gateway", "", "api.example.com", + false, constants.GatewayFunctionalityTypeRegular, "1.0", nil, nil, + ) + if err != nil { + t.Fatalf("RegisterGateway() unexpected error: %v", err) + } + if response == nil { + t.Fatal("RegisterGateway() returned nil response") + } + + // vhost stored correctly + if repo.createdGateway.Vhost != "api.example.com" { + t.Errorf("createdGateway.Vhost = %q, want %q", repo.createdGateway.Vhost, "api.example.com") + } + + // endpoints stored as nil in model (nothing was provided) + if repo.createdGateway.Endpoints != nil { + t.Errorf("createdGateway.Endpoints = %v, want nil", repo.createdGateway.Endpoints) + } + + // response reflects the vhost + if response.Vhost == nil || *response.Vhost != "api.example.com" { + t.Errorf("response.Vhost = %v, want \"api.example.com\"", response.Vhost) + } + + // response endpoints is a pointer to an empty slice (not nil) so it serialises as [] + if response.Endpoints == nil { + t.Error("response.Endpoints is nil, want pointer to empty slice") + } else if len(*response.Endpoints) != 0 { + t.Errorf("response.Endpoints length = %d, want 0", len(*response.Endpoints)) + } + }) + + t.Run("both vhost and single endpoint", func(t *testing.T) { + svc, repo := newService() + + input := []model.GatewayEndpoint{ + {Host: "api.example.com", Protocol: "https", Port: 443}, + } + + response, err := svc.RegisterGateway( + orgID, "gw-single-ep", "Single Endpoint Gateway", "", "api.example.com", + false, constants.GatewayFunctionalityTypeRegular, "1.0", nil, input, + ) + if err != nil { + t.Fatalf("RegisterGateway() unexpected error: %v", err) + } + if response == nil { + t.Fatal("RegisterGateway() returned nil response") + } + + // vhost and endpoint stored correctly + if repo.createdGateway.Vhost != "api.example.com" { + t.Errorf("createdGateway.Vhost = %q, want %q", repo.createdGateway.Vhost, "api.example.com") + } + if !reflect.DeepEqual(repo.createdGateway.Endpoints, input) { + t.Errorf("createdGateway.Endpoints = %v, want %v", repo.createdGateway.Endpoints, input) + } + + // response reflects both + if response.Vhost == nil || *response.Vhost != "api.example.com" { + t.Errorf("response.Vhost = %v, want \"api.example.com\"", response.Vhost) + } + if response.Endpoints == nil { + t.Fatal("response.Endpoints is nil") + } + if len(*response.Endpoints) != 1 { + t.Fatalf("response.Endpoints length = %d, want 1", len(*response.Endpoints)) + } + ep := (*response.Endpoints)[0] + if ep.Host != "api.example.com" || string(ep.Protocol) != "https" || int(ep.Port) != 443 { + t.Errorf("response endpoint = {host:%s protocol:%s port:%d}, want {api.example.com https 443}", + ep.Host, ep.Protocol, ep.Port) + } + }) + + t.Run("both vhost and multiple endpoints", func(t *testing.T) { + svc, repo := newService() + + input := []model.GatewayEndpoint{ + {Host: "api.example.com", Protocol: "https", Port: 8443}, + {Host: "events.example.com", Protocol: "wss", Port: 8444}, + {Host: "events.example.com", Protocol: "sse", Port: 8445}, + } + + response, err := svc.RegisterGateway( + orgID, "gw-multi-ep", "Multi Endpoint Gateway", "", "api.example.com", + false, constants.GatewayFunctionalityTypeRegular, "1.0", nil, input, + ) + if err != nil { + t.Fatalf("RegisterGateway() unexpected error: %v", err) + } + if response == nil { + t.Fatal("RegisterGateway() returned nil response") + } + + if !reflect.DeepEqual(repo.createdGateway.Endpoints, input) { + t.Errorf("createdGateway.Endpoints = %v, want %v", repo.createdGateway.Endpoints, input) + } + + if response.Endpoints == nil { + t.Fatal("response.Endpoints is nil") + } + if len(*response.Endpoints) != len(input) { + t.Fatalf("response.Endpoints length = %d, want %d", len(*response.Endpoints), len(input)) + } + for i, ep := range *response.Endpoints { + if ep.Host != input[i].Host || string(ep.Protocol) != input[i].Protocol || int(ep.Port) != input[i].Port { + t.Errorf("response endpoint[%d] = {host:%s protocol:%s port:%d}, want {%s %s %d}", + i, ep.Host, ep.Protocol, ep.Port, input[i].Host, input[i].Protocol, input[i].Port) + } + } + }) +} diff --git a/platform-api/src/internal/service/gateway_properties_test.go b/platform-api/src/internal/service/gateway_properties_test.go index aef134977..def8e8ccd 100644 --- a/platform-api/src/internal/service/gateway_properties_test.go +++ b/platform-api/src/internal/service/gateway_properties_test.go @@ -103,6 +103,7 @@ func TestRegisterGatewayProperties(t *testing.T) { constants.GatewayFunctionalityTypeRegular, "1.0", properties, + []model.GatewayEndpoint{{Host: "api.example.com", Protocol: "https", Port: 443}}, ) if err != nil { t.Fatalf("RegisterGateway() error = %v", err) @@ -150,7 +151,7 @@ func TestUpdateGatewayProperties(t *testing.T) { } newDescription := "New description" - response, err := service.UpdateGateway(gatewayID, orgID, &newDescription, nil, nil, nil) + response, err := service.UpdateGateway(gatewayID, orgID, &newDescription, nil, nil, nil, nil) if err != nil { t.Fatalf("UpdateGateway() error = %v", err) } @@ -188,7 +189,7 @@ func TestUpdateGatewayProperties(t *testing.T) { "tier": "premium", } - response, err := service.UpdateGateway(gatewayID, orgID, nil, nil, nil, &newProperties) + response, err := service.UpdateGateway(gatewayID, orgID, nil, nil, nil, &newProperties, nil) if err != nil { t.Fatalf("UpdateGateway() error = %v", err) } diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index e6e43fb55..33539ac8f 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -6600,6 +6600,34 @@ components: description: Timestamp when the API was deployed example: "2025-10-15T11:00:00Z" + GatewayEndpoint: + type: object + required: + - host + - protocol + - port + properties: + host: + type: string + description: Hostname for this endpoint + maxLength: 255 + minLength: 1 + # hostname regex as per RFC 1123 (http://tools.ietf.org/html/rfc1123) + pattern: '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' + example: gateway.example.com + protocol: + type: string + enum: [https, wss] + description: Protocol for this endpoint + example: https + port: + type: integer + format: int32 + minimum: 1 + maximum: 65535 + description: Port number for this endpoint + example: 443 + CreateGatewayRequest: description: | Request body for creating a gateway. Organization ID is automatically extracted @@ -6636,6 +6664,16 @@ components: # hostname regex as per RFC 1123 (http://tools.ietf.org/html/rfc1123) and appended * pattern: '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' example: mg.wso2.com + endpoints: + type: array + description: List of endpoints (host/protocol/port combinations) exposed by this gateway + minItems: 1 + uniqueItems: true + items: + $ref: '#/components/schemas/GatewayEndpoint' + example: + - { host: api.example.com, protocol: https, port: 8443 } + - { host: events.example.com, protocol: wss, port: 8444 } isCritical: type: boolean description: Whether the gateway is critical for production @@ -6779,6 +6817,15 @@ components: type: string description: Virtual host (domain name) for the gateway example: mg.wso2.com + endpoints: + type: array + description: List of endpoints (host/protocol/port combinations) exposed by this gateway + uniqueItems: true + items: + $ref: '#/components/schemas/GatewayEndpoint' + example: + - { host: api.example.com, protocol: https, port: 8443 } + - { host: events.example.com, protocol: wss, port: 8444 } isCritical: type: boolean description: Whether the gateway is critical for production @@ -6820,6 +6867,16 @@ components: type: string description: Description of the gateway example: "Updated gateway description" + endpoints: + type: array + description: Updated list of endpoints (host/protocol/port combinations) exposed by this gateway + minItems: 1 + uniqueItems: true + items: + $ref: '#/components/schemas/GatewayEndpoint' + example: + - { host: api.example.com, protocol: https, port: 8443 } + - { host: events.example.com, protocol: wss, port: 8444 } isCritical: type: boolean description: Whether the gateway is critical for production