-
Notifications
You must be signed in to change notification settings - Fork 32
Tag based gateway deployment #572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package it | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
| "strings" | ||
| "sync" | ||
|
|
||
| "github.com/cucumber/godog" | ||
| ) | ||
|
|
||
| const ConfigTagPrefix = "@config-" | ||
|
|
||
| // GatewayConfigManager handles configuration switching for the gateway | ||
| type GatewayConfigManager struct { | ||
| registry *ConfigProfileRegistry | ||
| composeManager *ComposeManager | ||
| currentProfile string | ||
| mu sync.Mutex | ||
| } | ||
|
|
||
| // NewGatewayConfigManager creates a new config manager | ||
| func NewGatewayConfigManager(cm *ComposeManager) *GatewayConfigManager { | ||
| return &GatewayConfigManager{ | ||
| registry: NewConfigProfileRegistry(), | ||
| composeManager: cm, | ||
| currentProfile: "default", // Assume default on startup | ||
| } | ||
| } | ||
|
|
||
| // EnsureConfig checks the scenario tags and restarts the gateway if a different config is required | ||
| func (m *GatewayConfigManager) EnsureConfig(ctx context.Context, sc *godog.Scenario) error { | ||
| m.mu.Lock() | ||
| defer m.mu.Unlock() | ||
|
|
||
| requiredProfile := m.extractConfigTag(sc) | ||
| if requiredProfile == "" { | ||
| requiredProfile = "default" | ||
| } | ||
|
|
||
| if m.currentProfile == requiredProfile { | ||
| return nil // No restart needed | ||
| } | ||
|
|
||
| log.Printf("Switching gateway config from '%s' to '%s'...", m.currentProfile, requiredProfile) | ||
|
|
||
| profile, ok := m.registry.Get(requiredProfile) | ||
| if !ok { | ||
| return fmt.Errorf("unknown config profile: %s", requiredProfile) | ||
| } | ||
|
|
||
| // Restart gateway-controller with new env vars | ||
| if err := m.composeManager.RestartGatewayController(ctx, profile.EnvVars); err != nil { | ||
| return fmt.Errorf("failed to restart gateway with profile %s: %w", requiredProfile, err) | ||
| } | ||
|
|
||
| m.currentProfile = requiredProfile | ||
| log.Printf("Switched to '%s' profile successfully", requiredProfile) | ||
| return nil | ||
| } | ||
|
|
||
| // extractConfigTag finds the first tag starting with @config- and returns the suffix | ||
| func (m *GatewayConfigManager) extractConfigTag(sc *godog.Scenario) string { | ||
| for _, tag := range sc.Tags { | ||
| if strings.HasPrefix(tag.Name, ConfigTagPrefix) { | ||
| return strings.TrimPrefix(tag.Name, ConfigTagPrefix) | ||
| } | ||
| } | ||
| return "" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package it | ||
|
|
||
| // ConfigProfile defines a named configuration set for the gateway | ||
| type ConfigProfile struct { | ||
| Name string | ||
| EnvVars map[string]string | ||
| Description string | ||
| } | ||
|
|
||
| // ConfigProfileRegistry manages the available configuration profiles | ||
| type ConfigProfileRegistry struct { | ||
| profiles map[string]*ConfigProfile | ||
| defaultProfile string | ||
| } | ||
|
|
||
| // NewConfigProfileRegistry creates a new registry with standard profiles | ||
| func NewConfigProfileRegistry() *ConfigProfileRegistry { | ||
| registry := &ConfigProfileRegistry{ | ||
| profiles: make(map[string]*ConfigProfile), | ||
| defaultProfile: "default", | ||
| } | ||
|
|
||
| // Register standard profiles | ||
| registry.Register(&ConfigProfile{ | ||
| Name: "default", | ||
| EnvVars: map[string]string{ | ||
| "GATEWAY_LOGGING_LEVEL": "info", | ||
| "GATEWAY_STORAGE_TYPE": "sqlite", | ||
| }, | ||
| Description: "Standard configuration using SQLite and Info logging", | ||
| }) | ||
|
|
||
| registry.Register(&ConfigProfile{ | ||
| Name: "debug", | ||
| EnvVars: map[string]string{ | ||
| "GATEWAY_LOGGING_LEVEL": "debug", | ||
| "GATEWAY_STORAGE_TYPE": "sqlite", | ||
| }, | ||
| Description: "Debug configuration enabling verbose logging", | ||
| }) | ||
|
|
||
| registry.Register(&ConfigProfile{ | ||
| Name: "memory", | ||
| EnvVars: map[string]string{ | ||
| "GATEWAY_LOGGING_LEVEL": "info", | ||
| "GATEWAY_STORAGE_TYPE": "memory", | ||
| }, | ||
| Description: "In-memory storage configuration (non-persistent)", | ||
| }) | ||
|
|
||
| registry.Register(&ConfigProfile{ | ||
| Name: "tracing", | ||
| EnvVars: map[string]string{ | ||
| "GATEWAY_LOGGING_LEVEL": "info", | ||
| "GATEWAY_STORAGE_TYPE": "memory", | ||
| "GATEWAY_TRACING_ENABLED": "true", | ||
| }, | ||
| Description: "Configuration with OpenTelemetry tracing enabled", | ||
| }) | ||
|
|
||
| return registry | ||
|
|
||
| } | ||
|
|
||
| // Register adds a profile to the registry | ||
| func (r *ConfigProfileRegistry) Register(profile *ConfigProfile) { | ||
| r.profiles[profile.Name] = profile | ||
| } | ||
|
|
||
| // Get retrieves a profile by name | ||
| func (r *ConfigProfileRegistry) Get(name string) (*ConfigProfile, bool) { | ||
| profile, ok := r.profiles[name] | ||
| return profile, ok | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| Feature: Distributed Tracing | ||
| As a developer | ||
| I want to ensure that API requests are traced | ||
| So that I can observe the system behavior | ||
|
|
||
| @config-tracing | ||
| Scenario: API invocation generates a trace | ||
| Given the Gateway is running with tracing enabled | ||
| And I have a valid API Key for the "Sales API" | ||
| When I send a GET request to "http://localhost:9090/api/v1/sales/orders" | ||
| Then the response status code should be 200 | ||
| And I should see a trace for "Sales API" in the OpenTelemetry collector logs | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| receivers: | ||
| otlp: | ||
| protocols: | ||
| grpc: | ||
| http: | ||
|
|
||
| exporters: | ||
| logging: | ||
| loglevel: debug | ||
|
Comment on lines
+7
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace deprecated The 🤖 Prompt for AI Agents |
||
|
|
||
| service: | ||
| pipelines: | ||
| traces: | ||
| receivers: [otlp] | ||
| processors: [] | ||
| exporters: [logging] | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,7 @@ import ( | |||||
| "os/exec" | ||||||
| "os/signal" | ||||||
| "path/filepath" | ||||||
| "strings" | ||||||
| "sync" | ||||||
| "syscall" | ||||||
| "time" | ||||||
|
|
@@ -139,6 +140,9 @@ func (cm *ComposeManager) Start() error { | |||||
| return fmt.Errorf("failed to start docker compose: %w", err) | ||||||
| } | ||||||
|
|
||||||
| // Stream logs in background | ||||||
| cm.StreamLogs() | ||||||
|
|
||||||
| log.Println("Docker Compose services started, waiting for health checks...") | ||||||
|
|
||||||
| // Wait for services to be healthy (additional verification) | ||||||
|
|
@@ -156,6 +160,87 @@ func (cm *ComposeManager) Start() error { | |||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| // RestartGatewayController restarts the gateway-controller service with specific environment variables | ||||||
| func (cm *ComposeManager) RestartGatewayController(ctx context.Context, envVars map[string]string) error { | ||||||
| // Project name is "gateway-it", service is "gateway-controller". | ||||||
| // Default naming is usually project-service-1 or project_service_1. | ||||||
| // However, explicit container_name might be set in compose file. | ||||||
| // Given the context, we should rely on docker compose commands rather than assuming container name for 'docker stop/rm' | ||||||
| // BUT the prompt explicitly used "docker stop containerName" and "docker rm containerName". | ||||||
| // I should check if I can just use `docker compose stop` and `docker compose up`. | ||||||
| // The prompt suggested: | ||||||
| // exec.CommandContext(ctx, "docker", "stop", containerName).Run() | ||||||
| // exec.CommandContext(ctx, "docker", "rm", containerName).Run() | ||||||
| // args := []string{"compose", "-f", cm.composeFile, "-p", cm.projectName, "up", "-d", "gateway-controller"} | ||||||
|
|
||||||
| // I'll stick to 'docker compose' commands to be safe with names. | ||||||
|
|
||||||
| log.Println("Restarting gateway-controller with new configuration...") | ||||||
|
|
||||||
| // Stop and remove the service container | ||||||
| stopCmd := execCommandContext(ctx, "docker", "compose", "-f", cm.composeFile, "-p", cm.projectName, "stop", "gateway-controller") | ||||||
| if err := stopCmd.Run(); err != nil { | ||||||
| return fmt.Errorf("failed to stop gateway-controller: %w", err) | ||||||
| } | ||||||
|
|
||||||
| // Force remove the container by declared name to avoid conflicts | ||||||
| // We use direct docker rm because compose rm sometimes doesn't clear the name reservation fast enough | ||||||
| // or behaves differently with static container_names. | ||||||
| rmCmd := execCommandContext(ctx, "docker", "rm", "-f", "it-gateway-controller") | ||||||
| // We ignore error here because if it doesn't exist, that's fine. | ||||||
| _ = rmCmd.Run() | ||||||
|
|
||||||
| // Start with new env vars | ||||||
| args := []string{"compose", "-f", cm.composeFile, "-p", cm.projectName, "up", "-d", "gateway-controller"} | ||||||
| cmd := execCommandContext(ctx, "docker", args...) | ||||||
|
|
||||||
| // Copy existing env and append new ones | ||||||
| cmd.Env = os.Environ() | ||||||
| for k, v := range envVars { | ||||||
| cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) | ||||||
| log.Printf("Setting env: %s=%s", k, v) | ||||||
| } | ||||||
|
|
||||||
| output, err := cmd.CombinedOutput() | ||||||
| if err != nil { | ||||||
| return fmt.Errorf("failed to start gateway-controller: %w\nOutput: %s", err, string(output)) | ||||||
| } | ||||||
|
||||||
|
|
||||||
| // Wait for health check | ||||||
| return cm.WaitForGatewayControllerHealthy(ctx) | ||||||
| } | ||||||
|
|
||||||
| // WaitForGatewayControllerHealthy waits for the gateway-controller to be healthy | ||||||
| func (cm *ComposeManager) WaitForGatewayControllerHealthy(ctx context.Context) error { | ||||||
| endpoint := fmt.Sprintf("http://localhost:%s/health", GatewayControllerPort) | ||||||
| client := &http.Client{ | ||||||
| Timeout: 2 * time.Second, | ||||||
| } | ||||||
|
|
||||||
| ticker := time.NewTicker(500 * time.Millisecond) | ||||||
| defer ticker.Stop() | ||||||
|
|
||||||
| timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second) | ||||||
| defer cancel() | ||||||
|
|
||||||
| for { | ||||||
| select { | ||||||
| case <-timeoutCtx.Done(): | ||||||
| return fmt.Errorf("timeout waiting for gateway-controller to be healthy") | ||||||
| case <-ticker.C: | ||||||
| resp, err := client.Get(endpoint) | ||||||
| if err != nil { | ||||||
| continue | ||||||
| } | ||||||
| resp.Body.Close() | ||||||
|
|
||||||
| if resp.StatusCode == http.StatusOK { | ||||||
| return nil | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // WaitForHealthy waits for all services to pass health checks | ||||||
| func (cm *ComposeManager) WaitForHealthy(ctx context.Context) error { | ||||||
| services := []struct { | ||||||
|
|
@@ -245,24 +330,45 @@ func (cm *ComposeManager) Cleanup() { | |||||
| cm.isShutdown = true | ||||||
|
|
||||||
| log.Println("Cleaning up Docker Compose services...") | ||||||
|
|
||||||
| // Cancel context to stop any ongoing operations | ||||||
| cm.cancel() | ||||||
|
|
||||||
| // Stop signal handling | ||||||
| signal.Stop(cm.signalChan) | ||||||
| close(cm.signalChan) | ||||||
| cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||||
| defer cancel() | ||||||
|
|
||||||
| // Run docker compose down with cleanup context | ||||||
| cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||||
| defer cleanupCancel() | ||||||
| if err := cm.compose.Down(cleanupCtx, tc.RemoveOrphans(true), tc.RemoveImagesLocal); err != nil { | ||||||
| log.Printf("Failed to stop docker compose: %v", err) | ||||||
| } | ||||||
| }) | ||||||
| } | ||||||
|
|
||||||
| if err := cm.compose.Down(cleanupCtx, tc.RemoveOrphans(true), tc.RemoveVolumes(true)); err != nil { | ||||||
| log.Printf("Warning: error during cleanup: %v", err) | ||||||
| // StreamLogs streams service logs to stdout | ||||||
| func (cm *ComposeManager) StreamLogs() { | ||||||
| go func() { | ||||||
| log.Println("Streaming logs from containers...") | ||||||
| cmd := execCommandContext(cm.ctx, "docker", "compose", "-f", cm.composeFile, "-p", cm.projectName, "logs", "-f") | ||||||
| cmd.Stdout = os.Stdout | ||||||
| cmd.Stderr = os.Stderr | ||||||
| if err := cmd.Run(); err != nil { | ||||||
| // Don't log error on context cancellation (standard shutdown) | ||||||
| if cm.ctx.Err() == nil { | ||||||
| log.Printf("Background log streaming stopped: %v", err) | ||||||
| } | ||||||
| } | ||||||
| }() | ||||||
| } | ||||||
|
|
||||||
| log.Println("Cleanup complete") | ||||||
| }) | ||||||
| // CheckLogsForText checks if a container's logs contain specific text | ||||||
| func (cm *ComposeManager) CheckLogsForText(ctx context.Context, containerName, text string) (bool, error) { | ||||||
| // Need to use the actual container name (project name + service name usually, or explicit name) | ||||||
| // In our compose file, we set container_name explicitly (e.g., it-otel-collector) | ||||||
|
|
||||||
| cmd := execCommandContext(ctx, "docker", "logs", containerName) | ||||||
| output, err := cmd.CombinedOutput() | ||||||
| if err != nil { | ||||||
| return false, fmt.Errorf("failed to get logs for %s: %w", containerName, err) | ||||||
| } | ||||||
|
|
||||||
| return strings.Contains(string(output), text), nil | ||||||
| } | ||||||
|
|
||||||
| // CheckDockerAvailable verifies that Docker is running and accessible | ||||||
|
|
@@ -287,7 +393,7 @@ func CheckPortsAvailable() error { | |||||
| "8443", // HTTPS | ||||||
| EnvoyAdminPort, // 9901 | ||||||
| "9002", // Policy engine | ||||||
| "5000", // Sample backend | ||||||
| "5050", // Sample backend | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Port check mismatch with docker-compose.test.yaml. The port check uses 🔎 Suggested fix- "5050", // Sample backend
+ "5000", // Sample backend📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| "18000", // xDS gRPC | ||||||
| "18001", // xDS gRPC | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: wso2/api-platform
Length of output: 265
🏁 Script executed:
Repository: wso2/api-platform
Length of output: 98
🏁 Script executed:
Repository: wso2/api-platform
Length of output: 50375
🏁 Script executed:
Repository: wso2/api-platform
Length of output: 1616
🏁 Script executed:
Repository: wso2/api-platform
Length of output: 657
🏁 Script executed:
Repository: wso2/api-platform
Length of output: 235
🏁 Script executed:
Repository: wso2/api-platform
Length of output: 795
Port 9090 is for the gateway-controller REST API, not for API requests.
The request should use
http://localhost:8080/api/v1/sales/ordersinstead. The router service exposes HTTP traffic on port 8080, while port 9090 is reserved for the gateway-controller management API.🤖 Prompt for AI Agents