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.
- offbox/__init__.py +2 -0
- offbox/__main__.py +4 -0
- offbox/_data/host/cloud-init.yaml.j2 +15 -0
- offbox/_data/host/host-init.sh +230 -0
- offbox/_data/images/universal/Dockerfile +101 -0
- offbox/_data/images/universal/offbox-init +75 -0
- offbox/_data/images/universal/offbox-run +131 -0
- offbox/_data/providers/gcp/.terraform.lock.hcl +52 -0
- offbox/_data/providers/gcp/main.tf +67 -0
- offbox/_data/providers/gcp/outputs.tf +7 -0
- offbox/_data/providers/gcp/provider.toml +9 -0
- offbox/_data/providers/gcp/variables.tf +33 -0
- offbox/_data/providers/gcp/versions.tf +12 -0
- offbox/_data/providers/hetzner/.terraform.lock.hcl +51 -0
- offbox/_data/providers/hetzner/main.tf +32 -0
- offbox/_data/providers/hetzner/outputs.tf +7 -0
- offbox/_data/providers/hetzner/provider.toml +9 -0
- offbox/_data/providers/hetzner/variables.tf +28 -0
- offbox/_data/providers/hetzner/versions.tf +12 -0
- offbox/_data/providers/ssh/provider.toml +4 -0
- offbox/cli/__init__.py +117 -0
- offbox/cli/config.py +39 -0
- offbox/cli/env.py +109 -0
- offbox/cli/exec.py +50 -0
- offbox/cli/host.py +313 -0
- offbox/cli/misc.py +132 -0
- offbox/cli/ports.py +44 -0
- offbox/config.py +189 -0
- offbox/console.py +63 -0
- offbox/doctor.py +174 -0
- offbox/drivers/__init__.py +0 -0
- offbox/drivers/base.py +40 -0
- offbox/drivers/none.py +114 -0
- offbox/drivers/tofu.py +59 -0
- offbox/errors.py +45 -0
- offbox/execute.py +772 -0
- offbox/host/__init__.py +89 -0
- offbox/host/activity.py +58 -0
- offbox/host/auto.py +322 -0
- offbox/host/cloudinit.py +44 -0
- offbox/host/create.py +198 -0
- offbox/host/destroy.py +151 -0
- offbox/host/init.py +196 -0
- offbox/host/ready.py +106 -0
- offbox/host/timer.py +227 -0
- offbox/host/usage.py +123 -0
- offbox/host/warnings.py +61 -0
- offbox/locking.py +27 -0
- offbox/logging.py +154 -0
- offbox/logs.py +135 -0
- offbox/naming.py +67 -0
- offbox/paths.py +55 -0
- offbox/ports.py +610 -0
- offbox/project.py +187 -0
- offbox/providers.py +117 -0
- offbox/py.typed +0 -0
- offbox/runner.py +291 -0
- offbox/runtime.py +887 -0
- offbox/setup.py +181 -0
- offbox/share.py +286 -0
- offbox/ssh.py +179 -0
- offbox/state.py +157 -0
- offbox/status.py +378 -0
- offbox/sync.py +380 -0
- offbox/timings.py +69 -0
- offbox/tools.py +227 -0
- offbox/uninstall.py +114 -0
- offbox-0.1.0.dist-info/METADATA +140 -0
- offbox-0.1.0.dist-info/RECORD +72 -0
- offbox-0.1.0.dist-info/WHEEL +4 -0
- offbox-0.1.0.dist-info/entry_points.txt +2 -0
- offbox-0.1.0.dist-info/licenses/LICENSE +21 -0
offbox/__init__.py
ADDED
offbox/__main__.py
ADDED
|
@@ -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
|
+
}
|