mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-17 05:59:42 +02:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
319 lines
7.8 KiB
Markdown
319 lines
7.8 KiB
Markdown
# Route Provider
|
|
|
|
Discovers and loads routes from Docker containers, YAML files, and remote agents.
|
|
|
|
## Overview
|
|
|
|
The `internal/route/provider` package implements route discovery and loading for GoDoxy. It supports multiple provider types (Docker, File, Agent) and manages route lifecycle including validation, start/stop, and event handling.
|
|
|
|
### Primary Consumers
|
|
|
|
- **Main entry point**: Creates providers during startup
|
|
- **Route layer**: Provides route instances to registry
|
|
- **Watcher system**: Receives container/config change events
|
|
|
|
### Non-goals
|
|
|
|
- Does not implement route execution (handled by route package)
|
|
- Does not manage global configuration
|
|
- Does not provide authentication
|
|
|
|
### Stability
|
|
|
|
Internal package with stable provider interface.
|
|
|
|
## Public API
|
|
|
|
### Exported Types
|
|
|
|
```go
|
|
type Provider struct {
|
|
ProviderImpl
|
|
t provider.Type
|
|
routes route.Routes
|
|
routesMu sync.RWMutex
|
|
watcher W.Watcher
|
|
}
|
|
|
|
type ProviderImpl interface {
|
|
fmt.Stringer
|
|
ShortName() string
|
|
IsExplicitOnly() bool
|
|
loadRoutesImpl() (route.Routes, error)
|
|
NewWatcher() W.Watcher
|
|
Logger() *zerolog.Logger
|
|
}
|
|
```
|
|
|
|
### Exported Functions
|
|
|
|
```go
|
|
// Create a file-based provider
|
|
func NewFileProvider(filename string) (p *Provider, err error)
|
|
|
|
// Create a Docker-based provider
|
|
func NewDockerProvider(name string, dockerCfg types.DockerProviderConfig) *Provider
|
|
|
|
// Create an agent-based provider
|
|
func NewAgentProvider(cfg *agent.AgentConfig) *Provider
|
|
```
|
|
|
|
### Provider Methods
|
|
|
|
```go
|
|
func (p *Provider) GetType() provider.Type
|
|
func (p *Provider) Start(parent task.Parent) error
|
|
func (p *Provider) LoadRoutes() error
|
|
func (p *Provider) IterRoutes(yield func(string, types.Route) bool)
|
|
func (p *Provider) GetRoute(alias string) (types.Route, bool)
|
|
func (p *Provider) FindService(project, service string) (types.Route, bool)
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### Core Components
|
|
|
|
```mermaid
|
|
classDiagram
|
|
class Provider {
|
|
+ProviderImpl
|
|
+t provider.Type
|
|
+routes route.Routes
|
|
+watcher W.Watcher
|
|
+Start(parent) error
|
|
+LoadRoutes() error
|
|
+IterRoutes(yield)
|
|
}
|
|
|
|
class ProviderImpl {
|
|
<<interface>>
|
|
+String() string
|
|
+ShortName() string
|
|
+IsExplicitOnly() bool
|
|
+loadRoutesImpl() (route.Routes, error)
|
|
+NewWatcher() W.Watcher
|
|
+Logger() *zerolog.Logger
|
|
}
|
|
|
|
class DockerProviderImpl {
|
|
+name string
|
|
+dockerCfg types.DockerProviderConfig
|
|
+ShortName() string
|
|
+loadRoutesImpl() (route.Routes, error)
|
|
}
|
|
|
|
class FileProviderImpl {
|
|
+filename string
|
|
+ShortName() string
|
|
+loadRoutesImpl() (route.Routes, error)
|
|
}
|
|
|
|
class AgentProviderImpl {
|
|
+*agent.AgentConfig
|
|
+docker DockerProviderImpl
|
|
+ShortName() string
|
|
+loadRoutesImpl() (route.Routes, error)
|
|
}
|
|
|
|
Provider --> ProviderImpl : wraps
|
|
ProviderImpl <|-- DockerProviderImpl
|
|
ProviderImpl <|-- FileProviderImpl
|
|
ProviderImpl <|-- AgentProviderImpl
|
|
```
|
|
|
|
### Provider Types
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[Provider] --> B{Docker}
|
|
A --> C{File}
|
|
A --> D{Agent}
|
|
|
|
B --> E[DockerWatcher]
|
|
C --> F[ConfigFileWatcher]
|
|
D --> G[DockerWatcher]
|
|
|
|
E --> H[Container Labels]
|
|
F --> I[YAML Files]
|
|
G --> J[Remote Agent]
|
|
```
|
|
|
|
### Route Loading Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Main as Application
|
|
participant Provider as Provider
|
|
participant Impl as ProviderImpl
|
|
participant Watcher as Watcher
|
|
participant Routes as Route Registry
|
|
|
|
Main->>Provider: Start()
|
|
Provider->>Provider: LoadRoutes()
|
|
Provider->>Impl: loadRoutesImpl()
|
|
Impl-->>Provider: Routes
|
|
Provider->>Routes: Validate & Add Routes
|
|
|
|
loop Event Loop
|
|
Watcher->>Provider: Container/Config Changed
|
|
Provider->>Provider: Reload Routes
|
|
Provider->>Impl: loadRoutesImpl()
|
|
Provider->>Routes: Update Registry
|
|
end
|
|
```
|
|
|
|
### Docker Provider Features
|
|
|
|
- Lists all containers and reads their labels
|
|
- Supports Docker Compose project/service discovery
|
|
- Handles host network mode with port detection
|
|
- Supports alias references (`#1`, `#2` for container aliases)
|
|
- Explicit-only mode for providers ending with `!`
|
|
|
|
### File Provider Features
|
|
|
|
- Reads YAML files from the config directory
|
|
- Validates routes on load
|
|
- Supports embedded presets
|
|
|
|
### Agent Provider Features
|
|
|
|
- Connects to a remote agent via Unix socket or TCP
|
|
- Delegates to a Docker provider internally
|
|
- Supports the same Docker label-based route discovery
|
|
|
|
## Configuration Surface
|
|
|
|
### Docker Provider Labels
|
|
|
|
```yaml
|
|
labels:
|
|
proxy.aliases: app1,app2
|
|
proxy.app1.listen: http://0.0.0.0:8080
|
|
proxy.app1.target: http://app:3000
|
|
proxy.app1.rules: |
|
|
- name: default
|
|
do: pass
|
|
```
|
|
|
|
### File Provider Configuration
|
|
|
|
```yaml
|
|
# config/routes/myapp.yml
|
|
routes:
|
|
myapp:
|
|
target: http://localhost:8080
|
|
rules:
|
|
- name: default
|
|
do: pass
|
|
```
|
|
|
|
### Agent Provider Configuration
|
|
|
|
```yaml
|
|
agents:
|
|
remote:
|
|
socket: /run/godoxy-agent.sock
|
|
name: remote-agent
|
|
```
|
|
|
|
## Dependency and Integration Map
|
|
|
|
| Dependency | Purpose |
|
|
| -------------------------------- | -------------------------- |
|
|
| `internal/route` | Route types and validation |
|
|
| `internal/route/routes` | Route registry |
|
|
| `internal/docker` | Docker API integration |
|
|
| `internal/serialization` | YAML parsing |
|
|
| `internal/watcher` | Container/config watching |
|
|
| `internal/watcher/events` | Event queue handling |
|
|
| `agent/pkg/agent` | Agent configuration |
|
|
| `github.com/yusing/goutils/errs` | Error handling |
|
|
|
|
## Observability
|
|
|
|
### Logs
|
|
|
|
- **INFO**: Provider start/stop, route count
|
|
- **DEBUG**: Route loading details, label parsing
|
|
- **ERROR**: Container fetch errors, parse failures
|
|
|
|
Log context includes: `provider`, `alias`, `route_count`
|
|
|
|
### Metrics
|
|
|
|
- `routes_loaded_total` by provider
|
|
- `provider_events_total` by type
|
|
|
|
## Security Considerations
|
|
|
|
- Docker provider requires socket access
|
|
- Agent provider uses Unix socket or TCP with auth
|
|
- Route validation prevents SSRF via URL validation
|
|
- Container labels are validated before use
|
|
|
|
## Failure Modes and Recovery
|
|
|
|
| Failure | Behavior | Recovery |
|
|
| ------------------------- | ---------------------------- | ----------------------- |
|
|
| Docker socket unavailable | Provider fails to start | Fix socket permissions |
|
|
| Container not found | Route excluded with error | Verify container exists |
|
|
| YAML parse error | Route excluded, error logged | Fix configuration file |
|
|
| Agent connection lost | Routes removed, reconnection | Fix agent connectivity |
|
|
| Watcher error | Provider finishes with error | Check watcher logs |
|
|
|
|
## Usage Examples
|
|
|
|
### Creating a Docker Provider
|
|
|
|
```go
|
|
provider := provider.NewDockerProvider("default", types.DockerProviderConfig{
|
|
URL: "unix:///var/run/docker.sock",
|
|
})
|
|
|
|
if err := provider.LoadRoutes(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := provider.Start(parentTask); err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
### Creating a File Provider
|
|
|
|
```go
|
|
provider, err := provider.NewFileProvider("routes/myapp.yml")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := provider.Start(parentTask); err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
### Iterating Over Routes
|
|
|
|
```go
|
|
for alias, r := range provider.IterRoutes {
|
|
log.Printf("Route: %s -> %s", alias, r.Name())
|
|
}
|
|
```
|
|
|
|
### Finding a Service
|
|
|
|
```go
|
|
route, ok := provider.FindService("myproject", "myservice")
|
|
if ok {
|
|
log.Printf("Found service: %s", route.Name())
|
|
}
|
|
```
|
|
|
|
## Testing Notes
|
|
|
|
- Docker provider tests use test containers
|
|
- File provider tests use temp directories
|
|
- Agent provider tests use mock agents
|
|
- Integration tests cover event handling
|