Skip to content

Commit 8ee2ae0

Browse files
authored
Podman support (#1859)
1 parent f6b82c5 commit 8ee2ae0

1 file changed

Lines changed: 86 additions & 4 deletions

File tree

src/utils/index.ts

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ export function notNull(el: any): boolean {
459459
return el !== null;
460460
}
461461

462-
/** Determine the compose command to use (`docker-compose` or `docker compose`). */
462+
/** Determine the compose command to use (`docker-compose`, `docker compose`, `podman compose`, or `podman-compose`). */
463463
async function composeCommand(cwd?: string): Promise<string> {
464464
return new Promise<string>((resolve) => {
465465
let cmd = "docker compose";
@@ -468,11 +468,89 @@ async function composeCommand(cwd?: string): Promise<string> {
468468
// 'docker compose' is not present, so default to 'docker-compose'
469469
cmd = "docker-compose";
470470
}
471-
resolve(cmd);
471+
exec(`${cmd} version`, { cwd }, (error) => {
472+
if (error) {
473+
// Neither Docker Compose variant is present; try Podman Compose. 'podman compose' can locate a
474+
// provider that a plain PATH lookup for 'podman-compose' wouldn't, so try it first.
475+
cmd = "podman compose";
476+
}
477+
exec(`${cmd} version`, { cwd }, (error) => {
478+
if (error) {
479+
cmd = "podman-compose";
480+
}
481+
resolve(cmd);
482+
});
483+
});
472484
});
473485
});
474486
}
475487

488+
/**
489+
* Podman's compose tooling doesn't support the same `ps --services --filter status=running` and
490+
* `port` output format as Docker Compose (bare port number instead of a host:port pair, and no
491+
* `--services`/`--filter` flags), so it needs its own port/running-state resolution.
492+
*/
493+
async function portFromPodmanCompose(
494+
cmd: string,
495+
cwd: string,
496+
file: string,
497+
service: string,
498+
internalPort: number,
499+
internalSuperserverPort: number,
500+
dockerCompose: { internalSuperserverPort?: number },
501+
result: { port: number | null; superserverPort: number | null; docker: boolean; service?: string }
502+
): Promise<typeof result> {
503+
const ps = await run("ps --format json");
504+
let containers: { State?: string; Labels?: Record<string, string> }[] = [];
505+
try {
506+
containers = JSON.parse(ps);
507+
} catch {
508+
// Leave containers empty; treated as not running below
509+
}
510+
if (!containers.some((c) => c.State === "running" && c.Labels?.["com.docker.compose.service"] === service)) {
511+
throw `Service '${service}' not found in '${path.join(cwd, file)}', or not running.`;
512+
}
513+
const port = parsePort(await run(`port --protocol=tcp ${service} ${internalPort}`));
514+
if (!port) {
515+
throw `Webserver port ${internalPort} not published for service '${service}' in '${path.join(cwd, file)}'.`;
516+
}
517+
result.port = parseInt(port, 10);
518+
let superserverPort: string | undefined;
519+
try {
520+
superserverPort = parsePort(await run(`port --protocol=tcp ${service} ${internalSuperserverPort}`));
521+
} catch (error) {
522+
// Not an error if we were merely looking for the default port and the container doesn't publish it
523+
if (!dockerCompose.internalSuperserverPort) return result;
524+
throw error;
525+
}
526+
if (!superserverPort) {
527+
throw `Superserver port ${internalSuperserverPort} not published for service '${service}' in '${path.join(cwd, file)}'.`;
528+
}
529+
result.superserverPort = parseInt(superserverPort, 10);
530+
return result;
531+
532+
// Runs a compose subcommand and returns its stdout, rejecting with a plain string on failure.
533+
function run(args: string): Promise<string> {
534+
return new Promise((resolve, reject) => {
535+
exec(`${cmd} ${args}`, { cwd }, (error, stdout) => {
536+
if (error) {
537+
reject(error.message);
538+
return;
539+
}
540+
resolve(stdout);
541+
});
542+
});
543+
}
544+
545+
function parsePort(stdout: string) {
546+
return stdout
547+
.trim()
548+
.split("\n")
549+
.pop()
550+
?.match(/(\d+)$/)?.[1];
551+
}
552+
}
553+
476554
export async function portFromDockerCompose(
477555
workspaceFolderName?: string
478556
): Promise<{ port: number | null; superserverPort: number | null; docker: boolean; service?: string }> {
@@ -529,6 +607,10 @@ export async function portFromDockerCompose(
529607
const envFileParam = envFile ? `--env-file ${envFile}` : "";
530608
const cmd = `${await composeCommand(cwd)} -f ${file} ${envFileParam} `;
531609

610+
if (cmd.startsWith("podman compose ") || cmd.startsWith("podman-compose ")) {
611+
return portFromPodmanCompose(cmd, cwd, file, service, internalPort, internalSuperserverPort, dockerCompose, result);
612+
}
613+
532614
return new Promise((resolve, reject) => {
533615
exec(`${cmd} ps --services --filter status=running`, { cwd }, (error, stdout) => {
534616
if (error) {
@@ -580,7 +662,7 @@ export async function terminalWithDocker(): Promise<vscode.Terminal> {
580662
if (!terminal) {
581663
let exe = await composeCommand();
582664
const argsArr: string[] = [];
583-
if (exe == "docker compose") {
665+
if (exe.includes(" ")) {
584666
const exeSplit = exe.split(" ");
585667
exe = exeSplit[0];
586668
argsArr.push(exeSplit[1]);
@@ -615,7 +697,7 @@ export async function shellWithDocker(): Promise<vscode.Terminal> {
615697
if (!terminal) {
616698
let exe = await composeCommand();
617699
const argsArr: string[] = [];
618-
if (exe == "docker compose") {
700+
if (exe.includes(" ")) {
619701
const exeSplit = exe.split(" ");
620702
exe = exeSplit[0];
621703
argsArr.push(exeSplit[1]);

0 commit comments

Comments
 (0)