From 0dd73e1679cf917f5d8829730bd8f6befd1025bf Mon Sep 17 00:00:00 2001 From: Luiz Oliveira Date: Mon, 27 Jul 2026 12:02:33 -0400 Subject: [PATCH] Add pod selector to the WorkerPool scale subresource (#547) WorkerPool already served /scale, so `kubectl scale` worked, but a HPA pointed at one did not: the subresource declared no labelSelectorPath, and HPA needs a selector to find the pods whose metrics it is averaging or to compute ambiguous selectors. https://github.com/agent-substrate/substrate/issues/198 - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- .../internal/controllers/workerpool_controller.go | 11 ++++++++++- .../controllers/workerpool_controller_test.go | 6 +++++- docs/api-guide.md | 7 +++++++ .../ate-install/generated/ate.dev_workerpools.yaml | 4 ++++ pkg/api/v1alpha1/workerpool_types.go | 6 +++++- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/cmd/atecontroller/internal/controllers/workerpool_controller.go b/cmd/atecontroller/internal/controllers/workerpool_controller.go index c8895ff00..8c811290d 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_controller.go +++ b/cmd/atecontroller/internal/controllers/workerpool_controller.go @@ -21,6 +21,7 @@ import ( appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/equality" k8errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -98,7 +99,15 @@ func (r *WorkerPoolReconciler) applyDeployment(ctx context.Context, wp *atev1alp } func (r *WorkerPoolReconciler) syncStatus(ctx context.Context, wp *atev1alpha1.WorkerPool, dep *appsv1.Deployment) error { - want := atev1alpha1.WorkerPoolStatus{Replicas: dep.Status.Replicas} + selector, err := metav1.LabelSelectorAsSelector(dep.Spec.Selector) + if err != nil { + return fmt.Errorf("failed to convert Deployment selector: %w", err) + } + + want := atev1alpha1.WorkerPoolStatus{ + Replicas: dep.Status.Replicas, + Selector: selector.String(), + } if equality.Semantic.DeepEqual(wp.Status, want) { return nil } diff --git a/cmd/atecontroller/internal/controllers/workerpool_controller_test.go b/cmd/atecontroller/internal/controllers/workerpool_controller_test.go index 5acad6714..4b14dd110 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_controller_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_controller_test.go @@ -297,7 +297,8 @@ func TestDeletedDeploymentRecreated(t *testing.T) { } // TestStatusReplicasPropagation verifies that the controller syncs the -// Deployment's status.replicas into WorkerPool.status.replicas. +// Deployment's status.replicas into WorkerPool.status.replicas, and publishes +// the Deployment's pod selector as WorkerPool.status.selector. func TestStatusReplicasPropagation(t *testing.T) { wp := makeWorkerPool("test-status", "default", 3, "ateom:v1") if err := k8sClient.Create(testCtx, wp); err != nil { @@ -320,6 +321,9 @@ func TestStatusReplicasPropagation(t *testing.T) { if err := k8sClient.Get(ctx, types.NamespacedName{Name: wp.Name, Namespace: wp.Namespace}, current); err != nil { return false, nil } + if current.Status.Selector != "ate.dev/worker-pool="+wp.Name { + return false, nil + } return current.Status.Replicas == 3, nil }) } diff --git a/docs/api-guide.md b/docs/api-guide.md index 719d14e8f..5200dc4e1 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -78,6 +78,13 @@ spec: memory: 2Gi ``` +### Status (`WorkerPoolStatus`) + +| Field | Type | Description | +| :--- | :--- | :--- | +| `replicas` | `int32` | Total number of worker pods, mirrored from the managed Deployment. | +| `selector` | `string` | Label selector for the worker pods. | + --- ## 2. ActorTemplate: The Workload Blueprint diff --git a/manifests/ate-install/generated/ate.dev_workerpools.yaml b/manifests/ate-install/generated/ate.dev_workerpools.yaml index 35dee75d3..b9d8c14a6 100644 --- a/manifests/ate-install/generated/ate.dev_workerpools.yaml +++ b/manifests/ate-install/generated/ate.dev_workerpools.yaml @@ -424,6 +424,9 @@ spec: format: int32 minimum: 0 type: integer + selector: + description: Selector is the label selector for the worker pods. + type: string type: object required: - spec @@ -432,6 +435,7 @@ spec: storage: true subresources: scale: + labelSelectorPath: .status.selector specReplicasPath: .spec.replicas statusReplicasPath: .status.replicas status: {} diff --git a/pkg/api/v1alpha1/workerpool_types.go b/pkg/api/v1alpha1/workerpool_types.go index 64a62e9ed..ecd386f2f 100644 --- a/pkg/api/v1alpha1/workerpool_types.go +++ b/pkg/api/v1alpha1/workerpool_types.go @@ -93,6 +93,10 @@ type WorkerPoolStatus struct { // +kubebuilder:validation:Minimum=0 // +optional Replicas int32 `json:"replicas"` + + // Selector is the label selector for the worker pods. + // +optional + Selector string `json:"selector,omitempty"` } // WorkerPool is the Schema for the workerpools API @@ -101,7 +105,7 @@ type WorkerPoolStatus struct { // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Namespaced,shortName=workerpool // +kubebuilder:subresource:status -// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector // +kubebuilder:printcolumn:name="Desired",type=integer,JSONPath=`.spec.replicas` // +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.status.replicas` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`