refactor: Align Epic 0 & Epic 1 with true microservices architecture

Refactor core kernel and infrastructure to support true microservices
architecture where services are independently deployable.

Phase 1: Core Kernel Cleanup
- Remove database provider from CoreModule (services create their own)
- Update ProvideHealthRegistry to not depend on database
- Add schema support to database client (NewClientWithSchema)
- Update main entry point to remove database dependency
- Core kernel now provides only: config, logger, error bus, health, metrics, tracer, service registry

Phase 2: Service Registry Implementation
- Create ServiceRegistry interface (pkg/registry/registry.go)
- Implement Consul registry (internal/registry/consul/consul.go)
- Add Consul dependency (github.com/hashicorp/consul/api)
- Add registry configuration to config/default.yaml
- Add ProvideServiceRegistry() to DI container

Phase 3: Service Client Interfaces
- Create service client interfaces:
  - pkg/services/auth.go - AuthServiceClient
  - pkg/services/identity.go - IdentityServiceClient
  - pkg/services/authz.go - AuthzServiceClient
  - pkg/services/audit.go - AuditServiceClient
- Create ServiceClientFactory (internal/client/factory.go)
- Create stub gRPC client implementations (internal/client/grpc/)
- Add ProvideServiceClientFactory() to DI container

Phase 4: gRPC Service Definitions
- Create proto files for all core services:
  - api/proto/auth.proto
  - api/proto/identity.proto
  - api/proto/authz.proto
  - api/proto/audit.proto
- Add generate-proto target to Makefile

Phase 5: API Gateway Implementation
- Create API Gateway service entry point (cmd/api-gateway/main.go)
- Create Gateway implementation (services/gateway/gateway.go)
- Add gateway configuration to config/default.yaml
- Gateway registers with Consul and routes requests to backend services

All code compiles successfully. Core services (Auth, Identity, Authz, Audit)
will be implemented in Epic 2 using these foundations.
This commit is contained in:
2025-11-06 09:23:36 +01:00
parent 38a251968c
commit 16731fc1d1
25 changed files with 1826 additions and 21 deletions

127
services/gateway/gateway.go Normal file
View File

@@ -0,0 +1,127 @@
// Package gateway provides API Gateway implementation.
package gateway
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"git.dcentral.systems/toolz/goplt/internal/client"
"git.dcentral.systems/toolz/goplt/pkg/config"
"git.dcentral.systems/toolz/goplt/pkg/logger"
"git.dcentral.systems/toolz/goplt/pkg/registry"
"github.com/gin-gonic/gin"
)
// Gateway handles routing requests to backend services.
type Gateway struct {
config config.ConfigProvider
log logger.Logger
clientFactory *client.ServiceClientFactory
registry registry.ServiceRegistry
routes []RouteConfig
}
// RouteConfig defines a route configuration.
type RouteConfig struct {
Path string
Service string
AuthRequired bool
}
// NewGateway creates a new API Gateway instance.
func NewGateway(
cfg config.ConfigProvider,
log logger.Logger,
clientFactory *client.ServiceClientFactory,
reg registry.ServiceRegistry,
) (*Gateway, error) {
// Load route configurations
routes := loadRoutes(cfg)
return &Gateway{
config: cfg,
log: log,
clientFactory: clientFactory,
registry: reg,
routes: routes,
}, nil
}
// SetupRoutes configures routes on the Gin router.
func (g *Gateway) SetupRoutes(router *gin.Engine) {
// Setup route handlers
for _, route := range g.routes {
route := route // Capture for closure
router.Any(route.Path, g.handleRoute(route))
}
// Default handler for unmatched routes
router.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": "Route not found",
"path": c.Request.URL.Path,
})
})
}
// handleRoute returns a handler function for a route.
func (g *Gateway) handleRoute(route RouteConfig) gin.HandlerFunc {
return func(c *gin.Context) {
// TODO: Add authentication middleware if auth_required is true
// TODO: Add rate limiting middleware
// TODO: Add CORS middleware
// Discover service instances
ctx := c.Request.Context()
instances, err := g.registry.Discover(ctx, route.Service)
if err != nil {
g.log.Error("Failed to discover service",
logger.String("service", route.Service),
logger.Error(err),
)
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Service unavailable",
})
return
}
if len(instances) == 0 {
g.log.Warn("No instances found for service",
logger.String("service", route.Service),
)
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Service unavailable",
})
return
}
// Use first healthy instance (load balancing can be added later)
instance := instances[0]
targetURL := fmt.Sprintf("http://%s:%d", instance.Address, instance.Port)
// Create reverse proxy
target, err := url.Parse(targetURL)
if err != nil {
g.log.Error("Failed to parse target URL",
logger.String("url", targetURL),
logger.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
})
return
}
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.ServeHTTP(c.Writer, c.Request)
}
}
// loadRoutes loads route configurations from config.
func loadRoutes(cfg config.ConfigProvider) []RouteConfig {
// For now, return empty routes - will be loaded from config in future
// This is a placeholder implementation
return []RouteConfig{}
}

