UNPKG

the-moby-effect

Version:
199 lines (198 loc) 10.4 kB
"use strict"; 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