diff --git a/go.mod b/go.mod index bfe0feea..1662aef7 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/PuerkitoBio/goquery v1.10.2 // parsing HTML for extract fav icon github.com/coder/websocket v1.8.12 // websocket for API and agent github.com/coreos/go-oidc/v3 v3.12.0 // oidc authentication - github.com/docker/cli v27.5.1+incompatible // docker CLI - github.com/docker/docker v27.5.1+incompatible // docker daemon + github.com/docker/cli v28.0.0+incompatible // docker CLI + github.com/docker/docker v28.0.0+incompatible // docker daemon github.com/fsnotify/fsnotify v1.8.0 // file watcher github.com/go-acme/lego/v4 v4.22.2 // acme client github.com/go-playground/validator/v10 v10.25.0 // validator @@ -15,9 +15,10 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 // jwt for default auth github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics - github.com/prometheus/client_golang v1.20.5 // metrics + github.com/prometheus/client_golang v1.21.0 // metrics github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations github.com/rs/zerolog v1.33.0 // logging + github.com/shirou/gopsutil/v4 v4.25.1 // system info metrics github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon golang.org/x/crypto v0.33.0 // encrypting password with bcrypt golang.org/x/net v0.35.0 // HTTP header utilities @@ -25,7 +26,6 @@ require ( golang.org/x/text v0.22.0 // string utilities golang.org/x/time v0.10.0 // time utilities gopkg.in/yaml.v3 v3.0.1 // yaml parsing for different config files - github.com/shirou/gopsutil/v4 v4.25.1 // system info metrics ) require ( @@ -51,7 +51,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/mattn/go-colorable v0.1.14 // indirect diff --git a/go.sum b/go.sum index d160bc5b..a98e45e7 100644 --- a/go.sum +++ b/go.sum @@ -27,10 +27,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY= -github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8= -github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.0.0+incompatible h1:ido37VmLUqEp+5NFb9icd6BuBB+SNDgCn+5kPCr2buA= +github.com/docker/cli v28.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= +github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -87,8 +87,8 @@ github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nu github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -135,8 +135,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= diff --git a/internal/api/handler.go b/internal/api/handler.go index 3766f398..4dff2dca 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -89,7 +89,7 @@ func NewHandler(cfg config.ConfigInstance) http.Handler { mux.HandleFunc("GET", "/v1/metrics/uptime", uptime.Poller.ServeHTTP, true) mux.HandleFunc("GET", "/v1/cert/info", certapi.GetCertInfo, true) mux.HandleFunc("", "/v1/cert/renew", certapi.RenewCert, true) - mux.HandleFunc("GET", "/v1/docker/info", dockerapi.Info, true) + mux.HandleFunc("GET", "/v1/docker/info", dockerapi.DockerInfo, true) mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true) mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true) diff --git a/internal/api/v1/dockerapi/containers.go b/internal/api/v1/dockerapi/containers.go index 6419ce0c..eabac6da 100644 --- a/internal/api/v1/dockerapi/containers.go +++ b/internal/api/v1/dockerapi/containers.go @@ -4,14 +4,9 @@ import ( "context" "net/http" "sort" - "time" - "github.com/coder/websocket" - "github.com/coder/websocket/wsjson" "github.com/docker/docker/api/types/container" "github.com/yusing/go-proxy/internal/gperr" - "github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket" - "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders" ) type Container struct { @@ -23,30 +18,10 @@ type Container struct { } func Containers(w http.ResponseWriter, r *http.Request) { - if httpheaders.IsWebsocket(r.Header) { - gpwebsocket.Periodic(w, r, 5*time.Second, func(conn *websocket.Conn) error { - containers, err := listContainers(r.Context()) - if err != nil { - return err - } - return wsjson.Write(r.Context(), conn, containers) - }) - } else { - containers, err := listContainers(r.Context()) - handleResult(w, err, containers) - } + serveHTTP[Container, []Container](w, r, GetContainers) } -func listContainers(ctx context.Context) ([]Container, error) { - ctx, cancel := context.WithTimeout(ctx, reqTimeout) - defer cancel() - - dockerClients, err := getDockerClients() - if err != nil { - return nil, err - } - defer closeAllClients(dockerClients) - +func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) { errs := gperr.NewBuilder("failed to get containers") containers := make([]Container, 0) for server, dockerClient := range dockerClients { diff --git a/internal/api/v1/dockerapi/info.go b/internal/api/v1/dockerapi/info.go index f3805849..02b00dd3 100644 --- a/internal/api/v1/dockerapi/info.go +++ b/internal/api/v1/dockerapi/info.go @@ -4,43 +4,39 @@ import ( "context" "encoding/json" "net/http" + "sort" dockerSystem "github.com/docker/docker/api/types/system" "github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/utils/strutils" ) -type DockerInfo dockerSystem.Info +type dockerInfo dockerSystem.Info -func (d *DockerInfo) MarshalJSON() ([]byte, error) { +func (d *dockerInfo) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]any{ - "host": d.Name, + "name": d.Name, + "version": d.ServerVersion, "containers": map[string]int{ "total": d.Containers, "running": d.ContainersRunning, "paused": d.ContainersPaused, "stopped": d.ContainersStopped, }, - "images": d.Images, - "n_cpu": d.NCPU, - "memory": strutils.FormatByteSizeWithUnit(d.MemTotal), - "version": d.ServerVersion, + "images": d.Images, + "n_cpu": d.NCPU, + "memory": strutils.FormatByteSizeWithUnit(d.MemTotal), }) } -func Info(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), reqTimeout) - defer cancel() - - dockerClients, ok := getDockerClientsWithErrHandling(w) - if !ok { - return - } - defer closeAllClients(dockerClients) +func DockerInfo(w http.ResponseWriter, r *http.Request) { + serveHTTP[dockerInfo, []dockerInfo](w, r, GetDockerInfo) +} +func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) { errs := gperr.NewBuilder("failed to get docker info") + dockerInfos := make([]dockerInfo, len(dockerClients)) - dockerInfos := make([]DockerInfo, len(dockerClients)) i := 0 for name, dockerClient := range dockerClients { info, err := dockerClient.Info(ctx) @@ -49,9 +45,12 @@ func Info(w http.ResponseWriter, r *http.Request) { continue } info.Name = name - dockerInfos[i] = DockerInfo(info) + dockerInfos[i] = dockerInfo(info) i++ } - handleResult(w, errs.Error(), dockerInfos) + sort.Slice(dockerInfos, func(i, j int) bool { + return dockerInfos[i].Name < dockerInfos[j].Name + }) + return dockerInfos, errs.Error() } diff --git a/internal/api/v1/dockerapi/utils.go b/internal/api/v1/dockerapi/utils.go index 9742b9db..9f2fc880 100644 --- a/internal/api/v1/dockerapi/utils.go +++ b/internal/api/v1/dockerapi/utils.go @@ -1,12 +1,25 @@ package dockerapi import ( + "context" "encoding/json" "net/http" + "time" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" config "github.com/yusing/go-proxy/internal/config/types" "github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/gperr" + "github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket" + "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders" +) + +type ( + DockerClients map[string]*docker.SharedClient + ResultType[T any] interface { + map[string]T | []T + } ) // getDockerClients returns a map of docker clients for the current config. @@ -14,11 +27,11 @@ import ( // Returns a map of docker clients by server name and an error if any. // // Even if there are errors, the map of docker clients might not be empty. -func getDockerClients() (map[string]*docker.SharedClient, gperr.Error) { +func getDockerClients() (DockerClients, gperr.Error) { cfg := config.GetInstance() dockerHosts := cfg.Value().Providers.Docker - dockerClients := make(map[string]*docker.SharedClient) + dockerClients := make(DockerClients) connErrs := gperr.NewBuilder("failed to connect to docker") @@ -43,21 +56,6 @@ func getDockerClients() (map[string]*docker.SharedClient, gperr.Error) { return dockerClients, connErrs.Error() } -// getDockerClientsWithErrHandling returns a map of docker clients for the current config. -// -// Returns a map of docker clients by server name and a boolean indicating if http handler should stop/ -func getDockerClientsWithErrHandling(w http.ResponseWriter) (map[string]*docker.SharedClient, bool) { - dockerClients, err := getDockerClients() - if err != nil { - gperr.LogError("failed to get docker clients", err) - if len(dockerClients) == 0 { - http.Error(w, "no docker hosts connected successfully", http.StatusInternalServerError) - return nil, false - } - } - return dockerClients, true -} - func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient, bool, error) { cfg := config.GetInstance() var host string @@ -86,13 +84,13 @@ func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient // closeAllClients closes all docker clients after a delay. // // This is used to ensure that all docker clients are closed after the http handler returns. -func closeAllClients(dockerClients map[string]*docker.SharedClient) { +func closeAllClients(dockerClients DockerClients) { for _, dockerClient := range dockerClients { dockerClient.Close() } } -func handleResult[T any](w http.ResponseWriter, errs error, result []T) { +func handleResult[V any, T ResultType[V]](w http.ResponseWriter, errs error, result T) { if errs != nil { gperr.LogError("docker errors", errs) if len(result) == 0 { @@ -102,3 +100,25 @@ func handleResult[T any](w http.ResponseWriter, errs error, result []T) { } json.NewEncoder(w).Encode(result) } + +func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) { + dockerClients, err := getDockerClients() + if err != nil { + handleResult[V, T](w, err, nil) + return + } + defer closeAllClients(dockerClients) + + if httpheaders.IsWebsocket(r.Header) { + gpwebsocket.Periodic(w, r, 5*time.Second, func(conn *websocket.Conn) error { + result, err := getResult(r.Context(), dockerClients) + if err != nil { + return err + } + return wsjson.Write(r.Context(), conn, result) + }) + } else { + result, err := getResult(r.Context(), dockerClients) + handleResult[V, T](w, err, result) + } +}