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 -->
![Screenshot 2022-12-15 at 3 51 18 PM](https://user-images.githubusercontent.com/6422176/207964858-5fb97a74-4f73-4952-8132-5b1af08ad572.png)
![Screenshot 2022-12-15 at 3 50 38 PM](https://user-images.githubusercontent.com/6422176/207964861-a9c1f72c-89d6-4816-beab-397af4125620.png)

### 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:
Charlie Drage 2023-01-03 11:23:05 -08:00 committed by GitHub
parent 2713059ec7
commit 0c027e5356
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 290 additions and 1 deletions

View file

@ -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);
}

View 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;
}

View file

@ -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[]>;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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,
};

View file

@ -78,6 +78,7 @@ export class TrayMenu {
version: '',
links: [],
images: {},
warnings: [],
installationSupport: false,
containerProviderConnectionCreation: false,
kubernetesProviderConnectionCreation: false,

View file

@ -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>

View 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>

View file

@ -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();
});

View file

@ -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