From 1c5b4ee6a8c889ab1aa4e819a187252849543f87 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 17:13:39 -0400 Subject: [PATCH] Persist server auth state on session storage --- .../workflows/build-ghcr-kubeopencode.yaml | 49 ++++++++++++ internal/controller/server_builder.go | 18 +++++ internal/controller/server_builder_test.go | 76 +++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 .github/workflows/build-ghcr-kubeopencode.yaml diff --git a/.github/workflows/build-ghcr-kubeopencode.yaml b/.github/workflows/build-ghcr-kubeopencode.yaml new file mode 100644 index 00000000..239b54db --- /dev/null +++ b/.github/workflows/build-ghcr-kubeopencode.yaml @@ -0,0 +1,49 @@ +name: Build GHCR KubeOpenCode + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build-kubeopencode: + name: Build and Push Controller Image + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Build UI + run: | + cd ui && npm install && npm run build + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push kubeopencode + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ghcr.io/${{ github.repository_owner }}/kubeopencode:${{ github.sha }} diff --git a/internal/controller/server_builder.go b/internal/controller/server_builder.go index d7fa7a91..6737fc2e 100644 --- a/internal/controller/server_builder.go +++ b/internal/controller/server_builder.go @@ -43,6 +43,18 @@ const ( // ServerSessionDBPath is the full path to the OpenCode session database. ServerSessionDBPath = ServerSessionMountPath + "/opencode.db" + // ServerPersistentHomeDir is the HOME directory for persisted server-mode agents. + ServerPersistentHomeDir = ServerSessionMountPath + "/home" + + // ServerPersistentXDGConfigHome is the XDG config root for persisted server-mode agents. + ServerPersistentXDGConfigHome = ServerSessionMountPath + "/.config" + + // ServerPersistentXDGDataHome is the XDG data root for persisted server-mode agents. + ServerPersistentXDGDataHome = ServerSessionMountPath + "/.local/share" + + // ServerPersistentXDGStateHome is the XDG state root for persisted server-mode agents. + ServerPersistentXDGStateHome = ServerSessionMountPath + "/.local/state" + // DefaultSessionPVCSize is the default size for the session PVC. DefaultSessionPVCSize = "1Gi" @@ -244,6 +256,12 @@ func BuildServerDeployment(agent *kubeopenv1alpha1.Agent, agentCfg agentConfig, Name: OpenCodeDBEnvVar, Value: ServerSessionDBPath, }) + envVars = append(envVars, + corev1.EnvVar{Name: "HOME", Value: ServerPersistentHomeDir}, + corev1.EnvVar{Name: "XDG_CONFIG_HOME", Value: ServerPersistentXDGConfigHome}, + corev1.EnvVar{Name: "XDG_DATA_HOME", Value: ServerPersistentXDGDataHome}, + corev1.EnvVar{Name: "XDG_STATE_HOME", Value: ServerPersistentXDGStateHome}, + ) } // Add credentials (secrets as env vars or file mounts) diff --git a/internal/controller/server_builder_test.go b/internal/controller/server_builder_test.go index cb5c13e4..97ed3f00 100644 --- a/internal/controller/server_builder_test.go +++ b/internal/controller/server_builder_test.go @@ -253,6 +253,54 @@ func TestBuildServerDeployment_WithHOMEAndSHELLEnvVars(t *testing.T) { } } +func TestBuildServerDeployment_WithSessionPersistenceUsesPersistentHomeAndXDG(t *testing.T) { + agent := &kubeopenv1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-agent", + Namespace: "default", + }, + Spec: kubeopenv1alpha1.AgentSpec{ + Port: 4096, + Persistence: &kubeopenv1alpha1.PersistenceConfig{ + Sessions: &kubeopenv1alpha1.VolumePersistence{}, + }, + }, + } + + cfg := agentConfig{ + executorImage: "test-executor:v1.0.0", + agentImage: "test-agent:v1.0.0", + workspaceDir: "/workspace", + } + + deployment := BuildServerDeployment(agent, cfg, defaultSystemConfig(), nil, nil, nil, nil, nil) + if deployment == nil { + t.Fatal("BuildServerDeployment returned nil") + } + + container := deployment.Spec.Template.Spec.Containers[0] + envMap := make(map[string]string, len(container.Env)) + for _, env := range container.Env { + envMap[env.Name] = env.Value + } + + if got := envMap["HOME"]; got != ServerPersistentHomeDir { + t.Errorf("HOME = %q, want %q", got, ServerPersistentHomeDir) + } + if got := envMap["XDG_CONFIG_HOME"]; got != ServerPersistentXDGConfigHome { + t.Errorf("XDG_CONFIG_HOME = %q, want %q", got, ServerPersistentXDGConfigHome) + } + if got := envMap["XDG_DATA_HOME"]; got != ServerPersistentXDGDataHome { + t.Errorf("XDG_DATA_HOME = %q, want %q", got, ServerPersistentXDGDataHome) + } + if got := envMap["XDG_STATE_HOME"]; got != ServerPersistentXDGStateHome { + t.Errorf("XDG_STATE_HOME = %q, want %q", got, ServerPersistentXDGStateHome) + } + if got := envMap["SHELL"]; got != DefaultShell { + t.Errorf("SHELL = %q, want %q", got, DefaultShell) + } +} + func TestBuildServerDeployment_WithTextContext(t *testing.T) { agent := &kubeopenv1alpha1.Agent{ ObjectMeta: metav1.ObjectMeta{ @@ -1085,7 +1133,9 @@ func TestBuildServerDeployment_WithSessionPersistence(t *testing.T) { // Verify OPENCODE_DB env var var foundEnv bool + envMap := make(map[string]string, len(container.Env)) for _, env := range container.Env { + envMap[env.Name] = env.Value if env.Name == OpenCodeDBEnvVar { foundEnv = true if env.Value != ServerSessionDBPath { @@ -1096,6 +1146,18 @@ func TestBuildServerDeployment_WithSessionPersistence(t *testing.T) { if !foundEnv { t.Errorf("%s env var not found", OpenCodeDBEnvVar) } + if got := envMap["HOME"]; got != ServerPersistentHomeDir { + t.Errorf("HOME = %q, want %q", got, ServerPersistentHomeDir) + } + if got := envMap["XDG_CONFIG_HOME"]; got != ServerPersistentXDGConfigHome { + t.Errorf("XDG_CONFIG_HOME = %q, want %q", got, ServerPersistentXDGConfigHome) + } + if got := envMap["XDG_DATA_HOME"]; got != ServerPersistentXDGDataHome { + t.Errorf("XDG_DATA_HOME = %q, want %q", got, ServerPersistentXDGDataHome) + } + if got := envMap["XDG_STATE_HOME"]; got != ServerPersistentXDGStateHome { + t.Errorf("XDG_STATE_HOME = %q, want %q", got, ServerPersistentXDGStateHome) + } } func TestBuildServerDeployment_WithoutSessionPersistence(t *testing.T) { @@ -1129,11 +1191,25 @@ func TestBuildServerDeployment_WithoutSessionPersistence(t *testing.T) { // Verify no OPENCODE_DB env var container := deployment.Spec.Template.Spec.Containers[0] + envMap := make(map[string]string, len(container.Env)) for _, env := range container.Env { + envMap[env.Name] = env.Value if env.Name == OpenCodeDBEnvVar { t.Errorf("%s env var should not be present without persistence config", OpenCodeDBEnvVar) } } + if got := envMap["HOME"]; got != DefaultHomeDir { + t.Errorf("HOME = %q, want %q", got, DefaultHomeDir) + } + if _, ok := envMap["XDG_CONFIG_HOME"]; ok { + t.Errorf("XDG_CONFIG_HOME should not be set without session persistence") + } + if _, ok := envMap["XDG_DATA_HOME"]; ok { + t.Errorf("XDG_DATA_HOME should not be set without session persistence") + } + if _, ok := envMap["XDG_STATE_HOME"]; ok { + t.Errorf("XDG_STATE_HOME should not be set without session persistence") + } } func TestBuildServerWorkspacePVC(t *testing.T) {