mirror of https://github.com/portainer/portainer
feat(docker/images): show used tag correctly [EE-5396] (#10305)
parent
b895e88075
commit
9bf2957ea7
@ -0,0 +1,32 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/docker/client"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
*mux.Router
|
||||||
|
dockerClientFactory *client.ClientFactory
|
||||||
|
bouncer security.BouncerService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||||
|
func NewHandler(routePrefix string, bouncer security.BouncerService, dockerClientFactory *client.ClientFactory) *Handler {
|
||||||
|
h := &Handler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
dockerClientFactory: dockerClientFactory,
|
||||||
|
bouncer: bouncer,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := h.PathPrefix(routePrefix).Subrouter()
|
||||||
|
router.Use(bouncer.AuthenticatedAccess)
|
||||||
|
|
||||||
|
router.Handle("", httperror.LoggerHandler(h.imagesList)).Methods(http.MethodGet)
|
||||||
|
return h
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageResponse struct {
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
NodeName string `json:"nodeName"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
|
||||||
|
// Used is true if the image is used by at least one container
|
||||||
|
// supplied only when withUsage is true
|
||||||
|
Used bool `json:"used"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id dockerImagesList
|
||||||
|
// @summary Fetch images
|
||||||
|
// @description
|
||||||
|
// @description **Access policy**:
|
||||||
|
// @tags docker
|
||||||
|
// @security jwt
|
||||||
|
// @param environmentId path int true "Environment identifier"
|
||||||
|
// @param withUsage query boolean false "Include image usage information"
|
||||||
|
// @produce json
|
||||||
|
// @success 200 {array} ImageResponse "Success"
|
||||||
|
// @failure 400 "Bad request"
|
||||||
|
// @failure 500 "Internal server error"
|
||||||
|
// @router /docker/{environmentId}/images [get]
|
||||||
|
func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli, httpErr := utils.GetClient(r, handler.dockerClientFactory)
|
||||||
|
if httpErr != nil {
|
||||||
|
return httpErr
|
||||||
|
}
|
||||||
|
|
||||||
|
images, err := cli.ImageList(r.Context(), types.ImageListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve Docker images", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
withUsage, err := request.RetrieveBooleanQueryParameter(r, "withUsage", true)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.BadRequest("Invalid query parameter: withUsage", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageUsageSet := set.Set[string]{}
|
||||||
|
if withUsage {
|
||||||
|
containers, err := cli.ContainerList(r.Context(), types.ContainerListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve Docker containers", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, container := range containers {
|
||||||
|
imageUsageSet.Add(container.ImageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesList := make([]ImageResponse, len(images))
|
||||||
|
for i, image := range images {
|
||||||
|
imagesList[i] = ImageResponse{
|
||||||
|
Created: image.Created,
|
||||||
|
ID: image.ID,
|
||||||
|
Size: image.Size,
|
||||||
|
Tags: image.RepoTags,
|
||||||
|
Used: imageUsageSet.Contains(image.ID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, imagesList)
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
prclient "github.com/portainer/portainer/api/docker/client"
|
||||||
|
"github.com/portainer/portainer/api/http/middlewares"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetClient returns a Docker client based on the request context
|
||||||
|
func GetClient(r *http.Request, dockerClientFactory *prclient.ClientFactory) (*dockerclient.Client, *httperror.HandlerError) {
|
||||||
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httperror.NotFound("Unable to find an environment on request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||||
|
|
||||||
|
cli, err := dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httperror.InternalServerError("Unable to connect to the Docker daemon", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli, nil
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
type Status = 'outdated' | 'updated' | 'inprocess' | string;
|
||||||
|
|
||||||
|
export enum ResourceType {
|
||||||
|
CONTAINER,
|
||||||
|
SERVICE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageStatus {
|
||||||
|
Status: Status;
|
||||||
|
Message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResourceID = string;
|
@ -1,11 +0,0 @@
|
|||||||
import { Environment } from '@/react/portainer/environments/types';
|
|
||||||
|
|
||||||
import { createRowContext } from '@@/datatables/RowContext';
|
|
||||||
|
|
||||||
interface RowContextState {
|
|
||||||
environment: Environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
|
|
||||||
|
|
||||||
export { RowProvider, useRowContext };
|
|
@ -1,16 +1,5 @@
|
|||||||
import { createColumnHelper } from '@tanstack/react-table';
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { DockerImage } from '@/react/docker/images/types';
|
import { ImagesListResponse } from '@/react/docker/images/queries/useImages';
|
||||||
|
|
||||||
export const columnHelper = createColumnHelper<
|
export const columnHelper = createColumnHelper<ImagesListResponse>();
|
||||||
DockerImage & { NodeName?: string }
|
|
||||||
>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Docker response from proxy (with added portainer metadata)
|
|
||||||
* images view model
|
|
||||||
* images snapshot
|
|
||||||
* snapshots view model
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { buildUrl as buildDockerUrl } from '@/react/docker/queries/utils/build-url';
|
||||||
|
|
||||||
|
export function buildUrl(environmentId: EnvironmentId) {
|
||||||
|
return buildDockerUrl(environmentId, 'images');
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { queryKeys as proxyQueryKeys } from '../query-keys';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
base: (environmentId: EnvironmentId) =>
|
||||||
|
[proxyQueryKeys.base(environmentId), 'images'] as const,
|
||||||
|
list: (environmentId: EnvironmentId) => queryKeys.base(environmentId),
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { ImageSummary } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { buildUrl } from '../build-url';
|
||||||
|
|
||||||
|
import { queryKeys } from './queryKeys';
|
||||||
|
|
||||||
|
type ImagesListResponse = Array<ImageSummary>;
|
||||||
|
|
||||||
|
export function useImages<T = ImagesListResponse>(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
{
|
||||||
|
select,
|
||||||
|
enabled,
|
||||||
|
}: { select?(data: ImagesListResponse): T; enabled?: boolean } = {}
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
queryKeys.list(environmentId),
|
||||||
|
() => getImages(environmentId),
|
||||||
|
{ select, enabled }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getImages(environmentId: EnvironmentId) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<ImagesListResponse>(
|
||||||
|
buildUrl(environmentId, 'images', 'json')
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error, 'Unable to retrieve images');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
export function buildUrl(environmentId: EnvironmentId, path: string) {
|
||||||
|
return `/docker/${environmentId}/${path}`;
|
||||||
|
}
|
Loading…
Reference in new issue