fix(provisioner): SSOT workspace volume naming + e2e-names CLI (SEV-2499 follow-up) #2543
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user