offbox 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. offbox/__init__.py +2 -0
  2. offbox/__main__.py +4 -0
  3. offbox/_data/host/cloud-init.yaml.j2 +15 -0
  4. offbox/_data/host/host-init.sh +230 -0
  5. offbox/_data/images/universal/Dockerfile +101 -0
  6. offbox/_data/images/universal/offbox-init +75 -0
  7. offbox/_data/images/universal/offbox-run +131 -0
  8. offbox/_data/providers/gcp/.terraform.lock.hcl +52 -0
  9. offbox/_data/providers/gcp/main.tf +67 -0
  10. offbox/_data/providers/gcp/outputs.tf +7 -0
  11. offbox/_data/providers/gcp/provider.toml +9 -0
  12. offbox/_data/providers/gcp/variables.tf +33 -0
  13. offbox/_data/providers/gcp/versions.tf +12 -0
  14. offbox/_data/providers/hetzner/.terraform.lock.hcl +51 -0
  15. offbox/_data/providers/hetzner/main.tf +32 -0
  16. offbox/_data/providers/hetzner/outputs.tf +7 -0
  17. offbox/_data/providers/hetzner/provider.toml +9 -0
  18. offbox/_data/providers/hetzner/variables.tf +28 -0
  19. offbox/_data/providers/hetzner/versions.tf +12 -0
  20. offbox/_data/providers/ssh/provider.toml +4 -0
  21. offbox/cli/__init__.py +117 -0
  22. offbox/cli/config.py +39 -0
  23. offbox/cli/env.py +109 -0
  24. offbox/cli/exec.py +50 -0
  25. offbox/cli/host.py +313 -0
  26. offbox/cli/misc.py +132 -0
  27. offbox/cli/ports.py +44 -0
  28. offbox/config.py +189 -0
  29. offbox/console.py +63 -0
  30. offbox/doctor.py +174 -0
  31. offbox/drivers/__init__.py +0 -0
  32. offbox/drivers/base.py +40 -0
  33. offbox/drivers/none.py +114 -0
  34. offbox/drivers/tofu.py +59 -0
  35. offbox/errors.py +45 -0
  36. offbox/execute.py +772 -0
  37. offbox/host/__init__.py +89 -0
  38. offbox/host/activity.py +58 -0
  39. offbox/host/auto.py +322 -0
  40. offbox/host/cloudinit.py +44 -0
  41. offbox/host/create.py +198 -0
  42. offbox/host/destroy.py +151 -0
  43. offbox/host/init.py +196 -0
  44. offbox/host/ready.py +106 -0
  45. offbox/host/timer.py +227 -0
  46. offbox/host/usage.py +123 -0
  47. offbox/host/warnings.py +61 -0
  48. offbox/locking.py +27 -0
  49. offbox/logging.py +154 -0
  50. offbox/logs.py +135 -0
  51. offbox/naming.py +67 -0
  52. offbox/paths.py +55 -0
  53. offbox/ports.py +610 -0
  54. offbox/project.py +187 -0
  55. offbox/providers.py +117 -0
  56. offbox/py.typed +0 -0
  57. offbox/runner.py +291 -0
  58. offbox/runtime.py +887 -0
  59. offbox/setup.py +181 -0
  60. offbox/share.py +286 -0
  61. offbox/ssh.py +179 -0
  62. offbox/state.py +157 -0
  63. offbox/status.py +378 -0
  64. offbox/sync.py +380 -0
  65. offbox/timings.py +69 -0
  66. offbox/tools.py +227 -0
  67. offbox/uninstall.py +114 -0
  68. offbox-0.1.0.dist-info/METADATA +140 -0
  69. offbox-0.1.0.dist-info/RECORD +72 -0
  70. offbox-0.1.0.dist-info/WHEEL +4 -0
  71. offbox-0.1.0.dist-info/entry_points.txt +2 -0
  72. offbox-0.1.0.dist-info/licenses/LICENSE +21 -0
