Skip to content

Commit 9daab25

Browse files
sallyomclaude
andcommitted
Refactor operator into organized packages and fix stop session bug
- Restructure monolithic main.go into internal packages following backend pattern - internal/config: Configuration and K8s client initialization - internal/handlers: Event handlers for sessions, namespaces, and project settings - internal/services: Infrastructure services (PVC, content service) - internal/types: Resource type definitions - Fix stop session bug: operator now properly terminates jobs when sessions marked as "Stopped" - Preserve all existing functionality and behavior - Update module name to ambient-code-operator - Reduce main.go from 1000+ lines to 33 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent acc2c08 commit 9daab25

File tree

8 files changed

+1136
-979
lines changed

8 files changed

+1136
-979
lines changed

components/operator/go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
module research-operator
1+
module ambient-code-operator
22

33
go 1.24.0
44

55
toolchain go1.24.7
66

77
require (
8-
github.com/stretchr/testify v1.10.0
98
k8s.io/api v0.34.0
109
k8s.io/apimachinery v0.34.0
1110
k8s.io/client-go v0.34.0
@@ -29,7 +28,6 @@ require (
2928
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
3029
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
3130
github.com/pkg/errors v0.9.1 // indirect
32-
github.com/pmezard/go-difflib v1.0.0 // indirect
3331
github.com/spf13/pflag v1.0.6 // indirect
3432
github.com/x448/float16 v0.8.4 // indirect
3533
go.yaml.in/yaml/v2 v2.4.2 // indirect
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/client-go/dynamic"
9+
"k8s.io/client-go/kubernetes"
10+
"k8s.io/client-go/rest"
11+
"k8s.io/client-go/tools/clientcmd"
12+
)
13+
14+
var (
15+
K8sClient *kubernetes.Clientset
16+
DynamicClient dynamic.Interface
17+
)
18+
19+
// Config holds the operator configuration
20+
type Config struct {
21+
Namespace string
22+
BackendNamespace string
23+
AmbientCodeRunnerImage string
24+
ContentServiceImage string
25+
ImagePullPolicy corev1.PullPolicy
26+
}
27+
28+
// LoadConfig loads configuration from environment variables
29+
func LoadConfig() *Config {
30+
// Get namespace from environment or use default
31+
namespace := os.Getenv("NAMESPACE")
32+
if namespace == "" {
33+
namespace = "default"
34+
}
35+
36+
// Get backend namespace from environment or use operator namespace
37+
backendNamespace := os.Getenv("BACKEND_NAMESPACE")
38+
if backendNamespace == "" {
39+
backendNamespace = namespace // Default to same namespace as operator
40+
}
41+
42+
// Get ambient-code runner image from environment or use default
43+
ambientCodeRunnerImage := os.Getenv("AMBIENT_CODE_RUNNER_IMAGE")
44+
if ambientCodeRunnerImage == "" {
45+
ambientCodeRunnerImage = "quay.io/ambient_code/vteam_claude_runner:latest"
46+
}
47+
48+
// Image for per-namespace content service (defaults to backend image)
49+
contentServiceImage := os.Getenv("CONTENT_SERVICE_IMAGE")
50+
if contentServiceImage == "" {
51+
contentServiceImage = "quay.io/ambient_code/vteam_backend:latest"
52+
}
53+
54+
// Get image pull policy from environment or use default
55+
imagePullPolicyStr := os.Getenv("IMAGE_PULL_POLICY")
56+
if imagePullPolicyStr == "" {
57+
imagePullPolicyStr = "Always"
58+
}
59+
imagePullPolicy := corev1.PullPolicy(imagePullPolicyStr)
60+
61+
return &Config{
62+
Namespace: namespace,
63+
BackendNamespace: backendNamespace,
64+
AmbientCodeRunnerImage: ambientCodeRunnerImage,
65+
ContentServiceImage: contentServiceImage,
66+
ImagePullPolicy: imagePullPolicy,
67+
}
68+
}
69+
70+
// InitK8sClients initializes the Kubernetes clients
71+
func InitK8sClients() error {
72+
var config *rest.Config
73+
var err error
74+
75+
// Try in-cluster config first
76+
if config, err = rest.InClusterConfig(); err != nil {
77+
// If in-cluster config fails, try kubeconfig
78+
kubeconfig := os.Getenv("KUBECONFIG")
79+
if kubeconfig == "" {
80+
kubeconfig = fmt.Sprintf("%s/.kube/config", os.Getenv("HOME"))
81+
}
82+
83+
if config, err = clientcmd.BuildConfigFromFlags("", kubeconfig); err != nil {
84+
return fmt.Errorf("failed to create Kubernetes config: %v", err)
85+
}
86+
}
87+
88+
// Create standard Kubernetes client
89+
K8sClient, err = kubernetes.NewForConfig(config)
90+
if err != nil {
91+
return fmt.Errorf("failed to create Kubernetes client: %v", err)
92+
}
93+
94+
// Create dynamic client for custom resources
95+
DynamicClient, err = dynamic.NewForConfig(config)
96+
if err != nil {
97+
return fmt.Errorf("failed to create dynamic client: %v", err)
98+
}
99+
100+
return nil
101+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package handlers
2+
3+
import (
4+
"context"
5+
"log"
6+
"time"
7+
8+
"ambient-code-operator/internal/config"
9+
"ambient-code-operator/internal/services"
10+
11+
corev1 "k8s.io/api/core/v1"
12+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14+
"k8s.io/apimachinery/pkg/watch"
15+
)
16+
17+
// WatchNamespaces watches for managed namespaces
18+
func WatchNamespaces() {
19+
for {
20+
watcher, err := config.K8sClient.CoreV1().Namespaces().Watch(context.TODO(), v1.ListOptions{
21+
LabelSelector: "ambient-code.io/managed=true",
22+
})
23+
if err != nil {
24+
log.Printf("Failed to create namespace watcher: %v", err)
25+
time.Sleep(5 * time.Second)
26+
continue
27+
}
28+
29+
log.Println("Watching for managed namespaces...")
30+
31+
for event := range watcher.ResultChan() {
32+
switch event.Type {
33+
case watch.Added:
34+
namespace := event.Object.(*corev1.Namespace)
35+
log.Printf("Detected new managed namespace: %s", namespace.Name)
36+
37+
// Auto-create ProjectSettings for this namespace
38+
if err := CreateDefaultProjectSettings(namespace.Name); err != nil {
39+
log.Printf("Error creating default ProjectSettings for namespace %s: %v", namespace.Name, err)
40+
}
41+
42+
// Ensure shared workspace PVC and content service exist
43+
if err := services.EnsureProjectWorkspacePVC(namespace.Name); err != nil {
44+
log.Printf("Failed to ensure workspace PVC in %s: %v", namespace.Name, err)
45+
}
46+
if err := services.EnsureContentService(namespace.Name); err != nil {
47+
log.Printf("Failed to ensure content service in %s: %v", namespace.Name, err)
48+
}
49+
case watch.Error:
50+
obj := event.Object.(*unstructured.Unstructured)
51+
log.Printf("Watch error for namespaces: %v", obj)
52+
}
53+
}
54+
55+
log.Println("Namespace watch channel closed, restarting...")
56+
watcher.Stop()
57+
time.Sleep(2 * time.Second)
58+
}
59+
}

0 commit comments

Comments
 (0)