Skip to content

Commit

Permalink
Automatic fallback to docker-compose v2 (#620)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianrgreco authored Jul 20, 2023
1 parent f3d896b commit f9e4aa6
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 168 deletions.
20 changes: 13 additions & 7 deletions src/docker-compose-environment/docker-compose-environment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ContainerInfo } from "dockerode";
import { BoundPorts } from "../bound-ports";
import { resolveContainerName } from "../docker-compose/functions/container-name-resolver";
import { resolveContainerName } from "../docker-compose/container-name-resolver";
import { StartedGenericContainer } from "../generic-container/started-generic-container";
import { containerLog, log } from "../logger";
import { WaitStrategy } from "../wait-strategy/wait-strategy";
Expand All @@ -12,11 +12,8 @@ import { getDockerClient } from "../docker/client/docker-client";
import { inspectContainer } from "../docker/functions/container/inspect-container";
import { containerLogs } from "../docker/functions/container/container-logs";
import { StartedDockerComposeEnvironment } from "./started-docker-compose-environment";
import { dockerComposeDown } from "../docker-compose/functions/docker-compose-down";
import { dockerComposeUp } from "../docker-compose/functions/docker-compose-up";
import { waitForContainer } from "../wait-for-container";
import { DefaultPullPolicy, PullPolicy } from "../pull-policy";
import { dockerComposePull } from "../docker-compose/functions/docker-compose-pull";
import { Wait } from "../wait-strategy/wait";
import { registerComposeProjectForCleanup } from "../reaper";

