Skip to content

Commit

Permalink
feat: add "deploy to kubernetes" label for compose group
Browse files Browse the repository at this point in the history
### What does this PR do?

* Adds button to Deploy to Kubernetes for a Compose group
* Utilizes DeployPodToKube same code, but added a "compose" type
* Adds a new function for listSimpleContainersByLabel for an easier way
  to find containers by label that you only need the name / id of.

### Screenshot/screencast of this PR

<!-- Please include a screenshot or a screencast explaining what is doing this PR -->

### What issues does this PR fix or reference?

<!-- Please include any related issue from Podman Desktop repository (or from another issue tracker).
-->

Closes #3104

### How to test this PR?

1. Deploy a Docker Compose example
2. Click on the "hamburger" button to bring up the Deploy To Kubernetes
   button.
3. Deploy to Kubernetes and make sure it's deployed correctly.

<!-- Please explain steps to reproduce -->

Signed-off-by: Charlie Drage <charlie@charliedrage.com>
  • Loading branch information
cdrage committed Jul 20, 2023
1 parent 5e2055d commit 9fed2ad
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 6 deletions.
26 changes: 26 additions & 0 deletions packages/main/src/plugin/container-registry.spec.ts
Expand Up @@ -278,3 +278,29 @@ test('deleteContainersByLabel should succeed successfully if project name is pro
// Expect deleteContainer tohave been called 3 times
expect(deleteContainer).toHaveBeenCalledTimes(3);
});

test('test listSimpleContainersByLabel with compose label', async () => {
const engine = {
// Fake that we have 3 containers of the same project
listSimpleContainers: vi
.fn()
.mockResolvedValue([
fakeContainerWithComposeProject,
fakeContainerWithComposeProject,
fakeContainerWithComposeProject,
fakeContainer,
]),
listPods: vi.fn().mockResolvedValue([]),
};
vi.spyOn(containerRegistry, 'getMatchingEngine').mockReturnValue(engine as unknown as Dockerode);
vi.spyOn(containerRegistry, 'listSimpleContainers').mockReturnValue(engine.listSimpleContainers());

// List all containers with the label 'com.docker.compose.project' and value 'project1'
const result = await containerRegistry.listSimpleContainersByLabel(
'com.docker.compose.project',
'project1',
);

// We expect ONLY to return 3 since the last container does not have the correct label.
expect(result).toHaveLength(3);
});
12 changes: 12 additions & 0 deletions packages/main/src/plugin/container-registry.ts
Expand Up @@ -265,6 +265,18 @@ export class ContainerProviderRegistry {
return flatttenedContainers;
}

// listSimpleContainers by matching label and key
async listSimpleContainersByLabel(label: string, key: string): Promise<SimpleContainerInfo[]> {
// Get all the containers using listSimpleContainers
const containers = await this.listSimpleContainers();

// Find all the containers that match the label + key
return containers.filter(container => {
const labels = container.Labels;
return labels && labels[label] === key;
});
}