View File

@@ -0,0 +1,127 @@
// Package gateway provides API Gateway implementation.
package gateway
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"git.dcentral.systems/toolz/goplt/internal/client"
"git.dcentral.systems/toolz/goplt/pkg/config"
"git.dcentral.systems/toolz/goplt/pkg/logger"
"git.dcentral.systems/toolz/goplt/pkg/registry"
"github.com/gin-gonic/gin"
)
// Gateway handles routing requests to backend services.
type Gateway struct {
config config.ConfigProvider
log logger.Logger
clientFactory *client.ServiceClientFactory
registry registry.ServiceRegistry
routes []RouteConfig
}
// RouteConfig defines a route configuration.
type RouteConfig struct {
Path string
Service string
AuthRequired bool
}
// NewGateway creates a new API Gateway instance.
func NewGateway(
cfg config.ConfigProvider,
log logger.Logger,
clientFactory *client.ServiceClientFactory,
reg registry.ServiceRegistry,
) (*Gateway, error) {
// Load route configurations
routes := loadRoutes(cfg)
return &Gateway{
config: cfg,
log: log,
clientFactory: clientFactory,
registry: reg,
routes: routes,
}, nil
}
// SetupRoutes configures routes on the Gin router.
func (g *Gateway) SetupRoutes(router *gin.Engine) {
// Setup route handlers
for _, route := range g.routes {
route := route // Capture for closure
router.Any(route.Path, g.handleRoute(route))
}
// Default handler for unmatched routes
router.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": "Route not found",
"path": c.Request.URL.Path,
})
})
}
// handleRoute returns a handler function for a route.
func (g *Gateway) handleRoute(route RouteConfig) gin.HandlerFunc {
return func(c *gin.Context) {
// TODO: Add authentication middleware if auth_required is true
// TODO: Add rate limiting middleware
// TODO: Add CORS middleware
// Discover service instances
ctx := c.Request.Context()
instances, err := g.registry.Discover(ctx, route.Service)
if err != nil {
g.log.Error("Failed to discover service",
logger.String("service", route.Service),
logger.Error(err),
)
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Service unavailable",
})
return
}
if len(instances) == 0 {
g.log.Warn("No instances found for service",
logger.String("service", route.Service),
)
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Service unavailable",
})
return
}
// Use first healthy instance (load balancing can be added later)
instance := instances[0]
targetURL := fmt.Sprintf("http://%s:%d", instance.Address, instance.Port)
// Create reverse proxy
target, err := url.Parse(targetURL)
if err != nil {
g.log.Error("Failed to parse target URL",
logger.String("url", targetURL),
logger.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
})
return
}
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.ServeHTTP(c.Writer, c.Request)
}
}
// loadRoutes loads route configurations from config.
func loadRoutes(cfg config.ConfigProvider) []RouteConfig {
// For now, return empty routes - will be loaded from config in future
// This is a placeholder implementation
return []RouteConfig{}
}