Skip to content

Commit

Permalink
Merge branch 'main' into issue-1985-kind-blog-post
Browse files Browse the repository at this point in the history
  • Loading branch information
themr0c committed May 2, 2023
2 parents b61d27a + ade6f98 commit 3f9808b
Show file tree
Hide file tree
Showing 36 changed files with 1,109 additions and 193 deletions.
13 changes: 12 additions & 1 deletion packages/main/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import { AutostartEngine } from './autostart-engine';
import { CloseBehavior } from './close-behavior';
import { TrayIconColor } from './tray-icon-color';
import { KubernetesClient } from './kubernetes-client';
import type { V1Pod, V1ConfigMap, V1NamespaceList, V1PodList, V1Service } from '@kubernetes/client-node';
import type { V1Pod, V1ConfigMap, V1NamespaceList, V1PodList, V1Service, V1Ingress } from '@kubernetes/client-node';
import type { V1Route } from './api/openshift-types';
import type { NetworkInspectInfo } from './api/network-info';
import { FilesystemMonitoring } from './filesystem-monitoring';
Expand Down Expand Up @@ -1343,6 +1343,13 @@ export class PluginSystem {
},
);

this.ipcHandle(
'kubernetes-client:createIngress',
async (_listener, namespace: string, ingress: V1Ingress): Promise<V1Ingress> => {
return kubernetesClient.createIngress(namespace, ingress);
},
);

this.ipcHandle('kubernetes-client:listPods', async (): Promise<PodInfo[]> => {
return kubernetesClient.listPods();
});
Expand Down Expand Up @@ -1436,6 +1443,10 @@ export class PluginSystem {
return telemetry.configureTelemetry();
});

this.ipcHandle('app:getVersion', async (): Promise<string> => {
return app.getVersion();
});

const dockerDesktopInstallation = new DockerDesktopInstallation(
apiSender,
containerProviderRegistry,
Expand Down
15 changes: 15 additions & 0 deletions packages/main/src/plugin/kubernetes-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ beforeAll(() => {
CoreV1Api: {},
AppsV1Api: {},
CustomObjectsApi: {},
NetworkingV1Api: {},
};
});
});
Expand Down Expand Up @@ -67,6 +68,20 @@ test('Create Kubernetes resources with apps/v1 resource should return ok', async
expect(createNamespacedDeploymentMock).toBeCalledWith('default', { apiVersion: 'apps/v1', kind: 'Deployment' });
});

test('Create Kubernetes resources with networking.k8s.io/v1 resource should return ok', async () => {
const client = new KubernetesClient({} as ApiSenderType, configurationRegistry, fileSystemMonitoring);
const createNamespacedIngressMock = vi.fn();
makeApiClientMock.mockReturnValue({
createNamespacedIngress: createNamespacedIngressMock,
});

await client.createResources('dummy', [{ apiVersion: 'networking.k8s.io/v1', kind: 'Ingress' }]);
expect(createNamespacedIngressMock).toBeCalledWith('default', {
apiVersion: 'networking.k8s.io/v1',
kind: 'Ingress',
});
});

