Let one host carry runners for more than one registration token
A registration token is minted at a scope and serves only repositories inside it, so serving both an organization and a user account from one machine takes two registrations. There was no way to express that: one invocation owned ~/runners, and a second would have overwritten the first. The obstacle was that the label did too much. It named the runs-on label AND the compose project, the container names and the volumes - and two groups on one host usually want the SAME label, since a job from either scope should reach a runner. Sharing the label was fine; sharing an identity meant the second group replacing the first. So identity is now --name, separate from --label. It scopes the directory, the compose project, the containers, the volumes and the runner names in the forge; the label scopes nothing and is free to be shared. --name defaults to the label, so a host with one group needs no new argument and gets what it got before. A name containing a slash or a space is refused rather than producing a directory or a compose project that does not mean what it says. Three tests: two groups sharing a label collide nowhere and appear in nothing of each other's; the default keeps a single group working without --name; and unusable names are refused. Also rendered two groups end to end and confirmed separate projects, separate container names and two 0600 token files. 79 ok, 0 failed.
This commit is contained in:
+33
-3
@@ -40,6 +40,7 @@ fi
|
||||
ROLE=""
|
||||
DIR=""
|
||||
FORGE=""
|
||||
OPT_NAME=""
|
||||
OPT_LABEL=""
|
||||
OPT_COUNT=""
|
||||
OPT_VOLUMES=""
|
||||
@@ -63,6 +64,11 @@ Usage: sudo $PROG --role confined|privileged [--dir PATH] [--forge URL]
|
||||
--forge URL the forge, e.g. https://gitea.example.com. Read from
|
||||
$CONFIG when install.sh has run; required on a privileged
|
||||
host, where it has not
|
||||
--name NAME identifies this group of runners on this host: its
|
||||
directory, compose project, containers and volumes. Give
|
||||
a second group its own name to register a second token -
|
||||
another organization, or a user account - alongside the
|
||||
first. Defaults to the label
|
||||
--label NAME the label a workflow names in runs-on:
|
||||
--count N how many runners (confined only; privileged is always 1)
|
||||
--volumes PATHS host paths jobs may bind-mount, space-separated, or
|
||||
@@ -85,6 +91,7 @@ while [ $# -gt 0 ]; do
|
||||
--role) ROLE="${2:?}"; shift ;;
|
||||
--dir) DIR="${2:?}"; shift ;;
|
||||
--forge) FORGE="${2:?}"; shift ;;
|
||||
--name) OPT_NAME="${2:?}"; shift ;;
|
||||
--label) OPT_LABEL="${2:?}"; shift ;;
|
||||
--count) OPT_COUNT="${2:?}"; shift ;;
|
||||
--volumes) OPT_VOLUMES="${2:?}"; shift ;;
|
||||
@@ -161,7 +168,6 @@ cfg_get() {
|
||||
target_user="${SUDO_USER:-$(id -un)}"
|
||||
target_home=$(getent passwd "$target_user" | cut -d: -f6)
|
||||
[ -n "$target_home" ] || target_home="$HOME"
|
||||
: "${DIR:=$target_home/runners}"
|
||||
|
||||
say "=============================================================="
|
||||
say " Runner setup - $ROLE tier"
|
||||
@@ -219,6 +225,23 @@ say "a job naming it can land there instead, on a host nothing here hardened."
|
||||
say
|
||||
ask label "Runner label" "${OPT_LABEL:-$default_label}"
|
||||
[ -n "$label" ] || die "a label is required"
|
||||
# The group's identity, kept separate from its label. Two groups on one host
|
||||
# may share a label - that is the point of registering one at organization
|
||||
# scope and another for a user account, so either scope's workflows reach the
|
||||
# same pool - but they cannot share a compose project, container names or
|
||||
# volumes without colliding.
|
||||
say
|
||||
say "A name for this group of runners on this host. It names the directory,"
|
||||
say "the compose project, the containers and the volumes - not anything a"
|
||||
say "workflow sees. Give a second group its own name to register a second"
|
||||
say "token alongside this one."
|
||||
ask name "Group name" "${OPT_NAME:-$label}"
|
||||
[ -n "$name" ] || die "a group name is required"
|
||||
case "$name" in
|
||||
*/*|*' '*) die "a group name cannot contain a slash or a space: $name" ;;
|
||||
esac
|
||||
: "${DIR:=$target_home/runners/$name}"
|
||||
|
||||
case "$label" in
|
||||
ubuntu-*|ubuntu|latest|self-hosted)
|
||||
say
|
||||
@@ -270,7 +293,7 @@ if [ "$ROLE" = privileged ]; then
|
||||
fi
|
||||
|
||||
# --- render -----------------------------------------------------------------
|
||||
project="${label}-runners"
|
||||
project="${name}-runners"
|
||||
|
||||
install -d -m 0755 "$DIR" || die "cannot create $DIR"
|
||||
|
||||
@@ -320,6 +343,7 @@ subst() {
|
||||
# would break the YAML anyway.
|
||||
sed -e "s|@@PROJECT@@|$project|g" \
|
||||
-e "s|@@LABEL@@|$label|g" \
|
||||
-e "s|@@NAME@@|$name|g" \
|
||||
-e "s|@@IMAGE@@|$act_image|g" \
|
||||
-e "s|@@JOB_IMAGE@@|$job_image|g" \
|
||||
-e "s|@@INSTANCE_URL@@|$instance|g" \
|
||||
@@ -557,4 +581,10 @@ say
|
||||
say " project $DIR"
|
||||
say " token $ENV_FILE (0600 - empty it once they are Online)"
|
||||
say " restart cd $DIR && docker compose restart"
|
||||
say " change re-run $PROG --role $ROLE"
|
||||
say " change re-run $PROG --role $ROLE --name $name"
|
||||
say
|
||||
say "To register a second token on this host - another organization, or a user"
|
||||
say "account - run this again with a different --name. The label may be the"
|
||||
say "same; everything else is kept apart:"
|
||||
say
|
||||
say " $PROG --role $ROLE --name <other>"
|
||||
|
||||
@@ -337,6 +337,31 @@ Give it one label that nothing else carries, and grant it to as few
|
||||
repositories as you can stand. Then take the clean snapshot, which §7 below
|
||||
covers for both tiers.
|
||||
|
||||
### More than one registration token on the same host
|
||||
|
||||
A registration token is minted at a scope — an organization, or a user
|
||||
account — and serves only repositories inside it. To serve two scopes from one
|
||||
host, register two groups:
|
||||
|
||||
```sh
|
||||
sudo dejima-ci-runners --role confined --name dejima --label ci-shared
|
||||
sudo dejima-ci-runners --role confined --name branderson --label ci-shared
|
||||
```
|
||||
|
||||
Each gets its own directory, compose project, containers, volumes and `.env`,
|
||||
so the tokens never meet and either group can be changed or removed without
|
||||
touching the other. Run the command again per group; it is idempotent, so the
|
||||
first is undisturbed by the second.
|
||||
|
||||
**The label may be the same for both, and usually should be.** It is what a
|
||||
workflow names in `runs-on:`, and a job from either scope should be able to
|
||||
reach a runner. That is why the group name is a separate thing: sharing an
|
||||
identity as well would collide on the compose project and the container names,
|
||||
and the second group would replace the first rather than join it.
|
||||
|
||||
What the group name must not be shared with is another group. Everything the
|
||||
host uses to tell them apart is derived from it.
|
||||
|
||||
---
|
||||
|
||||
## 7. Snapshot it clean, once the runner is registered
|
||||
|
||||
@@ -52,13 +52,20 @@ services:
|
||||
# label and one job network. That is what makes them a pool: a job naming
|
||||
# @@LABEL@@ lands on whichever is free. Only the name and the data volume
|
||||
# differ.
|
||||
#
|
||||
# Those names come from @@NAME@@, which identifies this GROUP of runners on
|
||||
# this host, rather than from the label. The two are separate on purpose: a
|
||||
# host can carry a second group registered with a different token - another
|
||||
# organization, or a user account - and both may legitimately answer the
|
||||
# same label. Sharing an identity as well would collide on the compose
|
||||
# project, the container names and the volumes.
|
||||
# @@SERVICE_BEGIN@@
|
||||
"@@LABEL@@-@@N@@":
|
||||
"@@NAME@@-@@N@@":
|
||||
<<: *ci-runner
|
||||
container_name: "@@LABEL@@-@@N@@"
|
||||
container_name: "@@NAME@@-@@N@@"
|
||||
environment:
|
||||
<<: *ci-env
|
||||
GITEA_RUNNER_NAME: "@@LABEL@@-@@N@@"
|
||||
GITEA_RUNNER_NAME: "@@NAME@@-@@N@@"
|
||||
volumes:
|
||||
# The runner's OWN access to the daemon, so it can create job
|
||||
# containers. NOT the same setting as container.docker_host, which
|
||||
@@ -69,7 +76,7 @@ services:
|
||||
- ./config.yaml:/config.yaml:ro
|
||||
# Its registration identity. One per runner, never shared - two runners
|
||||
# cannot share a .runner.
|
||||
- "@@LABEL@@-@@N@@-data:/data"
|
||||
- "@@NAME@@-@@N@@-data:/data"
|
||||
# @@SERVICE_END@@
|
||||
|
||||
networks:
|
||||
@@ -89,5 +96,5 @@ networks:
|
||||
|
||||
volumes:
|
||||
# @@VOLUME_BEGIN@@
|
||||
"@@LABEL@@-@@N@@-data":
|
||||
"@@NAME@@-@@N@@-data":
|
||||
# @@VOLUME_END@@
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
name: "@@PROJECT@@"
|
||||
|
||||
services:
|
||||
"@@LABEL@@":
|
||||
"@@NAME@@":
|
||||
# Pin the tag. This process executes untrusted code on every pull request,
|
||||
# and :latest means the thing doing that can change without you deciding
|
||||
# it should.
|
||||
image: "@@IMAGE@@"
|
||||
restart: unless-stopped
|
||||
container_name: "@@LABEL@@"
|
||||
container_name: "@@NAME@@"
|
||||
networks: [runners]
|
||||
|
||||
environment:
|
||||
@@ -43,7 +43,7 @@ services:
|
||||
# invisible. Check it first when an edit appears to do nothing.
|
||||
CONFIG_FILE: /config.yaml
|
||||
GITEA_INSTANCE_URL: "@@INSTANCE_URL@@"
|
||||
GITEA_RUNNER_NAME: "@@LABEL@@"
|
||||
GITEA_RUNNER_NAME: "@@NAME@@"
|
||||
# Needed only until registration completes; the identity then lives in
|
||||
# the volume below. Comes from a .env file beside this one (chmod 600)
|
||||
# so it is not in the file you commit, and is worth removing once
|
||||
@@ -60,7 +60,7 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./config.yaml:/config.yaml:ro
|
||||
# Its own registration identity. Never shared with another runner.
|
||||
- "@@LABEL@@-data:/data"
|
||||
- "@@NAME@@-data:/data"
|
||||
# No bind for /tmp/dejima-ci. The job container gets that path because
|
||||
# the workflow asks for it and container.valid_volumes permits it, and
|
||||
# the host daemon resolves the source host-side. This container never
|
||||
@@ -74,4 +74,4 @@ networks:
|
||||
runners:
|
||||
|
||||
volumes:
|
||||
"@@LABEL@@-data":
|
||||
"@@NAME@@-data":
|
||||
|
||||
@@ -226,6 +226,48 @@ privileged() {
|
||||
done
|
||||
}
|
||||
|
||||
@test "two groups on one host can share a label without colliding" {
|
||||
# The reason to register a second token at all: one group at organization
|
||||
# scope and another for a user account, both answering the same label so
|
||||
# either scope's workflows reach a runner. Sharing the label is the point;
|
||||
# sharing a compose project, container names or volumes would mean the
|
||||
# second group quietly replacing the first.
|
||||
confined --name org --label shared --count 2
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
OUT="$TMP/second" confined --name user --label shared --count 2
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
grep -q '^name: "org-runners"$' "$TMP/runners/docker-compose.yml"
|
||||
grep -q '^name: "user-runners"$' "$TMP/second/docker-compose.yml"
|
||||
grep -q 'container_name: "org-1"' "$TMP/runners/docker-compose.yml"
|
||||
grep -q 'container_name: "user-1"' "$TMP/second/docker-compose.yml"
|
||||
grep -q '"org-1-data":' "$TMP/runners/docker-compose.yml"
|
||||
grep -q '"user-1-data":' "$TMP/second/docker-compose.yml"
|
||||
|
||||
# Nothing identifying one group appears in the other.
|
||||
! grep -q 'user-' "$TMP/runners/docker-compose.yml"
|
||||
! grep -q '\borg-' "$TMP/second/docker-compose.yml"
|
||||
|
||||
# ...while both answer the same label, which is what a workflow names.
|
||||
grep -q '"shared:docker://' "$TMP/runners/config.yaml"
|
||||
grep -q '"shared:docker://' "$TMP/second/config.yaml"
|
||||
}
|
||||
|
||||
@test "the group name defaults to the label, so one group needs no --name" {
|
||||
confined --label solo
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q '^name: "solo-runners"$' "$OUT/docker-compose.yml"
|
||||
grep -q 'container_name: "solo-1"' "$OUT/docker-compose.yml"
|
||||
}
|
||||
|
||||
@test "a group name that would break a path or a compose project is refused" {
|
||||
confined --name 'a/b'
|
||||
[ "$status" -ne 0 ]
|
||||
confined --name 'a b'
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "both rendered files are parseable YAML" {
|
||||
if ! command -v js-yaml >/dev/null 2>&1 && ! python3 -c 'import yaml' 2>/dev/null; then
|
||||
skip "no YAML parser available"
|
||||
|
||||
Reference in New Issue
Block a user