nix-installer-action/src/index.ts

1006 lines
32 KiB
TypeScript
Raw Normal View History

2024-04-15 19:23:29 -03:00
import * as actionsCore from "@actions/core";
2023-10-04 15:31:05 -04:00
import * as github from "@actions/github";
2024-04-15 19:23:29 -03:00
import * as actionsExec from "@actions/exec";
import { access, writeFile, readFile } from "node:fs/promises";
2023-07-13 10:11:00 -07:00
import { join } from "node:path";
import fs from "node:fs";
import { userInfo } from "node:os";
2023-07-13 10:11:00 -07:00
import stringArgv from "string-argv";
2024-05-15 16:45:00 -04:00
import * as path from "path";
2024-04-15 19:23:29 -03:00
import { IdsToolbox, inputs, platform } from "detsys-ts";
import { randomUUID } from "node:crypto";
2023-07-11 10:34:16 -07:00
2024-04-15 19:34:51 -03:00
// Nix installation events
const EVENT_INSTALL_NIX_FAILURE = "install_nix_failure";
const EVENT_INSTALL_NIX_START = "install_nix_start";
const EVENT_INSTALL_NIX_SUCCESS = "install_nix_start";
const EVENT_SETUP_KVM = "setup_kvm";
const EVENT_UNINSTALL_NIX = "uninstall";
// Docker events
const EVENT_CLEAN_UP_DOCKER_SHIM = "clean_up_docker_shim";
const EVENT_START_DOCKER_SHIM = "start_docker_shim";
// FlakeHub events
const EVENT_LOGIN_TO_FLAKEHUB = "login_to_flakehub";
// Other events
const EVENT_CONCLUDE_WORKFLOW = "conclude_workflow";
// Facts
const FACT_HAS_DOCKER = "has_docker";
const FACT_HAS_SYSTEMD = "has_systemd";
const FACT_IN_GITHUB_ACTIONS = "in_act";
const FACT_IN_NAMESPACE_SO = "in_namespace_so";
const FACT_NIX_INSTALLER_PLANNER = "nix_installer_planner";
2023-07-11 10:34:16 -07:00
class NixInstallerAction {
idslib: IdsToolbox;
2023-07-13 10:11:00 -07:00
platform: string;
2024-04-15 19:23:29 -03:00
nixPackageUrl: string | null;
2023-07-13 10:11:00 -07:00
backtrace: string | null;
2024-04-15 19:23:29 -03:00
extraArgs: string | null;
extraConf: string[] | null;
flakehub: boolean;
kvm: boolean;
2024-04-15 19:23:29 -03:00
githubServerUrl: string | null;
githubToken: string | null;
forceDockerShim: boolean | null;
2023-07-13 10:11:00 -07:00
init: string | null;
2024-04-15 19:23:29 -03:00
localRoot: string | null;
logDirectives: string | null;
2023-07-13 10:11:00 -07:00
logger: string | null;
2024-04-15 19:23:29 -03:00
sslCertFile: string | null;
2023-07-13 10:11:00 -07:00
proxy: string | null;
2024-04-15 19:23:29 -03:00
macCaseSensitive: string | null;
macEncrypt: string | null;
macRootDisk: string | null;
macVolumeLabel: string | null;
modifyProfile: boolean;
nixBuildGroupId: number | null;
nixBuildGroupName: string | null;
nixBuildUserBase: number | null;
nixBuildUserCount: number | null;
nixBuildUserPrefix: string | null;
2023-07-13 10:11:00 -07:00
planner: string | null;
reinstall: boolean;
2024-04-15 19:23:29 -03:00
startDaemon: boolean;
trustRunnerUser: boolean | null;
2023-07-11 10:34:16 -07:00
constructor() {
this.idslib = new IdsToolbox({
name: "nix-installer",
fetchStyle: "nix-style",
legacySourcePrefix: "nix-installer",
requireNix: "ignore",
});
2023-10-04 15:31:05 -04:00
2024-04-15 19:23:29 -03:00
this.platform = platform.getNixPlatform(platform.getArchOs());
this.nixPackageUrl = inputs.getStringOrNull("nix-package-url");
2024-04-15 19:09:42 -03:00
this.backtrace = inputs.getStringOrNull("backtrace");
2024-04-15 19:23:29 -03:00
this.extraArgs = inputs.getStringOrNull("extra-args");
this.extraConf = inputs.getMultilineStringOrNull("extra-conf");
2024-04-15 19:09:42 -03:00
this.flakehub = inputs.getBool("flakehub");
this.kvm = inputs.getBool("kvm");
2024-04-15 19:23:29 -03:00
this.forceDockerShim = inputs.getBool("force-docker-shim");
this.githubToken = inputs.getStringOrNull("github-token");
this.githubServerUrl = inputs.getStringOrNull("github-server-url");
2024-04-15 19:09:42 -03:00
this.init = inputs.getStringOrNull("init");
2024-04-15 19:23:29 -03:00
this.localRoot = inputs.getStringOrNull("local-root");
this.logDirectives = inputs.getStringOrNull("log-directives");
2024-04-15 19:09:42 -03:00
this.logger = inputs.getStringOrNull("logger");
2024-04-15 19:23:29 -03:00
this.sslCertFile = inputs.getStringOrNull("ssl-cert-file");
2024-04-15 19:09:42 -03:00
this.proxy = inputs.getStringOrNull("proxy");
2024-04-15 19:23:29 -03:00
this.macCaseSensitive = inputs.getStringOrNull("mac-case-sensitive");
this.macEncrypt = inputs.getStringOrNull("mac-encrypt");
this.macRootDisk = inputs.getStringOrNull("mac-root-disk");
this.macVolumeLabel = inputs.getStringOrNull("mac-volume-label");
this.modifyProfile = inputs.getBool("modify-profile");
this.nixBuildGroupId = inputs.getNumberOrNull("nix-build-group-id");
this.nixBuildGroupName = inputs.getStringOrNull("nix-build-group-name");
this.nixBuildUserBase = inputs.getNumberOrNull("nix-build-user-base");
this.nixBuildUserCount = inputs.getNumberOrNull("nix-build-user-count");
this.nixBuildUserPrefix = inputs.getStringOrNull("nix-build-user-prefix");
2024-04-15 19:09:42 -03:00
this.planner = inputs.getStringOrNull("planner");
this.reinstall = inputs.getBool("reinstall");
2024-04-15 19:23:29 -03:00
this.startDaemon = inputs.getBool("start-daemon");
this.trustRunnerUser = inputs.getBool("trust-runner-user");
2023-07-11 10:34:16 -07:00
}
async detectAndForceDockerShim(): Promise<void> {
2024-04-15 19:42:44 -03:00
const runnerOs = process.env["RUNNER_OS"];
// Detect if we're in a GHA runner which is Linux, doesn't have Systemd, and does have Docker.
// This is a common case in self-hosted runners, providers like [Namespace](https://namespace.so/),
// and especially GitHub Enterprise Server.
2024-04-15 19:42:44 -03:00
if (runnerOs !== "Linux") {
2024-04-15 19:23:29 -03:00
if (this.forceDockerShim) {
actionsCore.warning(
"Ignoring force-docker-shim which is set to true, as it is only supported on Linux.",
);
2024-04-15 19:23:29 -03:00
this.forceDockerShim = false;
}
return;
}
const systemdCheck = fs.statSync("/run/systemd/system", {
throwIfNoEntry: false,
});
if (systemdCheck?.isDirectory()) {
2024-04-15 19:23:29 -03:00
if (this.forceDockerShim) {
actionsCore.warning(
"Systemd is detected, but ignoring it since force-docker-shim is enabled.",
);
} else {
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_HAS_SYSTEMD, true);
return;
}
}
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_HAS_SYSTEMD, false);
2024-04-15 19:23:29 -03:00
actionsCore.debug(
"Linux detected without systemd, testing for Docker with `docker info` as an alternative daemon supervisor.",
);
2024-01-09 10:36:54 -08:00
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_HAS_DOCKER, false); // Set to false here, and only in the success case do we set it to true
2024-04-15 19:23:29 -03:00
let exitCode;
2024-01-09 10:36:54 -08:00
try {
2024-04-15 19:23:29 -03:00
exitCode = await actionsExec.exec("docker", ["info"], {
2024-01-09 10:36:54 -08:00
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
2024-01-09 10:36:54 -08:00
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
2024-01-09 10:36:54 -08:00
}
},
},
2024-01-09 10:36:54 -08:00
});
2024-05-02 13:03:12 -03:00
} catch {
2024-04-15 19:23:29 -03:00
actionsCore.debug("Docker not detected, not enabling docker shim.");
2024-01-09 10:36:54 -08:00
return;
}
2024-04-15 19:23:29 -03:00
if (exitCode !== 0) {
if (this.forceDockerShim) {
actionsCore.warning(
"docker info check failed, but trying anyway since force-docker-shim is enabled.",
);
} else {
return;
}
}
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_HAS_DOCKER, true);
if (
2024-04-15 19:23:29 -03:00
!this.forceDockerShim &&
(await this.detectDockerWithMountedDockerSocket())
) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(
"Detected a Docker container with a Docker socket mounted, not enabling docker shim.",
);
return;
}
2024-04-15 19:23:29 -03:00
actionsCore.startGroup(
"Enabling the Docker shim for running Nix on Linux in CI without Systemd.",
);
if (this.init !== "none") {
2024-04-15 19:23:29 -03:00
actionsCore.info(`Changing init from '${this.init}' to 'none'`);
this.init = "none";
}
if (this.planner !== "linux") {
2024-04-15 19:23:29 -03:00
actionsCore.info(`Changing planner from '${this.planner}' to 'linux'`);
this.planner = "linux";
}
2024-04-15 19:23:29 -03:00
this.forceDockerShim = true;
2024-04-15 19:23:29 -03:00
actionsCore.endGroup();
}
// Detect if we are running under `act` or some other system which is not using docker-in-docker,
// and instead using a mounted docker socket.
// In the case of the socket mount solution, the shim will cause issues since the given mount paths will
// equate to mount paths on the host, not mount paths to the docker container in question.
async detectDockerWithMountedDockerSocket(): Promise<boolean> {
2024-04-15 19:23:29 -03:00
let cgroupsBuffer;
try {
// If we are inside a docker container, the last line of `/proc/self/cgroup` should be
// 0::/docker/$SOME_ID
//
// If we are not, the line will likely be `0::/`
2024-04-15 19:23:29 -03:00
cgroupsBuffer = await readFile("/proc/self/cgroup", {
encoding: "utf-8",
});
} catch (e) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(
`Did not detect \`/proc/self/cgroup\` existence, bailing on docker container ID detection:\n${e}`,
);
return false;
}
2024-04-15 19:23:29 -03:00
const cgroups = cgroupsBuffer.trim().split("\n");
const lastCgroup = cgroups[cgroups.length - 1];
const lastCgroupParts = lastCgroup.split(":");
const lastCgroupPath = lastCgroupParts[lastCgroupParts.length - 1];
if (!lastCgroupPath.includes("/docker/")) {
actionsCore.debug(
"Did not detect a container ID, bailing on docker.sock detection",
);
return false;
}
// We are in a docker container, now to determine if this container is visible from
// the `docker` command, and if so, if there is a `docker.socket` mounted.
2024-04-15 19:23:29 -03:00
const lastCgroupPathParts = lastCgroupPath.split("/");
const containerId = lastCgroupPathParts[lastCgroupPathParts.length - 1];
// If we cannot `docker inspect` this discovered container ID, we'll fall through to the `catch` below.
2024-04-15 19:23:29 -03:00
let stdoutBuffer = "";
let stderrBuffer = "";
let exitCode;
try {
2024-04-15 19:23:29 -03:00
exitCode = await actionsExec.exec("docker", ["inspect", containerId], {
silent: true,
listeners: {
stdout: (data: Buffer) => {
2024-04-15 19:23:29 -03:00
stdoutBuffer += data.toString("utf-8");
},
stderr: (data: Buffer) => {
2024-04-15 19:23:29 -03:00
stderrBuffer += data.toString("utf-8");
},
},
});
} catch (e) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(
`Could not execute \`docker inspect ${containerId}\`, bailing on docker container inspection:\n${e}`,
);
return false;
}
2024-04-15 19:23:29 -03:00
if (exitCode !== 0) {
actionsCore.debug(
`Unable to inspect detected docker container with id \`${containerId}\`, bailing on container inspection (exit ${exitCode}):\n${stderrBuffer}`,
);
return false;
}
2024-04-15 19:23:29 -03:00
const output = JSON.parse(stdoutBuffer);
// `docker inspect $ID` prints an array containing objects.
// In our use case, we should only see 1 item in the array.
if (output.length !== 1) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(
`Got \`docker inspect ${containerId}\` output which was not one item (was ${output.length}), bailing on docker.sock detection.`,
);
return false;
}
const item = output[0];
// On this array item we want the `Mounts` field, which is an array
// containing `{ Type, Source, Destination, Mode}`.
// We are looking for a `Destination` ending with `docker.sock`.
const mounts = item["Mounts"];
if (typeof mounts !== "object") {
2024-04-15 19:23:29 -03:00
actionsCore.debug(
`Got non-object in \`Mounts\` field of \`docker inspect ${containerId}\` output, bailing on docker.sock detection.`,
);
return false;
}
2024-04-15 19:23:29 -03:00
let foundDockerSockMount = false;
for (const mount of mounts) {
const destination = mount["Destination"];
if (typeof destination === "string") {
if (destination.endsWith("docker.sock")) {
2024-04-15 19:23:29 -03:00
foundDockerSockMount = true;
break;
}
}
}
2024-04-15 19:23:29 -03:00
return foundDockerSockMount;
}
private async executionEnvironment(): Promise<ExecuteEnvironment> {
2024-04-15 19:23:29 -03:00
const executionEnv: ExecuteEnvironment = {};
2024-04-15 19:42:44 -03:00
const runnerOs = process.env["RUNNER_OS"];
2023-07-12 10:24:35 -07:00
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_NO_CONFIRM = "true";
executionEnv.NIX_INSTALLER_DIAGNOSTIC_ATTRIBUTION = JSON.stringify(
this.idslib.getCorrelationHashes(),
);
2023-07-11 10:34:16 -07:00
if (this.backtrace !== null) {
2024-04-15 19:23:29 -03:00
executionEnv.RUST_BACKTRACE = this.backtrace;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.modifyProfile !== null) {
if (this.modifyProfile) {
executionEnv.NIX_INSTALLER_MODIFY_PROFILE = "true";
2023-07-11 10:34:16 -07:00
} else {
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_MODIFY_PROFILE = "false";
2023-07-11 10:34:16 -07:00
}
}
2024-04-15 19:23:29 -03:00
if (this.nixBuildGroupId !== null) {
executionEnv.NIX_INSTALLER_NIX_BUILD_GROUP_ID = `${this.nixBuildGroupId}`;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.nixBuildGroupName !== null) {
executionEnv.NIX_INSTALLER_NIX_BUILD_GROUP_NAME = this.nixBuildGroupName;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.nixBuildUserPrefix !== null) {
executionEnv.NIX_INSTALLER_NIX_BUILD_USER_PREFIX =
this.nixBuildUserPrefix;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.nixBuildUserCount !== null) {
executionEnv.NIX_INSTALLER_NIX_BUILD_USER_COUNT = `${this.nixBuildUserCount}`;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.nixBuildUserBase !== null) {
executionEnv.NIX_INSTALLER_NIX_BUILD_USER_ID_BASE = `${this.nixBuildUserCount}`;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.nixPackageUrl !== null) {
executionEnv.NIX_INSTALLER_NIX_PACKAGE_URL = `${this.nixPackageUrl}`;
2023-07-11 10:34:16 -07:00
}
if (this.proxy !== null) {
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_PROXY = this.proxy;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.sslCertFile !== null) {
executionEnv.NIX_INSTALLER_SSL_CERT_FILE = this.sslCertFile;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_DIAGNOSTIC_ENDPOINT =
2024-04-24 11:13:40 -03:00
this.idslib.getDiagnosticsUrl()?.toString() ?? "";
2023-07-11 10:34:16 -07:00
// TODO: Error if the user uses these on not-MacOS
2024-04-15 19:23:29 -03:00
if (this.macEncrypt !== null) {
2024-04-15 19:42:44 -03:00
if (runnerOs !== "macOS") {
2023-07-13 11:03:55 -07:00
throw new Error("`mac-encrypt` while `$RUNNER_OS` was not `macOS`");
}
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_ENCRYPT = this.macEncrypt;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.macCaseSensitive !== null) {
2024-04-15 19:42:44 -03:00
if (runnerOs !== "macOS") {
2023-07-13 11:03:55 -07:00
throw new Error(
"`mac-case-sensitive` while `$RUNNER_OS` was not `macOS`",
);
}
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_CASE_SENSITIVE = this.macCaseSensitive;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.macVolumeLabel !== null) {
2024-04-15 19:42:44 -03:00
if (runnerOs !== "macOS") {
2023-07-13 11:03:55 -07:00
throw new Error(
"`mac-volume-label` while `$RUNNER_OS` was not `macOS`",
);
}
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_VOLUME_LABEL = this.macVolumeLabel;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.macRootDisk !== null) {
2024-04-15 19:42:44 -03:00
if (runnerOs !== "macOS") {
2023-07-13 11:03:55 -07:00
throw new Error("`mac-root-disk` while `$RUNNER_OS` was not `macOS`");
}
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_ROOT_DISK = this.macRootDisk;
2023-07-11 10:34:16 -07:00
}
2023-07-12 13:17:46 -07:00
if (this.logger !== null) {
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_LOGGER = this.logger;
2023-07-12 13:17:46 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.logDirectives !== null) {
executionEnv.NIX_INSTALLER_LOG_DIRECTIVES = this.logDirectives;
2023-07-12 13:17:46 -07:00
}
2023-07-11 10:34:16 -07:00
// TODO: Error if the user uses these on MacOS
if (this.init !== null) {
2024-04-15 19:42:44 -03:00
if (runnerOs === "macOS") {
2023-07-13 11:03:55 -07:00
throw new Error(
"`init` is not a valid option when `$RUNNER_OS` is `macOS`",
);
}
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_INIT = this.init;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.startDaemon !== null) {
if (this.startDaemon) {
executionEnv.NIX_INSTALLER_START_DAEMON = "true";
2023-07-11 10:34:16 -07:00
} else {
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_START_DAEMON = "false";
2023-07-11 10:34:16 -07:00
}
}
2024-04-15 19:23:29 -03:00
let extraConf = "";
if (this.githubServerUrl !== null && this.githubToken !== null) {
const serverUrl = this.githubServerUrl.replace("https://", "");
extraConf += `access-tokens = ${serverUrl}=${this.githubToken}`;
extraConf += "\n";
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.trustRunnerUser !== null) {
const user = userInfo().username;
if (user) {
2024-04-15 19:23:29 -03:00
extraConf += `trusted-users = root ${user}`;
} else {
2024-04-15 19:23:29 -03:00
extraConf += `trusted-users = root`;
}
2024-04-15 19:23:29 -03:00
extraConf += "\n";
2023-07-12 10:24:35 -07:00
}
if (this.flakehub) {
try {
2024-05-02 13:03:12 -03:00
const flakeHubNetrcFile = await this.flakehubLogin();
extraConf += `netrc-file = ${flakeHubNetrcFile}`;
extraConf += "\n";
} catch (e) {
2024-05-02 13:03:12 -03:00
actionsCore.warning(`Failed to set up FlakeHub: ${e}`);
}
}
2024-04-15 19:23:29 -03:00
if (this.extraConf !== null && this.extraConf.length !== 0) {
extraConf += this.extraConf.join("\n");
extraConf += "\n";
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_EXTRA_CONF = extraConf;
2023-07-12 10:24:35 -07:00
2024-04-15 19:42:44 -03:00
if (process.env["ACT"] && !process.env["NOT_ACT"]) {
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_IN_GITHUB_ACTIONS, true);
2024-04-15 19:23:29 -03:00
actionsCore.info(
2023-07-13 11:10:08 -07:00
"Detected `$ACT` environment, assuming this is a https://github.com/nektos/act created container, set `NOT_ACT=true` to override this. This will change the setting of the `init` to be compatible with `act`",
2023-07-13 10:11:00 -07:00
);
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_INIT = "none";
}
2024-04-15 19:42:44 -03:00
if (process.env["NSC_VM_ID"] && !process.env["NOT_NAMESPACE"]) {
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_IN_NAMESPACE_SO, true);
2024-04-15 19:23:29 -03:00
actionsCore.info(
"Detected Namespace runner, assuming this is a https://namespace.so created container, set `NOT_NAMESPACE=true` to override this. This will change the setting of the `init` to be compatible with Namespace",
);
2024-04-15 19:23:29 -03:00
executionEnv.NIX_INSTALLER_INIT = "none";
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:23:29 -03:00
return executionEnv;
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
private async executeInstall(binaryPath: string): Promise<number> {
const executionEnv = await this.executionEnvironment();
actionsCore.debug(
`Execution environment: ${JSON.stringify(executionEnv, null, 4)}`,
2023-07-13 10:11:00 -07:00
);
2023-07-12 10:24:35 -07:00
2023-07-13 10:11:00 -07:00
const args = ["install"];
2023-07-12 10:24:35 -07:00
if (this.planner) {
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_NIX_INSTALLER_PLANNER, this.planner);
2023-07-13 10:11:00 -07:00
args.push(this.planner);
2023-07-12 10:24:35 -07:00
} else {
2024-04-15 19:34:51 -03:00
this.idslib.addFact(FACT_NIX_INSTALLER_PLANNER, getDefaultPlanner());
2024-04-15 19:23:29 -03:00
args.push(getDefaultPlanner());
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:23:29 -03:00
if (this.extraArgs) {
const extraArgs = stringArgv(this.extraArgs);
args.concat(extraArgs);
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_INSTALL_NIX_START);
2024-04-15 19:23:29 -03:00
const exitCode = await actionsExec.exec(binaryPath, args, {
env: {
2024-04-15 19:23:29 -03:00
...executionEnv,
...process.env, // To get $PATH, etc
},
2023-07-13 10:11:00 -07:00
});
2023-07-11 10:34:16 -07:00
2024-04-15 19:23:29 -03:00
if (exitCode !== 0) {
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_INSTALL_NIX_FAILURE, {
2024-04-15 19:23:29 -03:00
exitCode,
});
2024-04-15 19:23:29 -03:00
throw new Error(`Non-zero exit code of \`${exitCode}\` detected`);
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_INSTALL_NIX_SUCCESS);
2024-04-15 19:23:29 -03:00
return exitCode;
2023-07-11 10:34:16 -07:00
}
async install(): Promise<void> {
2024-04-15 19:23:29 -03:00
const existingInstall = await this.detectExisting();
if (existingInstall) {
2023-07-12 10:24:35 -07:00
if (this.reinstall) {
// We need to uninstall, then reinstall
2024-04-15 19:23:29 -03:00
actionsCore.info(
2023-07-13 10:11:00 -07:00
"Nix was already installed, `reinstall` is set, uninstalling for a reinstall",
);
2024-04-15 19:23:29 -03:00
await this.executeUninstall();
2023-07-12 10:24:35 -07:00
} else {
// We're already installed, and not reinstalling, just set GITHUB_PATH and finish early
2024-04-15 19:23:29 -03:00
await this.setGithubPath();
actionsCore.info("Nix was already installed, using existing install");
2023-07-13 10:11:00 -07:00
return;
2023-07-12 10:24:35 -07:00
}
}
if (this.kvm) {
2024-04-15 19:23:29 -03:00
actionsCore.startGroup("Configuring KVM");
if (await this.setupKvm()) {
actionsCore.endGroup();
actionsCore.info("\u001b[32m Accelerated KVM is enabled \u001b[33m⚡");
actionsCore.exportVariable("DETERMINATE_NIX_KVM", "1");
} else {
2024-04-15 19:23:29 -03:00
actionsCore.endGroup();
actionsCore.info("KVM is not available.");
actionsCore.exportVariable("DETERMINATE_NIX_KVM", "0");
}
}
2023-07-12 10:24:35 -07:00
// Normal just doing of the install
2024-04-15 19:23:29 -03:00
actionsCore.startGroup("Installing Nix");
const binaryPath = await this.fetchBinary();
await this.executeInstall(binaryPath);
actionsCore.endGroup();
2024-04-15 19:23:29 -03:00
if (this.forceDockerShim) {
await this.spawnDockerShim();
}
2024-04-15 19:23:29 -03:00
await this.setGithubPath();
2023-07-12 10:24:35 -07:00
}
async spawnDockerShim(): Promise<void> {
2024-04-15 19:23:29 -03:00
actionsCore.startGroup(
"Configuring the Docker shim as the Nix Daemon's process supervisor",
);
const images: { [key: string]: string } = {
2024-05-15 16:45:00 -04:00
X64: path.join(__dirname, "/../docker-shim/amd64.tar.gz"),
ARM64: path.join(__dirname, "/../docker-shim/arm64.tar.gz"),
};
2024-04-15 19:42:44 -03:00
const runnerArch = process.env["RUNNER_ARCH"];
let arch;
2024-04-15 19:42:44 -03:00
if (runnerArch === "X64") {
arch = "X64";
2024-04-15 19:42:44 -03:00
} else if (runnerArch === "ARM64") {
arch = "ARM64";
} else {
throw Error("Architecture not supported in Docker shim mode.");
}
2024-04-15 19:23:29 -03:00
actionsCore.debug("Loading image: determinate-nix-shim:latest...");
{
2024-04-15 19:23:29 -03:00
const exitCode = await actionsExec.exec(
"docker",
["image", "load", "--input", images[arch]],
{
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
},
},
);
2024-04-15 19:23:29 -03:00
if (exitCode !== 0) {
throw new Error(
2024-04-15 19:23:29 -03:00
`Failed to build the shim image, exit code: \`${exitCode}\``,
);
}
}
{
2024-04-15 19:23:29 -03:00
actionsCore.debug("Starting the Nix daemon through Docker...");
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_START_DOCKER_SHIM);
2024-04-15 19:23:29 -03:00
const exitCode = await actionsExec.exec(
"docker",
[
"--log-level=debug",
"run",
"--detach",
"--privileged",
"--network=host",
"--userns=host",
"--pid=host",
"--mount",
"type=bind,src=/bin,dst=/bin,readonly",
"--mount",
"type=bind,src=/lib,dst=/lib,readonly",
"--mount",
"type=bind,src=/home,dst=/home,readonly",
"--mount",
"type=bind,src=/tmp,dst=/tmp",
"--mount",
"type=bind,src=/nix,dst=/nix",
"--mount",
"type=bind,src=/etc,dst=/etc,readonly",
"--restart",
"always",
"--init",
"--name",
`determinate-nix-shim-${this.idslib.getUniqueId()}-${randomUUID()}`,
"determinate-nix-shim:latest",
],
{
silent: true,
listeners: {
stdline: (data: string) => {
2024-04-15 19:23:29 -03:00
actionsCore.saveState("docker_shim_container_id", data.trimEnd());
},
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
},
},
);
2024-04-15 19:23:29 -03:00
if (exitCode !== 0) {
throw new Error(
2024-04-15 19:23:29 -03:00
`Failed to start the Nix daemon through Docker, exit code: \`${exitCode}\``,
);
}
}
2024-04-15 19:23:29 -03:00
actionsCore.endGroup();
return;
}
async cleanupDockerShim(): Promise<void> {
2024-04-15 19:23:29 -03:00
const containerId = actionsCore.getState("docker_shim_container_id");
2024-04-15 19:23:29 -03:00
if (containerId !== "") {
actionsCore.startGroup("Cleaning up the Nix daemon's Docker shim");
let cleaned = false;
try {
2024-04-15 19:23:29 -03:00
await actionsExec.exec("docker", ["rm", "--force", containerId]);
cleaned = true;
} catch {
2024-04-15 19:23:29 -03:00
actionsCore.warning("failed to cleanup nix daemon container");
}
if (!cleaned) {
2024-04-15 19:23:29 -03:00
actionsCore.info("trying to pkill the container's shim process");
try {
2024-04-15 19:23:29 -03:00
await actionsExec.exec("pkill", [containerId]);
cleaned = true;
} catch {
2024-04-15 19:23:29 -03:00
actionsCore.warning(
"failed to forcibly kill the container's shim process",
);
}
}
if (cleaned) {
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_CLEAN_UP_DOCKER_SHIM);
} else {
2024-04-15 19:23:29 -03:00
actionsCore.warning(
"Giving up on cleaning up the nix daemon container",
);
}
2024-04-15 19:23:29 -03:00
actionsCore.endGroup();
}
}
2024-04-15 19:23:29 -03:00
async setGithubPath(): Promise<void> {
// Interim versions of the `nix-installer` crate may have already manipulated `$GITHUB_PATH`, as root even! Accessing that will be an error.
try {
2024-04-15 19:23:29 -03:00
const nixVarNixProfilePath = "/nix/var/nix/profiles/default/bin";
2024-04-15 19:42:44 -03:00
const homeNixProfilePath = `${process.env["HOME"]}/.nix-profile/bin`;
2024-04-15 19:23:29 -03:00
actionsCore.addPath(nixVarNixProfilePath);
actionsCore.addPath(homeNixProfilePath);
actionsCore.info(
`Added \`${nixVarNixProfilePath}\` and \`${homeNixProfilePath}\` to \`$GITHUB_PATH\``,
);
2024-05-02 13:03:12 -03:00
} catch {
2024-04-15 19:23:29 -03:00
actionsCore.info(
"Skipping setting $GITHUB_PATH in action, the `nix-installer` crate seems to have done this already. From `nix-installer` version 0.11.0 and up, this step is done in the action. Prior to 0.11.0, this was only done in the `nix-installer` binary.",
);
}
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:23:29 -03:00
async flakehubLogin(): Promise<string> {
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_LOGIN_TO_FLAKEHUB);
2024-04-15 19:23:29 -03:00
const netrcPath = `${process.env["RUNNER_TEMP"]}/determinate-nix-installer-netrc`;
2024-04-15 19:23:29 -03:00
const jwt = await actionsCore.getIDToken("api.flakehub.com");
await writeFile(
2024-04-15 19:23:29 -03:00
netrcPath,
[
`machine api.flakehub.com login flakehub password ${jwt}`,
`machine flakehub.com login flakehub password ${jwt}`,
].join("\n"),
);
2024-04-15 19:23:29 -03:00
actionsCore.info("Logging in to FlakeHub.");
// the join followed by a match on ^... looks silly, but extra_config
// could contain multi-line values
2024-04-15 19:23:29 -03:00
if (this.extraConf?.join("\n").match(/^netrc-file/m)) {
actionsCore.warning(
"Logging in to FlakeHub conflicts with the Nix option `netrc-file`.",
);
}
2024-04-15 19:23:29 -03:00
return netrcPath;
}
2024-04-15 19:23:29 -03:00
async executeUninstall(): Promise<number> {
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_UNINSTALL_NIX);
2024-04-15 19:23:29 -03:00
const exitCode = await actionsExec.exec(
`/nix/nix-installer`,
["uninstall"],
{
env: {
NIX_INSTALLER_NO_CONFIRM: "true",
...process.env, // To get $PATH, etc
},
2023-07-13 10:11:00 -07:00
},
);
2023-07-12 10:24:35 -07:00
2024-04-15 19:23:29 -03:00
if (exitCode !== 0) {
throw new Error(`Non-zero exit code of \`${exitCode}\` detected`);
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:23:29 -03:00
return exitCode;
2023-07-12 10:24:35 -07:00
}
2024-04-15 19:23:29 -03:00
async detectExisting(): Promise<boolean> {
const receiptPath = "/nix/receipt.json";
2023-07-12 10:24:35 -07:00
try {
2024-04-15 19:23:29 -03:00
await access(receiptPath);
2023-07-12 10:24:35 -07:00
// There is a /nix/receipt.json
2023-07-13 10:11:00 -07:00
return true;
2023-07-12 10:24:35 -07:00
} catch {
// No /nix/receipt.json
2023-07-13 10:11:00 -07:00
return false;
2023-07-12 10:24:35 -07:00
}
2023-07-11 10:34:16 -07:00
}
2024-04-15 19:23:29 -03:00
private async setupKvm(): Promise<boolean> {
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_SETUP_KVM);
2024-04-15 19:23:29 -03:00
const currentUser = userInfo();
const isRoot = currentUser.uid === 0;
const maybeSudo = isRoot ? "" : "sudo";
2024-01-08 10:50:02 -08:00
2024-04-15 19:23:29 -03:00
const kvmRules = "/etc/udev/rules.d/99-determinate-nix-installer-kvm.rules";
try {
2024-04-15 19:23:29 -03:00
const writeFileExitCode = await actionsExec.exec(
"sh",
[
"-c",
2024-04-15 19:23:29 -03:00
`echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | ${maybeSudo} tee ${kvmRules} > /dev/null`,
],
{
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
},
},
);
2024-04-15 19:23:29 -03:00
if (writeFileExitCode !== 0) {
throw new Error(
2024-04-15 19:23:29 -03:00
`Non-zero exit code of \`${writeFileExitCode}\` detected while writing '${kvmRules}'`,
);
}
2024-04-15 19:23:29 -03:00
const debugRootRunThrow = async (
action: string,
command: string,
args: string[],
): Promise<void> => {
2024-04-15 19:23:29 -03:00
if (!isRoot) {
2024-01-08 10:50:02 -08:00
args = [command, ...args];
command = "sudo";
}
2024-04-15 19:23:29 -03:00
const reloadExitCode = await actionsExec.exec(command, args, {
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
2024-04-15 19:23:29 -03:00
actionsCore.debug(trimmed);
}
},
},
});
2024-04-15 19:23:29 -03:00
if (reloadExitCode !== 0) {
throw new Error(
2024-04-15 19:23:29 -03:00
`Non-zero exit code of \`${reloadExitCode}\` detected while ${action}.`,
);
}
};
2024-04-15 19:23:29 -03:00
await debugRootRunThrow("reloading udev rules", "udevadm", [
"control",
"--reload-rules",
]);
2024-04-15 19:23:29 -03:00
await debugRootRunThrow("triggering udev against kvm", "udevadm", [
"trigger",
"--name-match=kvm",
]);
return true;
2024-05-02 13:03:12 -03:00
} catch {
2024-04-15 19:23:29 -03:00
if (isRoot) {
await actionsExec.exec("rm", ["-f", kvmRules]);
2024-01-08 10:50:02 -08:00
} else {
2024-04-15 19:23:29 -03:00
await actionsExec.exec("sudo", ["rm", "-f", kvmRules]);
2024-01-08 10:50:02 -08:00
}
return false;
}
}
2024-04-15 19:23:29 -03:00
private async fetchBinary(): Promise<string> {
if (!this.localRoot) {
return await this.idslib.fetchExecutable();
2023-07-11 10:34:16 -07:00
} else {
2024-04-15 19:23:29 -03:00
const localPath = join(this.localRoot, `nix-installer-${this.platform}`);
actionsCore.info(`Using binary ${localPath}`);
return localPath;
2023-07-11 10:34:16 -07:00
}
}
2023-10-04 15:31:05 -04:00
2024-04-15 19:23:29 -03:00
async reportOverall(): Promise<void> {
2023-10-04 15:31:05 -04:00
try {
2024-04-15 19:34:51 -03:00
this.idslib.recordEvent(EVENT_CONCLUDE_WORKFLOW, {
2024-04-15 19:23:29 -03:00
conclusion: await this.getWorkflowConclusion(),
2023-10-04 15:31:05 -04:00
});
2024-05-02 13:03:12 -03:00
} catch (e) {
actionsCore.debug(`Error submitting post-run diagnostics report: ${e}`);
2023-10-04 15:31:05 -04:00
}
}
2024-04-15 19:23:29 -03:00
private async getWorkflowConclusion(): Promise<
2023-10-04 15:31:05 -04:00
undefined | "success" | "failure" | "cancelled" | "unavailable" | "no-jobs"
> {
2024-04-15 19:23:29 -03:00
if (this.githubToken == null) {
2023-10-04 15:31:05 -04:00
return undefined;
}
try {
2024-04-15 19:23:29 -03:00
const octokit = github.getOctokit(this.githubToken);
2023-10-04 15:31:05 -04:00
const jobs = await octokit.paginate(
octokit.rest.actions.listJobsForWorkflowRun,
{
owner: github.context.repo.owner,
repo: github.context.repo.repo,
2024-04-24 11:09:48 -03:00
/* eslint-disable camelcase */
2023-10-04 15:31:05 -04:00
run_id: github.context.runId,
},
);
2024-04-15 19:23:29 -03:00
actionsCore.debug(`awaited jobs: ${jobs}`);
2023-10-04 15:31:05 -04:00
const job = jobs
.filter((candidate) => candidate.name === github.context.job)
.at(0);
if (job === undefined) {
return "no-jobs";
}
2024-04-24 11:13:40 -03:00
const outcomes = (job.steps ?? []).map((j) => j.conclusion ?? "unknown");
2023-10-04 15:31:05 -04:00
// Possible values: success, failure, cancelled, or skipped
// from: https://docs.github.com/en/actions/learn-github-actions/contexts
if (outcomes.includes("failure")) {
// Any failures fails the job
return "failure";
}
if (outcomes.includes("cancelled")) {
// Any cancellations cancels the job
return "cancelled";
}
// Assume success if no jobs failed or were canceled
return "success";
2024-05-02 13:03:12 -03:00
} catch (e) {
actionsCore.debug(`Error determining final disposition: ${e}`);
2023-10-04 15:31:05 -04:00
return "unavailable";
}
}
2023-07-11 10:34:16 -07:00
}
type ExecuteEnvironment = {
// All env vars are strings, no fanciness here.
2023-07-13 10:11:00 -07:00
RUST_BACKTRACE?: string;
NIX_INSTALLER_MODIFY_PROFILE?: string;
NIX_INSTALLER_NIX_BUILD_GROUP_NAME?: string;
NIX_INSTALLER_NIX_BUILD_GROUP_ID?: string;
NIX_INSTALLER_NIX_BUILD_USER_PREFIX?: string;
NIX_INSTALLER_NIX_BUILD_USER_COUNT?: string;
NIX_INSTALLER_NIX_BUILD_USER_ID_BASE?: string;
NIX_INSTALLER_NIX_PACKAGE_URL?: string;
NIX_INSTALLER_PROXY?: string;
NIX_INSTALLER_SSL_CERT_FILE?: string;
NIX_INSTALLER_DIAGNOSTIC_ENDPOINT?: string;
2023-10-04 15:31:05 -04:00
NIX_INSTALLER_DIAGNOSTIC_ATTRIBUTION?: string;
2023-07-13 10:11:00 -07:00
NIX_INSTALLER_ENCRYPT?: string;
NIX_INSTALLER_CASE_SENSITIVE?: string;
NIX_INSTALLER_VOLUME_LABEL?: string;
NIX_INSTALLER_ROOT_DISK?: string;
NIX_INSTALLER_INIT?: string;
NIX_INSTALLER_START_DAEMON?: string;
NIX_INSTALLER_NO_CONFIRM?: string;
NIX_INSTALLER_EXTRA_CONF?: string;
NIX_INSTALLER_LOG_DIRECTIVES?: string;
NIX_INSTALLER_LOGGER?: string;
};
2023-07-11 10:34:16 -07:00
2024-04-15 19:23:29 -03:00
function getDefaultPlanner(): string {
2024-04-15 19:42:44 -03:00
const envOs = process.env["RUNNER_OS"];
2023-07-12 10:24:35 -07:00
2024-04-15 19:23:29 -03:00
if (envOs === "macOS") {
2023-07-13 10:11:00 -07:00
return "macos";
2024-04-15 19:23:29 -03:00
} else if (envOs === "Linux") {
2023-07-13 10:11:00 -07:00
return "linux";
2023-07-12 10:24:35 -07:00
} else {
2024-04-15 19:23:29 -03:00
throw new Error(`Unsupported \`RUNNER_OS\` (currently \`${envOs}\`)`);
2023-07-12 10:24:35 -07:00
}
}
function main(): void {
const installer = new NixInstallerAction();
2023-07-12 10:24:35 -07:00
installer.idslib.onMain(async () => {
await installer.detectAndForceDockerShim();
await installer.install();
});
installer.idslib.onPost(async () => {
await installer.cleanupDockerShim();
2024-04-15 19:23:29 -03:00
await installer.reportOverall();
});
installer.idslib.execute();
2023-07-11 10:34:16 -07:00
}
main();