fix(provisioner): SSOT workspace volume naming + e2e-names CLI (SEV-2499 follow-up) #2543

Merged
agent-reviewer-cr2 merged 1 commits from fix/sev-2499-ssot-volume-names into main 2026-06-11 01:07:26 +00:00
5 changed files with 122 additions and 10 deletions
+17
View File
@@ -92,6 +92,10 @@ jobs:
cache: true
cache-dependency-path: workspace-server/go.sum
- name: Build e2e-names SSOT CLI
working-directory: workspace-server
run: go build -o /usr/local/bin/e2e-names ./cmd/e2e-names
- name: Ensure provisioner network + pre-pull alpine
run: |
# The local provisioner attaches workspace containers to
@@ -100,6 +104,19 @@ jobs:
# named config volume. Pre-pull + ensure the bridge (idempotent).
docker pull alpine:3 >/dev/null
docker network create molecule-core-net >/dev/null 2>&1 || true
# SEV-2499 / mc#2499: connect the act_runner job container to
# molecule-core-net so workspace containers can reach the platform
# server. Without this, the platform runs in the job container's
# namespace and workspace containers on molecule-core-net time out
# trying to reach the gateway IP (the host cannot forward to an
# unpublished container port).
JOB_CID="$(hostname)"
echo "JOB_CID=${JOB_CID}" >> "$GITHUB_ENV"
if docker network connect molecule-core-net "$JOB_CID" >/dev/null 2>&1; then
echo "Connected job container ($JOB_CID) to molecule-core-net."
else
echo "WARN: could not connect job container to molecule-core-net (fallback to gateway)."
fi
echo "alpine:3 pre-pulled; molecule-core-net ensured."
- name: Start Postgres (docker, ephemeral host port)
@@ -190,8 +190,26 @@ except Exception:
print('')"
}
# SEV-2499 SSOT: use the same Go naming helpers the provisioner uses so the
# e2e script can never drift from the real naming convention. The CI job
# pre-builds e2e-names to /usr/local/bin; local runs must build it once:
# go build -o /usr/local/bin/e2e-names ./cmd/e2e-names
_e2e_name() {
local kind="$1" wid="$2"
if [ -x "$(command -v e2e-names)" ]; then
e2e-names "$kind" "$wid"
else
echo "SEV-2499: e2e-names not found in PATH — build it with: go build -o /usr/local/bin/e2e-names ./cmd/e2e-names" >&2
exit 2
fi
}
config_volume_name() { _e2e_name config-volume "$1"; }
session_volume_name() { _e2e_name session-volume "$1"; }
workspace_volume_name(){ _e2e_name workspace-volume "$1"; }
container_name() { _e2e_name container "$1"; }
container_running() { # container_running <ws-id> -> echoes name if running
docker ps --filter "name=ws-${1}" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1
docker ps --filter "name=$(container_name "$1")" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1
}
diagnose_provision() {
@@ -224,11 +242,11 @@ cleanup() {
# SCOPED teardown — only the workspace this test created. Never a blanket
# sweep (other dev workspaces may be live on this shared daemon).
e2e_delete_workspace "$WSID" "" >/dev/null 2>&1 || true
docker rm -f "ws-${WSID}" >/dev/null 2>&1 || true
docker rm -f "$(container_name "$WSID")" >/dev/null 2>&1 || true
docker volume rm -f \
"ws-${WSID}-configs" "ws-${WSID}-claude-sessions" \
"ws-${WSID}-workspace" >/dev/null 2>&1 || true
echo "cleaned workspace $WSID + ws-${WSID} container/volumes"
"$(config_volume_name "$WSID")" "$(session_volume_name "$WSID")" \
"$(workspace_volume_name "$WSID")" >/dev/null 2>&1 || true
echo "cleaned workspace $WSID + $(container_name "$WSID") container/volumes"
fi
# Restore the cache tag to whatever it pointed at before we retagged it, so a
# stub run doesn't leave the real claude-code tag aliased to the stub.
@@ -347,7 +365,7 @@ if [ -z "$WSID" ]; then
exit 1
fi
pass "workspace created: $WSID"
CONFIG_VOL="ws-${WSID}-configs"
CONFIG_VOL="$(config_volume_name "$WSID")"
# Mint a workspace bearer for the WorkspaceAuth-gated secret + /restart calls.
WTOKEN=$(e2e_mint_workspace_token "$WSID" || true)
+45
View File
@@ -0,0 +1,45 @@
// e2e-names prints Docker container/volume names for a workspace ID.
//
// This is the shell-side single source of truth (SSOT) for SEV-2499:
// it imports the same Go naming helpers the provisioner uses, so E2E
// scripts can never drift from the real naming convention again.
//
// Usage:
//
// e2e-names container <workspace-id>
// e2e-names config-volume <workspace-id>
// e2e-names session-volume <workspace-id>
// e2e-names workspace-volume <workspace-id>
package main
import (
"fmt"
"os"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/provisioner"
)
func main() {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: e2e-names <kind> <workspace-id>")
fmt.Fprintln(os.Stderr, " kinds: container, config-volume, session-volume, workspace-volume")
os.Exit(1)
}
kind := os.Args[1]
wsid := os.Args[2]
switch kind {
case "container":
fmt.Println(provisioner.ContainerName(wsid))
case "config-volume":
fmt.Println(provisioner.ConfigVolumeName(wsid))
case "session-volume":
fmt.Println(provisioner.ClaudeSessionVolumeName(wsid))
case "workspace-volume":
fmt.Println(provisioner.WorkspaceVolumeName(wsid))
default:
fmt.Fprintf(os.Stderr, "e2e-names: unknown kind %q\n", kind)
os.Exit(1)
}
}
@@ -219,6 +219,12 @@ func ClaudeSessionVolumeName(workspaceID string) string {
return fmt.Sprintf("ws-%s-claude-sessions", workspaceID)
}
// WorkspaceVolumeName returns the Docker named volume for a workspace's
// /workspace mount.
func WorkspaceVolumeName(workspaceID string) string {
return fmt.Sprintf("ws-%s-workspace", workspaceID)
}
// legacyClaudeSessionVolumeName returns the pre-KI-013 truncated session volume name.
func legacyClaudeSessionVolumeName(workspaceID string) string {
id := workspaceID
@@ -790,15 +796,13 @@ func (p *Provisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string, e
func buildWorkspaceMount(cfg WorkspaceConfig) string {
// Named volume when no host path is configured.
if cfg.WorkspacePath == "" {
volumeName := fmt.Sprintf("ws-%s-workspace", cfg.WorkspaceID)
return fmt.Sprintf("%s:/workspace", volumeName)
return fmt.Sprintf("%s:/workspace", WorkspaceVolumeName(cfg.WorkspaceID))
}
// Host bind mount. Append :ro for read-only mode; otherwise default
// (implicit read-write). "none" explicitly opts out of the mount
// even when a path is set.
if cfg.WorkspaceAccess == WorkspaceAccessNone {
volumeName := fmt.Sprintf("ws-%s-workspace", cfg.WorkspaceID)
return fmt.Sprintf("%s:/workspace", volumeName)
return fmt.Sprintf("%s:/workspace", WorkspaceVolumeName(cfg.WorkspaceID))
}
if cfg.WorkspaceAccess == WorkspaceAccessReadOnly {
return fmt.Sprintf("%s:/workspace:ro", cfg.WorkspacePath)
@@ -482,6 +482,34 @@ func TestConfigVolumeName_DistinctSamePrefix12(t *testing.T) {
}
}
// TestWorkspaceVolumeName verifies workspace volume naming.
func TestWorkspaceVolumeName(t *testing.T) {
tests := []struct {
id string
want string
}{
{"short", "ws-short-workspace"},
{"exactly12ch", "ws-exactly12ch-workspace"},
{"longer-than-twelve-characters", "ws-longer-than-twelve-characters-workspace"},
{"abc", "ws-abc-workspace"},
}
for _, tt := range tests {
got := WorkspaceVolumeName(tt.id)
if got != tt.want {
t.Errorf("WorkspaceVolumeName(%q) = %q, want %q", tt.id, got, tt.want)
}
}
}
// TestWorkspaceVolumeName_DistinctSamePrefix12 is a regression guard for KI-013.
func TestWorkspaceVolumeName_DistinctSamePrefix12(t *testing.T) {
id1 := "123456789abc-4def-1234-567890abcdef"
id2 := "123456789abc-4def-1234-567890abcdf0"
if WorkspaceVolumeName(id1) == WorkspaceVolumeName(id2) {
t.Fatalf("WorkspaceVolumeName must differ for same-first-12 UUIDs: both = %q", WorkspaceVolumeName(id1))
}
}
// ---------- #12 — claude-sessions volume naming ----------
// TestClaudeSessionVolumeName_Deterministic: same ID → same volume name, and