the-moby-effect
Version:
Moby/Docker API client built using effect-ts
298 lines • 11.8 kB
JavaScript
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 MutableHashMap from "effect/MutableHashMap";
import * as Option from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as Schema from "effect/Schema";
import * as Semaphore from "effect/Semaphore";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as Tuple from "effect/Tuple";
import * as Socket from "effect/unstable/socket/Socket";
import * as MobyDemux from "../../MobyDemux.js";
import * as MobyEndpoints from "../../MobyEndpoints.js";
import * as internalCircular from "../endpoints/circular.js";
/** @internal */
export const pull = ({
image,
platform
}) => Stream.unwrap(Effect.map(MobyEndpoints.Images, images => images.create({
fromImage: image,
platform
})));
/** @internal */
export const pullScoped = ({
image,
platform
}) => Effect.Do.pipe(Effect.bind("images", () => MobyEndpoints.Images), Effect.let("stream", () => pull({
image,
platform
})), Effect.let("acquire", ({
images,
stream
}) => Stream.provideService(stream, MobyEndpoints.Images, images)), Effect.let("release", ({
images
}) => images.delete(image)), Effect.flatMap(({
acquire,
release
}) => Effect.acquireRelease(Effect.sync(() => acquire), () => Effect.orDie(release))));
/** @internal */
export const build = ({
buildargs,
context,
dockerfile,
platform,
tag
}) => Stream.unwrap(Effect.map(MobyEndpoints.Images, images => images.build(context, {
buildargs,
dockerfile,
platform,
t: tag
})));
/** @internal */
export const buildScoped = ({
buildargs,
context,
dockerfile,
platform,
tag
}) => Effect.Do.pipe(Effect.bind("images", () => MobyEndpoints.Images), Effect.let("stream", () => build({
tag,
buildargs,
context,
platform,
dockerfile
})), Effect.let("acquire", ({
images,
stream
}) => Stream.provideService(stream, MobyEndpoints.Images, images)), Effect.let("release", ({
images
}) => images.delete(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 = options => Effect.gen(function* () {
const containers = yield* MobyEndpoints.Containers;
const {
Id: containerId
} = yield* containers.create(options);
yield* containers.start(containerId);
const inspect = yield* containers.inspect(containerId);
return inspect;
});
/** @internal */
export const runScoped = options => {
const acquire = Effect.map(MobyEndpoints.Containers.use(containers => containers.create(options)), ({
Id
}) => Id);
const release = containerId => Effect.orDie(MobyEndpoints.Containers.use(containers => containers.delete(containerId, {
force: true
})));
return Effect.acquireRelease(acquire, release).pipe(Effect.tap(containerId => start(containerId)), Effect.flatMap(containerId => MobyEndpoints.Containers.use(containers => containers.inspect(containerId))));
};
/** @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,
Detach: false,
Cmd: Predicate.isString(command) ? command.split(" ") : command
});
const socket = yield* execs.start(execId.Id, {
Detach: detach,
Tty: false
});
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.reduce(() => "", (acc, chunk) => acc + chunk));
const execInspectResponse = yield* MobyEndpoints.Execs.use(execs => execs.inspect(execId));
if (execInspectResponse.Running === true) return yield* Effect.die("Exec is still running");else return Tuple.make(execInspectResponse.ExitCode, output);
});
/**
* Serializes websocket execs per container. Attach websockets talk to the
* container's single stdio (there is no exec-create over websockets), so
* concurrent execs against the same container would interleave their input
* and output. Entries are reference counted and evicted once the last
* in-flight exec for a container releases.
*
* @internal
*/
const execWebsocketsRegistry = /*#__PURE__*/MutableHashMap.empty();
/**
* Runs a command in a container by typing it into the container's own
* stdin-attached shell over attach websockets - the websocket transport has
* no exec-create endpoint, attach only talks to the container's main process.
* This shapes everything unusual about this function: the container's
* entrypoint must be a known shell (the command text is fed to it on stdin),
* the command is suffixed with `; exit` so the shell terminates and the
* daemon flushes and closes the attach streams, and afterwards the container
* is restarted to return it to a usable state for subsequent execs.
*
* @internal
*/
export const execWebsocketsNonBlocking = ({
command,
containerId,
cwd
}) => Effect.gen(function* () {
const containers = yield* MobyEndpoints.Containers;
const acquire = Effect.gen(function* () {
const inspect = yield* containers.inspect(containerId);
const command = inspect.Config?.Cmd;
const entrypoint = Option.fromNullishOr(inspect.Config?.Entrypoint).pipe(Option.flatMap(Array.head), Option.getOrUndefined);
yield* Schema.decodeUnknownEffect(Schema.Union([Schema.Undefined, Schema.Null]))(command).pipe(Effect.mapError(cause => new internalCircular.DockerError({
module: "containers",
method: "execWebsocketsNonBlocking",
cause
})));
yield* Schema.decodeUnknownEffect(Schema.Literals(["/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
]))(entrypoint).pipe(Effect.mapError(cause => new internalCircular.DockerError({
module: "containers",
method: "execWebsocketsNonBlocking",
cause
})));
// The registry lookup, insert, and reference count increment must
// stay in one synchronous step so concurrent execs cannot observe
// a half-registered entry. The increment happens only after the
// validations above so a failed acquire never leaks a reference.
const entry = MutableHashMap.get(execWebsocketsRegistry, containerId).pipe(Option.getOrElse(() => {
const fresh = {
semaphore: Semaphore.makeUnsafe(1),
pending: 0
};
MutableHashMap.set(execWebsocketsRegistry, containerId, fresh);
return fresh;
}));
entry.pending += 1;
yield* entry.semaphore.take(1);
return entry;
});
// TODO: should this be un-interuptible?
const release = entry => entry.semaphore.release(1).pipe(Effect.andThen(containers.wait(containerId, {
condition: "not-running"
})), Effect.andThen(containers.start(containerId)), Effect.orDie, Effect.ensuring(Effect.sync(() => {
entry.pending -= 1;
if (entry.pending === 0) {
MutableHashMap.remove(execWebsocketsRegistry, containerId);
}
})));
const use = Effect.gen(function* () {
const cmd = Predicate.isString(command) ? command : Array.join(command, " ");
const cwdCommand = Predicate.isUndefined(cwd) ? cmd : `cd ${cwd} && ${cmd}`;
// The attaches connect eagerly, so ordering here is meaningful:
// attach the output sockets before either stdin socket so no
// output can be produced (and discarded by the daemon) before the
// output attaches are complete. The command gets its own attach
// because every raw socket is a single-use connection.
const stdoutSocket = yield* containers.attachWebsocket(containerId, {
stdout: true,
stream: true
});
const stderrSocket = yield* containers.attachWebsocket(containerId, {
stderr: true,
stream: true
});
const stdinSocket = yield* containers.attachWebsocket(containerId, {
stdin: true,
stream: true
});
const commandSocket = yield* containers.attachWebsocket(containerId, {
stdin: true,
stream: true
});
const sockets = {
stdin: stdinSocket,
stdout: stdoutSocket,
stderr: stderrSocket
};
const multiplexedSocket = yield* MobyDemux.pack(sockets, {
requestedCapacity: 16
});
// Write the command and half-close its attach BEFORE exposing the
// channel. The command and any downstream input travel on separate
// attaches that the daemon merges into the container's stdin, so
// writing concurrently would let a fast consumer's input reach the
// shell before the command does (observed hanging the exec under
// ci load). Completing the write first makes the ordering
// structural instead of a race.
const input = Stream.make(`${cwdCommand}; exit\n`, new Socket.CloseEvent(1000, "command written"));
yield* MobyDemux.demuxRawToSingleSink(commandSocket, input, Sink.drain);
return multiplexedSocket.underlying;
});
const multiplexedChannel = Effect.acquireRelease(acquire, release).pipe(Effect.map(() => Channel.unwrap(use)), Channel.unwrap, 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.reduce(() => "", (acc, chunk) => acc + chunk), Sink.reduce(() => "", (acc, chunk) => acc + chunk))));
/** @internal */
export const ps = options => MobyEndpoints.Containers.use(containers => containers.list(options));
/** @internal */
export const push = (name, options) => Stream.unwrap(Effect.map(MobyEndpoints.Images, images => images.push(name, 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.System.use(systems => systems.version()));
/** @internal */
export const info = /*#__PURE__*/Function.constant(/*#__PURE__*/MobyEndpoints.System.use(systems => systems.info()));
/** @internal */
export const ping = /*#__PURE__*/Function.constant(/*#__PURE__*/MobyEndpoints.System.use(systems => systems.ping()));
/** @internal */
export const pingHead = /*#__PURE__*/Function.constant(/*#__PURE__*/MobyEndpoints.System.use(systems => systems.ping()));
//# sourceMappingURL=docker.js.map