package docker import ( "context" "io" "net/http" "reflect" "strings" "sync" "emperror.dev/errors" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/goccy/go-json" "github.com/pterodactyl/wings/config" ) var ( o sync.Once cli cliSettings fastEnabled bool ) type cliSettings struct { enabled bool proto string host string scheme string version string } func configure(c *client.Client) { o.Do(func() { fastEnabled = config.Get().Docker.UsePerformantInspect r := reflect.ValueOf(c).Elem() cli.proto = r.FieldByName("proto").String() cli.host = r.FieldByName("addr").String() cli.scheme = r.FieldByName("scheme").String() cli.version = r.FieldByName("version").String() }) } // ContainerInspect is a rough equivalent of Docker's client.ContainerInspect() // but re-written to use a more performant JSON decoder. This is important since // a large number of requests to this endpoint are spawned by Wings, and the // standard "encoding/json" shows its performance woes badly even with single // containers running. func (e *Environment) ContainerInspect(ctx context.Context) (types.ContainerJSON, error) { configure(e.client) // Support feature flagging of this functionality so that if something goes // wrong for now it is easy enough for people to switch back to the older method // of fetching stats. if !fastEnabled { return e.client.ContainerInspect(ctx, e.Id) } var st types.ContainerJSON req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/containers/"+e.Id+"/json", nil) if err != nil { return st, errors.WithStack(err) } if cli.proto == "unix" || cli.proto == "npipe" { req.Host = "docker" } req.URL.Host = cli.host req.URL.Scheme = cli.scheme res, err := e.client.HTTPClient().Do(req) if err != nil { if res == nil { return st, errdefs.Unknown(err) } return st, errdefs.FromStatusCode(err, res.StatusCode) } body, err := io.ReadAll(res.Body) if err != nil { return st, errors.Wrap(err, "failed to read response body from Docker") } if err := parseErrorFromResponse(res, body); err != nil { return st, errdefs.FromStatusCode(err, res.StatusCode) } if err := json.Unmarshal(body, &st); err != nil { return st, errors.WithStack(err) } return st, nil } // parseErrorFromResponse is a re-implementation of Docker's // client.checkResponseErr() function. func parseErrorFromResponse(res *http.Response, body []byte) error { if res.StatusCode >= 200 && res.StatusCode < 400 { return nil } var ct string if res.Header != nil { ct = res.Header.Get("Content-Type") } var emsg string if (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) && ct == "application/json" { var errResp types.ErrorResponse if err := json.Unmarshal(body, &errResp); err != nil { return errors.WithStack(err) } emsg = strings.TrimSpace(errResp.Message) } else { emsg = strings.TrimSpace(string(body)) } return errors.Wrap(errors.New(emsg), "Error response from daemon") }