test('Create Kubernetes resources with v1 resource in error should return error', async () => {
const client = new KubernetesClient({} as ApiSenderType, configurationRegistry, fileSystemMonitoring);
const spy = vi.spyOn(client, 'createV1Resource').mockRejectedValue(new Error('V1Error'));
Expand Down
26 changes: 24 additions & 2 deletions packages/main/src/plugin/kubernetes-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ import type {
V1PodList,
V1NamespaceList,
V1Service,
V1Ingress,
V1ContainerState,
V1APIResource,
} from '@kubernetes/client-node';
import { AppsV1Api, CustomObjectsApi, CoreV1Api, KubeConfig, Log, Watch } from '@kubernetes/client-node';
import type { V1APIResource } from '@kubernetes/client-node';
import { NetworkingV1Api } from '@kubernetes/client-node';
import { AppsV1Api } from '@kubernetes/client-node';
import { CustomObjectsApi } from '@kubernetes/client-node';
import { CoreV1Api, KubeConfig, Log, Watch } from '@kubernetes/client-node';
import type { V1Route } from './api/openshift-types';
import type * as containerDesktopAPI from '@podman-desktop/api';
import { Emitter } from './events/emitter';
Expand Down Expand Up @@ -312,6 +316,17 @@ export class KubernetesClient {
}
}

async createIngress(namespace: string, body: V1Ingress): Promise<V1Ingress> {
const k8sCoreApi = this.kubeConfig.makeApiClient(NetworkingV1Api);

try {
const createdIngressData = await k8sCoreApi.createNamespacedIngress(namespace, body);
return createdIngressData.body;
} catch (error) {
throw this.wrapK8sClientError(error);
}
}

async createOpenShiftRoute(namespace: string, body: V1Route): Promise<V1Route> {
const k8sCustomObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi);

Expand Down Expand Up @@ -635,6 +650,13 @@ export class KubernetesClient {
} else if (manifest.kind === 'DaemonSet') {
await k8sAppsApi.createNamespacedDaemonSet(namespaceToUse, manifest);
}
} else if (groupVersion.group === 'networking.k8s.io') {
// Add networking object support (Ingress for now)
const k8sNetworkingApi = this.kubeConfig.makeApiClient(NetworkingV1Api);
const namespaceToUse = optionalNamespace || manifest.metadata?.namespace || 'default';
if (manifest.kind === 'Ingress') {
await k8sNetworkingApi.createNamespacedIngress(namespaceToUse, manifest);
}
} else {
const client = ctx.makeApiClient(CustomObjectsApi);
await this.createCustomResource(
Expand Down
13 changes: 12 additions & 1 deletion packages/preload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import type {
PodCreateOptions,
ContainerCreateOptions as PodmanContainerCreateOptions,
} from '../../main/src/plugin/dockerode/libpod-dockerode';
import type { V1ConfigMap, V1NamespaceList, V1Pod, V1PodList, V1Service } from '@kubernetes/client-node';
import type { V1ConfigMap, V1Ingress, V1NamespaceList, V1Pod, V1PodList, V1Service } from '@kubernetes/client-node';
import type { Menu } from '../../main/src/plugin/menu-registry';
import type { MessageBoxOptions, MessageBoxReturnValue } from '../../main/src/plugin/message-box';

Expand Down Expand Up @@ -1125,6 +1125,13 @@ function initExposure(): void {
},
);

contextBridge.exposeInMainWorld(
'kubernetesCreateIngress',
async (namespace: string, ingress: V1Ingress): Promise<V1Ingress> => {
return ipcInvoke('kubernetes-client:createIngress', namespace, ingress);
},
);

contextBridge.exposeInMainWorld('kubernetesListPods', async (): Promise<PodInfo[]> => {
return ipcInvoke('kubernetes-client:listPods');
});
Expand Down Expand Up @@ -1282,6 +1289,10 @@ function initExposure(): void {
resolveCallback();
}
});

contextBridge.exposeInMainWorld('getPodmanDesktopVersion', async (): Promise<string> => {
return ipcInvoke('app:getVersion');
});
}

