the-moby-effect
Version:
Moby/Docker API client built using effect-ts
144 lines • 6.14 kB
JavaScript
import * as Effect from "effect/Effect";
import * as Function from "effect/Function";
import * as Layer from "effect/Layer";
import * as Socket from "effect/unstable/socket/Socket";
import * as internalAgnostic from "./agnostic.js";
import * as internalConnection from "./connection.js";
/** @internal */
export const makeNodeSshAgent = (httpLazy, ssh2Lazy, connectionOptions) => new class extends httpLazy.Agent {
// The ssh client that will be connecting to the server
sshClient;
// How to connect to the remote server and where the moby socket is located.
sshConfig;
// Internal state to know if we are connected or not
sshConnection = "unopened";
openFailedError = null;
constructor(ssh2ConnectConfig, agentOptions) {
super(agentOptions);
this.sshConfig = ssh2ConnectConfig;
this.sshClient = new ssh2Lazy.Client();
}
/**
* When the agent is destroyed, we also want to close the ssh connection
* if it is still open.
*
* @since 1.0.0
* @see https://nodejs.org/api/http.html#agentdestroy
*/
destroy() {
this.sshClient.end();
this.sshClient.destroy();
super.destroy();
}
/**
* When creating a new connection, first start by trying to connect to
* the remote server over ssh. Then, if that was successful, we send a
* forwardOurStreamLocal request which is an OpenSSH extension that
* opens a connection to the unix domain socket at socketPath on the
* remote server and forwards traffic.
*
* @since 1.0.0
* @see https://nodejs.org/api/http.html#agentcreateconnectionoptions-callback
*/
createConnection(_options, callback) {
const onError = error => {
/**
* Indicates 'client-socket' for socket-level errors and
* 'client-ssh' for SSH disconnection messages. If the error is
* at the client-ssh level, we want to close the ssh connection
* and this agent cannot be used anymore.
*/
// if (error.level === "client-ssh") this.destroy();
callback(error, undefined);
};
// No connection yet, start connecting
if (this.sshConnection === "unopened") {
this.sshConnection = "connecting";
const unableToOpen = error => {
this.sshConnection = "open-failed";
this.openFailedError = error;
onError(error);
};
this.sshClient.once("ready", () => {
this.sshConnection = "ready";
this.sshClient.off("error", unableToOpen);
this.createConnection(_options, callback);
}).once("error", unableToOpen).connect(this.sshConfig);
}
// Already tried to connect but failed
else if (this.sshConnection === "open-failed") {
callback(this.openFailedError, undefined);
}
// Another connection attempt is already in progress, wait for it
else if (this.sshConnection === "connecting") {
this.sshClient.once("ready", () => {
this.sshClient.off("error", onError);
this.createConnection(_options, callback);
}).once("error", onError);
}
// We are connected to the ssh server, now forward our stream local
else if (this.sshConnection === "ready") {
this.sshClient.openssh_forwardOutStreamLocal(this.sshConfig.remoteSocketPath, (error, stream) => {
this.sshClient.off("error", onError);
if (error) return callback(error, void 0);else return callback(null, stream);
}).once("error", onError);
}
// Should never happen
else {
return Function.absurd(this.sshConnection);
}
}
}(connectionOptions);
/** @internal */
export const getNodeAgent = connectionOptions => Function.pipe(Effect.all({
httpLazy: Effect.promise(() => import("node:http")),
httpsLazy: Effect.promise(() => import("node:https"))
}, {
concurrency: 2
}), Effect.flatMap(({
httpLazy,
httpsLazy
}) => {
const AcquireNodeHttpAgent = internalConnection.MobyConnectionOptions.$match({
http: options => Effect.succeed(new httpLazy.Agent({
host: options.host,
port: options.port
})),
socket: options => Effect.succeed(new httpLazy.Agent({
socketPath: options.socketPath
})),
ssh: options => Effect.map(Effect.promise(() => import("ssh2")), ssh2Lazy => makeNodeSshAgent(httpLazy, ssh2Lazy, options)),
https: options => Effect.succeed(new httpsLazy.Agent({
ca: options.ca,
key: options.key,
cert: options.cert,
host: options.host,
port: options.port,
passphrase: options.passphrase
}))
});
const releaseNodeHttpAgent = agent => Effect.sync(() => agent.destroy());
const resource = Effect.acquireRelease(AcquireNodeHttpAgent(connectionOptions), releaseNodeHttpAgent);
return Effect.map(resource, agent => ({
http: agent,
https: agent
}));
}));
/** @internal */
export const getWebsocketConstructor = connectionOptions => Layer.effect(Socket.WebSocketConstructor, Effect.gen(function* () {
const ws = yield* Effect.promise(() => import("ws"));
const nodeHttpClientLazy = yield* Effect.promise(() => import("@effect/platform-node/NodeHttpClient"));
const agent = yield* nodeHttpClientLazy.HttpAgent;
return (url, protocols) => new ws.WebSocket(`ws://0.0.0.0${internalAgnostic.makeVersionPath(connectionOptions)}${url}`, protocols, {
agent: agent.http
});
}));
/** @internal */
export const makeNodeHttpClientLayer = connectionOptions => {
const httpAgentLive = Function.pipe(Effect.promise(() => import("@effect/platform-node/NodeHttpClient")), Effect.map(nodeHttpClientLazy => Layer.effect(nodeHttpClientLazy.HttpAgent, getNodeAgent(connectionOptions))), Layer.unwrap);
const websocketConstructorLive = getWebsocketConstructor(connectionOptions);
const nodeHttpClientLive = Function.pipe(Effect.promise(() => import("@effect/platform-node/NodeHttpClient")), Effect.map(nodeHttpClientLazy => nodeHttpClientLazy.layerNodeHttpNoAgent), Layer.unwrap);
const agnosticHttpClientLayer = internalAgnostic.makeAgnosticHttpClientLayer(connectionOptions);
return agnosticHttpClientLayer.pipe(Layer.merge(websocketConstructorLive), Layer.provide(nodeHttpClientLive), Layer.provide(httpAgentLive));
};
//# sourceMappingURL=node.js.map