Skip to content

Commit

Permalink
feat: add warnings to provider, add warn re. docker socket
Browse files Browse the repository at this point in the history
### 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 podman-desktop#905
Fixes podman-desktop#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 <[email protected]>
  • Loading branch information
cdrage committed Dec 20, 2022
1 parent 578ea94 commit 4460a88
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 1 deletion.
60 changes: 59 additions & 1 deletion extensions/podman/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -312,6 +318,7 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
// update detection checks
detectionChecks.push(...getDetectionChecks(installedPodman));


const providerOptions: extensionApi.ProviderOptions = {
name: 'Podman',
id: 'podman',
Expand Down Expand Up @@ -343,6 +350,11 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
];

const provider = extensionApi.provider.createProvider(providerOptions);

// 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({
Expand Down Expand Up @@ -496,10 +508,12 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
monitorMachines(provider);
}


// monitor provider
// like version, checks
// like version, checks, warnings
monitorProvider(provider);


// register the registries
const registrySetup = new RegistrySetup();
await registrySetup.setup(extensionContext);
Expand All @@ -512,3 +526,47 @@ 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
const socketWatcher = extensionApi.fs.createFileSystemWatcher(socketFile);

console.log('socket file:', socketFile)

socketWatcher.onDidChange(() => {
console.log('socket changed')
checkDisguisedPodmanSocket(provider);
});

socketWatcher.onDidCreate(() => {
console.log('socket created')
checkDisguisedPodmanSocket(provider);
});

socketWatcher.onDidDelete(() => {
console.log('socket deleted')
checkDisguisedPodmanSocket(provider);
});

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.updateWarning(retrievedWarnings);
}
101 changes: 101 additions & 0 deletions extensions/podman/src/warnings.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions packages/extension-api/src/extension-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -146,6 +153,9 @@ declare module '@tmpwip/extension-api' {
images?: ProviderImages;
links?: ProviderLinks[];
detectionChecks?: ProviderDetectionCheck[];

// Provide way to add additional warnings to the provider
warning?: ProviderInformation[];
}

export type ProviderConnectionStatus = 'started' | 'stopped' | 'starting' | 'stopping' | 'unknown';
Expand Down Expand Up @@ -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 warning: ProviderInformation[];
updateWarning(warning: ProviderInformation[]): void;

// notify that detection checks have changed
onDidUpdateDetectionChecks: Event<ProviderDetectionCheck[]>;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/main/src/plugin/api/provider-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
ProviderLinks,
ProviderStatus,
Link,
ProviderInformation,
} from '@tmpwip/extension-api';

export type LifecycleMethod = 'start' | 'stop' | 'delete';
Expand Down Expand Up @@ -65,6 +66,9 @@ export interface ProviderInfo {
links: ProviderLinks[];
detectionChecks: ProviderDetectionCheck[];

// warning messages regarding the provider
warning: ProviderInformation[];

images: ProviderImages;

// can install a provider
Expand Down
16 changes: 16 additions & 0 deletions packages/main/src/plugin/provider-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,6 +66,10 @@ export class ProviderImpl implements Provider, IDisposable {
private readonly _onDidUpdateDetectionChecks = new Emitter<ProviderDetectionCheck[]>();
readonly onDidUpdateDetectionChecks: Event<ProviderDetectionCheck[]> = this._onDidUpdateDetectionChecks.event;

private _warning: ProviderInformation[];
private readonly _onDidUpdateWarning = new Emitter<ProviderInformation[]>();
readonly onDidUpdateWarning: Event<ProviderInformation[]> = this._onDidUpdateWarning.event;

constructor(
private _internalId: string,
private providerOptions: ProviderOptions,
Expand All @@ -80,6 +85,7 @@ export class ProviderImpl implements Provider, IDisposable {
this._links = providerOptions.links || [];
this._detectionChecks = providerOptions.detectionChecks || [];
this._images = providerOptions.images || {};
this._warning = providerOptions.warning || [];

// monitor connection statuses
setInterval(async () => {
Expand Down Expand Up @@ -116,6 +122,10 @@ export class ProviderImpl implements Provider, IDisposable {
return this._detectionChecks;
}

get warning(): ProviderInformation[] {
return this._warning;
}

get images(): ProviderImages {
return this._images;
}
Expand All @@ -139,6 +149,12 @@ export class ProviderImpl implements Provider, IDisposable {
this._onDidUpdateDetectionChecks.fire(detectionChecks);
}

// Update the warnings
updateWarning(warning: ProviderInformation[]): void {
this._warning = warning;
this._onDidUpdateWarning.fire(warning);
}

get status(): ProviderStatus {
return this._status;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/main/src/plugin/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,32 @@ 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);
}
});
}, 2000);
}
Expand Down Expand Up @@ -491,6 +505,7 @@ export class ProviderRegistry {
detectionChecks: provider.detectionChecks,
images: provider.images,
version: provider.version,
warning: provider.warning,
installationSupport,
};

Expand Down
1 change: 1 addition & 0 deletions packages/main/src/tray-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export class TrayMenu {
version: '',
links: [],
images: {},
warning: [],
installationSupport: false,
containerProviderConnectionCreation: false,
kubernetesProviderConnectionCreation: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer/src/lib/welcome/ProviderReady.svelte
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -36,5 +37,6 @@ let preflightChecks: CheckStatus[] = [];
{/if}
<PreflightChecks preflightChecks="{preflightChecks}" />

<ProviderWarnings provider="{provider}" />
<ProviderLinks provider="{provider}" />
</div>
26 changes: 26 additions & 0 deletions packages/renderer/src/lib/welcome/ProviderWarnings.svelte
Original file line number Diff line number Diff line change
@@ -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?.warning?.length > 0}
{#each providerInfo.warning 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>
Loading

0 comments on commit 4460a88

Please sign in to comment.