mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-05-24 10:18:53 +00:00
feat: add warnings to provider, add warn re. docker socket (#1047)
* feat: add warnings 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" 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 <!-- 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). --> 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 <!-- Please explain steps to reproduce --> Signed-off-by: Charlie Drage <charlie@charliedrage.com> * chore: watch the directory instead of the file Change-Id: I81a5ae01dbc4fb37fb652a3e3ee678364bb46625 Signed-off-by: Florent Benoit <fbenoit@redhat.com> * use warnings not warning in variable names Signed-off-by: Charlie Drage <charlie@charliedrage.com> * remove console.log debug output Signed-off-by: Charlie Drage <charlie@charliedrage.com> * json stringify and compare the cache vs new update before pushing Signed-off-by: Charlie Drage <charlie@charliedrage.com> * add listener Signed-off-by: Charlie Drage <charlie@charliedrage.com> * update Signed-off-by: Charlie Drage <charlie@charliedrage.com> Signed-off-by: Charlie Drage <charlie@charliedrage.com> Signed-off-by: Florent Benoit <fbenoit@redhat.com> Co-authored-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
parent
2713059ec7
commit
0c027e5356
11 changed files with 290 additions and 1 deletions
|
|
@ -29,6 +29,7 @@ import type { InstalledPodman } from './podman-cli';
|
|||
import { execPromise, getPodmanCli, getPodmanInstallation } from './podman-cli';
|
||||
import { PodmanConfiguration } from './podman-configuration';
|
||||
import { getDetectionChecks } from './detection-checks';
|
||||
import { getDisguisedPodmanInformation, getSocketPath, isDisguisedPodman } from './warnings';
|
||||
|
||||
type StatusHandler = (name: string, event: extensionApi.ProviderConnectionStatus) => void;
|
||||
|
||||
|
|
@ -42,6 +43,11 @@ const podmanMachinesStatuses = new Map<string, extensionApi.ProviderConnectionSt
|
|||
const podmanMachinesInfo = new Map<string, MachineInfo>();
|
||||
const currentConnections = new Map<string, extensionApi.Disposable>();
|
||||
|
||||
// Warning to check to see if the socket is a disguised Podman socket,
|
||||
// by default we assume it is until proven otherwise when we check
|
||||
let isDisguisedPodmanSocket = true;
|
||||
let disguisedPodmanSocketWatcher: extensionApi.FileSystemWatcher;
|
||||
|
||||
type MachineJSON = {
|
||||
Name: string;
|
||||
CPUs: number;
|
||||
|
|
@ -343,6 +349,13 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
|
|||
];
|
||||
|
||||
const provider = extensionApi.provider.createProvider(providerOptions);
|
||||
|
||||
// Check on initial setup
|
||||
checkDisguisedPodmanSocket(provider);
|
||||
// update the status of the provider if the socket is changed, created or deleted
|
||||
disguisedPodmanSocketWatcher = setupDisguisedPodmanSocketWatcher(provider, getSocketPath());
|
||||
extensionContext.subscriptions.push(disguisedPodmanSocketWatcher);
|
||||
|
||||
// provide an installation path ?
|
||||
if (podmanInstall.isAbleToInstall()) {
|
||||
provider.registerInstallation({
|
||||
|
|
@ -497,7 +510,7 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
|
|||
}
|
||||
|
||||
// monitor provider
|
||||
// like version, checks
|
||||
// like version, checks, warnings
|
||||
monitorProvider(provider);
|
||||
|
||||
// register the registries
|
||||
|
|
@ -512,3 +525,55 @@ export function deactivate(): void {
|
|||
stopLoop = true;
|
||||
console.log('stopping podman extension');
|
||||
}
|
||||
|
||||
function setupDisguisedPodmanSocketWatcher(
|
||||
provider: extensionApi.Provider,
|
||||
socketFile: string,
|
||||
): extensionApi.FileSystemWatcher {
|
||||
// Monitor the socket file for any changes, creation or deletion
|
||||
// and trigger a change if that happens
|
||||
|
||||
// Add the check to the listeners as well to make sure we check on podman status change as well
|
||||
listeners.add(() => {
|
||||
checkDisguisedPodmanSocket(provider);
|
||||
});
|
||||
|
||||
// watch parent directory
|
||||
const socketWatcher = extensionApi.fs.createFileSystemWatcher(path.dirname(socketFile));
|
||||
|
||||
// only trigger if the watched file is the socket file
|
||||
const updateSocket = (uri: extensionApi.Uri) => {
|
||||
if (uri.fsPath === socketFile) {
|
||||
checkDisguisedPodmanSocket(provider);
|
||||
}
|
||||
};
|
||||
|
||||
socketWatcher.onDidChange(uri => {
|
||||
updateSocket(uri);
|
||||
});
|
||||
|
||||
socketWatcher.onDidCreate(uri => {
|
||||
updateSocket(uri);
|
||||
});
|
||||
|
||||
socketWatcher.onDidDelete(uri => {
|
||||
updateSocket(uri);
|
||||
});
|
||||
|
||||
return socketWatcher;
|
||||
}
|
||||
|
||||
async function checkDisguisedPodmanSocket(provider: extensionApi.Provider) {
|
||||
// Check to see if the socket is disguised or not. If it is, we'll push a warning up
|
||||
// to the plugin library to the let the provider know that there is a warning
|
||||
const disguisedCheck = await isDisguisedPodman();
|
||||
if (isDisguisedPodmanSocket !== disguisedCheck) {
|
||||
isDisguisedPodmanSocket = disguisedCheck;
|
||||
}
|
||||
|
||||
// If isDisguisedPodmanSocket is true, we'll push a warning up to the plugin library with getDisguisedPodmanWarning()
|
||||
// If isDisguisedPodmanSocket is false, we'll push an empty array up to the plugin library to clear the warning
|
||||
// as we have no other warnings to display (or implemented)
|
||||
const retrievedWarnings = isDisguisedPodmanSocket ? [] : [getDisguisedPodmanInformation()];
|
||||
provider.updateWarnings(retrievedWarnings);
|
||||
}
|
||||
|
|
|
|||
101
extensions/podman/src/warnings.ts
Normal file
101
extensions/podman/src/warnings.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**********************************************************************
|
||||
* 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';
|
||||
|
||||
// Return the warning information to the user if the socket is not a disguised Podman socket
|
||||
export function getDisguisedPodmanInformation(): extensionApi.ProviderInformation {
|
||||
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;
|
||||
}
|
||||
|
||||
// Return ProviderInformation with the details message
|
||||
return {
|
||||
name: 'Docker Socket Compatibility',
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
// Async function that checks to see if the current Docker socket is a disguised Podman socket
|
||||
export async function isDisguisedPodman(): Promise<boolean> {
|
||||
const socketPath = getSocketPath();
|
||||
|
||||
const podmanPingUrl = {
|
||||
path: '/libpod/_ping',
|
||||
socketPath,
|
||||
};
|
||||
|
||||
return new Promise<boolean>(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;
|
||||
}
|
||||
14
packages/extension-api/src/extension-api.d.ts
vendored
14
packages/extension-api/src/extension-api.d.ts
vendored
|
|
@ -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,9 @@ declare module '@tmpwip/extension-api' {
|
|||
images?: ProviderImages;
|
||||
links?: ProviderLinks[];
|
||||
detectionChecks?: ProviderDetectionCheck[];
|
||||
|
||||
// Provide way to add additional warnings to the provider
|
||||
warnings?: ProviderInformation[];
|
||||
}
|
||||
|
||||
export type ProviderConnectionStatus = 'started' | 'stopped' | 'starting' | 'stopping' | 'unknown';
|
||||
|
|
@ -295,6 +305,10 @@ declare module '@tmpwip/extension-api' {
|
|||
// it may happen after an update or an installation
|
||||
updateDetectionChecks(detectionChecks: ProviderDetectionCheck[]): void;
|
||||
|
||||
// update warning information for the provider
|
||||
readonly warnings: ProviderInformation[];
|
||||
updateWarnings(warnings: ProviderInformation[]): void;
|
||||
|
||||
// notify that detection checks have changed
|
||||
onDidUpdateDetectionChecks: Event<ProviderDetectionCheck[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import type {
|
|||
ProviderLinks,
|
||||
ProviderStatus,
|
||||
Link,
|
||||
ProviderInformation,
|
||||
} from '@tmpwip/extension-api';
|
||||
|
||||
export type LifecycleMethod = 'start' | 'stop' | 'delete';
|
||||
|
|
@ -65,6 +66,9 @@ export interface ProviderInfo {
|
|||
links: ProviderLinks[];
|
||||
detectionChecks: ProviderDetectionCheck[];
|
||||
|
||||
// warning messages regarding the provider
|
||||
warnings: ProviderInformation[];
|
||||
|
||||
images: ProviderImages;
|
||||
|
||||
// can install a provider
|
||||
|
|
|
|||
|
|
@ -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,10 @@ export class ProviderImpl implements Provider, IDisposable {
|
|||
private readonly _onDidUpdateDetectionChecks = new Emitter<ProviderDetectionCheck[]>();
|
||||
readonly onDidUpdateDetectionChecks: Event<ProviderDetectionCheck[]> = this._onDidUpdateDetectionChecks.event;
|
||||
|
||||
private _warnings: ProviderInformation[];
|
||||
private readonly _onDidUpdateWarnings = new Emitter<ProviderInformation[]>();
|
||||
readonly onDidUpdateWarnings: Event<ProviderInformation[]> = this._onDidUpdateWarnings.event;
|
||||
|
||||
constructor(
|
||||
private _internalId: string,
|
||||
private providerOptions: ProviderOptions,
|
||||
|
|
@ -80,6 +85,7 @@ export class ProviderImpl implements Provider, IDisposable {
|
|||
this._links = providerOptions.links || [];
|
||||
this._detectionChecks = providerOptions.detectionChecks || [];
|
||||
this._images = providerOptions.images || {};
|
||||
this._warnings = providerOptions.warnings || [];
|
||||
|
||||
// monitor connection statuses
|
||||
setInterval(async () => {
|
||||
|
|
@ -116,6 +122,10 @@ export class ProviderImpl implements Provider, IDisposable {
|
|||
return this._detectionChecks;
|
||||
}
|
||||
|
||||
get warnings(): ProviderInformation[] {
|
||||
return this._warnings;
|
||||
}
|
||||
|
||||
get images(): ProviderImages {
|
||||
return this._images;
|
||||
}
|
||||
|
|
@ -139,6 +149,12 @@ export class ProviderImpl implements Provider, IDisposable {
|
|||
this._onDidUpdateDetectionChecks.fire(detectionChecks);
|
||||
}
|
||||
|
||||
// Update the warnings
|
||||
updateWarnings(warnings: ProviderInformation[]): void {
|
||||
this._warnings = warnings;
|
||||
this._onDidUpdateWarnings.fire(warnings);
|
||||
}
|
||||
|
||||
get status(): ProviderStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import type {
|
|||
UnregisterKubernetesConnectionEvent,
|
||||
RegisterKubernetesConnectionEvent,
|
||||
Logger,
|
||||
ProviderInformation,
|
||||
} from '@tmpwip/extension-api';
|
||||
import type {
|
||||
ProviderContainerConnectionInfo,
|
||||
|
|
@ -69,6 +70,7 @@ export class ProviderRegistry {
|
|||
private count = 0;
|
||||
private providers: Map<string, ProviderImpl>;
|
||||
private providerStatuses = new Map<string, ProviderStatus>();
|
||||
private providerWarnings = new Map<string, ProviderInformation[]>();
|
||||
|
||||
private providerLifecycles: Map<string, ProviderLifecycle> = new Map();
|
||||
private providerLifecycleContexts: Map<string, LifecycleContextImpl> = new Map();
|
||||
|
|
@ -111,18 +113,38 @@ 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);
|
||||
const providerWarnings = this.providerWarnings.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);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the warnings of the provider
|
||||
if (provider) {
|
||||
// If the warnings do not match the current cache, we will send an update event to the renderer
|
||||
// and update the local warnings cache
|
||||
if (JSON.stringify(providerWarnings) !== JSON.stringify(provider?.warnings)) {
|
||||
this.apiSender.send('provider:update-warnings', provider.id);
|
||||
this.providerWarnings.set(providerKey, provider.warnings);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
|
@ -491,6 +513,7 @@ export class ProviderRegistry {
|
|||
detectionChecks: provider.detectionChecks,
|
||||
images: provider.images,
|
||||
version: provider.version,
|
||||
warnings: provider.warnings,
|
||||
installationSupport,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export class TrayMenu {
|
|||
version: '',
|
||||
links: [],
|
||||
images: {},
|
||||
warnings: [],
|
||||
installationSupport: false,
|
||||
containerProviderConnectionCreation: false,
|
||||
kubernetesProviderConnectionCreation: false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { CheckStatus, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
|
||||
import PreflightChecks from './PreflightChecks.svelte';
|
||||
import ProviderWarnings from './ProviderWarnings.svelte';
|
||||
import ProviderLinks from './ProviderLinks.svelte';
|
||||
import ProviderLogo from './ProviderLogo.svelte';
|
||||
import ProviderUpdateButton from './ProviderUpdateButton.svelte';
|
||||
|
|
@ -36,5 +37,6 @@ let preflightChecks: CheckStatus[] = [];
|
|||
{/if}
|
||||
<PreflightChecks preflightChecks="{preflightChecks}" />
|
||||
|
||||
<ProviderWarnings provider="{provider}" />
|
||||
<ProviderLinks provider="{provider}" />
|
||||
</div>
|
||||
|
|
|
|||
26
packages/renderer/src/lib/welcome/ProviderWarnings.svelte
Normal file
26
packages/renderer/src/lib/welcome/ProviderWarnings.svelte
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
|
||||
import type * as extensionApi from '@tmpwip/extension-api';
|
||||
import { providerInfos } from '../../stores/providers';
|
||||
|
||||
export let provider: ProviderInfo;
|
||||
|
||||
// Retrieve the provider information from the store
|
||||
let providerInfo: ProviderInfo;
|
||||
$: {
|
||||
providerInfo = $providerInfos.find(providerSearch => providerSearch.internalId === provider.internalId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center text-center mt-3">
|
||||
<!-- TODO: Add dismiss button / ignore warning? -->
|
||||
{#if providerInfo?.warnings?.length > 0}
|
||||
{#each providerInfo.warnings as warn}
|
||||
<div class="flex-row items-center mt-0.5">
|
||||
⚠️
|
||||
<span class="ml-1 text-sm text-gray-200 font-semibold">{warn.name}:</span>
|
||||
<span class="ml-1 text-sm text-gray-400">{warn.details}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -64,6 +64,9 @@ window?.events.receive('provider-delete', () => {
|
|||
window?.events.receive('provider:update-status', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
window?.events.receive('provider:update-warnings', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
window.addEventListener('system-ready', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,6 +149,40 @@ helper_binaries_dir=["/Users/user/example_directory"]
|
|||
|
||||
**NOTE**: A pre-built binary will be added to the Podman release page so you do not have to build `podman-mac-helper`. An [issue is open for this](https://github.com/containers/podman/issues/16746).
|
||||
|
||||
### Warning about Docker compatibility mode
|
||||
|
||||
|
||||
#### Issue:
|
||||
|
||||
When running the Podman provider, a warning shows regarding Docker compatibility mode on the dashboard:
|
||||
|
||||
```sh
|
||||
⚠️ Docker Socket Compatibility: Podman is not emulating the default Docker socket path: '/var/run/docker.sock'. Docker-specific tools may not work. See troubleshooting page on podman-desktop.io for more information.
|
||||
```
|
||||
|
||||
This may appear when either:
|
||||
* The Docker socket is not mounted correctly
|
||||
* Docker Desktop is also being ran at the same time
|
||||
|
||||
#### Solution:
|
||||
|
||||
**On macOS:**
|
||||
|
||||
1. Stop Docker Desktop (if install)
|
||||
2. Run the `podman-mac-helper` binary:
|
||||
```sh
|
||||
sudo podman-mac-helper install
|
||||
```
|
||||
3. Restart the Podman machine (the default Docker socket path will be recreated and Podman will emulate it)
|
||||
|
||||
|
||||
**On Linux / Windows:**
|
||||
|
||||
1. Stop Docker Desktop (if installed)
|
||||
2. Restart the Podman machine (the default Docker socket path will be recreated and Podman will emulate it)
|
||||
|
||||
*Note:* If Docker Desktop is started again, it will automatically re-alias the default Docker socket location and the Podman compatibilty warning will re-appear.
|
||||
|
||||
|
||||
## Code Ready Containers
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue