Support GitHub Enterprise Server using ARC (#59)

* Test nix-installer-action on Namespace.so

It is special in that it doesn't have systemd, and it'd be great to
support Namespace.so. It is also a good test case for a variety
of self-hosted GHA runner use cases.

* Make correlation more confident

* Borrow docker as a process supervisor on Linux GHA runners without systemd

This change introduces a Docker container shim which spawns the Nix
daemon after bind mounting all the relevant paths into the container.

The image is actually completely empty, other than metadata about what
to run.

This is a cheap and cheerful way to get decent process supervision in
environments that don't bring systemd, but do have docker ... which
is most everywhere in the GHA ecosystem.

* Ignore generated files

* Run on arm64 why not

* Load a pre-built image, don't build

* Check the userInfo.username instead of an env var

* Stop double-printing output to the console

* can't rm and restart

* what

* Clean up the container at the end

* Emit the fetch line in the 'installing nix' section

* tweak output

* delete what
This commit is contained in:
Graham Christensen 2023-12-04 14:17:47 -05:00 committed by GitHub
parent 84fe9e450f
commit cd46bde16a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 525 additions and 558 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
dist/* linguist-generated=true

View file

@ -18,7 +18,13 @@ jobs:
- run: test $(git status --porcelain=v1 2>/dev/null | wc -l) -eq 0
run-x86_64-linux:
name: Run x86_64 Linux
runs-on: ubuntu-22.04
strategy:
matrix:
runner:
- ubuntu-latest
- nscloud-ubuntu-22.04-amd64-4x16
- namespace-profile-default-arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v3
- name: Install Nix

View file

@ -91,6 +91,7 @@ Differing from the upstream [Nix](https://github.com/NixOS/nix) installer script
| `extra-args` | Extra arguments to pass to the planner (prefer using structured `with:` arguments unless using a custom [planner]!) | string | |
| `extra-conf` | Extra configuration lines for `/etc/nix/nix.conf` (includes `access-tokens` with `secrets.GITHUB_TOKEN` automatically if `github-token` is set) | string | |
| `flakehub` | Log in to FlakeHub to pull private flakes using the GitHub Actions [JSON Web Token](https://jwt.io) (JWT), which is bound to the `api.flakehub.com` audience. | Boolean | `false` |
| `force-docker-shim` | Force the use of Docker as a process supervisor. This setting is automatically enabled when necessary. | Boolean | `false` |
| `github-token` | A [GitHub token] for making authenticated requests (which have a higher rate-limit quota than unauthenticated requests) | string | `${{ github.token }}` |
| `github-server-url` | The URL for the GitHub server, to use with the `github-token` token. Defaults to the current GitHub server, supporting GitHub Enterprise Server automatically. Only change this value if the provided `github-token` is for a different GitHub server than the current server. | string | `${{ github.server }}` |
| `init` | The init system to configure (requires `planner: linux-multi`) | enum (`none` or `systemd`) | |

View file

@ -17,6 +17,9 @@ inputs:
description: Automatically log in to your [FlakeHub](https://flakehub.com) account, for accessing private flakes.
required: false
default: false
force-docker-shim:
description: Force the use of Docker as a process supervisor. This setting is automatically enabled when necessary.
default: false
github-token:
description: A GitHub token for making authenticated requests (which have a higher rate-limit quota than unauthenticated requests)
default: ${{ github.token }}

453
dist/37.index.js generated vendored
View file

@ -1,453 +0,0 @@
"use strict";
exports.id = 37;
exports.ids = [37];
exports.modules = {
/***/ 4037:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "toFormData": () => (/* binding */ toFormData)
/* harmony export */ });
/* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2185);
/* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8010);
let s = 0;
const S = {
START_BOUNDARY: s++,
HEADER_FIELD_START: s++,
HEADER_FIELD: s++,
HEADER_VALUE_START: s++,
HEADER_VALUE: s++,
HEADER_VALUE_ALMOST_DONE: s++,
HEADERS_ALMOST_DONE: s++,
PART_DATA_START: s++,
PART_DATA: s++,
END: s++
};
let f = 1;
const F = {
PART_BOUNDARY: f,
LAST_BOUNDARY: f *= 2
};
const LF = 10;
const CR = 13;
const SPACE = 32;
const HYPHEN = 45;
const COLON = 58;
const A = 97;
const Z = 122;
const lower = c => c | 0x20;
const noop = () => {};
class MultipartParser {
/**
* @param {string} boundary
*/
constructor(boundary) {
this.index = 0;
this.flags = 0;
this.onHeaderEnd = noop;
this.onHeaderField = noop;
this.onHeadersEnd = noop;
this.onHeaderValue = noop;
this.onPartBegin = noop;
this.onPartData = noop;
this.onPartEnd = noop;
this.boundaryChars = {};
boundary = '\r\n--' + boundary;
const ui8a = new Uint8Array(boundary.length);
for (let i = 0; i < boundary.length; i++) {
ui8a[i] = boundary.charCodeAt(i);
this.boundaryChars[ui8a[i]] = true;
}
this.boundary = ui8a;
this.lookbehind = new Uint8Array(this.boundary.length + 8);
this.state = S.START_BOUNDARY;
}
/**
* @param {Uint8Array} data
*/
write(data) {
let i = 0;
const length_ = data.length;
let previousIndex = this.index;
let {lookbehind, boundary, boundaryChars, index, state, flags} = this;
const boundaryLength = this.boundary.length;
const boundaryEnd = boundaryLength - 1;
const bufferLength = data.length;
let c;
let cl;
const mark = name => {
this[name + 'Mark'] = i;
};
const clear = name => {
delete this[name + 'Mark'];
};
const callback = (callbackSymbol, start, end, ui8a) => {
if (start === undefined || start !== end) {
this[callbackSymbol](ui8a && ui8a.subarray(start, end));
}
};
const dataCallback = (name, clear) => {
const markSymbol = name + 'Mark';
if (!(markSymbol in this)) {
return;
}
if (clear) {
callback(name, this[markSymbol], i, data);
delete this[markSymbol];
} else {
callback(name, this[markSymbol], data.length, data);
this[markSymbol] = 0;
}
};
for (i = 0; i < length_; i++) {
c = data[i];
switch (state) {
case S.START_BOUNDARY:
if (index === boundary.length - 2) {
if (c === HYPHEN) {
flags |= F.LAST_BOUNDARY;
} else if (c !== CR) {
return;
}
index++;
break;
} else if (index - 1 === boundary.length - 2) {
if (flags & F.LAST_BOUNDARY && c === HYPHEN) {
state = S.END;
flags = 0;
} else if (!(flags & F.LAST_BOUNDARY) && c === LF) {
index = 0;
callback('onPartBegin');
state = S.HEADER_FIELD_START;
} else {
return;
}
break;
}
if (c !== boundary[index + 2]) {
index = -2;
}
if (c === boundary[index + 2]) {
index++;
}
break;
case S.HEADER_FIELD_START:
state = S.HEADER_FIELD;
mark('onHeaderField');
index = 0;
// falls through
case S.HEADER_FIELD:
if (c === CR) {
clear('onHeaderField');
state = S.HEADERS_ALMOST_DONE;
break;
}
index++;
if (c === HYPHEN) {
break;
}
if (c === COLON) {
if (index === 1) {
// empty header field
return;
}
dataCallback('onHeaderField', true);
state = S.HEADER_VALUE_START;
break;
}
cl = lower(c);
if (cl < A || cl > Z) {
return;
}
break;
case S.HEADER_VALUE_START:
if (c === SPACE) {
break;
}
mark('onHeaderValue');
state = S.HEADER_VALUE;
// falls through
case S.HEADER_VALUE:
if (c === CR) {
dataCallback('onHeaderValue', true);
callback('onHeaderEnd');
state = S.HEADER_VALUE_ALMOST_DONE;
}
break;
case S.HEADER_VALUE_ALMOST_DONE:
if (c !== LF) {
return;
}
state = S.HEADER_FIELD_START;
break;
case S.HEADERS_ALMOST_DONE:
if (c !== LF) {
return;
}
callback('onHeadersEnd');
state = S.PART_DATA_START;
break;
case S.PART_DATA_START:
state = S.PART_DATA;
mark('onPartData');
// falls through
case S.PART_DATA:
previousIndex = index;
if (index === 0) {
// boyer-moore derrived algorithm to safely skip non-boundary data
i += boundaryEnd;
while (i < bufferLength && !(data[i] in boundaryChars)) {
i += boundaryLength;
}
i -= boundaryEnd;
c = data[i];
}
if (index < boundary.length) {
if (boundary[index] === c) {
if (index === 0) {
dataCallback('onPartData', true);
}
index++;
} else {
index = 0;
}
} else if (index === boundary.length) {
index++;
if (c === CR) {
// CR = part boundary
flags |= F.PART_BOUNDARY;
} else if (c === HYPHEN) {
// HYPHEN = end boundary
flags |= F.LAST_BOUNDARY;
} else {
index = 0;
}
} else if (index - 1 === boundary.length) {
if (flags & F.PART_BOUNDARY) {
index = 0;
if (c === LF) {
// unset the PART_BOUNDARY flag
flags &= ~F.PART_BOUNDARY;
callback('onPartEnd');
callback('onPartBegin');
state = S.HEADER_FIELD_START;
break;
}
} else if (flags & F.LAST_BOUNDARY) {
if (c === HYPHEN) {
callback('onPartEnd');
state = S.END;
flags = 0;
} else {
index = 0;
}
} else {
index = 0;
}
}
if (index > 0) {
// when matching a possible boundary, keep a lookbehind reference
// in case it turns out to be a false lead
lookbehind[index - 1] = c;
} else if (previousIndex > 0) {
// if our boundary turned out to be rubbish, the captured lookbehind
// belongs to partData
const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength);
callback('onPartData', 0, previousIndex, _lookbehind);
previousIndex = 0;
mark('onPartData');
// reconsider the current character even so it interrupted the sequence
// it could be the beginning of a new sequence
i--;
}
break;
case S.END:
break;
default:
throw new Error(`Unexpected state entered: ${state}`);
}
}
dataCallback('onHeaderField');
dataCallback('onHeaderValue');
dataCallback('onPartData');
// Update properties for the next call
this.index = index;
this.state = state;
this.flags = flags;
}
end() {
if ((this.state === S.HEADER_FIELD_START && this.index === 0) ||
(this.state === S.PART_DATA && this.index === this.boundary.length)) {
this.onPartEnd();
} else if (this.state !== S.END) {
throw new Error('MultipartParser.end(): stream ended unexpectedly');
}
}
}
function _fileName(headerValue) {
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);
if (!m) {
return;
}
const match = m[2] || m[3] || '';
let filename = match.slice(match.lastIndexOf('\\') + 1);
filename = filename.replace(/%22/g, '"');
filename = filename.replace(/&#(\d{4});/g, (m, code) => {
return String.fromCharCode(code);
});
return filename;
}
async function toFormData(Body, ct) {
if (!/multipart/i.test(ct)) {
throw new TypeError('Failed to fetch');
}
const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
if (!m) {
throw new TypeError('no or bad content-type header, no multipart boundary');
}
const parser = new MultipartParser(m[1] || m[2]);
let headerField;
let headerValue;
let entryValue;
let entryName;
let contentType;
let filename;
const entryChunks = [];
const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__/* .FormData */ .Ct();
const onPartData = ui8a => {
entryValue += decoder.decode(ui8a, {stream: true});
};
const appendToFile = ui8a => {
entryChunks.push(ui8a);
};
const appendFileToFormData = () => {
const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__/* .File */ .$B(entryChunks, filename, {type: contentType});
formData.append(entryName, file);
};
const appendEntryToFormData = () => {
formData.append(entryName, entryValue);
};
const decoder = new TextDecoder('utf-8');
decoder.decode();
parser.onPartBegin = function () {
parser.onPartData = onPartData;
parser.onPartEnd = appendEntryToFormData;
headerField = '';
headerValue = '';
entryValue = '';
entryName = '';
contentType = '';
filename = null;
entryChunks.length = 0;
};
parser.onHeaderField = function (ui8a) {
headerField += decoder.decode(ui8a, {stream: true});
};
parser.onHeaderValue = function (ui8a) {
headerValue += decoder.decode(ui8a, {stream: true});
};
parser.onHeaderEnd = function () {
headerValue += decoder.decode();
headerField = headerField.toLowerCase();
if (headerField === 'content-disposition') {
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);
if (m) {
entryName = m[2] || m[3] || '';
}
filename = _fileName(headerValue);
if (filename) {
parser.onPartData = appendToFile;
parser.onPartEnd = appendFileToFormData;
}
} else if (headerField === 'content-type') {
contentType = headerValue;
}
headerValue = '';
headerField = '';
};
for await (const chunk of Body) {
parser.write(chunk);
}
parser.end();
return formData;
}
/***/ })
};
;
//# sourceMappingURL=37.index.js.map

1
dist/37.index.js.map generated vendored

File diff suppressed because one or more lines are too long

BIN
dist/amd64.tar.gz generated vendored Normal file

Binary file not shown.

BIN
dist/arm64.tar.gz generated vendored Normal file

Binary file not shown.

256
dist/index.js generated vendored
View file

@ -23,7 +23,13 @@ __nccwpck_require__.r(__webpack_exports__);
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__nccwpck_require__.n(node_path__WEBPACK_IMPORTED_MODULE_6__);
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_7__ = __nccwpck_require__(7561);
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__nccwpck_require__.n(node_fs__WEBPACK_IMPORTED_MODULE_7__);
/* harmony import */ var string_argv__WEBPACK_IMPORTED_MODULE_8__ = __nccwpck_require__(1810);
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_8__ = __nccwpck_require__(612);
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__nccwpck_require__.n(node_os__WEBPACK_IMPORTED_MODULE_8__);
/* harmony import */ var string_argv__WEBPACK_IMPORTED_MODULE_10__ = __nccwpck_require__(1810);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_9__ = __nccwpck_require__(1017);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__nccwpck_require__.n(path__WEBPACK_IMPORTED_MODULE_9__);
@ -34,7 +40,7 @@ __nccwpck_require__.r(__webpack_exports__);
class NixInstallerAction {
constructor() {
constructor(correlation) {
this.platform = get_nix_platform();
this.nix_package_url = action_input_string_or_null("nix-package-url");
this.backtrace = action_input_string_or_null("backtrace");
@ -42,6 +48,7 @@ class NixInstallerAction {
this.extra_conf = action_input_multiline_string_or_null("extra-conf");
this.flakehub = action_input_bool("flakehub");
this.kvm = action_input_bool("kvm");
this.force_docker_shim = action_input_bool("force-docker-shim");
this.github_token = action_input_string_or_null("github-token");
this.github_server_url = action_input_string_or_null("github-server-url");
this.init = action_input_string_or_null("init");
@ -65,9 +72,69 @@ class NixInstallerAction {
this.start_daemon = action_input_bool("start-daemon");
this.diagnostic_endpoint = action_input_string_or_null("diagnostic-endpoint");
this.trust_runner_user = action_input_bool("trust-runner-user");
this.correlation = process.env["STATE_correlation"];
this.correlation = correlation;
this.nix_installer_url = resolve_nix_installer_url(this.platform, this.correlation);
}
async detectAndForceDockerShim() {
// 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.
if (process.env.RUNNER_OS !== "Linux") {
if (this.force_docker_shim) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning("Ignoring force-docker-shim which is set to true, as it is only supported on Linux.");
this.force_docker_shim = false;
}
return;
}
const systemdCheck = node_fs__WEBPACK_IMPORTED_MODULE_7___default().statSync("/run/systemd/system", {
throwIfNoEntry: false,
});
if (systemdCheck === null || systemdCheck === void 0 ? void 0 : systemdCheck.isDirectory()) {
if (this.force_docker_shim) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning("Systemd is detected, but ignoring it since force-docker-shim is enabled.");
}
else {
return;
}
}
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug("Linux detected without systemd, testing for Docker with `docker info` as an alternative daemon supervisor.");
const exit_code = await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec("docker", ["info"], {
silent: true,
listeners: {
stdout: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(trimmed);
}
},
stderr: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(trimmed);
}
},
},
});
if (exit_code !== 0) {
if (this.force_docker_shim) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning("docker info check failed, but trying anyway since force-docker-shim is enabled.");
}
else {
return;
}
}
_actions_core__WEBPACK_IMPORTED_MODULE_0__.startGroup("Enabling the Docker shim for running Nix on Linux in CI without Systemd.");
if (this.init !== "none") {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`Changing init from '${this.init}' to 'none'`);
this.init = "none";
}
if (this.planner !== "linux") {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`Changing planner from '${this.planner}' to 'linux'`);
this.planner = "linux";
}
this.force_docker_shim = true;
_actions_core__WEBPACK_IMPORTED_MODULE_0__.endGroup();
}
async executionEnvironment() {
const execution_env = {};
execution_env.NIX_INSTALLER_NO_CONFIRM = "true";
@ -166,7 +233,13 @@ class NixInstallerAction {
extra_conf += "\n";
}
if (this.trust_runner_user !== null) {
extra_conf += `trusted-users = root ${process.env.USER}`;
const user = (0,node_os__WEBPACK_IMPORTED_MODULE_8__.userInfo)().username;
if (user) {
extra_conf += `trusted-users = root ${user}`;
}
else {
extra_conf += `trusted-users = root`;
}
extra_conf += "\n";
}
if (this.flakehub) {
@ -190,7 +263,7 @@ class NixInstallerAction {
}
async execute_install(binary_path) {
const execution_env = await this.executionEnvironment();
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`Execution environment: ${JSON.stringify(execution_env, null, 4)}`);
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(`Execution environment: ${JSON.stringify(execution_env, null, 4)}`);
const args = ["install"];
if (this.planner) {
args.push(this.planner);
@ -199,25 +272,11 @@ class NixInstallerAction {
args.push(get_default_planner());
}
if (this.extra_args) {
const extra_args = (0,string_argv__WEBPACK_IMPORTED_MODULE_8__/* ["default"] */ .Z)(this.extra_args);
const extra_args = (0,string_argv__WEBPACK_IMPORTED_MODULE_10__/* ["default"] */ .Z)(this.extra_args);
args.concat(extra_args);
}
const exit_code = await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec(binary_path, args, {
env: Object.assign(Object.assign({}, execution_env), process.env),
listeners: {
stdout: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(trimmed);
}
},
stderr: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(trimmed);
}
},
},
});
if (exit_code !== 0) {
throw new Error(`Non-zero exit code of \`${exit_code}\` detected`);
@ -254,10 +313,110 @@ class NixInstallerAction {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.exportVariable("DETERMINATE_NIX_KVM", "0");
}
// Normal just doing of the install
_actions_core__WEBPACK_IMPORTED_MODULE_0__.startGroup("Installing Nix");
const binary_path = await this.fetch_binary();
await this.execute_install(binary_path);
_actions_core__WEBPACK_IMPORTED_MODULE_0__.endGroup();
if (this.force_docker_shim) {
await this.spawnDockerShim();
}
await this.set_github_path();
}
async spawnDockerShim() {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.startGroup("Configuring the Docker shim as the Nix Daemon's process supervisor");
const images = {
X64: __nccwpck_require__.ab + "amd64.tar.gz",
ARM64: __nccwpck_require__.ab + "arm64.tar.gz",
};
let arch;
if (process.env.RUNNER_ARCH === "X64") {
arch = "X64";
}
else if (process.env.RUNNER_ARCH === "ARM64") {
arch = "ARM64";
}
else {
throw Error("Architecture not supported in Docker shim mode.");
}
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug("Loading image: determinate-nix-shim:latest...");
{
const exit_code = await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec("docker", ["image", "load", "--input", images[arch]], {
silent: true,
listeners: {
stdout: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(trimmed);
}
},
stderr: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(trimmed);
}
},
},
});
if (exit_code !== 0) {
throw new Error(`Failed to build the shim image, exit code: \`${exit_code}\``);
}
}
{
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug("Starting the Nix daemon through Docker...");
const exit_code = await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec("docker", [
"--log-level=debug",
"run",
"--detach",
"--privileged",
"--userns=host",
"--pid=host",
"--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.correlation}`,
"determinate-nix-shim:latest",
], {
silent: true,
listeners: {
stdline: (data) => {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.saveState("docker_shim_container_id", data.trimEnd());
},
stdout: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(trimmed);
}
},
stderr: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(trimmed);
}
},
},
});
if (exit_code !== 0) {
throw new Error(`Failed to start the Nix daemon through Docker, exit code: \`${exit_code}\``);
}
}
_actions_core__WEBPACK_IMPORTED_MODULE_0__.endGroup();
return;
}
async cleanupDockerShim() {
const container_id = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getState("docker_shim_container_id");
if (container_id !== "") {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.startGroup("Cleaning up the Nix daemon's Docker shim");
await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec("docker", ["rm", "--force", container_id]);
_actions_core__WEBPACK_IMPORTED_MODULE_0__.endGroup();
}
}
async set_github_path() {
// Interim versions of the `nix-installer` crate may have already manipulated `$GITHUB_PATH`, as root even! Accessing that will be an error.
try {
@ -290,20 +449,6 @@ class NixInstallerAction {
async execute_uninstall() {
const exit_code = await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec(`/nix/nix-installer`, ["uninstall"], {
env: Object.assign({ NIX_INSTALLER_NO_CONFIRM: "true" }, process.env),
listeners: {
stdout: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(trimmed);
}
},
stderr: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(trimmed);
}
},
},
});
if (exit_code !== 0) {
throw new Error(`Non-zero exit code of \`${exit_code}\` detected`);
@ -329,7 +474,14 @@ class NixInstallerAction {
"-c",
`echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee ${kvm_rules} > /dev/null`,
], {
silent: true,
listeners: {
stdout: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.debug(trimmed);
}
},
stderr: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
@ -343,6 +495,7 @@ class NixInstallerAction {
}
const debug_run_throw = async (action, command, args) => {
const reload_exit_code = await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec(command, args, {
silent: true,
listeners: {
stdout: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
@ -375,16 +528,7 @@ class NixInstallerAction {
return true;
}
catch (error) {
await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec("sudo", ["rm", "-f", kvm_rules], {
listeners: {
stderr: (data) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.info(trimmed);
}
},
},
});
await _actions_exec__WEBPACK_IMPORTED_MODULE_3__.exec("sudo", ["rm", "-f", kvm_rules]);
return false;
}
}
@ -562,18 +706,20 @@ function action_input_bool(name) {
}
async function main() {
try {
if (!process.env["STATE_correlation"]) {
const correlation = `GH-${(0,node_crypto__WEBPACK_IMPORTED_MODULE_5__.randomUUID)()}`;
let correlation = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getState("correlation");
if (correlation === "") {
correlation = `GH-${(0,node_crypto__WEBPACK_IMPORTED_MODULE_5__.randomUUID)()}`;
_actions_core__WEBPACK_IMPORTED_MODULE_0__.saveState("correlation", correlation);
process.env["STATE_correlation"] = correlation;
}
const installer = new NixInstallerAction();
const isPost = !!process.env["STATE_isPost"];
if (!isPost) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__.saveState("isPost", "true");
const installer = new NixInstallerAction(correlation);
const isPost = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getState("isPost");
if (isPost !== "true") {
await installer.detectAndForceDockerShim();
await installer.install();
_actions_core__WEBPACK_IMPORTED_MODULE_0__.saveState("isPost", "true");
}
else {
await installer.cleanupDockerShim();
await installer.report_overall();
}
}
@ -17293,6 +17439,14 @@ module.exports = require("node:fs/promises");
/***/ }),
/***/ 612:
/***/ ((module) => {
"use strict";
module.exports = require("node:os");
/***/ }),
/***/ 9411:
/***/ ((module) => {

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

19
docker-shim/Dockerfile Normal file
View file

@ -0,0 +1,19 @@
# Determinate Nix Installer: Docker Shim
#
# This empty image exists to lean on Docker as a process supervisor when
# systemd isn't available. Specifically intended for self-hosted GitHub
# Actions runners using Docker-in-Docker.
#
# See: https://github.com/DeterminateSystems/nix-installer-action
FROM scratch
ENTRYPOINT [ "/nix/var/nix/profiles/default/bin/nix-daemon"]
CMD []
HEALTHCHECK \
--interval=5m \
--timeout=3s \
CMD ["/nix/var/nix/profiles/default/bin/nix", "store", "ping", "--store", "daemon"]
COPY ./Dockerfile /README.md

52
docker-shim/README.md Normal file
View file

@ -0,0 +1,52 @@
# Determinate Nix Installer Action: Docker Shim
The image in this repository is a product of the contained Dockerfile.
It is an otherwise empty image with a configuration layer.
This image is to be used in GitHub Actions runners which don't have systemd available, like self-hosted ARC runners.
The image would have no layers / content at all, however Docker has a bug and refuses to export those images.
This isn't a technical limitation preventing us from creating and distributing that image, but an ease-of-use limitation.
Since some of Docker's inspection tools break on an empty image, the image contains a single layer containing a README.
To build:
```shell
docker build . --tag determinate-nix-shim:latest
docker image save determinate-nix-shim:latest | gzip --best > amd64.tar
```
Then, extract the tarball:
```
mkdir extract
cd extract
tar -xf ../amd64.tar
```
It'll look like this, though the hashes will be different.
```
.
├── 771204abb853cdde06bbbc680001a02642050a1db1a7b0a48cf5f20efa8bdc5d.json
├── c4088111818e553e834adfc81bda8fe6da281afa9a40012eaa82796fb5476e98
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── manifest.json
└── repositories
```
Ignore `manifest.json`, and edit the other two JSON documents to replace `amd64` with `arm64`, both in a key named "architecture:
```
"architecture":"amd64"
```
Then re-create the tar, from within the `extract` directory:
```
tar --options gzip:compression-level=9 -zcf ../arm64.tar.gz .
```
Then `git add` the two .tar.gz's and you're done.

BIN
docker-shim/amd64.tar.gz Normal file

Binary file not shown.

BIN
docker-shim/arm64.tar.gz Normal file

Binary file not shown.

View file

@ -6,7 +6,9 @@ import { chmod, access, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { join } from "node:path";
import fs from "node:fs";
import { userInfo } from "node:os";
import stringArgv from "string-argv";
import * as path from "path";
class NixInstallerAction {
platform: string;
@ -18,7 +20,7 @@ class NixInstallerAction {
kvm: boolean;
github_server_url: string | null;
github_token: string | null;
// TODO: linux_init
force_docker_shim: boolean | null;
init: string | null;
local_root: string | null;
log_directives: string | null;
@ -46,9 +48,9 @@ class NixInstallerAction {
// This is for monitoring the real impact of Nix updates, to avoid breaking large
// swaths of users at once with botched Nix releases. For example:
// https://github.com/NixOS/nix/issues/9052.
correlation: string | undefined;
correlation: string;
constructor() {
constructor(correlation: string) {
this.platform = get_nix_platform();
this.nix_package_url = action_input_string_or_null("nix-package-url");
this.backtrace = action_input_string_or_null("backtrace");
@ -56,6 +58,7 @@ class NixInstallerAction {
this.extra_conf = action_input_multiline_string_or_null("extra-conf");
this.flakehub = action_input_bool("flakehub");
this.kvm = action_input_bool("kvm");
this.force_docker_shim = action_input_bool("force-docker-shim");
this.github_token = action_input_string_or_null("github-token");
this.github_server_url = action_input_string_or_null("github-server-url");
this.init = action_input_string_or_null("init");
@ -89,13 +92,89 @@ class NixInstallerAction {
"diagnostic-endpoint",
);
this.trust_runner_user = action_input_bool("trust-runner-user");
this.correlation = process.env["STATE_correlation"];
this.correlation = correlation;
this.nix_installer_url = resolve_nix_installer_url(
this.platform,
this.correlation,
);
}
async detectAndForceDockerShim(): Promise<void> {
// 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.
if (process.env.RUNNER_OS !== "Linux") {
if (this.force_docker_shim) {
actions_core.warning(
"Ignoring force-docker-shim which is set to true, as it is only supported on Linux.",
);
this.force_docker_shim = false;
}
return;
}
const systemdCheck = fs.statSync("/run/systemd/system", {
throwIfNoEntry: false,
});
if (systemdCheck?.isDirectory()) {
if (this.force_docker_shim) {
actions_core.warning(
"Systemd is detected, but ignoring it since force-docker-shim is enabled.",
);
} else {
return;
}
}
actions_core.debug(
"Linux detected without systemd, testing for Docker with `docker info` as an alternative daemon supervisor.",
);
const exit_code = await actions_exec.exec("docker", ["info"], {
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.debug(trimmed);
}
},
},
});
if (exit_code !== 0) {
if (this.force_docker_shim) {
actions_core.warning(
"docker info check failed, but trying anyway since force-docker-shim is enabled.",
);
} else {
return;
}
}
actions_core.startGroup(
"Enabling the Docker shim for running Nix on Linux in CI without Systemd.",
);
if (this.init !== "none") {
actions_core.info(`Changing init from '${this.init}' to 'none'`);
this.init = "none";
}
if (this.planner !== "linux") {
actions_core.info(`Changing planner from '${this.planner}' to 'linux'`);
this.planner = "linux";
}
this.force_docker_shim = true;
actions_core.endGroup();
}
private async executionEnvironment(): Promise<ExecuteEnvironment> {
const execution_env: ExecuteEnvironment = {};
@ -218,7 +297,12 @@ class NixInstallerAction {
extra_conf += "\n";
}
if (this.trust_runner_user !== null) {
extra_conf += `trusted-users = root ${process.env.USER}`;
const user = userInfo().username;
if (user) {
extra_conf += `trusted-users = root ${user}`;
} else {
extra_conf += `trusted-users = root`;
}
extra_conf += "\n";
}
if (this.flakehub) {
@ -250,7 +334,7 @@ class NixInstallerAction {
private async execute_install(binary_path: string): Promise<number> {
const execution_env = await this.executionEnvironment();
actions_core.info(
actions_core.debug(
`Execution environment: ${JSON.stringify(execution_env, null, 4)}`,
);
@ -271,20 +355,6 @@ class NixInstallerAction {
...execution_env,
...process.env, // To get $PATH, etc
},
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.info(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.info(trimmed);
}
},
},
});
if (exit_code !== 0) {
@ -329,11 +399,137 @@ class NixInstallerAction {
}
// Normal just doing of the install
actions_core.startGroup("Installing Nix");
const binary_path = await this.fetch_binary();
await this.execute_install(binary_path);
actions_core.endGroup();
if (this.force_docker_shim) {
await this.spawnDockerShim();
}
await this.set_github_path();
}
async spawnDockerShim(): Promise<void> {
actions_core.startGroup(
"Configuring the Docker shim as the Nix Daemon's process supervisor",
);
const images: { [key: string]: string } = {
X64: path.join(__dirname, "/../docker-shim/amd64.tar.gz"),
ARM64: path.join(__dirname, "/../docker-shim/arm64.tar.gz"),
};
let arch;
if (process.env.RUNNER_ARCH === "X64") {
arch = "X64";
} else if (process.env.RUNNER_ARCH === "ARM64") {
arch = "ARM64";
} else {
throw Error("Architecture not supported in Docker shim mode.");
}
actions_core.debug("Loading image: determinate-nix-shim:latest...");
{
const exit_code = await actions_exec.exec(
"docker",
["image", "load", "--input", images[arch]],
{
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.debug(trimmed);
}
},
},
},
);
if (exit_code !== 0) {
throw new Error(
`Failed to build the shim image, exit code: \`${exit_code}\``,
);
}
}
{
actions_core.debug("Starting the Nix daemon through Docker...");
const exit_code = await actions_exec.exec(
"docker",
[
"--log-level=debug",
"run",
"--detach",
"--privileged",
"--userns=host",
"--pid=host",
"--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.correlation}`,
"determinate-nix-shim:latest",
],
{
silent: true,
listeners: {
stdline: (data: string) => {
actions_core.saveState(
"docker_shim_container_id",
data.trimEnd(),
);
},
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.debug(trimmed);
}
},
},
},
);
if (exit_code !== 0) {
throw new Error(
`Failed to start the Nix daemon through Docker, exit code: \`${exit_code}\``,
);
}
}
actions_core.endGroup();
return;
}
async cleanupDockerShim(): Promise<void> {
const container_id = actions_core.getState("docker_shim_container_id");
if (container_id !== "") {
actions_core.startGroup("Cleaning up the Nix daemon's Docker shim");
await actions_exec.exec("docker", ["rm", "--force", container_id]);
actions_core.endGroup();
}
}
async set_github_path(): 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 {
@ -386,20 +582,6 @@ class NixInstallerAction {
NIX_INSTALLER_NO_CONFIRM: "true",
...process.env, // To get $PATH, etc
},
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.info(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.info(trimmed);
}
},
},
},
);
@ -433,7 +615,14 @@ class NixInstallerAction {
`echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee ${kvm_rules} > /dev/null`,
],
{
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.debug(trimmed);
}
},
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
@ -456,6 +645,7 @@ class NixInstallerAction {
args: string[],
): Promise<void> => {
const reload_exit_code = await actions_exec.exec(command, args, {
silent: true,
listeners: {
stdout: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
@ -493,16 +683,7 @@ class NixInstallerAction {
return true;
} catch (error) {
await actions_exec.exec("sudo", ["rm", "-f", kvm_rules], {
listeners: {
stderr: (data: Buffer) => {
const trimmed = data.toString("utf-8").trimEnd();
if (trimmed.length >= 0) {
actions_core.info(trimmed);
}
},
},
});
await actions_exec.exec("sudo", ["rm", "-f", kvm_rules]);
return false;
}
@ -751,18 +932,21 @@ function action_input_bool(name: string): boolean {
async function main(): Promise<void> {
try {
if (!process.env["STATE_correlation"]) {
const correlation = `GH-${randomUUID()}`;
let correlation: string = actions_core.getState("correlation");
if (correlation === "") {
correlation = `GH-${randomUUID()}`;
actions_core.saveState("correlation", correlation);
process.env["STATE_correlation"] = correlation;
}
const installer = new NixInstallerAction();
const isPost = !!process.env["STATE_isPost"];
if (!isPost) {
actions_core.saveState("isPost", "true");
const installer = new NixInstallerAction(correlation);
const isPost = actions_core.getState("isPost");
if (isPost !== "true") {
await installer.detectAndForceDockerShim();
await installer.install();
actions_core.saveState("isPost", "true");
} else {
await installer.cleanupDockerShim();
await installer.report_overall();
}
} catch (error) {