UNPKG

the-moby-effect

Version:
306 lines 14.6 kB
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