diff --git a/client/go.mod b/client/go.mod index 1778185..5be2884 100644 --- a/client/go.mod +++ b/client/go.mod @@ -37,7 +37,7 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect diff --git a/client/go.sum b/client/go.sum index 06ffd97..2e7b3c4 100644 --- a/client/go.sum +++ b/client/go.sum @@ -746,6 +746,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6bba1f4..c2a0969 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -15,6 +15,7 @@ import { CircularProgress, Grid, LinearProgress, + Skeleton, Stack, Tooltip, Typography, @@ -91,6 +92,9 @@ export function App() { const [actionsInProgress, setActionsInProgress] = React.useState({}); + const [recalculateVolumeSize, setRecalculateVolumeSize] = + React.useState(null); + const columns = [ { field: "volumeDriver", headerName: "Driver", hide: true }, { @@ -103,6 +107,14 @@ export function App() { headerName: "Containers", flex: 1, renderCell: (params) => { + if (isVolumesSizeLoading) { + return ( + + + + ); + } + if (params.row.volumeContainers) { return ( @@ -119,10 +131,13 @@ export function App() { field: "volumeSize", headerName: "Size", renderCell: (params) => { - if (volumesSizeLoadingMap[params.row.volumeName]) { + if ( + isVolumesSizeLoading || + volumesSizeLoadingMap[params.row.volumeName] + ) { return ( - + ); } @@ -290,31 +305,13 @@ export function App() { } }; - const { data: rows, isLoading, listVolumes, setData } = useGetVolumes(); - - useEffect(() => { - const volumeEvents = async () => { - console.log("listening to volume events..."); - await ddClient.docker.cli.exec( - "events", - ["--format", `"{{ json . }}"`, "--filter", "type=volume"], - { - stream: { - onOutput() { - listVolumes(); - }, - onClose(exitCode) { - console.log("onClose with exit code " + exitCode); - }, - splitOutputLines: true, - }, - } - ); - }; - - volumeEvents(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { + data: rows, + listVolumes, + isLoading, + isVolumesSizeLoading, + setData, + } = useGetVolumes(); const getActionsInProgress = async () => { ddClient.extension.vm.service @@ -396,45 +393,83 @@ export function App() { }); }; - const handleExportDialogClose = (actionSuccessfullyCompleted: boolean) => { + const handleExportDialogClose = () => { setOpenExportDialog(false); context.actions.setVolume(null); - if (actionSuccessfullyCompleted) { - listVolumes(); - } }; - const handleImportIntoNewDialogClose = ( - actionSuccessfullyCompleted: boolean - ) => { + const handleImportIntoNewDialogClose = () => { setOpenImportIntoNewDialog(false); context.actions.setVolume(null); + }; + + const handleImportIntoNewDialogCompletion = ( + actionSuccessfullyCompleted: boolean, + selectedVolumeName: string + ) => { if (actionSuccessfullyCompleted) { - if (context.store.volume) + if (selectedVolumeName && context.store.volume) { + // the import is performed on an existing volume calculateVolumeSize(context.store.volume.volumeName); + } else { + // the import is performed on a new volume, so we fetch all volumes to populate the table + listVolumes(); + } } }; - const handleCloneDialogClose = (actionSuccessfullyCompleted: boolean) => { + const handleCloneDialogClose = () => { setOpenCloneDialog(false); context.actions.setVolume(null); + }; + + const handleCloneDialogOnCompletion = ( + clonedVolumeName: string, + actionSuccessfullyCompleted: boolean + ) => { if (actionSuccessfullyCompleted) { - listVolumes(); + const rowsCopy = rows.slice(); + rowsCopy.push({ + id: rows.length, + volumeName: clonedVolumeName, + volumeDriver: "local", + }); + + setData(rowsCopy); + setRecalculateVolumeSize(clonedVolumeName); } }; + useEffect(() => { + if (!recalculateVolumeSize) { + return; + } + calculateVolumeSize(recalculateVolumeSize); + }, [recalculateVolumeSize]); + const handleTransferDialogClose = () => { setOpenTransferDialog(false); context.actions.setVolume(null); }; - const handleDeleteForeverDialogClose = ( - actionSuccessfullyCompleted: boolean - ) => { + const handleDeleteForeverDialogClose = () => { setOpenDeleteForeverDialog(false); context.actions.setVolume(null); - if (actionSuccessfullyCompleted) { - listVolumes(); + }; + + const handleDeleteForeverDialogCompletion = ( + actionSuccessfullyCompleted: boolean + ) => { + if (actionSuccessfullyCompleted && context.store.volume) { + const rowsCopy = rows.slice(); + const index = rowsCopy.findIndex( + (element) => element.volumeName === context.store.volume.volumeName + ); + if (index > -1) { + rowsCopy.splice(index, 1); + } + + setData(rowsCopy); } }; @@ -528,6 +563,7 @@ export function App() { volumes={rows} open={openImportIntoNewDialog} onClose={handleImportIntoNewDialogClose} + onCompletion={handleImportIntoNewDialogCompletion} /> )} @@ -535,6 +571,7 @@ export function App() { )} @@ -548,9 +585,8 @@ export function App() { {openDeleteForeverDialog && ( { - handleDeleteForeverDialogClose(e); - }} + onClose={handleDeleteForeverDialogClose} + onCompletion={handleDeleteForeverDialogCompletion} /> )} diff --git a/ui/src/components/CloneDialog.tsx b/ui/src/components/CloneDialog.tsx index 5992ff7..d4fb06a 100644 --- a/ui/src/components/CloneDialog.tsx +++ b/ui/src/components/CloneDialog.tsx @@ -15,7 +15,8 @@ const ddClient = createDockerDesktopClient(); interface Props { open: boolean; - onClose(v?: boolean): void; + onClose(): void; + onCompletion(clonedVolumeName: string, v?: boolean): void; } export default function CloneDialog({ ...props }: Props) { @@ -44,13 +45,16 @@ export default function CloneDialog({ ...props }: Props) { }, ] ); + props.onCompletion(volumeName, true); }) .catch((error) => { sendNotification.error( `Failed to clone volume ${context.store.volume.volumeName} to destination volume ${volumeName}: ${error.stderr} Exit code: ${error.code}` ); + props.onCompletion(volumeName, false); }); - props.onClose(true); + + props.onClose(); }; return ( @@ -89,7 +93,7 @@ export default function CloneDialog({ ...props }: Props) { variant="outlined" onClick={() => { track({ action: "CloneVolumeCancel" }); - props.onClose(false); + props.onClose(); }} > Cancel diff --git a/ui/src/components/DeleteForeverDialog.tsx b/ui/src/components/DeleteForeverDialog.tsx index 1ae02f0..56a0f5f 100644 --- a/ui/src/components/DeleteForeverDialog.tsx +++ b/ui/src/components/DeleteForeverDialog.tsx @@ -15,7 +15,8 @@ const ddClient = createDockerDesktopClient(); interface Props { open: boolean; - onClose(v?: boolean): void; + onClose(): void; + onCompletion(v?: boolean): void; } export default function DeleteForeverDialog({ ...props }: Props) { @@ -30,13 +31,15 @@ export default function DeleteForeverDialog({ ...props }: Props) { sendNotification.info( `Volume ${context.store.volume.volumeName} deleted` ); + props.onCompletion(true); }) .catch((error) => { sendNotification.error( `Failed to delete volume ${context.store.volume.volumeName}: ${error.stderr} Exit code: ${error.code}` ); + props.onCompletion(false); }); - props.onClose(true); + props.onClose(); }; return ( @@ -53,7 +56,7 @@ export default function DeleteForeverDialog({ ...props }: Props) { variant="outlined" onClick={() => { track({ action: "DeleteVolumeCancel" }); - props.onClose(false); + props.onClose(); }} > Cancel diff --git a/ui/src/components/ImportDialog.tsx b/ui/src/components/ImportDialog.tsx index 8a7c24b..01246b2 100644 --- a/ui/src/components/ImportDialog.tsx +++ b/ui/src/components/ImportDialog.tsx @@ -33,11 +33,17 @@ const ddClient = createDockerDesktopClient(); interface Props { open: boolean; - onClose(v: boolean): void; + onClose(): void; + onCompletion(v: boolean, selectedVolumeName: string): void; volumes: IVolumeRow[]; } -export default function ImportDialog({ volumes, open, onClose }: Props) { +export default function ImportDialog({ + volumes, + open, + onClose, + onCompletion, +}: Props) { const [fromRadioValue, setFromRadioValue] = useState< "file" | "image" | "pull-registry" >("file"); @@ -86,21 +92,39 @@ export default function ImportDialog({ volumes, open, onClose }: Props) { importVolume({ volumeName: volumeId?.[0] || selectedVolumeName, path, - }); + }) + .then(() => { + onCompletion(true, selectedVolumeName); + }) + .catch(() => { + onCompletion(false, selectedVolumeName); + }); } else if (fromRadioValue === "image") { track({ ...metrics, importType: "fromLocalImage" }); loadImage({ volumeName: volumeId?.[0] || selectedVolumeName, imageName: image, - }); + }) + .then(() => { + onCompletion(true, selectedVolumeName); + }) + .catch(() => { + onCompletion(false, selectedVolumeName); + }); } else { track({ ...metrics, importType: "fromRegistry" }); pullFromRegistry({ imageName: registryImage, volumeId: volumeId?.[0], - }); + }) + .then(() => { + onCompletion(true, selectedVolumeName); + }) + .catch(() => { + onCompletion(false, selectedVolumeName); + }); } - onClose(true); + onClose(); }; const handleChange = (event: React.ChangeEvent) => { @@ -245,7 +269,7 @@ export default function ImportDialog({ volumes, open, onClose }: Props) { onClick={() => { track({ action: "ImportVolumeCancel" }); setPath(""); - onClose(false); + onClose(); }} > Cancel diff --git a/ui/src/hooks/useGetVolumes.ts b/ui/src/hooks/useGetVolumes.ts index 4bd1550..080dba2 100644 --- a/ui/src/hooks/useGetVolumes.ts +++ b/ui/src/hooks/useGetVolumes.ts @@ -15,13 +15,14 @@ export interface IVolumeRow { id: number; volumeDriver: string; volumeName: string; - volumeContainers: unknown[] | null; - volumeSize: string; - volumeBytes: number; + volumeContainers?: unknown[] | null; + volumeSize?: string; + volumeBytes?: number; } export const useGetVolumes = () => { const [isLoading, setIsLoading] = useState(false); + const [isVolumesSizeLoading, setIsVolumesSizeLoading] = useState(false); const [data, setData] = useState(); const { sendNotification } = useNotificationContext(); @@ -44,13 +45,8 @@ export const useGetVolumes = () => { const value = results[key]; rows.push({ id: index, - volumeDriver: value.Driver, volumeName: key, - volumeContainers: value.Containers?.length - ? value.Containers - : null, - volumeSize: value.SizeHuman, - volumeBytes: value.Size, + volumeDriver: value.Driver, }); index++; } @@ -59,9 +55,57 @@ export const useGetVolumes = () => { const endTime = performance.now(); console.log(`[listVolumes] took ${endTime - startTime} ms.`); setData(rows); + + setIsVolumesSizeLoading(true); + const fetchVolumesSize = new Promise((resolve) => { + ddClient.extension.vm.service + .get("/volumes/size") + .then((results: Record) => { + resolve(results); + }); + }); + + const fetchVolumesContainer = new Promise((resolve) => { + ddClient.extension.vm.service + .get("/volumes/container") + .then((results: Record) => { + resolve(results); + }); + }); + + // Fetch volumes size and containers attached + Promise.all([fetchVolumesSize, fetchVolumesContainer]).then( + (values) => { + const sizesMap = values[0]; + const containersMap = values[1]; + + const updatedRows: IVolumeRow[] = []; + for (const key in rows) { + const row = rows[key]; + + if (containersMap[row.volumeName] !== undefined) { + if (containersMap[row.volumeName].Containers?.length) { + row.volumeContainers = + containersMap[row.volumeName].Containers; + } + } + + if (sizesMap[row.volumeName] !== undefined) { + row.volumeSize = sizesMap[row.volumeName].Human; + row.volumeBytes = sizesMap[row.volumeName].Bytes; + } + + updatedRows.push(row); + } + + setData(updatedRows); + setIsVolumesSizeLoading(false); + } + ); }); } catch (error) { setIsLoading(false); + setIsVolumesSizeLoading(false); sendNotification.error(`Failed to list volumes: ${error.stderr}`); } }; @@ -69,6 +113,7 @@ export const useGetVolumes = () => { return { listVolumes, isLoading, + isVolumesSizeLoading, data, setData, }; diff --git a/vm/go.mod b/vm/go.mod index 1da8549..3fb9d45 100644 --- a/vm/go.mod +++ b/vm/go.mod @@ -48,9 +48,9 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/text v0.3.7 // indirect v0.0.0-20220708220712-1185a9018129 golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect google.golang.org/protobuf v1.26.0-rc.1 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/vm/go.sum b/vm/go.sum index 93f8283..d812553 100644 --- a/vm/go.sum +++ b/vm/go.sum @@ -189,6 +189,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -221,6 +223,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/vm/internal/handler/clone.go b/vm/internal/handler/clone.go index a60c35c..2cc5083 100644 --- a/vm/internal/handler/clone.go +++ b/vm/internal/handler/clone.go @@ -2,6 +2,7 @@ package handler import ( "fmt" + volumetypes "github.com/docker/docker/api/types/volume" "io" "net/http" "os" @@ -69,6 +70,19 @@ func (h *Handler) CloneVolume(ctx echo.Context) error { return err } + // Create destination volume with the same labels as the source volume + volInspect, err := cli.VolumeInspect(ctx.Request().Context(), volumeName) + if err != nil { + return err + } + _, err = cli.VolumeCreate(ctx.Request().Context(), volumetypes.VolumeCreateBody{ + Labels: volInspect.Labels, + Name: destVolume, + }) + if err != nil { + return err + } + // Clone resp, err := cli.ContainerCreate(ctxReq, &container.Config{ Image: internal.BusyboxImage, diff --git a/vm/internal/handler/clone_test.go b/vm/internal/handler/clone_test.go index 7f0c7ee..b2f9b59 100644 --- a/vm/internal/handler/clone_test.go +++ b/vm/internal/handler/clone_test.go @@ -50,6 +50,11 @@ func TestCloneVolume(t *testing.T) { _, err := cli.VolumeCreate(c.Request().Context(), volumetypes.VolumeCreateBody{ Driver: "local", Name: volume, + Labels: map[string]string{ + "com.docker.compose.project": "my-compose-project", + "com.docker.compose.version": "2.10.2", + "com.docker.compose.volume": "foo-bar", + }, }) if err != nil { t.Fatal(err) @@ -100,4 +105,14 @@ func TestCloneVolume(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(16000), sizes[destVolume].Bytes) require.Equal(t, "16.0 kB", sizes[destVolume].Human) + + // Check volume labels + volInspect, err := cli.VolumeInspect(context.Background(), destVolume) + if err != nil { + t.Fatal(err) + } + require.Len(t, volInspect.Labels, 3) + require.Equal(t, "my-compose-project", volInspect.Labels["com.docker.compose.project"]) + require.Equal(t, "2.10.2", volInspect.Labels["com.docker.compose.version"]) + require.Equal(t, "foo-bar", volInspect.Labels["com.docker.compose.volume"]) } diff --git a/vm/internal/handler/containers.go b/vm/internal/handler/containers.go new file mode 100644 index 0000000..d11696d --- /dev/null +++ b/vm/internal/handler/containers.go @@ -0,0 +1,52 @@ +package handler + +import ( + "github.com/docker/docker/api/types/filters" + "net/http" + "sync" + + "github.com/docker/volumes-backup-extension/internal/backend" + "github.com/labstack/echo" +) + +func (h *Handler) VolumesContainer(ctx echo.Context) error { + ctxReq := ctx.Request().Context() + cli, err := h.DockerClient() + if err != nil { + return err + } + + v, err := cli.VolumeList(ctxReq, filters.NewArgs()) + if err != nil { + return err + } + + var res = VolumesResponse{ + data: map[string]VolumeData{}, + } + + var wg sync.WaitGroup + for _, vol := range v.Volumes { + wg.Add(1) + + go func(volumeName string) { + defer wg.Done() + containers := backend.GetContainersForVolume(ctxReq, cli, volumeName) + res.Lock() + defer res.Unlock() + entry, ok := res.data[volumeName] + if !ok { + res.data[volumeName] = VolumeData{ + Containers: containers, + } + return + } + entry.Containers = containers + res.data[volumeName] = entry + }(vol.Name) + } + + wg.Wait() + + return ctx.JSON(http.StatusOK, res.data) +} diff --git a/vm/internal/handler/sizes.go b/vm/internal/handler/sizes.go new file mode 100644 index 0000000..17ee95e --- /dev/null +++ b/vm/internal/handler/sizes.go @@ -0,0 +1,19 @@ +package handler + +import ( + "net/http" + + "github.com/docker/volumes-backup-extension/internal/backend" + "github.com/labstack/echo" +) + +func (h *Handler) VolumesSize(ctx echo.Context) error { + cli, err := h.DockerClient() + if err != nil { + return err + } + + m := backend.GetVolumesSize(ctx.Request().Context(), cli, "*") + + return ctx.JSON(http.StatusOK, m) +} diff --git a/vm/internal/handler/volumes.go b/vm/internal/handler/volumes.go index 730aaf6..f7208db 100644 --- a/vm/internal/handler/volumes.go +++ b/vm/internal/handler/volumes.go @@ -1,12 +1,10 @@ package handler import ( - "context" "net/http" "sync" - "github.com/docker/docker/api/types/filters" - "github.com/docker/volumes-backup-extension/internal/backend" + "github.com/docker/docker/api/types/filters "github.com/labstack/echo" ) @@ -39,66 +37,11 @@ func (h *Handler) Volumes(ctx echo.Context) error { data: map[string]VolumeData{}, } - var wg sync.WaitGroup - // Calculating the volume size by spinning a container that execs "du " **per volume** is too time-consuming. - // To reduce the time it takes, we get the volumes size by running only one container that execs "du" - // into the /var/lib/docker/volumes inside the VM. - volumesSize, err := backend.GetVolumesSize(ctxReq, cli, "*") - if err != nil { - return err - } - - res.Lock() - for k, v := range volumesSize { - entry, ok := res.data[k] - if !ok { - res.data[k] = VolumeData{ - Size: v.Bytes, - SizeHuman: v.Human, - } - continue - } - entry.Size = v.Bytes - entry.SizeHuman = v.Human - res.data[k] = entry - } - res.Unlock() - for _, vol := range v.Volumes { - wg.Add(2) - go func(volumeName string) { - defer wg.Done() - driver := backend.GetVolumeDriver(context.Background(), cli, volumeName) // TODO: use request context - res.Lock() - defer res.Unlock() - entry, ok := res.data[volumeName] - if !ok { - res.data[volumeName] = VolumeData{ - Driver: driver, - } - return - } - entry.Driver = driver - res.data[volumeName] = entry - }(vol.Name) - - go func(volumeName string) { - defer wg.Done() - containers := backend.GetContainersForVolume(context.Background(), cli, volumeName) // TODO: use request context - res.Lock() - defer res.Unlock() - entry, ok := res.data[volumeName] - if !ok { - res.data[volumeName] = VolumeData{ - Containers: containers, - } - return - } - entry.Containers = containers - res.data[volumeName] = entry - }(vol.Name) + res.data[vol.Name] = VolumeData{ + Driver: vol.Driver, + } } - wg.Wait() return ctx.JSON(http.StatusOK, res.data) } diff --git a/vm/internal/handler/volumes_test.go b/vm/internal/handler/volumes_test.go index d41c2a0..2e29905 100644 --- a/vm/internal/handler/volumes_test.go +++ b/vm/internal/handler/volumes_test.go @@ -51,7 +51,7 @@ func TestVolumes(t *testing.T) { require.Contains(t, m, volume) require.Equal(t, "local", m[volume].Driver) require.Equal(t, int64(0), m[volume].Size) - require.Equal(t, "0 B", m[volume].SizeHuman) + require.Equal(t, "", m[volume].SizeHuman) require.Len(t, m[volume].Containers, 0) } diff --git a/vm/main.go b/vm/main.go index ad577d6..f7f638e 100644 --- a/vm/main.go +++ b/vm/main.go @@ -72,6 +72,8 @@ func main() { router.GET("/progress", h.ActionsInProgress) router.GET("/volumes", h.Volumes) + router.GET("/volumes/size", h.VolumesSize) + router.GET("/volumes/container", h.VolumesContainer) router.GET("/volumes/:volume/size", h.VolumeSize) router.POST("/volumes/:volume/clone", h.CloneVolume) router.POST("/volumes/:volume/delete", h.DeleteVolume) diff --git a/vm/volumes-backup-extension b/vm/volumes-backup-extension deleted file mode 100755 index fb128be..0000000 Binary files a/vm/volumes-backup-extension and /dev/null differ