the-moby-effect
Version:
Moby/Docker API client built using effect-ts
306 lines • 14.6 kB
JavaScript
import * as PlatformError from "@effect/platform/Error";
import * as Socket from "@effect/platform/Socket";
import * as Array from "effect/Array";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Function from "effect/Function";
import * as Layer from "effect/Layer";
import * as Predicate from "effect/Predicate";
import * as Record from "effect/Record";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as String from "effect/String";
import * as Tuple from "effect/Tuple";
import * as DockerEngine from "../../DockerEngine.js";
import * as MobyDemux from "../../MobyDemux.js";
import * as MobyEndpoints from "../../MobyEndpoints.js";
/** @internal */
export const DockerComposeErrorTypeId = /*#__PURE__*/Symbol.for("@the-moby-effect/engines/DockerCompose/DockerComposeError");
/** @internal */
export const TypeId = /*#__PURE__*/Symbol.for("@the-moby-effect/engines/DockerCompose");
/** @internal */
export const DockerComposeProjectTypeId = /*#__PURE__*/Symbol.for("@the-moby-effect/engines/DockerComposeProject");
/** @internal */
export const isDockerComposeError = u => Predicate.hasProperty(u, DockerComposeErrorTypeId);
/** @internal */
export class DockerComposeError extends /*#__PURE__*/PlatformError.TypeIdError(DockerComposeErrorTypeId, "DockerComposeError") {
get message() {
return `${this.method}`;
}
}
/** @internal */
export const DockerCompose = /*#__PURE__*/Context.GenericTag("@the-moby-effect/engines/DockerCompose");
/** @internal */
export const make = /*#__PURE__*/Effect.gen(function* () {
const containers = yield* MobyEndpoints.Containers;
yield* DockerEngine.pingHead();
const dindContainerId = yield* DockerEngine.runScoped({
spec: {
Image: "docker.io/library/docker:latest",
Entrypoint: ["/bin/sh"],
Tty: false,
OpenStdin: true,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
HostConfig: {
Privileged: true,
Binds: ["/var/run/docker.sock:/var/run/docker.sock"]
}
}
}).pipe(Effect.map(({
Id
}) => Id));
// Helper to upload the project to the dind
const uploadProject = project => Effect.catchTag(containers.putArchive(dindContainerId, {
path: "/",
stream: project
}), "ContainersError", cause => new DockerComposeError({
method: "uploadProject",
cause
}));
// Convert a string from camelCase to kebab-case
const camelToKebab = Function.compose(String.camelToSnake, String.snakeToKebab);
// Stringify options for a command
const stringifyOptions = options => Function.pipe(options, Record.toEntries, Array.filter(Function.flow(Tuple.getSecond, Predicate.isNotUndefined)), Array.flatMap(([key, value]) => {
if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) {
return [Tuple.make(key, value)];
} else if (Array.isArray(value)) {
return Array.map(value, v => [key, v]);
} else if (Predicate.isRecord(value)) {
return Record.toEntries(value);
} else {
return Function.absurd(value);
}
}), Array.map(([key, value]) => `--${camelToKebab(key)}=${value}`), Array.join(" "));
// Helper to run a command in the dind
const runCommand = (method, services = [], options = {}) => Effect.gen(function* () {
const multiplexed = yield* DockerEngine.execWebsocketsNonBlocking({
containerId: dindContainerId,
command: `COMPOSE_STATUS_STDOUT=1 docker compose ${method} ${stringifyOptions(options)} ${Array.join(services, " ")}`
});
const {
stderr,
stdin,
stdout
} = yield* MobyDemux.fan(multiplexed, {
requestedCapacity: 16
});
const streamFailableEnsuring = (stream, effect) => Stream.flatMap(stream, a => Stream.fromEffect(Effect.map(effect, Function.constant(a))));
return streamFailableEnsuring(MobyDemux.mergeRawToTaggedStream(stdout, stderr), MobyDemux.demuxRawToSingleSink(stdin, Stream.make(new Socket.CloseEvent()), Sink.drain));
}).pipe(Stream.unwrap, Stream.mapEffect(entry => {
if (Predicate.isTagged(entry, "stdout")) {
return Effect.succeed(entry.value);
} else {
const decoded = new TextDecoder("utf-8").decode(entry.value);
return Effect.fail(new DockerComposeError({
method,
cause: decoded
}));
}
}), Stream.mapError(cause => new DockerComposeError({
method,
cause
})), Stream.provideService(MobyEndpoints.Containers, containers));
// Actual compose implementation
return DockerCompose.of({
[TypeId]: TypeId,
build: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("build", services, {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.splitLines),
config: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("config", services, {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.splitLines, Stream.run(Sink.mkString)),
// cp: <E1>(
// project: Stream.Stream<Uint8Array, E1, never>,
// service: string,
// srcPath: string,
// destPath: string,
// options?: DockerComposeEngine.CopyOptions | undefined
// ): Stream.Stream<Uint8Array, E1 | DockerComposeError, never> => {},
create: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("create", services, {
...options
})), Stream.unwrap, Stream.runDrain),
down: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("down", services, {
...options
})), Stream.unwrap, Stream.runDrain),
// events: <E1>(
// project: Stream.Stream<Uint8Array, E1, never>,
// services?: Array<string> | undefined,
// options?: DockerComposeEngine.EventsOptions | undefined
// ): Stream.Stream<Uint8Array, E1 | DockerComposeError, never> => {},
// exec: <E1>(
// project: Stream.Stream<Uint8Array, E1, never>,
// service: string,
// command: string,
// args?: Array<string> | undefined,
// options?: DockerComposeEngine.ExecOptions | undefined
// ): Stream.Stream<Uint8Array, E1 | DockerComposeError, never> => {},
images: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("images", services, {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.splitLines),
kill: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("kill", services, {
...options
})), Stream.unwrap, Stream.runDrain),
logs: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("logs", services, {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.splitLines),
ls: (project, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("ls", [], {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.splitLines),
pause: (project, services) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("pause", services)), Stream.unwrap, Stream.runDrain),
// port: <E1>(
// project: Stream.Stream<Uint8Array, E1, never>,
// service: string,
// privatePort: number,
// options?: DockerComposeEngine.PortOptions | undefined
// ): Effect.Effect<number, E1 | DockerComposeError, never> => {},
ps: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("ps", services, {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.splitLines),
pull: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("pull", services, {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.splitLines),
push: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("push", services, {
...options
})), Stream.unwrap, Stream.runDrain),
restart: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("restart", services, {
...options
})), Stream.unwrap, Stream.runDrain),
rm: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("rm", services, {
...options
})), Stream.unwrap, Stream.runDrain),
// run: <E1>(
// project: Stream.Stream<Uint8Array, E1, never>,
// service: string,
// command: string,
// args?: Array<string> | undefined,
// options?: DockerComposeEngine.RunOptions | undefined
// ): Stream.Stream<Uint8Array, E1 | DockerComposeError, never> => {},
start: (project, services) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("start", services)), Stream.unwrap, Stream.runDrain),
stop: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("stop", services, {
...options
})), Stream.unwrap, Stream.runDrain),
top: (project, services) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("top", services)), Stream.unwrap, Stream.decodeText(), Stream.splitLines),
unpause: (project, services) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("unpause", services)), Stream.unwrap, Stream.runDrain),
up: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("up", services, {
...options
})), Stream.unwrap, Stream.runDrain),
version: (project, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("version", [], {
...options
})), Stream.unwrap, Stream.decodeText(), Stream.run(Sink.mkString)),
wait: (project, services, options) => Function.pipe(uploadProject(project), Effect.map(() => runCommand("wait", services, {
...options
})), Stream.unwrap, Stream.runDrain),
forProject: project => makeProjectLayer(project, uploadProject, runCommand)
});
});
/** @internal */
export const layer = /*#__PURE__*/Layer.scoped(DockerCompose, make);
/** @internal */
export const makeProjectLayer = (project, uploadProject, runCommand) => Effect.gen(function* () {
yield* uploadProject(project);
return {
[DockerComposeProjectTypeId]: DockerComposeProjectTypeId,
build: (services, options) => Function.pipe(runCommand("build", services, {
...options
}), Stream.decodeText(), Stream.splitLines),
config: (services, options) => Function.pipe(runCommand("config", services, {
...options
}), Stream.decodeText(), Stream.splitLines, Stream.run(Sink.mkString)),
// cp: (
//
// service: string,
// srcPath: string,
// destPath: string,
// options?: DockerComposeEngine.CopyOptions | undefined
// ): Stream.Stream<Uint8Array, DockerComposeError, never> => {},
create: (services, options) => Function.pipe(runCommand("create", services, {
...options
}), Stream.runDrain),
down: (services, options) => Function.pipe(runCommand("down", services, {
...options
}), Stream.runDrain),
// events: (
//
// services?: Array<string> | undefined,
// options?: DockerComposeEngine.EventsOptions | undefined
// ): Stream.Stream<Uint8Array, DockerComposeError, never> => {},
// exec: (
//
// service: string,
// command: string,
// args?: Array<string> | undefined,
// options?: DockerComposeEngine.ExecOptions | undefined
// ): Stream.Stream<Uint8Array, DockerComposeError, never> => {},
images: (services, options) => Function.pipe(runCommand("images", services, {
...options
}), Stream.decodeText(), Stream.splitLines),
kill: (services, options) => Function.pipe(runCommand("kill", services, {
...options
}), Stream.runDrain),
logs: (services, options) => Function.pipe(runCommand("logs", services, {
...options
}), Stream.decodeText(), Stream.splitLines),
ls: options => Function.pipe(runCommand("ls", [], {
...options
}), Stream.decodeText(), Stream.splitLines),
pause: services => Function.pipe(runCommand("pause", services), Stream.runDrain),
// port: (
//
// service: string,
// privatePort: number,
// options?: DockerComposeEngine.PortOptions | undefined
// ): Effect.Effect<number, DockerComposeError, never> => {},
ps: (services, options) => Function.pipe(runCommand("ps", services, {
...options
}), Stream.decodeText(), Stream.splitLines),
pull: (services, options) => Function.pipe(runCommand("pull", services, {
...options
}), Stream.decodeText(), Stream.splitLines),
push: (services, options) => Function.pipe(runCommand("push", services, {
...options
}), Stream.runDrain),
restart: (services, options) => Function.pipe(runCommand("restart", services, {
...options
}), Stream.runDrain),
rm: (services, options) => Function.pipe(runCommand("rm", services, {
...options
}), Stream.runDrain),
// run: (
//
// service: string,
// command: string,
// args?: Array<string> | undefined,
// options?: DockerComposeEngine.RunOptions | undefined
// ): Stream.Stream<Uint8Array, DockerComposeError, never> => {},
start: services => Function.pipe(runCommand("start", services), Stream.runDrain),
stop: (services, options) => Function.pipe(runCommand("stop", services, {
...options
}), Stream.runDrain),
top: services => Function.pipe(runCommand("top", services), Stream.decodeText(), Stream.splitLines),
unpause: services => Function.pipe(runCommand("unpause", services), Stream.runDrain),
up: (services, options) => Function.pipe(runCommand("up", services, {
...options
}), Stream.runDrain),
version: options => Function.pipe(runCommand("version", [], {
...options
}), Stream.decodeText(), Stream.run(Sink.mkString)),
wait: (services, options) => Function.pipe(runCommand("wait", services, {
...options
}), Stream.runDrain)
};
});
/** @internal */
export const layerProject = (project, tagIdentifier) => {
const tag = Context.GenericTag(tagIdentifier);
const effect = Effect.flatMap(DockerCompose, ({
forProject
}) => forProject(project));
const layer = Layer.effect(tag, effect);
return {
tag,
layer
};
};
//# sourceMappingURL=dockerCompose.js.map