the-moby-effect
Version:
Moby/Docker API client built using effect-ts
745 lines (701 loc) • 31.5 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 Stream from "effect/Stream";
import * as String from "effect/String";
import * as HttpClient from "effect/unstable/http/HttpClient";
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
import * as Url from "effect/unstable/http/Url";
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 HttpApiError from "effect/unstable/httpapi/HttpApiError";
import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema";
import * as Socket from "effect/unstable/socket/Socket";
import { MobyConnectionOptions } from "../../MobyConnection.ts";
import { makeAgnosticHttpClientLayer } from "../../MobyPlatforms.ts";
import { responseToStreamingSocketOrFailUnsafe } from "../demux/hijack.ts";
import { makeRawSocket } from "../demux/raw.ts";
import {
ArchiveChange,
ContainerConfig,
ContainerCreateRequest,
ContainerHealth,
ContainerHostConfig,
ContainerInspectResponse,
ContainerState,
ContainerStatsResponse,
ContainerSummary,
ContainerTopResponse,
ContainerWaitResponse,
} from "../generated/index.ts";
import { replacer as quoteWireNumbers } from "../platforms/agnostic.ts";
import { ContainerIdentifier } from "../schemas/id.ts";
import { DockerError } from "./circular.ts";
import { BadRequest, Conflict, Forbidden, InternalServerError, NotAcceptable, NotFound } from "./errors.ts";
import { BooleanFilter, StringFilter } from "./filters.ts";
/** @since 1.0.0 */
export const ListFilters = Schema.fromJsonString(
Schema.Struct({
ancestor: Schema.optional(Schema.Array(Schema.String)),
before: Schema.optional(Schema.Array(Schema.String)),
expose: Schema.optional(Schema.Array(Schema.String)),
exited: Schema.optional(Schema.Array(Schema.NumberFromString)),
health: Schema.optional(Schema.Array(ContainerHealth.fields["Status"])),
identifier: Schema.optional(Schema.Array(ContainerIdentifier)),
// isolation: Schema.optional(Schema.Array(ContainerHostConfig.fields["Isolation"])),
"is-task": Schema.optional(BooleanFilter("is-task")),
label: Schema.optional(Schema.Array(Schema.String)),
name: Schema.optional(Schema.Array(Schema.String)),
network: Schema.optional(Schema.Array(Schema.String)),
publish: Schema.optional(Schema.Array(Schema.String)),
since: Schema.optional(Schema.Array(Schema.String)),
status: Schema.optional(Schema.Array(ContainerState.fields["Status"])),
volume: Schema.optional(StringFilter),
})
);
/** @since 1.0.0 */
export const PruneFilters = Schema.fromJsonString(
Schema.Struct({
until: Schema.optional(StringFilter),
label: Schema.optional(Schema.Array(Schema.String)),
})
);
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerList */
const listContainersEndpoint = HttpApiEndpoint.get("list", "/json", {
query: {
all: Schema.optional(Schema.Boolean),
limit: Schema.optional(Schema.Number),
size: Schema.optional(Schema.Boolean),
filters: Schema.optional(ListFilters),
},
success: Schema.Array(ContainerSummary), // 200 OK
error: [
BadRequest, // 400 Bad parameter
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerCreate */
const createContainerEndpoint = HttpApiEndpoint.post("create", "/create", {
query: {
platform: Schema.optional(Schema.String),
name: Schema.String.pipe(Schema.check(Schema.isPattern(/^[a-zA-Z0-9][a-zA-Z0-9_.-]+/)), Schema.optional),
},
payload: ContainerCreateRequest,
success: Schema.Struct({
Id: ContainerIdentifier,
Warnings: Schema.NullOr(Schema.Array(Schema.String)),
}).pipe(HttpApiSchema.status(201)), // 201 Created
error: [
BadRequest, // 400 Bad parameter
Forbidden, // 403 Forbidden
NotFound, // 404 Not Found
NotAcceptable, // 406 Not Acceptable
Conflict, // 409 Name conflicts
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerInspect */
const inspectContainerEndpoint = HttpApiEndpoint.get("inspect", "/:identifier/json", {
params: { identifier: ContainerIdentifier },
query: { size: Schema.optional(Schema.Boolean) },
success: ContainerInspectResponse, // 200 OK
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerTop */
const topContainerEndpoint = HttpApiEndpoint.get("top", "/:identifier/top", {
params: { identifier: ContainerIdentifier },
query: { ps_args: Schema.optional(Schema.String) },
success: ContainerTopResponse, // 200 OK
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerLogs */
const logsContainerEndpoint = HttpApiEndpoint.get("logs", "/:identifier/logs", {
params: { identifier: ContainerIdentifier },
query: {
follow: Schema.optional(Schema.Boolean),
stdout: Schema.optional(Schema.Boolean),
stderr: Schema.optional(Schema.Boolean),
since: Schema.optional(Schema.Number),
until: Schema.optional(Schema.Number),
timestamps: Schema.optional(Schema.Boolean),
tail: Schema.optional(Schema.String),
},
success: HttpApiSchema.StreamUint8Array(), // 200 OK (streaming response)
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerChanges */
const changesContainerEndpoint = HttpApiEndpoint.get("changes", "/:identifier/changes", {
params: { identifier: ContainerIdentifier },
success: Schema.NullOr(Schema.Array(ArchiveChange)), // 200 OK
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerExport */
const exportContainerEndpoint = HttpApiEndpoint.get("export", "/:identifier/export", {
params: { identifier: ContainerIdentifier },
success: HttpApiSchema.StreamUint8Array(), // 200 OK (streaming response)
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerStats */
const statsContainerEndpoint = HttpApiEndpoint.get("stats", "/:identifier/stats", {
params: { identifier: ContainerIdentifier },
query: {
stream: Schema.optional(Schema.Boolean),
"one-shot": Schema.optional(Schema.Boolean),
},
success: HttpApiSchema.StreamUint8Array(), // 200 OK (streaming response)
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerResize */
const resizeContainerEndpoint = HttpApiEndpoint.post("resize", "/:identifier/resize", {
params: { identifier: ContainerIdentifier },
query: {
h: Schema.optional(Schema.Number),
w: Schema.optional(Schema.Number),
},
success: HttpApiSchema.Empty(200), // 200 OK
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerStart */
const startContainerEndpoint = HttpApiEndpoint.post("start", "/:identifier/start", {
params: { identifier: ContainerIdentifier },
query: {
detachKeys: Schema.optional(Schema.String),
},
success: [
HttpApiSchema.Empty(204), // 204 No Content
HttpApiSchema.Empty(304), // 304 Container already started
],
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerStop */
const stopContainerEndpoint = HttpApiEndpoint.post("stop", "/:identifier/stop", {
params: { identifier: ContainerIdentifier },
query: {
signal: Schema.optional(Schema.String),
t: Schema.optional(Schema.Number),
},
success: [
HttpApiSchema.Empty(204), // 204 No Content
HttpApiSchema.Empty(304), // 304 Container already stopped
],
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerRestart */
const restartContainerEndpoint = HttpApiEndpoint.post("restart", "/:identifier/restart", {
params: { identifier: ContainerIdentifier },
query: {
signal: Schema.optional(Schema.String),
t: Schema.optional(Schema.Number),
},
success: HttpApiSchema.Empty(204), // 204 No Content
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerKill */
const killContainerEndpoint = HttpApiEndpoint.post("kill", "/:identifier/kill", {
params: { identifier: ContainerIdentifier },
query: {
signal: Schema.optional(Schema.String),
},
success: HttpApiSchema.Empty(204), // 204 No Content
error: [
NotFound, // 404 Not Found
Conflict, // 409 Container is not running
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerUpdate */
const updateContainerEndpoint = HttpApiEndpoint.post("update", "/:identifier/update", {
params: { identifier: ContainerIdentifier },
payload: ContainerConfig,
success: Schema.Struct({ Warnings: Schema.NullOr(Schema.Array(Schema.String)) }), // 200 OK
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerRename */
const renameContainerEndpoint = HttpApiEndpoint.post("rename", "/:identifier/rename", {
params: { identifier: ContainerIdentifier },
query: { name: Schema.String },
success: HttpApiSchema.Empty(204), // 204 No Content
error: [
NotFound, // 404 Not Found
Conflict, // 409 Name already in use
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerPause */
const pauseContainerEndpoint = HttpApiEndpoint.post("pause", "/:identifier/pause", {
params: { identifier: ContainerIdentifier },
success: HttpApiSchema.Empty(204), // 204 No Content
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerUnpause */
const unpauseContainerEndpoint = HttpApiEndpoint.post("unpause", "/:identifier/unpause", {
params: { identifier: ContainerIdentifier },
success: HttpApiSchema.Empty(204), // 204 No Content
error: [
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerAttach */
const attachContainerEndpoint = HttpApiEndpoint.post("attach", "/:identifier/attach", {
params: { identifier: ContainerIdentifier },
query: {
detachKeys: Schema.optional(Schema.String),
logs: Schema.optional(Schema.Boolean),
stream: Schema.optional(Schema.Boolean),
stdin: Schema.optional(Schema.Boolean),
stdout: Schema.optional(Schema.Boolean),
stderr: Schema.optional(Schema.Boolean),
},
headers: {
Upgrade: Schema.Literal("tcp"),
Connection: Schema.Literal("Upgrade"),
},
success: [
HttpApiSchema.Empty(101), // 101 Switching Protocols
HttpApiSchema.Empty(200), // 200 OK
],
error: [
BadRequest, // 400 Bad parameter
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerAttachWebsocket */
const attachWebsocketContainerEndpoint = HttpApiEndpoint.get("attachWebsocket", "/:identifier/attach/ws", {
params: { identifier: ContainerIdentifier },
query: {
detachKeys: Schema.optional(Schema.String),
logs: Schema.optional(Schema.Boolean),
stream: Schema.optional(Schema.Boolean),
stdin: Schema.optional(Schema.Boolean),
stdout: Schema.optional(Schema.Boolean),
stderr: Schema.optional(Schema.Boolean),
},
success: [
HttpApiSchema.Empty(101), // 101 Switching Protocols
HttpApiSchema.Empty(200), // 200 OK
],
error: [
BadRequest, // 400 Bad parameter
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerWait */
const waitContainerEndpoint = HttpApiEndpoint.post("wait", "/:identifier/wait", {
params: { identifier: ContainerIdentifier },
query: { condition: Schema.optional(Schema.String) },
success: ContainerWaitResponse, // 200 OK
error: [
BadRequest, // 400 Bad parameter
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerDelete */
const deleteContainerEndpoint = HttpApiEndpoint.delete("delete", "/:identifier", {
params: { identifier: ContainerIdentifier },
query: {
v: Schema.optional(Schema.Boolean),
force: Schema.optional(Schema.Boolean),
link: Schema.optional(Schema.Boolean),
},
success: HttpApiSchema.Empty(204), // 204 No Content
error: [
BadRequest, // 400 Bad parameter
NotFound, // 404 Not Found
Conflict, // 409 Conflict
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerArchive */
const archiveContainerEndpoint = HttpApiEndpoint.get("archive", "/:identifier/archive", {
params: { identifier: ContainerIdentifier },
query: { path: Schema.String },
success: HttpApiSchema.StreamUint8Array(), // 200 OK (streaming response)
error: [
BadRequest, // 400 Bad parameter
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerArchiveInfo */
const archiveInfoContainerEndpoint = HttpApiEndpoint.head("archiveInfo", "/:identifier/archive", {
params: { identifier: ContainerIdentifier },
query: { path: Schema.String },
success: HttpApiSchema.Empty(200), // 200 OK
error: [
BadRequest, // 400 Bad parameter
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/PutContainerArchive */
const putArchiveContainerEndpoint = HttpApiEndpoint.put("putArchive", "/:identifier/archive", {
params: { identifier: ContainerIdentifier },
payload: HttpApiSchema.StreamUint8Array(),
query: {
path: Schema.String,
noOverwriteDirNonDir: Schema.optional(Schema.String),
copyUIDGidentifier: Schema.optional(Schema.String),
},
success: HttpApiSchema.Empty(200), // 200 OK
error: [
BadRequest, // 400 Bad parameter
Forbidden, // 403 Forbidden
NotFound, // 404 Not Found
InternalServerError,
],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container/operation/ContainerPrune */
const pruneContainersEndpoint = HttpApiEndpoint.post("prune", "/prune", {
query: { filters: Schema.optional(PruneFilters) },
success: Schema.Struct({
ContainersDeleted: Schema.NullOr(Schema.Array(ContainerIdentifier)),
SpaceReclaimed: Schema.BigIntFromString.check(Schema.isBetweenBigInt({ minimum: 0n, maximum: 2n ** 64n - 1n })),
}), // 200 OK
error: [InternalServerError],
});
/** @see https://docs.docker.com/reference/api/engine/latest/#tag/Container */
const ContainersGroup = HttpApiGroup.make("containers")
.add(
listContainersEndpoint,
createContainerEndpoint,
inspectContainerEndpoint,
topContainerEndpoint,
logsContainerEndpoint,
changesContainerEndpoint,
exportContainerEndpoint,
statsContainerEndpoint,
resizeContainerEndpoint,
startContainerEndpoint,
stopContainerEndpoint,
restartContainerEndpoint,
killContainerEndpoint,
updateContainerEndpoint,
renameContainerEndpoint,
pauseContainerEndpoint,
unpauseContainerEndpoint,
attachContainerEndpoint,
attachWebsocketContainerEndpoint,
waitContainerEndpoint,
deleteContainerEndpoint,
archiveContainerEndpoint,
archiveInfoContainerEndpoint,
putArchiveContainerEndpoint,
pruneContainersEndpoint
)
.prefix("/containers");
/**
* @since 1.0.0
* @category HttpApi
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Container
*/
export const ContainersApi = HttpApi.make("ContainersApi").add(ContainersGroup);
/**
* @since 1.0.0
* @category Services
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Container
*/
export class Containers extends Context.Service<Containers>()("@the-moby-effect/endpoints/Containers", {
make: Effect.gen(function* () {
type ContainersEndpoints = HttpApiGroup.Endpoints<typeof ContainersGroup>;
type Options<Name extends ContainersEndpoints["identifier"]> = HttpApiEndpoint.WithIdentifier<
ContainersEndpoints,
Name
>["~Query"]["Type"];
const httpClient = yield* HttpClient.HttpClient;
const ContainersError = DockerError.WrapForModule("containers");
const websocketConstructor = yield* Socket.WebSocketConstructor;
const client = yield* HttpApiClient.group(ContainersApi, {
httpClient,
group: "containers",
});
const list_ = (options?: Options<"list"> | undefined) =>
client
.list({
query: {
all: options?.all,
limit: options?.limit,
size: options?.size,
filters: options?.filters,
},
})
.pipe(Effect.mapError(ContainersError("list")));
const create_ = (
options: Omit<(typeof ContainerCreateRequest)["~type.make.in"], "HostConfig"> & {
readonly Name?: string | undefined;
readonly Platform?: string | undefined;
readonly HostConfig?: (typeof ContainerHostConfig)["~type.make.in"] | undefined;
}
) =>
Effect.gen(function* () {
const hostConfig = options.HostConfig
? yield* ContainerHostConfig.makeEffect(options.HostConfig)
: undefined;
const payload = yield* ContainerCreateRequest.makeEffect({ ...options, HostConfig: hostConfig });
return yield* client.create({ query: { name: options.Name, platform: options.Platform }, payload });
}).pipe(Effect.mapError(ContainersError("create")));
const inspect_ = (identifier: ContainerIdentifier, options?: Options<"inspect">) =>
client
.inspect({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("inspect")));
const top_ = (identifier: ContainerIdentifier, options?: Options<"top">) =>
client.top({ params: { identifier }, query: { ...options } }).pipe(Effect.mapError(ContainersError("top")));
const logs_ = (identifier: ContainerIdentifier, options?: Options<"logs">) =>
client
.logs({ params: { identifier }, query: { ...options } })
.pipe(Stream.unwrap, Stream.decodeText(), Stream.splitLines, Stream.mapError(ContainersError("logs")));
const changes_ = (identifier: ContainerIdentifier) =>
client.changes({ params: { identifier } }).pipe(Effect.mapError(ContainersError("changes")));
const export_ = (identifier: ContainerIdentifier) =>
client.export({ params: { identifier } }).pipe(Stream.unwrap, Stream.mapError(ContainersError("export")));
const stats_ = (identifier: ContainerIdentifier, options?: Options<"stats">) =>
client.stats({ params: { identifier }, query: { ...options } }).pipe(
Stream.unwrap,
Stream.decodeText(),
Stream.splitLines,
Stream.map(quoteWireNumbers),
Stream.mapEffect((line) => Schema.decodeEffect(Schema.fromJsonString(ContainerStatsResponse))(line)),
Stream.mapError(ContainersError("stats"))
);
const resize_ = (identifier: ContainerIdentifier, options?: Options<"resize">) =>
client
.resize({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("resize")));
const start_ = (identifier: ContainerIdentifier, options?: Options<"start">) =>
client
.start({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("start")));
const stop_ = (identifier: ContainerIdentifier, options?: Options<"stop">) =>
client
.stop({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("stop")));
const restart_ = (identifier: ContainerIdentifier, options?: Options<"restart">) =>
client
.restart({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("restart")));
const kill_ = (identifier: ContainerIdentifier, options?: Options<"kill">) =>
client
.kill({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("kill")));
const update_ = (identifier: ContainerIdentifier, config: (typeof ContainerConfig)["~type.make.in"]) =>
ContainerConfig.makeEffect(config).pipe(
Effect.flatMap((payload) => client.update({ params: { identifier }, payload })),
Effect.mapError(ContainersError("update"))
);
const rename_ = (identifier: ContainerIdentifier, name: string) =>
client.rename({ params: { identifier }, query: { name } }).pipe(Effect.mapError(ContainersError("rename")));
const pause_ = (identifier: ContainerIdentifier) =>
client.pause({ params: { identifier } }).pipe(Effect.mapError(ContainersError("pause")));
const unpause_ = (identifier: ContainerIdentifier) =>
client.unpause({ params: { identifier } }).pipe(Effect.mapError(ContainersError("unpause")));
const attach_ = (identifier: ContainerIdentifier, options?: Options<"attach">) =>
client
.attach({
params: { identifier },
query: { ...options },
headers: { Connection: "Upgrade", Upgrade: "tcp" }, // FIXME: Broken on undici
responseMode: "response-only",
})
.pipe(
Effect.flatMap(responseToStreamingSocketOrFailUnsafe),
Effect.mapError(ContainersError("attach"))
);
const attachWebsocket_ = (identifier: ContainerIdentifier, options?: Options<"attachWebsocket">) =>
Effect.gen(function* () {
const baseUrl = "ws://0.0.0.0";
const stripBaseUrl = String.replace(baseUrl, String.empty);
const emptyResponse = new Response(undefined, { status: 200 });
const noopHttpClient = HttpClient.make((request) =>
Effect.succeed(HttpClientResponse.fromWeb(request, emptyResponse))
);
const client = yield* HttpApiClient.endpoint(ContainersApi, {
endpoint: "attachWebsocket",
httpClient: noopHttpClient,
group: "containers",
baseUrl,
});
const response = yield* client({
responseMode: "response-only",
params: { identifier },
query: { ...options },
});
const { hash, url, urlParams } = response.request;
const wsUrl = yield* Url.make(url, urlParams, hash.valueOrUndefined).pipe(
Effect.fromResult,
Effect.mapError((_cause) => new HttpApiError.BadRequest()),
Effect.map((url) => url.toString()),
Effect.map(stripBaseUrl)
);
const websocket = websocketConstructor(wsUrl);
yield* Effect.callback<void, Socket.SocketError>((resume) => {
if (websocket.readyState === 1) {
resume(Effect.void);
return;
}
const onOpen = () => {
cleanup();
resume(Effect.void);
};
const onFailure = (cause: unknown) => {
cleanup();
resume(
Effect.fail(
new Socket.SocketError({
reason: new Socket.SocketOpenError({ kind: "Unknown", cause }),
})
)
);
};
const cleanup = () => {
websocket.removeEventListener("open", onOpen);
websocket.removeEventListener("error", onFailure);
websocket.removeEventListener("close", onFailure);
};
websocket.addEventListener("open", onOpen, { once: true });
websocket.addEventListener("error", onFailure, { once: true });
websocket.addEventListener("close", onFailure, { once: true });
return Effect.sync(cleanup);
});
const socket = yield* Socket.fromWebSocket(
Effect.acquireRelease(Effect.succeed(websocket), (ws) => Effect.sync(() => ws.close())),
{ closeCodeIsError: (code) => code !== 1000 }
);
return makeRawSocket(socket);
}).pipe(Effect.mapError(ContainersError("attachWebsocket")));
const wait_ = (identifier: ContainerIdentifier, options?: Options<"wait">) =>
client
.wait({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("wait")));
const delete_ = (identifier: ContainerIdentifier, options?: Options<"delete">) =>
client
.delete({ params: { identifier }, query: { ...options } })
.pipe(Effect.mapError(ContainersError("delete")));
const archive_ = (identifier: ContainerIdentifier, options: Options<"archive">) =>
client
.archive({ params: { identifier }, query: { ...options } })
.pipe(Stream.unwrap, Stream.mapError(ContainersError("archive")));
const archiveInfo_ = (identifier: ContainerIdentifier, options: Options<"archiveInfo">) =>
client.archiveInfo({ params: { identifier }, query: { ...options }, responseMode: "response-only" }).pipe(
Effect.flatMap(
HttpClientResponse.schemaHeaders(
Schema.Struct({
"x-docker-container-path-stat": Schema.StringFromBase64.pipe(
Schema.decodeTo(Schema.fromJsonString(Schema.Unknown)),
Schema.optional
),
})
)
),
Effect.map(({ "x-docker-container-path-stat": pathStat }) => pathStat),
Effect.mapError(ContainersError("archiveInfo"))
);
const putArchive_ = <E, R>(
identifier: ContainerIdentifier,
stream: Stream.Stream<Uint8Array, E, R>,
options: Options<"putArchive">
) =>
Effect.contextWith((context: Context.Context<R>) =>
client.putArchive({
params: { identifier },
query: { ...options },
payload: Stream.provideContext(stream, context),
})
).pipe(Effect.mapError(ContainersError("putArchive")));
const prune_ = (filters?: Schema.Schema.Type<typeof PruneFilters> | undefined) =>
client.prune({ query: { filters } }).pipe(Effect.mapError(ContainersError("prune")));
return {
list: list_,
create: create_,
inspect: inspect_,
top: top_,
logs: logs_,
changes: changes_,
export: export_,
stats: stats_,
resize: resize_,
start: start_,
stop: stop_,
restart: restart_,
kill: kill_,
update: update_,
rename: rename_,
pause: pause_,
unpause: unpause_,
attach: attach_,
attachWebsocket: attachWebsocket_,
wait: wait_,
delete: delete_,
archive: archive_,
archiveInfo: archiveInfo_,
putArchive: putArchive_,
prune: prune_,
};
}),
}) {}
/**
* @since 1.0.0
* @category Layers
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Container
*/
export const ContainersLayer: Layer.Layer<Containers, never, HttpClient.HttpClient | Socket.WebSocketConstructor> =
Layer.effect(Containers, Containers.make);
/**
* @since 1.0.0
* @category Layers
* @see https://docs.docker.com/reference/api/engine/latest/#tag/Container
*/
export const ContainersLayerLocalSocket: Layer.Layer<
Containers,
never,
HttpClient.HttpClient | Socket.WebSocketConstructor
> = ContainersLayer.pipe(
Layer.provide(
makeAgnosticHttpClientLayer(
MobyConnectionOptions.socket({
socketPath: "/var/run/docker.sock",
})
)
)
);