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
This commit is contained in:
Luiz Oliveira
2026-07-27 12:02:33 -04:00
committed by GitHub
parent aa1d14a7b3
commit 0dd73e1679
5 changed files with 31 additions and 3 deletions
@@ -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
}
@@ -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
})
}
+7
View File
@@ -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
@@ -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: {}
+5 -1
View File
@@ -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`