the-moby-effect
Version:
Moby/Docker API client built using effect-ts
172 lines (158 loc) • 6.63 kB
text/typescript
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as HttpClient from "effect/unstable/http/HttpClient";
import * as HttpApi from "effect/unstable/httpapi/HttpApi";
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema";
import type { MultiplexedSocket, RawSocket } from "../../MobyDemux.ts";
import { MobyConnectionOptions } from "../../MobyConnection.ts";
import { makeAgnosticHttpClientLayer } from "../../MobyPlatforms.ts";
import { responseToStreamingSocketOrFailUnsafe } from "../demux/hijack.ts";
import {
ContainerExecStartOptions,
ContainerExecOptions as ExecConfig,
ContainerExecInspect as ExecInspectResponse,
} from "../generated/index.ts";
import { ExecIdentifier } from "../schemas/id.ts";
import { DockerError } from "./circular.ts";
import { BadRequest, Conflict, InternalServerError, NotFound } from "./errors.ts";
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec/operation/ContainerExec */
const createExecEndpoint = HttpApiEndpoint.post("container", "/containers/:id/exec", {
params: { id: Schema.String },
payload: ExecConfig,
success: Schema.Struct({ Id: ExecIdentifier }).pipe(HttpApiSchema.status(201)), // 201 Created
error: [
NotFound, // 404 No such container
Conflict, // 409 Container is not running
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec/operation/ExecStart */
const startExecEndpoint = HttpApiEndpoint.post("start", "/exec/:id/start", {
params: { id: ExecIdentifier },
headers: {
Upgrade: Schema.Literal("tcp"),
Connection: Schema.Literal("Upgrade"),
},
payload: ContainerExecStartOptions,
success: [
HttpApiSchema.Empty(101), // 101 Switching Protocols
HttpApiSchema.Empty(200), // 200 OK
],
error: [
NotFound, // 404 No such Exec instance
Conflict, // 409 Container is not running
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec/operation/ExecResize */
const resizeExecEndpoint = HttpApiEndpoint.post("resize", "/exec/:id/resize", {
params: { id: ExecIdentifier },
query: {
h: Schema.Number,
w: Schema.Number,
},
success: HttpApiSchema.Empty(200), // 200 OK
error: [
BadRequest, // 400 Invalid parameters
NotFound, // 404 No such Exec instance
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec/operation/ExecInspect */
const inspectExecEndpoint = HttpApiEndpoint.get("inspect", "/exec/:id/json", {
params: { id: ExecIdentifier },
success: ExecInspectResponse, // 200 OK
error: [
NotFound, // 404 No such Exec instance
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec */
const ExecsGroup = HttpApiGroup.make("exec").add(
createExecEndpoint,
startExecEndpoint,
resizeExecEndpoint,
inspectExecEndpoint
);
/**
* @since 1.0.0
* @category HttpApi
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec
*/
export const ExecsApi = HttpApi.make("ExecsApi").add(ExecsGroup);
/**
* @since 1.0.0
* @category Services
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec
*/
export class Execs extends Context.Service<Execs>()("@the-moby-effect/endpoints/Execs", {
make: Effect.gen(function* () {
const httpClient = yield* HttpClient.HttpClient;
const ExecsError = DockerError.WrapForModule("execs");
const client = yield* HttpApiClient.group(ExecsApi, { group: "exec", httpClient });
const container_ = (id: string, payload: (typeof ExecConfig)["~type.make.in"]) =>
ExecConfig.makeEffect(payload).pipe(
Effect.flatMap((payload) => client.container({ params: { id }, payload })),
Effect.mapError(ExecsError("container"))
);
const start_ = <const T extends boolean = false>(
id: ExecIdentifier,
payload: Omit<(typeof ContainerExecStartOptions)["~type.make.in"], "Detach"> & {
Detach: T;
}
): Effect.Effect<[T] extends [false] ? RawSocket | MultiplexedSocket : void, DockerError, never> =>
ContainerExecStartOptions.makeEffect(payload).pipe(
Effect.flatMap((startOptions) =>
client.start({
params: { id },
payload: startOptions,
headers: { Connection: "Upgrade", Upgrade: "tcp" }, // FIXME: Broken on undici
responseMode: "response-only",
})
),
Effect.flatMap(responseToStreamingSocketOrFailUnsafe),
Effect.map(
(socket) =>
(payload.Detach === true ? void 0 : socket) as [T] extends [false]
? RawSocket | MultiplexedSocket
: void
),
Effect.mapError(ExecsError("start"))
);
const resize_ = (id: ExecIdentifier, params: { w: number; h: number }) =>
client.resize({ params: { id }, query: { ...params } }).pipe(Effect.mapError(ExecsError("resize")));
const inspect_ = (id: ExecIdentifier) =>
client.inspect({ params: { id } }).pipe(Effect.mapError(ExecsError("inspect")));
return {
container: container_,
start: start_,
resize: resize_,
inspect: inspect_,
};
}),
}) {}
/**
* @since 1.0.0
* @category Layers
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec
*/
export const ExecsLayer: Layer.Layer<Execs, never, HttpClient.HttpClient> = Layer.effect(Execs, Execs.make);
/**
* @since 1.0.0
* @category Layers
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Exec
*/
export const ExecsLayerLocalSocket: Layer.Layer<Execs, never, HttpClient.HttpClient> = ExecsLayer.pipe(
Layer.provide(
makeAgnosticHttpClientLayer(
MobyConnectionOptions.socket({
socketPath: "/var/run/docker.sock",
})
)
)
);