Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- 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>
242 lines
7.5 KiB
Go
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")
|
|
}
|
|
}
|