Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions .github/workflows/runner-lane-registration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ jobs:
${{ vars.LAUNCHPLANE_RUNNER_HOST_HYGIENE_EXECUTION_LANE }}
RUNNER_REGISTRATION_SERVICE_USER: >-
${{ vars.LAUNCHPLANE_RUNNER_HOST_HYGIENE_SERVICE_USER }}
RUNNER_REGISTRATION_ALLOWED_ROOT: >-
${{ vars.LAUNCHPLANE_RUNNER_REGISTRATION_ALLOWED_ROOT }}
LAUNCHPLANE_SERVICE_URL: ${{ vars.LAUNCHPLANE_PUBLIC_URL }}
steps:
- name: Checkout
Expand Down Expand Up @@ -90,11 +92,14 @@ jobs:
echo "Runner must execute as expected service user." >&2
exit 1
fi
if [ "${{ inputs.mutate }}" = "true" ] &&
[ "${{ inputs.confirmation }}" != "register runner lane" ]; then
if [ "${MUTATE_REQUESTED}" = "true" ] &&
[ "${CONFIRMATION}" != "register runner lane" ]; then
echo "mutate=true requires confirmation: register runner lane" >&2
exit 1
fi
env:
MUTATE_REQUESTED: ${{ inputs.mutate }}
CONFIRMATION: ${{ inputs.confirmation }}

- name: Read pre-registration inventory
shell: bash
Expand Down Expand Up @@ -122,29 +127,47 @@ jobs:
RUNNER_LABELS_INPUT: ${{ inputs.labels }}
AUDIT_RECORD_KEY: ${{ inputs.audit_record_key }}
GH_TOKEN: ${{ secrets.LAUNCHPLANE_RUNNER_REGISTRATION_GITHUB_TOKEN }}
MUTATE_REQUESTED: ${{ inputs.mutate }}
run: |
set -euo pipefail
approved_root="${RUNNER_REGISTRATION_ALLOWED_ROOT:-$HOME/actions-runners}"
jq -n \
--arg audit_record_key "$AUDIT_RECORD_KEY" \
--arg message \
"runner lane registration executor did not produce a result" \
'{status:"failed", audit_record_key:$audit_record_key,
message:$message}' \
> runner-lane-registration-result.json
if [ -z "${GH_TOKEN:-}" ]; then
echo "Missing runner registration GitHub token secret." >&2
exit 1
fi
if [ "$REGISTRATION_ROOT" = "auto" ]; then
REGISTRATION_ROOT="$HOME/actions-runners"
REGISTRATION_ROOT="$approved_root"
fi
if [ "${REGISTRATION_ROOT#/}" = "$REGISTRATION_ROOT" ]; then
echo "registration_root must be absolute or auto." >&2
exit 1
fi
label_args=()
IFS=',' read -r -a labels <<< "$RUNNER_LABELS_INPUT"
for label in "${labels[@]}"; do
trimmed="$(xargs <<< "$label")"
if [ -n "$trimmed" ]; then
label_args+=(--label "$trimmed")
fi
done
if [ -n "${RUNNER_REGISTRATION_ALLOWED_ROOT:-}" ] &&
[ "$REGISTRATION_ROOT" != "$RUNNER_REGISTRATION_ALLOWED_ROOT" ]; then
echo "registration_root must match Launchplane approved root." >&2
exit 1
fi
python - <<'PY' > runner-lane-registration-label-args.bin
import os
import sys

labels = os.environ["RUNNER_LABELS_INPUT"].split(",")
for label in labels:
trimmed = label.strip()
if trimmed:
sys.stdout.write("--label\0")
sys.stdout.write(f"{trimmed}\0")
PY
mapfile -d '' -t label_args < runner-lane-registration-label-args.bin
mutate_flag=--dry-run
if [ "${{ inputs.mutate }}" = "true" ]; then
if [ "$MUTATE_REQUESTED" = "true" ]; then
mutate_flag=--mutate
fi
GITHUB_TOKEN="$GH_TOKEN" uv run launchplane work-graph \
Expand All @@ -159,23 +182,27 @@ jobs:
--audit-record-key "$AUDIT_RECORD_KEY" \
--allowed-repository "$TARGET_REPOSITORY" \
--approved-host "$RUNNER_REGISTRATION_HOST" \
--allowed-registration-root "$REGISTRATION_ROOT" \
--allowed-registration-root "$approved_root" \
--inventory-file runner-lane-registration-pre-inventory.json \
--audit-mode service \
--service-url "$LAUNCHPLANE_SERVICE_URL" \
"$mutate_flag" \
> runner-lane-registration-result.json
> runner-lane-registration-result.tmp
mv \
runner-lane-registration-result.tmp \
runner-lane-registration-result.json
status="$(jq -r '.status // ""' runner-lane-registration-result.json)"
echo "registration_status=$status" >>"$GITHUB_OUTPUT"

