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