From 38136a6bb6ad7a43e2c7f5d00a993ea297394647 Mon Sep 17 00:00:00 2001 From: Florent Benoit Date: Tue, 22 Aug 2023 11:19:23 +0200 Subject: [PATCH] chore: allow to grab volumes with or without the Size data related to https://github.com/containers/podman-desktop/issues/3376 Signed-off-by: Florent Benoit --- .../src/plugin/container-registry.spec.ts | 370 ++++++++++++++++++ .../main/src/plugin/container-registry.ts | 47 ++- packages/main/src/plugin/index.ts | 9 +- packages/preload/src/index.ts | 4 +- 4 files changed, 408 insertions(+), 22 deletions(-) diff --git a/packages/main/src/plugin/container-registry.spec.ts b/packages/main/src/plugin/container-registry.spec.ts index 798ca6fe51639..c6af1a1cdf08c 100644 --- a/packages/main/src/plugin/container-registry.spec.ts +++ b/packages/main/src/plugin/container-registry.spec.ts @@ -1022,3 +1022,373 @@ describe('buildImage', () => { }); }); }); + +describe('listVolumes', () => { + test('with fetching the volumes size', async () => { + const volumesDataMock = { + Volumes: [ + { + CreatedAt: '2023-08-21T18:35:28+02:00', + Driver: 'local', + Labels: {}, + Mountpoint: '/var/lib/containers/storage/volumes/foo/_data', + Name: 'foo', + Options: {}, + Scope: 'local', + }, + { + CreatedAt: '2023-08-21T18:35:34+02:00', + Driver: 'local', + Labels: {}, + Mountpoint: '/var/lib/containers/storage/volumes/fooeeee/_data', + Name: 'fooeeee', + Options: {}, + Scope: 'local', + }, + { + CreatedAt: '2023-08-21T10:50:52+02:00', + Driver: 'local', + Labels: {}, + Mountpoint: '/var/lib/containers/storage/volumes/myFirstVolume/_data', + Name: 'myFirstVolume', + Options: {}, + Scope: 'local', + }, + ], + Warnings: [], + }; + + const systemDfDataMock = { + LayersSize: 0, + // empty images for mock + Images: [], + Containers: [ + { + Id: '5c69247085f8ae225535a6051515eb08a6d1e79ff8d70d57fda52555b5fce0dd', + Names: ['strange_rhodes'], + Image: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + ImageID: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692607778, + Ports: null, + SizeRw: 1921681, + SizeRootFs: 647340350, + Labels: {}, + State: 'running', + Status: 'running', + HostConfig: {}, + NetworkSettings: null, + Mounts: null, + }, + { + Id: 'ae84549539d26cdcafb9865a77bce53ea072fd256cc419b376ce3f33d66bbe75', + Names: ['kind_antonelli'], + Image: 'ab73c7fd672341e41ec600081253d0b99ea31d0c1acdfb46a1485004472da7ac', + ImageID: 'ab73c7fd672341e41ec600081253d0b99ea31d0c1acdfb46a1485004472da7ac', + Command: 'nginx -g daemon off;', + Created: 1692624321, + Ports: null, + SizeRw: 12595, + SizeRootFs: 196209217, + Labels: {}, + State: 'running', + Status: 'running', + HostConfig: {}, + NetworkSettings: null, + Mounts: null, + }, + { + Id: 'afa18fe0f64509ce24011a0a402852ceb393448951421199c214d912aadc3cf6', + Names: ['elegant_mirzakhani'], + Image: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + ImageID: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692607777, + Ports: null, + SizeRw: 1921687, + SizeRootFs: 647340356, + Labels: {}, + State: 'running', + Status: 'running', + HostConfig: {}, + NetworkSettings: null, + Mounts: null, + }, + { + Id: 'e471d29de42a8a411b7bcd6fb0fa1a0f24ce28284d42bd11bd1decd7946dfa3a', + Names: ['friendly_keldysh'], + Image: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + ImageID: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692634818, + Ports: null, + SizeRw: 1920353, + SizeRootFs: 647339022, + Labels: {}, + State: 'running', + Status: 'running', + HostConfig: {}, + NetworkSettings: null, + Mounts: null, + }, + { + Id: 'e679f6fde4504a9323810548045ac6bee8dbb006869324b0b80c446b464407f0', + Names: ['amazing_tharp'], + Image: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + ImageID: 'ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692607778, + Ports: null, + SizeRw: 1922070, + SizeRootFs: 647340739, + Labels: {}, + State: 'running', + Status: 'running', + HostConfig: {}, + NetworkSettings: null, + Mounts: null, + }, + ], + Volumes: [ + { + Driver: '', + Labels: {}, + Mountpoint: '', + Name: 'foo', + Options: null, + Scope: 'local', + UsageData: { RefCount: 0, Size: 0 }, + }, + { + Driver: '', + Labels: {}, + Mountpoint: '', + Name: 'fooeeee', + Options: null, + Scope: 'local', + UsageData: { RefCount: 0, Size: 0 }, + }, + { + Driver: '', + Labels: {}, + Mountpoint: '', + Name: 'myFirstVolume', + Options: null, + Scope: 'local', + UsageData: { RefCount: 1, Size: 83990640 }, + }, + ], + BuildCache: [], + }; + + const containersJsonMock = [ + { + Id: 'ae84549539d26cdcafb9865a77bce53ea072fd256cc419b376ce3f33d66bbe75', + Names: ['/kind_antonelli'], + Image: 'foo-image', + ImageID: 'sha256:ab73c7fd672341e41ec600081253d0b99ea31d0c1acdfb46a1485004472da7ac', + Created: 1692624321, + Mounts: [ + { + Type: 'volume', + Name: 'myFirstVolume', + Source: '/var/lib/containers/storage/volumes/myFirstVolume/_data', + Destination: '/app', + Driver: 'local', + Mode: '', + RW: true, + Propagation: 'rprivate', + }, + ], + }, + { + Id: 'afa18fe0f64509ce24011a0a402852ceb393448951421199c214d912aadc3cf6', + Names: ['/elegant_mirzakhani'], + Image: 'foo-image', + ImageID: 'sha256:ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692607777, + Mounts: [], + }, + { + Id: 'e471d29de42a8a411b7bcd6fb0fa1a0f24ce28284d42bd11bd1decd7946dfa3a', + Names: ['/friendly_keldysh'], + Image: 'foo-image', + ImageID: 'sha256:ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692634818, + Mounts: [], + }, + { + Id: 'e679f6fde4504a9323810548045ac6bee8dbb006869324b0b80c446b464407f0', + Names: ['/amazing_tharp'], + Image: 'foo-image', + ImageID: 'sha256:ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692607778, + Ports: [], + Mounts: [], + }, + ]; + + nock('http://localhost').get('/volumes').reply(200, volumesDataMock); + nock('http://localhost').get('/containers/json?all=true').reply(200, containersJsonMock); + nock('http://localhost').get('/system/df').reply(200, systemDfDataMock); + + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + // set provider + containerRegistry.addInternalProvider('podman', { + name: 'podman', + id: 'podman1', + api, + connection: { + type: 'podman', + }, + } as unknown as InternalContainerProvider); + + // ask for volumes and data + const volumes = await containerRegistry.listVolumes(true); + + // ensure the field are correct + expect(volumes).toBeDefined(); + expect(volumes).toHaveLength(1); + const volume = volumes[0]; + expect(volume.engineId).toBe('podman1'); + expect(volume.engineName).toBe('podman'); + expect(volume.Volumes).toHaveLength(3); + + const volumeData = volume.Volumes[2]; + + expect(volumeData.Name).toBe('myFirstVolume'); + + // check UsageData is set (provided by system/df) + // refcount is 1 as one container is using it + expect(volumeData.UsageData).toStrictEqual({ + RefCount: 1, + Size: 83990640, + }); + }); + + test('without fetching the volumes size', async () => { + const volumesDataMock = { + Volumes: [ + { + CreatedAt: '2023-08-21T18:35:28+02:00', + Driver: 'local', + Labels: {}, + Mountpoint: '/var/lib/containers/storage/volumes/foo/_data', + Name: 'foo', + Options: {}, + Scope: 'local', + }, + { + CreatedAt: '2023-08-21T18:35:34+02:00', + Driver: 'local', + Labels: {}, + Mountpoint: '/var/lib/containers/storage/volumes/fooeeee/_data', + Name: 'fooeeee', + Options: {}, + Scope: 'local', + }, + { + CreatedAt: '2023-08-21T10:50:52+02:00', + Driver: 'local', + Labels: {}, + Mountpoint: '/var/lib/containers/storage/volumes/myFirstVolume/_data', + Name: 'myFirstVolume', + Options: {}, + Scope: 'local', + }, + ], + Warnings: [], + }; + + const containersJsonMock = [ + { + Id: 'ae84549539d26cdcafb9865a77bce53ea072fd256cc419b376ce3f33d66bbe75', + Names: ['/kind_antonelli'], + Image: 'foo-image', + ImageID: 'sha256:ab73c7fd672341e41ec600081253d0b99ea31d0c1acdfb46a1485004472da7ac', + Created: 1692624321, + Mounts: [ + { + Type: 'volume', + Name: 'myFirstVolume', + Source: '/var/lib/containers/storage/volumes/myFirstVolume/_data', + Destination: '/app', + Driver: 'local', + Mode: '', + RW: true, + Propagation: 'rprivate', + }, + ], + }, + { + Id: 'afa18fe0f64509ce24011a0a402852ceb393448951421199c214d912aadc3cf6', + Names: ['/elegant_mirzakhani'], + Image: 'foo-image', + ImageID: 'sha256:ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692607777, + Mounts: [], + }, + { + Id: 'e471d29de42a8a411b7bcd6fb0fa1a0f24ce28284d42bd11bd1decd7946dfa3a', + Names: ['/friendly_keldysh'], + Image: 'foo-image', + ImageID: 'sha256:ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692634818, + Mounts: [], + }, + { + Id: 'e679f6fde4504a9323810548045ac6bee8dbb006869324b0b80c446b464407f0', + Names: ['/amazing_tharp'], + Image: 'foo-image', + ImageID: 'sha256:ee9bfd27b1dbb584a40687ec1f9db5f5c16c53c2f3041cf702e9495ceda22195', + Command: '/entrypoint.sh', + Created: 1692607778, + Ports: [], + Mounts: [], + }, + ]; + + nock('http://localhost').get('/volumes').reply(200, volumesDataMock); + nock('http://localhost').get('/containers/json?all=true').reply(200, containersJsonMock); + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + // set provider + containerRegistry.addInternalProvider('podman', { + name: 'podman', + id: 'podman1', + api, + connection: { + type: 'podman', + }, + } as unknown as InternalContainerProvider); + + // ask for volumes and data + const volumes = await containerRegistry.listVolumes(false); + + // ensure the field are correct + expect(volumes).toBeDefined(); + expect(volumes).toHaveLength(1); + const volume = volumes[0]; + expect(volume.engineId).toBe('podman1'); + expect(volume.engineName).toBe('podman'); + expect(volume.Volumes).toHaveLength(3); + + const volumeData = volume.Volumes[2]; + + expect(volumeData.Name).toBe('myFirstVolume'); + + // check UsageData is set (provided by system/df) + // refcount is 1 as one container is using it + // but size is -1 as we skip system df call + expect(volumeData.UsageData).toStrictEqual({ + RefCount: 1, + Size: -1, + }); + }); +}); diff --git a/packages/main/src/plugin/container-registry.ts b/packages/main/src/plugin/container-registry.ts index 9f2930da21e03..6a553fe1440e1 100644 --- a/packages/main/src/plugin/container-registry.ts +++ b/packages/main/src/plugin/container-registry.ts @@ -542,7 +542,7 @@ export class ContainerProviderRegistry { return flatttenedNetworks; } - async listVolumes(): Promise { + async listVolumes(fetchUsage = false): Promise { let telemetryOptions = {}; const volumes = await Promise.all( Array.from(this.internalProviders.values()).map(async provider => { @@ -551,31 +551,29 @@ export class ContainerProviderRegistry { return []; } - // grab the storage information - const storageDefinition = await provider.api.df(); - // grab containers const containers = await provider.api.listContainers({ all: true }); // any as there is a CreatedAt field missing in the type // eslint-disable-next-line @typescript-eslint/no-explicit-any const volumeListInfo: any = await provider.api.listVolumes(); + + let storageDefinition = { + Volumes: [], + }; + + if (fetchUsage) { + // grab the storage information + storageDefinition = await provider.api.df(); + } + const engineName = provider.name; const engineId = provider.id; // eslint-disable-next-line @typescript-eslint/no-explicit-any const volumeInfos = volumeListInfo.Volumes.map((volumeList: any) => { const volumeInfo: VolumeInfo = { ...volumeList, engineName, engineId }; - // do we have a matching volume in storage definition ? - const matchingVolume = (storageDefinition.Volumes || []).find( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (volumeStorage: any) => volumeStorage.Name === volumeInfo.Name, - ); - if (matchingVolume) { - volumeInfo.UsageData = matchingVolume.UsageData; - } - - // if refCount > 0, we need to find the container using it + // compute containers using this volume const containersUsingThisVolume = containers .filter(container => container.Mounts.find(mount => mount.Name === volumeInfo.Name)) .map(container => { @@ -583,11 +581,26 @@ export class ContainerProviderRegistry { }); volumeInfo.containersUsage = containersUsingThisVolume; - // invalid in Podman https://github.com/containers/podman/issues/15720 - if (volumeInfo.UsageData) { - volumeInfo.UsageData.RefCount = volumeInfo.containersUsage.length; + // no usage data, set to -1 for size and 0 for refCount + if (!volumeInfo.UsageData) { + volumeInfo.UsageData = { + Size: -1, + RefCount: 0, + }; } + // defines the refCount + volumeInfo.UsageData.RefCount = volumeInfo.containersUsage.length; + // do we have a matching volume in storage definition ? + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const matchingVolume: any = (storageDefinition?.Volumes || []).find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (volumeStorage: any) => volumeStorage.Name === volumeInfo.Name, + ); + // update the size if asked and then there is a matching volume + if (matchingVolume) { + volumeInfo.UsageData.Size = matchingVolume.UsageData.Size; + } return volumeInfo; }); return { Volumes: volumeInfos, Warnings: volumeListInfo.Warnings, engineName, engineId }; diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index 0dfb7f1c2f888..a04562a7b9967 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -717,9 +717,12 @@ export class PluginSystem { this.ipcHandle('container-provider-registry:listNetworks', async (): Promise => { return containerProviderRegistry.listNetworks(); }); - this.ipcHandle('container-provider-registry:listVolumes', async (): Promise => { - return containerProviderRegistry.listVolumes(); - }); + this.ipcHandle( + 'container-provider-registry:listVolumes', + async (_listener, fetchUsage: boolean): Promise => { + return containerProviderRegistry.listVolumes(fetchUsage); + }, + ); this.ipcHandle('container-provider-registry:reconnectContainerProviders', async (): Promise => { return containerProviderRegistry.reconnectContainerProviders(); diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index e8400eaf467cc..87f7d4be5e132 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -194,8 +194,8 @@ function initExposure(): void { return ipcInvoke('container-provider-registry:listImages'); }); - contextBridge.exposeInMainWorld('listVolumes', async (): Promise => { - return ipcInvoke('container-provider-registry:listVolumes'); + contextBridge.exposeInMainWorld('listVolumes', async (fetchUsage = true): Promise => { + return ipcInvoke('container-provider-registry:listVolumes', fetchUsage); }); contextBridge.exposeInMainWorld('removeVolume', async (engine: string, volumeName: string): Promise => { return ipcInvoke('container-provider-registry:removeVolume', engine, volumeName);