- name: Upload registration result
if: always()
uses: actions/upload-artifact@v7
with:
name: runner-lane-registration-result
path: |
runner-lane-registration-pre-inventory.json
runner-lane-registration-result.json
if-no-files-found: error
if-no-files-found: warn

- name: Fail on unexpected registration result
env:
Expand Down
39 changes: 24 additions & 15 deletions control_plane/cli_runner_lanes.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
from control_plane.workflows.runner_lane_registration_executor import (
execute_runner_lane_registration_executor,
)
from control_plane.workflows.runner_lane_registration_executor import (
validate_local_executor_environment as validate_runner_registration_environment,
)
from control_plane.workflows.runner_host_hygiene_executor import RunnerHostHygieneExecutorRequest
from control_plane.workflows.runner_host_hygiene_executor import DOCKER_BUILDX_PLUGIN_PATH_COMMAND
from control_plane.workflows.runner_host_hygiene_executor import RemoteCommandRunner
Expand All @@ -67,7 +70,9 @@
from control_plane.workflows.runner_host_hygiene_executor import (
execute_runner_host_hygiene_executor,
)
from control_plane.workflows.runner_host_hygiene_executor import validate_local_executor_environment
from control_plane.workflows.runner_host_hygiene_executor import (
validate_local_executor_environment as validate_runner_host_hygiene_environment,
)
from control_plane.workflows.ship import utc_now_timestamp


