Initial import
This commit is contained in:
17
Makefile
Normal file
17
Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
all:
|
||||
cc -O2 -Wall -Wextra -DSTAGING_ROOT=\"$(HOME)/opencode-workspace/workspace\" -o src/mountctl src/mountctl.c
|
||||
|
||||
install: all
|
||||
mkdir -p workspace
|
||||
touch mounts.list
|
||||
ln -sf src/mount-opencode mount-opencode
|
||||
ln -sf src/unmount-opencode unmount-opencode
|
||||
ln -sf src/recover-mounts recover-mounts
|
||||
@echo
|
||||
@echo "Now make the helper setuid root:"
|
||||
@echo " sudo chown root $(CURDIR)/src/mountctl && sudo chmod 4755 $(CURDIR)/src/mountctl"
|
||||
|
||||
clean:
|
||||
rm -f src/mountctl
|
||||
|
||||
.PHONY: all install clean
|
||||
133
README.org
Normal file
133
README.org
Normal file
@@ -0,0 +1,133 @@
|
||||
#+TITLE: OpenCode live mounts
|
||||
#+STARTUP: :indent
|
||||
Bind-mount host folders into a *running* Docker container, add and remove
|
||||
them at will, and re-apply them after a reboot. No daemon, no Docker API
|
||||
tricks — one setuid helper and three shell scripts.
|
||||
|
||||
* Tutorial: get started
|
||||
|
||||
Build and install the helper and scripts:
|
||||
|
||||
#+begin_src bash
|
||||
make
|
||||
make install
|
||||
sudo chown root ~/opencode-workspace/src/mountctl
|
||||
sudo chmod 4755 ~/opencode-workspace/src/mountctl
|
||||
#+end_src
|
||||
|
||||
Give the container a window into the staging area. Add this =volumes=
|
||||
block to your service (replace =source= with the absolute path to your
|
||||
staging directory) and bring it up once:
|
||||
|
||||
#+begin_src yaml
|
||||
services:
|
||||
app:
|
||||
image: ubuntu
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /home/you/opencode-workspace/workspace
|
||||
target: /workspace
|
||||
bind:
|
||||
propagation: rslave
|
||||
entrypoint: "tail -f /dev/null"
|
||||
#+end_src
|
||||
|
||||
#+begin_src bash
|
||||
docker compose up -d
|
||||
#+end_src
|
||||
|
||||
Now mount something:
|
||||
|
||||
#+begin_src bash
|
||||
~/opencode-workspace/mount-opencode /home/you/code/elm
|
||||
#+end_src
|
||||
|
||||
It shows up at =/workspace/elm= inside the running container. That's it.
|
||||
|
||||
* How-to guides
|
||||
|
||||
** Add a mount
|
||||
|
||||
#+begin_src bash
|
||||
~/opencode-workspace/mount-opencode <source> [name]
|
||||
#+end_src
|
||||
|
||||
=name= defaults to =basename <source>=. The mount is recorded in
|
||||
=mounts.list=.
|
||||
|
||||
** Remove a mount
|
||||
|
||||
#+begin_src bash
|
||||
~/opencode-workspace/unmount-opencode <name>
|
||||
#+end_src
|
||||
|
||||
** Recover all mounts after a reboot
|
||||
|
||||
Bring the container back up first (its rslave bind must be in place),
|
||||
then:
|
||||
|
||||
#+begin_src bash
|
||||
~/opencode-workspace/recover-mounts
|
||||
#+end_src
|
||||
|
||||
It re-runs every mount listed in =mounts.list=; safe to run anytime.
|
||||
|
||||
* Explanation
|
||||
|
||||
Docker has no way to add a mount to a running container, and =nsenter=
|
||||
mounts can't see host paths that aren't already bind-mounted in. The
|
||||
=rslave= propagation on the staging directory is the bridge: it is the
|
||||
one path shared between host and container namespaces, and any sub-mount
|
||||
created *on the host* under it propagates into the container. =rslave=
|
||||
is one-way (host → container), so container-side mounts don't leak back
|
||||
onto the host.
|
||||
|
||||
The staging directory must therefore exist before the container starts,
|
||||
and the container must be recreated (a brief restart) if it was started
|
||||
without the bind. This is a Docker limitation, not a tool choice.
|
||||
|
||||
Why setuid root: =mount(2)= requires =CAP_SYS_ADMIN=, which ordinary
|
||||
users don't have. A compiled C helper is the only thing Linux honours
|
||||
setuid on (scripts are ignored). The helper does *only* raw bind mounts
|
||||
and umounts — no shell, no =system()= — and rejects names that could
|
||||
escape the staging root (= /=, leading dot, empty) or that point at an
|
||||
existing non-directory (symlink-escape guard).
|
||||
|
||||
* Reference
|
||||
|
||||
** =mountctl= (the setuid helper)
|
||||
|
||||
#+begin_example
|
||||
mountctl mount <source> <name> # bind-mount source at STAGING_ROOT/name
|
||||
mountctl umount <name> # umount and rmdir the mountpoint
|
||||
#+end_example
|
||||
|
||||
=STAGING_ROOT= is baked in at compile time; default is
|
||||
=$(HOME)/opencode-workspace/workspace=. Override at build time:
|
||||
|
||||
#+begin_src bash
|
||||
make STAGING_ROOT=/some/other/path
|
||||
#+end_src
|
||||
|
||||
** =mounts.list=
|
||||
|
||||
Plain text, one mount per line, tab-separated:
|
||||
|
||||
#+begin_example
|
||||
<source>\t<name>
|
||||
#+end_example
|
||||
|
||||
Managed by the scripts; edit by hand if you must.
|
||||
|
||||
** Makefile targets
|
||||
|
||||
| =make= | compile =src/mountctl= |
|
||||
| =make install= | deploy to =~/opencode-workspace/= and create the symlinks |
|
||||
| =make clean= | remove the binary |
|
||||
|
||||
** Authorization note
|
||||
|
||||
The helper is setuid root (mode 4755) for now. TODO: restrict to a
|
||||
=live-mount= Unix group via =getgroups(2)=. If your home filesystem is
|
||||
mounted =nosuid= the setuid bit is silently ignored — check with
|
||||
=mount | grep 'on /home'=.
|
||||
23
src/mount-opencode
Executable file
23
src/mount-opencode
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mount a host folder into the staging area so it appears inside the
|
||||
# running container (which bind-mounts the staging area with rslave).
|
||||
# Records the mount in mounts.list so recover-mounts can redo it.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$(realpath "$0")")" && pwd)" # .../src
|
||||
root="$(dirname "$here")" # .../opencode-workspace
|
||||
staging="$root/workspace"
|
||||
record="$root/mounts.list"
|
||||
mountctl="$here/mountctl"
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: mount-opencode <source> [name]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
src="$(realpath "$1")"
|
||||
name="${2:-$(basename "$src")}"
|
||||
|
||||
mkdir -p "$staging/$name"
|
||||
"$mountctl" mount "$src" "$name"
|
||||
printf '%s\t%s\n' "$src" "$name" >> "$record"
|
||||
120
src/mountctl.c
Normal file
120
src/mountctl.c
Normal file
@@ -0,0 +1,120 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <sys/mount.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef STAGING_ROOT
|
||||
#error "compile with -DSTAGING_ROOT=..."
|
||||
#endif
|
||||
|
||||
/* Authorization: setuid root for now. TODO: restrict to a "live-mount"
|
||||
Unix group via getgroups(2) so only members may run this binary. */
|
||||
|
||||
/* The name is a relative path under STAGING_ROOT: non-empty components,
|
||||
each not starting with '.' and surrounded by '/'. */
|
||||
static int valid_name(const char *name) {
|
||||
const char *p = name;
|
||||
if (!p || !*p || *p == '/' || *p == '.') {
|
||||
return 0;
|
||||
} else {
|
||||
while (*p) {
|
||||
if (*p == '.') {
|
||||
return 0;
|
||||
} else {
|
||||
while (*p && *p != '/') {
|
||||
p++;
|
||||
}
|
||||
if (*p == '/') {
|
||||
if (p[1] == '/' || p[1] == '.' || !p[1]) {
|
||||
return 0;
|
||||
} else {
|
||||
p++;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static char *target_for(const char *name) {
|
||||
char *t = malloc(strlen(STAGING_ROOT) + 1 + strlen(name) + 1);
|
||||
if (t) {
|
||||
sprintf(t, "%s/%s", STAGING_ROOT, name);
|
||||
} else {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
static int do_mount(const char *source, const char *name) {
|
||||
if (valid_name(name)) {
|
||||
char *target = target_for(name);
|
||||
if (target) {
|
||||
if (mount(source, target, NULL, MS_BIND, NULL) == 0) {
|
||||
printf("%s -> %s\n", source, target);
|
||||
free(target);
|
||||
return 0;
|
||||
} else {
|
||||
perror("mount");
|
||||
free(target);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "bad name: %s\n", name ? name : "(null)");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int do_umount(const char *name) {
|
||||
if (valid_name(name)) {
|
||||
char *target = target_for(name);
|
||||
if (target) {
|
||||
if (umount2(target, 0) == 0) {
|
||||
printf("unmounted %s\n", target);
|
||||
rmdir(target);
|
||||
free(target);
|
||||
return 0;
|
||||
} else {
|
||||
perror("umount");
|
||||
free(target);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "bad name: %s\n", name ? name : "(null)");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1 && strcmp(argv[1], "mount") == 0) {
|
||||
if (argc == 4) {
|
||||
return do_mount(argv[2], argv[3]);
|
||||
} else {
|
||||
fprintf(stderr, "usage: mountctl mount <source> <name>\n");
|
||||
return 2;
|
||||
}
|
||||
} else if (argc > 1 && strcmp(argv[1], "umount") == 0) {
|
||||
if (argc == 3) {
|
||||
return do_umount(argv[2]);
|
||||
} else {
|
||||
fprintf(stderr, "usage: mountctl umount <name>\n");
|
||||
return 2;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "usage: mountctl mount <source> <name>\n");
|
||||
fprintf(stderr, " mountctl umount <name>\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
18
src/recover-mounts
Executable file
18
src/recover-mounts
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Re-apply every mount recorded in mounts.list. Run this after a reboot,
|
||||
# once the container (with its rslave bind) is back up.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$(realpath "$0")")" && pwd)"
|
||||
root="$(dirname "$here")"
|
||||
staging="$root/workspace"
|
||||
record="$root/mounts.list"
|
||||
mountctl="$here/mountctl"
|
||||
|
||||
mkdir -p "$staging"
|
||||
[ -f "$record" ] || exit 0
|
||||
while IFS=$'\t' read -r src name; do
|
||||
[ -z "${name:-}" ] && continue
|
||||
mkdir -p "$staging/$name"
|
||||
"$mountctl" mount "$src" "$name"
|
||||
done < "$record"
|
||||
19
src/unmount-opencode
Executable file
19
src/unmount-opencode
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unmount a name from the staging area and drop it from mounts.list.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$(realpath "$0")")" && pwd)"
|
||||
root="$(dirname "$here")"
|
||||
record="$root/mounts.list"
|
||||
mountctl="$here/mountctl"
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: unmount-opencode <name>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
name="$1"
|
||||
"$mountctl" umount "$name"
|
||||
tmp="$(mktemp)"
|
||||
awk -F'\t' -v n="$name" '$2 != n' "$record" > "$tmp"
|
||||
mv "$tmp" "$record"
|
||||
Reference in New Issue
Block a user