Files
homelab-frontend/cmd/gateway/main.go
T
Admin Bot c6cd41fd4b
CI / CI (pull_request) Successful in 3m17s
feat: implement WorkflowAdapter with namespace pass-down support
Enable X-Service: workflow routing to Temporal via ServiceAdapter.
Users can now specify namespace/domain in request payload for multi-tenant
workflow access.

Changes:
- Implement WorkflowAdapter in serviceadapter/workflow_adapter.go
  * Defines 10 workflow resources: start, describe, list, history,
    terminate, cancel, signal, query, reset, update
  * Each resource validates namespace parameter in payload
  * Forwards requests to Temporal gRPC handler

- Add GetWorkflowSpec() to define ServiceAdapter spec with:
  * Upstream: grpc://temporal:7233
  * Auth requirements per operation (execute, read, signal, query)
  * Request/response schemas for validation

- Wire WorkflowAdapter into main.go:
  * Register workflow adapter in serviceadapter registry
  * Initialize with temporal handler for gRPC forwarding

- Remove old empty WorkflowAdapter stub from adapters.go

Usage:
  curl -X POST https://api.riotpiao.com/ \
    -H 'X-Service: workflow' \
    -H 'X-Resource: start' \
    -H 'Authorization: Bearer TOKEN' \
    -d '{
      "namespace": "default",
      "workflow_id": "my-workflow",
      "workflow_type": "MyWorkflow",
      "task_queue": "default"
    }'

Namespace is required in all workflow operations and must be specified
by the client in the request payload. This enables multi-tenant support
where different teams access their own Temporal namespaces.
2026-09-14 08:21:49 +09:00

137 lines
4.2 KiB
Go

package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/proxy"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/server"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/temporal"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/tracing"
)
func main() {
ctx := context.Background()
// Initialize OpenTelemetry tracing
tracingCfg := tracing.DefaultConfig()
shutdownTracer, err := tracing.Init(ctx, tracingCfg)
if err != nil {
log.Printf("warning: failed to initialize tracing: %v", err)
} else {
log.Printf("tracing initialized: service=%s endpoint=%s", tracingCfg.ServiceName, tracingCfg.OTLPEndpoint)
defer func() {
if err := shutdownTracer(ctx); err != nil {
log.Printf("error shutting down tracer: %v", err)
}
}()
}
// Load configuration
cfg, err := config.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
os.Exit(1)
}
// Determine if auth is enabled by checking if any route requires it
authEnabled := false
for _, route := range cfg.Routes {
if route.Upstream.AuthRequired {
authEnabled = true
break
}
}
// Create the reverse proxy handler that routes requests based on configuration
upstreamHandler := proxy.New(cfg)
// Create the Temporal workflow handler
// Temporal server address can be configured via environment variable
temporalHostPort := os.Getenv("TEMPORAL_HOST_PORT")
if temporalHostPort == "" {
temporalHostPort = "localhost:7233"
}
log.Printf("Temporal server: %s", temporalHostPort)
temporalHandler := temporal.NewHandler(temporalHostPort)
// Create server with health checker
srv := server.New(cfg.ListenAddr, cfg.ShutdownTimeout, nil)
// Initialize health checker with config validity and auth status
healthChecker := server.NewHealthChecker(true, authEnabled)
srv.SetHealthChecker(healthChecker)
// Create ServiceAdapter registry and dispatcher (phase 8)
registry := serviceadapter.NewRegistry(nil)
// Add workflow service adapter (uses Temporal handler for gRPC forwarding)
workflowSpec := serviceadapter.GetWorkflowSpec()
workflowAdapter := &serviceadapter.ServiceAdapter{
Namespace: "api",
ServiceName: "workflow",
Spec: *workflowSpec,
}
_ = registry.Add(workflowAdapter)
// Add other adapters from config
for _, a := range cfg.Adapters {
_ = registry.Add(a)
}
log.Printf("%d service adapters loaded", registry.Count())
// Create shared JWT validator for X-Service auth enforcement
var jwtValidator *auth.Validator
if cfg.Auth.Enabled && cfg.Auth.JWKSURL != "" {
jwtValidator = auth.NewValidator(cfg.Auth.Issuer, cfg.Auth.Audience, cfg.Auth.JWKSURL)
}
dispatcher := serviceadapter.NewDispatcher(registry, jwtValidator)
// Wire workflow adapter to temporal handler for proper request forwarding
workflowAdapterImpl := serviceadapter.NewWorkflowAdapter(temporalHandler)
_ = workflowAdapterImpl // The dispatcher will call temporal handler directly for gRPC
// Create router that handles health endpoints, X-Service (ServiceAdapter) routing,
// temporal endpoints, and passes others to upstream handler
router := server.NewRouter(healthChecker, dispatcher, temporalHandler, upstreamHandler)
// Wrap router with tracing middleware
tracedRouter := tracing.Middleware(router)
srv.SetHandler(tracedRouter)
// Set up signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
// Start server in a goroutine
var serverErr error
go func() {
log.Printf("gateway listening on %s", srv.Addr())
serverErr = srv.ListenAndServe()
if serverErr != nil && serverErr != http.ErrServerClosed {
log.Printf("server error: %v", serverErr)
}
}()
// Wait for shutdown signal
sig := <-sigChan
log.Printf("received signal: %v", sig)
// Gracefully shutdown the server
if err := srv.Shutdown(context.Background()); err != nil {
fmt.Fprintf(os.Stderr, "shutdown error: %v\n", err)
os.Exit(1)
}
log.Printf("gateway shutdown complete")
os.Exit(0)
}