// expose methods
Expand Down
1 change: 1 addition & 0 deletions packages/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"devDependencies": {
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@patternfly/patternfly": "^4.224.2",
"@sveltejs/vite-plugin-svelte": "^2.1.1",
"@testing-library/jest-dom": "^5.16.5",
Expand Down
4 changes: 3 additions & 1 deletion packages/renderer/src/TelemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export class TelemetryService {
}

this.handlerFlusher = setTimeout(() => {
window.telemetryPage(pagePath);
if (window.telemetryPage) {
window.telemetryPage(pagePath);
}
}, 200);
}
}
2 changes: 1 addition & 1 deletion packages/renderer/src/lib/ContainerList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function createPodFromContainers() {
const podCreation = {
name: 'my-pod',
containers: selectedContainers.map(container => {
return { id: container.id, name: container.name, engineId: container.engineId, ports: container.port };
return { id: container.id, name: container.name, engineId: container.engineId, ports: container.ports };
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export let container: ContainerInfoUI;
<tr>
<td class="pt-2 pr-2">Ports</td>
<td class="pt-2 pr-2" class:hidden="{container.hasPublicPort}">N/A</td>
<td class="pt-2 pr-2" class:hidden="{!container.hasPublicPort}">{container.port}</td>
<td class="pt-2 pr-2" class:hidden="{!container.hasPublicPort}">{container.portsAsString}</td>
</tr>
</table>
</div>
Expand Down
3 changes: 2 additions & 1 deletion packages/renderer/src/lib/container/ContainerInfoUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export interface ContainerInfoUI {
state: string;
uptime: string;
startedAt: string;
port: string;
ports: number[];
portsAsString: string;
displayPort: string;
command: string;
hasPublicPort: boolean;
Expand Down
32 changes: 18 additions & 14 deletions packages/renderer/src/lib/container/container-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,16 @@ export class ContainerUtils {
return image;
}

getPort(containerInfo: ContainerInfo): string {
const ports = containerInfo.Ports?.filter(port => port.PublicPort).map(port => port.PublicPort);

if (ports && ports.length > 1) {
return ports.join(', ');
} else if (ports && ports.length === 1) {
return `${ports[0]}`;
} else {
return '';
}
getPorts(containerInfo: ContainerInfo): number[] {
return containerInfo.Ports?.filter(port => port.PublicPort).map(port => port.PublicPort) || [];
}

getDisplayPort(containerInfo: ContainerInfo): string {
const ports = this.getPort(containerInfo);
if (ports === '') {
const ports = this.getPorts(containerInfo);
if (ports.length === 0) {
return '';
}
return `PORT${ports.indexOf(',') > 0 ? 'S' : ''} ${ports}`;
return `PORT${ports.length > 1 ? 'S' : ''} ${ports}`;
}

hasPublicPort(containerInfo: ContainerInfo): boolean {
Expand Down Expand Up @@ -134,7 +126,8 @@ export class ContainerUtils {
engineName: this.getEngineName(containerInfo),
engineType: containerInfo.engineType,
command: containerInfo.Command,
port: this.getPort(containerInfo),
ports: this.getPorts(containerInfo),
portsAsString: this.getPortsAsString(containerInfo),
displayPort: this.getDisplayPort(containerInfo),
hasPublicPort: this.hasPublicPort(containerInfo),
openingUrl: this.getOpeningUrl(containerInfo),
Expand Down Expand Up @@ -205,4 +198,15 @@ export class ContainerUtils {
getMemoryUsageTitle(usedMemory: number): string {
return `${filesize(usedMemory)}`;
}

getPortsAsString(containerInfo: ContainerInfo): string {
const ports = containerInfo.Ports;
if (ports.length > 1) {
return `${ports.join(', ')}`;
} else if (ports.length === 1) {
return `${ports[0]}`;
} else {
return '';
}
}
}
33 changes: 17 additions & 16 deletions packages/renderer/src/lib/dialogs/MessageBox.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
<script lang="ts">
import { onDestroy, onMount, tick } from 'svelte';
import Fa from 'svelte-fa/src/fa.svelte';
import {
faCircleExclamation,
faTriangleExclamation,
faCircleInfo,
faCircleQuestion,
} from '@fortawesome/free-solid-svg-icons';
import { faCircleQuestion, faCircle } from '@fortawesome/free-regular-svg-icons';
import { faCircleExclamation, faInfo, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
import type { MessageBoxOptions } from './messagebox-input';
let currentId = 0;
Expand Down Expand Up @@ -114,32 +110,37 @@ function handleKeydown(e: KeyboardEvent) {

{#if display}
<!-- Create overlay-->
<div class="fixed top-0 left-0 right-0 bottom-0 bg-black bg-opacity-60 h-full grid z-50">
<div class="flex flex-col place-self-center w-[550px] rounded-xl bg-zinc-900 shadow-xl shadow-black">
<div class="flex items-center justify-between px-6 py-5 space-x-2">
<div class="fixed top-0 left-0 right-0 bottom-0 bg-black bg-opacity-60 bg-blend-multiply h-full grid z-50">
<div class="flex flex-col place-self-center w-[550px] rounded-xl bg-charcoal-800 shadow-xl shadow-black">
<div class="flex items-center justify-between pl-4 pr-3 py-3 space-x-2 text-gray-400">
{#if type === 'error'}
<Fa class="h-4 w-4" icon="{faCircleExclamation}" />
<Fa class="h-4 w-4 text-red-500" icon="{faCircleExclamation}" />
{:else if type === 'warning'}
<Fa class="h-4 w-4" icon="{faTriangleExclamation}" />
<Fa class="h-4 w-4 text-amber-400" icon="{faTriangleExclamation}" />
{:else if type === 'info'}
<Fa class="h-4 w-4" icon="{faCircleInfo}" />
<div class="flex">
<Fa class="h-4 w-4 place-content-center" icon="{faCircle}" />
<Fa class="h-4 w-4 place-content-center -ml-4 mt-px text-xs" icon="{faInfo}" />
</div>
{:else if type === 'question'}
<Fa class="h-4 w-4" icon="{faCircleQuestion}" />
{/if}
<h1 class="grow text-lg font-bold capitalize">{title}</h1>

<button class="hover:text-gray-300 py-1" on:click="{() => clickButton(cancelId >= 0 ? cancelId : undefined)}">
<button
class="p-2 hover:text-gray-300 hover:bg-charcoal-500 rounded-full cursor-pointer"
on:click="{() => clickButton(cancelId >= 0 ? cancelId : undefined)}">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>

<div class="px-10 py-4 text-sm leading-5">{message}</div>
<div class="px-10 py-4 text-sm text-gray-500 leading-5">{message}</div>

{#if detail}
<div class="px-10 pb-4 text-sm leading-5">{detail}</div>
<div class="px-10 pb-4 text-sm text-gray-500 leading-5">{detail}</div>
{/if}

<div class="px-6 py-5 mt-2 flex flex-row w-full justify-end space-x-5">
<div class="px-5 py-5 mt-2 flex flex-row w-full justify-end space-x-5">
{#each buttonOrder as i}
{#if i == cancelId}
<button aria-label="Cancel" class="text-xs hover:underline" on:click="{() => clickButton(i)}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

/* eslint-disable @typescript-eslint/no-explicit-any */

import '@testing-library/jest-dom';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import PreferencesPageDockerExtensions from './PreferencesPageDockerExtensions.svelte';

// fake the window.events object
beforeAll(() => {
(window.events as unknown) = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
receive: (_channel: string, func: any) => {
func();
},
};
});

const buttonText = 'Install extension from the OCI image';

describe('PreferencesPageDockerExtensions', () => {
test('Expect that textbox is available and button is displayed', async () => {
render(PreferencesPageDockerExtensions, {});

const input = screen.getByRole('textbox', { name: 'Image name:' });
expect(input).toBeInTheDocument();
const button = screen.getByRole('button', { name: buttonText });
expect(button).toBeInTheDocument();
expect(button).toBeDisabled();
});

test('Expect that whitespace does not enable button', async () => {
render(PreferencesPageDockerExtensions, { ociImage: ' ' });

const button = screen.getByRole('button', { name: buttonText });
expect(button).toBeInTheDocument();
expect(button).toBeDisabled();
});

test('Expect that valid entry enables button', async () => {
render(PreferencesPageDockerExtensions, { ociImage: 'some-valid-image-name' });

const button = screen.getByRole('button', { name: buttonText });
expect(button).toBeInTheDocument();
expect(button).toBeEnabled();
});
});

0 comments on commit 3f9808b

Please sign in to comment.