Files
godoxy-yusing/internal/docker/README.md

434 lines
12 KiB
Markdown

# Docker Integration
Docker container discovery, connection management, and label-based route configuration.
## Overview
The docker package implements Docker container integration, providing shared client connections, container parsing from Docker API responses, label processing for route configuration, and container filtering capabilities.
### Primary consumers
- `internal/route/provider` - Creates Docker-based route providers
- `internal/idlewatcher` - Container idle detection
- Operators - Configure routes via Docker labels
### Non-goals
- Docker image building or management
- Container lifecycle operations (start/stop)
- Volume management
- Docker Swarm orchestration
### Stability
Stable internal package. Public API consists of client management and container parsing functions.
## Public API
### Exported types
```go
type SharedClient struct {
*client.Client
cfg types.DockerProviderConfig
refCount atomic.Int32
closedOn atomic.Int64
key string
addr string
dial func(ctx context.Context) (net.Conn, error)
unique bool
}
```
```go
type Container struct {
DockerCfg types.DockerProviderConfig
Image Image
ContainerName string
ContainerID string
Labels map[string]string
ActualLabels map[string]string
Mounts []Mount
Network string
PublicPortMapping map[int]PortSummary
PrivatePortMapping map[int]PortSummary
Aliases []string
IsExcluded bool
IsExplicit bool
IsHostNetworkMode bool
Running bool
State string
PublicHostname string
PrivateHostname string
Agent *agentpool.Agent
IdlewatcherConfig *IdlewatcherConfig
}
```
### Exported functions
```go
func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, error)
```
Creates or returns a Docker client. Reuses existing clients for the same URL. Thread-safe.
```go
func Clients() map[string]*SharedClient
```
Returns all currently connected clients. Callers must close returned clients.
```go
func FromDocker(c *container.Summary, dockerCfg types.DockerProviderConfig) *types.Container
```
Converts Docker API container summary to internal container type. Parses labels for route configuration.
```go
func UpdatePorts(ctx context.Context, c *Container) error
```
Refreshes port mappings from container inspect.
```go
func DockerComposeProject(c *Container) string
```
Returns the Docker Compose project name.
```go
func DockerComposeService(c *Container) string
```
Returns the Docker Compose service name.
```go
func Dependencies(c *Container) []string
```
Returns container dependencies from labels.
```go
func IsBlacklisted(c *Container) bool
```
Checks if container should be excluded from routing.
## Architecture
### Core components
```mermaid
graph TD
A[Docker API] --> B[SharedClient Pool]
B --> C{Client Request}
C -->|New Client| D[Create Connection]
C -->|Existing| E[Increment RefCount]
F[Container List] --> G[FromDocker Parser]
G --> H[Container Struct]
H --> I[Route Builder]
J[Container Labels] --> K[Label Parser]
K --> L[Route Config]
subgraph Client Pool
B --> M[clientMap]
N[Cleaner Goroutine]
end
```
### Client lifecycle
```mermaid
stateDiagram-v2
[*] --> New: NewClient() called
New --> Shared: Refcount = 1, stored in pool
Shared --> Shared: Same URL, increment refcount
Shared --> Idle: Close() called, refcount = 0
Idle --> Closed: 10s timeout elapsed
Idle --> Shared: NewClient() for same URL
Closed --> [*]: Client closed
Unique --> [*]: Close() immediately
```
### Container parsing flow
```mermaid
sequenceDiagram
participant Provider
participant SharedClient
participant DockerAPI
participant ContainerParser
participant RouteBuilder
Provider->>SharedClient: NewClient(cfg)
SharedClient->>SharedClient: Check Pool
alt Existing Client
SharedClient->>SharedClient: Increment RefCount
else New Client
SharedClient->>DockerAPI: Connect
DockerAPI-->>SharedClient: Client
end
Provider->>SharedClient: ListContainers()
SharedClient->>DockerAPI: GET /containers/json
DockerAPI-->>SharedClient: Container List
SharedClient-->>Provider: Container List
loop For Each Container
Provider->>ContainerParser: FromDocker()
ContainerParser->>ContainerParser: Parse Labels
ContainerParser->>ContainerParser: Resolve Hostnames
ContainerParser-->>Provider: *Container
end
Provider->>RouteBuilder: Create Routes
RouteBuilder-->>Provider: Routes
```
### Client pool management
The docker package maintains a pool of shared clients:
```go
var (
clientMap = make(map[string]*SharedClient, 10)
clientMapMu sync.RWMutex
)
func initClientCleaner() {
cleaner := task.RootTask("docker_clients_cleaner", true)
go func() {
ticker := time.NewTicker(cleanInterval)
for {
select {
case <-ticker.C:
closeTimedOutClients()
case <-cleaner.Context().Done():
// Cleanup all clients
}
}
}()
}
```
## Configuration Surface
### Docker provider configuration
```yaml
providers:
docker:
local: ${DOCKER_HOST}
remote1:
scheme: tcp
host: docker1.local
port: 2375
remote2:
scheme: tls
host: docker2.local
port: 2375
tls:
ca_file: /path/to/ca.pem
cert_file: /path/to/cert.pem
key_file: /path/to/key.pem
```
### Route configuration labels
Route labels use the format `proxy.<alias>.<field>` where `<alias>` is the route alias (or `*` for wildcard). The base labels apply to all routes.
| Label | Description | Example |
| ---------------------- | ------------------------------- | ------------------------------- |
| `proxy.aliases` | Route aliases (comma-separated) | `proxy.aliases: www,app` |
| `proxy.exclude` | Exclude from routing | `proxy.exclude: true` |
| `proxy.network` | Docker network | `proxy.network: frontend` |
| `proxy.<alias>.host` | Override hostname | `proxy.app.host: 192.168.1.100` |
| `proxy.<alias>.port` | Target port | `proxy.app.port: 8080` |
| `proxy.<alias>.scheme` | HTTP scheme | `proxy.app.scheme: https` |
| `proxy.<alias>.*` | Any route-specific setting | `proxy.app.no_tls_verify: true` |
#### Wildcard alias
Use `proxy.*.<field>` to apply settings to all routes:
```yaml
labels:
proxy.aliases: app1,app2
proxy.*.scheme: https
proxy.app1.port: 3000 # overrides wildcard
```
### Idle watcher labels
| Label | Description | Example |
| ----------------------- | ------------------------------- | ---------------------------------- |
| `proxy.idle_timeout` | Idle timeout duration | `proxy.idle_timeout: 30m` |
| `proxy.wake_timeout` | Max time to wait for wake | `proxy.wake_timeout: 10s` |
| `proxy.stop_method` | Stop method (pause, stop, kill) | `proxy.stop_method: stop` |
| `proxy.stop_signal` | Signal to send (e.g., SIGTERM) | `proxy.stop_signal: SIGTERM` |
| `proxy.stop_timeout` | Stop timeout in seconds | `proxy.stop_timeout: 30` |
| `proxy.depends_on` | Container dependencies | `proxy.depends_on: database` |
| `proxy.start_endpoint` | Optional path restriction | `proxy.start_endpoint: /api/ready` |
| `proxy.no_loading_page` | Skip loading page | `proxy.no_loading_page: true` |
### Docker Compose labels
Those are created by Docker Compose.
| Label | Description |
| ------------------------------- | -------------------- |
| `com.docker.compose.project` | Compose project name |
| `com.docker.compose.service` | Service name |
| `com.docker.compose.depends_on` | Dependencies |
## Dependency and Integration Map
### Internal dependencies
- `internal/agentpool` - Agent-based Docker host connections
- `internal/maxmind` - Container geolocation
- `internal/types` - Container and provider types
- `internal/task/task.go` - Lifetime management
### External dependencies
- `github.com/docker/cli/cli/connhelper` - Connection helpers
- `github.com/moby/moby/client` - Docker API client
- `github.com/docker/go-connections/nat` - Port parsing
### Integration points
```go
// Route provider uses docker for container discovery
client, err := docker.NewClient(cfg)
containers, err := client.ContainerList(ctx, container.ListOptions{})
for _, c := range containers {
container := docker.FromDocker(c, cfg)
// Create routes from container
}
```
## Observability
### Logs
- Client initialization and cleanup
- Connection errors
- Container parsing errors
### Metrics
No metrics are currently exposed.
## Security Considerations
- Docker socket access requires proper permissions
- TLS certificates for remote connections
- Agent-based connections are authenticated via TLS
- Database containers are automatically blacklisted
### Blacklist detection
Containers are automatically blacklisted if they:
- Mount database directories:
- `/var/lib/postgresql/data`
- `/var/lib/mysql`
- `/var/lib/mongodb`
- `/var/lib/mariadb`
- `/var/lib/memcached`
- `/var/lib/rabbitmq`
- Expose database ports:
- 5432 (PostgreSQL)
- 3306 (MySQL/MariaDB)
- 6379 (Redis)
- 11211 (Memcached)
- 27017 (MongoDB)
## Failure Modes and Recovery
| Failure | Behavior | Recovery |
| -------------------------- | ---------------------------- | ------------------------ |
| Docker socket inaccessible | NewClient returns error | Fix socket permissions |
| Remote connection failed | NewClient returns error | Check network/tls config |
| Container inspect failed | UpdatePorts returns error | Container may be stopped |
| Invalid labels | Container created with error | Fix label syntax |
| Agent not found | Panic during client creation | Add agent to pool |
## Performance Characteristics
- Client pooling reduces connection overhead
- Reference counting prevents premature cleanup
- Background cleaner removes idle clients after 10s
- O(n) container parsing where n is container count
## Usage Examples
### Creating a Docker client
```go
dockerCfg := types.DockerProviderConfig{
URL: "unix:///var/run/docker.sock",
}
client, err := docker.NewClient(dockerCfg)
if err != nil {
log.Fatal(err)
}
defer client.Close()
```
### Using unique client
```go
// Create a unique client that won't be shared
client, err := docker.NewClient(cfg, true)
if err != nil {
log.Fatal(err)
}
// Remember to close when done
client.Close()
```
### Getting all clients
```go
clients := docker.Clients()
for host, client := range clients {
log.Printf("Connected to: %s", host)
}
// Use clients...
// Close all clients when done
for _, client := range clients {
client.Close()
}
```
### Parsing containers
```go
containers, err := dockerClient.ContainerList(ctx, container.ListOptions{})
for _, c := range containers {
container := docker.FromDocker(c, dockerCfg)
if container.Errors != nil {
log.Printf("Container %s has errors: %v", container.ContainerName, container.Errors)
continue
}
log.Printf("Container: %s, Aliases: %v", container.ContainerName, container.Aliases)
}
```
### Checking if container is blacklisted
```go
container := docker.FromDocker(c, dockerCfg)
if docker.IsBlacklisted(container) {
log.Printf("Container %s is blacklisted, skipping", container.ContainerName)
continue
}
```