-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·839 lines (739 loc) · 31.2 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·839 lines (739 loc) · 31.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
REPO="mensfeld/code-on-incus"
BINARY_NAME="coi"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
VERSION="${VERSION:-latest}"
# Detect non-interactive mode (CI, explicit opt-in, or no usable controlling terminal)
if [ "${NONINTERACTIVE:-0}" = "1" ] || [ "${CI:-}" = "true" ] || ! { true </dev/tty; } 2>/dev/null; then
NONINTERACTIVE=1
else
NONINTERACTIVE=0
fi
# Prompt user for yes/no confirmation.
# In non-interactive mode (curl|bash, CI), exits with error since we can't ask.
# In interactive mode, reads from /dev/tty so it works even when script is piped.
prompt_continue() {
local message="${1:-Continue anyway?}"
if [ "$NONINTERACTIVE" = "1" ]; then
echo -e "${YELLOW}⚠ Non-interactive mode: cannot prompt. Aborting.${NC}"
echo " Re-run the script directly (not piped) or fix the issue above."
exit 1
fi
read -p "$message [y/N] " -n 1 -r </dev/tty
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
}
# Prompt user for a choice (single character).
# In non-interactive mode, returns the default value.
# In interactive mode, reads from /dev/tty.
prompt_choice() {
local message="$1"
local default="$2"
if [ "$NONINTERACTIVE" = "1" ]; then
echo -e "${BLUE}→ Non-interactive mode: using default ($default)${NC}"
REPLY="$default"
return
fi
read -p "$message" -n 1 -r </dev/tty
echo ""
if [ -z "$REPLY" ]; then
REPLY="$default"
fi
}
# Detect package manager
detect_pkg_manager() {
if command -v apt-get &> /dev/null; then
PKG_MANAGER="apt"
elif command -v pacman &> /dev/null; then
PKG_MANAGER="pacman"
elif command -v dnf &> /dev/null; then
PKG_MANAGER="dnf"
elif command -v zypper &> /dev/null; then
PKG_MANAGER="zypper"
else
PKG_MANAGER="unknown"
fi
}
# Install a package using the detected package manager
# Usage: pkg_install <apt-name> [pacman-name] [dnf-name] [zypper-name]
# If a distro-specific name is omitted, the apt name is used as fallback.
pkg_install() {
local apt_name="$1"
local pacman_name="${2:-$apt_name}"
local dnf_name="${3:-$apt_name}"
local zypper_name="${4:-$apt_name}"
case "$PKG_MANAGER" in
apt) sudo apt-get install -y "$apt_name" ;;
pacman) sudo pacman -S --noconfirm "$pacman_name" ;;
dnf) sudo dnf install -y "$dnf_name" ;;
zypper) sudo zypper install -y "$zypper_name" ;;
*)
echo -e "${RED}✗ Unknown package manager — install '$apt_name' manually${NC}"
return 1
;;
esac
}
# Detect OS and architecture
detect_platform() {
local os
local arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux*)
OS="linux"
;;
*)
echo -e "${RED}✗ Unsupported OS: $os${NC}"
echo " code-on-incus requires Linux (Incus is Linux-only)"
echo " On macOS: Run inside a Colima or Lima VM"
echo " See: https://github.com/mensfeld/code-on-incus/wiki/macOS-Setup-Guide"
exit 1
;;
esac
case "$arch" in
x86_64|amd64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
echo -e "${RED}✗ Unsupported architecture: $arch${NC}"
exit 1
;;
esac
echo -e "${BLUE}→ Detected platform: ${OS}/${ARCH}${NC}"
}
# Check if Incus is installed
check_incus() {
echo -e "${BLUE}→ Checking Incus installation...${NC}"
if ! command -v incus &> /dev/null; then
echo -e "${YELLOW}⚠ Incus not found${NC}"
echo ""
echo " code-on-incus requires Incus to be installed."
echo " Install Incus: https://linuxcontainers.org/incus/docs/main/installing/"
echo ""
echo " Quick install examples:"
echo " Ubuntu/Debian: sudo apt install -y incus"
echo " Arch Linux: sudo pacman -S incus"
echo " Fedora: sudo dnf install incus"
echo ""
echo " Then: sudo incus admin init --auto"
echo " sudo usermod -aG incus-admin \$USER"
echo ""
prompt_continue "Continue installation anyway?"
else
local incus_version_output
incus_version_output="$(incus version 2>/dev/null)"
echo -e "${GREEN}✓ Incus found: ${incus_version_output}${NC}"
# Check minimum version (>= 6.1)
local server_version
server_version="$(echo "$incus_version_output" | grep '^Server version:' | cut -d: -f2 | tr -d ' ')"
if [ -z "$server_version" ]; then
# Fallback: single-line output (older Incus)
server_version="$(echo "$incus_version_output" | head -n1 | tr -d ' ')"
fi
if [ -n "$server_version" ]; then
local ver_major ver_minor
ver_major="$(echo "$server_version" | cut -d. -f1)"
ver_minor="$(echo "$server_version" | cut -d. -f2)"
if [ -n "$ver_major" ] && [ -n "$ver_minor" ]; then
if [ "$ver_major" -lt 6 ] || { [ "$ver_major" -eq 6 ] && [ "$ver_minor" -lt 1 ]; }; then
echo ""
echo -e "${YELLOW}⚠ Incus version ${server_version} is below the minimum required 6.1${NC}"
echo ""
echo " Ubuntu ships Incus 6.0.x which lacks required idmapping support."
echo " You may see errors like:"
echo " 'Failed to setup device mount: idmapping abilities are required'"
echo ""
echo " Please install Incus >= 6.1 from the Zabbly repository:"
echo " https://github.com/zabbly/incus"
echo ""
prompt_continue "Continue installation anyway?"
fi
fi
fi
fi
}
# Check if user is in incus-admin group
check_group() {
if groups | grep -q incus-admin; then
echo -e "${GREEN}✓ User is in incus-admin group${NC}"
else
echo -e "${YELLOW}⚠ User is not in incus-admin group${NC}"
echo ""
echo " You need to be in the incus-admin group to use code-on-incus."
echo " Run: sudo usermod -aG incus-admin \$USER"
echo " Then log out and back in for changes to take effect."
echo ""
fi
}
# Set up passwordless sudo for nft (required for network isolation)
setup_nft_sudoers() {
local nft_path
nft_path="$(command -v nft 2>/dev/null)"
if [ -z "$nft_path" ]; then
return
fi
# Already configured? Check for the sudoers drop-in directly so we don't
# get a false positive from a cached sudo timestamp.
if [ -f /etc/sudoers.d/coi-nft ]; then
echo -e "${GREEN}✓ Passwordless sudo for nft already configured${NC}"
return
fi
echo -e "${BLUE}→ Configuring passwordless sudo for nft...${NC}"
echo "$USER ALL=(ALL) NOPASSWD: $nft_path" | sudo tee /etc/sudoers.d/coi-nft > /dev/null
sudo chmod 0440 /etc/sudoers.d/coi-nft
echo -e "${GREEN}✓ Passwordless sudo configured for nft${NC}"
}
# Check nftables availability for network isolation
check_nft() {
echo -e "${BLUE}→ Checking nftables (for network isolation)...${NC}"
if ! command -v nft &> /dev/null; then
echo -e "${YELLOW}⚠ nft not found${NC}"
echo ""
echo " Network isolation (restricted/allowlist modes) requires nftables."
echo " Without it, you can still use open mode."
echo ""
if [ "$NONINTERACTIVE" = "1" ]; then
echo -e "${BLUE}→ Non-interactive mode: installing nftables...${NC}"
pkg_install nftables
else
read -p " Install nftables now? [Y/n] " -n 1 -r </dev/tty
echo ""
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
echo -e "${BLUE}→ Installing nftables...${NC}"
pkg_install nftables
fi
fi
else
echo -e "${GREEN}✓ nft is installed${NC}"
fi
setup_nft_sudoers
}
# Copy a built/downloaded binary into INSTALL_DIR (sudo only when needed) and
# remove any leftover legacy claude-on-incus symlink from pre-0.10 installs —
# 0.10 retired the name, so upgrades should not keep the alias alive.
install_binary() {
local src="$1"
if [ -w "$INSTALL_DIR" ]; then
cp "$src" "${INSTALL_DIR}/${BINARY_NAME}"
rm -f "${INSTALL_DIR}/claude-on-incus"
else
sudo cp "$src" "${INSTALL_DIR}/${BINARY_NAME}"
sudo rm -f "${INSTALL_DIR}/claude-on-incus"
fi
}
# Download binary from GitHub releases
download_binary() {
local download_url
local tmp_dir
local binary_path
echo -e "${BLUE}→ Downloading code-on-incus...${NC}"
tmp_dir="$(mktemp -d)"
trap "rm -rf '$tmp_dir'" EXIT
if [ "$VERSION" = "latest" ]; then
download_url="https://github.com/${REPO}/releases/latest/download/coi-${OS}-${ARCH}"
else
download_url="https://github.com/${REPO}/releases/download/${VERSION}/coi-${OS}-${ARCH}"
fi
binary_path="${tmp_dir}/${BINARY_NAME}"
if command -v curl &> /dev/null; then
curl -fsSL "$download_url" -o "$binary_path"
elif command -v wget &> /dev/null; then
wget -q -O "$binary_path" "$download_url"
else
echo -e "${RED}✗ Neither curl nor wget found${NC}"
echo " Please install curl or wget and try again."
exit 1
fi
chmod +x "$binary_path"
# Install to system
echo -e "${BLUE}→ Installing to ${INSTALL_DIR}...${NC}"
install_binary "$binary_path"
echo -e "${GREEN}✓ Installed to ${INSTALL_DIR}/${BINARY_NAME}${NC}"
# Grant immutable-attribute capability for host-side protected path hardening
grant_immutable_capability
}
# Build from source
build_from_source() {
local tmp_dir
echo -e "${BLUE}→ Building from source...${NC}"
# Check for Go
if ! command -v go &> /dev/null; then
echo -e "${RED}✗ Go not found${NC}"
echo " Install Go: https://go.dev/doc/install"
exit 1
fi
echo -e "${BLUE}→ Go version: $(go version)${NC}"
tmp_dir="$(mktemp -d)"
trap "rm -rf '$tmp_dir'" EXIT
# Clone repository
echo -e "${BLUE}→ Cloning repository...${NC}"
git clone --depth 1 "https://github.com/${REPO}.git" "$tmp_dir"
# Build (as the current user — never under sudo, because sudo strips PATH
# and user-scoped Go toolchains like mise/asdf/$HOME/go/bin would disappear).
cd "$tmp_dir"
echo -e "${BLUE}→ Building binary...${NC}"
make build
# Install the freshly-built binary. We intentionally do NOT run
# `sudo make install` here: that would re-invoke the `build` prerequisite
# under sudo, strip PATH, and crash on systems where Go is user-scoped.
# Mirror download_binary and copy the binary directly, only sudo-ing when
# $INSTALL_DIR is not writable.
echo -e "${BLUE}→ Installing to ${INSTALL_DIR}...${NC}"
local built_binary="${tmp_dir}/${BINARY_NAME}"
install_binary "$built_binary"
echo -e "${GREEN}✓ Built and installed${NC}"
# Grant immutable-attribute capability for host-side protected path hardening
grant_immutable_capability
}
# Grant CAP_LINUX_IMMUTABLE on the installed binary so COI can apply
# chattr +i on host-side protected paths (defense-in-depth against
# unshare+umount bypass of read-only bind mounts).
grant_immutable_capability() {
if command -v setcap &> /dev/null; then
if sudo setcap cap_linux_immutable=ep "${INSTALL_DIR}/${BINARY_NAME}" 2>/dev/null; then
echo -e "${GREEN}✓ Granted cap_linux_immutable capability (host-side path protection)${NC}"
else
echo -e "${YELLOW}⚠ Could not set cap_linux_immutable on binary${NC}"
echo " Host-side immutable protection will be unavailable."
echo " To enable: sudo setcap cap_linux_immutable=ep ${INSTALL_DIR}/${BINARY_NAME}"
fi
else
echo -e "${YELLOW}⚠ setcap not found (install libcap2-bin)${NC}"
echo " Host-side immutable protection will be unavailable."
fi
}
# Ensure the Incus systemd service is enabled and running
ensure_incus_service() {
if ! command -v incus &> /dev/null; then
return
fi
# Check if incus service (or socket) is active
if systemctl is-active --quiet incus.service 2>/dev/null || systemctl is-active --quiet incus.socket 2>/dev/null; then
echo -e "${GREEN}✓ Incus service is running${NC}"
return
fi
echo -e "${BLUE}→ Enabling and starting Incus service...${NC}"
if sudo systemctl enable --now incus.service 2>/dev/null; then
echo -e "${GREEN}✓ Incus service enabled and started${NC}"
elif sudo systemctl enable --now incus.socket 2>/dev/null; then
echo -e "${GREEN}✓ Incus socket enabled and started${NC}"
else
echo -e "${YELLOW}⚠ Could not start Incus service${NC}"
echo " Try manually: sudo systemctl enable --now incus.service"
fi
}
# Ensure subordinate UID/GID ranges are configured for unprivileged containers
ensure_idmap() {
if ! command -v incus &> /dev/null; then
return
fi
echo -e "${BLUE}→ Checking subordinate UID/GID mapping...${NC}"
local needs_fix=0
# Check if root has a subuid range with at least 65536 UIDs
if [ -f /etc/subuid ] && grep -qE '^root:[0-9]+:[0-9]{5,}' /etc/subuid; then
echo -e "${GREEN}✓ /etc/subuid has root mapping${NC}"
else
echo -e "${YELLOW}⚠ /etc/subuid missing root subordinate range${NC}"
needs_fix=1
fi
if [ -f /etc/subgid ] && grep -qE '^root:[0-9]+:[0-9]{5,}' /etc/subgid; then
echo -e "${GREEN}✓ /etc/subgid has root mapping${NC}"
else
echo -e "${YELLOW}⚠ /etc/subgid missing root subordinate range${NC}"
needs_fix=1
fi
if [ "$needs_fix" = "0" ]; then
return
fi
echo ""
echo " Incus needs subordinate UID/GID ranges for unprivileged containers."
echo " Without this, container launches will fail with:"
echo " \"System doesn't have a functional idmap setup\""
echo ""
if [ "$NONINTERACTIVE" = "1" ]; then
echo -e "${BLUE}→ Non-interactive mode: configuring idmap...${NC}"
else
read -p " Configure subordinate UID/GID ranges now? [Y/n] " -n 1 -r </dev/tty
echo ""
if [[ $REPLY =~ ^[Nn]$ ]]; then
return
fi
fi
if ! grep -qE '^root:[0-9]+:[0-9]{5,}' /etc/subuid 2>/dev/null; then
echo "root:1000000:1000000000" | sudo tee -a /etc/subuid > /dev/null
echo -e "${GREEN}✓ Added root range to /etc/subuid${NC}"
fi
if ! grep -qE '^root:[0-9]+:[0-9]{5,}' /etc/subgid 2>/dev/null; then
echo "root:1000000:1000000000" | sudo tee -a /etc/subgid > /dev/null
echo -e "${GREEN}✓ Added root range to /etc/subgid${NC}"
fi
# Restart Incus to pick up new mappings
if systemctl is-active --quiet incus.service 2>/dev/null; then
echo -e "${BLUE}→ Restarting Incus to apply idmap changes...${NC}"
restart_incus
echo -e "${GREEN}✓ Incus restarted${NC}"
fi
}
# Ensure Incus has been initialized (creates default network, profile devices, etc.)
ensure_incus_initialized() {
# Skip if Incus is not installed
if ! command -v incus &> /dev/null; then
return
fi
# Decide whether Incus has been initialized without being fooled by the
# unmanaged physical/loopback interfaces that appear on every real host: a
# bare "is the network list non-empty?" check is never empty and falsely
# skips init (#703). `incus admin init --auto` creates both a MANAGED network
# (incusbr0) and a storage pool, so treat either as proof of initialization -
# a host set up with an existing/custom network and no managed bridge still
# has a pool, which is the reliable signal.
# If the network query itself fails (daemon down, no permissions), warn and
# bail out rather than incorrectly triggering init.
local networks pools
if ! networks="$(incus network list --format=csv 2>/dev/null)"; then
echo -e "${YELLOW}⚠ Unable to determine whether Incus has been initialized${NC}"
echo " Could not query Incus networks. Ensure the Incus daemon is running and your user has access."
return 1
fi
pools="$(incus storage list --format=csv 2>/dev/null)"
# MANAGED is CSV column 3 (YES/NO); awk avoids the cut|grep -q pipe whose
# early exit trips `set -o pipefail`.
if printf '%s\n' "$networks" | awk -F, '$3 == "YES" { found=1 } END { exit !found }' \
|| [ -n "$pools" ]; then
return
fi
echo -e "${BLUE}→ Incus has not been initialized, running incus admin init --auto...${NC}"
local output
if output="$(sudo incus admin init --auto 2>&1)"; then
echo -e "${GREEN}✓ Incus initialized${NC}"
else
echo -e "${YELLOW}⚠ Incus initialization failed${NC}"
if [ -n "$output" ]; then
printf " %s\n" "$output"
fi
return 1
fi
}
# Restart Incus and wait for the daemon to accept connections again before
# returning. Callers issue `incus` commands right after a restart (re-read idmaps,
# probe a freshly installed storage driver), and `systemctl restart` returns as
# soon as the unit is active, which can be a beat before the API socket is ready.
# waitready is bounded and best-effort — it never aborts the installer.
restart_incus() {
sudo systemctl restart incus.service
incus admin waitready --timeout=30 2>/dev/null || true
}
# Detect an OrbStack guest, where ZFS can never work.
# OrbStack injects the kernel from the host: the guest has no kernel packages,
# no headers and no module tree, so the out-of-tree ZFS module can neither be
# shipped precompiled nor built with DKMS. btrfs is compiled into that kernel,
# so it works. Mirrors the osrelease check in internal/vmhost/vmhost.go.
is_orbstack() {
case "$(uname -r 2>/dev/null | tr '[:upper:]' '[:lower:]')" in
*orbstack*) return 0 ;;
*) return 1 ;;
esac
}
# Set up fast copy-on-write storage for containers.
# ZFS is the first choice (fastest), btrfs the fallback when ZFS is unavailable.
# btrfs is still copy-on-write, so still far faster than the default `dir` pool.
setup_fast_storage() {
echo ""
if is_orbstack; then
echo -e "${YELLOW}⚠ Skipping ZFS: not supported on OrbStack${NC}"
echo " OrbStack guests get their kernel from the host, with no headers or module"
echo " tree, so the out-of-tree ZFS module can never be built or loaded."
echo " Using btrfs instead, which is built into the OrbStack kernel."
elif command -v zfs &> /dev/null || [ "$PKG_MANAGER" = "apt" ]; then
# ZFS is already present, or we can install it cleanly (apt is a plain
# userspace install). Try it; fall back to btrfs if the pool can't be made.
if setup_zfs_storage; then
return 0
fi
echo -e "${BLUE}→ Falling back to btrfs...${NC}"
else
# ZFS is not installed and this distro's ZFS packages can break the
# initramfs on install (#666 — e.g. Arch/EndeavourOS, where installing
# zfs-utils triggers a dracut/mkinitcpio rebuild that fails on the missing
# zfs module). Use btrfs, which is in-kernel and safe to install.
echo -e "${BLUE}→ ZFS not installed; using btrfs (in-kernel, safe to install)...${NC}"
fi
if ! setup_btrfs_storage; then
echo -e "${YELLOW} Containers will use default storage (slower but functional)${NC}"
return 1
fi
}
# Set up ZFS storage (for instant container creation)
setup_zfs_storage() {
echo -e "${BLUE}→ Setting up fast storage (ZFS)...${NC}"
# Check if ZFS is already installed
if command -v zfs &> /dev/null; then
echo -e "${GREEN}✓ ZFS already installed${NC}"
else
# Installing ZFS is only safe on apt (zfsutils-linux is a plain userspace
# package); on pacman/dnf/zypper the ZFS packages can rebuild and break the
# initramfs (#666). setup_fast_storage already gates on this, but guard
# here too so a future caller can't reach a wrong-distro install: the
# pkg_install below hard-codes the apt package name, so a non-apt distro
# would otherwise silently install the wrong (or a destructive) package.
if [ "$PKG_MANAGER" != "apt" ]; then
echo -e "${YELLOW}⚠ ZFS not installed and auto-install is only supported on apt${NC}"
return 1
fi
echo -e "${BLUE}→ Installing ZFS...${NC}"
if ! pkg_install zfsutils-linux 2>/dev/null; then
echo -e "${YELLOW}⚠ ZFS installation failed (may not be available for your kernel)${NC}"
return 1
fi
echo -e "${GREEN}✓ ZFS installed${NC}"
fi
# Check if ZFS pool already exists
if incus storage list --format=csv 2>/dev/null | grep -q "^zfs-pool,"; then
echo -e "${GREEN}✓ ZFS storage pool already configured${NC}"
return 0
fi
# Create ZFS storage pool
echo -e "${BLUE}→ Creating ZFS storage pool (50GiB)...${NC}"
local storage_output
if storage_output="$(sudo incus storage create zfs-pool zfs size=50GiB 2>&1)"; then
echo -e "${GREEN}✓ ZFS storage pool created${NC}"
# Configure default profile to use ZFS
echo -e "${BLUE}→ Configuring default profile to use ZFS...${NC}"
local profile_output
if profile_output="$(incus profile device set default root pool=zfs-pool 2>&1)"; then
echo -e "${GREEN}✓ Default profile configured for ZFS${NC}"
echo -e "${GREEN}✓ Containers will now start instantly (~50ms vs 5-10s)${NC}"
else
echo -e "${YELLOW}⚠ Failed to configure default profile${NC}"
if [ -n "$profile_output" ]; then
printf "${YELLOW} %s${NC}\n" "$profile_output"
fi
echo -e "${YELLOW} You can manually configure it later with:${NC}"
echo -e " ${BLUE}incus profile device add default root disk pool=zfs-pool path=/${NC}"
fi
else
echo -e "${YELLOW}⚠ ZFS storage pool creation failed${NC}"
if [ -n "$storage_output" ]; then
printf "${YELLOW} %s${NC}\n" "$storage_output"
fi
return 1
fi
}
# Set up btrfs storage, the fallback when ZFS is unavailable.
# Still copy-on-write, so container launches from a cached image are roughly 7x
# faster than the default `dir` pool (~0.2s vs ~1.1s measured on OrbStack).
setup_btrfs_storage() {
echo -e "${BLUE}→ Setting up fast storage (btrfs)...${NC}"
# Check if btrfs tools are already installed
if command -v mkfs.btrfs &> /dev/null; then
echo -e "${GREEN}✓ btrfs tools already installed${NC}"
else
echo -e "${BLUE}→ Installing btrfs tools...${NC}"
if ! pkg_install btrfs-progs btrfs-progs btrfs-progs btrfsprogs 2>/dev/null; then
echo -e "${YELLOW}⚠ btrfs installation failed${NC}"
return 1
fi
echo -e "${GREEN}✓ btrfs tools installed${NC}"
# Incus probes for available storage drivers when it starts, so a freshly
# installed mkfs.btrfs is invisible until the daemon is restarted.
if systemctl is-active --quiet incus.service 2>/dev/null; then
echo -e "${BLUE}→ Restarting Incus to pick up the btrfs driver...${NC}"
restart_incus
fi
fi
# Check if btrfs pool already exists
if incus storage list --format=csv 2>/dev/null | grep -q "^btrfs-pool,"; then
echo -e "${GREEN}✓ btrfs storage pool already configured${NC}"
return 0
fi
# Create btrfs storage pool
echo -e "${BLUE}→ Creating btrfs storage pool (50GiB)...${NC}"
local storage_output
if storage_output="$(sudo incus storage create btrfs-pool btrfs size=50GiB 2>&1)"; then
echo -e "${GREEN}✓ btrfs storage pool created${NC}"
# Configure default profile to use btrfs
echo -e "${BLUE}→ Configuring default profile to use btrfs...${NC}"
local profile_output
if profile_output="$(incus profile device set default root pool=btrfs-pool 2>&1)"; then
echo -e "${GREEN}✓ Default profile configured for btrfs${NC}"
echo -e "${GREEN}✓ Containers will now start much faster (~0.2s vs 1-5s)${NC}"
else
echo -e "${YELLOW}⚠ Failed to configure default profile${NC}"
if [ -n "$profile_output" ]; then
printf "${YELLOW} %s${NC}\n" "$profile_output"
fi
echo -e "${YELLOW} You can manually configure it later with:${NC}"
echo -e " ${BLUE}incus profile device add default root disk pool=btrfs-pool path=/${NC}"
fi
else
echo -e "${YELLOW}⚠ btrfs storage pool creation failed${NC}"
if [ -n "$storage_output" ]; then
printf "${YELLOW} %s${NC}\n" "$storage_output"
fi
return 1
fi
}
# Fetch detection databases (GTFOBins + Sigma)
fetch_detection_databases() {
if ! command -v coi &> /dev/null; then
return
fi
echo ""
echo -e "${BLUE}→ Fetching detection databases (GTFOBins + Sigma)...${NC}"
echo " This clones the GTFOBins reverse-shell database and Sigma linux/process_creation"
echo " rules used by the monitoring daemon. The Sigma clone is sparse (~300 KB)."
echo ""
if coi update patterns; then
echo -e "${GREEN}✓ Detection databases fetched${NC}"
else
echo -e "${YELLOW}⚠ Detection database fetch failed (requires git and network access)${NC}"
echo -e " Run manually later: ${BLUE}coi update patterns${NC}"
fi
}
# Post-install setup
post_install() {
setup_nm_unmanaged_veths
ensure_incus_service || true
ensure_idmap || true
ensure_incus_initialized || true
# Try to set up fast storage (best-effort, don't abort installer on failure)
setup_fast_storage || true
# Fetch GTFOBins and Sigma detection databases
fetch_detection_databases || true
echo ""
echo -e "${GREEN}✓ Installation complete!${NC}"
echo ""
echo "Next steps:"
echo ""
echo " 1. Build the COI image:"
echo -e " ${BLUE}coi build${NC}"
echo ""
echo " 2. Start your first session:"
echo -e " ${BLUE}coi shell${NC}"
echo ""
echo " 3. View available commands:"
echo -e " ${BLUE}coi --help${NC}"
echo ""
if ! groups | grep -q incus-admin; then
echo -e "${YELLOW}⚠ Remember to add yourself to incus-admin group:${NC}"
echo -e " ${BLUE}sudo usermod -aG incus-admin \$USER${NC}"
echo " Then log out and back in."
echo ""
fi
if ! command -v nft &> /dev/null; then
echo -e "${YELLOW}⚠ nftables is not installed — network isolation (restricted/allowlist modes) will not work.${NC}"
echo -e " Install with: ${BLUE}sudo apt install nftables${NC}"
echo ""
elif ! [ -f /etc/sudoers.d/coi-nft ]; then
echo -e "${YELLOW}⚠ Passwordless sudo for nft not configured — network isolation will not work.${NC}"
echo -e " Run: ${BLUE}echo \"\$USER ALL=(ALL) NOPASSWD: \$(command -v nft)\" | sudo tee /etc/sudoers.d/coi-nft && sudo chmod 0440 /etc/sudoers.d/coi-nft${NC}"
echo ""
fi
echo "Documentation: https://github.com/${REPO}"
echo ""
}
# Main installation
# Prevent NetworkManager from enrolling container veths into firewalld zones.
# NM assigns each new veth to firewalld's default zone; leaked registrations
# survive container deletion, and firewalld generates FORWARD rules as the
# CROSS PRODUCT of zone interfaces — dead veths grow the ruleset quadratically
# (145 leaked veths ~= 101k rules, issue #695). Marking veth* unmanaged stops
# the enrollment at the source; container traffic policy lives on the bridge.
#
# Entirely best-effort: every step tolerates failure (a firewall nicety must
# never abort the install under set -e / the ERR trap), and it is skippable
# with COI_SKIP_NM_UNMANAGED=1 for hosts that intentionally manage veths
# through NetworkManager (e.g. nmcli-configured veth pairs).
setup_nm_unmanaged_veths() {
[ "${COI_SKIP_NM_UNMANAGED:-0}" = "1" ] && return 0
# COI_NM_CONF_DIR is a test seam; production always uses the real path.
local conf_dir="${COI_NM_CONF_DIR:-/etc/NetworkManager/conf.d}"
local conf_file="$conf_dir/99-coi-unmanaged.conf"
[ -d "$conf_dir" ] || return 0
if [ -f "$conf_file" ]; then
return 0
fi
# Only an ACTIVE (uncommented) rule that mentions veths counts as existing
# coverage — a commented-out example must not suppress the real one.
if grep -rhs '^[[:space:]]*unmanaged-devices' "$conf_dir"/*.conf 2>/dev/null | grep -q 'veth'; then
echo -e "${GREEN}✓ NetworkManager already has an unmanaged-devices rule mentioning veths — leaving it alone${NC}"
return 0
fi
echo -e "${BLUE}→ Marking veth* unmanaged in NetworkManager (prevents firewalld zone bloat, #695; skip with COI_SKIP_NM_UNMANAGED=1)...${NC}"
# unmanaged-devices+= APPENDS to any list set elsewhere; plain '=' would
# REPLACE a user's own exclusions under NM's last-file-wins semantics.
if ! sudo tee "$conf_file" > /dev/null 2>&1 <<'NMEOF'
# Installed by code-on-incus (coi): container veths must not be enrolled in
# firewalld zones — leaked registrations grow the firewall ruleset
# quadratically. See https://github.com/mensfeld/code-on-incus/issues/695
# Remove this file (and reload NetworkManager) to undo.
[keyfile]
unmanaged-devices+=interface-name:veth*
NMEOF
then
echo -e "${YELLOW}⚠ Could not write $conf_file; skipping (see issue #695 for the manual step)${NC}"
return 0
fi
sudo systemctl reload NetworkManager 2>/dev/null || true
echo -e "${GREEN}✓ NetworkManager veth exclusion installed${NC}"
}
main() {
echo ""
echo -e "${BLUE}════════════════════════════════════════${NC}"
echo -e "${BLUE} code-on-incus (coi) installer${NC}"
echo -e "${BLUE}════════════════════════════════════════${NC}"
echo ""
detect_platform
detect_pkg_manager
check_incus
check_group
check_nft
echo ""
echo "Installation method:"
echo " 1. Download pre-built binary (fastest)"
echo " 2. Build from source"
echo ""
# Check if releases exist
if curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" &> /dev/null; then
prompt_choice "Choose [1/2] (default: 1): " "1"
case $REPLY in
2)
build_from_source
;;
*)
download_binary
;;
esac
else
echo -e "${YELLOW}⚠ No pre-built binaries available, building from source...${NC}"
build_from_source
fi
post_install
}
# Handle errors
error_handler() {
echo ""
echo -e "${RED}✗ Installation failed${NC}"
echo ""
echo "If you need help:"
echo " - Check the documentation: https://github.com/${REPO}"
echo " - File an issue: https://github.com/${REPO}/issues"
exit 1
}
trap error_handler ERR
# Run main
main "$@"