diff --git a/internal/controller/reconciler.go b/internal/controller/reconciler.go index 8a3cde17..51828b6a 100644 --- a/internal/controller/reconciler.go +++ b/internal/controller/reconciler.go @@ -224,7 +224,7 @@ func (r *TaskReconciler) Reconcile(ctx context.Context, task *v1alpha1.Task, wor host = h port = p } - readyURL := fmt.Sprintf("http://%s:%s/readyz?check=workspace", host, port) + readyURL := fmt.Sprintf("http://%s/readyz?check=workspace", net.JoinHostPort(host, port)) // Workspace setup happens once per task. After it has completed, WorkspaceReady stays // True across suspend/resume cycles, so only poll while it is still initializing. workspaceReady := r.conditionTrue(task, condWorkspaceReady) @@ -238,11 +238,13 @@ func (r *TaskReconciler) Reconcile(ctx context.Context, task *v1alpha1.Task, wor checkReady := func() bool { // 1. Direct readyz check - req, _ := http.NewRequestWithContext(pollCtx, http.MethodGet, readyURL, nil) - if resp, err := r.httpClient.Do(req); err == nil { - _ = resp.Body.Close() - if resp.StatusCode == http.StatusOK { - return true + req, err := http.NewRequestWithContext(pollCtx, http.MethodGet, readyURL, nil) + if err == nil { + if resp, err := r.httpClient.Do(req); err == nil { + _ = resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return true + } } } diff --git a/internal/controller/reconciler_test.go b/internal/controller/reconciler_test.go index 94396024..0036024b 100644 --- a/internal/controller/reconciler_test.go +++ b/internal/controller/reconciler_test.go @@ -387,6 +387,52 @@ func TestTaskReconciler_WorkspaceReady(t *testing.T) { } } +func TestTaskReconciler_WorkspaceReadyIPv6(t *testing.T) { + httpLis, err := net.Listen("tcp", "[::1]:0") + if err != nil { + t.Skipf("IPv6 loopback unavailable: %v", err) + } + defer httpLis.Close() + httpServer := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })} + go httpServer.Serve(httpLis) + defer httpServer.Close() + + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer lis.Close() + mockSrv := &mockControlServer{workerIP: httpLis.Addr().String()} + grpcServer := grpc.NewServer() + ateapipb.RegisterControlServer(grpcServer, mockSrv) + go grpcServer.Serve(lis) + defer grpcServer.Stop() + + client, err := substrate.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("failed to create substrate client: %v", err) + } + defer client.Close() + + reconciler := controller.NewTaskReconciler(client, "test-template", "ax-system") + reconciler.SecretResolver = noSecrets + reconciler.WorkspaceReadyTimeout = 200 * time.Millisecond + + task := &v1alpha1.Task{ + ApiVersion: v1alpha1.APIVersion, + Kind: v1alpha1.KindTask, + Metadata: &v1alpha1.ObjectMeta{Name: "ipv6-task", Atespace: "default"}, + Spec: &v1alpha1.TaskSpec{}, + } + reconciled, err := reconciler.Reconcile(context.Background(), task, nil) + if err != nil { + t.Fatalf("Reconcile failed: %v", err) + } + assertCondition(t, reconciled, "WorkspaceReady", "True", "SetupComplete") +} + // assertCondition fails the test unless the task has a condition of the given type with // the expected status and reason. func assertCondition(t *testing.T, task *v1alpha1.Task, condType, wantStatus, wantReason string) {