the-moby-effect
Version:
Moby/Docker API client built using effect-ts
199 lines (198 loc) • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.makeDindLayerFromPlatformConstructor = void 0;
var FileSystem = _interopRequireWildcard(require("@effect/platform/FileSystem"));
var Path = _interopRequireWildcard(require("@effect/platform/Path"));
var Array = _interopRequireWildcard(require("effect/Array"));
var Effect = _interopRequireWildcard(require("effect/Effect"));
var Function = _interopRequireWildcard(require("effect/Function"));
var HashMap = _interopRequireWildcard(require("effect/HashMap"));
var Layer = _interopRequireWildcard(require("effect/Layer"));
var Match = _interopRequireWildcard(require("effect/Match"));
var Number = _interopRequireWildcard(require("effect/Number"));
var Option = _interopRequireWildcard(require("effect/Option"));
var Schedule = _interopRequireWildcard(require("effect/Schedule"));
var Stream = _interopRequireWildcard(require("effect/Stream"));
var String = _interopRequireWildcard(require("effect/String"));
var Tuple = _interopRequireWildcard(require("effect/Tuple"));
var Tar = _interopRequireWildcard(require("eftar/Tar"));
var Untar = _interopRequireWildcard(require("eftar/Untar"));
var DockerEngine = _interopRequireWildcard(require("../../DockerEngine.js"));
var MobyConnection = _interopRequireWildcard(require("../../MobyConnection.js"));
var MobyConvey = _interopRequireWildcard(require("../../MobyConvey.js"));
var MobyEndpoints = _interopRequireWildcard(require("../../MobyEndpoints.js"));
var internalHttpBlob = _interopRequireWildcard(require("../blobs/http.js"));
var internalHttpsBlob = _interopRequireWildcard(require("../blobs/https.js"));
var internalSocketBlob = _interopRequireWildcard(require("../blobs/socket.js"));
var internalSshBlob = _interopRequireWildcard(require("../blobs/ssh.js"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/** @internal */
const downloadDindCertificates = dindContainerId => Effect.gen(function* () {
const containers = yield* MobyEndpoints.Containers;
const certs = yield* Untar.Untar(containers.archive(dindContainerId, {
path: "/certs"
}));
const readAndAssemble = path => Function.flow(HashMap.findFirst((_stream, header) => header.filename === path), Option.getOrThrow, Tuple.getSecond, Stream.decodeText(), Stream.mkString);
return yield* Effect.all({
ca: readAndAssemble("certs/server/ca.pem")(certs),
key: readAndAssemble("certs/server/key.pem")(certs),
cert: readAndAssemble("certs/server/cert.pem")(certs)
}, {
concurrency: 3
});
});
/** @internal */
const blobForExposeBy = /*#__PURE__*/Function.pipe(/*#__PURE__*/Match.type(), /*#__PURE__*/Match.when("ssh", () => internalSshBlob.content), /*#__PURE__*/Match.when("http", () => internalHttpBlob.content), /*#__PURE__*/Match.when("https", () => internalHttpsBlob.content), /*#__PURE__*/Match.when("socket", () => internalSocketBlob.content), Match.exhaustive);
/** @internal */
const makeDindBinds = exposeDindBy => Effect.gen(function* () {
const acquireScopedVolume = Effect.acquireRelease(MobyEndpoints.Volumes.use(volumes => volumes.create({})), ({
Name
}) => Effect.orDie(MobyEndpoints.Volumes.use(volumes => volumes.delete(Name))));
const volume1 = yield* acquireScopedVolume;
const volume2 = yield* acquireScopedVolume;
const tempSocketDirectory = yield* Effect.if(exposeDindBy === "socket", {
onFalse: () => Effect.succeed(""),
onTrue: () => Effect.flatMap(FileSystem.FileSystem, fs => Effect.gen(function* () {
const tempDirectory = yield* fs.makeTempDirectoryScoped();
yield* fs.chown(tempDirectory, 1000, 1000);
yield* fs.chmod(tempDirectory, 0o777);
return tempDirectory;
}))
});
const boundDockerSocket = yield* Effect.if(exposeDindBy === "socket", {
onFalse: () => Effect.succeed(""),
onTrue: () => Effect.map(Path.Path, path => path.join(tempSocketDirectory, "docker.sock"))
});
const mountBinds = exposeDindBy === "socket" ? [`${tempSocketDirectory}:/run/user/1000`] : [];
const volumeBinds = Tuple.make(`${volume1.Name}:/var/lib/docker`, `${volume2.Name}:/home/rootless/.local/share/docker`);
const binds = Array.appendAll(mountBinds, volumeBinds);
return [boundDockerSocket, binds];
});
/**
* Since the dind containers do not have health checks, we must wait until a
* specific log line is printed to know that the engine is ready.
*
* @internal
*/
const waitForDindContainerToBeReady = dindContainerId => Function.pipe(MobyEndpoints.Containers.use(containers => containers.logs(dindContainerId, {
follow: true,
stdout: true,
stderr: true
})), Stream.unwrap, Stream.takeUntil(String.includes("Daemon has completed initialization")), Stream.runDrain);
/**
* Spawns a docker in docker container on the remote host provided by another
* layer and exposes the dind container as a layer. This dind engine was built
* to power the unit tests and used for docker compose.
*
* @internal
*/
const makeDindLayerFromPlatformConstructor = platformLayerConstructor => options => Effect.gen(function* () {
// The generic type of the layer constructor is too wide
// since we want to be able to pass it as the only required generic
const platformLayerConstructorCasted = platformLayerConstructor;
// Building a layer here instead of providing it to the final effect
// prevents conflicting services with the same tag in the final layer
const hostDocker = yield* Layer.build(platformLayerConstructorCasted(options.connectionOptionsToHost));
const effectWithHostDocker = Effect.provide(hostDocker);
const streamWithHostDocker = Stream.provideContext(hostDocker);
// Test that the host docker is reachable
yield* Function.pipe(DockerEngine.pingHead(), Effect.retry(Schedule.recurs(5).pipe(Schedule.addDelay(() => "3 seconds"))), effectWithHostDocker);
// Build the docker image for the dind container
const dindTag = Array.lastNonEmpty(String.split(options.dindBaseImage, ":"));
const dindBlob = blobForExposeBy(options.exposeDindContainerBy);
const buildStream = streamWithHostDocker(DockerEngine.build({
dockerfile: "Dockerfile",
buildArgs: {
DIND_BASE_IMAGE: options.dindBaseImage
},
tag: `the-moby-effect-${options.exposeDindContainerBy}-${dindTag}:latest`,
context: Tar.tarballFromMemory(HashMap.make(["Dockerfile", dindBlob]))
}));
// Wait for the image to be built
yield* MobyConvey.waitForProgressToComplete(buildStream);
// Create volumes and binds for the container so they can be cleaned up after
const [boundDockerSocket, binds] = yield* effectWithHostDocker(makeDindBinds(options.exposeDindContainerBy));
// Create the dind container
const containerInspectResponse = yield* effectWithHostDocker(DockerEngine.runScoped({
spec: {
Image: `the-moby-effect-${options.exposeDindContainerBy}-${dindTag}:latest`,
Volumes: {
"/var/lib/docker": {},
"/home/rootless/.local/share/docker": {}
},
ExposedPorts: {
"22/tcp": {},
"2375/tcp": {},
"2376/tcp": {}
},
HostConfig: {
Privileged: true,
Binds: binds,
PortBindings: {
"22/tcp": [{
HostPort: "0"
}],
"2375/tcp": [{
HostPort: "0"
}],
"2376/tcp": [{
HostPort: "0"
}]
}
}
}
}));
// Extract the ports from the container inspect response
const tryGetPort = Function.flow(Option.fromNullable, Option.flatMap(Number.parse), Option.getOrThrow);
const sshPort = tryGetPort(containerInspectResponse.NetworkSettings?.Ports?.["22/tcp"]?.[0]?.HostPort);
const httpPort = tryGetPort(containerInspectResponse.NetworkSettings?.Ports?.["2375/tcp"]?.[0]?.HostPort);
const httpsPort = tryGetPort(containerInspectResponse.NetworkSettings?.Ports?.["2376/tcp"]?.[0]?.HostPort);
// Get the host from the connection options
const host = Function.pipe(Match.value(options.connectionOptionsToHost), Match.tag("socket", () => "localhost"), Match.orElse(({
host
}) => host));
// Wait for the dind container to be ready
yield* effectWithHostDocker(waitForDindContainerToBeReady(containerInspectResponse.Id));
// Get the engine certificates if we are exposing the dind container by https
const {
ca,
cert,
key
} = yield* Effect.if(options.exposeDindContainerBy === "https", {
onFalse: () => Effect.succeed({
ca: "",
cert: "",
key: ""
}),
onTrue: () => effectWithHostDocker(downloadDindCertificates(containerInspectResponse.Id))
});
// Craft the connection options
const connectionOptions = Function.pipe(Match.value(options.exposeDindContainerBy), Match.when("socket", () => MobyConnection.SocketConnectionOptions({
socketPath: boundDockerSocket
})), Match.when("http", () => MobyConnection.HttpConnectionOptions({
host,
port: httpPort
})), Match.when("https", () => MobyConnection.HttpsConnectionOptions({
host,
port: httpsPort,
ca,
key,
cert
})), Match.when("ssh", () => MobyConnection.SshConnectionOptions({
host,
port: sshPort,
username: "root",
password: "password",
remoteSocketPath: "/var/run/docker.sock"
})), Match.exhaustive);
// Build the layer for the same platform that we are on
const layer = platformLayerConstructorCasted(connectionOptions);
// Test that the dind container is reachable
yield* Function.pipe(DockerEngine.pingHead(), Effect.retry(Schedule.recurs(5).pipe(Schedule.addDelay(() => "3 seconds"))), Effect.provide(layer));
return layer;
}).pipe(Layer.unwrapScoped);
exports.makeDindLayerFromPlatformConstructor = makeDindLayerFromPlatformConstructor;
//# sourceMappingURL=dind.js.map