From b988969ff043e74c2659cdec156c6f667d2c57ea Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Fri, 16 Dec 2022 10:33:27 -0500 Subject: [PATCH] feat: add warnings and info to provider, add warn re. docker socket ### What does this PR do? * Reworks the plugin and extension API's to provide the ability to pass "up" information and warnings about the provider * Adds a warning check for Podman to see if the docker socket is actually Podman in disguise and to warn the user ### Screenshot/screencast of this PR ### What issues does this PR fix or reference? Fixes https://github.com/containers/podman-desktop/issues/905 Fixes https://github.com/containers/podman-desktop/issues/758 ### How to test this PR? If you have podman and docker installed: 1. `yarn watch` 2. Start Podman Desktop 3. Start Docker Desktop (Podman Desktop should now warn you that the socket is not being used by Podman) 4. Quit Docker Desktop 5. `podman machine stop && podman machine start` or restart via UI on Podman Desktop in order for `podman machine` to re-enable using the /var/run/docker.sock 6. Warning should now clear If only using podman: 1. `yarn watch` 2. Use the podman provider 3. `sudo rm -f /var/run/docker.sock` or mv 4. Warning should appear that cannot find the docker socket Signed-off-by: Charlie Drage --- extensions/podman/src/extension.ts | 36 +++++- extensions/podman/src/information.ts | 38 ++++++ extensions/podman/src/warning.ts | 109 ++++++++++++++++++ packages/extension-api/src/extension-api.d.ts | 20 ++++ packages/main/src/plugin/api/provider-info.ts | 5 + packages/main/src/plugin/provider-impl.ts | 31 +++++ packages/main/src/plugin/provider-registry.ts | 17 +++ packages/main/src/tray-menu.ts | 2 + .../src/lib/welcome/ProviderConfigured.svelte | 1 + .../lib/welcome/ProviderInformation.svelte | 35 ++++++ .../src/lib/welcome/ProviderReady.svelte | 2 + packages/renderer/src/stores/providers.ts | 6 + website/docs/troubleshooting.md | 34 ++++++ 13 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 extensions/podman/src/information.ts create mode 100644 extensions/podman/src/warning.ts create mode 100644 packages/renderer/src/lib/welcome/ProviderInformation.svelte diff --git a/extensions/podman/src/extension.ts b/extensions/podman/src/extension.ts index 6c378317b477d..e6482d255aba1 100644 --- a/extensions/podman/src/extension.ts +++ b/extensions/podman/src/extension.ts @@ -29,6 +29,8 @@ import type { InstalledPodman } from './podman-cli'; import { execPromise, getPodmanCli, getPodmanInstallation } from './podman-cli'; import { PodmanConfiguration } from './podman-configuration'; import { getDetectionChecks } from './detection-checks'; +import { getInformation } from './information'; +import { getWarning } from './warning'; type StatusHandler = (name: string, event: extensionApi.ProviderConnectionStatus) => void; @@ -215,6 +217,36 @@ async function monitorProvider(provider: extensionApi.Provider) { provider.updateStatus('installed'); } } + + // Retrieve the newest information + warnings, but only update if there is a change + const retrievedInformation = await getInformation(); + const retrievedWarnings = await getWarning(); + + // Only send an update if the length of the array is different, and if it's not, + // we will check the array to see if any content is different. + if (retrievedInformation.length !== provider.info.length) { + provider.updateInfo(retrievedInformation); + } else { + for (let i = 0; i < retrievedInformation.length; i++) { + if (retrievedInformation[i].details !== provider.info[i].details) { + provider.updateInfo(retrievedInformation); + break; + } + } + } + + // Only send an update if the length of the array is different, and if it's not, + // we will check the array to see if any content is different. + if (retrievedWarnings.length !== provider.warning.length) { + provider.updateWarning(retrievedWarnings); + } else { + for (let i = 0; i < retrievedWarnings.length; i++) { + if (retrievedWarnings[i].details !== provider.warning[i].details) { + provider.updateWarning(retrievedWarnings); + break; + } + } + } } catch (error) { // ignore the update } @@ -312,6 +344,8 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): // update detection checks detectionChecks.push(...getDetectionChecks(installedPodman)); + // Pass information here with regards to warnings / info messages with regards to docker socket + // information const providerOptions: extensionApi.ProviderOptions = { name: 'Podman', id: 'podman', @@ -497,7 +531,7 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): } // monitor provider - // like version, checks + // like version, checks, information and warnings monitorProvider(provider); // register the registries diff --git a/extensions/podman/src/information.ts b/extensions/podman/src/information.ts new file mode 100644 index 0000000000000..ada18aee8b275 --- /dev/null +++ b/extensions/podman/src/information.ts @@ -0,0 +1,38 @@ +/********************************************************************** + * Copyright (C) 2022 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 + ***********************************************************************/ + +import type * as extensionApi from '@tmpwip/extension-api'; + +// This function gets any "essential" information with regards to the Podman plugin that the user should know. +// For example: the Podman version, the Podman API version, how many containers, etc. +// With this information, you can help the user to understand what is going on with the plugin. +export async function getInformation(): Promise { + const infos: extensionApi.ProviderInformation[] = []; + + /* Test information + // TO REMOVE: + // Purposely pass in test information to the provider. + const testInfo: extensionApi.ProviderInformation = { + name: 'Status', + details: 'Current status of the provider', + }; + infos.push(testInfo); + */ + + return infos; +} diff --git a/extensions/podman/src/warning.ts b/extensions/podman/src/warning.ts new file mode 100644 index 0000000000000..e05ac4235bc44 --- /dev/null +++ b/extensions/podman/src/warning.ts @@ -0,0 +1,109 @@ +/********************************************************************** + * Copyright (C) 2022 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 + ***********************************************************************/ + +import type * as extensionApi from '@tmpwip/extension-api'; +import * as os from 'node:os'; +import * as http from 'node:http'; + +// Explanations +const detailsExplanation = 'Podman is not emulating the default Docker socket path: '; +const detailsNotWorking = '. Docker-specific tools may not work.'; + +// Default socket paths +const windowsSocketPath = '//./pipe/docker_engine'; +const defaultSocketPath = '/var/run/docker.sock'; + +// Passes any warnings with regards to the plugin to the user. +export async function getWarning(): Promise { + console.log('getWarning() called'); + const warnings: extensionApi.ProviderInformation[] = []; + + // FIRST CHECK + // Check to see if the user is running the disguised Podman socket and warn if not + const isPodman = await isDisguisedPodman(getSocketPath()); + if (!isPodman) { + let details: string; + + // Set the details message based on the OS + switch (os.platform()) { + case 'win32': + details = detailsExplanation.concat(windowsSocketPath, detailsNotWorking); + break; + case 'darwin': + // Due to how `podman-mac-helper` does not (by default) map the emulator to /var/run/docker.sock, we need to explain + // that the user must go on the Podman Desktop website for more information. This is because the user must manually + // map the socket to /var/run/docker.sock if not done by `podman machine` already (podman machine automatically maps the socket if Docker is not running) + details = detailsExplanation.concat( + defaultSocketPath, + detailsNotWorking, + ' See troubleshooting page on podman-desktop.io for more information.', + ); + break; + default: + details = detailsExplanation.concat(defaultSocketPath, detailsNotWorking); + break; + } + + // Create the message and push it to the array + const podmanWarn: extensionApi.ProviderInformation = { + name: 'Docker Socket Compatibility', + details: details, + }; + warnings.push(podmanWarn); + } + + return warnings; +} + +// Async function that checks to see if the current Docker socket is a disguised Podman socket +export async function isDisguisedPodman(socketPath: string): Promise { + const podmanPingUrl = { + path: '/libpod/_ping', + socketPath, + }; + return new Promise(resolve => { + const req = http.get(podmanPingUrl, res => { + res.on('data', () => { + // do nothing + }); + + res.on('end', () => { + if (res.statusCode === 200) { + resolve(true); + } else { + resolve(false); + } + }); + }); + + req.once('error', err => { + console.debug('Error while pinging docker as podman', err); + resolve(false); + }); + }); +} + +// Function that checks whether you are running windows, mac or linux and returns back +// the correct Docker socket location +export function getSocketPath(): string { + let socketPath: string = defaultSocketPath; + if (os.platform() === 'win32') { + socketPath = windowsSocketPath; + } + return socketPath; +} diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index c67b0d0923eec..7771b7735abf4 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -132,6 +132,13 @@ declare module '@tmpwip/extension-api' { status(): ProviderStatus; } + // For displaying essential information to the user + // "name" of the warning / title and a "details" field for more information + export interface ProviderInformation { + name: string; + details?: string; + } + export interface ProviderDetectionCheck { name: string; details?: string; @@ -146,6 +153,11 @@ declare module '@tmpwip/extension-api' { images?: ProviderImages; links?: ProviderLinks[]; detectionChecks?: ProviderDetectionCheck[]; + + // Provide way to add additional information to the provider + // in the form of information and warnings + info?: ProviderInformation[]; + warning?: ProviderInformation[]; } export type ProviderConnectionStatus = 'started' | 'stopped' | 'starting' | 'stopping' | 'unknown'; @@ -295,6 +307,14 @@ declare module '@tmpwip/extension-api' { // it may happen after an update or an installation updateDetectionChecks(detectionChecks: ProviderDetectionCheck[]): void; + // update need-to-know information about the provider + readonly info: ProviderInformation[]; + updateInfo(info: ProviderInformation[]): void; + + // update warning information for the provider + readonly warning: ProviderInformation[]; + updateWarning(warning: ProviderInformation[]): void; + // notify that detection checks have changed onDidUpdateDetectionChecks: Event; } diff --git a/packages/main/src/plugin/api/provider-info.ts b/packages/main/src/plugin/api/provider-info.ts index c02fd956b016c..724bfb4d4a2dd 100644 --- a/packages/main/src/plugin/api/provider-info.ts +++ b/packages/main/src/plugin/api/provider-info.ts @@ -23,6 +23,7 @@ import type { ProviderLinks, ProviderStatus, Link, + ProviderInformation, } from '@tmpwip/extension-api'; export type LifecycleMethod = 'start' | 'stop' | 'delete'; @@ -65,6 +66,10 @@ export interface ProviderInfo { links: ProviderLinks[]; detectionChecks: ProviderDetectionCheck[]; + // Need-to-know information and warning messages regarding the provider + info: ProviderInformation[]; + warning: ProviderInformation[]; + images: ProviderImages; // can install a provider diff --git a/packages/main/src/plugin/provider-impl.ts b/packages/main/src/plugin/provider-impl.ts index ede246f34c32b..2451c34018f84 100644 --- a/packages/main/src/plugin/provider-impl.ts +++ b/packages/main/src/plugin/provider-impl.ts @@ -37,6 +37,7 @@ import type { ProviderUpdate, ProviderAutostart, KubernetesProviderConnectionFactory, + ProviderInformation, } from '@tmpwip/extension-api'; import type { ProviderRegistry } from './provider-registry'; import { Emitter } from './events/emitter'; @@ -65,6 +66,14 @@ export class ProviderImpl implements Provider, IDisposable { private readonly _onDidUpdateDetectionChecks = new Emitter(); readonly onDidUpdateDetectionChecks: Event = this._onDidUpdateDetectionChecks.event; + private _info: ProviderInformation[]; + private readonly _onDidUpdateInfo = new Emitter(); + readonly onDidUpdateInfo: Event = this._onDidUpdateInfo.event; + + private _warning: ProviderInformation[]; + private readonly _onDidUpdateWarning = new Emitter(); + readonly onDidUpdateWarning: Event = this._onDidUpdateWarning.event; + constructor( private _internalId: string, private providerOptions: ProviderOptions, @@ -80,6 +89,8 @@ export class ProviderImpl implements Provider, IDisposable { this._links = providerOptions.links || []; this._detectionChecks = providerOptions.detectionChecks || []; this._images = providerOptions.images || {}; + this._info = providerOptions.info || []; + this._warning = providerOptions.warning || []; // monitor connection statuses setInterval(async () => { @@ -116,6 +127,14 @@ export class ProviderImpl implements Provider, IDisposable { return this._detectionChecks; } + get info(): ProviderInformation[] { + return this._info; + } + + get warning(): ProviderInformation[] { + return this._warning; + } + get images(): ProviderImages { return this._images; } @@ -139,6 +158,18 @@ export class ProviderImpl implements Provider, IDisposable { this._onDidUpdateDetectionChecks.fire(detectionChecks); } + // Update the information + updateInfo(info: ProviderInformation[]): void { + this._info = info; + this._onDidUpdateInfo.fire(info); + } + + // Update the warnings + updateWarning(warning: ProviderInformation[]): void { + this._warning = warning; + this._onDidUpdateWarning.fire(warning); + } + get status(): ProviderStatus { return this._status; } diff --git a/packages/main/src/plugin/provider-registry.ts b/packages/main/src/plugin/provider-registry.ts index c66d8874dd157..ae2eddc7e6197 100644 --- a/packages/main/src/plugin/provider-registry.ts +++ b/packages/main/src/plugin/provider-registry.ts @@ -106,18 +106,33 @@ export class ProviderRegistry { this.lifecycleListeners = []; this.containerConnectionLifecycleListeners = []; + // Every 2 seconds, we will check: + // * The status of the providers + // * Any new warnings or informations for each provider setInterval(async () => { Array.from(this.providers.keys()).forEach(providerKey => { + // Get the provider and its lifecycle const provider = this.providers.get(providerKey); const providerLifecycle = this.providerLifecycles.get(providerKey); + + // If the provider and its lifecycle exist, we will check if (provider && providerLifecycle) { + // Get the status const status = providerLifecycle.status(); + + // If the status does not match the current one, we will send a listener event and update the status if (status !== this.providerStatuses.get(providerKey)) { provider.updateStatus(status); this.listeners.forEach(listener => listener('provider:update-status', this.getProviderInfo(provider))); this.providerStatuses.set(providerKey, status); } } + + // Let the renderer know to check for new warnings and information + if (provider) { + this.apiSender.send('provider:update-warning', provider.id); + this.apiSender.send('provider:update-info', provider.id); + } }); }, 2000); } @@ -476,6 +491,8 @@ export class ProviderRegistry { detectionChecks: provider.detectionChecks, images: provider.images, version: provider.version, + info: provider.info, + warning: provider.warning, installationSupport, }; diff --git a/packages/main/src/tray-menu.ts b/packages/main/src/tray-menu.ts index 313328c4c6eba..63d70e69afd7f 100644 --- a/packages/main/src/tray-menu.ts +++ b/packages/main/src/tray-menu.ts @@ -78,6 +78,8 @@ export class TrayMenu { version: '', links: [], images: {}, + info: [], + warning: [], installationSupport: false, containerProviderConnectionCreation: false, kubernetesProviderConnectionCreation: false, diff --git a/packages/renderer/src/lib/welcome/ProviderConfigured.svelte b/packages/renderer/src/lib/welcome/ProviderConfigured.svelte index a93a0be17a48a..95f51cd3fdb9e 100644 --- a/packages/renderer/src/lib/welcome/ProviderConfigured.svelte +++ b/packages/renderer/src/lib/welcome/ProviderConfigured.svelte @@ -1,6 +1,7 @@ + +
+ {#if providerInfo?.info?.length > 0} + {#each providerInfo.info as info} +
+ ℹ️ + {info.name}: + {info.details} +
+ {/each} + {/if} + + {#if providerInfo?.warning?.length > 0} + {#each providerInfo.warning as warn} +
+ ⚠️ + {warn.name}: + {warn.details} +
+ {/each} + {/if} +
diff --git a/packages/renderer/src/lib/welcome/ProviderReady.svelte b/packages/renderer/src/lib/welcome/ProviderReady.svelte index dfc52148bce99..060be8e9accab 100644 --- a/packages/renderer/src/lib/welcome/ProviderReady.svelte +++ b/packages/renderer/src/lib/welcome/ProviderReady.svelte @@ -1,6 +1,7 @@