offbox/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+ DEFAULT_IMAGE = "ghcr.io/o-avdieiev/offbox-universal:1"
offbox/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from offbox.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,15 @@
1
+ #cloud-config
2
+ ssh_deletekeys: true
3
+ ssh_genkeytypes: []
4
+ ssh_keys:
5
+ ed25519_private: {{ private_host_key | tojson }}
6
+ ed25519_public: {{ public_host_key | tojson }}
7
+ disable_root: true
8
+ ssh_pwauth: false
9
+ write_files:
10
+ - path: /usr/local/sbin/offbox-host-init
11
+ owner: root:root
12
+ permissions: "0700"
13
+ content: {{ script | tojson }}
14
+ runcmd:
15
+ - {{ bootstrap_argv | tojson }}
@@ -0,0 +1,230 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ docker_key_fingerprint=9DC858229FC7DD38854AE2D88D81803C0EBFCD88
5
+ stage=preflight
6
+ failure=
7
+ work_dir=
8
+
9
+ fail() {
10
+ failure=$1
11
+ printf 'offbox: %s\n Fix: %s\n' "$1" "$2" >&2
12
+ exit 3
13
+ }
14
+
15
+ cleanup() {
16
+ result=$?
17
+ if [ -n "$work_dir" ]; then
18
+ rm -rf "$work_dir"
19
+ fi
20
+ if [ "$result" -ne 0 ]; then
21
+ if [ -d /var/lib/offbox ] && [ -w /var/lib/offbox ]; then
22
+ printf 'exit=%s stage=%s %s\n' "$result" "$stage" "$failure" > /var/lib/offbox/failed || :
23
+ fi
24
+ if [ "$result" -ne 3 ]; then
25
+ printf 'offbox: bootstrap failed during %s.\n Fix: Review the bootstrap log and retry after resolving the failure.\n' "$stage" >&2
26
+ fi
27
+ fi
28
+ }
29
+ trap cleanup EXIT
30
+ trap 'exit 129' HUP
31
+ trap 'exit 130' INT
32
+ trap 'exit 143' TERM
33
+
34
+ apt_get() {
35
+ apt-get -o DPkg::Lock::Timeout=600 "$@"
36
+ }
37
+
38
+ [ "$(id -u)" -eq 0 ] || fail 'Bootstrap requires root.' 'Run offbox host init with passwordless sudo access.'
39
+ install -d -m 0755 /var/lib/offbox
40
+ rm -f /var/lib/offbox/ready /var/lib/offbox/failed
41
+ [ "$#" -eq 2 ] || fail 'Expected image and SSH public key arguments.' 'Invoke bootstrap through offbox host create or host init.'
42
+ image=$1
43
+ public_key=$2
44
+ harden=${OFFBOX_HARDEN:-1}
45
+ version=${OFFBOX_VERSION:-}
46
+ case "$harden" in 0|1) ;; *) fail 'Invalid OFFBOX_HARDEN.' 'Use 1 for cloud hosts or 0 for an existing host.' ;; esac
47
+ case "$version" in ''|*[!A-Za-z0-9.+_-]*) fail 'Missing or invalid OFFBOX_VERSION.' 'Pass the installed offbox version through OFFBOX_VERSION.' ;; esac
48
+ case "$image" in ''|-*) fail 'Invalid container image.' 'Pass the configured offbox image.' ;; esac
49
+ case "$public_key" in
50
+ ssh-ed25519\ *|ssh-rsa\ *|ecdsa-sha2-*\ *|sk-ssh-ed25519@openssh.com\ *|sk-ecdsa-sha2-*\ *) ;;
51
+ *) fail 'Invalid SSH public key.' 'Select an OpenSSH public key file.' ;;
52
+ esac
53
+ newline='
54
+ '
55
+ carriage_return=$(printf '\r')
56
+ case "$public_key" in *"$newline"*|*"$carriage_return"*) fail 'SSH public key must contain one line.' 'Select a single OpenSSH public key.' ;; esac
57
+ [ -f /etc/os-release ] || fail 'Cannot identify host OS.' 'Use Ubuntu 24.04.'
58
+ os_id=$(awk -F= '$1 == "ID" {gsub(/"/, "", $2); print $2}' /etc/os-release)
59
+ os_version=$(awk -F= '$1 == "VERSION_ID" {gsub(/"/, "", $2); print $2}' /etc/os-release)
60
+ [ "$os_id:$os_version" = ubuntu:24.04 ] || fail 'Host must run Ubuntu 24.04.' 'Use an Ubuntu 24.04 host.'
61
+
62
+ uid_owner=$(getent passwd 1000 | cut -d: -f1 || true)
63
+ if [ -n "$uid_owner" ] && [ "$uid_owner" != offbox ]; then
64
+ if [ "$harden" != 1 ] || [ "$uid_owner" != ubuntu ]; then
65
+ fail "UID 1000 belongs to $uid_owner; refusing to change that account." 'Use a dedicated host with UID 1000 available, or move the account manually before retrying.'
66
+ fi
67
+ if pgrep -u ubuntu >/dev/null; then
68
+ fail 'The default ubuntu account has running processes.' 'Use a fresh cloud host without an active ubuntu session.'
69
+ fi
70
+ fi
71
+ if id offbox >/dev/null 2>&1; then
72
+ [ "$(getent passwd offbox | cut -d: -f6)" = /home/offbox ] || fail 'Existing offbox account has an unexpected home directory.' 'Resolve the account configuration before retrying.'
73
+ [ "$(id -u offbox)" -eq 1000 ] || fail 'Existing offbox account does not have UID 1000.' 'Use a dedicated host or resolve the conflicting offbox account manually.'
74
+ fi
75
+ for package in docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc; do
76
+ if dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q '^install ok installed$'; then
77
+ fail "Conflicting package $package is installed." 'Review the existing Docker installation and remove conflicting packages before retrying.'
78
+ fi
79
+ done
80
+ installed_docker=$(dpkg-query -W -f='${Version}' docker-ce 2>/dev/null || true)
81
+ case "$installed_docker" in ''|5:29.*) ;; *) fail "Existing Docker version $installed_docker differs from required major 29." 'Review and migrate Docker to major 29 before retrying.' ;; esac
82
+
83
+ work_dir=$(mktemp -d)
84
+
85
+ install_config() {
86
+ source_file=$1
87
+ target_file=$2
88
+ mode=$3
89
+ if [ ! -f "$target_file" ] || ! cmp -s "$source_file" "$target_file"; then
90
+ install -m "$mode" "$source_file" "$target_file"
91
+ config_changed=1
92
+ fi
93
+ chmod "$mode" "$target_file"
94
+ }
95
+
96
+ printf '%s\n' "$public_key" > "$work_dir/public_key"
97
+ ssh-keygen -lf "$work_dir/public_key" >/dev/null 2>&1 || fail 'SSH public key cannot be parsed.' 'Select a valid OpenSSH public key.'
98
+
99
+ stage=account
100
+ if [ "$uid_owner" = ubuntu ]; then
101
+ userdel -r ubuntu
102
+ fi
103
+ if ! getent group offbox >/dev/null; then
104
+ groupadd offbox
105
+ fi
106
+ if ! id offbox >/dev/null 2>&1; then
107
+ useradd --uid 1000 --gid offbox --create-home --shell /bin/bash --password '*' offbox
108
+ fi
109
+ usermod --password '*' offbox
110
+ install -d -m 0700 -o offbox -g offbox /home/offbox/.ssh
111
+ printf '%s\n' "$public_key" > "$work_dir/authorized_keys"
112
+ install_config "$work_dir/authorized_keys" /home/offbox/.ssh/authorized_keys 0600
113
+ chown offbox:offbox /home/offbox/.ssh/authorized_keys
114
+
115
+ stage=packages
116
+ export DEBIAN_FRONTEND=noninteractive
117
+ apt_get update
118
+ apt_get install -y ca-certificates curl sudo jq gpg
119
+ if [ "$harden" = 1 ]; then
120
+ apt_get install -y ufw fail2ban unattended-upgrades
121
+ fi
122
+ printf 'offbox ALL=(ALL) NOPASSWD:ALL\n' > "$work_dir/sudoers"
123
+ visudo -cf "$work_dir/sudoers"
124
+ install_config "$work_dir/sudoers" /etc/sudoers.d/offbox 0440
125
+
126
+ if [ "$harden" = 1 ]; then
127
+ stage=hardening
128
+ install -d -m 0755 /etc/ssh/sshd_config.d
129
+ cat > "$work_dir/sshd" <<'SSH'
130
+ PasswordAuthentication no
131
+ KbdInteractiveAuthentication no
132
+ PermitRootLogin no
133
+ AllowUsers offbox
134
+ SSH
135
+ config_changed=0
136
+ install_config "$work_dir/sshd" /etc/ssh/sshd_config.d/00-offbox.conf 0644
137
+ /usr/sbin/sshd -t
138
+ /usr/sbin/sshd -T > "$work_dir/sshd-effective"
139
+ for setting in 'passwordauthentication no' 'kbdinteractiveauthentication no' 'permitrootlogin no' 'allowusers offbox'; do
140
+ grep -qx "$setting" "$work_dir/sshd-effective" || fail 'SSH hardening was overridden by host configuration.' 'Review sshd_config before retrying bootstrap.'
141
+ done
142
+ if [ "$config_changed" = 1 ]; then
143
+ systemctl try-reload-or-restart ssh
144
+ fi
145
+ ufw default deny incoming
146
+ ufw allow 22/tcp
147
+ ufw --force enable
148
+ printf '[sshd]\nenabled = true\nbackend = systemd\n' > "$work_dir/fail2ban"
149
+ config_changed=0
150
+ install_config "$work_dir/fail2ban" /etc/fail2ban/jail.d/offbox.local 0644
151
+ systemctl enable --now fail2ban
152
+ if [ "$config_changed" = 1 ]; then
153
+ systemctl restart fail2ban
154
+ fi
155
+ fi
156
+
157
+ stage=docker
158
+ key_fingerprints() {
159
+ install -d -m 0700 "$work_dir/gnupg"
160
+ GNUPGHOME="$work_dir/gnupg" gpg --batch --show-keys --with-colons "$1" 2>/dev/null | awk -F: '$1 == "pub" {primary = 1; next} primary && $1 == "fpr" {print $10; primary = 0}'
161
+ }
162
+ install -d -m 0755 /etc/apt/keyrings
163
+ docker_key=/etc/apt/keyrings/docker.asc
164
+ if [ ! -f "$docker_key" ]; then
165
+ curl -fsSL --max-time 60 https://download.docker.com/linux/ubuntu/gpg -o "$work_dir/docker.asc"
166
+ docker_key=$work_dir/docker.asc
167
+ fi
168
+ [ "$(key_fingerprints "$docker_key")" = "$docker_key_fingerprint" ] || fail 'The Docker repository signing key has an unexpected fingerprint.' "Verify $docker_key against Docker's published key $docker_key_fingerprint before retrying."
169
+ if [ "$docker_key" != /etc/apt/keyrings/docker.asc ]; then
170
+ install_config "$docker_key" /etc/apt/keyrings/docker.asc 0644
171
+ fi
172
+ architecture=$(dpkg --print-architecture)
173
+ cat > "$work_dir/docker.sources" <<APT
174
+ Types: deb
175
+ URIs: https://download.docker.com/linux/ubuntu
176
+ Suites: noble
177
+ Components: stable
178
+ Architectures: $architecture
179
+ Signed-By: /etc/apt/keyrings/docker.asc
180
+ APT
181
+ install_config "$work_dir/docker.sources" /etc/apt/sources.list.d/docker.sources 0644
182
+ cat > "$work_dir/docker.preferences" <<'APT'
183
+ Package: docker-ce docker-ce-cli
184
+ Pin: version 5:29.*
185
+ Pin-Priority: 1001
186
+ APT
187
+ install_config "$work_dir/docker.preferences" /etc/apt/preferences.d/offbox-docker 0644
188
+ apt_get update
189
+ if [ -n "$installed_docker" ]; then
190
+ docker_version=$installed_docker
191
+ else
192
+ docker_version=$(apt-cache madison docker-ce | awk '$3 ~ /^5:29[.]/ {print $3; exit}')
193
+ fi
194
+ [ -n "$docker_version" ] || fail 'Docker major 29 is unavailable in the official repository.' 'Check Docker repository access and package availability.'
195
+ set -- "docker-ce=$docker_version" "docker-ce-cli=$docker_version"
196
+ for package in containerd.io docker-buildx-plugin docker-compose-plugin; do
197
+ if dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q '^install ok installed$'; then
198
+ set -- "$@" "$package=$(dpkg-query -W -f='${Version}' "$package")"
199
+ else
200
+ set -- "$@" "$package"
201
+ fi
202
+ done
203
+ apt_get install -y "$@"
204
+ usermod -aG docker offbox
205
+ systemctl enable --now docker
206
+ stage=network
207
+ if docker network inspect offbox >/dev/null 2>&1; then
208
+ subnet=$(docker network inspect --format '{{(index .IPAM.Config 0).Subnet}}' offbox)
209
+ [ "$subnet" = 10.89.0.0/16 ] || fail 'The existing offbox network has a different subnet.' 'Remove the conflicting offbox network after reviewing its attached containers.'
210
+ else
211
+ docker network create --label offbox.managed=1 --subnet 10.89.0.0/16 offbox
212
+ fi
213
+ install -d -m 0750 -o offbox -g offbox /srv/offbox/envs
214
+ if [ ! -e /var/lib/offbox/lock ]; then
215
+ touch /var/lib/offbox/lock
216
+ fi
217
+ chown offbox:offbox /var/lib/offbox/lock
218
+ chmod 0600 /var/lib/offbox/lock
219
+ stage=image
220
+ if ! docker pull -- "$image"; then
221
+ printf 'offbox: warning: image pull failed; offbox up will retry.\n' >&2
222
+ fi
223
+ if [ "$harden" = 1 ]; then
224
+ stage=updates
225
+ printf 'APT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' > "$work_dir/auto-upgrades"
226
+ install_config "$work_dir/auto-upgrades" /etc/apt/apt.conf.d/20auto-upgrades 0644
227
+ systemctl enable --now unattended-upgrades
228
+ fi
229
+ printf '%s\n' "$version" > /var/lib/offbox/version
230
+ touch /var/lib/offbox/ready
@@ -0,0 +1,101 @@
1
+ # syntax=docker/dockerfile:1
2
+ FROM ubuntu:24.04@sha256:68ee22004a9109b44973313b02ff7b1de250f1de0f5ecb628d73f579aaff33ac
3
+
4
+ ARG TARGETARCH
5
+ ARG NODE_VERSION=22.23.4
6
+ ARG NODE_SHA256=1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce
7
+ ARG GO_VERSION=1.27.3
8
+ ARG GO_SHA256=65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40
9
+ ARG UV_VERSION=0.13.5
10
+ ARG UV_SHA256=f68d291c75783122dad58ab77def0e13f3233f358731db5d6d7e8715603b928f
11
+ ARG BUN_VERSION=1.4.4
12
+ ARG BUN_SHA256=e0446eb55995772eb9894bebae9a6be368d985a8b9271ee124486f9946eedd76
13
+ ARG MISE_VERSION=2026.10.18
14
+ ARG MISE_SHA256=e97c38973336aa2ff350fea50f47b412b266fe263b102b7200ff1e9b118c6942
15
+ ARG RUSTUP_VERSION=1.29.2
16
+ ARG RUSTUP_SHA256=5be191e8c70a5007074254cb74d78dbfdd25cd5a61c27de7b8e5410122ec24f4
17
+ ARG RUST_VERSION=1.99.1
18
+ ARG PLAYWRIGHT_VERSION=1.64.1
19
+ ARG DOCKER_KEY_FINGERPRINT=9DC858229FC7DD38854AE2D88D81803C0EBFCD88
20
+
21
+ ENV LANG=en_US.UTF-8 \
22
+ LC_ALL=en_US.UTF-8 \
23
+ HOME=/home/dev \
24
+ CARGO_HOME=/home/dev/.cargo \
25
+ RUSTUP_HOME=/home/dev/.rustup \
26
+ GOPATH=/home/dev/go \
27
+ PLAYWRIGHT_BROWSERS_PATH=/home/dev/.cache/ms-playwright \
28
+ PATH=/home/dev/.local/share/mise/shims:/home/dev/.local/bin:/home/dev/.cargo/bin:/home/dev/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
29
+
30
+ RUN test "$TARGETARCH" = amd64 \
31
+ && if id ubuntu >/dev/null 2>&1; then userdel -r ubuntu; fi \
32
+ && groupadd --gid 1000 dev \
33
+ && useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash dev \
34
+ && apt-get update \
35
+ && DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends \
36
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
37
+ build-essential curl wget jq unzip xz-utils make cmake pkg-config ca-certificates git openssh-client \
38
+ sudo tini locales pipx procps util-linux iptables iproute2 kmod gpg \
39
+ postgresql-client redis-tools sqlite3 ffmpeg libpq-dev libssl-dev \
40
+ && locale-gen en_US.UTF-8 \
41
+ && printf 'dev ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dev \
42
+ && chmod 0440 /etc/sudoers.d/dev \
43
+ && install -m 0755 -d /etc/apt/keyrings \
44
+ && curl -fsSL --max-time 60 https://download.docker.com/linux/ubuntu/gpg -o /tmp/docker.asc \
45
+ && docker_key_fingerprints=$(GNUPGHOME=$(mktemp -d) gpg --batch --show-keys --with-colons /tmp/docker.asc | awk -F: '$1 == "pub" {primary = 1; next} primary && $1 == "fpr" {print $10; primary = 0}') \
46
+ && test "$docker_key_fingerprints" = "$DOCKER_KEY_FINGERPRINT" \
47
+ && install -m 0644 /tmp/docker.asc /etc/apt/keyrings/docker.asc \
48
+ && rm /tmp/docker.asc \
49
+ && printf 'Types: deb\nURIs: https://download.docker.com/linux/ubuntu\nSuites: noble\nComponents: stable\nArchitectures: amd64\nSigned-By: /etc/apt/keyrings/docker.asc\n' > /etc/apt/sources.list.d/docker.sources \
50
+ && printf 'Package: docker-ce docker-ce-cli\nPin: version 5:29.*\nPin-Priority: 1001\n' > /etc/apt/preferences.d/offbox-docker \
51
+ && apt-get update \
52
+ && docker_version=$(apt-cache madison docker-ce | awk '$3 ~ /^5:29[.]/ {print $3; exit}') \
53
+ && test -n "$docker_version" \
54
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
55
+ "docker-ce=$docker_version" "docker-ce-cli=$docker_version" containerd.io docker-buildx-plugin docker-compose-plugin \
56
+ && usermod -aG docker dev \
57
+ && rm -rf /var/lib/apt/lists/*
58
+
59
+ RUN curl -fsSL --retry 3 --max-time 120 "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz \
60
+ && printf '%s /tmp/node.tar.xz\n' "$NODE_SHA256" | sha256sum -c - \
61
+ && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \
62
+ && corepack enable \
63
+ && curl -fsSL --retry 3 --max-time 120 "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz \
64
+ && printf '%s /tmp/go.tar.gz\n' "$GO_SHA256" | sha256sum -c - \
65
+ && tar -xzf /tmp/go.tar.gz -C /usr/local \
66
+ && curl -fsSL --retry 3 --max-time 120 "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-x86_64-unknown-linux-gnu.tar.gz" -o /tmp/uv.tar.gz \
67
+ && printf '%s /tmp/uv.tar.gz\n' "$UV_SHA256" | sha256sum -c - \
68
+ && tar -xzf /tmp/uv.tar.gz -C /tmp \
69
+ && install -m 0755 /tmp/uv-x86_64-unknown-linux-gnu/uv /tmp/uv-x86_64-unknown-linux-gnu/uvx /usr/local/bin/ \
70
+ && curl -fsSL --retry 3 --max-time 120 "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64-baseline.zip" -o /tmp/bun.zip \
71
+ && printf '%s /tmp/bun.zip\n' "$BUN_SHA256" | sha256sum -c - \
72
+ && unzip -q /tmp/bun.zip -d /tmp \
73
+ && install -m 0755 /tmp/bun-linux-x64-baseline/bun /usr/local/bin/bun \
74
+ && curl -fsSL --retry 3 --max-time 120 "https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/mise-v${MISE_VERSION}-linux-x64" -o /tmp/mise \
75
+ && printf '%s /tmp/mise\n' "$MISE_SHA256" | sha256sum -c - \
76
+ && install -m 0755 /tmp/mise /usr/local/bin/mise \
77
+ && npx -y "playwright@${PLAYWRIGHT_VERSION}" install-deps chromium \
78
+ && rm -rf /tmp/node.tar.xz /tmp/go.tar.gz /tmp/uv.tar.gz /tmp/uv-x86_64-unknown-linux-gnu /tmp/bun.zip /tmp/bun-linux-x64-baseline /tmp/mise /home/dev/.npm /var/lib/apt/lists/* \
79
+ && chown -R dev:dev /home/dev
80
+
81
+ USER dev
82
+ RUN uv python install 3.13 --default \
83
+ && uv tool install poetry \
84
+ && curl -fsSL --retry 3 --max-time 120 "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/x86_64-unknown-linux-gnu/rustup-init" -o /tmp/rustup-init \
85
+ && printf '%s /tmp/rustup-init\n' "$RUSTUP_SHA256" | sha256sum -c - \
86
+ && chmod 0755 /tmp/rustup-init \
87
+ && /tmp/rustup-init -y --no-modify-path --profile minimal --default-toolchain "$RUST_VERSION" \
88
+ && rm /tmp/rustup-init \
89
+ && uv cache clean \
90
+ && printf '\neval "$(mise activate bash)"\n' >> /home/dev/.bashrc
91
+
92
+ USER root
93
+ COPY --chmod=0755 images/universal/offbox-init images/universal/offbox-run /usr/local/bin/
94
+ RUN install -d -m 0755 /run/offbox \
95
+ && install -d -m 0700 -o dev -g dev /run/offbox/exec \
96
+ && install -d -m 0755 -o dev -g dev /work /home/dev/.cache/ms-playwright \
97
+ && chown -R dev:dev /home/dev/.cache \
98
+ && printf 'export PATH="/home/dev/.local/share/mise/shims:/home/dev/.local/bin:/home/dev/.cargo/bin:/home/dev/go/bin:/usr/local/go/bin:$PATH"\n' > /etc/profile.d/offbox-path.sh
99
+
100
+ WORKDIR /work
101
+ ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/offbox-init"]
@@ -0,0 +1,75 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ fail() {
5
+ printf 'offbox: %s\n Fix: Check the privileged container settings and dockerd log.\n' "$1" >&2
6
+ if [ -f /var/log/dockerd.log ]; then
7
+ tail -n 40 /var/log/dockerd.log >&2
8
+ fi
9
+ exit 1
10
+ }
11
+
12
+ # Invoked by TERM/INT traps while waiting for dockerd.
13
+ # shellcheck disable=SC2317,SC2329
14
+ stop_daemon() {
15
+ trap '' TERM INT
16
+ rm -f /run/offbox/ready
17
+ if [ -n "$daemon_pid" ]; then
18
+ kill -TERM "$daemon_pid" 2>/dev/null || :
19
+ wait "$daemon_pid" || :
20
+ fi
21
+ exit 0
22
+ }
23
+
24
+ daemon_pid=
25
+ trap 'stop_daemon' TERM INT
26
+
27
+ install -d -m 0755 /etc/docker /run/offbox
28
+ install -d -m 0700 -o dev -g dev /run/offbox/exec
29
+ rm -f /run/offbox/ready /var/run/docker.pid /var/run/docker.sock
30
+ rm -rf /var/run/docker
31
+ cat > /etc/docker/daemon.json <<'JSON'
32
+ {"data-root":"/var/lib/docker","storage-driver":"overlay2","bip":"172.31.0.1/24","default-address-pools":[{"base":"172.30.0.0/16","size":24}],"log-driver":"local"}
33
+ JSON
34
+
35
+ if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
36
+ mkdir -p /sys/fs/cgroup/init || fail 'Cannot create the nested cgroup.'
37
+ processes=$(cat /sys/fs/cgroup/cgroup.procs)
38
+ for pid in $processes; do
39
+ if ! printf '%s\n' "$pid" > /sys/fs/cgroup/init/cgroup.procs; then
40
+ [ ! -e "/proc/$pid" ] || fail 'Cannot move processes into the nested cgroup.'
41
+ fi
42
+ done
43
+ controllers=$(cat /sys/fs/cgroup/cgroup.controllers)
44
+ for controller in $controllers; do
45
+ case "$controller" in
46
+ cpu|memory|pids|io)
47
+ printf '+%s\n' "$controller" > /sys/fs/cgroup/cgroup.subtree_control || fail 'Cannot enable nested cgroup controllers.'
48
+ ;;
49
+ esac
50
+ done
51
+ fi
52
+
53
+ log_fifo=/run/offbox/dockerd.log.fifo
54
+ rm -f "$log_fifo"
55
+ mkfifo -m 0600 "$log_fifo"
56
+ awk -v log_file=/var/log/dockerd.log -v limit=10485760 '
57
+ { print > log_file; fflush(log_file); size += length($0) + 1 }
58
+ size >= limit { close(log_file); system("mv -f " log_file " " log_file ".1"); size = 0 }
59
+ ' < "$log_fifo" &
60
+ dockerd --host=unix:///var/run/docker.sock > "$log_fifo" 2>&1 &
61
+ daemon_pid=$!
62
+ deadline=$(($(date +%s) + 60))
63
+ while [ "$(date +%s)" -lt "$deadline" ]; do
64
+ kill -0 "$daemon_pid" 2>/dev/null || fail 'The nested Docker daemon exited during startup.'
65
+ if [ -S /var/run/docker.sock ] && timeout 1 docker --host=unix:///var/run/docker.sock info >/dev/null 2>&1; then
66
+ touch /run/offbox/ready
67
+ set +e
68
+ wait "$daemon_pid"
69
+ status=$?
70
+ rm -f /run/offbox/ready
71
+ exit "$status"
72
+ fi
73
+ sleep 1
74
+ done
75
+ fail 'The nested Docker daemon was not ready within 60 seconds.'
@@ -0,0 +1,131 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ umask 077
4
+
5
+ fail() {
6
+ printf 'offbox-run: %s\n Fix: %s\n' "$1" "$2" >&2
7
+ exit 2
8
+ }
9
+
10
+ validate_id() {
11
+ case "$1" in ''|*[!A-Za-z0-9_-]*) fail 'Invalid run ID.' 'Use a unique alphanumeric run ID without path separators.' ;; esac
12
+ [ "${#1}" -le 64 ] || fail 'Run ID is too long.' 'Use at most 64 characters.'
13
+ }
14
+
15
+ process_info() {
16
+ if [ -r "/proc/$1/stat" ]; then
17
+ sed 's/^.*) //' "/proc/$1/stat" 2>/dev/null | awk '{print $3, $20}'
18
+ fi
19
+ }
20
+
21
+ state_dir=/run/offbox/exec
22
+ mkdir -p "$state_dir"
23
+ if [ "${1:-}" = --signal ]; then
24
+ [ "$#" -eq 3 ] || fail 'Expected --signal SIGNAL ID.' 'Pass INT, TERM or KILL and the recorded run ID.'
25
+ signal=$2
26
+ run_id=$3
27
+ case "$signal" in INT|TERM|KILL) ;; *) fail 'Unsupported signal.' 'Use INT, TERM or KILL.' ;; esac
28
+ validate_id "$run_id"
29
+ [ ! -f "$state_dir/$run_id.code" ] || exit 0
30
+ [ ! -f "$state_dir/$run_id.tty" ] || fail 'External group signaling is unavailable for TTY runs.' 'Use terminal controls for a TTY command.'
31
+ if [ ! -f "$state_dir/$run_id.pid" ] || [ ! -f "$state_dir/$run_id.start" ]; then
32
+ fail 'Run metadata is unavailable.' 'Check the run status before retrying.'
33
+ fi
34
+ pid=$(cat "$state_dir/$run_id.pid")
35
+ started=$(cat "$state_dir/$run_id.start")
36
+ case "$pid:$started" in *[!0-9:]*|:*|*:) fail 'Invalid process metadata.' 'Check the run status before retrying.' ;; esac
37
+ [ "$pid" -gt 1 ] || fail 'Unsafe process group.' 'Check the run status before retrying.'
38
+ actual=$(process_info "$pid")
39
+ [ "$actual" = "$pid $started" ] || fail 'Run process group no longer matches its recorded identity.' 'Check the run status; do not signal a reused PID.'
40
+ /bin/kill -"$signal" -- "-$pid"
41
+ exit 0
42
+ fi
43
+
44
+ tty=0
45
+ if [ "${1:-}" = --tty ]; then
46
+ tty=1
47
+ shift
48
+ fi
49
+ [ "$#" -ge 4 ] || fail 'Expected ID CWD -- COMMAND.' 'Pass the run ID, absolute work directory and command argv.'
50
+ run_id=$1
51
+ directory=$2
52
+ shift 2
53
+ [ "$1" = -- ] || fail 'Missing command separator.' 'Put -- before the command argv.'
54
+ shift
55
+ validate_id "$run_id"
56
+ case "$directory" in /*) ;; *) fail 'Work directory must be absolute.' 'Use the mapped /work directory.' ;; esac
57
+ if [ -e "$state_dir/$run_id.pid" ] || [ -e "$state_dir/$run_id.code" ]; then
58
+ fail 'Run ID already exists.' 'Use a new run ID.'
59
+ fi
60
+ find "$state_dir" -maxdepth 1 -type f -mmin +1440 -print | while IFS= read -r expired; do
61
+ expired_name=${expired##*/}
62
+ case "$expired_name" in
63
+ *.code.tmp) expired_id=${expired_name%.code.tmp} ;;
64
+ *.code|*.pid|*.start|*.tty) expired_id=${expired_name%.*} ;;
65
+ *) continue ;;
66
+ esac
67
+ case "$expired_id" in ''|*[!A-Za-z0-9_-]*) continue ;; esac
68
+ if [ -f "$state_dir/$expired_id.code" ] && [ "$expired_name" != "$expired_id.code" ]; then
69
+ continue
70
+ fi
71
+ expired_pid=$(cat "$state_dir/$expired_id.pid" 2>/dev/null || :)
72
+ expired_start=$(cat "$state_dir/$expired_id.start" 2>/dev/null || :)
73
+ case "$expired_pid:$expired_start" in
74
+ *[!0-9:]*|:*|*:) ;;
75
+ *)
76
+ expired_identity=$(process_info "$expired_pid")
77
+ if [ ! -f "$state_dir/$expired_id.code" ] && [ -n "$expired_identity" ] && [ "${expired_identity##* }" = "$expired_start" ]; then
78
+ continue
79
+ fi
80
+ ;;
81
+ esac
82
+ rm -f "$state_dir/$expired_id.code" "$state_dir/$expired_id.pid" "$state_dir/$expired_id.start" "$state_dir/$expired_id.tty" "$state_dir/$expired_id.code.tmp"
83
+ done
84
+ cd "$directory" || fail 'Cannot enter the work directory.' 'Flush sync and check that the remote directory exists.'
85
+ child_pid=
86
+ pending_signal=
87
+ forward_signal() {
88
+ pending_signal=$1
89
+ if [ -n "$child_pid" ]; then
90
+ if [ "$tty" = 1 ]; then
91
+ [ "$1" != INT ] || return 0
92
+ /bin/kill -s "$1" "$child_pid" 2>/dev/null || :
93
+ else
94
+ /bin/kill -"$1" -- "-$child_pid" 2>/dev/null || /bin/kill -s "$1" "$child_pid" 2>/dev/null || :
95
+ fi
96
+ fi
97
+ }
98
+ trap 'forward_signal INT' INT
99
+ trap 'forward_signal TERM' TERM
100
+ trap 'forward_signal HUP' HUP
101
+ exec 3<&0
102
+ if [ "$tty" = 1 ]; then
103
+ touch "$state_dir/$run_id.tty"
104
+ fi
105
+ umask 022
106
+ if [ "$tty" = 1 ]; then
107
+ env --default-signal=INT,TERM,HUP -- "$@" <&3 3<&- &
108
+ else
109
+ setsid env --default-signal=INT,TERM,HUP -- "$@" <&3 3<&- &
110
+ fi
111
+ child_pid=$!
112
+ umask 077
113
+ exec 3<&-
114
+ identity=$(process_info "$child_pid")
115
+ printf '%s\n' "${identity##* }" > "$state_dir/$run_id.start"
116
+ printf '%s\n' "$child_pid" > "$state_dir/$run_id.pid"
117
+ if [ -n "$pending_signal" ]; then
118
+ forward_signal "$pending_signal"
119
+ fi
120
+ while :; do
121
+ if wait "$child_pid"; then
122
+ status=0
123
+ else
124
+ status=$?
125
+ fi
126
+ kill -0 "$child_pid" 2>/dev/null || break
127
+ done
128
+ child_pid=
129
+ printf '%s\n' "$status" > "$state_dir/$run_id.code.tmp"
130
+ mv "$state_dir/$run_id.code.tmp" "$state_dir/$run_id.code"
131
+ exit "$status"
@@ -0,0 +1,52 @@
1
+ # Provider checksums for Terraform (registry.terraform.io) and OpenTofu (registry.opentofu.org).
2
+ # Each tool verifies its own registry entry; init drops the other entry from the host workspace copy.
3
+ # Regenerate both with "providers lock" for linux_amd64, linux_arm64, darwin_amd64 and darwin_arm64.
4
+
5
+ provider "registry.terraform.io/hashicorp/google" {
6
+ version = "8.7.0"
7
+ constraints = ">= 8.7.0, < 8.8.0"
8
+ hashes = [
9
+ "h1:cIHzuLy+Umw4IzKl9oZPmPGfiBgvhSjZRodgPtF7xtM=",
10
+ "h1:jTjjmmzCvAvLSD9MErHVepggzn0/OXoETsmLPNqQrGc=",
11
+ "h1:r0evOKSNhYYj04qb6VCTVtm7/uWr67jMZ2TsgLubk6o=",
12
+ "h1:vCPO1IbM2/WfU4R3QYtzxIyM5ZLYL0pgDS8bmap20hI=",
13
+ "zh:087771b1c22126084ec1ac26272f7e0bce1030bedd9d692b6eef38e6baf5db2e",
14
+ "zh:4e075872421717124f1a059f80a9cd2a742e868e94ee74546e52494a02f656d2",
15
+ "zh:5024e53d378a551d354244db25085c2db530dfdd7bdbd3a3df03abdc957a8a20",
16
+ "zh:52dd649ad0c39f51bf39218e896ced370d4c56d65876715b6c12e2ac17f16f71",
17
+ "zh:70d6c8f5371a354f8265fe4f2c279e0c2f6a06054538dbe2d92e428692192439",
18
+ "zh:7aceb5fe517d2dbd270644663a2b237fc3be3e06577a40d2009983b5a5088fcd",
19
+ "zh:bb3a6fd8f472080f3199e57e61cbbdd253c8210b2b70e6c4e30e06b08c39e7de",
20
+ "zh:cbd1a1de219397ba98f17203751300433766f5c36953e6aece4a1fb1817638a9",
21
+ "zh:d12886e54a30e3b6f174f879777616401f8749466443bc6db870eddd42985fa7",
22
+ "zh:d58a84d3a387c78e8ac58b200ceb51a3e332105a11420f33e03aa69b3ec4449f",
23
+ "zh:ed11511bc8ee66fbe2d0e6b59f694c127f140cba7ea9654e332c3ebfbdf8bea9",
24
+ "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
25
+ ]
26
+ }
27
+
28
+ provider "registry.opentofu.org/hashicorp/google" {
29
+ version = "8.7.0"
30
+ constraints = ">= 8.7.0, < 8.8.0"
31
+ hashes = [
32
+ "h1:4f5uR7DRaSDcWTCnI8ESIrjLcWBP6uvveOgb/fdaUq8=",
33
+ "h1:OTU1cJ4k7eTOT3gXrHqfUKhnr/Tev4kfK3g0s//VhEc=",
34
+ "h1:WppZq4GTyQzA/OukXL1hxZmzGmvdwfIXPNXQ2j5KeIc=",
35
+ "h1:dIPWOtPEfzBV/4jfx2DxCHkco5MTHuummMIiryZKSZY=",
36
+ "zh:008989fe85359406fc8360d22d2b16ed489af46d188c9977240af7728dd213e0",
37
+ "zh:2517a434f1ba090f3acdb8e4233f7e25c8356e9fb1db3b783aef72a3de8fb3d7",
38
+ "zh:5e966672da5706954d28a7827301bb6d10a571c008ff2053fb2ac1da6f9c4b64",
39
+ "zh:5f1a7c5092cb0181e0f8b4ff8292811ac81f4ea653fd0d76c8ae716fa86e4cfc",
40
+ "zh:9566f745e3cea3fa80f41c9e4c925246362f06a9d57594e0e9898b3f9f371ec2",
41
+ "zh:964bd33553e43dbd119000bc0e72aae9c5e962d9a5da56691bdb40540df1e555",
42
+ "zh:96f862ceba3417764985b2aee67ca70bc44ee4c423139ee804e47b0a7c34d877",
43
+ "zh:a4b8c18426ca672d3928ae7693810afd40805d77d79c7c297d8f7a3ece9236a3",
44
+ "zh:b76b615ca838bb3f108339d97772ca0cf071b5e2be072ec8ee78f092bab0d65d",
45
+ "zh:c10933f0e23728a1998583a30d0ccd9c4a09be93230c396a40a7b0bbc6b89e59",
46
+ "zh:d90496a56ccd9a8a926f8b83bfb27a334efbc98813090898e1ee87bae416e03e",
47
+ "zh:dbe5e8a6db8dd5c2ec1f15be6648860dd543898eff94d4228bc2929725d7697f",
48
+ "zh:e4a31b8c8f41530608722b6752a474561b596171ac7828227c99c67cd596764d",
49
+ "zh:e754176ae7dacfc977393e977e874ed875578271ba6ac47c2e91b9bac07b90b2",
50
+ "zh:ea30e4a294b6a52641406b2bb49d847f2e253833c2fb425de2e1fd10a532c893",
51
+ ]
52
+ }