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:
Florent BENOIT 2023-03-16 18:47:47 +01:00 committed by GitHub
parent c482fd0dc0
commit e884bc8964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 7144 additions and 3 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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 "$@"

File diff suppressed because one or more lines are too long

View file

@ -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"
}
]

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

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

View file

@ -159,6 +159,7 @@ const setupExtensionApiWatcher = name => {
});
await viteDevServer.listen();
await setupExtensionApiWatcher('compose');
await setupExtensionApiWatcher('docker');
await setupExtensionApiWatcher('kube-context');
await setupExtensionApiWatcher('lima');

View file

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