Expand Down Expand Up @@ -83,6 +80,7 @@ export class DockerComposeEnvironment {

public async up(services?: Array<string>): Promise<StartedDockerComposeEnvironment> {
log.info(`Starting DockerCompose environment "${this.projectName}"...`);
const { dockerComposeClient } = await getDockerClient();
await registerComposeProjectForCleanup(this.projectName);

const options = {
Expand All @@ -106,9 +104,17 @@ export class DockerComposeEnvironment {
this.profiles.forEach((profile) => composeOptions.push("--profile", profile));

if (this.pullPolicy.shouldPull()) {
await dockerComposePull(options, services);
await dockerComposeClient.pull(options, services);
}
await dockerComposeUp({ ...options, commandOptions, composeOptions, environment: this.environment }, services);
await dockerComposeClient.up(
{
...options,
commandOptions,
composeOptions,
environment: this.environment,
},
services
);

const startedContainers = (await listContainers()).filter(
(container) => container.Labels["com.docker.compose.project"] === this.projectName
Expand Down Expand Up @@ -150,7 +156,7 @@ export class DockerComposeEnvironment {
await waitForContainer(container, waitStrategy, boundPorts);
} catch (err) {
try {
await dockerComposeDown(options, { removeVolumes: true, timeout: 0 });
await dockerComposeClient.down(options, { removeVolumes: true, timeout: 0 });
} catch {
log.warn(`Failed to stop DockerCompose environment after failed up`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { StartedGenericContainer } from "../generic-container/started-generic-container";
import { log } from "../logger";
import { dockerComposeDown } from "../docker-compose/functions/docker-compose-down";
import { dockerComposeStop } from "../docker-compose/functions/docker-compose-stop";
import { StoppedDockerComposeEnvironment } from "./stopped-docker-compose-environment";
import { DownedDockerComposeEnvironment } from "./downed-docker-compose-environment";
import { DockerComposeOptions } from "../docker-compose/docker-compose-options";
import { DockerComposeDownOptions } from "../test-container";
import { DockerComposeDownOptions, DockerComposeOptions } from "../docker-compose/docker-compose-options";
import { getDockerClient } from "../docker/client/docker-client";

export class StartedDockerComposeEnvironment {
constructor(
Expand All @@ -14,13 +12,15 @@ export class StartedDockerComposeEnvironment {
) {}

public async stop(): Promise<StoppedDockerComposeEnvironment> {
await dockerComposeStop(this.options);
const { dockerComposeClient } = await getDockerClient();
await dockerComposeClient.stop(this.options);
return new StoppedDockerComposeEnvironment(this.options);
}

public async down(options: Partial<DockerComposeDownOptions> = {}): Promise<DownedDockerComposeEnvironment> {
const { dockerComposeClient } = await getDockerClient();
const downOptions: DockerComposeDownOptions = { timeout: 0, removeVolumes: true, ...options };
await dockerComposeDown(this.options, downOptions);
await dockerComposeClient.down(this.options, downOptions);
return new DownedDockerComposeEnvironment();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { dockerComposeDown } from "../docker-compose/functions/docker-compose-down";
import { DownedDockerComposeEnvironment } from "./downed-docker-compose-environment";
import { DockerComposeOptions } from "../docker-compose/docker-compose-options";
import { DockerComposeDownOptions } from "../test-container";
import { DockerComposeDownOptions, DockerComposeOptions } from "../docker-compose/docker-compose-options";
import { getDockerClient } from "../docker/client/docker-client";

export class StoppedDockerComposeEnvironment {
constructor(private readonly options: DockerComposeOptions) {}

public async down(options: Partial<DockerComposeDownOptions> = {}): Promise<DownedDockerComposeEnvironment> {
const { dockerComposeClient } = await getDockerClient();
const resolvedOptions: DockerComposeDownOptions = { timeout: 0, removeVolumes: true, ...options };
await dockerComposeDown(this.options, resolvedOptions);
await dockerComposeClient.down(this.options, resolvedOptions);
return new DownedDockerComposeEnvironment();
}
}
37 changes: 0 additions & 37 deletions src/docker-compose/default-docker-compose-options.ts

This file was deleted.

195 changes: 195 additions & 0 deletions src/docker-compose/docker-compose-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { DockerComposeDownOptions, DockerComposeOptions } from "./docker-compose-options";
import { log, pullLog } from "../logger";
import v1, { v2 } from "docker-compose";
import { defaultDockerComposeOptions } from "./docker-compose-options";
import { DockerComposeCompatibility } from "../system-info";

export interface DockerComposeClient {
up: (options: DockerComposeOptions, services?: Array<string>) => Promise<void>;
pull: (options: DockerComposeOptions, services?: Array<string>) => Promise<void>;
stop: (options: DockerComposeOptions) => Promise<void>;
down: (options: DockerComposeOptions, downOptions: DockerComposeDownOptions) => Promise<void>;
}

export function getDockerComposeClient(compat?: DockerComposeCompatibility): DockerComposeClient {
switch (compat) {
case undefined:
return new MissingDockerComposeClient();
case "v1":
return new DockerComposeV1Client();
case "v2":
return new DockerComposeV2Client();
}
}

class DockerComposeV1Client implements DockerComposeClient {
async up(options: DockerComposeOptions, services: Array<string> | undefined): Promise<void> {
try {
if (services) {
log.info(`Upping DockerCompose environment services ${services.join(", ")}...`);
await v1.upMany(services, await defaultDockerComposeOptions(options));
} else {
log.info(`Upping DockerCompose environment...`);
await v1.upAll(await defaultDockerComposeOptions(options));
}
log.info(`Upped DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) => {
try {
log.error(`Failed to up DockerCompose environment: ${error.message}`);
await this.down(options, { removeVolumes: true, timeout: 0 });
} catch {
log.error(`Failed to down DockerCompose environment after failed up`);
}
});
}
}

async pull(options: DockerComposeOptions, services: Array<string> | undefined): Promise<void> {
try {
if (services) {
log.info(`Pulling DockerCompose environment images "${services.join('", "')}"...`);
await v1.pullMany(services, await defaultDockerComposeOptions({ ...options, logger: pullLog }));
} else {
log.info(`Pulling DockerCompose environment images...`);
await v1.pullAll(await defaultDockerComposeOptions({ ...options, logger: pullLog }));
}
log.info(`Pulled DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to pull DockerCompose environment images: ${error.message}`)
);
}
}

async stop(options: DockerComposeOptions): Promise<void> {
try {
log.info(`Stopping DockerCompose environment...`);
await v1.stop(await defaultDockerComposeOptions(options));
log.info(`Stopped DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to stop DockerCompose environment: ${error.message}`)
);
}
}

async down(options: DockerComposeOptions, downOptions: DockerComposeDownOptions): Promise<void> {
try {
log.info(`Downing DockerCompose environment...`);
await v1.down({
...(await defaultDockerComposeOptions(options)),
commandOptions: dockerComposeDownCommandOptions(downOptions),
});
log.info(`Downed DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to down DockerCompose environment: ${error.message}`)
);
}
}
}

class DockerComposeV2Client implements DockerComposeClient {
async up(options: DockerComposeOptions, services: Array<string> | undefined): Promise<void> {
try {
if (services) {
log.info(`Upping DockerCompose environment services ${services.join(", ")}...`);
await v2.upMany(services, await defaultDockerComposeOptions(options));
} else {
log.info(`Upping DockerCompose environment...`);
await v2.upAll(await defaultDockerComposeOptions(options));
}
log.info(`Upped DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) => {
try {
log.error(`Failed to up DockerCompose environment: ${error.message}`);
await this.down(options, { removeVolumes: true, timeout: 0 });
} catch {
log.error(`Failed to down DockerCompose environment after failed up`);
}
});
}
}

async pull(options: DockerComposeOptions, services: Array<string> | undefined): Promise<void> {
try {
if (services) {
log.info(`Pulling DockerCompose environment images "${services.join('", "')}"...`);
await v2.pullMany(services, await defaultDockerComposeOptions({ ...options, logger: pullLog }));
} else {
log.info(`Pulling DockerCompose environment images...`);
await v2.pullAll(await defaultDockerComposeOptions({ ...options, logger: pullLog }));
}
log.info(`Pulled DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to pull DockerCompose environment images: ${error.message}`)
);
}
}

async stop(options: DockerComposeOptions): Promise<void> {
try {
log.info(`Stopping DockerCompose environment...`);
await v2.stop(await defaultDockerComposeOptions(options));
log.info(`Stopped DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to stop DockerCompose environment: ${error.message}`)
);
}
}

async down(options: DockerComposeOptions, downOptions: DockerComposeDownOptions): Promise<void> {
try {
log.info(`Downing DockerCompose environment...`);
await v2.down({
...(await defaultDockerComposeOptions(options)),
commandOptions: dockerComposeDownCommandOptions(downOptions),
});
log.info(`Downed DockerCompose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to down DockerCompose environment: ${error.message}`)
);
}
}
}

class MissingDockerComposeClient implements DockerComposeClient {
up(): Promise<void> {
throw new Error("DockerCompose is not installed");
}

pull(): Promise<void> {
throw new Error("DockerCompose is not installed");
}

stop(): Promise<void> {
throw new Error("DockerCompose is not installed");
}

down(): Promise<void> {
throw new Error("DockerCompose is not installed");
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function handleAndRethrow(err: any, handle: (error: Error) => Promise<void>): Promise<never> {
const error = err instanceof Error ? err : new Error(err.err.trim());
await handle(error);
throw error;
}

function dockerComposeDownCommandOptions(options: DockerComposeDownOptions): string[] {
const result: string[] = [];
if (options.removeVolumes) {
result.push("-v");
}
if (options.timeout) {
result.push("-t", `${options.timeout / 1000}`);
}
return result;
}
42 changes: 41 additions & 1 deletion src/docker-compose/docker-compose-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Environment } from "../docker/types";
import { Logger } from "../logger";
import { composeLog, Logger } from "../logger";
import { IDockerComposeOptions } from "docker-compose";
import { getDockerClient } from "../docker/client/docker-client";
import { EOL } from "os";
import { isNotEmptyString } from "../type-guards";

export type DockerComposeOptions = {
filePath: string;
Expand All @@ -10,3 +14,39 @@ export type DockerComposeOptions = {
environment?: Environment;
logger?: Logger;
};

export const defaultDockerComposeOptions = async ({
environment = {},
...options
}: DockerComposeOptions): Promise<Partial<IDockerComposeOptions>> => {
const { composeEnvironment } = await getDockerClient();
const log = options.logger ?? composeLog;

return {
log: false,
callback: log.enabled()
? (chunk) => {
chunk
.toString()
.split(EOL)
.filter(isNotEmptyString)
.forEach((line) => log.trace(line.trim()));
}
: undefined,
cwd: options.filePath,
config: options.files,
composeOptions: options.composeOptions,
commandOptions: options.commandOptions,
env: {
...process.env,
COMPOSE_PROJECT_NAME: options.projectName,
...composeEnvironment,
...environment,
},
};
};

export type DockerComposeDownOptions = {
timeout: number;
removeVolumes: boolean;
};
Loading

0 comments on commit f9e4aa6

Please sign in to comment.