diff --git a/api/docker/client/client.go b/api/docker/client/client.go index 91073829b..17e912cb3 100644 --- a/api/docker/client/client.go +++ b/api/docker/client/client.go @@ -1,15 +1,21 @@ package client import ( + "bytes" "errors" "fmt" + "io" + "maps" "net/http" "strings" "time" - "github.com/docker/docker/client" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/segmentio/encoding/json" ) var errUnsupportedEnvironmentType = errors.New("Environment not supported") @@ -150,8 +156,59 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer. ) } +type NodeNameTransport struct { + *http.Transport + nodeNames map[string]string +} + +func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := t.Transport.RoundTrip(req) + if err != nil || + resp.StatusCode != http.StatusOK || + resp.ContentLength == 0 || + !strings.HasSuffix(req.URL.Path, "/images/json") { + return resp, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + resp.Body.Close() + return resp, err + } + + resp.Body.Close() + + resp.Body = io.NopCloser(bytes.NewReader(body)) + + var rs []struct { + types.ImageSummary + Portainer struct { + Agent struct { + NodeName string + } + } + } + + if err = json.Unmarshal(body, &rs); err != nil { + return resp, nil + } + + t.nodeNames = make(map[string]string) + for _, r := range rs { + t.nodeNames[r.ID] = r.Portainer.Agent.NodeName + } + + return resp, err +} + +func (t *NodeNameTransport) NodeNames() map[string]string { + return maps.Clone(t.nodeNames) +} + func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) { - transport := &http.Transport{} + transport := &NodeNameTransport{ + Transport: &http.Transport{}, + } if endpoint.TLSConfig.TLS { tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) diff --git a/api/http/handler/docker/images/images_list.go b/api/http/handler/docker/images/images_list.go index a61372422..9067aebec 100644 --- a/api/http/handler/docker/images/images_list.go +++ b/api/http/handler/docker/images/images_list.go @@ -4,12 +4,14 @@ import ( "net/http" "strings" - "github.com/docker/docker/api/types" + "github.com/portainer/portainer/api/docker/client" "github.com/portainer/portainer/api/http/handler/docker/utils" "github.com/portainer/portainer/api/internal/set" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/response" + + "github.com/docker/docker/api/types" ) type ImageResponse struct { @@ -48,6 +50,12 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http return httperror.InternalServerError("Unable to retrieve Docker images", err) } + // Extract the node name from the custom transport + nodeNames := make(map[string]string) + if t, ok := cli.HTTPClient().Transport.(*client.NodeNameTransport); ok { + nodeNames = t.NodeNames() + } + withUsage, err := request.RetrieveBooleanQueryParameter(r, "withUsage", true) if err != nil { return httperror.BadRequest("Invalid query parameter: withUsage", err) @@ -74,11 +82,12 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http } imagesList[i] = ImageResponse{ - Created: image.Created, - ID: image.ID, - Size: image.Size, - Tags: image.RepoTags, - Used: imageUsageSet.Contains(image.ID), + Created: image.Created, + NodeName: nodeNames[image.ID], + ID: image.ID, + Size: image.Size, + Tags: image.RepoTags, + Used: imageUsageSet.Contains(image.ID), } }