mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 17:47:22 +00:00
feat: add compose extension (#1578)
* feat: add compose extension extension is responsible to download docker-compose and setup a 'podman-desktop' binary that will setup properly DOCKER_HOST fixes https://github.com/containers/podman-desktop/issues/1478 Change-Id: Id0e6628ca5860649c2a0b19f7a85eb326605c9ac Signed-off-by: Florent Benoit <fbenoit@redhat.com> * fixup! feat: add compose extension extension is responsible to download docker-compose and setup a 'podman-desktop' binary that will setup properly DOCKER_HOST Change-Id: Id1304316ca7b1160e4852e1dd622c41d232df734 Signed-off-by: Florent Benoit <fbenoit@redhat.com> * Update extensions/compose/src/compose-extension.ts Co-authored-by: Luca Stocchi <49404737+lstocchi@users.noreply.github.com> Signed-off-by: Florent BENOIT <fbenoit@redhat.com> * fixup! feat: add compose extension extension is responsible to download docker-compose and setup a 'podman-desktop' binary that will setup properly DOCKER_HOST Change-Id: I1d2977d36fc7970c44c063e8bcd79f283bd8e4ed Signed-off-by: Florent Benoit <fbenoit@redhat.com> * fixup! feat: add compose extension extension is responsible to download docker-compose and setup a 'podman-desktop' binary that will setup properly DOCKER_HOST Change-Id: Ied497b8ead4b9966d1a2eadfe84e0cbdb8def834 Signed-off-by: Florent Benoit <fbenoit@redhat.com> * fixup! feat: add compose extension extension is responsible to download docker-compose and setup a 'podman-desktop' binary that will setup properly DOCKER_HOST Change-Id: Ibe90411dee94afa96dbd689b779864eb138f295f Signed-off-by: Florent Benoit <fbenoit@redhat.com> --------- Signed-off-by: Florent Benoit <fbenoit@redhat.com> Signed-off-by: Florent BENOIT <fbenoit@redhat.com> Co-authored-by: Luca Stocchi <49404737+lstocchi@users.noreply.github.com>
This commit is contained in:
parent
c482fd0dc0
commit
e884bc8964
23 changed files with 7144 additions and 3 deletions
36
extensions/compose/package.json
Normal file
36
extensions/compose/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "compose",
|
||||
"displayName": "Compose",
|
||||
"description": "Install Compose binary to work with Podman Engine.",
|
||||
"version": "0.0.1",
|
||||
"publisher": "benoitf",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"podman-desktop": "^0.0.1"
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "compose.install",
|
||||
"title": "Install Compose"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build && node ./scripts/build.js",
|
||||
"watch": "vite build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"mustache": "^4.2.0",
|
||||
"shell-path": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"7zip-min": "^1.4.3",
|
||||
"@podman-desktop/api": "^0.0.1",
|
||||
"mkdirp": "^2.1.3",
|
||||
"vite": "^4.1.4",
|
||||
"zip-local": "^0.3.5"
|
||||
}
|
||||
}
|
||||
36
extensions/compose/scripts/build.js
Executable file
36
extensions/compose/scripts/build.js
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env node
|
||||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
const zipper = require('zip-local');
|
||||
const path = require('path');
|
||||
const package = require('../package.json');
|
||||
const fs = require('fs');
|
||||
|
||||
const destFile = path.resolve(__dirname, `../${package.name}.cdix`);
|
||||
const builtinDirectory = path.resolve(__dirname, '../builtin');
|
||||
// remove the .cdix file before zipping
|
||||
if (fs.existsSync(destFile)) {
|
||||
fs.rmSync(destFile);
|
||||
}
|
||||
// remove the builtin folder before zipping
|
||||
if (fs.existsSync(builtinDirectory)) {
|
||||
fs.rmSync(builtinDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
zipper.sync.zip(path.resolve(__dirname, '../')).compress().save(destFile);
|
||||
119
extensions/compose/src/cli-run.ts
Normal file
119
extensions/compose/src/cli-run.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
import type * as extensionApi from '@podman-desktop/api';
|
||||
import type { OS } from './os';
|
||||
|
||||
export interface SpawnResult {
|
||||
exitCode: number;
|
||||
stdOut: string;
|
||||
stdErr: string;
|
||||
error: undefined | string;
|
||||
}
|
||||
|
||||
export interface RunOptions {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
logger?: extensionApi.Logger;
|
||||
}
|
||||
|
||||
const macosExtraPath = '/usr/local/bin:/opt/homebrew/bin:/opt/local/bin';
|
||||
|
||||
export class CliRun {
|
||||
constructor(private readonly extensionContext: extensionApi.ExtensionContext, private os: OS) {}
|
||||
|
||||
getPath(): string {
|
||||
const env = process.env;
|
||||
|
||||
// extension storage bin path (where to store binaries)
|
||||
const extensionBinPath = resolve(this.extensionContext.storagePath, 'bin');
|
||||
|
||||
if (this.os.isMac()) {
|
||||
if (!env.PATH) {
|
||||
return macosExtraPath.concat(':').concat(extensionBinPath);
|
||||
} else {
|
||||
return env.PATH.concat(':').concat(macosExtraPath).concat(':').concat(extensionBinPath);
|
||||
}
|
||||
} else if (this.os.isWindows()) {
|
||||
return env.PATH.concat(';').concat(extensionBinPath);
|
||||
} else {
|
||||
return env.PATH.concat(':').concat(extensionBinPath);
|
||||
}
|
||||
}
|
||||
|
||||
runCommand(command: string, args: string[], options?: RunOptions): Promise<SpawnResult> {
|
||||
return new Promise(resolve => {
|
||||
let stdOut = '';
|
||||
let stdErr = '';
|
||||
let err = '';
|
||||
let env = Object.assign({}, process.env); // clone original env object
|
||||
|
||||
// In production mode, applications don't have access to the 'user' path like brew
|
||||
if (this.os.isMac() || this.os.isWindows()) {
|
||||
env.PATH = this.getPath();
|
||||
if (this.os.isWindows()) {
|
||||
// Escape any whitespaces in command
|
||||
command = `"${command}"`;
|
||||
}
|
||||
} else if (env.FLATPAK_ID) {
|
||||
// need to execute the command on the host
|
||||
args = ['--host', command, ...args];
|
||||
command = 'flatpak-spawn';
|
||||
}
|
||||
|
||||
if (options?.env) {
|
||||
env = Object.assign(env, options.env);
|
||||
}
|
||||
|
||||
const spawnProcess = spawn(command, args, { shell: this.os.isWindows(), env });
|
||||
// do not reject as we want to store exit code in the result
|
||||
spawnProcess.on('error', error => {
|
||||
if (options?.logger) {
|
||||
options.logger.error(error);
|
||||
}
|
||||
stdErr += error;
|
||||
err += error;
|
||||
});
|
||||
|
||||
spawnProcess.stdout.setEncoding('utf8');
|
||||
spawnProcess.stdout.on('data', data => {
|
||||
if (options?.logger) {
|
||||
options.logger.log(data);
|
||||
}
|
||||
stdOut += data;
|
||||
});
|
||||
spawnProcess.stderr.setEncoding('utf8');
|
||||
spawnProcess.stderr.on('data', data => {
|
||||
if (options?.logger) {
|
||||
// log create to stdout instead of stderr
|
||||
if (args?.[0] === 'create') {
|
||||
options.logger.log(data);
|
||||
} else {
|
||||
options.logger.error(data);
|
||||
}
|
||||
}
|
||||
stdErr += data;
|
||||
});
|
||||
|
||||
spawnProcess.on('close', exitCode => {
|
||||
resolve({ exitCode, stdOut, stdErr, error: err });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
453
extensions/compose/src/compose-extension.spec.ts
Normal file
453
extensions/compose/src/compose-extension.spec.ts
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
/**********************************************************************
|
||||
* 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 * as fs from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import type { Mock } from 'vitest';
|
||||
import { afterEach, beforeEach, test, expect, vi, describe } from 'vitest';
|
||||
import { ComposeExtension } from './compose-extension';
|
||||
import type { Detect } from './detect';
|
||||
import type { ComposeGitHubReleases } from './compose-github-releases';
|
||||
import * as extensionApi from '@podman-desktop/api';
|
||||
import { promises } from 'node:fs';
|
||||
import type { ComposeWrapperGenerator } from './compose-wrapper-generator';
|
||||
|
||||
const extensionContext: extensionApi.ExtensionContext = {
|
||||
storagePath: '/fake/path',
|
||||
subscriptions: [],
|
||||
} as unknown as extensionApi.ExtensionContext;
|
||||
let composeExtension: TestComposeExtension;
|
||||
|
||||
const osMock = {
|
||||
isLinux: vi.fn(),
|
||||
isMac: vi.fn(),
|
||||
isWindows: vi.fn(),
|
||||
};
|
||||
|
||||
const detectMock = {
|
||||
checkForDockerCompose: vi.fn(),
|
||||
checkStoragePath: vi.fn(),
|
||||
checkDefaultSocketIsAlive: vi.fn(),
|
||||
getSocketPath: vi.fn(),
|
||||
};
|
||||
|
||||
const composeGitHubReleasesMock = {
|
||||
grabLatestsReleasesMetadata: vi.fn(),
|
||||
getReleaseAssetId: vi.fn(),
|
||||
downloadReleaseAsset: vi.fn(),
|
||||
};
|
||||
|
||||
const composeWrapperGeneratorMock = {
|
||||
generate: vi.fn(),
|
||||
} as unknown as ComposeWrapperGenerator;
|
||||
|
||||
const statusBarItemMock = {
|
||||
tooltip: '',
|
||||
iconClass: '',
|
||||
command: '',
|
||||
show: vi.fn(),
|
||||
};
|
||||
|
||||
const dummyConnection1 = {
|
||||
connection: {
|
||||
status: () => 'stopped',
|
||||
endpoint: {
|
||||
socketPath: '/endpoint1.sock',
|
||||
},
|
||||
},
|
||||
} as extensionApi.ProviderContainerConnection;
|
||||
|
||||
const dummyConnection2 = {
|
||||
connection: {
|
||||
status: () => 'started',
|
||||
endpoint: {
|
||||
socketPath: '/endpoint2.sock',
|
||||
},
|
||||
},
|
||||
} as extensionApi.ProviderContainerConnection;
|
||||
|
||||
vi.mock('@podman-desktop/api', () => {
|
||||
return {
|
||||
StatusBarAlignLeft: 1,
|
||||
commands: {
|
||||
registerCommand: vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
dispose: () => {
|
||||
// do nothing
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
provider: {
|
||||
getContainerConnections: vi.fn(),
|
||||
},
|
||||
window: {
|
||||
showQuickPick: vi.fn(),
|
||||
createStatusBarItem: vi.fn(),
|
||||
showInformationMessage: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// allows to call protected methods
|
||||
class TestComposeExtension extends ComposeExtension {
|
||||
public publicNotifyOnChecks(firstCheck: boolean) {
|
||||
return super.notifyOnChecks(firstCheck);
|
||||
}
|
||||
|
||||
setCurrentInformation(value: string | undefined) {
|
||||
this.currentInformation = value;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
composeExtension = new TestComposeExtension(
|
||||
extensionContext,
|
||||
detectMock as unknown as Detect,
|
||||
composeGitHubReleasesMock as unknown as ComposeGitHubReleases,
|
||||
osMock,
|
||||
composeWrapperGeneratorMock,
|
||||
);
|
||||
|
||||
const createStatusBarItem = vi.spyOn(extensionApi.window, 'createStatusBarItem');
|
||||
createStatusBarItem.mockImplementation(() => statusBarItemMock as unknown as extensionApi.StatusBarItem);
|
||||
|
||||
(extensionApi.provider.getContainerConnections as Mock).mockReturnValue([dummyConnection1, dummyConnection2]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should prompt the user to download docker-desktop if podman-compose is not installed and docker-compose is not installed', async () => {
|
||||
detectMock.checkForDockerCompose.mockResolvedValueOnce(false);
|
||||
|
||||
// activate the extension
|
||||
await composeExtension.activate();
|
||||
|
||||
// now, check that if podman-compose is installed, we report the error as expected
|
||||
expect(statusBarItemMock.tooltip).toContain('Install Compose');
|
||||
expect(statusBarItemMock.iconClass).toBe(ComposeExtension.ICON_DOWNLOAD);
|
||||
|
||||
// command should be the install command
|
||||
expect(statusBarItemMock.command).toBe('compose.install');
|
||||
|
||||
expect(statusBarItemMock.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should report to the user that docker-compose is installed if docker-compose is installed with compatibily mode', async () => {
|
||||
detectMock.checkForDockerCompose.mockResolvedValueOnce(true);
|
||||
detectMock.checkDefaultSocketIsAlive.mockResolvedValueOnce(true);
|
||||
detectMock.checkStoragePath.mockResolvedValueOnce(true);
|
||||
|
||||
// activate the extension
|
||||
await composeExtension.activate();
|
||||
|
||||
// now, check that if podman-compose is installed, we report the error as expected
|
||||
expect(statusBarItemMock.tooltip).toContain('Compose is installed');
|
||||
expect(statusBarItemMock.iconClass).toBe(ComposeExtension.ICON_CHECK);
|
||||
|
||||
// command should be the checks command
|
||||
expect(statusBarItemMock.command).toBe('compose.checks');
|
||||
|
||||
expect(statusBarItemMock.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should report error to the user that docker-compose is installed if docker-compose is installed without compatibily mode', async () => {
|
||||
detectMock.checkForDockerCompose.mockResolvedValueOnce(true);
|
||||
detectMock.checkDefaultSocketIsAlive.mockResolvedValueOnce(false);
|
||||
detectMock.checkStoragePath.mockResolvedValueOnce(true);
|
||||
|
||||
const spyOnaddComposeWrapper = vi.spyOn(composeExtension, 'addComposeWrapper').mockImplementation(async () => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
// activate the extension
|
||||
await composeExtension.activate();
|
||||
|
||||
// now, check that if compose is installed, we report the error as expected
|
||||
expect(statusBarItemMock.tooltip).toContain('Compose is installed and usable with ');
|
||||
expect(statusBarItemMock.iconClass).toBe(ComposeExtension.ICON_CHECK);
|
||||
|
||||
// command should be the checks command
|
||||
expect(statusBarItemMock.command).toBe('compose.checks');
|
||||
|
||||
expect(statusBarItemMock.show).toHaveBeenCalled();
|
||||
expect(spyOnaddComposeWrapper).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should report to the user that path is not setup if compose is not in the PATH', async () => {
|
||||
detectMock.getSocketPath.mockReturnValueOnce('/fake/path');
|
||||
detectMock.checkForDockerCompose.mockResolvedValueOnce(true);
|
||||
detectMock.checkDefaultSocketIsAlive.mockResolvedValueOnce(false);
|
||||
detectMock.checkStoragePath.mockResolvedValueOnce(false);
|
||||
|
||||
const spyOnaddComposeWrapper = vi.spyOn(composeExtension, 'addComposeWrapper').mockImplementation(async () => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
// activate the extension
|
||||
await composeExtension.activate();
|
||||
|
||||
expect(statusBarItemMock.tooltip).toContain('/fake/path is not enabled. Need to us');
|
||||
expect(statusBarItemMock.iconClass).toBe(ComposeExtension.ICON_WARNING);
|
||||
|
||||
// command should be the checks command
|
||||
expect(statusBarItemMock.command).toBe('compose.checks');
|
||||
|
||||
expect(statusBarItemMock.show).toHaveBeenCalled();
|
||||
expect(spyOnaddComposeWrapper).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should report to the user that no container engine is running without compatibility mode', async () => {
|
||||
detectMock.getSocketPath.mockReturnValueOnce('/fake/path');
|
||||
detectMock.checkForDockerCompose.mockResolvedValueOnce(true);
|
||||
detectMock.checkDefaultSocketIsAlive.mockResolvedValueOnce(false);
|
||||
detectMock.checkStoragePath.mockResolvedValueOnce(false);
|
||||
|
||||
const spyOnaddComposeWrapper = vi.spyOn(composeExtension, 'addComposeWrapper').mockImplementation(async () => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
// no container connection
|
||||
(extensionApi.provider.getContainerConnections as Mock).mockReset();
|
||||
(extensionApi.provider.getContainerConnections as Mock).mockReturnValue([]);
|
||||
|
||||
// activate the extension
|
||||
await composeExtension.activate();
|
||||
|
||||
expect(statusBarItemMock.tooltip).toContain('No running container engine');
|
||||
expect(statusBarItemMock.iconClass).toBe(ComposeExtension.ICON_WARNING);
|
||||
|
||||
// command should be the checks command
|
||||
expect(statusBarItemMock.command).toBe('compose.checks');
|
||||
|
||||
expect(statusBarItemMock.show).toHaveBeenCalled();
|
||||
// no compose wrapper should be added
|
||||
expect(spyOnaddComposeWrapper).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Check that we have registered commands', async () => {
|
||||
const registerCommandMock = vi.spyOn(extensionApi.commands, 'registerCommand');
|
||||
const commands = new Map<string, (...args: any[]) => any>();
|
||||
|
||||
registerCommandMock.mockImplementation((command: string, callback: (...args: any[]) => any) => {
|
||||
commands.set(command, callback);
|
||||
return vi.fn() as unknown as extensionApi.Disposable;
|
||||
});
|
||||
|
||||
await composeExtension.activate();
|
||||
|
||||
// 2 commands should have been registered
|
||||
expect(extensionApi.commands.registerCommand).toHaveBeenCalledTimes(2);
|
||||
|
||||
// check that check command is registered
|
||||
const checkCommand = commands.get(ComposeExtension.COMPOSE_CHECKS_COMMAND);
|
||||
const spyRunCheck = vi.spyOn(composeExtension, 'runChecks');
|
||||
spyRunCheck.mockImplementation(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
expect(checkCommand).toBeDefined();
|
||||
// call the callback
|
||||
checkCommand?.();
|
||||
expect(spyRunCheck).toHaveBeenCalled();
|
||||
|
||||
const installCommand = commands.get(ComposeExtension.COMPOSE_INSTALL_COMMAND);
|
||||
const spyInstallCompose = vi.spyOn(composeExtension, 'installDockerCompose');
|
||||
spyInstallCompose.mockImplementation(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
expect(installCommand).toBeDefined();
|
||||
// call the callback
|
||||
installCommand?.();
|
||||
expect(spyInstallCompose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ existDir: false, os: 'Windows' },
|
||||
{ existDir: true, os: 'Linux' },
|
||||
])('Check install docker compose command', ({ existDir, os }) => {
|
||||
test(`Check install docker compose command dir exists ${existDir}`, async () => {
|
||||
let dockerComposeFileExtension = '';
|
||||
|
||||
// mock the fs module
|
||||
vi.mock('node:fs');
|
||||
|
||||
const showQuickPickMock = vi.spyOn(extensionApi.window, 'showQuickPick');
|
||||
|
||||
// mock the existSync and mkdir methods
|
||||
const existSyncSpy = vi.spyOn(fs, 'existsSync');
|
||||
existSyncSpy.mockImplementation(() => existDir);
|
||||
|
||||
const mkdirSpy = vi.spyOn(promises, 'mkdir');
|
||||
mkdirSpy.mockImplementation(() => Promise.resolve(''));
|
||||
|
||||
if (os === 'Windows') {
|
||||
osMock.isWindows.mockReturnValue(true);
|
||||
dockerComposeFileExtension = '.exe';
|
||||
// mock one item
|
||||
showQuickPickMock.mockResolvedValue({ label: 'latest', id: 'LATEST' } as any);
|
||||
} else if (os === 'Linux') {
|
||||
osMock.isLinux.mockReturnValue(true);
|
||||
// mock no choice from user
|
||||
showQuickPickMock.mockResolvedValue(undefined);
|
||||
}
|
||||
// fake chmod
|
||||
const chmodMock = vi.spyOn(promises, 'chmod');
|
||||
chmodMock.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const items = [{ label: 'latest' }];
|
||||
const fakeAssetId = 123;
|
||||
composeGitHubReleasesMock.grabLatestsReleasesMetadata.mockResolvedValue(items);
|
||||
composeGitHubReleasesMock.getReleaseAssetId.mockResolvedValue(fakeAssetId);
|
||||
composeGitHubReleasesMock.downloadReleaseAsset.mockResolvedValue(undefined);
|
||||
|
||||
// mock internal call
|
||||
const runChecksSpy = vi.spyOn(composeExtension, 'runChecks');
|
||||
runChecksSpy.mockResolvedValue(undefined);
|
||||
|
||||
await composeExtension.installDockerCompose();
|
||||
|
||||
expect(showQuickPickMock).toHaveBeenCalledWith(items, { placeHolder: 'Select docker compose version to install' });
|
||||
// should have fetched latest releases
|
||||
expect(composeGitHubReleasesMock.grabLatestsReleasesMetadata).toHaveBeenCalled();
|
||||
// should have downloaded the release asset
|
||||
expect(composeGitHubReleasesMock.downloadReleaseAsset).toHaveBeenCalledWith(
|
||||
fakeAssetId,
|
||||
resolve(extensionContext.storagePath, `bin/docker-compose${dockerComposeFileExtension}`),
|
||||
);
|
||||
|
||||
// should have called run checks
|
||||
expect(runChecksSpy).toHaveBeenCalled();
|
||||
|
||||
// should have created the directory if non-existent
|
||||
if (!existDir) {
|
||||
expect(mkdirSpy).toHaveBeenCalledWith(resolve(extensionContext.storagePath, 'bin'), { recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ existDir: false, os: 'Windows' },
|
||||
{ existDir: true, os: 'Linux' },
|
||||
])('Check install compose wrapper command', ({ existDir, os }) => {
|
||||
test(`Check install compose wrapper command dir exists ${existDir}`, async () => {
|
||||
let composeWrapperFileExtension = '';
|
||||
|
||||
// mock the fs module
|
||||
vi.mock('node:fs');
|
||||
|
||||
// mock the existSync and mkdir methods
|
||||
const existSyncSpy = vi.spyOn(fs, 'existsSync');
|
||||
existSyncSpy.mockImplementation(() => existDir);
|
||||
|
||||
const mkdirSpy = vi.spyOn(promises, 'mkdir');
|
||||
mkdirSpy.mockImplementation(() => Promise.resolve(''));
|
||||
|
||||
if (os === 'Windows') {
|
||||
osMock.isWindows.mockReturnValue(true);
|
||||
composeWrapperFileExtension = '.bat';
|
||||
} else if (os === 'Linux') {
|
||||
osMock.isLinux.mockReturnValue(true);
|
||||
}
|
||||
// fake chmod
|
||||
const chmodMock = vi.spyOn(promises, 'chmod');
|
||||
chmodMock.mockImplementation(() => Promise.resolve());
|
||||
|
||||
await composeExtension.addComposeWrapper(dummyConnection2);
|
||||
|
||||
// should have created the directory if non-existent
|
||||
if (!existDir) {
|
||||
expect(mkdirSpy).toHaveBeenCalledWith(resolve(extensionContext.storagePath, 'bin'), { recursive: true });
|
||||
}
|
||||
|
||||
// should have call the podman generator
|
||||
expect(composeWrapperGeneratorMock.generate).toHaveBeenCalledWith(
|
||||
dummyConnection2,
|
||||
`/fake/path/bin/compose${composeWrapperFileExtension}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifyOnChecks', async () => {
|
||||
test('first check true', async () => {
|
||||
// no current information
|
||||
composeExtension.setCurrentInformation(undefined);
|
||||
|
||||
const showCurrentInformationMock = vi.spyOn(composeExtension, 'showCurrentInformation');
|
||||
const showInformationMessageMock = vi.spyOn(extensionApi.window, 'showInformationMessage');
|
||||
await composeExtension.publicNotifyOnChecks(true);
|
||||
expect(showInformationMessageMock).not.toHaveBeenCalled();
|
||||
expect(showCurrentInformationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('first check false and information', async () => {
|
||||
const info = 'this is a current information';
|
||||
composeExtension.setCurrentInformation(info);
|
||||
|
||||
const showCurrentInformationMock = vi.spyOn(composeExtension, 'showCurrentInformation');
|
||||
const showInformationMessageMock = vi.spyOn(extensionApi.window, 'showInformationMessage');
|
||||
await composeExtension.publicNotifyOnChecks(false);
|
||||
expect(showInformationMessageMock).toHaveBeenCalled();
|
||||
expect(showCurrentInformationMock).toHaveBeenCalled();
|
||||
expect(showInformationMessageMock).toHaveBeenCalledWith(info);
|
||||
});
|
||||
});
|
||||
|
||||
test('deactivate', async () => {
|
||||
await composeExtension.deactivate();
|
||||
});
|
||||
|
||||
describe('makeExecutable', async () => {
|
||||
const fakePath = '/fake/path';
|
||||
test('mac', async () => {
|
||||
osMock.isMac.mockReturnValue(true);
|
||||
|
||||
// fake chmod
|
||||
const chmodMock = vi.spyOn(promises, 'chmod');
|
||||
chmodMock.mockImplementation(() => Promise.resolve());
|
||||
await composeExtension.makeExecutable(fakePath);
|
||||
// check it has been called
|
||||
expect(chmodMock).toHaveBeenCalledWith(fakePath, 0o755);
|
||||
});
|
||||
|
||||
test('linux', async () => {
|
||||
osMock.isLinux.mockReturnValue(true);
|
||||
|
||||
// fake chmod
|
||||
const chmodMock = vi.spyOn(promises, 'chmod');
|
||||
chmodMock.mockImplementation(() => Promise.resolve());
|
||||
await composeExtension.makeExecutable(fakePath);
|
||||
// check it has been called
|
||||
expect(chmodMock).toHaveBeenCalledWith(fakePath, 0o755);
|
||||
});
|
||||
|
||||
test('windows', async () => {
|
||||
osMock.isWindows.mockReturnValue(true);
|
||||
|
||||
// fake chmod
|
||||
const chmodMock = vi.spyOn(promises, 'chmod');
|
||||
chmodMock.mockImplementation(() => Promise.resolve());
|
||||
await composeExtension.makeExecutable(fakePath);
|
||||
// check it has not been called on Windows
|
||||
expect(chmodMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
236
extensions/compose/src/compose-extension.ts
Normal file
236
extensions/compose/src/compose-extension.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
import * as path from 'node:path';
|
||||
import { existsSync, promises } from 'node:fs';
|
||||
import * as extensionApi from '@podman-desktop/api';
|
||||
import type { Detect } from './detect';
|
||||
import type { ComposeGitHubReleases } from './compose-github-releases';
|
||||
import type { OS } from './os';
|
||||
import { platform, arch } from 'node:os';
|
||||
import type { ComposeWrapperGenerator } from './compose-wrapper-generator';
|
||||
|
||||
export class ComposeExtension {
|
||||
public static readonly COMPOSE_INSTALL_COMMAND = 'compose.install';
|
||||
public static readonly COMPOSE_CHECKS_COMMAND = 'compose.checks';
|
||||
|
||||
public static readonly ICON_CHECK = 'fa fa-check';
|
||||
public static readonly ICON_DOWNLOAD = 'fa fa-download';
|
||||
public static readonly ICON_WARNING = 'fa fa-exclamation-triangle';
|
||||
|
||||
private statusBarItem: extensionApi.StatusBarItem | undefined;
|
||||
|
||||
protected currentInformation: string | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly extensionContext: extensionApi.ExtensionContext,
|
||||
private readonly detect: Detect,
|
||||
private readonly composeGitHubReleases: ComposeGitHubReleases,
|
||||
private readonly os: OS,
|
||||
private podmanComposeGenerator: ComposeWrapperGenerator,
|
||||
) {}
|
||||
|
||||
async runChecks(firstCheck: boolean): Promise<void> {
|
||||
this.currentInformation = undefined;
|
||||
|
||||
// reset status bar information
|
||||
const statusBarChangesToApply = {
|
||||
iconClass: '',
|
||||
tooltip: '',
|
||||
command: ComposeExtension.COMPOSE_CHECKS_COMMAND,
|
||||
};
|
||||
|
||||
// check for docker-compose
|
||||
const dockerComposeInstalled = await this.detect.checkForDockerCompose();
|
||||
if (dockerComposeInstalled) {
|
||||
// check if we have compatibility mode or docker setup
|
||||
const compatibilityModeSetup = await this.detect.checkDefaultSocketIsAlive();
|
||||
if (compatibilityModeSetup) {
|
||||
// it's installed so we're good
|
||||
statusBarChangesToApply.iconClass = ComposeExtension.ICON_CHECK;
|
||||
statusBarChangesToApply.tooltip = 'Compose is installed and DOCKER_HOST is reachable';
|
||||
} else {
|
||||
// grab the current connection to container engine
|
||||
const connections = extensionApi.provider.getContainerConnections();
|
||||
const startedConnections = connections.filter(
|
||||
providerConnection => providerConnection.connection.status() === 'started',
|
||||
);
|
||||
if (startedConnections.length === 0) {
|
||||
statusBarChangesToApply.iconClass = ComposeExtension.ICON_WARNING;
|
||||
statusBarChangesToApply.tooltip =
|
||||
'No running container engine. Unable to write a compose wrapper script that will set DOCKER_HOST in that case. Please start a container engine first.';
|
||||
} else {
|
||||
// need to write the wrapper for docker-compose and use the name 'compose'
|
||||
// add wrapper script
|
||||
await this.addComposeWrapper(startedConnections[0]);
|
||||
|
||||
// check if the extension bin folder is in the PATH
|
||||
const extensionBinFolderInPath = await this.detect.checkStoragePath();
|
||||
if (!extensionBinFolderInPath) {
|
||||
// not there, ask the user to setup the PATH
|
||||
statusBarChangesToApply.iconClass = ComposeExtension.ICON_WARNING;
|
||||
statusBarChangesToApply.tooltip = `${this.detect.getSocketPath()} is not enabled. Need to use wrapper script`;
|
||||
this.currentInformation = `Please add the compose wrapper bin folder to your PATH environment variable. Value is ${path.resolve(
|
||||
this.extensionContext.storagePath,
|
||||
'bin',
|
||||
)}. The script ${path.resolve(
|
||||
this.extensionContext.storagePath,
|
||||
'bin',
|
||||
'compose',
|
||||
)} will setup for you the DOCKER_HOST environment variable.`;
|
||||
} else {
|
||||
// it's installed so we're good
|
||||
statusBarChangesToApply.iconClass = ComposeExtension.ICON_CHECK;
|
||||
statusBarChangesToApply.tooltip = `Compose is installed and usable with ${path.resolve(
|
||||
this.extensionContext.storagePath,
|
||||
'bin',
|
||||
'compose',
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// not installed, propose to install it
|
||||
statusBarChangesToApply.iconClass = ComposeExtension.ICON_DOWNLOAD;
|
||||
statusBarChangesToApply.tooltip = 'Install Compose';
|
||||
statusBarChangesToApply.command = ComposeExtension.COMPOSE_INSTALL_COMMAND;
|
||||
}
|
||||
// apply status bar changes
|
||||
if (this.statusBarItem) {
|
||||
this.statusBarItem.iconClass = statusBarChangesToApply.iconClass;
|
||||
this.statusBarItem.tooltip = statusBarChangesToApply.tooltip;
|
||||
this.statusBarItem.command = statusBarChangesToApply.command;
|
||||
}
|
||||
|
||||
this.notifyOnChecks(firstCheck);
|
||||
}
|
||||
|
||||
protected notifyOnChecks(firstCheck: boolean): void {
|
||||
if (this.currentInformation && !firstCheck) {
|
||||
this.showCurrentInformation();
|
||||
}
|
||||
}
|
||||
|
||||
async activate(): Promise<void> {
|
||||
if (!this.statusBarItem) {
|
||||
// create a status bar item
|
||||
this.statusBarItem = extensionApi.window.createStatusBarItem(extensionApi.StatusBarAlignLeft, 100);
|
||||
this.statusBarItem.text = 'Compose';
|
||||
this.statusBarItem.command = ComposeExtension.COMPOSE_CHECKS_COMMAND;
|
||||
this.statusBarItem.show();
|
||||
this.extensionContext.subscriptions.push(this.statusBarItem);
|
||||
}
|
||||
|
||||
// run init checks
|
||||
await this.runChecks(true);
|
||||
|
||||
const disposableInstall = extensionApi.commands.registerCommand(ComposeExtension.COMPOSE_INSTALL_COMMAND, () =>
|
||||
this.installDockerCompose(),
|
||||
);
|
||||
const disposableShowInfo = extensionApi.commands.registerCommand(ComposeExtension.COMPOSE_CHECKS_COMMAND, () =>
|
||||
this.runChecks(false),
|
||||
);
|
||||
this.extensionContext.subscriptions.push(disposableInstall, disposableShowInfo);
|
||||
}
|
||||
|
||||
async installDockerCompose(): Promise<void> {
|
||||
// grab latest assets metadata
|
||||
const lastReleasesMetadata = await this.composeGitHubReleases.grabLatestsReleasesMetadata();
|
||||
|
||||
// display a choice to the user with quickpick
|
||||
let selectedRelease = await extensionApi.window.showQuickPick(lastReleasesMetadata, {
|
||||
placeHolder: 'Select docker compose version to install',
|
||||
});
|
||||
if (!selectedRelease) {
|
||||
// user cancelled
|
||||
selectedRelease = lastReleasesMetadata[0];
|
||||
}
|
||||
|
||||
// get asset id
|
||||
const assetId = await this.composeGitHubReleases.getReleaseAssetId(selectedRelease.id, platform(), arch());
|
||||
|
||||
// get storage data
|
||||
const storageData = await this.extensionContext.storagePath;
|
||||
const storageBinFolder = path.resolve(storageData, 'bin');
|
||||
if (!existsSync(storageBinFolder)) {
|
||||
// create the folder
|
||||
await promises.mkdir(storageBinFolder, { recursive: true });
|
||||
}
|
||||
|
||||
// append file extension
|
||||
let fileExtension = '';
|
||||
if (this.os.isWindows()) {
|
||||
fileExtension = '.exe';
|
||||
}
|
||||
|
||||
// path
|
||||
const dockerComposeDownloadLocation = path.resolve(storageBinFolder, `docker-compose${fileExtension}`);
|
||||
|
||||
// download the asset
|
||||
await this.composeGitHubReleases.downloadReleaseAsset(assetId, dockerComposeDownloadLocation);
|
||||
|
||||
// make it executable
|
||||
await this.makeExecutable(dockerComposeDownloadLocation);
|
||||
|
||||
extensionApi.window.showInformationMessage(`Docker Compose ${selectedRelease.label} installed`);
|
||||
|
||||
// update checks
|
||||
this.runChecks(false);
|
||||
}
|
||||
|
||||
// add script that is redirecting to docker-compose and configuring the socket using DOCKER_HOST
|
||||
async addComposeWrapper(connection: extensionApi.ProviderContainerConnection): Promise<void> {
|
||||
// get storage data
|
||||
const storageData = await this.extensionContext.storagePath;
|
||||
const storageBinFolder = path.resolve(storageData, 'bin');
|
||||
|
||||
if (!existsSync(storageBinFolder)) {
|
||||
// create the folder
|
||||
await promises.mkdir(storageBinFolder, { recursive: true });
|
||||
}
|
||||
|
||||
// append file extension
|
||||
let fileExtension = '';
|
||||
if (this.os.isWindows()) {
|
||||
fileExtension = '.bat';
|
||||
}
|
||||
|
||||
// create the script file
|
||||
const composeWrapperScript = path.resolve(storageBinFolder, `compose${fileExtension}`);
|
||||
|
||||
await this.podmanComposeGenerator.generate(connection, composeWrapperScript);
|
||||
|
||||
// make it executable
|
||||
await this.makeExecutable(composeWrapperScript);
|
||||
}
|
||||
|
||||
showCurrentInformation(): void {
|
||||
if (this.currentInformation) {
|
||||
extensionApi.window.showInformationMessage(this.currentInformation);
|
||||
}
|
||||
}
|
||||
|
||||
async makeExecutable(filePath: string): Promise<void> {
|
||||
if (this.os.isLinux() || this.os.isMac()) {
|
||||
await promises.chmod(filePath, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
async deactivate(): Promise<void> {
|
||||
console.log('stopping compose extension');
|
||||
}
|
||||
}
|
||||
174
extensions/compose/src/compose-github-releases.spec.ts
Normal file
174
extensions/compose/src/compose-github-releases.spec.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
import { ComposeGitHubReleases } from './compose-github-releases';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
let composeGitHubReleases: ComposeGitHubReleases;
|
||||
|
||||
const listReleaseAssetsMock = vi.fn();
|
||||
const listReleasesMock = vi.fn();
|
||||
const getReleaseAssetMock = vi.fn();
|
||||
const octokitMock: Octokit = {
|
||||
repos: {
|
||||
listReleases: listReleasesMock,
|
||||
listReleaseAssets: listReleaseAssetsMock,
|
||||
getReleaseAsset: getReleaseAssetMock,
|
||||
},
|
||||
} as unknown as Octokit;
|
||||
|
||||
beforeEach(() => {
|
||||
composeGitHubReleases = new ComposeGitHubReleases(octokitMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('expect grab 5 releases', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const fsActual = await vi.importActual<typeof import('node:fs')>('node:fs');
|
||||
|
||||
// mock the result of listReleases REST API
|
||||
const resultREST = JSON.parse(
|
||||
fsActual.readFileSync(path.resolve(__dirname, '../tests/resources/compose-github-release-all.json'), 'utf8'),
|
||||
);
|
||||
listReleasesMock.mockImplementation(() => {
|
||||
return { data: resultREST };
|
||||
});
|
||||
|
||||
const result = await composeGitHubReleases.grabLatestsReleasesMetadata();
|
||||
expect(result).toBeDefined();
|
||||
expect(result.length).toBe(5);
|
||||
});
|
||||
|
||||
describe('Grab asset id for a given release id', async () => {
|
||||
beforeEach(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const fsActual = await vi.importActual<typeof import('node:fs')>('node:fs');
|
||||
|
||||
// mock the result of listReleaseAssetsMock REST API
|
||||
const resultREST = JSON.parse(
|
||||
fsActual.readFileSync(path.resolve(__dirname, '../tests/resources/compose-github-release-assets.json'), 'utf8'),
|
||||
);
|
||||
|
||||
listReleaseAssetsMock.mockImplementation(() => {
|
||||
return { data: resultREST };
|
||||
});
|
||||
});
|
||||
|
||||
test('macOS x86_64', async () => {
|
||||
const result = await composeGitHubReleases.getReleaseAssetId(91727807, 'darwin', 'x64');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(94785284);
|
||||
});
|
||||
|
||||
test('macOS arm64', async () => {
|
||||
const result = await composeGitHubReleases.getReleaseAssetId(91727807, 'darwin', 'arm64');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(94785273);
|
||||
});
|
||||
|
||||
test('windows x86_64', async () => {
|
||||
const result = await composeGitHubReleases.getReleaseAssetId(91727807, 'win32', 'x64');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(94785408);
|
||||
});
|
||||
|
||||
test('windows arm64', async () => {
|
||||
const result = await composeGitHubReleases.getReleaseAssetId(91727807, 'win32', 'arm64');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(94785398);
|
||||
});
|
||||
|
||||
test('linux x86_64', async () => {
|
||||
const result = await composeGitHubReleases.getReleaseAssetId(91727807, 'linux', 'x64');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(94785376);
|
||||
});
|
||||
|
||||
test('linux arm64', async () => {
|
||||
const result = await composeGitHubReleases.getReleaseAssetId(91727807, 'linux', 'arm64');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(94785298);
|
||||
});
|
||||
|
||||
test('invalid', async () => {
|
||||
await expect(composeGitHubReleases.getReleaseAssetId(91727807, 'invalid', 'invalid')).rejects.toThrow(
|
||||
'No asset found for',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should download the file if parent folder does exist', async () => {
|
||||
vi.mock('node:fs');
|
||||
|
||||
getReleaseAssetMock.mockImplementation(() => {
|
||||
return { data: 'foo' };
|
||||
});
|
||||
|
||||
// mock fs
|
||||
const existSyncSpy = vi.spyOn(fs, 'existsSync').mockImplementation(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
const writeFileSpy = vi.spyOn(fs.promises, 'writeFile').mockResolvedValue();
|
||||
|
||||
// generate a temporary file
|
||||
const destFile = '/fake/path/to/file';
|
||||
await composeGitHubReleases.downloadReleaseAsset(123, destFile);
|
||||
// check that parent director has been checked
|
||||
expect(existSyncSpy).toBeCalledWith('/fake/path/to');
|
||||
|
||||
// check that we've written the file
|
||||
expect(writeFileSpy).toBeCalledWith(destFile, Buffer.from('foo'));
|
||||
});
|
||||
|
||||
test('should download the file if parent folder does not exist', async () => {
|
||||
vi.mock('node:fs');
|
||||
|
||||
getReleaseAssetMock.mockImplementation(() => {
|
||||
return { data: 'foo' };
|
||||
});
|
||||
|
||||
// mock fs
|
||||
const existSyncSpy = vi.spyOn(fs, 'existsSync').mockImplementation(() => {
|
||||
return false;
|
||||
});
|
||||
const mkdirSpy = vi.spyOn(fs.promises, 'mkdir').mockImplementation(async () => {
|
||||
return '';
|
||||
});
|
||||
|
||||
const writeFileSpy = vi.spyOn(fs.promises, 'writeFile').mockResolvedValue();
|
||||
|
||||
// generate a temporary file
|
||||
const destFile = '/fake/path/to/file';
|
||||
await composeGitHubReleases.downloadReleaseAsset(123, destFile);
|
||||
// check that parent director has been checked
|
||||
expect(existSyncSpy).toBeCalledWith('/fake/path/to');
|
||||
|
||||
// check that we've created the parent folder
|
||||
expect(mkdirSpy).toBeCalledWith('/fake/path/to', { recursive: true });
|
||||
|
||||
// check that we've written the file
|
||||
expect(writeFileSpy).toBeCalledWith(destFile, Buffer.from('foo'));
|
||||
});
|
||||
109
extensions/compose/src/compose-github-releases.ts
Normal file
109
extensions/compose/src/compose-github-releases.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
import type { QuickPickItem } from '@podman-desktop/api';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
export interface ComposeGithubReleaseArtifactMetadata extends QuickPickItem {
|
||||
tag: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
// Allows to interact with Compose Releases on GitHub
|
||||
export class ComposeGitHubReleases {
|
||||
private static readonly COMPOSE_GITHUB_OWNER = 'docker';
|
||||
private static readonly COMPOSE_GITHUB_REPOSITORY = 'compose';
|
||||
|
||||
constructor(private readonly octokit: Octokit) {}
|
||||
|
||||
// Provides last 5 majors releases from GitHub using the GitHub API
|
||||
// return name, tag and id of the release
|
||||
async grabLatestsReleasesMetadata(): Promise<ComposeGithubReleaseArtifactMetadata[]> {
|
||||
// Grab last 5 majors releases from GitHub using the GitHub API
|
||||
|
||||
const lastReleases = await this.octokit.repos.listReleases({
|
||||
owner: ComposeGitHubReleases.COMPOSE_GITHUB_OWNER,
|
||||
repo: ComposeGitHubReleases.COMPOSE_GITHUB_REPOSITORY,
|
||||
per_page: 5, // limit to last 5 releases
|
||||
});
|
||||
|
||||
return lastReleases.data.map(release => {
|
||||
return {
|
||||
label: release.name || release.tag_name,
|
||||
tag: release.tag_name,
|
||||
id: release.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Get the asset id of a given release number for a given operating system and architecture
|
||||
// operatingSystem: win32, darwin, linux (see os.platform())
|
||||
// arch: x64, arm64 (see os.arch())
|
||||
async getReleaseAssetId(releaseId: number, operatingSystem: string, arch: string): Promise<number> {
|
||||
let extension = '';
|
||||
if (operatingSystem === 'win32') {
|
||||
operatingSystem = 'windows';
|
||||
extension = '.exe';
|
||||
}
|
||||
if (arch === 'x64') {
|
||||
arch = 'x86_64';
|
||||
}
|
||||
if (arch === 'arm64') {
|
||||
arch = 'aarch64';
|
||||
}
|
||||
|
||||
const listOfAssets = await this.octokit.repos.listReleaseAssets({
|
||||
owner: ComposeGitHubReleases.COMPOSE_GITHUB_OWNER,
|
||||
repo: ComposeGitHubReleases.COMPOSE_GITHUB_REPOSITORY,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const searchedAssetName = `docker-compose-${operatingSystem}-${arch}${extension}`;
|
||||
|
||||
// search for the right asset
|
||||
const asset = listOfAssets.data.find(asset => searchedAssetName === asset.name);
|
||||
if (!asset) {
|
||||
throw new Error(`No asset found for ${operatingSystem} and ${arch}`);
|
||||
}
|
||||
|
||||
return asset.id;
|
||||
}
|
||||
|
||||
// download the given asset id
|
||||
async downloadReleaseAsset(assetId: number, destination: string): Promise<void> {
|
||||
const asset = await this.octokit.repos.getReleaseAsset({
|
||||
owner: ComposeGitHubReleases.COMPOSE_GITHUB_OWNER,
|
||||
repo: ComposeGitHubReleases.COMPOSE_GITHUB_REPOSITORY,
|
||||
asset_id: assetId,
|
||||
headers: {
|
||||
accept: 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
|
||||
// check the parent folder exists
|
||||
const parentFolder = path.dirname(destination);
|
||||
|
||||
if (!fs.existsSync(parentFolder)) {
|
||||
await fs.promises.mkdir(parentFolder, { recursive: true });
|
||||
}
|
||||
// write the file
|
||||
await fs.promises.writeFile(destination, Buffer.from(asset.data as unknown as ArrayBuffer));
|
||||
}
|
||||
}
|
||||
120
extensions/compose/src/compose-wrapper-generator.spec.ts
Normal file
120
extensions/compose/src/compose-wrapper-generator.spec.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import { promises } from 'node:fs';
|
||||
|
||||
import { afterEach, expect, beforeEach, test, vi, vitest } from 'vitest';
|
||||
import { ComposeWrapperGenerator } from './compose-wrapper-generator';
|
||||
import * as extensionApi from '@podman-desktop/api';
|
||||
|
||||
// expose methods publicly for testing
|
||||
class TestComposeWrapperGenerator extends ComposeWrapperGenerator {
|
||||
public async generateContent(connection: extensionApi.ProviderContainerConnection): Promise<string> {
|
||||
return super.generateContent(connection);
|
||||
}
|
||||
}
|
||||
|
||||
const dummyConnection = {
|
||||
connection: {
|
||||
status: () => 'started',
|
||||
endpoint: {
|
||||
socketPath: '/endpoint.sock',
|
||||
},
|
||||
},
|
||||
} as extensionApi.ProviderContainerConnection;
|
||||
|
||||
vi.mock('@podman-desktop/api', () => {
|
||||
return {
|
||||
window: {
|
||||
showErrorMessage: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const osMock = {
|
||||
isWindows: vi.fn(),
|
||||
isLinux: vi.fn(),
|
||||
isMac: vi.fn(),
|
||||
};
|
||||
|
||||
let composeWrapperGenerator: TestComposeWrapperGenerator;
|
||||
beforeEach(() => {
|
||||
composeWrapperGenerator = new TestComposeWrapperGenerator(osMock, '/fake-dir');
|
||||
vi.mock('node:fs');
|
||||
vitest.spyOn(promises, 'writeFile').mockImplementation(() => Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('generate', async () => {
|
||||
osMock.isLinux.mockReturnValue(true);
|
||||
const generateContent = vi.spyOn(composeWrapperGenerator, 'generateContent');
|
||||
await composeWrapperGenerator.generate(dummyConnection, '/destFile');
|
||||
|
||||
// no error
|
||||
expect(extensionApi.window.showErrorMessage).not.toHaveBeenCalled();
|
||||
expect(generateContent).toHaveBeenCalled();
|
||||
|
||||
// check file is written
|
||||
expect(promises.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('generateContent on linux', async () => {
|
||||
osMock.isLinux.mockReturnValue(true);
|
||||
const content = await composeWrapperGenerator.generateContent(dummyConnection);
|
||||
// no error
|
||||
expect(extensionApi.window.showErrorMessage).not.toHaveBeenCalled();
|
||||
|
||||
// check content
|
||||
// should have sh for linux/mac
|
||||
expect(content).toContain('#!/bin/sh');
|
||||
|
||||
// contains the right endpoint
|
||||
expect(content).toContain('unix:///endpoint.sock');
|
||||
});
|
||||
|
||||
test('generateContent on mac', async () => {
|
||||
osMock.isMac.mockReturnValue(true);
|
||||
const content = await composeWrapperGenerator.generateContent(dummyConnection);
|
||||
// no error
|
||||
expect(extensionApi.window.showErrorMessage).not.toHaveBeenCalled();
|
||||
|
||||
// check content
|
||||
// should have sh for linux/mac
|
||||
expect(content).toContain('#!/bin/sh');
|
||||
|
||||
// contains the right endpoint
|
||||
expect(content).toContain('unix:///endpoint.sock');
|
||||
});
|
||||
|
||||
test('generateContent on windows', async () => {
|
||||
osMock.isWindows.mockReturnValue(true);
|
||||
const content = await composeWrapperGenerator.generateContent(dummyConnection);
|
||||
// no error
|
||||
expect(extensionApi.window.showErrorMessage).not.toHaveBeenCalled();
|
||||
|
||||
// check content
|
||||
// should have echo for windows
|
||||
expect(content).toContain('@echo off');
|
||||
|
||||
// contains the right endpoint
|
||||
expect(content).toContain('npipe:///endpoint.sock');
|
||||
});
|
||||
53
extensions/compose/src/compose-wrapper-generator.ts
Normal file
53
extensions/compose/src/compose-wrapper-generator.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import type { OS } from './os';
|
||||
import type * as extensionApi from '@podman-desktop/api';
|
||||
import { promises } from 'node:fs';
|
||||
import mustache from 'mustache';
|
||||
|
||||
import shMustacheTemplate from './templates/podman-compose.sh.mustache?raw';
|
||||
import batMustacheTemplate from './templates/podman-compose.bat.mustache?raw';
|
||||
|
||||
// Generate the script to run docker-compose by setting up all environment variables
|
||||
export class ComposeWrapperGenerator {
|
||||
constructor(private os: OS, private binFolder: string) {}
|
||||
|
||||
protected async generateContent(connection: extensionApi.ProviderContainerConnection): Promise<string> {
|
||||
// take first one
|
||||
const socketPath = connection.connection.endpoint.socketPath;
|
||||
|
||||
let template;
|
||||
if (this.os.isMac() || this.os.isLinux()) {
|
||||
template = shMustacheTemplate;
|
||||
} else {
|
||||
template = batMustacheTemplate;
|
||||
}
|
||||
|
||||
// render the template
|
||||
return mustache.render(template, { socketPath, binFolder: this.binFolder });
|
||||
}
|
||||
|
||||
async generate(connection: extensionApi.ProviderContainerConnection, path: string): Promise<void> {
|
||||
// generate content
|
||||
const content = await this.generateContent(connection);
|
||||
if (content.length > 0) {
|
||||
await promises.writeFile(path, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
219
extensions/compose/src/detect.spec.ts
Normal file
219
extensions/compose/src/detect.spec.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/**********************************************************************
|
||||
* 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 type { Mock, SpyInstance } from 'vitest';
|
||||
import * as shellPath from 'shell-path';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi, vitest } from 'vitest';
|
||||
import { Detect } from './detect';
|
||||
import type { CliRun } from './cli-run';
|
||||
import type { OS } from './os';
|
||||
import * as http from 'node:http';
|
||||
|
||||
const osMock: OS = {
|
||||
isWindows: vi.fn(),
|
||||
isLinux: vi.fn(),
|
||||
isMac: vi.fn(),
|
||||
};
|
||||
|
||||
const cliRunMock: CliRun = {
|
||||
extensionContext: {
|
||||
storagePath: '/storage-path',
|
||||
},
|
||||
runCommand: vi.fn(),
|
||||
getPath: vi.fn(),
|
||||
} as unknown as CliRun;
|
||||
|
||||
let detect: Detect;
|
||||
|
||||
vi.mock('shell-path', () => {
|
||||
return {
|
||||
shellPath: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const originalConsoleDebug = console.debug;
|
||||
|
||||
beforeEach(() => {
|
||||
console.debug = vi.fn();
|
||||
detect = new Detect(cliRunMock, osMock, '/storage-path');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
console.debug = originalConsoleDebug;
|
||||
});
|
||||
|
||||
describe('Check for Docker Compose', async () => {
|
||||
test('not installed', async () => {
|
||||
(cliRunMock.runCommand as Mock).mockResolvedValue({ exitCode: -1 });
|
||||
const result = await detect.checkForDockerCompose();
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('installed', async () => {
|
||||
(cliRunMock.runCommand as Mock).mockResolvedValue({ exitCode: 0 });
|
||||
const result = await detect.checkForDockerCompose();
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check for path', async () => {
|
||||
test('not included', async () => {
|
||||
(cliRunMock.runCommand as Mock).mockResolvedValue({ exitCode: -1 });
|
||||
vitest.spyOn(shellPath, 'shellPath').mockResolvedValue('/different-path');
|
||||
const result = await detect.checkStoragePath();
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('included', async () => {
|
||||
(cliRunMock.runCommand as Mock).mockResolvedValue({ exitCode: -1 });
|
||||
vitest.spyOn(shellPath, 'shellPath').mockResolvedValue('/storage-path/bin');
|
||||
const result = await detect.checkStoragePath();
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check default socket path', async () => {
|
||||
test('linux', async () => {
|
||||
(osMock.isLinux as Mock).mockReturnValue(true);
|
||||
(osMock.isMac as Mock).mockReturnValue(false);
|
||||
(osMock.isWindows as Mock).mockReturnValue(false);
|
||||
const result = await detect.getSocketPath();
|
||||
expect(result).toBe('/var/run/docker.sock');
|
||||
});
|
||||
|
||||
test('macOS', async () => {
|
||||
(osMock.isLinux as Mock).mockReturnValue(false);
|
||||
(osMock.isMac as Mock).mockReturnValue(true);
|
||||
(osMock.isWindows as Mock).mockReturnValue(false);
|
||||
const result = await detect.getSocketPath();
|
||||
expect(result).toBe('/var/run/docker.sock');
|
||||
});
|
||||
|
||||
test('windows', async () => {
|
||||
(osMock.isLinux as Mock).mockReturnValue(false);
|
||||
(osMock.isMac as Mock).mockReturnValue(false);
|
||||
(osMock.isWindows as Mock).mockReturnValue(true);
|
||||
const result = await detect.getSocketPath();
|
||||
expect(result).toBe('//./pipe/docker_engine');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check docker socket', async () => {
|
||||
test('is alive', async () => {
|
||||
const socketPathMock = vitest.spyOn(detect, 'getSocketPath');
|
||||
socketPathMock.mockResolvedValue('/foo/docker.sock');
|
||||
|
||||
// mock http request
|
||||
|
||||
vi.mock('node:http', () => {
|
||||
return {
|
||||
get: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const spyGet = vi.spyOn(http, 'get') as unknown as SpyInstance;
|
||||
const clientRequestEmitter = new EventEmitter();
|
||||
const myRequest = clientRequestEmitter as unknown as http.ClientRequest;
|
||||
|
||||
spyGet.mockImplementation((_url: any, callback: (res: http.IncomingMessage) => void) => {
|
||||
const emitter = new EventEmitter();
|
||||
callback(emitter as unknown as http.IncomingMessage);
|
||||
|
||||
// mock fake data
|
||||
emitter.emit('data', 'foo');
|
||||
|
||||
// mock a successful response
|
||||
(emitter as any).statusCode = 200;
|
||||
emitter.emit('end', {});
|
||||
return myRequest;
|
||||
});
|
||||
|
||||
const result = await detect.checkDefaultSocketIsAlive();
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('test ping invalid status', async () => {
|
||||
const socketPathMock = vitest.spyOn(detect, 'getSocketPath');
|
||||
socketPathMock.mockResolvedValue('/foo/docker.sock');
|
||||
|
||||
// mock http request
|
||||
|
||||
vi.mock('node:http', () => {
|
||||
return {
|
||||
get: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const spyGet = vi.spyOn(http, 'get') as unknown as SpyInstance;
|
||||
const clientRequestEmitter = new EventEmitter();
|
||||
const myRequest = clientRequestEmitter as unknown as http.ClientRequest;
|
||||
|
||||
spyGet.mockImplementation((_url: any, callback: (res: http.IncomingMessage) => void) => {
|
||||
const emitter = new EventEmitter();
|
||||
callback(emitter as unknown as http.IncomingMessage);
|
||||
|
||||
// mock an invalid response
|
||||
(emitter as any).statusCode = 500;
|
||||
emitter.emit('end', {});
|
||||
return myRequest;
|
||||
});
|
||||
|
||||
const result = await detect.checkDefaultSocketIsAlive();
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('test error', async () => {
|
||||
const socketPathMock = vitest.spyOn(detect, 'getSocketPath');
|
||||
socketPathMock.mockResolvedValue('/foo/docker.sock');
|
||||
|
||||
// mock http request
|
||||
|
||||
vi.mock('node:http', () => {
|
||||
return {
|
||||
get: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const spyGet = vi.spyOn(http, 'get') as unknown as SpyInstance;
|
||||
const clientRequestEmitter = new EventEmitter();
|
||||
const myRequest = clientRequestEmitter as unknown as http.ClientRequest;
|
||||
const spyOnce = vi.spyOn(clientRequestEmitter, 'once');
|
||||
|
||||
spyGet.mockImplementation((_url: any, callback: (res: http.IncomingMessage) => void) => {
|
||||
const emitter = new EventEmitter();
|
||||
callback(emitter as unknown as http.IncomingMessage);
|
||||
|
||||
// send an error
|
||||
setTimeout(() => {
|
||||
clientRequestEmitter.emit('error', new Error('test error'));
|
||||
}, 500);
|
||||
|
||||
return myRequest;
|
||||
});
|
||||
|
||||
const result = await detect.checkDefaultSocketIsAlive();
|
||||
expect(result).toBeFalsy();
|
||||
expect(spyOnce).toBeCalledWith('error', expect.any(Function));
|
||||
expect(console.debug).toBeCalledWith('Error while pinging docker', expect.any(Error));
|
||||
});
|
||||
});
|
||||
93
extensions/compose/src/detect.ts
Normal file
93
extensions/compose/src/detect.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import type { CliRun } from './cli-run';
|
||||
import { shellPath } from 'shell-path';
|
||||
import { resolve } from 'path';
|
||||
import * as http from 'node:http';
|
||||
import type { OS } from './os';
|
||||
|
||||
export class Detect {
|
||||
static readonly WINDOWS_SOCKET_PATH = '//./pipe/docker_engine';
|
||||
static readonly UNIX_SOCKET_PATH = '/var/run/docker.sock';
|
||||
|
||||
constructor(private cliRun: CliRun, private os: OS, private storagePath: string) {}
|
||||
|
||||
// search if docker-compose is available in the path (+ include storage/bin folder)
|
||||
async checkForDockerCompose(): Promise<boolean> {
|
||||
const result = await this.cliRun.runCommand('docker-compose', ['--version']);
|
||||
if (result.exitCode === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// search if the podman-compose is available in the storage/bin path
|
||||
async checkStoragePath(): Promise<boolean> {
|
||||
// check that extension/bin folder is in the PATH
|
||||
const extensionBinPath = resolve(this.storagePath, 'bin');
|
||||
|
||||
// grab current path
|
||||
const currentPath = await shellPath();
|
||||
if (currentPath.includes(extensionBinPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Async function that checks to see if the current Docker socket is a disguised Podman socket
|
||||
async checkDefaultSocketIsAlive(): Promise<boolean> {
|
||||
const socketPath = this.getSocketPath();
|
||||
|
||||
const podmanPingUrl = {
|
||||
path: '/_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', err);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function that checks whether you are running windows, mac or linux and returns back
|
||||
// the correct Docker socket location
|
||||
getSocketPath(): string {
|
||||
let socketPath: string = Detect.UNIX_SOCKET_PATH;
|
||||
if (this.os.isWindows()) {
|
||||
socketPath = Detect.WINDOWS_SOCKET_PATH;
|
||||
}
|
||||
return socketPath;
|
||||
}
|
||||
}
|
||||
54
extensions/compose/src/extension.ts
Normal file
54
extensions/compose/src/extension.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import type * as extensionApi from '@podman-desktop/api';
|
||||
import { CliRun } from './cli-run';
|
||||
import { Detect } from './detect';
|
||||
import { ComposeExtension } from './compose-extension';
|
||||
import { ComposeGitHubReleases } from './compose-github-releases';
|
||||
import { OS } from './os';
|
||||
import { ComposeWrapperGenerator } from './compose-wrapper-generator';
|
||||
import * as path from 'path';
|
||||
let composeExtension: ComposeExtension | undefined;
|
||||
|
||||
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
|
||||
// do not hold the activation promise
|
||||
setTimeout(() => postActivate(extensionContext), 0);
|
||||
}
|
||||
|
||||
async function postActivate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
|
||||
const octokit = new Octokit();
|
||||
const os = new OS();
|
||||
const cliRun = new CliRun(extensionContext, os);
|
||||
const podmanComposeGenerator = new ComposeWrapperGenerator(os, path.resolve(extensionContext.storagePath, 'bin'));
|
||||
const composeExtension = new ComposeExtension(
|
||||
extensionContext,
|
||||
new Detect(cliRun, os, extensionContext.storagePath),
|
||||
new ComposeGitHubReleases(octokit),
|
||||
os,
|
||||
podmanComposeGenerator,
|
||||
);
|
||||
await composeExtension.activate();
|
||||
}
|
||||
|
||||
export async function deactivate(): Promise<void> {
|
||||
if (composeExtension) {
|
||||
await composeExtension.deactivate();
|
||||
}
|
||||
}
|
||||
60
extensions/compose/src/os.spec.ts
Normal file
60
extensions/compose/src/os.spec.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import { afterEach, beforeEach, expect, test, vi, vitest } from 'vitest';
|
||||
import { OS } from './os';
|
||||
|
||||
let os: OS;
|
||||
beforeEach(() => {
|
||||
os = new OS();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('linux', async () => {
|
||||
vitest.spyOn(process, 'platform', 'get').mockReturnValue('linux');
|
||||
const isWindows = os.isWindows();
|
||||
const isLinux = os.isLinux();
|
||||
const isMac = os.isMac();
|
||||
expect(isWindows).toBeFalsy();
|
||||
expect(isLinux).toBeTruthy();
|
||||
expect(isMac).toBeFalsy();
|
||||
});
|
||||
|
||||
test('mac', async () => {
|
||||
vitest.spyOn(process, 'platform', 'get').mockReturnValue('darwin');
|
||||
const isWindows = os.isWindows();
|
||||
const isLinux = os.isLinux();
|
||||
const isMac = os.isMac();
|
||||
expect(isWindows).toBeFalsy();
|
||||
expect(isLinux).toBeFalsy();
|
||||
expect(isMac).toBeTruthy();
|
||||
});
|
||||
|
||||
test('windows', async () => {
|
||||
vitest.spyOn(process, 'platform', 'get').mockReturnValue('win32');
|
||||
const isWindows = os.isWindows();
|
||||
const isLinux = os.isLinux();
|
||||
const isMac = os.isMac();
|
||||
expect(isWindows).toBeTruthy();
|
||||
expect(isLinux).toBeFalsy();
|
||||
expect(isMac).toBeFalsy();
|
||||
});
|
||||
31
extensions/compose/src/os.ts
Normal file
31
extensions/compose/src/os.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
export class OS {
|
||||
isWindows(): boolean {
|
||||
return process.platform === 'win32';
|
||||
}
|
||||
|
||||
isMac(): boolean {
|
||||
return process.platform === 'darwin';
|
||||
}
|
||||
|
||||
isLinux(): boolean {
|
||||
return process.platform === 'linux';
|
||||
}
|
||||
}
|
||||
10
extensions/compose/src/templates/podman-compose.bat.mustache
Normal file
10
extensions/compose/src/templates/podman-compose.bat.mustache
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
@echo off
|
||||
rem This script is generated by Podman Desktop
|
||||
|
||||
rem Add into the PATH the folder where docker-compose is installed
|
||||
set PATH="%PATH%;{{{ binFolder }}}"
|
||||
|
||||
rem Set the DOCKER_HOST to the socket of the podman service
|
||||
set DOCKER_HOST=npipe://{{{ socketPath }}}
|
||||
|
||||
docker-compose %*
|
||||
10
extensions/compose/src/templates/podman-compose.sh.mustache
Normal file
10
extensions/compose/src/templates/podman-compose.sh.mustache
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
# This script is generated by Podman Desktop
|
||||
|
||||
# Add into the PATH the folder where docker-compose is installed
|
||||
export PATH="$PATH:{{{ binFolder }}}"
|
||||
|
||||
# Set the DOCKER_HOST to the socket of the podman service
|
||||
export DOCKER_HOST=unix://{{{ socketPath }}}
|
||||
|
||||
docker-compose "$@"
|
||||
4349
extensions/compose/tests/resources/compose-github-release-all.json
Normal file
4349
extensions/compose/tests/resources/compose-github-release-all.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,821 @@
|
|||
[
|
||||
{
|
||||
"//": "",
|
||||
"//GENERATED": "Generated from https://api.github.com/repos/docker/compose/releases/91727807/assets",
|
||||
"//": "",
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785271",
|
||||
"id": 94785271,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk73",
|
||||
"name": "checksums.txt",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 1050,
|
||||
"download_count": 524,
|
||||
"created_at": "2023-02-08T10:59:25Z",
|
||||
"updated_at": "2023-02-08T10:59:26Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/checksums.txt"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785273",
|
||||
"id": 94785273,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk75",
|
||||
"name": "docker-compose-darwin-aarch64",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 52788706,
|
||||
"download_count": 700,
|
||||
"created_at": "2023-02-08T10:59:26Z",
|
||||
"updated_at": "2023-02-08T10:59:29Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-darwin-aarch64"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785280",
|
||||
"id": 94785280,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8A",
|
||||
"name": "docker-compose-darwin-aarch64.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 96,
|
||||
"download_count": 240,
|
||||
"created_at": "2023-02-08T10:59:29Z",
|
||||
"updated_at": "2023-02-08T10:59:29Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-darwin-aarch64.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785284",
|
||||
"id": 94785284,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8E",
|
||||
"name": "docker-compose-darwin-x86_64",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 53625440,
|
||||
"download_count": 1979,
|
||||
"created_at": "2023-02-08T10:59:30Z",
|
||||
"updated_at": "2023-02-08T10:59:33Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-darwin-x86_64"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785296",
|
||||
"id": 94785296,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8Q",
|
||||
"name": "docker-compose-darwin-x86_64.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 95,
|
||||
"download_count": 330,
|
||||
"created_at": "2023-02-08T10:59:33Z",
|
||||
"updated_at": "2023-02-08T10:59:34Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-darwin-x86_64.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785298",
|
||||
"id": 94785298,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8S",
|
||||
"name": "docker-compose-linux-aarch64",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 45481984,
|
||||
"download_count": 3783,
|
||||
"created_at": "2023-02-08T10:59:34Z",
|
||||
"updated_at": "2023-02-08T10:59:37Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-aarch64"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785312",
|
||||
"id": 94785312,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8g",
|
||||
"name": "docker-compose-linux-aarch64.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 95,
|
||||
"download_count": 122,
|
||||
"created_at": "2023-02-08T10:59:38Z",
|
||||
"updated_at": "2023-02-08T10:59:38Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-aarch64.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785314",
|
||||
"id": 94785314,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8i",
|
||||
"name": "docker-compose-linux-armv6",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 44957696,
|
||||
"download_count": 251,
|
||||
"created_at": "2023-02-08T10:59:38Z",
|
||||
"updated_at": "2023-02-08T10:59:41Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-armv6"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785317",
|
||||
"id": 94785317,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8l",
|
||||
"name": "docker-compose-linux-armv6.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 93,
|
||||
"download_count": 48,
|
||||
"created_at": "2023-02-08T10:59:41Z",
|
||||
"updated_at": "2023-02-08T10:59:41Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-armv6.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785322",
|
||||
"id": 94785322,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8q",
|
||||
"name": "docker-compose-linux-armv7",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 44957696,
|
||||
"download_count": 609,
|
||||
"created_at": "2023-02-08T10:59:42Z",
|
||||
"updated_at": "2023-02-08T10:59:44Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-armv7"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785329",
|
||||
"id": 94785329,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk8x",
|
||||
"name": "docker-compose-linux-armv7.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 93,
|
||||
"download_count": 34,
|
||||
"created_at": "2023-02-08T10:59:45Z",
|
||||
"updated_at": "2023-02-08T10:59:48Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-armv7.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785335",
|
||||
"id": 94785335,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk83",
|
||||
"name": "docker-compose-linux-ppc64le",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 46530560,
|
||||
"download_count": 41,
|
||||
"created_at": "2023-02-08T10:59:48Z",
|
||||
"updated_at": "2023-02-08T10:59:52Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-ppc64le"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785347",
|
||||
"id": 94785347,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9D",
|
||||
"name": "docker-compose-linux-ppc64le.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 95,
|
||||
"download_count": 26,
|
||||
"created_at": "2023-02-08T10:59:52Z",
|
||||
"updated_at": "2023-02-08T10:59:53Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-ppc64le.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785355",
|
||||
"id": 94785355,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9L",
|
||||
"name": "docker-compose-linux-riscv64",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 45809664,
|
||||
"download_count": 32,
|
||||
"created_at": "2023-02-08T10:59:53Z",
|
||||
"updated_at": "2023-02-08T10:59:56Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-riscv64"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785362",
|
||||
"id": 94785362,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9S",
|
||||
"name": "docker-compose-linux-riscv64.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 95,
|
||||
"download_count": 25,
|
||||
"created_at": "2023-02-08T10:59:56Z",
|
||||
"updated_at": "2023-02-08T10:59:56Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-riscv64.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785364",
|
||||
"id": 94785364,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9U",
|
||||
"name": "docker-compose-linux-s390x",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 50397184,
|
||||
"download_count": 45,
|
||||
"created_at": "2023-02-08T10:59:57Z",
|
||||
"updated_at": "2023-02-08T11:00:00Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-s390x"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785373",
|
||||
"id": 94785373,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9d",
|
||||
"name": "docker-compose-linux-s390x.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 93,
|
||||
"download_count": 25,
|
||||
"created_at": "2023-02-08T11:00:00Z",
|
||||
"updated_at": "2023-02-08T11:00:00Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-s390x.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785376",
|
||||
"id": 94785376,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9g",
|
||||
"name": "docker-compose-linux-x86_64",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 47706112,
|
||||
"download_count": 89460,
|
||||
"created_at": "2023-02-08T11:00:01Z",
|
||||
"updated_at": "2023-02-08T11:00:05Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-x86_64"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785395",
|
||||
"id": 94785395,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9z",
|
||||
"name": "docker-compose-linux-x86_64.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 94,
|
||||
"download_count": 5370,
|
||||
"created_at": "2023-02-08T11:00:05Z",
|
||||
"updated_at": "2023-02-08T11:00:05Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-x86_64.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785398",
|
||||
"id": 94785398,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk92",
|
||||
"name": "docker-compose-windows-aarch64.exe",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 46059520,
|
||||
"download_count": 48,
|
||||
"created_at": "2023-02-08T11:00:06Z",
|
||||
"updated_at": "2023-02-08T11:00:09Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-windows-aarch64.exe"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785406",
|
||||
"id": 94785406,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk9-",
|
||||
"name": "docker-compose-windows-aarch64.exe.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 101,
|
||||
"download_count": 27,
|
||||
"created_at": "2023-02-08T11:00:09Z",
|
||||
"updated_at": "2023-02-08T11:00:10Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-windows-aarch64.exe.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785408",
|
||||
"id": 94785408,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk-A",
|
||||
"name": "docker-compose-windows-x86_64.exe",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 48416256,
|
||||
"download_count": 2864,
|
||||
"created_at": "2023-02-08T11:00:10Z",
|
||||
"updated_at": "2023-02-08T11:00:13Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-windows-x86_64.exe"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785413",
|
||||
"id": 94785413,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk-F",
|
||||
"name": "docker-compose-windows-x86_64.exe.sha256",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 100,
|
||||
"download_count": 281,
|
||||
"created_at": "2023-02-08T11:00:13Z",
|
||||
"updated_at": "2023-02-08T11:00:13Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-windows-x86_64.exe.sha256"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/docker/compose/releases/assets/94785416",
|
||||
"id": 94785416,
|
||||
"node_id": "RA_kwDOAOWUd84Fpk-I",
|
||||
"name": "LICENSE",
|
||||
"label": "",
|
||||
"uploader": {
|
||||
"login": "github-actions[bot]",
|
||||
"id": 41898282,
|
||||
"node_id": "MDM6Qm90NDE4OTgyODI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/github-actions",
|
||||
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"site_admin": false
|
||||
},
|
||||
"content_type": "raw",
|
||||
"state": "uploaded",
|
||||
"size": 300,
|
||||
"download_count": 33,
|
||||
"created_at": "2023-02-08T11:00:14Z",
|
||||
"updated_at": "2023-02-08T11:00:14Z",
|
||||
"browser_download_url": "https://github.com/docker/compose/releases/download/v2.16.0/LICENSE"
|
||||
}
|
||||
]
|
||||
25
extensions/compose/tsconfig.json
Normal file
25
extensions/compose/tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"lib": [
|
||||
"ES2017",
|
||||
"webworker"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
]
|
||||
},
|
||||
|
||||
"include": [
|
||||
"src",
|
||||
"types/*.d.ts",
|
||||
"../../types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
22
extensions/compose/types/template.d.ts
vendored
Normal file
22
extensions/compose/types/template.d.ts
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
declare module '*.mustache' {
|
||||
const contents: string;
|
||||
export = contents;
|
||||
}
|
||||
84
extensions/compose/vite.config.js
Normal file
84
extensions/compose/vite.config.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
import {join} from 'path';
|
||||
import {builtinModules} from 'module';
|
||||
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
|
||||
export function coverageConfig(packageRoot) {
|
||||
const obj = { coverage: {
|
||||
all: true,
|
||||
clean: true,
|
||||
exclude: [
|
||||
'**/dist/**',
|
||||
'**/node_modules/**',
|
||||
'**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'**/*.{svelte,tsx,cjs,js,d.ts}',
|
||||
'**/*-info.ts',
|
||||
'**/.{cache,git,idea,output,temp,cdix}/**',
|
||||
'**/*{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tailwind,postcss}.config.*',
|
||||
],
|
||||
provider: 'c8',
|
||||
reportsDirectory: `../../test-resources/coverage/${packageRoot}`,
|
||||
reporter: ['lcov', 'json', 'text-summary'],
|
||||
},
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
/**
|
||||
* @type {import('vite').UserConfig}
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
const config = {
|
||||
mode: process.env.MODE,
|
||||
root: PACKAGE_ROOT,
|
||||
envDir: process.cwd(),
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/': join(PACKAGE_ROOT, 'src') + '/',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: 'inline',
|
||||
target: 'esnext',
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: process.env.MODE === 'production' ? 'esbuild' : false,
|
||||
lib: {
|
||||
entry: 'src/extension.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'@podman-desktop/api',
|
||||
...builtinModules.flatMap(p => [p, `node:${p}`]),
|
||||
],
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: false,
|
||||
},
|
||||
test: {
|
||||
...coverageConfig('main'),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -159,6 +159,7 @@ const setupExtensionApiWatcher = name => {
|
|||
});
|
||||
|
||||
await viteDevServer.listen();
|
||||
await setupExtensionApiWatcher('compose');
|
||||
await setupExtensionApiWatcher('docker');
|
||||
await setupExtensionApiWatcher('kube-context');
|
||||
await setupExtensionApiWatcher('lima');
|
||||
|
|
|
|||
32
yarn.lock
32
yarn.lock
|
|
@ -7,7 +7,7 @@
|
|||
resolved "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz"
|
||||
integrity sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==
|
||||
|
||||
"7zip-min@^1.4.4":
|
||||
"7zip-min@^1.4.3", "7zip-min@^1.4.4":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/7zip-min/-/7zip-min-1.4.4.tgz#82a50a8d3f0a2d86b4c908433c9ec35627f4138c"
|
||||
integrity sha512-mYB1WW5tcXfZxUN4+2joKk4+6j8jp+mpO2YiMU5z1gNNFbACxI2ADasffsdNPovZSwn/E662ZIH5gRkFPMufmA==
|
||||
|
|
@ -5520,6 +5520,11 @@ default-gateway@^6.0.3:
|
|||
dependencies:
|
||||
execa "^5.0.0"
|
||||
|
||||
default-shell@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-2.2.0.tgz#31481c19747bfe59319b486591643eaf115a1864"
|
||||
integrity sha512-sPpMZcVhRQ0nEMDtuMJ+RtCxt7iHPAMBU+I4tAlo5dU1sjRpNax0crj6nR3qKpvVnckaQ9U38enXcwW9nZJeCw==
|
||||
|
||||
defer-to-connect@^1.0.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz"
|
||||
|
|
@ -6354,7 +6359,7 @@ events@^3.2.0:
|
|||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||
|
||||
execa@^5.0.0:
|
||||
execa@^5.0.0, execa@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
|
||||
|
|
@ -8925,7 +8930,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
|
|||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mkdirp@^2.1.5:
|
||||
mkdirp@^2.1.3, mkdirp@^2.1.5:
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.5.tgz#78d7eaf15e069ba7b6b47d76dd94cfadf7a4062f"
|
||||
integrity sha512-jbjfql+shJtAPrFoKxHOXip4xS+kul9W3OzfzzrqueWK2QMGon2bFH2opl6W9EagBThjEz+iysyi/swOoVfB/w==
|
||||
|
|
@ -8998,6 +9003,11 @@ multicast-dns@^7.2.5:
|
|||
dns-packet "^5.2.2"
|
||||
thunky "^1.0.2"
|
||||
|
||||
mustache@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
|
||||
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
|
||||
|
||||
nan@^2.15.0, nan@^2.16.0:
|
||||
version "2.17.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||
|
|
@ -11058,6 +11068,22 @@ shebang-regex@^3.0.0:
|
|||
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-env@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-4.0.1.tgz#883302d9426095d398a39b102a851adb306b8cb8"
|
||||
integrity sha512-w3oeZ9qg/P6Lu6qqwavvMnB/bwfsz67gPB3WXmLd/n6zuh7TWQZtGa3iMEdmua0kj8rivkwl+vUjgLWlqZOMPw==
|
||||
dependencies:
|
||||
default-shell "^2.0.0"
|
||||
execa "^5.1.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
shell-path@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-path/-/shell-path-3.0.0.tgz#5c95bc68aade43c06082a0655cb5c97586e4feb0"
|
||||
integrity sha512-HNIZ+W/3P0JuVTV03xjGqYKt3e3h0/Z4AH8TQWeth1LBtCusSjICgkdNdb3VZr6mI7ijE2AiFFpgkVMNKsALeQ==
|
||||
dependencies:
|
||||
shell-env "^4.0.0"
|
||||
|
||||
shell-quote@^1.7.3:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
|
||||
|
|
|
|||
Loading…
Reference in a new issue