feat(agent): add container runtime support and enhance agent configuration

- Introduced ContainerRuntime field in AgentConfig and AgentEnvConfig.
- Added IterAgents and NumAgents functions for agent pool management.
- Updated agent creation and verification endpoints to handle container runtime.
- Enhanced Docker Compose template to support different container runtimes.
- Added runtime endpoint to retrieve agent runtime information.
This commit is contained in:
yusing
2025-09-13 23:44:03 +08:00
parent 4509622dde
commit 2717dc963a
11 changed files with 158 additions and 28 deletions

View File

@@ -1,6 +1,7 @@
package agent package agent
import ( import (
"iter"
"github.com/puzpuzpuz/xsync/v4" "github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
@@ -52,6 +53,14 @@ func ListAgents() []*AgentConfig {
return agents return agents
} }
func IterAgents() iter.Seq2[string, *AgentConfig] {
return agentPool.Range
}
func NumAgents() int {
return agentPool.Size()
}
func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) { func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) {
agent, ok = agentPool.Load(addr) agent, ok = agentPool.Load(addr)
return return

View File

@@ -10,6 +10,16 @@ var (
AGENT_PORT="{{.Port}}" \ AGENT_PORT="{{.Port}}" \
AGENT_CA_CERT="{{.CACert}}" \ AGENT_CA_CERT="{{.CACert}}" \
AGENT_SSL_CERT="{{.SSLCert}}" \ AGENT_SSL_CERT="{{.SSLCert}}" \
{{ if eq .ContainerRuntime "nerdctl" -}}
DOCKER_SOCKET="/var/run/containerd/containerd.sock" \
RUNTIME="nerdctl" \
{{ else if eq .ContainerRuntime "podman" -}}
DOCKER_SOCKET="/var/run/podman/podman.sock" \
RUNTIME="podman" \
{{ else -}}
DOCKER_SOCKET="/var/run/docker.sock" \
RUNTIME="docker" \
{{ end -}}
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"` bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"`
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript)) installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
) )

View File

@@ -20,9 +20,10 @@ import (
) )
type AgentConfig struct { type AgentConfig struct {
Addr string `json:"addr"` Addr string `json:"addr"`
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
Runtime ContainerRuntime `json:"runtime"`
httpClient *http.Client httpClient *http.Client
tlsConfig *tls.Config tlsConfig *tls.Config
@@ -32,6 +33,7 @@ type AgentConfig struct {
const ( const (
EndpointVersion = "/version" EndpointVersion = "/version"
EndpointName = "/name" EndpointName = "/name"
EndpointRuntime = "/runtime"
EndpointProxyHTTP = "/proxy/http" EndpointProxyHTTP = "/proxy/http"
EndpointHealth = "/health" EndpointHealth = "/health"
EndpointLogs = "/logs" EndpointLogs = "/logs"
@@ -122,6 +124,30 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
return err return err
} }
// check agent runtime
runtimeBytes, status, err := cfg.Fetch(ctx, EndpointRuntime)
if err != nil {
return err
}
switch status {
case http.StatusOK:
switch string(runtimeBytes) {
case "docker":
cfg.Runtime = ContainerRuntimeDocker
// case "nerdctl":
// cfg.Runtime = ContainerRuntimeNerdctl
case "podman":
cfg.Runtime = ContainerRuntimePodman
default:
return fmt.Errorf("invalid agent runtime: %s", runtimeBytes)
}
case http.StatusNotFound:
// backward compatibility, old agent does not have runtime endpoint
cfg.Runtime = ContainerRuntimeDocker
default:
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtimeBytes)
}
cfg.Version = string(agentVersionBytes) cfg.Version = string(agentVersionBytes)
agentVersion := pkg.ParseVersion(cfg.Version) agentVersion := pkg.ParseVersion(cfg.Version)

View File

@@ -8,9 +8,9 @@ import (
) )
var ( var (
//go:embed templates/agent.compose.yml //go:embed templates/agent.compose.yml.tmpl
agentComposeYAML string agentComposeYAML string
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml").Parse(agentComposeYAML)) agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml.tmpl").Parse(agentComposeYAML))
) )
const ( const (
@@ -20,7 +20,8 @@ const (
func (c *AgentComposeConfig) Generate() (string, error) { func (c *AgentComposeConfig) Generate() (string, error) {
buf := bytes.NewBuffer(make([]byte, 0, 1024)) buf := bytes.NewBuffer(make([]byte, 0, 1024))
if err := agentComposeYAMLTemplate.Execute(buf, c); err != nil { err := agentComposeYAMLTemplate.Execute(buf, c)
if err != nil {
return "", err return "", err
} }
return buf.String(), nil return buf.String(), nil

View File

@@ -1,11 +1,13 @@
package agent package agent
type ( type (
AgentEnvConfig struct { ContainerRuntime string
Name string AgentEnvConfig struct {
Port int Name string
CACert string Port int
SSLCert string CACert string
SSLCert string
ContainerRuntime ContainerRuntime
} }
AgentComposeConfig struct { AgentComposeConfig struct {
Image string Image string
@@ -15,3 +17,9 @@ type (
Generate() (string, error) Generate() (string, error)
} }
) )
const (
ContainerRuntimeDocker ContainerRuntime = "docker"
ContainerRuntimePodman ContainerRuntime = "podman"
// ContainerRuntimeNerdctl ContainerRuntime = "nerdctl"
)

View File

@@ -0,0 +1,66 @@
services:
agent:
image: "{{.Image}}"
container_name: godoxy-agent
restart: always
{{ if eq .ContainerRuntime "podman" -}}
ports:
- "{{.Port}}:{{.Port}}"
{{ else -}}
network_mode: host # do not change this
{{ end -}}
environment:
{{ if eq .ContainerRuntime "nerdctl" -}}
DOCKER_SOCKET: "/var/run/containerd/containerd.sock"
RUNTIME: "nerdctl"
{{ else if eq .ContainerRuntime "podman" -}}
DOCKER_SOCKET: "/var/run/podman/podman.sock"
RUNTIME: "podman"
{{ else -}}
DOCKER_SOCKET: "/var/run/docker.sock"
RUNTIME: "docker"
{{ end -}}
AGENT_NAME: "{{.Name}}"
AGENT_PORT: "{{.Port}}"
AGENT_CA_CERT: "{{.CACert}}"
AGENT_SSL_CERT: "{{.SSLCert}}"
# use agent as a docker socket proxy: [host]:port
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
LISTEN_ADDR:
POST: false
ALLOW_RESTARTS: false
ALLOW_START: false
ALLOW_STOP: false
AUTH: false
BUILD: false
COMMIT: false
CONFIGS: false
CONTAINERS: false
DISTRIBUTION: false
EVENTS: true
EXEC: false
GRPC: false
IMAGES: false
INFO: false
NETWORKS: false
NODES: false
PING: true
PLUGINS: false
SECRETS: false
SERVICES: false
SESSION: false
SWARM: false
SYSTEM: false
TASKS: false
VERSION: true
VOLUMES: false
volumes:
{{ if eq .ContainerRuntime "podman" -}}
- /var/run/podman/podman.sock:/var/run/podman/podman.sock
{{ else if eq .ContainerRuntime "nerdctl" -}}
- /var/run/containerd/containerd.sock:/var/run/containerd/containerd.sock
- /var/lib/nerdctl:/var/lib/nerdctl:ro # required to read metadata like network info
{{ else -}}
- /var/run/docker.sock:/var/run/docker.sock
{{ end -}}
- ./data:/app/data

View File

@@ -50,6 +50,9 @@ func NewAgentHandler() http.Handler {
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) { mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.AgentName) fmt.Fprint(w, env.AgentName)
}) })
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.Runtime)
})
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth) mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP) mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket)) mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))

View File

@@ -13,11 +13,12 @@ import (
) )
type NewAgentRequest struct { type NewAgentRequest struct {
Name string `form:"name" validate:"required"` Name string `json:"name" binding:"required"`
Host string `form:"host" validate:"required"` Host string `json:"host" binding:"required"`
Port int `form:"port" validate:"required,min=1,max=65535"` Port int `json:"port" binding:"required,min=1,max=65535"`
Type string `form:"type" validate:"required,oneof=docker system"` Type string `json:"type" binding:"required,oneof=docker system"`
Nightly bool `form:"nightly" validate:"omitempty"` Nightly bool `json:"nightly" binding:"omitempty"`
ContainerRuntime agent.ContainerRuntime `json:"container_runtime" binding:"omitempty,oneof=docker podman" default:"docker"`
} // @name NewAgentRequest } // @name NewAgentRequest
type NewAgentResponse struct { type NewAgentResponse struct {
@@ -47,6 +48,7 @@ func Create(c *gin.Context) {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return return
} }
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port)) hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
if _, ok := agent.GetAgent(hostport); ok { if _, ok := agent.GetAgent(hostport); ok {
c.JSON(http.StatusConflict, apitypes.Error("agent already exists")) c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
@@ -67,10 +69,11 @@ func Create(c *gin.Context) {
} }
var cfg agent.Generator = &agent.AgentEnvConfig{ var cfg agent.Generator = &agent.AgentEnvConfig{
Name: request.Name, Name: request.Name,
Port: request.Port, Port: request.Port,
CACert: ca.String(), CACert: ca.String(),
SSLCert: srv.String(), SSLCert: srv.String(),
ContainerRuntime: request.ContainerRuntime,
} }
if request.Type == "docker" { if request.Type == "docker" {
cfg = &agent.AgentComposeConfig{ cfg = &agent.AgentComposeConfig{

View File

@@ -6,15 +6,17 @@ import (
"os" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/certs" "github.com/yusing/go-proxy/agent/pkg/certs"
. "github.com/yusing/go-proxy/internal/api/types" . "github.com/yusing/go-proxy/internal/api/types"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
) )
type VerifyNewAgentRequest struct { type VerifyNewAgentRequest struct {
Host string `json:"host"` Host string `json:"host"`
CA PEMPairResponse `json:"ca"` CA PEMPairResponse `json:"ca"`
Client PEMPairResponse `json:"client"` Client PEMPairResponse `json:"client"`
ContainerRuntime agent.ContainerRuntime `json:"container_runtime"`
} // @name VerifyNewAgentRequest } // @name VerifyNewAgentRequest
// @x-id "verify" // @x-id "verify"
@@ -55,7 +57,7 @@ func Verify(c *gin.Context) {
return return
} }
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client) nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, Error("invalid request", err)) c.JSON(http.StatusBadRequest, Error("invalid request", err))
return return

View File

@@ -6,15 +6,17 @@ import (
"github.com/yusing/go-proxy/internal/route/provider" "github.com/yusing/go-proxy/internal/route/provider"
) )
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) { func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
for _, a := range cfg.value.Providers.Agents { for _, a := range cfg.value.Providers.Agents {
if a.Addr == host { if a.Addr == host {
return 0, gperr.New("agent already exists") return 0, gperr.New("agent already exists")
} }
} }
var agentCfg agent.AgentConfig agentCfg := agent.AgentConfig{
agentCfg.Addr = host Addr: host,
Runtime: containerRuntime,
}
err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key) err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key)
if err != nil { if err != nil {
return 0, gperr.Wrap(err, "failed to start agent") return 0, gperr.Wrap(err, "failed to start agent")

View File

@@ -52,7 +52,7 @@ type (
Statistics() map[string]any Statistics() map[string]any
RouteProviderList() []RouteProviderListResponse RouteProviderList() []RouteProviderListResponse
Context() context.Context Context() context.Context
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error)
AutoCertProvider() *autocert.Provider AutoCertProvider() *autocert.Provider
} }
) )