Skip to content

Commit 9b0ef98

Browse files
authored
Merge pull request #997 from dushaniw/gw-undeploy
Handle API undeployment and redeployment events from CP in Gateway
2 parents 54ea668 + 32153b0 commit 9b0ef98

File tree

20 files changed

+534
-276
lines changed

20 files changed

+534
-276
lines changed

gateway/gateway-controller/api/openapi.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2569,6 +2569,7 @@ components:
25692569
- pending
25702570
- deployed
25712571
- failed
2572+
- undeployed
25722573
example: deployed
25732574
created_at:
25742575
type: string
@@ -3636,6 +3637,7 @@ components:
36363637
- pending
36373638
- deployed
36383639
- failed
3640+
- undeployed
36393641
example: deployed
36403642
created_at:
36413643
type: string

gateway/gateway-controller/pkg/api/generated/generated.go

Lines changed: 199 additions & 197 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gateway/gateway-controller/pkg/api/handlers/handlers.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,10 +2105,10 @@ func (s *APIServer) waitForDeploymentAndNotify(configID string, correlationID st
21052105
// Extract API ID from stored config (use config ID as API ID)
21062106
apiID := configID
21072107

2108-
// Use empty revision ID for now (can be made configurable later)
2109-
revisionID := ""
2108+
// Use empty deployment ID for now (can be made configurable later)
2109+
deploymentID := ""
21102110

2111-
if err := s.controlPlaneClient.NotifyAPIDeployment(apiID, cfg, revisionID); err != nil {
2111+
if err := s.controlPlaneClient.NotifyAPIDeployment(apiID, cfg, deploymentID); err != nil {
21122112
log.Error("Failed to notify platform-api of successful deployment",
21132113
slog.String("api_id", apiID),
21142114
slog.Any("error", err))
@@ -2157,6 +2157,8 @@ func (s *APIServer) GetConfigDump(c *gin.Context) {
21572157
status = api.ConfigDumpAPIMetadataStatusFailed
21582158
case models.StatusPending:
21592159
status = api.ConfigDumpAPIMetadataStatusPending
2160+
case models.StatusUndeployed:
2161+
status = api.ConfigDumpAPIMetadataStatusUndeployed
21602162
default:
21612163
status = api.ConfigDumpAPIMetadataStatusPending
21622164
}

gateway/gateway-controller/pkg/api/handlers/handlers_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ func (m *MockControlPlaneClient) IsConnected() bool {
394394
return m.connected
395395
}
396396

397-
func (m *MockControlPlaneClient) NotifyAPIDeployment(apiID string, cfg *models.StoredConfig, revisionID string) error {
397+
func (m *MockControlPlaneClient) NotifyAPIDeployment(apiID string, cfg *models.StoredConfig, deploymentID string) error {
398398
return nil
399399
}
400400

@@ -442,7 +442,7 @@ func createTestAPIServer() *APIServer {
442442
},
443443
},
444444
}
445-
445+
446446
// Initialize API key service (needed for API key operations)
447447
apiKeyService := utils.NewAPIKeyService(store, mockDB, nil, &server.systemConfig.GatewayController.APIKey)
448448
server.apiKeyService = apiKeyService
@@ -1372,11 +1372,11 @@ func TestWaitForDeploymentAndNotifyTimeout(t *testing.T) {
13721372
func TestNewAPIServer(t *testing.T) {
13731373
store := storage.NewConfigStore()
13741374
mockDB := NewMockStorage()
1375-
1375+
13761376
policyDefs := map[string]api.PolicyDefinition{
13771377
"test|v1": {Name: "test", Version: "v1"},
13781378
}
1379-
1379+
13801380
// LLMProviderTemplate structure matches API spec
13811381
templateDefs := make(map[string]*api.LLMProviderTemplate)
13821382
templateName := "test-template"
@@ -1385,14 +1385,14 @@ func TestNewAPIServer(t *testing.T) {
13851385
Name: templateName,
13861386
},
13871387
}
1388-
1388+
13891389
validator := config.NewAPIValidator()
1390-
1390+
13911391
vhosts := &config.VHostsConfig{
13921392
Main: config.VHostEntry{Default: "localhost"},
13931393
Sandbox: config.VHostEntry{Default: "sandbox-localhost"},
13941394
}
1395-
1395+
13961396
systemConfig := &config.Config{
13971397
GatewayController: config.GatewayController{
13981398
Router: config.RouterConfig{
@@ -1404,7 +1404,7 @@ func TestNewAPIServer(t *testing.T) {
14041404
},
14051405
},
14061406
}
1407-
1407+
14081408
// This test is simplified - full test would require proper xDS mocks
14091409
// Instead, we just verify the structure
14101410
t.Run("verify test server creation", func(t *testing.T) {
@@ -1416,7 +1416,7 @@ func TestNewAPIServer(t *testing.T) {
14161416
assert.NotNil(t, server.parser)
14171417
assert.NotNil(t, server.validator)
14181418
})
1419-
1419+
14201420
// Verify configuration objects are created correctly
14211421
t.Run("verify config structures", func(t *testing.T) {
14221422
assert.NotNil(t, store)
@@ -2367,7 +2367,7 @@ func TestUpdateAPIKeyInvalidBody(t *testing.T) {
23672367
server.UpdateAPIKey(c, "test-handle", "test-key")
23682368

23692369
assert.Equal(t, http.StatusBadRequest, w.Code)
2370-
2370+
23712371
var response api.ErrorResponse
23722372
err := json.Unmarshal(w.Body.Bytes(), &response)
23732373
require.NoError(t, err)
@@ -2390,7 +2390,7 @@ func TestUpdateAPIKeyMissingAPIKey(t *testing.T) {
23902390
server.UpdateAPIKey(c, "test-handle", "test-key")
23912391

23922392
assert.Equal(t, http.StatusBadRequest, w.Code)
2393-
2393+
23942394
var response api.ErrorResponse
23952395
err := json.Unmarshal(w.Body.Bytes(), &response)
23962396
require.NoError(t, err)
@@ -2410,9 +2410,9 @@ func TestRevokeAPIKeyNotFound(t *testing.T) {
24102410
server.RevokeAPIKey(c, "test-handle", "nonexistent")
24112411

24122412
assert.Equal(t, http.StatusNotFound, w.Code)
2413-
2413+
24142414
var response api.ErrorResponse
24152415
err := json.Unmarshal(w.Body.Bytes(), &response)
24162416
require.NoError(t, err)
24172417
assert.Equal(t, "error", response.Status)
2418-
}
2418+
}

gateway/gateway-controller/pkg/controlplane/client.go

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ type ConnectionState struct {
8888
// ControlPlaneClient interface defines the methods needed from the control plane client
8989
type ControlPlaneClient interface {
9090
IsConnected() bool
91-
NotifyAPIDeployment(apiID string, apiConfig *models.StoredConfig, revisionID string) error
91+
NotifyAPIDeployment(apiID string, apiConfig *models.StoredConfig, deploymentID string) error
9292
}
9393

9494
// Client manages the WebSocket connection to the control plane
@@ -590,7 +590,7 @@ func (c *Client) handleAPIDeployedEvent(event map[string]interface{}) {
590590
c.logger.Info("Processing API deployment",
591591
slog.String("api_id", apiID),
592592
slog.String("environment", deployedEvent.Payload.Environment),
593-
slog.String("revision_id", deployedEvent.Payload.RevisionID),
593+
slog.String("deployment_id", deployedEvent.Payload.DeploymentID),
594594
slog.String("vhost", deployedEvent.Payload.VHost),
595595
slog.String("correlation_id", deployedEvent.CorrelationID),
596596
)
@@ -685,7 +685,114 @@ func (c *Client) handleAPIUndeployedEvent(event map[string]interface{}) {
685685
slog.Any("timestamp", event["timestamp"]),
686686
slog.Any("correlationId", event["correlationId"]),
687687
)
688-
// TODO: Implement actual API undeployment logic in Phase 6
688+
689+
// Parse the event into structured format
690+
eventBytes, err := json.Marshal(event)
691+
if err != nil {
692+
c.logger.Error("Failed to marshal event for parsing",
693+
slog.Any("error", err),
694+
)
695+
return
696+
}
697+
698+
var undeployedEvent APIUndeployedEvent
699+
if err := json.Unmarshal(eventBytes, &undeployedEvent); err != nil {
700+
c.logger.Error("Failed to parse API undeployment event",
701+
slog.Any("error", err),
702+
)
703+
return
704+
}
705+
706+
// Extract API ID
707+
apiID := undeployedEvent.Payload.APIID
708+
if apiID == "" {
709+
c.logger.Error("API ID is empty in undeployment event")
710+
return
711+
}
712+
713+
c.logger.Info("Processing API undeployment",
714+
slog.String("api_id", apiID),
715+
slog.String("environment", undeployedEvent.Payload.Environment),
716+
slog.String("vhost", undeployedEvent.Payload.VHost),
717+
slog.String("correlation_id", undeployedEvent.CorrelationID),
718+
)
719+
720+
// Check if config exists in database first (source of truth when persistent storage is available)
721+
var apiConfig *models.StoredConfig
722+
if c.db != nil {
723+
var err error
724+
apiConfig, err = c.db.GetConfig(apiID)
725+
if err != nil {
726+
c.logger.Warn("API configuration not found in database for undeployment",
727+
slog.String("api_id", apiID),
728+
slog.Any("error", err),
729+
)
730+
// Not an error - the API might already be undeployed or deleted
731+
return
732+
}
733+
} else {
734+
// Fall back to in-memory store if database is not available
735+
var err error
736+
apiConfig, err = c.store.Get(apiID)
737+
if err != nil {
738+
c.logger.Warn("API configuration not found in storage for undeployment",
739+
slog.String("api_id", apiID),
740+
slog.Any("error", err),
741+
)
742+
// Not an error - the API might already be undeployed or deleted
743+
return
744+
}
745+
}
746+
747+
// Set status to undeployed (preserve config, keys, and policies)
748+
apiConfig.Status = models.StatusUndeployed
749+
apiConfig.UpdatedAt = time.Now()
750+
// Keep DeployedVersion as-is - it tracks when it was last deployed
751+
752+
// Update database (only if persistent mode)
753+
if c.db != nil {
754+
if err := c.db.UpdateConfig(apiConfig); err != nil {
755+
c.logger.Error("Failed to update config status in database",
756+
slog.String("api_id", apiID),
757+
slog.Any("error", err),
758+
)
759+
return
760+
}
761+
}
762+
763+
// Update in-memory store
764+
if err := c.store.Update(apiConfig); err != nil {
765+
c.logger.Error("Failed to update config status in memory store",
766+
slog.String("api_id", apiID),
767+
slog.Any("error", err),
768+
)
769+
return
770+
}
771+
772+
// Note: We keep API keys and policies for potential redeploy
773+
// They will be reused if the API is redeployed
774+
775+
// Update xDS snapshot asynchronously (undeployed APIs will be filtered out)
776+
go func() {
777+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
778+
defer cancel()
779+
780+
if err := c.snapshotManager.UpdateSnapshot(ctx, undeployedEvent.CorrelationID); err != nil {
781+
c.logger.Error("Failed to update xDS snapshot after API undeployment",
782+
slog.String("api_id", apiID),
783+
slog.Any("error", err),
784+
)
785+
} else {
786+
c.logger.Info("Successfully updated xDS snapshot after API undeployment",
787+
slog.String("api_id", apiID),
788+
)
789+
}
790+
}()
791+
792+
c.logger.Info("Successfully processed API undeployment event",
793+
slog.String("api_id", apiID),
794+
slog.String("correlation_id", undeployedEvent.CorrelationID),
795+
)
689796
}
690797

691798
// handleAPIKeyCreatedEvent handles API key created events from platform-api
@@ -1119,7 +1226,7 @@ func (c *Client) IsConnected() bool {
11191226
}
11201227

11211228
// NotifyAPIDeployment sends a REST API call to platform-api when an API is deployed successfully
1122-
func (c *Client) NotifyAPIDeployment(apiID string, apiConfig *models.StoredConfig, revisionID string) error {
1229+
func (c *Client) NotifyAPIDeployment(apiID string, apiConfig *models.StoredConfig, deploymentID string) error {
11231230
// Check if connected to control plane
11241231
if !c.IsConnected() {
11251232
c.logger.Debug("Not connected to control plane, skipping API deployment notification",
@@ -1128,7 +1235,7 @@ func (c *Client) NotifyAPIDeployment(apiID string, apiConfig *models.StoredConfi
11281235
}
11291236

11301237
// Use the api utils service to send the deployment notification
1131-
return c.apiUtilsService.NotifyAPIDeployment(apiID, apiConfig, revisionID)
1238+
return c.apiUtilsService.NotifyAPIDeployment(apiID, apiConfig, deploymentID)
11321239
}
11331240

11341241
// getWebSocketURL constructs the base WebSocket URL from configuration

gateway/gateway-controller/pkg/controlplane/client_integration_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,10 @@ func TestClient_handleMessage_APIDeployedEvent(t *testing.T) {
116116
event := map[string]interface{}{
117117
"type": "api.deployed",
118118
"payload": map[string]interface{}{
119-
"apiId": "test-api-123",
120-
"environment": "production",
121-
"revisionId": "rev-1",
122-
"vhost": "api.example.com",
119+
"apiId": "test-api-123",
120+
"environment": "production",
121+
"deploymentId": "rev-1",
122+
"vhost": "api.example.com",
123123
},
124124
"timestamp": time.Now().Format(time.RFC3339),
125125
"correlationId": "corr-12345",
@@ -440,7 +440,7 @@ func TestAPIDeployedEvent_JSONParsing(t *testing.T) {
440440
"payload": {
441441
"apiId": "api-123",
442442
"environment": "production",
443-
"revisionId": "rev-1",
443+
"deploymentId": "rev-1",
444444
"vhost": "api.example.com"
445445
},
446446
"timestamp": "2025-01-30T12:00:00Z",

gateway/gateway-controller/pkg/controlplane/controlplane_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ func TestAPIDeployedEvent(t *testing.T) {
9090
event := APIDeployedEvent{
9191
Type: "api.deployed",
9292
Payload: APIDeployedEventPayload{
93-
APIID: "api-123",
94-
Environment: "production",
95-
RevisionID: "rev-1",
96-
VHost: "api.example.com",
93+
APIID: "api-123",
94+
Environment: "production",
95+
DeploymentID: "rev-1",
96+
VHost: "api.example.com",
9797
},
9898
Timestamp: "2025-01-30T12:00:00Z",
9999
CorrelationID: "corr-789",
@@ -108,8 +108,8 @@ func TestAPIDeployedEvent(t *testing.T) {
108108
if event.Payload.Environment != "production" {
109109
t.Errorf("Payload.Environment = %q, want %q", event.Payload.Environment, "production")
110110
}
111-
if event.Payload.RevisionID != "rev-1" {
112-
t.Errorf("Payload.RevisionID = %q, want %q", event.Payload.RevisionID, "rev-1")
111+
if event.Payload.DeploymentID != "rev-1" {
112+
t.Errorf("Payload.DeploymentID = %q, want %q", event.Payload.DeploymentID, "rev-1")
113113
}
114114
if event.Payload.VHost != "api.example.com" {
115115
t.Errorf("Payload.VHost = %q, want %q", event.Payload.VHost, "api.example.com")
@@ -121,10 +121,10 @@ func TestAPIDeployedEvent(t *testing.T) {
121121

122122
func TestAPIDeployedEventPayload(t *testing.T) {
123123
payload := APIDeployedEventPayload{
124-
APIID: "test-api",
125-
Environment: "staging",
126-
RevisionID: "rev-2",
127-
VHost: "staging.example.com",
124+
APIID: "test-api",
125+
Environment: "staging",
126+
DeploymentID: "rev-2",
127+
VHost: "staging.example.com",
128128
}
129129

130130
if payload.APIID != "test-api" {
@@ -133,8 +133,8 @@ func TestAPIDeployedEventPayload(t *testing.T) {
133133
if payload.Environment != "staging" {
134134
t.Errorf("Environment = %q, want %q", payload.Environment, "staging")
135135
}
136-
if payload.RevisionID != "rev-2" {
137-
t.Errorf("RevisionID = %q, want %q", payload.RevisionID, "rev-2")
136+
if payload.DeploymentID != "rev-2" {
137+
t.Errorf("DeploymentID = %q, want %q", payload.DeploymentID, "rev-2")
138138
}
139139
if payload.VHost != "staging.example.com" {
140140
t.Errorf("VHost = %q, want %q", payload.VHost, "staging.example.com")
@@ -307,7 +307,7 @@ func TestClient_NotifyAPIDeployment_NotConnected(t *testing.T) {
307307
client := createTestClient(t)
308308

309309
// When not connected, should return nil without error
310-
err := client.NotifyAPIDeployment("api-123", nil, "rev-1")
310+
err := client.NotifyAPIDeployment("api-123", nil, "deployment-1")
311311
if err != nil {
312312
t.Errorf("NotifyAPIDeployment() error = %v, want nil when not connected", err)
313313
}

0 commit comments

Comments
 (0)