mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 01:08:47 +02:00
feat: proxmox idlewatcher (#88)
* feat: idle sleep for proxmox LXCs * refactor: replace deprecated docker api types * chore(api): remove debug task list endpoint * refactor: move servemux to gphttp/servemux; favicon.go to v1/favicon * refactor: introduce Pool interface, move agent_pool to agent module * refactor: simplify api code * feat: introduce debug api * refactor: remove net.URL and net.CIDR types, improved unmarshal handling * chore: update Makefile for debug build tag, update README * chore: add gperr.Unwrap method * feat: relative time and duration formatting * chore: add ROOT_DIR environment variable, refactor * migration: move homepage override and icon cache to $BASE_DIR/data, add migration code * fix: nil dereference on marshalling service health * fix: wait for route deletion * chore: enhance tasks debuggability * feat: stdout access logger and MultiWriter * fix(agent): remove agent properly on verify error * fix(metrics): disk exclusion logic and added corresponding tests * chore: update schema and prettify, fix package.json and Makefile * fix: I/O buffer not being shrunk before putting back to pool * feat: enhanced error handling module * chore: deps upgrade * feat: better value formatting and handling --------- Co-authored-by: yusing <yusing@6uo.me>
This commit is contained in:
@@ -1,110 +1,128 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
IdleTimeout time.Duration `json:"idle_timeout,omitempty"`
|
||||
WakeTimeout time.Duration `json:"wake_timeout,omitempty"`
|
||||
StopTimeout int `json:"stop_timeout,omitempty"` // docker api takes integer seconds for timeout argument
|
||||
StopMethod StopMethod `json:"stop_method,omitempty"`
|
||||
Proxmox *ProxmoxConfig `json:"proxmox,omitempty"`
|
||||
Docker *DockerConfig `json:"docker,omitempty"`
|
||||
|
||||
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"`
|
||||
StartEndpoint string `json:"start_endpoint,omitempty"` // Optional path that must be hit to start container
|
||||
}
|
||||
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"
|
||||
)
|
||||
|
||||
var validSignals = map[string]struct{}{
|
||||
"": {},
|
||||
"SIGINT": {}, "SIGTERM": {}, "SIGHUP": {}, "SIGQUIT": {},
|
||||
"INT": {}, "TERM": {}, "HUP": {}, "QUIT": {},
|
||||
func (c *Config) Key() string {
|
||||
if c.Docker != nil {
|
||||
return c.Docker.ContainerID
|
||||
}
|
||||
return c.Proxmox.Node + ":" + strconv.Itoa(c.Proxmox.VMID)
|
||||
}
|
||||
|
||||
func ValidateConfig(cont *docker.Container) (*Config, gperr.Error) {
|
||||
if cont == nil || cont.IdleTimeout == "" {
|
||||
return nil, nil
|
||||
func (c *Config) ContainerName() string {
|
||||
if c.Docker != nil {
|
||||
return c.Docker.ContainerName
|
||||
}
|
||||
|
||||
errs := gperr.NewBuilder("invalid idlewatcher config")
|
||||
|
||||
idleTimeout := gperr.Collect(errs, validateDurationPostitive, cont.IdleTimeout)
|
||||
wakeTimeout := gperr.Collect(errs, validateDurationPostitive, cont.WakeTimeout)
|
||||
stopTimeout := gperr.Collect(errs, validateDurationPostitive, cont.StopTimeout)
|
||||
stopMethod := gperr.Collect(errs, validateStopMethod, cont.StopMethod)
|
||||
signal := gperr.Collect(errs, validateSignal, cont.StopSignal)
|
||||
startEndpoint := gperr.Collect(errs, validateStartEndpoint, cont.StartEndpoint)
|
||||
|
||||
if errs.HasError() {
|
||||
return nil, errs.Error()
|
||||
}
|
||||
|
||||
return &Config{
|
||||
IdleTimeout: idleTimeout,
|
||||
WakeTimeout: wakeTimeout,
|
||||
StopTimeout: int(stopTimeout.Seconds()),
|
||||
StopMethod: stopMethod,
|
||||
StopSignal: signal,
|
||||
StartEndpoint: startEndpoint,
|
||||
}, nil
|
||||
return "lxc " + strconv.Itoa(c.Proxmox.VMID)
|
||||
}
|
||||
|
||||
func validateDurationPostitive(value string) (time.Duration, error) {
|
||||
d, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
func (c *Config) Validate() gperr.Error {
|
||||
if c.IdleTimeout == 0 { // no idle timeout means no idle watcher
|
||||
return nil
|
||||
}
|
||||
if d < 0 {
|
||||
return 0, errors.New("duration must be positive")
|
||||
}
|
||||
return d, nil
|
||||
errs := gperr.NewBuilder("idlewatcher config validation error")
|
||||
errs.AddRange(
|
||||
c.validateProvider(),
|
||||
c.validateTimeouts(),
|
||||
c.validateStopMethod(),
|
||||
c.validateStopSignal(),
|
||||
c.validateStartEndpoint(),
|
||||
)
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
func validateSignal(s string) (Signal, error) {
|
||||
if _, ok := validSignals[s]; ok {
|
||||
return Signal(s), nil
|
||||
func (c *Config) validateProvider() error {
|
||||
if c.Docker == nil && c.Proxmox == nil {
|
||||
return gperr.New("missing idlewatcher provider config")
|
||||
}
|
||||
return "", errors.New("invalid signal " + s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateStopMethod(s string) (StopMethod, error) {
|
||||
sm := StopMethod(s)
|
||||
switch sm {
|
||||
func (c *Config) validateTimeouts() error {
|
||||
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 sm, nil
|
||||
return nil
|
||||
default:
|
||||
return "", errors.New("invalid stop method " + s)
|
||||
return gperr.New("invalid stop method").Subject(string(c.StopMethod))
|
||||
}
|
||||
}
|
||||
|
||||
func validateStartEndpoint(s string) (string, error) {
|
||||
if s == "" {
|
||||
return "", nil
|
||||
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(s, "#"); i > -1 {
|
||||
s = s[:i]
|
||||
if i := strings.Index(c.StartEndpoint, "#"); i > -1 {
|
||||
c.StartEndpoint = c.StartEndpoint[:i]
|
||||
}
|
||||
if len(s) == 0 {
|
||||
return "", errors.New("start endpoint must not be empty if defined")
|
||||
if len(c.StartEndpoint) == 0 {
|
||||
return gperr.New("start endpoint must not be empty if defined")
|
||||
}
|
||||
if _, err := url.ParseRequestURI(s); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s, nil
|
||||
_, err := url.ParseRequestURI(c.StartEndpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ func TestValidateStartEndpoint(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, err := validateStartEndpoint(tc.input)
|
||||
cfg := Config{StartEndpoint: tc.input}
|
||||
err := cfg.validateStartEndpoint()
|
||||
if err == nil {
|
||||
ExpectEqual(t, s, tc.input)
|
||||
ExpectEqual(t, cfg.StartEndpoint, tc.input)
|
||||
}
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("validateStartEndpoint() error = %v, wantErr %t", err, tc.wantErr)
|
||||
|
||||
14
internal/idlewatcher/types/container_status.go
Normal file
14
internal/idlewatcher/types/container_status.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package idlewatcher
|
||||
|
||||
import "github.com/yusing/go-proxy/internal/gperr"
|
||||
|
||||
type ContainerStatus string
|
||||
|
||||
const (
|
||||
ContainerStatusError ContainerStatus = "error"
|
||||
ContainerStatusRunning ContainerStatus = "running"
|
||||
ContainerStatusPaused ContainerStatus = "paused"
|
||||
ContainerStatusStopped ContainerStatus = "stopped"
|
||||
)
|
||||
|
||||
var ErrUnexpectedContainerStatus = gperr.New("unexpected container status")
|
||||
19
internal/idlewatcher/types/provider.go
Normal file
19
internal/idlewatcher/types/provider.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
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
|
||||
ContainerStatus(ctx context.Context) (ContainerStatus, error)
|
||||
Watch(ctx context.Context) (eventCh <-chan events.Event, errCh <-chan gperr.Error)
|
||||
Close()
|
||||
}
|
||||
Reference in New Issue
Block a user