async listContainers(): Promise<ContainerInfo[]> {
let telemetryOptions = {};
const containers = await Promise.all(
Expand Down
8 changes: 8 additions & 0 deletions packages/main/src/plugin/index.ts
Expand Up @@ -684,6 +684,14 @@ export class PluginSystem {
this.ipcHandle('container-provider-registry:listContainers', async (): Promise<ContainerInfo[]> => {
return containerProviderRegistry.listContainers();
});

this.ipcHandle(
'container-provider-registry:listSimpleContainersByLabel',
async (_listener, label: string, key: string): Promise<SimpleContainerInfo[]> => {
return containerProviderRegistry.listSimpleContainersByLabel(label, key);
},
);

this.ipcHandle('container-provider-registry:listSimpleContainers', async (): Promise<SimpleContainerInfo[]> => {
return containerProviderRegistry.listSimpleContainers();
});
Expand Down
13 changes: 12 additions & 1 deletion packages/preload/src/index.ts
Expand Up @@ -23,7 +23,11 @@
import type * as containerDesktopAPI from '@podman-desktop/api';
import { contextBridge, ipcRenderer } from 'electron';
import EventEmitter from 'events';
import type { ContainerCreateOptions, ContainerInfo } from '../../main/src/plugin/api/container-info';
import type {
ContainerCreateOptions,
ContainerInfo,
SimpleContainerInfo,
} from '../../main/src/plugin/api/container-info';
import type { ContributionInfo } from '../../main/src/plugin/api/contribution-info';
import type { ImageInfo } from '../../main/src/plugin/api/image-info';
import type { VolumeInspectInfo, VolumeListInfo } from '../../main/src/plugin/api/volume-info';
Expand Down Expand Up @@ -177,6 +181,13 @@ function initExposure(): void {
return ipcInvoke('container-provider-registry:listContainers');
});

contextBridge.exposeInMainWorld(
'listSimpleContainersByLabel',
async (label: string, key: string): Promise<SimpleContainerInfo[]> => {
return ipcInvoke('container-provider-registry:listSimpleContainersByLabel', label, key);
},
);

contextBridge.exposeInMainWorld('listImages', async (): Promise<ImageInfo[]> => {
return ipcInvoke('container-provider-registry:listImages');
});
Expand Down
10 changes: 9 additions & 1 deletion packages/renderer/src/App.svelte
Expand Up @@ -126,7 +126,15 @@ window.events?.receive('display-troubleshooting', () => {
<Route path="/deploy-to-kube/:resourceId/:engineId/*" breadcrumb="Deploy to Kubernetes" let:meta>
<DeployPodToKube
resourceId="{decodeURI(meta.params.resourceId)}"
engineId="{decodeURI(meta.params.engineId)}" />
engineId="{decodeURI(meta.params.engineId)}"
type="container" />
</Route>
<!-- Same DeployPodToKube route, but instead we pass in the compose group name, then redirect to DeployPodToKube -->
<Route path="/compose/deploy-to-kube/:composeGroupName/:engineId/*" breadcrumb="Deploy to Kubernetes" let:meta>
<DeployPodToKube
resourceId="{decodeURI(meta.params.composeGroupName)}"
engineId="{decodeURI(meta.params.engineId)}"
type="compose" />
</Route>
<Route path="/pods/:kind/:name/:engineId/*" breadcrumb="Pod Details" let:meta navigationHint="details">
<PodDetails
Expand Down
13 changes: 12 additions & 1 deletion packages/renderer/src/lib/compose/ComposeActions.svelte
@@ -1,9 +1,10 @@
<script lang="ts">
import { faTrash, faPlay, faStop, faArrowsRotate } from '@fortawesome/free-solid-svg-icons';
import { faTrash, faPlay, faStop, faArrowsRotate, faRocket } from '@fortawesome/free-solid-svg-icons';
import type { ComposeInfoUI } from './ComposeInfoUI';
import ListItemButtonIcon from '../ui/ListItemButtonIcon.svelte';
import DropdownMenu from '../ui/DropdownMenu.svelte';
import FlatMenu from '../ui/FlatMenu.svelte';
import { router } from 'tinro';
export let compose: ComposeInfoUI;
export let dropdownMenu = false;
Expand Down Expand Up @@ -57,6 +58,10 @@ async function restartCompose(composeInfoUI: ComposeInfoUI) {
}
}
function deployToKubernetes(): void {
router.goto(`/compose/deploy-to-kube/${compose.name}/${compose.engineId}`);
}
// If dropdownMenu = true, we'll change style to the imported dropdownMenu style
// otherwise, leave blank.
let actionsStyle;
Expand Down Expand Up @@ -93,6 +98,12 @@ if (dropdownMenu) {

<!-- If dropdownMenu is true, use it, otherwise just show the regular buttons -->
<svelte:component this="{actionsStyle}">
<ListItemButtonIcon
title="Deploy to Kubernetes"
onClick="{() => deployToKubernetes()}"
menu="{dropdownMenu}"
detailed="{detailed}"
icon="{faRocket}" />
<ListItemButtonIcon
title="Restart Compose"
onClick="{() => restartCompose(compose)}"
Expand Down
44 changes: 44 additions & 0 deletions packages/renderer/src/lib/pod/DeployPodToKube.spec.ts
Expand Up @@ -36,6 +36,7 @@ const kubernetesCreatePodMock = vi.fn();
const kubernetesCreateIngressMock = vi.fn();
const kubernetesCreateServiceMock = vi.fn();
const kubernetesIsAPIGroupSupported = vi.fn();
const listSimpleContainersByLabelMock = vi.fn();

beforeEach(() => {
Object.defineProperty(window, 'generatePodmanKube', {
Expand Down Expand Up @@ -69,6 +70,9 @@ beforeEach(() => {
Object.defineProperty(window, 'telemetryTrack', {
value: telemetryTrackMock,
});
Object.defineProperty(window, 'listSimpleContainersByLabel', {
value: listSimpleContainersByLabelMock,
});

// podYaml with volumes
const podYaml = {
Expand Down Expand Up @@ -97,6 +101,19 @@ beforeEach(() => {
},
};
generatePodmanKubeMock.mockResolvedValue(jsYaml.dump(podYaml));

// Mock listSimpleContainersByLabel with a SimpleContainerInfo[] array of 1 container
const simpleContainerInfo = {
id: '1234',
name: 'hello',
image: 'hello-world',
status: 'running',
labels: {
'com.docker.compose.project': 'hello',
},
};
listSimpleContainersByLabelMock.mockResolvedValue([simpleContainerInfo]);

});

afterEach(() => {
Expand Down Expand Up @@ -198,6 +215,33 @@ test('When deploying a pod, volumes should not be added (they are deleted by pod
);
});

// Test deploying a compose group of containers
test('Test deploying a group of compose containers with type compose still functions the same as normal deploy', async () => {
await waitRender({ type: 'compose' });
const createButton = screen.getByRole('button', { name: 'Deploy' });
expect(createButton).toBeInTheDocument();
expect(createButton).toBeEnabled();

// Press the deploy button
await fireEvent.click(createButton);

// Expect to return the correct create pod yaml
await waitFor(() =>
expect(kubernetesCreatePodMock).toBeCalledWith('default', {
metadata: { name: 'hello' },
spec: {
containers: [
{
name: 'hello',
image: 'hello-world',
imagePullPolicy: 'IfNotPresent',
},
],
},
}),
);
});

// After modifying the pod name, metadata.apps.label should also have been changed
test('When modifying the pod name, metadata.apps.label should also have been changed', async () => {
await waitRender({});
Expand Down
14 changes: 11 additions & 3 deletions packages/renderer/src/lib/pod/DeployPodToKube.svelte
Expand Up @@ -11,6 +11,7 @@ import { ensureRestrictedSecurityContext } from '/@/lib/pod/pod-utils';
export let resourceId: string;
export let engineId: string;
export let type: string;
let kubeDetails: string;
Expand Down Expand Up @@ -38,9 +39,16 @@ let containerPortArray: string[] = [];
let createdRoutes: V1Route[] = [];
onMount(async () => {
// grab kube result from the pod
const kubeResult = await window.generatePodmanKube(engineId, [resourceId]);
kubeDetails = kubeResult;
// If type = compose
// we will grab the containers by using the label com.docker.compose.project that matches the resourceId
// we can then pass the array of containers to generatePodmanKube rather than the singular pod id
if (type === 'compose') {
const containers = await window.listSimpleContainersByLabel('com.docker.compose.project', resourceId);
const containerIds = containers.map(container => container.Id);
kubeDetails = await window.generatePodmanKube(engineId, containerIds);
} else {
kubeDetails = await window.generatePodmanKube(engineId, [resourceId]);
}
// parse yaml
bodyPod = jsYaml.load(kubeDetails) as any;
Expand Down

0 comments on commit 9fed2ad

Please sign in to comment.