diff --git a/agent/pkg/agent/agent_pool.go b/agent/pkg/agent/agent_pool.go index 58c61eb0..594e1ad0 100644 --- a/agent/pkg/agent/agent_pool.go +++ b/agent/pkg/agent/agent_pool.go @@ -1,6 +1,7 @@ package agent import ( + "iter" "github.com/puzpuzpuz/xsync/v4" "github.com/yusing/go-proxy/internal/common" @@ -52,6 +53,14 @@ func ListAgents() []*AgentConfig { 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) { agent, ok = agentPool.Load(addr) return diff --git a/agent/pkg/agent/bare_metal.go b/agent/pkg/agent/bare_metal.go index 0c87c422..8176ebc8 100644 --- a/agent/pkg/agent/bare_metal.go +++ b/agent/pkg/agent/bare_metal.go @@ -10,6 +10,16 @@ var ( AGENT_PORT="{{.Port}}" \ AGENT_CA_CERT="{{.CACert}}" \ 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)"` installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript)) ) diff --git a/agent/pkg/agent/config.go b/agent/pkg/agent/config.go index fdda6d1f..175925bc 100644 --- a/agent/pkg/agent/config.go +++ b/agent/pkg/agent/config.go @@ -20,9 +20,10 @@ import ( ) type AgentConfig struct { - Addr string `json:"addr"` - Name string `json:"name"` - Version string `json:"version"` + Addr string `json:"addr"` + Name string `json:"name"` + Version string `json:"version"` + Runtime ContainerRuntime `json:"runtime"` httpClient *http.Client tlsConfig *tls.Config @@ -32,6 +33,7 @@ type AgentConfig struct { const ( EndpointVersion = "/version" EndpointName = "/name" + EndpointRuntime = "/runtime" EndpointProxyHTTP = "/proxy/http" EndpointHealth = "/health" EndpointLogs = "/logs" @@ -122,6 +124,30 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) 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) agentVersion := pkg.ParseVersion(cfg.Version) diff --git a/agent/pkg/agent/docker_compose.go b/agent/pkg/agent/docker_compose.go index 63b4669b..2c6a8a33 100644 --- a/agent/pkg/agent/docker_compose.go +++ b/agent/pkg/agent/docker_compose.go @@ -8,9 +8,9 @@ import ( ) var ( - //go:embed templates/agent.compose.yml + //go:embed templates/agent.compose.yml.tmpl agentComposeYAML string - agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml").Parse(agentComposeYAML)) + agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml.tmpl").Parse(agentComposeYAML)) ) const ( @@ -20,7 +20,8 @@ const ( func (c *AgentComposeConfig) Generate() (string, error) { 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 buf.String(), nil diff --git a/agent/pkg/agent/env.go b/agent/pkg/agent/env.go index 68bc2da3..f7683d02 100644 --- a/agent/pkg/agent/env.go +++ b/agent/pkg/agent/env.go @@ -1,11 +1,13 @@ package agent type ( - AgentEnvConfig struct { - Name string - Port int - CACert string - SSLCert string + ContainerRuntime string + AgentEnvConfig struct { + Name string + Port int + CACert string + SSLCert string + ContainerRuntime ContainerRuntime } AgentComposeConfig struct { Image string @@ -15,3 +17,9 @@ type ( Generate() (string, error) } ) + +const ( + ContainerRuntimeDocker ContainerRuntime = "docker" + ContainerRuntimePodman ContainerRuntime = "podman" + // ContainerRuntimeNerdctl ContainerRuntime = "nerdctl" +) diff --git a/agent/pkg/agent/templates/agent.compose.yml.tmpl b/agent/pkg/agent/templates/agent.compose.yml.tmpl new file mode 100644 index 00000000..cc0864c0 --- /dev/null +++ b/agent/pkg/agent/templates/agent.compose.yml.tmpl @@ -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 diff --git a/agent/pkg/handler/handler.go b/agent/pkg/handler/handler.go index 9391597e..f0ec77aa 100644 --- a/agent/pkg/handler/handler.go +++ b/agent/pkg/handler/handler.go @@ -50,6 +50,9 @@ func NewAgentHandler() http.Handler { mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) { 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.EndpointSystemInfo, metricsHandler.ServeHTTP) mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket)) diff --git a/internal/api/v1/agent/create.go b/internal/api/v1/agent/create.go index 354bf484..929b245b 100644 --- a/internal/api/v1/agent/create.go +++ b/internal/api/v1/agent/create.go @@ -13,11 +13,12 @@ import ( ) type NewAgentRequest struct { - Name string `form:"name" validate:"required"` - Host string `form:"host" validate:"required"` - Port int `form:"port" validate:"required,min=1,max=65535"` - Type string `form:"type" validate:"required,oneof=docker system"` - Nightly bool `form:"nightly" validate:"omitempty"` + Name string `json:"name" binding:"required"` + Host string `json:"host" binding:"required"` + Port int `json:"port" binding:"required,min=1,max=65535"` + Type string `json:"type" binding:"required,oneof=docker system"` + Nightly bool `json:"nightly" binding:"omitempty"` + ContainerRuntime agent.ContainerRuntime `json:"container_runtime" binding:"omitempty,oneof=docker podman" default:"docker"` } // @name NewAgentRequest type NewAgentResponse struct { @@ -47,6 +48,7 @@ func Create(c *gin.Context) { c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) return } + hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port)) if _, ok := agent.GetAgent(hostport); ok { c.JSON(http.StatusConflict, apitypes.Error("agent already exists")) @@ -67,10 +69,11 @@ func Create(c *gin.Context) { } var cfg agent.Generator = &agent.AgentEnvConfig{ - Name: request.Name, - Port: request.Port, - CACert: ca.String(), - SSLCert: srv.String(), + Name: request.Name, + Port: request.Port, + CACert: ca.String(), + SSLCert: srv.String(), + ContainerRuntime: request.ContainerRuntime, } if request.Type == "docker" { cfg = &agent.AgentComposeConfig{ diff --git a/internal/api/v1/agent/verify.go b/internal/api/v1/agent/verify.go index 69a59cc6..d455fdc1 100644 --- a/internal/api/v1/agent/verify.go +++ b/internal/api/v1/agent/verify.go @@ -6,15 +6,17 @@ import ( "os" "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/internal/api/types" config "github.com/yusing/go-proxy/internal/config/types" ) type VerifyNewAgentRequest struct { - Host string `json:"host"` - CA PEMPairResponse `json:"ca"` - Client PEMPairResponse `json:"client"` + Host string `json:"host"` + CA PEMPairResponse `json:"ca"` + Client PEMPairResponse `json:"client"` + ContainerRuntime agent.ContainerRuntime `json:"container_runtime"` } // @name VerifyNewAgentRequest // @x-id "verify" @@ -55,7 +57,7 @@ func Verify(c *gin.Context) { return } - nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client) + nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime) if err != nil { c.JSON(http.StatusBadRequest, Error("invalid request", err)) return diff --git a/internal/config/agents.go b/internal/config/agents.go index 8a8229c3..5c714f40 100644 --- a/internal/config/agents.go +++ b/internal/config/agents.go @@ -6,15 +6,17 @@ import ( "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 { if a.Addr == host { return 0, gperr.New("agent already exists") } } - var agentCfg agent.AgentConfig - agentCfg.Addr = host + agentCfg := agent.AgentConfig{ + Addr: host, + Runtime: containerRuntime, + } err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key) if err != nil { return 0, gperr.Wrap(err, "failed to start agent") diff --git a/internal/config/types/config.go b/internal/config/types/config.go index 0cb14e43..9b142799 100644 --- a/internal/config/types/config.go +++ b/internal/config/types/config.go @@ -52,7 +52,7 @@ type ( Statistics() map[string]any RouteProviderList() []RouteProviderListResponse 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 } )