diff --git a/api/v1alpha1/worker_types.go b/api/v1alpha1/worker_types.go index c557b9f5..a9a12cc6 100644 --- a/api/v1alpha1/worker_types.go +++ b/api/v1alpha1/worker_types.go @@ -108,6 +108,17 @@ type TemporalWorkerDeploymentStatus struct { // so it's generally not a good idea to read from the status of the root object. // Instead, you should reconstruct it every run. + // Replicas is the total number of non-terminated pods targeted by this TemporalWorkerDeployment. + // This is used by the /scale subresource to report current replica count to HPA/KEDA. + // +optional + Replicas int32 `json:"replicas,omitempty"` + + // Selector is the label selector for pods managed by this TemporalWorkerDeployment. + // This is used by the /scale subresource to allow HPA/KEDA to discover pods. + // Format: "app.kubernetes.io/name=" + // +optional + Selector string `json:"selector,omitempty"` + // TargetVersion is the desired next version. If TargetVersion.Deployment is nil, // then the controller should create it. If not nil, the controller should // wait for it to become healthy and then move it to the CurrentVersion. @@ -348,7 +359,9 @@ type ManualRolloutStrategy struct{} //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector // +kubebuilder:resource:shortName=twd;twdeployment;tworkerdeployment +//+kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".status.replicas",description="Current replicas" //+kubebuilder:printcolumn:name="Current",type="string",JSONPath=".status.currentVersion.buildID",description="Current build ID" //+kubebuilder:printcolumn:name="Target",type="string",JSONPath=".status.targetVersion.buildID",description="Target build ID" //+kubebuilder:printcolumn:name="Ramp %",type="number",JSONPath=".status.targetVersion.rampPercentage",description="Ramp percentage" diff --git a/helm/temporal-worker-controller/crds/temporal.io_temporalworkerdeployments.yaml b/helm/temporal-worker-controller/crds/temporal.io_temporalworkerdeployments.yaml index db9e987c..230b8094 100644 --- a/helm/temporal-worker-controller/crds/temporal.io_temporalworkerdeployments.yaml +++ b/helm/temporal-worker-controller/crds/temporal.io_temporalworkerdeployments.yaml @@ -19,6 +19,10 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: + - description: Current replicas + jsonPath: .status.replicas + name: Replicas + type: integer - description: Current build ID jsonPath: .status.currentVersion.buildID name: Current @@ -64,7 +68,6 @@ spec: gate: properties: input: - type: object x-kubernetes-preserve-unknown-fields: true inputFrom: properties: @@ -73,25 +76,27 @@ spec: key: type: string name: + default: "" type: string optional: type: boolean required: - key - - name type: object + x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: + default: "" type: string optional: type: boolean required: - key - - name type: object + x-kubernetes-map-type: atomic type: object workflowType: type: string @@ -4057,6 +4062,11 @@ spec: type: array lastModifierIdentity: type: string + replicas: + format: int32 + type: integer + selector: + type: string targetVersion: properties: buildID: @@ -4141,4 +4151,8 @@ spec: served: true storage: true subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} diff --git a/internal/controller/state_mapper.go b/internal/controller/state_mapper.go index 804d92a4..b82ed8cb 100644 --- a/internal/controller/state_mapper.go +++ b/internal/controller/state_mapper.go @@ -5,6 +5,8 @@ package controller import ( + "strings" + "github.com/temporalio/temporal-worker-controller/api/v1alpha1" "github.com/temporalio/temporal-worker-controller/internal/k8s" "github.com/temporalio/temporal-worker-controller/internal/temporal" @@ -35,6 +37,25 @@ func (m *stateMapper) mapToStatus(targetBuildID string) *v1alpha1.TemporalWorker status.LastModifierIdentity = m.temporalState.LastModifierIdentity + // Compute total replicas from all managed deployments for /scale subresource + // This allows HPA/KEDA to query the current replica count + var totalReplicas int32 + for _, deployment := range m.k8sState.Deployments { + if deployment.Status.Replicas > 0 { + totalReplicas += deployment.Status.Replicas + } + } + status.Replicas = totalReplicas + + // Set label selector for /scale subresource - allows HPA/KEDA to discover pods + // Uses app.kubernetes.io/name label since all managed pods share this label + // Note: workerDeploymentName is namespace/name, but we only want the name part for labels + name := m.workerDeploymentName + if idx := strings.LastIndex(name, "/"); idx >= 0 { + name = name[idx+1:] + } + status.Selector = "app.kubernetes.io/name=" + name + // Get build IDs directly from temporal state currentBuildID := m.temporalState.CurrentBuildID rampingBuildID := m.temporalState.RampingBuildID