UNPKG

the-moby-effect

Version:
344 lines 17.6 kB
import * as Array from "effect/Array"; import * as Context from "effect/Context"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Function from "effect/Function"; import * as Layer from "effect/Layer"; import * as Number from "effect/Number"; import * as Option from "effect/Option"; import * as Predicate from "effect/Predicate"; import * as Record from "effect/Record"; import * as Schema from "effect/Schema"; 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 Socket from "effect/unstable/socket/Socket"; import * as DockerEngine from "../../DockerEngine.js"; import * as MobyDemux from "../../MobyDemux.js"; import * as MobyEndpoints from "../../MobyEndpoints.js"; /** @internal */ const makeTempDirScoped = containerId => Effect.acquireRelease(Effect.gen(function* () { const [stderr, stdout] = yield* Effect.mapError(DockerEngine.execWebsockets({ containerId, command: "mktemp -d" }), cause => new DockerComposeError({ cause, method: "makeTempDirScoped" })); if (String.isNonEmpty(stderr)) { throw new DockerComposeError({ cause: stderr, method: "makeTempDirScoped" }); } const schema = Schema.String.pipe(Schema.check(Schema.isStartsWith("/tmp/"))); const tempDir = yield* Effect.mapError(Schema.decodeEffect(schema)(stdout), cause => new DockerComposeError({ cause, method: "makeTempDirScoped" })); yield* Effect.annotateCurrentSpan("tempDir", tempDir); return tempDir; }), tempDir => Effect.orDie(Effect.mapError(DockerEngine.execWebsockets({ containerId, command: `rm -rf ${tempDir}` }), cause => new DockerComposeError({ cause, method: "makeTempDirScoped" })))); /** @internal */ const uploadProject = /*#__PURE__*/Function.dual(2, (containerId, project) => Effect.gen(function* () { const containers = yield* MobyEndpoints.Containers; const projectUploadDir = yield* makeTempDirScoped(containerId); yield* Effect.annotateCurrentSpan("projectUploadDir", projectUploadDir); yield* containers.putArchive(containerId, project, { path: projectUploadDir }); return projectUploadDir; }).pipe(Effect.catchTag("DockerError", cause => new DockerComposeError({ cause, method: "uploadProject" })))); /** @internal */ const camelToKebab = /*#__PURE__*/Function.compose(String.camelToSnake, String.snakeToKebab); /** @internal */ const stringifyOptions = options => Function.pipe(options, Record.toEntries, Array.filter(([, value]) => Predicate.isNotUndefined(value)), 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.isObject(value)) { return Record.toEntries(value); } else { return Function.absurd(value); } }), Array.map(([key, value]) => `--${camelToKebab(key)}=${value}`), Array.join(" ")); /** @internal */ const runCommand = (containerId, projectPath, method, services = [], options = {}) => Effect.gen(function* () { const multiplexed = yield* DockerEngine.execWebsocketsNonBlocking({ containerId, cwd: projectPath, 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({ cause, method }))); /** @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__*/Data.TaggedError("DockerComposeError") { [DockerComposeErrorTypeId] = DockerComposeErrorTypeId; get message() { return `${String.capitalize(this.method)}`; } } /** @internal */ export const DockerCompose = /*#__PURE__*/Context.Service("@the-moby-effect/engines/DockerCompose"); /** @internal */ export const make = /*#__PURE__*/Effect.fnUntraced(function* (options) { const containers = yield* MobyEndpoints.Containers; const runCommandHelper = Function.flow(runCommand, Stream.provideService(MobyEndpoints.Containers, containers)); yield* DockerEngine.pingHead(); const build = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "build", services, { ...options }), Stream.decodeText(), Stream.splitLines); const config = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "config", services, { ...options }), Stream.decodeText(), Stream.splitLines, Stream.mkString); const cpTo = (dindContainerId, projectPath) => (service, localSrc, remoteDestLocation, options) => Effect.gen(function* () { const remoteTransferDir = yield* Effect.provideService(makeTempDirScoped(dindContainerId), MobyEndpoints.Containers, containers); yield* Effect.mapError(containers.putArchive(dindContainerId, localSrc, { path: remoteTransferDir }), cause => new DockerComposeError({ method: "cpTo", cause })); const command = `cp ${remoteTransferDir} ${service}:${remoteDestLocation}`; return yield* Stream.runDrain(runCommandHelper(dindContainerId, projectPath, command, [], { ...options })); }).pipe(Effect.scoped); const cpFrom = (dindContainerId, projectPath) => (service, remoteSrcLocation, options) => Function.pipe(makeTempDirScoped(dindContainerId), Stream.fromEffect, Stream.scoped, Stream.provideService(MobyEndpoints.Containers, containers), Stream.tap(remoteTransferDir => Stream.runDrain(runCommandHelper(dindContainerId, projectPath, `cp ${service}:${remoteSrcLocation} ${remoteTransferDir}`, [], { ...options }))), Stream.flatMap(remoteTransferDir => Stream.mapError(containers.archive(dindContainerId, { path: remoteTransferDir }), cause => new DockerComposeError({ cause, method: "cpFrom" })))); const create = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "create", services, { ...options }), Stream.runDrain); const down = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "down", services, { ...options }), Stream.runDrain); const events = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "events", services, { ...options }), Stream.decodeText(), Stream.splitLines); const exec = (dindContainerId, projectPath) => (service, command, args, options) => Effect.provideService(DockerEngine.execWebsocketsNonBlocking({ containerId: dindContainerId, cwd: projectPath, command: `COMPOSE_STATUS_STDOUT=1 docker compose exec ${stringifyOptions({ ...options })} ${service} ${command} ${Array.join(args ?? [], " ")}` }), MobyEndpoints.Containers, containers); const images = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "images", services, { ...options }), Stream.decodeText(), Stream.splitLines); const kill = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "kill", services, { ...options }), Stream.runDrain); const logs = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "logs", services, { ...options }), Stream.decodeText(), Stream.splitLines); const ls = (dindContainerId, projectPath) => options => Function.pipe(runCommandHelper(dindContainerId, projectPath, "ls", [], { ...options }), Stream.decodeText(), Stream.splitLines); const pause = (dindContainerId, projectPath) => services => Function.pipe(runCommandHelper(dindContainerId, projectPath, "pause", services), Stream.runDrain); const port = (dindContainerId, projectPath) => (service, privatePort, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "port", [service, privatePort.toString()], { ...options }), Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.runHead, Effect.map(Option.flatMap(Number.parse)), Effect.map(Option.getOrThrow)); const ps = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "ps", services, { ...options }), Stream.decodeText(), Stream.splitLines); const pull = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "pull", services, { ...options }), Stream.decodeText(), Stream.splitLines); const push = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "push", services, { ...options }), Stream.runDrain); const restart = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "restart", services, { ...options }), Stream.runDrain); const rm = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "rm", services, { ...options }), Stream.runDrain); const run = (dindContainerId, projectPath) => (service, command, args, options) => Effect.provideService(DockerEngine.execWebsocketsNonBlocking({ containerId: dindContainerId, cwd: projectPath, command: `COMPOSE_STATUS_STDOUT=1 docker compose run ${stringifyOptions({ ...options })} ${service} ${command} ${Array.join(args ?? [], " ")}` }), MobyEndpoints.Containers, containers); const start = (dindContainerId, projectPath) => services => Function.pipe(runCommandHelper(dindContainerId, projectPath, "start", services), Stream.runDrain); const stop = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "stop", services, { ...options }), Stream.runDrain); const top = (dindContainerId, projectPath) => services => Function.pipe(runCommandHelper(dindContainerId, projectPath, "top", services), Stream.decodeText(), Stream.splitLines); const unpause = (dindContainerId, projectPath) => services => Function.pipe(runCommandHelper(dindContainerId, projectPath, "unpause", services), Stream.runDrain); const up = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "up", services, { ...options }), Stream.runDrain); const version = (dindContainerId, projectPath) => options => Function.pipe(runCommandHelper(dindContainerId, projectPath, "version", [], { ...options }), Stream.decodeText(), Stream.mkString); const wait = (dindContainerId, projectPath) => (services, options) => Function.pipe(runCommandHelper(dindContainerId, projectPath, "wait", services, { ...options }), Stream.runDrain); const pullStream = DockerEngine.pull({ image: "docker.io/library/docker:latest" }); yield* Stream.runDrain(pullStream); const dindContainerIdResource = yield* DockerEngine.runScoped({ Image: "docker.io/library/docker:latest", Entrypoint: ["/bin/sh"], Tty: false, OpenStdin: true, AttachStdin: true, AttachStdout: true, AttachStderr: true, HostConfig: { Privileged: true, Binds: [`${options?.dockerEngineSocket ?? "/var/run/docker.sock"}:/var/run/docker.sock`] } }).pipe(Effect.map(({ Id }) => Id)); const uncurryEffectWithUploadProject = dindContainerId => func => (project, ...args) => uploadProject(project)(dindContainerId).pipe(Effect.provideService(MobyEndpoints.Containers, containers), Effect.flatMap(cwd => func(dindContainerId, cwd)(...args)), Effect.scoped); const uncurryStreamWithUploadProject = dindContainerId => func => (project, ...args) => uploadProject(project)(dindContainerId).pipe(Stream.fromEffect, Stream.scoped, Stream.provideService(MobyEndpoints.Containers, containers), Stream.flatMap(cwd => func(dindContainerId, cwd)(...args))); const uncurryEffect = uncurryEffectWithUploadProject(dindContainerIdResource); const uncurryStream = uncurryStreamWithUploadProject(dindContainerIdResource); // Actual compose implementation return DockerCompose.of({ [TypeId]: TypeId, build: uncurryStream(build), config: uncurryEffect(config), cpTo: (project, service, localSrc, remoteDestLocation, options) => Effect.gen(function* () { const cwd = yield* Effect.provideService(uploadProject(project)(dindContainerIdResource), MobyEndpoints.Containers, containers); yield* cpTo(dindContainerIdResource, cwd)(service, localSrc, remoteDestLocation, options); }).pipe(Effect.scoped), cpFrom: uncurryStream(cpFrom), create: uncurryEffect(create), down: uncurryEffect(down), events: uncurryStream(events), exec: uncurryEffect(exec), images: uncurryStream(images), kill: uncurryEffect(kill), logs: uncurryStream(logs), ls: uncurryStream(ls), pause: uncurryEffect(pause), port: uncurryEffect(port), ps: uncurryStream(ps), pull: uncurryStream(pull), push: uncurryEffect(push), restart: uncurryEffect(restart), rm: uncurryEffect(rm), run: uncurryEffect(run), start: uncurryEffect(start), stop: uncurryEffect(stop), top: uncurryStream(top), unpause: uncurryEffect(unpause), up: uncurryEffect(up), version: uncurryEffect(version), wait: uncurryEffect(wait), forProject: project => Effect.gen(function* () { const projectDindContainerIdResource = yield* DockerEngine.runScoped({ Image: "docker.io/library/docker:latest", Entrypoint: ["/bin/sh"], Tty: false, OpenStdin: true, AttachStdin: true, AttachStdout: true, AttachStderr: true, HostConfig: { Privileged: true, Binds: [`${options?.dockerEngineSocket ?? "/var/run/docker.sock"}:/var/run/docker.sock`] } }).pipe(Effect.map(({ Id }) => Id), Effect.provideService(MobyEndpoints.Containers, containers)); const cwd = yield* Effect.provideService(uploadProject(projectDindContainerIdResource, project), MobyEndpoints.Containers, containers); return { [DockerComposeProjectTypeId]: DockerComposeProjectTypeId, build: build(projectDindContainerIdResource, cwd), config: config(projectDindContainerIdResource, cwd), cpTo: cpTo(projectDindContainerIdResource, cwd), cpFrom: cpFrom(projectDindContainerIdResource, cwd), create: create(projectDindContainerIdResource, cwd), down: down(projectDindContainerIdResource, cwd), events: events(projectDindContainerIdResource, cwd), exec: exec(projectDindContainerIdResource, cwd), images: images(projectDindContainerIdResource, cwd), kill: kill(projectDindContainerIdResource, cwd), logs: logs(projectDindContainerIdResource, cwd), ls: ls(projectDindContainerIdResource, cwd), pause: pause(projectDindContainerIdResource, cwd), port: port(projectDindContainerIdResource, cwd), ps: ps(projectDindContainerIdResource, cwd), pull: pull(projectDindContainerIdResource, cwd), push: push(projectDindContainerIdResource, cwd), restart: restart(projectDindContainerIdResource, cwd), rm: rm(projectDindContainerIdResource, cwd), run: run(projectDindContainerIdResource, cwd), start: start(projectDindContainerIdResource, cwd), stop: stop(projectDindContainerIdResource, cwd), top: top(projectDindContainerIdResource, cwd), unpause: unpause(projectDindContainerIdResource, cwd), up: up(projectDindContainerIdResource, cwd), version: version(projectDindContainerIdResource, cwd), wait: wait(projectDindContainerIdResource, cwd) }; }) }); }); /** @internal */ export const layer = options => Layer.effect(DockerCompose, make(options)); /** @internal */ export const layerProject = (project, tagIdentifier) => { const tag = Context.Service(tagIdentifier); const effect = Effect.flatMap(DockerCompose, ({ forProject }) => forProject(project)); const layer = Layer.effect(tag, effect); return { tag, layer }; }; //# sourceMappingURL=dockerCompose.js.map