Expand Down Expand Up @@ -608,8 +613,10 @@ def runner_lane_registration_executor(
pre_inventory = _load_runner_lane_inventory(inventory_file)
token_env = github_token_env.strip()
token = os.environ.get(token_env, "").strip() if token_env else ""
if not token:
raise click.ClickException(f"Missing GitHub token in environment variable {token_env}.")
transport = UrllibMergeTrainGitHubTransport(
token=token or "dry-run-token",
token=token,
api_base_url=github_api_base_url,
)
inventory_reader = GitHubRunnerLaneInventoryReader(transport=transport)
Expand All @@ -628,19 +635,21 @@ def runner_lane_registration_executor(
label="runner lane registration",
),
)
request = RunnerLaneRegistrationExecutorRequest(
repository=repository,
host_name=host_name,
execution_lane=execution_lane,
service_user=service_user,
lane_name=lane_name,
registration_root=registration_root,
labels=labels,
mutate=mutate,
audit_record_key=audit_record_key,
timeout_seconds=timeout_seconds,
)
validate_runner_registration_environment(request=request)
result = execute_runner_lane_registration_executor(
request=RunnerLaneRegistrationExecutorRequest(
repository=repository,
host_name=host_name,
execution_lane=execution_lane,
service_user=service_user,
lane_name=lane_name,
registration_root=registration_root,
labels=labels,
mutate=mutate,
audit_record_key=audit_record_key,
timeout_seconds=timeout_seconds,
),
request=request,
policy=RunnerLaneRegistrationPolicy(
allowed_repositories=allowed_repositories,
approved_hosts=approved_hosts,
Expand Down Expand Up @@ -1225,7 +1234,7 @@ def runner_host_hygiene_executor(
timeout_seconds=timeout_seconds,
prune_until=prune_until,
)
validate_local_executor_environment(request=request)
validate_runner_host_hygiene_environment(request=request)
result = execute_runner_host_hygiene_executor(
request=request,
remote_runner=build_local_command_runner(),
Expand Down
28 changes: 25 additions & 3 deletions control_plane/contracts/runner_lane_registration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Literal
import re

from pydantic import BaseModel, ConfigDict, Field, model_validator

Expand Down Expand Up @@ -63,9 +64,17 @@ def _normalize_request(self) -> "RunnerLaneRegistrationRequest":
self.repository = _normalized_repository(self.repository)
self.host_name = _required_text(
self.host_name, "runner lane registration requires host_name"
)
).lower()
self.lane_name = _required_text(
self.lane_name, "runner lane registration requires lane_name"
).lower()
_validate_slug(
self.host_name,
"runner lane registration host_name must use letters, numbers, dots, underscores, or hyphens",
)
_validate_slug(
self.lane_name,
"runner lane registration lane_name must use letters, numbers, dots, underscores, or hyphens",
)
self.registration_root = _normalized_path(
_required_text(
Expand Down Expand Up @@ -256,7 +265,9 @@ def plan_runner_lane_registration(
f"runner lane inventory repository does not match request: {inventory_repository}",
)
)
existing_lanes = tuple(lane for lane in inventory.lanes if lane.name == request.lane_name)
existing_lanes = tuple(
lane for lane in inventory.lanes if lane.name.strip().lower() == request.lane_name
)
if existing_lanes:
blockers.append(
_blocker(
Expand Down Expand Up @@ -345,7 +356,13 @@ def _normalized_labels(values: tuple[str, ...]) -> tuple[str, ...]:


def _normalized_tokens(values: tuple[str, ...]) -> tuple[str, ...]:
return tuple(sorted({value.strip().lower() for value in values if value.strip()}))
normalized_tokens = tuple(sorted({value.strip().lower() for value in values if value.strip()}))
for token in normalized_tokens:
_validate_slug(
token,
"runner lane registration labels must use letters, numbers, dots, underscores, or hyphens",
)
return normalized_tokens


def _normalized_path(value: str) -> str:
Expand All @@ -364,6 +381,11 @@ def _normalized_path(value: str) -> str:
return "/" + "/".join(segments)


def _validate_slug(value: str, message: str) -> None:
if not re.fullmatch(r"[a-z0-9][a-z0-9._-]{0,127}", value):
raise ValueError(message)


def _required_text(value: str, message: str) -> str:
normalized_value = value.strip()
if not normalized_value:
Expand Down
42 changes: 33 additions & 9 deletions control_plane/workflows/runner_lane_registration_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import os
import pwd
import re
from urllib.error import HTTPError
from urllib.request import Request, urlopen

Expand Down Expand Up @@ -46,15 +47,27 @@ def _normalize_request(self) -> "RunnerLaneRegistrationExecutorRequest":
self.repository = repository_full_name(self.repository)
self.host_name = _required_text(
self.host_name, "runner lane registration executor requires host_name"
)
).lower()
self.execution_lane = _required_text(
self.execution_lane, "runner lane registration executor requires execution_lane"
)
).lower()
self.service_user = _required_text(
self.service_user, "runner lane registration executor requires service_user"
)
self.lane_name = _required_text(
self.lane_name, "runner lane registration executor requires lane_name"
).lower()
_validate_slug(
self.host_name,
"runner lane registration executor host_name must use letters, numbers, dots, underscores, or hyphens",
)
_validate_slug(
self.execution_lane,
"runner lane registration executor execution_lane must use letters, numbers, dots, underscores, or hyphens",
)
_validate_slug(
self.lane_name,
"runner lane registration executor lane_name must use letters, numbers, dots, underscores, or hyphens",
)
self.registration_root = _required_text(
self.registration_root,
Expand Down Expand Up @@ -156,19 +169,25 @@ def execute_runner_lane_registration_executor(
)


def validate_local_executor_environment(*, request: RunnerLaneRegistrationExecutorRequest) -> None:
current_user = pwd.getpwuid(os.getuid()).pw_name
if current_user != request.service_user:
def validate_local_executor_environment(
*,
request: RunnerLaneRegistrationExecutorRequest,
env: Mapping[str, str] | None = None,
current_user: str | None = None,
) -> None:
execution_env = os.environ if env is None else env
effective_user = current_user or pwd.getpwuid(os.getuid()).pw_name
if effective_user != request.service_user:
raise ValueError(f"runner lane registration executor must run as {request.service_user}.")
github_repository = os.environ.get("GITHUB_REPOSITORY", "").strip().lower()
if github_repository and github_repository != "cbusillo/launchplane":
github_repository = execution_env.get("GITHUB_REPOSITORY", "").strip().lower()
if github_repository != "cbusillo/launchplane":
raise ValueError("runner lane registration executor must run from cbusillo/launchplane.")
runner_labels = {
label.strip().lower()
for label in os.environ.get("RUNNER_LABELS", "").split(",")
for label in execution_env.get("RUNNER_LABELS", "").split(",")
if label.strip()
}
if runner_labels and request.execution_lane.lower() not in runner_labels:
if request.execution_lane.lower() not in runner_labels:
raise ValueError(
"runner lane registration executor is not running on the approved execution lane."
)
Expand Down Expand Up @@ -270,3 +289,8 @@ def _required_text(value: str, message: str) -> str:
if not normalized_value:
raise ValueError(message)
return normalized_value


def _validate_slug(value: str, message: str) -> None:
if not re.fullmatch(r"[a-z0-9][a-z0-9._-]{0,127}", value):
raise ValueError(message)
Loading