refactor(api): restructured API for type safety, maintainability and docs generation

- These changes makes the API incombatible with previous versions
- Added new types for error handling, success responses, and health checks.
- Updated health check logic to utilize the new types for better clarity and structure.
- Refactored existing handlers to improve response consistency and error handling.
- Updated Makefile to include a new target for generating API types from Swagger.
- Updated "new agent" API to respond an encrypted cert pair
This commit is contained in:
yusing
2025-08-16 13:04:05 +08:00
parent fce9ce21c9
commit 35a3e3fef6
149 changed files with 13173 additions and 2173 deletions

View File

@@ -1,11 +1,12 @@
package idlewatcher
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/yusing/go-proxy/internal/api/v1/favicon"
api "github.com/yusing/go-proxy/internal/api/v1"
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
)
@@ -62,7 +63,14 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
// handle favicon request
if isFaviconPath(r.URL.Path) {
favicon.GetFavIconFromAlias(rw, r, w.route.Name())
result := api.GetFavIconFromAlias(r.Context(), w.route.Name())
if !result.OK() {
rw.WriteHeader(result.StatusCode)
fmt.Fprint(rw, result.ErrMsg)
return false
}
rw.Header().Set("Content-Type", result.ContentType())
rw.WriteHeader(result.StatusCode)
return false
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/yusing/go-proxy/internal/gperr"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/types"
)
// Start implements health.HealthMonitor.
@@ -50,18 +50,18 @@ func (w *Watcher) Latency() time.Duration {
}
// Status implements health.HealthMonitor.
func (w *Watcher) Status() health.Status {
func (w *Watcher) Status() types.HealthStatus {
state := w.state.Load()
if state.err != nil {
return health.StatusError
return types.StatusError
}
if state.ready {
return health.StatusHealthy
return types.StatusHealthy
}
if state.status == idlewatcher.ContainerStatusRunning {
return health.StatusStarting
return types.StatusStarting
}
return health.StatusNapping
return types.StatusNapping
}
// Detail implements health.HealthMonitor.
@@ -89,7 +89,7 @@ func (w *Watcher) MarshalJSON() ([]byte, error) {
if err := w.error(); err != nil {
detail = err.Error()
}
return (&health.JSONRepresentation{
return (&types.HealthJSONRepr{
Name: w.Name(),
Status: w.Status(),
Config: dummyHealthCheckConfig,

View File

@@ -7,6 +7,7 @@ import (
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/gperr"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/watcher"
)
@@ -42,14 +43,14 @@ func (p *DockerProvider) ContainerStart(ctx context.Context) error {
return p.client.ContainerStart(ctx, p.containerID, startOptions)
}
func (p *DockerProvider) ContainerStop(ctx context.Context, signal idlewatcher.Signal, timeout int) error {
func (p *DockerProvider) ContainerStop(ctx context.Context, signal types.ContainerSignal, timeout int) error {
return p.client.ContainerStop(ctx, p.containerID, container.StopOptions{
Signal: string(signal),
Timeout: &timeout,
})
}
func (p *DockerProvider) ContainerKill(ctx context.Context, signal idlewatcher.Signal) error {
func (p *DockerProvider) ContainerKill(ctx context.Context, signal types.ContainerSignal) error {
return p.client.ContainerKill(ctx, p.containerID, string(signal))
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/yusing/go-proxy/internal/gperr"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
"github.com/yusing/go-proxy/internal/proxmox"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/watcher"
"github.com/yusing/go-proxy/internal/watcher/events"
)
@@ -52,11 +53,11 @@ func (p *ProxmoxProvider) ContainerStart(ctx context.Context) error {
return p.LXCAction(ctx, p.vmid, proxmox.LXCStart)
}
func (p *ProxmoxProvider) ContainerStop(ctx context.Context, _ idlewatcher.Signal, _ int) error {
func (p *ProxmoxProvider) ContainerStop(ctx context.Context, _ types.ContainerSignal, _ int) error {
return p.LXCAction(ctx, p.vmid, proxmox.LXCShutdown)
}
func (p *ProxmoxProvider) ContainerKill(ctx context.Context, _ idlewatcher.Signal) error {
func (p *ProxmoxProvider) ContainerKill(ctx context.Context, _ types.ContainerSignal) error {
return p.LXCAction(ctx, p.vmid, proxmox.LXCShutdown)
}

View File

@@ -1,138 +1 @@
package idlewatcher
import (
"net/url"
"strconv"
"strings"
"time"
"github.com/yusing/go-proxy/internal/gperr"
)
type (
ProviderConfig struct {
Proxmox *ProxmoxConfig `json:"proxmox,omitempty"`
Docker *DockerConfig `json:"docker,omitempty"`
}
IdlewatcherConfig struct {
// 0: no idle watcher.
// Positive: idle watcher with idle timeout.
// Negative: idle watcher as a dependency. IdleTimeout time.Duration `json:"idle_timeout" json_ext:"duration"`
IdleTimeout time.Duration `json:"idle_timeout"`
WakeTimeout time.Duration `json:"wake_timeout"`
StopTimeout time.Duration `json:"stop_timeout"`
StopMethod StopMethod `json:"stop_method"`
StopSignal Signal `json:"stop_signal,omitempty"`
}
Config struct {
ProviderConfig
IdlewatcherConfig
StartEndpoint string `json:"start_endpoint,omitempty"` // Optional path that must be hit to start container
DependsOn []string `json:"depends_on,omitempty"`
}
StopMethod string
Signal string
DockerConfig struct {
DockerHost string `json:"docker_host" validate:"required"`
ContainerID string `json:"container_id" validate:"required"`
ContainerName string `json:"container_name" validate:"required"`
}
ProxmoxConfig struct {
Node string `json:"node" validate:"required"`
VMID int `json:"vmid" validate:"required"`
}
)
const (
WakeTimeoutDefault = 30 * time.Second
StopTimeoutDefault = 1 * time.Minute
StopMethodPause StopMethod = "pause"
StopMethodStop StopMethod = "stop"
StopMethodKill StopMethod = "kill"
)
func (c *Config) Key() string {
if c.Docker != nil {
return c.Docker.ContainerID
}
return c.Proxmox.Node + ":" + strconv.Itoa(c.Proxmox.VMID)
}
func (c *Config) ContainerName() string {
if c.Docker != nil {
return c.Docker.ContainerName
}
return "lxc-" + strconv.Itoa(c.Proxmox.VMID)
}
func (c *Config) Validate() gperr.Error {
if c.IdleTimeout == 0 { // zero idle timeout means no idle watcher
return nil
}
errs := gperr.NewBuilder("idlewatcher config validation error")
errs.AddRange(
c.validateProvider(),
c.validateTimeouts(),
c.validateStopMethod(),
c.validateStopSignal(),
c.validateStartEndpoint(),
)
return errs.Error()
}
func (c *Config) validateProvider() error {
if c.Docker == nil && c.Proxmox == nil {
return gperr.New("missing idlewatcher provider config")
}
return nil
}
func (c *Config) validateTimeouts() error { //nolint:unparam
if c.WakeTimeout == 0 {
c.WakeTimeout = WakeTimeoutDefault
}
if c.StopTimeout == 0 {
c.StopTimeout = StopTimeoutDefault
}
return nil
}
func (c *Config) validateStopMethod() error {
switch c.StopMethod {
case "":
c.StopMethod = StopMethodStop
return nil
case StopMethodPause, StopMethodStop, StopMethodKill:
return nil
default:
return gperr.New("invalid stop method").Subject(string(c.StopMethod))
}
}
func (c *Config) validateStopSignal() error {
switch c.StopSignal {
case "", "SIGINT", "SIGTERM", "SIGQUIT", "SIGHUP", "INT", "TERM", "QUIT", "HUP":
return nil
default:
return gperr.New("invalid stop signal").Subject(string(c.StopSignal))
}
}
func (c *Config) validateStartEndpoint() error {
if c.StartEndpoint == "" {
return nil
}
// checks needed as of Go 1.6 because of change https://github.com/golang/go/commit/617c93ce740c3c3cc28cdd1a0d712be183d0b328#diff-6c2d018290e298803c0c9419d8739885L195
// emulate browser and strip the '#' suffix prior to validation. see issue-#237
if i := strings.Index(c.StartEndpoint, "#"); i > -1 {
c.StartEndpoint = c.StartEndpoint[:i]
}
if len(c.StartEndpoint) == 0 {
return gperr.New("start endpoint must not be empty if defined")
}
_, err := url.ParseRequestURI(c.StartEndpoint)
return err
}

View File

@@ -1,49 +0,0 @@
package idlewatcher
import (
"testing"
expect "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestValidateStartEndpoint(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "valid",
input: "/start",
wantErr: false,
},
{
name: "invalid",
input: "../foo",
wantErr: true,
},
{
name: "single fragment",
input: "#",
wantErr: true,
},
{
name: "empty",
input: "",
wantErr: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cfg := new(Config)
cfg.StartEndpoint = tc.input
err := cfg.validateStartEndpoint()
if err == nil {
expect.Equal(t, cfg.StartEndpoint, tc.input)
}
if (err != nil) != tc.wantErr {
t.Errorf("validateStartEndpoint() error = %v, wantErr %t", err, tc.wantErr)
}
})
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/watcher/events"
)
@@ -11,8 +12,8 @@ type Provider interface {
ContainerPause(ctx context.Context) error
ContainerUnpause(ctx context.Context) error
ContainerStart(ctx context.Context) error
ContainerStop(ctx context.Context, signal Signal, timeout int) error
ContainerKill(ctx context.Context, signal Signal) error
ContainerStop(ctx context.Context, signal types.ContainerSignal, timeout int) error
ContainerKill(ctx context.Context, signal types.ContainerSignal) error
ContainerStatus(ctx context.Context) (ContainerStatus, error)
Watch(ctx context.Context) (eventCh <-chan events.Event, errCh <-chan gperr.Error)
Close()

View File

@@ -4,11 +4,11 @@ import (
"net/http"
nettypes "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/types"
)
type Waker interface {
health.HealthMonitor
types.HealthMonitor
http.Handler
nettypes.Stream
Wake() error

View File

@@ -10,6 +10,7 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/idlewatcher/provider"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
@@ -17,10 +18,10 @@ import (
nettypes "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/route/routes"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/types"
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/atomic"
"github.com/yusing/go-proxy/internal/watcher/events"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/singleflight"
@@ -28,10 +29,10 @@ import (
type (
routeHelper struct {
route routes.Route
route types.Route
rp *reverseproxy.ReverseProxy
stream nettypes.Stream
hc health.HealthChecker
hc types.HealthChecker
}
containerState struct {
@@ -46,7 +47,7 @@ type (
l zerolog.Logger
cfg *idlewatcher.Config
cfg *types.IdlewatcherConfig
provider idlewatcher.Provider
@@ -80,7 +81,7 @@ const (
idleWakerCheckTimeout = time.Second
)
var dummyHealthCheckConfig = &health.HealthCheckConfig{
var dummyHealthCheckConfig = &types.HealthCheckConfig{
Interval: idleWakerCheckInterval,
Timeout: idleWakerCheckTimeout,
}
@@ -96,7 +97,7 @@ const reqTimeout = 3 * time.Second
const neverTick = time.Duration(1<<63 - 1)
// TODO: fix stream type.
func NewWatcher(parent task.Parent, r routes.Route, cfg *idlewatcher.Config) (*Watcher, error) {
func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig) (*Watcher, error) {
key := cfg.Key()
watcherMapMu.RLock()
@@ -109,7 +110,7 @@ func NewWatcher(parent task.Parent, r routes.Route, cfg *idlewatcher.Config) (*W
w.cfg.DependsOn = cfg.DependsOn
}
if cfg.IdleTimeout > 0 {
w.cfg.IdlewatcherConfig = cfg.IdlewatcherConfig
w.cfg.IdlewatcherConfigBase = cfg.IdlewatcherConfigBase
}
cfg = w.cfg
w.resetIdleTimer()
@@ -147,12 +148,12 @@ func NewWatcher(parent task.Parent, r routes.Route, cfg *idlewatcher.Config) (*W
cont := r.ContainerInfo()
var depRoute routes.Route
var depRoute types.Route
var ok bool
// try to find the dependency in the same provider and the same docker compose project first
if cont != nil {
depRoute, ok = r.GetProvider().FindService(cont.DockerComposeProject(), dep)
depRoute, ok = r.GetProvider().FindService(docker.DockerComposeProject(cont), dep)
}
if !ok {
@@ -178,8 +179,8 @@ func NewWatcher(parent task.Parent, r routes.Route, cfg *idlewatcher.Config) (*W
depCfg := depRoute.IdlewatcherConfig()
if depCfg == nil {
depCfg = new(idlewatcher.Config)
depCfg.IdlewatcherConfig = cfg.IdlewatcherConfig
depCfg = new(types.IdlewatcherConfig)
depCfg.IdlewatcherConfigBase = cfg.IdlewatcherConfigBase
depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies
} else if depCfg.IdleTimeout > 0 {
depErrors.Addf("dependency %q has positive idle timeout %s", dep, depCfg.IdleTimeout)
@@ -189,12 +190,12 @@ func NewWatcher(parent task.Parent, r routes.Route, cfg *idlewatcher.Config) (*W
if depCfg.Docker == nil && depCfg.Proxmox == nil {
depCont := depRoute.ContainerInfo()
if depCont != nil {
depCfg.Docker = &idlewatcher.DockerConfig{
depCfg.Docker = &types.DockerConfig{
DockerHost: depCont.DockerHost,
ContainerID: depCont.ContainerID,
ContainerName: depCont.ContainerName,
}
depCfg.DependsOn = depCont.Dependencies()
depCfg.DependsOn = docker.Dependencies(depCont)
} else {
depErrors.Addf("dependency %q has no idlewatcher config but is not a docker container", dep)
continue
@@ -258,9 +259,9 @@ func NewWatcher(parent task.Parent, r routes.Route, cfg *idlewatcher.Config) (*W
w.provider = p
switch r := r.(type) {
case routes.ReverseProxyRoute:
case types.ReverseProxyRoute:
w.rp = r.ReverseProxy()
case routes.StreamRoute:
case types.StreamRoute:
w.stream = r.Stream()
default:
w.provider.Close()
@@ -443,11 +444,11 @@ func (w *Watcher) stopByMethod() error {
// stop itself first.
var err error
switch cfg.StopMethod {
case idlewatcher.StopMethodPause:
case types.ContainerStopMethodPause:
err = w.provider.ContainerPause(ctx)
case idlewatcher.StopMethodStop:
case types.ContainerStopMethodStop:
err = w.provider.ContainerStop(ctx, cfg.StopSignal, int(cfg.StopTimeout.Seconds()))
case idlewatcher.StopMethodKill:
case types.ContainerStopMethodKill:
err = w.provider.ContainerKill(ctx, cfg.StopSignal)
default:
err = w.newWatcherError(gperr.Errorf("unexpected stop method: %q", cfg.StopMethod))