the-moby-effect
Version:
Moby/Docker API client built using effect-ts
344 lines • 17.6 kB
JavaScript
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