Add CertProvisioner that requests TLS certificates from Metacrypt's CA API during deploy. When a service has L7 routes, the agent checks for an existing cert, re-issues if missing or within 30 days of expiry, and writes chain+key to mc-proxy's cert directory before registering routes. - Add MetacryptConfig to agent config (server_url, ca_cert, mount, issuer, token_path) with defaults and env overrides - Add CertProvisioner (internal/agent/certs.go): REST client for Metacrypt IssueCert, atomic file writes, cert expiry checking - Wire into Agent struct and deploy flow (before route registration) - Add hasL7Routes/l7Hostnames helpers in deploy.go - Fix pre-existing lint issues: unreachable code in portalloc.go, gofmt in servicedef.go, gosec suppressions, golangci v2 config - Update vendored mc-proxy to fix protobuf init panic - 10 new tests, make all passes with 0 issues Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
69 lines
1.5 KiB
Go
69 lines
1.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"net"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
portRangeMin = 10000
|
|
portRangeMax = 60000
|
|
maxRetries = 10
|
|
)
|
|
|
|
// PortAllocator manages host port allocation for route-based deployments.
|
|
// It tracks allocated ports within the agent session to avoid double-allocation.
|
|
type PortAllocator struct {
|
|
mu sync.Mutex
|
|
allocated map[int]bool
|
|
}
|
|
|
|
// NewPortAllocator creates a new PortAllocator.
|
|
func NewPortAllocator() *PortAllocator {
|
|
return &PortAllocator{
|
|
allocated: make(map[int]bool),
|
|
}
|
|
}
|
|
|
|
// Allocate picks a free port in range [10000, 60000).
|
|
// It tries random ports, checks availability with net.Listen, and retries up to 10 times.
|
|
func (pa *PortAllocator) Allocate() (int, error) {
|
|
pa.mu.Lock()
|
|
defer pa.mu.Unlock()
|
|
|
|
for range maxRetries {
|
|
port := portRangeMin + rand.IntN(portRangeMax-portRangeMin) //nolint:gosec // port selection, not security
|
|
if pa.allocated[port] {
|
|
continue
|
|
}
|
|
|
|
if !isPortFree(port) {
|
|
continue
|
|
}
|
|
|
|
pa.allocated[port] = true
|
|
return port, nil
|
|
}
|
|
|
|
return 0, fmt.Errorf("failed to allocate port after %d attempts", maxRetries)
|
|
}
|
|
|
|
// Release marks a port as available again.
|
|
func (pa *PortAllocator) Release(port int) {
|
|
pa.mu.Lock()
|
|
defer pa.mu.Unlock()
|
|
delete(pa.allocated, port)
|
|
}
|
|
|
|
// isPortFree checks if a TCP port is available by attempting to listen on it.
|
|
func isPortFree(port int) bool {
|
|
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_ = ln.Close()
|
|
return true
|
|
}
|