rdev/internal/adapter/deployer/deployer_undeploy_test.go
jordan 9226454b85
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: label-based undeploy, GC reconciliation, checkout/sessions, pool status
- Add UndeployAll() using label selectors to clean up monorepo components
  on project deletion (replaces name-based Undeploy in DeleteProject and
  the direct undeploy handler)
- Add ResourceGC background worker that periodically finds K8s resources
  whose project label has no matching DB record, deletes after 1h safety
  window
- Widen deployer client type from *kubernetes.Clientset to
  kubernetes.Interface for testability
- UndeployAll accumulates errors via errors.Join instead of failing fast
- Add checkout/checkin sidecar dev flow: temporary git tokens, branch
  checkout, review on checkin with cleanup workers
- Add interactive sessions: pod binding, command execution, SSE streaming,
  ephemeral preview URLs with session cleanup workers
- Add GET /workers/pool endpoint for aggregate capacity and queue depth
- Add sessions:read and sessions:execute auth scopes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 19:11:28 -07:00

242 lines
7.5 KiB
Go

package deployer
import (
"context"
"testing"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func newTestDeployer(client *fake.Clientset) *Deployer {
return NewDeployer(client, Config{Namespace: "projects"})
}
func createFakeDeployment(t *testing.T, client *fake.Clientset, name, project string) {
t.Helper()
replicas := int32(1)
_, err := client.AppsV1().Deployments("projects").Create(context.Background(), &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "projects",
Labels: map[string]string{"app": name, "project": project},
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": name}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": name}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "test:latest"}}},
},
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("failed to create deployment %s: %v", name, err)
}
}
func createFakeService(t *testing.T, client *fake.Clientset, name, project string) {
t.Helper()
_, err := client.CoreV1().Services("projects").Create(context.Background(), &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "projects",
Labels: map[string]string{"app": name, "project": project},
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{"app": name},
Ports: []corev1.ServicePort{{Port: 8080}},
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("failed to create service %s: %v", name, err)
}
}
func createFakeIngress(t *testing.T, client *fake.Clientset, name, project string) {
t.Helper()
_, err := client.NetworkingV1().Ingresses("projects").Create(context.Background(), &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "projects",
Labels: map[string]string{"app": name, "project": project},
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("failed to create ingress %s: %v", name, err)
}
}
func createFakeSecret(t *testing.T, client *fake.Clientset, name, project string) {
t.Helper()
_, err := client.CoreV1().Secrets("projects").Create(context.Background(), &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "projects",
Labels: map[string]string{"project": project},
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("failed to create secret %s: %v", name, err)
}
}
func TestUndeployAll_SingleApp(t *testing.T) {
client := fake.NewSimpleClientset()
d := newTestDeployer(client)
ctx := context.Background()
createFakeDeployment(t, client, "myapp", "myapp")
createFakeService(t, client, "myapp", "myapp")
createFakeIngress(t, client, "myapp", "myapp")
createFakeSecret(t, client, "myapp-env", "myapp")
if err := d.UndeployAll(ctx, "myapp"); err != nil {
t.Fatalf("UndeployAll failed: %v", err)
}
deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
if len(deps.Items) != 0 {
t.Errorf("expected 0 deployments, got %d", len(deps.Items))
}
svcs, _ := client.CoreV1().Services("projects").List(ctx, metav1.ListOptions{})
if len(svcs.Items) != 0 {
t.Errorf("expected 0 services, got %d", len(svcs.Items))
}
ings, _ := client.NetworkingV1().Ingresses("projects").List(ctx, metav1.ListOptions{})
if len(ings.Items) != 0 {
t.Errorf("expected 0 ingresses, got %d", len(ings.Items))
}
secs, _ := client.CoreV1().Secrets("projects").List(ctx, metav1.ListOptions{})
if len(secs.Items) != 0 {
t.Errorf("expected 0 secrets, got %d", len(secs.Items))
}
}
func TestUndeployAll_MonorepoComponents(t *testing.T) {
client := fake.NewSimpleClientset()
d := newTestDeployer(client)
ctx := context.Background()
// Main project + two components all sharing "project=myapp" label
createFakeDeployment(t, client, "myapp", "myapp")
createFakeDeployment(t, client, "myapp-studio-ui", "myapp")
createFakeDeployment(t, client, "myapp-studio-api", "myapp")
createFakeService(t, client, "myapp", "myapp")
createFakeService(t, client, "myapp-studio-ui", "myapp")
createFakeService(t, client, "myapp-studio-api", "myapp")
createFakeIngress(t, client, "myapp", "myapp")
// Also create a resource for a DIFFERENT project - should not be deleted
createFakeDeployment(t, client, "other-project", "other-project")
createFakeService(t, client, "other-project", "other-project")
if err := d.UndeployAll(ctx, "myapp"); err != nil {
t.Fatalf("UndeployAll failed: %v", err)
}
deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
if len(deps.Items) != 1 {
t.Errorf("expected 1 deployment (other-project), got %d", len(deps.Items))
}
if len(deps.Items) == 1 && deps.Items[0].Name != "other-project" {
t.Errorf("remaining deployment = %q, want %q", deps.Items[0].Name, "other-project")
}
svcs, _ := client.CoreV1().Services("projects").List(ctx, metav1.ListOptions{})
if len(svcs.Items) != 1 {
t.Errorf("expected 1 service (other-project), got %d", len(svcs.Items))
}
ings, _ := client.NetworkingV1().Ingresses("projects").List(ctx, metav1.ListOptions{})
if len(ings.Items) != 0 {
t.Errorf("expected 0 ingresses, got %d", len(ings.Items))
}
}
func TestUndeployAll_IdempotentNoOp(t *testing.T) {
client := fake.NewSimpleClientset()
d := newTestDeployer(client)
ctx := context.Background()
// No resources exist for this project
if err := d.UndeployAll(ctx, "nonexistent"); err != nil {
t.Fatalf("UndeployAll should be idempotent, got: %v", err)
}
}
func TestListProjectLabels(t *testing.T) {
client := fake.NewSimpleClientset()
d := newTestDeployer(client)
ctx := context.Background()
createFakeDeployment(t, client, "proj-a", "proj-a")
createFakeDeployment(t, client, "proj-b", "proj-b")
createFakeDeployment(t, client, "proj-b-ui", "proj-b")
createFakeDeployment(t, client, "proj-b-api", "proj-b")
labels, err := d.ListProjectLabels(ctx)
if err != nil {
t.Fatalf("ListProjectLabels failed: %v", err)
}
// Should have exactly 2 unique labels
if len(labels) != 2 {
t.Fatalf("expected 2 unique labels, got %d: %v", len(labels), labels)
}
found := make(map[string]bool)
for _, l := range labels {
found[l] = true
}
if !found["proj-a"] || !found["proj-b"] {
t.Errorf("expected proj-a and proj-b, got %v", labels)
}
}
func TestListProjectLabels_Empty(t *testing.T) {
client := fake.NewSimpleClientset()
d := newTestDeployer(client)
labels, err := d.ListProjectLabels(context.Background())
if err != nil {
t.Fatalf("ListProjectLabels failed: %v", err)
}
if len(labels) != 0 {
t.Errorf("expected 0 labels, got %d", len(labels))
}
}
func TestGetOldestResourceTime(t *testing.T) {
client := fake.NewSimpleClientset()
d := newTestDeployer(client)
ctx := context.Background()
createFakeDeployment(t, client, "myapp", "myapp")
createFakeDeployment(t, client, "myapp-ui", "myapp")
_, found, err := d.GetOldestResourceTime(ctx, "myapp")
if err != nil {
t.Fatalf("GetOldestResourceTime failed: %v", err)
}
if !found {
t.Fatal("expected found=true")
}
}
func TestGetOldestResourceTime_NoResources(t *testing.T) {
client := fake.NewSimpleClientset()
d := newTestDeployer(client)
_, found, err := d.GetOldestResourceTime(context.Background(), "nonexistent")
if err != nil {
t.Fatalf("GetOldestResourceTime failed: %v", err)
}
if found {
t.Error("expected found=false for nonexistent project")
}
}