UNPKG

the-moby-effect

Version:
298 lines 11 kB
import * as Array from "effect/Array"; import * as Channel from "effect/Channel"; import * as Effect from "effect/Effect"; import * as Function from "effect/Function"; import * as Global from "effect/GlobalValue"; import * as Match from "effect/Match"; import * as MutableHashMap from "effect/MutableHashMap"; import * as Option from "effect/Option"; import * as Predicate from "effect/Predicate"; import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as Tuple from "effect/Tuple"; import * as MobyDemux from "../../MobyDemux.js"; import * as MobyEndpoints from "../../MobyEndpoints.js"; /** @internal */ export const pull = ({ auth, image, platform }) => Stream.unwrap(MobyEndpoints.Images.use(images => images.create({ fromImage: image, "X-Registry-Auth": auth, platform }))); /** @internal */ export const pullScoped = ({ auth, image, platform }) => Effect.Do.pipe(Effect.bind("images", () => MobyEndpoints.Images), Effect.let("stream", () => pull({ image, auth, platform })), Effect.let("acquire", ({ images, stream }) => Stream.provideService(stream, MobyEndpoints.Images, images)), Effect.let("release", ({ images }) => images.delete({ name: image })), Effect.flatMap(({ acquire, release }) => Effect.acquireRelease(Effect.sync(() => acquire), () => Effect.orDie(release)))); /** @internal */ export const build = ({ auth, buildArgs, context, dockerfile, platform, tag }) => Stream.unwrap(MobyEndpoints.Images.use(images => images.build({ context, buildArgs, dockerfile, platform, t: tag, "X-Registry-Config": auth }))); /** @internal */ export const buildScoped = ({ auth, buildArgs, context, dockerfile, platform, tag }) => Effect.Do.pipe(Effect.bind("images", () => MobyEndpoints.Images), Effect.let("stream", () => build({ tag, buildArgs, auth, context, platform, dockerfile })), Effect.let("acquire", ({ images, stream }) => Stream.provideService(stream, MobyEndpoints.Images, images)), Effect.let("release", ({ images }) => images.delete({ name: tag })), Effect.flatMap(({ acquire, release }) => Effect.acquireRelease(Effect.sync(() => acquire), () => Effect.orDie(release)))); /** @internal */ export const start = containerId => MobyEndpoints.Containers.use(containers => containers.start(containerId)); /** @internal */ export const stop = containerId => MobyEndpoints.Containers.use(containers => containers.stop(containerId)); /** @internal */ export const run = containerOptions => Effect.gen(function* () { const containers = yield* MobyEndpoints.Containers; const containerCreateResponse = yield* containers.create(containerOptions); yield* containers.start(containerCreateResponse.Id); // Helper to wait until a container is dead or running const waitUntilContainerDeadOrRunning = Function.pipe(containers.inspect(containerCreateResponse.Id), // Effect.tap(({ State }) => Effect.log(`Waiting for container to be running, state=${State?.Status}`)), Effect.flatMap(({ State }) => Function.pipe(Match.value(State?.Status), Match.when("running", _s => Effect.void), Match.when("created", _s => Effect.fail("Waiting")), // Match.when(Schemas.ContainerState_Status.RUNNING, (_s) => Effect.void), // Match.when(Schemas.ContainerState_Status.CREATED, (_s) => Effect.fail("Waiting")), Match.orElse(_s => Effect.fail("Container is dead or killed"))).pipe(Effect.mapError(s => new MobyEndpoints.ContainersError({ method: "inspect", cause: new Error(s) }))))).pipe(Effect.retry(Schedule.spaced(500).pipe(Schedule.whileInput(({ message }) => message === "Waiting")))); // Helper for if the container has a healthcheck, wait for it to report healthy const waitUntilContainerHealthy = Function.pipe(containers.inspect(containerCreateResponse.Id), // Effect.tap(({ State }) => // Effect.log(`Waiting for container to be healthy, health=${State?.Health?.Status}`) // ), Effect.flatMap(({ State }) => Function.pipe(Match.value(State?.Health?.Status), Match.when(undefined, _s => Effect.void), Match.when("healthy", _s => Effect.void), Match.when("starting", _s => Effect.fail("Waiting")), // Match.when(Schemas.Health_Status.HEALTHY, (_s) => Effect.void), // Match.when(Schemas.Health_Status.STARTING, (_s) => Effect.fail("Waiting")), Match.orElse(_s => Effect.fail("Container is unhealthy"))).pipe(Effect.mapError(s => new MobyEndpoints.ContainersError({ method: "inspect", cause: new Error(s) }))))).pipe(Effect.retry(Schedule.spaced(500).pipe(Schedule.whileInput(({ message }) => message === "Waiting")))); yield* waitUntilContainerDeadOrRunning; yield* waitUntilContainerHealthy; return yield* containers.inspect(containerCreateResponse.Id); }); /** @internal */ export const runScoped = containerOptions => { const acquire = run(containerOptions); const release = containerData => Effect.orDie(Effect.gen(function* () { const containers = yield* MobyEndpoints.Containers; // FIXME: this cleanup should be better yield* Effect.catchTag(containers.stop(containerData.Id), "ContainersError", () => Effect.void); yield* containers.delete(containerData.Id, { force: true }); })); return Effect.acquireRelease(acquire, release); }; /** @internal */ export const execNonBlocking = ({ command, containerId, detach }) => Effect.gen(function* () { const execs = yield* MobyEndpoints.Execs; const execId = yield* execs.container(containerId, { AttachStdin: true, AttachStderr: true, AttachStdout: true, Cmd: Predicate.isString(command) ? command.split(" ") : command }); const socket = yield* execs.start(execId.Id, { Detach: detach }); return Tuple.make(socket, execId.Id); }); /** @internal */ export const exec = ({ command, containerId }) => Effect.gen(function* () { const [socket, execId] = yield* execNonBlocking({ command, containerId, detach: false }); const output = yield* MobyDemux.demuxToSingleSink(socket, Stream.never, Sink.mkString); const execInspectResponse = yield* MobyEndpoints.Execs.use(execs => execs.inspect(execId)); if (execInspectResponse.Running === true) { return yield* new MobyEndpoints.ExecsError({ method: "exec", cause: new Error("Exec is still running") }); } else { return Tuple.make(execInspectResponse.ExitCode, output); } }); /** @internal */ const execWebsocketsRegistry = /*#__PURE__*/Global.globalValue("the-moby-effect/engines/docker/execWebsocketsRegistry", () => MutableHashMap.empty()); /** @internal */ export const execWebsocketsNonBlocking = ({ command, containerId }) => Effect.gen(function* () { const containers = yield* MobyEndpoints.Containers; const mutex = MutableHashMap.get(execWebsocketsRegistry, containerId).pipe(Option.getOrElse(() => { const semaphore = Effect.unsafeMakeSemaphore(1); MutableHashMap.set(execWebsocketsRegistry, containerId, semaphore); return semaphore; })); const acquire = Effect.gen(function* () { const inspect = yield* containers.inspect(containerId); const command = Array.join(inspect.Config?.Cmd ?? [], " "); yield* Effect.mapError(Schema.decodeUnknown(Schema.Literal("/bin/sh", // Basic shell available in most containers "/bin/bash", // Bash shell (common in many Linux distributions) "/usr/bin/bash", // Alternative location for bash "/bin/dash", // Debian Almquist shell (lightweight shell) "/bin/ash", // Lightweight shell used in Alpine Linux "/bin/zsh", // Z shell "/usr/bin/zsh", // Alternative location for zsh "/bin/ksh", // Korn shell "/bin/tcsh", // TENEX C shell "/bin/csh", // C shell "/usr/bin/fish", // Friendly interactive shell "/usr/local/bin/bash", // Alternative location for bash "/usr/local/bin/sh", // Alternative location for sh "/busybox/sh" // Busybox shell ))(command), cause => new MobyEndpoints.ContainersError({ method: "exec", cause })); yield* mutex.take(1); }); // TODO: should this be un-interuptible? const release = Effect.fnUntraced(function* () { yield* mutex.release(1); yield* containers.wait(containerId, { condition: "not-running" }); yield* containers.start(containerId); }, Effect.orDie); const use = Effect.gen(function* () { const cmd = Predicate.isString(command) ? command : Array.join(command, " "); const input = Stream.succeed(`${cmd}; exit\n`); const stdinSocket = yield* containers.attachWebsocket(containerId, { stdin: true, stream: true }); const stdoutSocket = yield* containers.attachWebsocket(containerId, { stdout: true, stream: true }); const stderrSocket = yield* containers.attachWebsocket(containerId, { stderr: true, stream: true }); const sockets = { stdin: stdinSocket, stdout: stdoutSocket, stderr: stderrSocket }; const multiplexedSocket = yield* MobyDemux.pack(sockets, { requestedCapacity: 16 }); const producer = Channel.fromEffect(MobyDemux.demuxRawToSingleSink(stdinSocket, input, Sink.drain)); const consumer = multiplexedSocket.underlying; const zipped = Channel.zipLeft(producer, consumer, { concurrent: true }); return zipped; }); const multiplexedChannel = Effect.acquireRelease(acquire, release).pipe(Effect.map(() => Channel.unwrap(use))).pipe(Channel.unwrapScoped).pipe(Channel.provideService(MobyEndpoints.Containers, containers)); return MobyDemux.makeMultiplexedChannel(multiplexedChannel); }); /** @internal */ export const execWebsockets = ({ command, containerId }) => Function.pipe(execWebsocketsNonBlocking({ command, containerId }), Effect.flatMap(MobyDemux.demuxMultiplexedToSeparateSinks(Stream.empty, Sink.mkString, Sink.mkString))); /** @internal */ export const ps = options => MobyEndpoints.Containers.use(containers => containers.list(options)); /** @internal */ export const push = options => Stream.unwrap(MobyEndpoints.Images.use(images => images.push(options))); /** @internal */ export const images = options => MobyEndpoints.Images.use(images => images.list(options)); /** @internal */ export const search = options => MobyEndpoints.Images.use(images => images.search(options)); /** @internal */ export const version = /*#__PURE__*/Function.constant(/*#__PURE__*/MobyEndpoints.Systems.use(systems => systems.version())); /** @internal */ export const info = /*#__PURE__*/Function.constant(/*#__PURE__*/MobyEndpoints.Systems.use(systems => systems.info())); /** @internal */ export const ping = /*#__PURE__*/Function.constant(/*#__PURE__*/MobyEndpoints.Systems.use(systems => systems.ping())); /** @internal */ export const pingHead = /*#__PURE__*/Function.constant(/*#__PURE__*/MobyEndpoints.Systems.use(systems => systems.ping())); //# sourceMappingURL=docker.js.map