From a2a50923eff7d0c3f29d1ea5af5755724b68e12a Mon Sep 17 00:00:00 2001
From: Alex Savanovich <40720931+savme@users.noreply.github.com>
Date: Wed, 29 Jul 2026 20:14:54 +0300
Subject: [PATCH] feat: add /scale subresource to WorkloadDeployment
---
api/v1alpha/workloaddeployment_types.go | 12 +
api/v1alpha/zz_generated.deepcopy.go | 5 +
...ute.datumapis.com_workloaddeployments.yaml | 14 +
docs/api/instances.md | 375 +++++++++++++++-
docs/api/workloaddeployments.md | 398 ++++++++++++++++-
docs/api/workloads.md | 415 +++++++++++++++++-
.../instancecontrol/instancecontrol.go | 1 +
.../stateful/stateful_control.go | 5 +-
.../stateful/stateful_control_test.go | 49 ++-
internal/controller/teardown.go | 3 +
internal/controller/workload_controller.go | 1 +
.../controller/workload_controller_test.go | 48 ++
.../workloaddeployment_controller.go | 32 +-
.../workloaddeployment_controller_test.go | 110 ++++-
.../workloaddeployment_location_test.go | 1 +
15 files changed, 1422 insertions(+), 47 deletions(-)
diff --git a/api/v1alpha/workloaddeployment_types.go b/api/v1alpha/workloaddeployment_types.go
index 7da6bf45..830aea25 100644
--- a/api/v1alpha/workloaddeployment_types.go
+++ b/api/v1alpha/workloaddeployment_types.go
@@ -33,6 +33,12 @@ type WorkloadDeploymentSpec struct {
//
// +kubebuilder:validation:Required
ScaleSettings HorizontalScaleSettings `json:"scaleSettings"`
+
+ // Replicas is the current desired replica target for this deployment. When
+ // unset, the deployment reconciles to scaleSettings.minReplicas.
+ //
+ // +kubebuilder:validation:Optional
+ Replicas *int32 `json:"replicas,omitempty"`
}
// WorkloadDeploymentStatus defines the observed state of WorkloadDeployment
@@ -65,6 +71,11 @@ type WorkloadDeploymentStatus struct {
// The number of instances which are ready.
ReadyReplicas int32 `json:"readyReplicas"`
+ // Selector is the label selector that identifies Pods backing this deployment.
+ //
+ // +kubebuilder:validation:Optional
+ Selector string `json:"selector,omitempty"`
+
// The most recent generation observed by the deployment controller. When
// this matches metadata.generation, the controller has reconciled the
// latest spec (e.g. a restart request).
@@ -84,6 +95,7 @@ const (
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
+// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector
// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project"
// WorkloadDeployment is the Schema for the workloaddeployments API
diff --git a/api/v1alpha/zz_generated.deepcopy.go b/api/v1alpha/zz_generated.deepcopy.go
index ca5b2830..65d1e1e9 100644
--- a/api/v1alpha/zz_generated.deepcopy.go
+++ b/api/v1alpha/zz_generated.deepcopy.go
@@ -984,6 +984,11 @@ func (in *WorkloadDeploymentSpec) DeepCopyInto(out *WorkloadDeploymentSpec) {
out.WorkloadRef = in.WorkloadRef
in.Template.DeepCopyInto(&out.Template)
in.ScaleSettings.DeepCopyInto(&out.ScaleSettings)
+ if in.Replicas != nil {
+ in, out := &in.Replicas, &out.Replicas
+ *out = new(int32)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadDeploymentSpec.
diff --git a/config/base/crd/bases/compute.datumapis.com_workloaddeployments.yaml b/config/base/crd/bases/compute.datumapis.com_workloaddeployments.yaml
index 777d3324..97afd204 100644
--- a/config/base/crd/bases/compute.datumapis.com_workloaddeployments.yaml
+++ b/config/base/crd/bases/compute.datumapis.com_workloaddeployments.yaml
@@ -78,6 +78,12 @@ spec:
placementName:
description: The placement in the workload which is driving a deployment
type: string
+ replicas:
+ description: |-
+ Replicas is the current desired replica target for this deployment. When
+ unset, the deployment reconciles to scaleSettings.minReplicas.
+ format: int32
+ type: integer
scaleSettings:
description: Scale settings such as minimum and maximum replica counts.
properties:
@@ -1173,6 +1179,10 @@ spec:
description: The number of instances created
format: int32
type: integer
+ selector:
+ description: Selector is the label selector that identifies Pods backing
+ this deployment.
+ type: string
updatedReplicas:
description: |-
The number of instances updated to the latest template revision, i.e.
@@ -1192,4 +1202,8 @@ spec:
served: true
storage: true
subresources:
+ scale:
+ labelSelectorPath: .status.selector
+ specReplicasPath: .spec.replicas
+ statusReplicasPath: .status.replicas
status: {}
diff --git a/docs/api/instances.md b/docs/api/instances.md
index 580ab30c..b603ad99 100644
--- a/docs/api/instances.md
+++ b/docs/api/instances.md
@@ -53,14 +53,16 @@ Instance is the Schema for the instances API
spec |
object |
- InstanceSpec defines the desired state of Instance
+ Spec defines the desired state of an Instance.
|
false |
| status |
object |
- InstanceStatus defines the observed state of Instance
+ Status defines the current state of an Instance.
+
+ Default: map[conditions:[map[lastTransitionTime:1970-01-01T00:00:00Z message:Waiting for controller reason:Pending status:Unknown type:Programmed] map[lastTransitionTime:1970-01-01T00:00:00Z message:Waiting for controller reason:Pending status:Unknown type:Available] map[lastTransitionTime:1970-01-01T00:00:00Z message:Waiting for controller reason:Pending status:Unknown type:Ready] map[lastTransitionTime:1970-01-01T00:00:00Z message:Waiting for quota evaluation reason:PendingEvaluation status:Unknown type:QuotaGranted]]]
|
false |
@@ -72,7 +74,7 @@ Instance is the Schema for the instances API
-InstanceSpec defines the desired state of Instance
+Spec defines the desired state of an Instance.
@@ -97,11 +99,26 @@ InstanceSpec defines the desired state of Instance
The runtime type of the instance, such as a container sandbox or a VM.
true |
+
+ | controller |
+ object |
+
+ Controller contains settings driven by the controller managing the instance.
+ |
+ false |
+
+ | location |
+ object |
+
+ The location which the instance has been scheduled to
+ |
+ false |
| volumes |
[]object |
-
+ Volumes that must be available to attach to an instance's containers or
+Virtual Machine.
|
false |
@@ -536,6 +553,30 @@ used by the instance.
The name of the container.
true |
+
+ | args |
+ []string |
+
+ Arguments to the entrypoint, overriding the image's CMD. Combined with
+Command: when Command is also set the resulting invocation is
+append(Command, Args...). When only Args is set it overrides CMD while
+preserving the image's ENTRYPOINT.
+
+If neither Command nor Args is set, the image's own ENTRYPOINT and CMD
+are used unchanged.
+ |
+ false |
+
+ | command |
+ []string |
+
+ Entrypoint array to run in the container image, overriding the image's
+ENTRYPOINT. Each element is a separate token, not a shell command — to run a
+shell command use: ["sh", "-c", "my command"].
+
+If not provided, the container image's own ENTRYPOINT is used.
+ |
+ false |
| env |
[]object |
@@ -545,6 +586,18 @@ used by the instance.
so replicate the structure here too.
false |
+
+ | envFrom |
+ []object |
+
+ List of sources to populate environment variables in the container.
+The keys defined within a source must be a C_IDENTIFIER. All invalid
+keys will be reported as an event when the container is starting. When a
+key exists in multiple sources, the value associated with the last source
+will take precedence. Values defined by an Env with a duplicate key will
+take precedence.
+ |
+ false |
| ports |
[]object |
@@ -590,7 +643,8 @@ EnvVar represents an environment variable present in a Container.
name |
string |
- Name of the environment variable. Must be a C_IDENTIFIER.
+ Name of the environment variable.
+May consist of any printable ASCII characters except '='.
|
true |
@@ -650,6 +704,14 @@ Source for the environment variable's value. Cannot be used if value is not empt
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
false |
+
+ | fileKeyRef |
+ object |
+
+ FileKeyRef selects a key of the env file.
+Requires the EnvFiles feature gate to be enabled.
+ |
+ false |
| resourceFieldRef |
object |
@@ -751,6 +813,66 @@ spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podI
+### Instance.spec.runtime.sandbox.containers[index].env[index].valueFrom.fileKeyRef
+[↩ Parent](#instancespecruntimesandboxcontainersindexenvindexvaluefrom)
+
+
+
+FileKeyRef selects a key of the env file.
+Requires the EnvFiles feature gate to be enabled.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | key |
+ string |
+
+ The key within the env file. An invalid key will prevent the pod from starting.
+The keys defined within a source may consist of any printable ASCII characters except '='.
+During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.
+ |
+ true |
+
+ | path |
+ string |
+
+ The path within the volume from which to select the file.
+Must be relative and may not contain the '..' path or start with '..'.
+ |
+ true |
+
+ | volumeName |
+ string |
+
+ The name of the volume mount containing the env file.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the file or its key must be defined. If the file or key
+does not exist, then the env var is not published.
+If optional is set to true and the specified key does not exist,
+the environment variable will not be set in the Pod's containers.
+
+If optional is set to false and the specified key does not exist,
+an error will be returned during Pod creation.
+
+ Default: false
+ |
+ false |
+
+
+
+
### Instance.spec.runtime.sandbox.containers[index].env[index].valueFrom.resourceFieldRef
[↩ Parent](#instancespecruntimesandboxcontainersindexenvindexvaluefrom)
@@ -840,6 +962,117 @@ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/nam
+### Instance.spec.runtime.sandbox.containers[index].envFrom[index]
+[↩ Parent](#instancespecruntimesandboxcontainersindex)
+
+
+
+EnvFromSource represents a source for a set of ConfigMaps or Secrets to be
+used as environment variables in a container.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | configMapRef |
+ object |
+
+ The ConfigMap to select from.
+ |
+ false |
+
+ | prefix |
+ string |
+
+ An optional identifier to prepend to each key in the referenced
+ConfigMap or Secret. Must be a valid C_IDENTIFIER.
+ |
+ false |
+
+ | secretRef |
+ object |
+
+ The Secret to select from.
+ |
+ false |
+
+
+
+
+### Instance.spec.runtime.sandbox.containers[index].envFrom[index].configMapRef
+[↩ Parent](#instancespecruntimesandboxcontainersindexenvfromindex)
+
+
+
+The ConfigMap to select from.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of the ConfigMap in the same namespace as the Workload.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the ConfigMap must be defined.
+ |
+ false |
+
+
+
+
+### Instance.spec.runtime.sandbox.containers[index].envFrom[index].secretRef
+[↩ Parent](#instancespecruntimesandboxcontainersindexenvfromindex)
+
+
+
+The Secret to select from.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of the Secret in the same namespace as the Workload.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the Secret must be defined.
+ |
+ false |
+
+
+
+
### Instance.spec.runtime.sandbox.containers[index].ports[index]
[↩ Parent](#instancespecruntimesandboxcontainersindex)
@@ -1108,6 +1341,102 @@ If not specified, this field defaults to TCP.
+### Instance.spec.controller
+[↩ Parent](#instancespec)
+
+
+
+Controller contains settings driven by the controller managing the instance.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | templateHash |
+ string |
+
+ TemplateHash is the hash of the instance template applied for this instance.
+ |
+ true |
+
+ | schedulingGates |
+ []object |
+
+ SchedulingGates is a list of gates that must be satisfied before the
+instance can be scheduled.
+ |
+ false |
+
+
+
+
+### Instance.spec.controller.schedulingGates[index]
+[↩ Parent](#instancespeccontroller)
+
+
+
+
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ The name of the gate.
+ |
+ true |
+
+
+
+
+### Instance.spec.location
+[↩ Parent](#instancespec)
+
+
+
+The location which the instance has been scheduled to
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of a datum location
+ |
+ true |
+
+ | namespace |
+ string |
+
+ Namespace for the datum location
+ |
+ true |
+
+
+
+
### Instance.spec.volumes[index]
[↩ Parent](#instancespec)
@@ -1690,7 +2019,7 @@ mode, like fsGroup, and the result can be other mode bits set.
-InstanceStatus defines the observed state of Instance
+Status defines the current state of an Instance.
@@ -1709,6 +2038,13 @@ InstanceStatus defines the observed state of Instance
Known condition types are: "Available", "Progressing"
false |
+
+ | controller |
+ object |
+
+ Controller contains status information about the controller managing the instance.
+ |
+ false |
| networkInterfaces |
[]object |
@@ -1797,6 +2133,33 @@ with respect to the current state of the instance.
+### Instance.status.controller
+[↩ Parent](#instancestatus)
+
+
+
+Controller contains status information about the controller managing the instance.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | observedTemplateHash |
+ string |
+
+ ObservedTemplateHash is the hash of the instance template applied for this instance.
+ |
+ true |
+
+
+
+
### Instance.status.networkInterfaces[index]
[↩ Parent](#instancestatus)
diff --git a/docs/api/workloaddeployments.md b/docs/api/workloaddeployments.md
index 63b905d1..e3e5b079 100644
--- a/docs/api/workloaddeployments.md
+++ b/docs/api/workloaddeployments.md
@@ -118,6 +118,16 @@ WorkloadDeploymentSpec defines the desired state of WorkloadDeployment
The workload that a deployment belongs to
true |
+
+ | replicas |
+ integer |
+
+ Replicas is the current desired replica target for this deployment. When
+unset, the deployment reconciles to scaleSettings.minReplicas.
+
+ Format: int32
+ |
+ false |
@@ -139,6 +149,16 @@ Scale settings such as minimum and maximum replica counts.
+ | instanceManagementPolicy |
+ string |
+
+ Controls how instances are managed during scale up and down, as well as
+during maintenance events.
+
+ Default: OrderedReady
+ |
+ true |
+
| minReplicas |
integer |
@@ -338,11 +358,26 @@ Describes the desired configuration of an instance
The runtime type of the instance, such as a container sandbox or a VM.
|
true |
+
+ | controller |
+ object |
+
+ Controller contains settings driven by the controller managing the instance.
+ |
+ false |
+
+ | location |
+ object |
+
+ The location which the instance has been scheduled to
+ |
+ false |
| volumes |
[]object |
-
+ Volumes that must be available to attach to an instance's containers or
+Virtual Machine.
|
false |
@@ -777,6 +812,30 @@ used by the instance.
The name of the container.
true |
+
+ | args |
+ []string |
+
+ Arguments to the entrypoint, overriding the image's CMD. Combined with
+Command: when Command is also set the resulting invocation is
+append(Command, Args...). When only Args is set it overrides CMD while
+preserving the image's ENTRYPOINT.
+
+If neither Command nor Args is set, the image's own ENTRYPOINT and CMD
+are used unchanged.
+ |
+ false |
+
+ | command |
+ []string |
+
+ Entrypoint array to run in the container image, overriding the image's
+ENTRYPOINT. Each element is a separate token, not a shell command — to run a
+shell command use: ["sh", "-c", "my command"].
+
+If not provided, the container image's own ENTRYPOINT is used.
+ |
+ false |
| env |
[]object |
@@ -786,6 +845,18 @@ used by the instance.
so replicate the structure here too.
false |
+
+ | envFrom |
+ []object |
+
+ List of sources to populate environment variables in the container.
+The keys defined within a source must be a C_IDENTIFIER. All invalid
+keys will be reported as an event when the container is starting. When a
+key exists in multiple sources, the value associated with the last source
+will take precedence. Values defined by an Env with a duplicate key will
+take precedence.
+ |
+ false |
| ports |
[]object |
@@ -831,7 +902,8 @@ EnvVar represents an environment variable present in a Container.
name |
string |
- Name of the environment variable. Must be a C_IDENTIFIER.
+ Name of the environment variable.
+May consist of any printable ASCII characters except '='.
|
true |
@@ -891,6 +963,14 @@ Source for the environment variable's value. Cannot be used if value is not empt
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
false |
+
+ | fileKeyRef |
+ object |
+
+ FileKeyRef selects a key of the env file.
+Requires the EnvFiles feature gate to be enabled.
+ |
+ false |
| resourceFieldRef |
object |
@@ -992,6 +1072,66 @@ spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podI
+### WorkloadDeployment.spec.template.spec.runtime.sandbox.containers[index].env[index].valueFrom.fileKeyRef
+[↩ Parent](#workloaddeploymentspectemplatespecruntimesandboxcontainersindexenvindexvaluefrom)
+
+
+
+FileKeyRef selects a key of the env file.
+Requires the EnvFiles feature gate to be enabled.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | key |
+ string |
+
+ The key within the env file. An invalid key will prevent the pod from starting.
+The keys defined within a source may consist of any printable ASCII characters except '='.
+During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.
+ |
+ true |
+
+ | path |
+ string |
+
+ The path within the volume from which to select the file.
+Must be relative and may not contain the '..' path or start with '..'.
+ |
+ true |
+
+ | volumeName |
+ string |
+
+ The name of the volume mount containing the env file.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the file or its key must be defined. If the file or key
+does not exist, then the env var is not published.
+If optional is set to true and the specified key does not exist,
+the environment variable will not be set in the Pod's containers.
+
+If optional is set to false and the specified key does not exist,
+an error will be returned during Pod creation.
+
+ Default: false
+ |
+ false |
+
+
+
+
### WorkloadDeployment.spec.template.spec.runtime.sandbox.containers[index].env[index].valueFrom.resourceFieldRef
[↩ Parent](#workloaddeploymentspectemplatespecruntimesandboxcontainersindexenvindexvaluefrom)
@@ -1081,6 +1221,117 @@ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/nam
+### WorkloadDeployment.spec.template.spec.runtime.sandbox.containers[index].envFrom[index]
+[↩ Parent](#workloaddeploymentspectemplatespecruntimesandboxcontainersindex)
+
+
+
+EnvFromSource represents a source for a set of ConfigMaps or Secrets to be
+used as environment variables in a container.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | configMapRef |
+ object |
+
+ The ConfigMap to select from.
+ |
+ false |
+
+ | prefix |
+ string |
+
+ An optional identifier to prepend to each key in the referenced
+ConfigMap or Secret. Must be a valid C_IDENTIFIER.
+ |
+ false |
+
+ | secretRef |
+ object |
+
+ The Secret to select from.
+ |
+ false |
+
+
+
+
+### WorkloadDeployment.spec.template.spec.runtime.sandbox.containers[index].envFrom[index].configMapRef
+[↩ Parent](#workloaddeploymentspectemplatespecruntimesandboxcontainersindexenvfromindex)
+
+
+
+The ConfigMap to select from.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of the ConfigMap in the same namespace as the Workload.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the ConfigMap must be defined.
+ |
+ false |
+
+
+
+
+### WorkloadDeployment.spec.template.spec.runtime.sandbox.containers[index].envFrom[index].secretRef
+[↩ Parent](#workloaddeploymentspectemplatespecruntimesandboxcontainersindexenvfromindex)
+
+
+
+The Secret to select from.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of the Secret in the same namespace as the Workload.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the Secret must be defined.
+ |
+ false |
+
+
+
+
### WorkloadDeployment.spec.template.spec.runtime.sandbox.containers[index].ports[index]
[↩ Parent](#workloaddeploymentspectemplatespecruntimesandboxcontainersindex)
@@ -1349,6 +1600,102 @@ If not specified, this field defaults to TCP.
+### WorkloadDeployment.spec.template.spec.controller
+[↩ Parent](#workloaddeploymentspectemplatespec)
+
+
+
+Controller contains settings driven by the controller managing the instance.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | templateHash |
+ string |
+
+ TemplateHash is the hash of the instance template applied for this instance.
+ |
+ true |
+
+ | schedulingGates |
+ []object |
+
+ SchedulingGates is a list of gates that must be satisfied before the
+instance can be scheduled.
+ |
+ false |
+
+
+
+
+### WorkloadDeployment.spec.template.spec.controller.schedulingGates[index]
+[↩ Parent](#workloaddeploymentspectemplatespeccontroller)
+
+
+
+
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ The name of the gate.
+ |
+ true |
+
+
+
+
+### WorkloadDeployment.spec.template.spec.location
+[↩ Parent](#workloaddeploymentspectemplatespec)
+
+
+
+The location which the instance has been scheduled to
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of a datum location
+ |
+ true |
+
+ | namespace |
+ string |
+
+ Namespace for the datum location
+ |
+ true |
+
+
+
+
### WorkloadDeployment.spec.template.spec.volumes[index]
[↩ Parent](#workloaddeploymentspectemplatespec)
@@ -2035,8 +2382,8 @@ WorkloadDeploymentStatus defines the observed state of WorkloadDeployment
currentReplicas |
integer |
- The number of instances created by a deployment and have the latest
-deployment generation settings applied.
+ The number of instances which have the latest workload settings applied
+and are programmed (a subset of UpdatedReplicas that are ready to serve).
Format: int32
|
@@ -2045,7 +2392,16 @@ deployment generation settings applied.
desiredReplicas |
integer |
- The desired number of instances to be managed by a deployment.
+ The desired number of instances
+
+ Format: int32
+ |
+ true |
+
+ | readyReplicas |
+ integer |
+
+ The number of instances which are ready.
Format: int32
|
@@ -2054,7 +2410,19 @@ deployment generation settings applied.
replicas |
integer |
- The number of instances created by a deployment
+ The number of instances created
+
+ Format: int32
+ |
+ true |
+
+ | updatedReplicas |
+ integer |
+
+ The number of instances updated to the latest template revision, i.e.
+whose observed template hash matches the desired template, regardless of
+readiness. Lags Replicas during a rolling update or restart, then catches
+back up — making an in-progress roll observable.
Format: int32
|
@@ -2074,6 +2442,24 @@ Known condition types are: "Available", "Progressing"
The location which the deployment has been scheduled to
false |
+
+ | observedGeneration |
+ integer |
+
+ The most recent generation observed by the deployment controller. When
+this matches metadata.generation, the controller has reconciled the
+latest spec (e.g. a restart request).
+
+ Format: int64
+ |
+ false |
+
+ | selector |
+ string |
+
+ Selector is the label selector that identifies Pods backing this deployment.
+ |
+ false |
diff --git a/docs/api/workloads.md b/docs/api/workloads.md
index c7742714..df10dcc0 100644
--- a/docs/api/workloads.md
+++ b/docs/api/workloads.md
@@ -160,6 +160,16 @@ Scale settings such as minimum and maximum replica counts.
+ | instanceManagementPolicy |
+ string |
+
+ Controls how instances are managed during scale up and down, as well as
+during maintenance events.
+
+ Default: OrderedReady
+ |
+ true |
+
| minReplicas |
integer |
@@ -359,11 +369,26 @@ Describes the desired configuration of an instance
The runtime type of the instance, such as a container sandbox or a VM.
|
true |
+
+ | controller |
+ object |
+
+ Controller contains settings driven by the controller managing the instance.
+ |
+ false |
+
+ | location |
+ object |
+
+ The location which the instance has been scheduled to
+ |
+ false |
| volumes |
[]object |
-
+ Volumes that must be available to attach to an instance's containers or
+Virtual Machine.
|
false |
@@ -798,6 +823,30 @@ used by the instance.
The name of the container.
true |
+
+ | args |
+ []string |
+
+ Arguments to the entrypoint, overriding the image's CMD. Combined with
+Command: when Command is also set the resulting invocation is
+append(Command, Args...). When only Args is set it overrides CMD while
+preserving the image's ENTRYPOINT.
+
+If neither Command nor Args is set, the image's own ENTRYPOINT and CMD
+are used unchanged.
+ |
+ false |
+
+ | command |
+ []string |
+
+ Entrypoint array to run in the container image, overriding the image's
+ENTRYPOINT. Each element is a separate token, not a shell command — to run a
+shell command use: ["sh", "-c", "my command"].
+
+If not provided, the container image's own ENTRYPOINT is used.
+ |
+ false |
| env |
[]object |
@@ -807,6 +856,18 @@ used by the instance.
so replicate the structure here too.
false |
+
+ | envFrom |
+ []object |
+
+ List of sources to populate environment variables in the container.
+The keys defined within a source must be a C_IDENTIFIER. All invalid
+keys will be reported as an event when the container is starting. When a
+key exists in multiple sources, the value associated with the last source
+will take precedence. Values defined by an Env with a duplicate key will
+take precedence.
+ |
+ false |
| ports |
[]object |
@@ -852,7 +913,8 @@ EnvVar represents an environment variable present in a Container.
name |
string |
- Name of the environment variable. Must be a C_IDENTIFIER.
+ Name of the environment variable.
+May consist of any printable ASCII characters except '='.
|
true |
@@ -912,6 +974,14 @@ Source for the environment variable's value. Cannot be used if value is not empt
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
false |
+
+ | fileKeyRef |
+ object |
+
+ FileKeyRef selects a key of the env file.
+Requires the EnvFiles feature gate to be enabled.
+ |
+ false |
| resourceFieldRef |
object |
@@ -1013,6 +1083,66 @@ spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podI
+### Workload.spec.template.spec.runtime.sandbox.containers[index].env[index].valueFrom.fileKeyRef
+[↩ Parent](#workloadspectemplatespecruntimesandboxcontainersindexenvindexvaluefrom)
+
+
+
+FileKeyRef selects a key of the env file.
+Requires the EnvFiles feature gate to be enabled.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | key |
+ string |
+
+ The key within the env file. An invalid key will prevent the pod from starting.
+The keys defined within a source may consist of any printable ASCII characters except '='.
+During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.
+ |
+ true |
+
+ | path |
+ string |
+
+ The path within the volume from which to select the file.
+Must be relative and may not contain the '..' path or start with '..'.
+ |
+ true |
+
+ | volumeName |
+ string |
+
+ The name of the volume mount containing the env file.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the file or its key must be defined. If the file or key
+does not exist, then the env var is not published.
+If optional is set to true and the specified key does not exist,
+the environment variable will not be set in the Pod's containers.
+
+If optional is set to false and the specified key does not exist,
+an error will be returned during Pod creation.
+
+ Default: false
+ |
+ false |
+
+
+
+
### Workload.spec.template.spec.runtime.sandbox.containers[index].env[index].valueFrom.resourceFieldRef
[↩ Parent](#workloadspectemplatespecruntimesandboxcontainersindexenvindexvaluefrom)
@@ -1102,6 +1232,117 @@ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/nam
+### Workload.spec.template.spec.runtime.sandbox.containers[index].envFrom[index]
+[↩ Parent](#workloadspectemplatespecruntimesandboxcontainersindex)
+
+
+
+EnvFromSource represents a source for a set of ConfigMaps or Secrets to be
+used as environment variables in a container.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | configMapRef |
+ object |
+
+ The ConfigMap to select from.
+ |
+ false |
+
+ | prefix |
+ string |
+
+ An optional identifier to prepend to each key in the referenced
+ConfigMap or Secret. Must be a valid C_IDENTIFIER.
+ |
+ false |
+
+ | secretRef |
+ object |
+
+ The Secret to select from.
+ |
+ false |
+
+
+
+
+### Workload.spec.template.spec.runtime.sandbox.containers[index].envFrom[index].configMapRef
+[↩ Parent](#workloadspectemplatespecruntimesandboxcontainersindexenvfromindex)
+
+
+
+The ConfigMap to select from.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of the ConfigMap in the same namespace as the Workload.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the ConfigMap must be defined.
+ |
+ false |
+
+
+
+
+### Workload.spec.template.spec.runtime.sandbox.containers[index].envFrom[index].secretRef
+[↩ Parent](#workloadspectemplatespecruntimesandboxcontainersindexenvfromindex)
+
+
+
+The Secret to select from.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of the Secret in the same namespace as the Workload.
+ |
+ true |
+
+ | optional |
+ boolean |
+
+ Specify whether the Secret must be defined.
+ |
+ false |
+
+
+
+
### Workload.spec.template.spec.runtime.sandbox.containers[index].ports[index]
[↩ Parent](#workloadspectemplatespecruntimesandboxcontainersindex)
@@ -1370,6 +1611,102 @@ If not specified, this field defaults to TCP.
+### Workload.spec.template.spec.controller
+[↩ Parent](#workloadspectemplatespec)
+
+
+
+Controller contains settings driven by the controller managing the instance.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | templateHash |
+ string |
+
+ TemplateHash is the hash of the instance template applied for this instance.
+ |
+ true |
+
+ | schedulingGates |
+ []object |
+
+ SchedulingGates is a list of gates that must be satisfied before the
+instance can be scheduled.
+ |
+ false |
+
+
+
+
+### Workload.spec.template.spec.controller.schedulingGates[index]
+[↩ Parent](#workloadspectemplatespeccontroller)
+
+
+
+
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ The name of the gate.
+ |
+ true |
+
+
+
+
+### Workload.spec.template.spec.location
+[↩ Parent](#workloadspectemplatespec)
+
+
+
+The location which the instance has been scheduled to
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name of a datum location
+ |
+ true |
+
+ | namespace |
+ string |
+
+ Namespace for the datum location
+ |
+ true |
+
+
+
+
### Workload.spec.template.spec.volumes[index]
[↩ Parent](#workloadspectemplatespec)
@@ -2022,8 +2359,17 @@ WorkloadStatus defines the observed state of Workload
currentReplicas |
integer |
- The number of instances created by a placement and have the latest
-workload generation settings applied.
+ The number of instances which have the latest workload settings applied
+and are programmed (a subset of UpdatedReplicas that are ready to serve).
+
+ Format: int32
+ |
+ true |
+
+ | deployments |
+ integer |
+
+ The number of deployments that currently exist
Format: int32
|
@@ -2032,7 +2378,16 @@ workload generation settings applied.
desiredReplicas |
integer |
- The desired number of instances to be managed by a placement.
+ The desired number of instances
+
+ Format: int32
+ |
+ true |
+
+ | readyReplicas |
+ integer |
+
+ The number of instances which are ready.
Format: int32
|
@@ -2041,7 +2396,19 @@ workload generation settings applied.
replicas |
integer |
- The number of instances created by a placement
+ The number of instances that currently exist
+
+ Format: int32
+ |
+ true |
+
+ | updatedReplicas |
+ integer |
+
+ The number of instances updated to the latest template revision (their
+observed template hash matches the desired template), regardless of
+readiness. Lags Replicas during a rolling update or restart, then catches
+back up — making an in-progress roll observable.
Format: int32
|
@@ -2061,6 +2428,15 @@ Known condition types are: "Available", "Progressing"
The status of the workload gateway if configured.
false |
+
+ | observedGeneration |
+ integer |
+
+ The most recent generation observed by the workload controller.
+
+ Format: int64
+ |
+ false |
| placements |
[]object |
@@ -2536,8 +2912,8 @@ RouteGroupKind indicates the group and kind of a Route resource.
currentReplicas |
integer |
- The number of instances created by a placement and have the latest
-workload generation settings applied.
+ The number of instances which have the latest workload settings applied
+and are programmed (a subset of UpdatedReplicas that are ready to serve).
Format: int32
|
@@ -2546,7 +2922,7 @@ workload generation settings applied.
desiredReplicas |
integer |
- The desired number of instances to be managed by a placement.
+ The desired number of instances
Format: int32
|
@@ -2558,11 +2934,30 @@ workload generation settings applied.
The name of the placement
true |
+
+ | readyReplicas |
+ integer |
+
+ The number of instances which are ready.
+
+ Format: int32
+ |
+ true |
| replicas |
integer |
- The number of instances created by a placement
+ The number of instances that currently exist
+
+ Format: int32
+ |
+ true |
+
+ | updatedReplicas |
+ integer |
+
+ The number of instances updated to the latest template revision, regardless
+of readiness. Lags Replicas during a rolling update or restart.
Format: int32
|
diff --git a/internal/controller/instancecontrol/instancecontrol.go b/internal/controller/instancecontrol/instancecontrol.go
index d2c83692..56ed2ebb 100644
--- a/internal/controller/instancecontrol/instancecontrol.go
+++ b/internal/controller/instancecontrol/instancecontrol.go
@@ -19,6 +19,7 @@ type Strategy interface {
ctx context.Context,
scheme *runtime.Scheme,
deployment *v1alpha.WorkloadDeployment,
+ desiredReplicas int32,
currentInstances []v1alpha.Instance,
) ([]Action, error)
}
diff --git a/internal/controller/instancecontrol/stateful/stateful_control.go b/internal/controller/instancecontrol/stateful/stateful_control.go
index ee64cb60..c9be87d3 100644
--- a/internal/controller/instancecontrol/stateful/stateful_control.go
+++ b/internal/controller/instancecontrol/stateful/stateful_control.go
@@ -57,6 +57,7 @@ func (c *statefulControl) GetActions(
ctx context.Context,
scheme *runtime.Scheme,
deployment *v1alpha.WorkloadDeployment,
+ desiredReplicas int32,
currentInstances []v1alpha.Instance,
) ([]instancecontrol.Action, error) {
instanceTemplateHash := instancecontrol.ComputeHash(deployment.Spec.Template)
@@ -75,7 +76,7 @@ func (c *statefulControl) GetActions(
// Instances that are desired to exist. We do not currently support the
// concept of a partition, so will fill the entire slice.
- desiredInstances := make([]*v1alpha.Instance, deployment.Spec.ScaleSettings.MinReplicas)
+ desiredInstances := make([]*v1alpha.Instance, desiredReplicas)
for _, instance := range currentInstances {
instanceIndex := getInstanceOrdinal(instance.Name)
@@ -88,7 +89,7 @@ func (c *statefulControl) GetActions(
// It's possible that the incoming currentInstances will have gaps in
// instances, so fill them in.
- for i := range deployment.Spec.ScaleSettings.MinReplicas {
+ for i := range desiredReplicas {
if desiredInstances[i] == nil {
desiredInstances[i] = &v1alpha.Instance{
ObjectMeta: metav1.ObjectMeta{
diff --git a/internal/controller/instancecontrol/stateful/stateful_control_test.go b/internal/controller/instancecontrol/stateful/stateful_control_test.go
index 63300be2..dc0557f2 100644
--- a/internal/controller/instancecontrol/stateful/stateful_control_test.go
+++ b/internal/controller/instancecontrol/stateful/stateful_control_test.go
@@ -36,7 +36,7 @@ func TestFreshDeployment(t *testing.T) {
// No instances
var currentInstances []v1alpha.Instance
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 2)
@@ -50,6 +50,21 @@ func TestFreshDeployment(t *testing.T) {
assert.True(t, actions[1].IsSkipped())
}
+func TestFreshDeployment_UsesDesiredReplicasInput(t *testing.T) {
+ ctx := context.Background()
+ control := NewWithOptions(Options{})
+
+ deployment := getWorkloadDeployment("test-fresh-deploy", 1)
+
+ actions, err := control.GetActions(ctx, scheme, deployment, 3, nil)
+
+ assert.NoError(t, err)
+ assert.Len(t, actions, 3)
+ assert.Equal(t, "test-fresh-deploy-0", actions[0].Object.GetName())
+ assert.Equal(t, "test-fresh-deploy-1", actions[1].Object.GetName())
+ assert.Equal(t, "test-fresh-deploy-2", actions[2].Object.GetName())
+}
+
// TestFreshDeployment_InstanceHasOwnerReference verifies that Instances produced
// by GetActions carry a controller owner reference to the WorkloadDeployment.
// The simplified WD finalizer relies on Kubernetes GC to cascade Instance
@@ -59,7 +74,7 @@ func TestFreshDeployment_InstanceHasOwnerReference(t *testing.T) {
control := NewWithOptions(Options{})
deployment := getWorkloadDeployment("test-wd", 1)
- actions, err := control.GetActions(ctx, scheme, deployment, nil)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, nil)
require.NoError(t, err)
require.Len(t, actions, 1)
@@ -88,7 +103,7 @@ func TestUpdateWithAllReadyInstances(t *testing.T) {
deployment.Spec.Template.Spec.Runtime.Sandbox.Containers[0].Image = "test-image-update"
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 2)
@@ -118,7 +133,7 @@ func TestScaleUpWithNotReadyInstance(t *testing.T) {
})
currentInstances = append(currentInstances, *notReadyInstance)
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 2)
@@ -145,7 +160,7 @@ func TestScaleUpWithDeletingReadyInstance(t *testing.T) {
deletingInstance.DeletionTimestamp = ptr.To(metav1.Now())
currentInstances = append(currentInstances, *deletingInstance)
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 2)
@@ -169,7 +184,7 @@ func TestScaleDownWithAllReadyInstances(t *testing.T) {
currentInstances = append(currentInstances, *getInstanceForDeployment(deployment, 0))
currentInstances = append(currentInstances, *getInstanceForDeployment(deployment, 1))
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 1)
@@ -189,7 +204,7 @@ func TestNetworkingEnabledAddsNetworkGate(t *testing.T) {
deployment := getWorkloadDeployment("test-deploy-net-on", 1)
var currentInstances []v1alpha.Instance
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 1)
@@ -220,7 +235,7 @@ func TestNetworkingDisabledOmitsNetworkGate(t *testing.T) {
deployment := getWorkloadDeployment("test-deploy-net-off", 1)
var currentInstances []v1alpha.Instance
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 1)
@@ -252,7 +267,7 @@ func TestInstanceLabels_FourNewLabelsStamped(t *testing.T) {
deployment := getWorkloadDeployment("test-labels-deploy", 1)
var currentInstances []v1alpha.Instance
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 1)
@@ -289,7 +304,7 @@ func TestInstanceLabels_RefreshedOnRecreate(t *testing.T) {
deployment.Spec.Template.Spec.Runtime.Sandbox.Containers[0].Image = "updated-image"
// First reconcile: the drifted instance is deleted (recreate), not updated.
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 1)
assert.Equal(t, instancecontrol.ActionTypeDelete, actions[0].ActionType())
@@ -297,7 +312,7 @@ func TestInstanceLabels_RefreshedOnRecreate(t *testing.T) {
// Next reconcile, after the old instance has been fully deleted and is gone:
// the empty slot is refilled by the create path, which stamps the labels.
- actions, err = control.GetActions(ctx, scheme, deployment, nil)
+ actions, err = control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, nil)
assert.NoError(t, err)
assert.Len(t, actions, 1)
assert.Equal(t, instancecontrol.ActionTypeCreate, actions[0].ActionType())
@@ -328,7 +343,7 @@ func TestInstanceLocation_SetWhenDeploymentStatusLocationPresent(t *testing.T) {
}
var currentInstances []v1alpha.Instance
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
assert.Len(t, actions, 1)
@@ -353,7 +368,7 @@ func TestInstanceLocation_NilWhenDeploymentStatusLocationAbsent(t *testing.T) {
// deployment.Status.Location is intentionally not set (nil)
var currentInstances []v1alpha.Instance
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err, "instance creation must succeed even when Status.Location is nil")
assert.Len(t, actions, 1, "exactly one create action must be produced")
@@ -393,7 +408,7 @@ func TestLabelBackfill_NotReadyMatchingHash(t *testing.T) {
// Instance 1: needs to be created (nil in desiredInstances), so we only provide instance0.
currentInstances := []v1alpha.Instance{*instance0}
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
@@ -465,7 +480,7 @@ func TestLabelBackfill_Idempotent(t *testing.T) {
}
currentInstances := []v1alpha.Instance{*instance}
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
@@ -490,7 +505,7 @@ func TestLabelBackfill_ReadyInstanceCorrected(t *testing.T) {
delete(instance.Labels, v1alpha.CityCodeLabel)
currentInstances := []v1alpha.Instance{*instance}
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
@@ -553,7 +568,7 @@ func TestLabelBackfill_DoesNotAffectRollingUpdate(t *testing.T) {
deployment.Spec.Template.Spec.Runtime.Sandbox.Containers[0].Image = "rolling-update-image"
currentInstances := []v1alpha.Instance{*instance0, *instance1}
- actions, err := control.GetActions(ctx, scheme, deployment, currentInstances)
+ actions, err := control.GetActions(ctx, scheme, deployment, deployment.Spec.ScaleSettings.MinReplicas, currentInstances)
assert.NoError(t, err)
diff --git a/internal/controller/teardown.go b/internal/controller/teardown.go
index 44df2cdd..9a287ee7 100644
--- a/internal/controller/teardown.go
+++ b/internal/controller/teardown.go
@@ -51,6 +51,9 @@ func NewComputeTeardown(
}
}
+// +kubebuilder:rbac:groups=services.miloapis.com,resources=serviceconsumers,verbs=get;list;watch
+// +kubebuilder:rbac:groups=services.miloapis.com,resources=services,verbs=get;list;watch
+
// TeardownConsumer implements consumer.Teardown.
func (ct *ComputeTeardown) TeardownConsumer(
ctx context.Context,
diff --git a/internal/controller/workload_controller.go b/internal/controller/workload_controller.go
index 861ed736..f97eecb2 100644
--- a/internal/controller/workload_controller.go
+++ b/internal/controller/workload_controller.go
@@ -490,6 +490,7 @@ func (r *WorkloadReconciler) getDeploymentsForWorkload(
CityCode: cityCode,
Template: workload.Spec.Template,
ScaleSettings: placement.ScaleSettings,
+ Replicas: new(placement.ScaleSettings.MinReplicas),
},
})
}
diff --git a/internal/controller/workload_controller_test.go b/internal/controller/workload_controller_test.go
index e8c01ae8..d8dbef80 100644
--- a/internal/controller/workload_controller_test.go
+++ b/internal/controller/workload_controller_test.go
@@ -8,12 +8,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
computev1alpha "go.datum.net/compute/api/v1alpha"
+ networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
)
// makeWorkload builds a Workload with the given generation for use in
@@ -79,6 +82,51 @@ func runReconcileWorkloadStatus(t *testing.T, workload *computev1alpha.Workload,
return cond
}
+func TestGetDeploymentsForWorkload_InitializesReplicas(t *testing.T) {
+ t.Parallel()
+
+ workload := &computev1alpha.Workload{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-workload",
+ Namespace: testDefaultNamespace,
+ UID: types.UID("workload-uid"),
+ },
+ Spec: computev1alpha.WorkloadSpec{
+ Placements: []computev1alpha.WorkloadPlacement{
+ {
+ Name: testDefaultPlacement,
+ CityCodes: []string{"DFW"},
+ ScaleSettings: computev1alpha.HorizontalScaleSettings{
+ MinReplicas: 2,
+ },
+ },
+ },
+ },
+ }
+ location := &networkingv1alpha.LocationBinding{
+ ObjectMeta: metav1.ObjectMeta{Name: "dfw"},
+ Spec: networkingv1alpha.LocationBindingSpec{
+ LocationRef: corev1.LocalObjectReference{Name: "dfw"},
+ Topology: map[string]string{"topology.datum.net/city-code": "DFW"},
+ },
+ }
+
+ s := newNetworkingScheme()
+ cl := fake.NewClientBuilder().
+ WithScheme(s).
+ WithObjects(location).
+ WithIndex(&computev1alpha.WorkloadDeployment{}, deploymentWorkloadUIDIndex, deploymentWorkloadUIDIndexFunc).
+ Build()
+ r := &WorkloadReconciler{}
+
+ desired, orphaned, err := r.getDeploymentsForWorkload(context.Background(), cl, workload)
+ require.NoError(t, err)
+ require.Empty(t, orphaned)
+ require.Len(t, desired, 1)
+ require.NotNil(t, desired[0].Spec.Replicas)
+ assert.Equal(t, int32(2), *desired[0].Spec.Replicas)
+}
+
// TestReconcileWorkloadStatus_AllDeploymentsSameReason verifies that when all
// deployments share the same blocking reason, that reason is propagated to the
// Workload Available condition with ObservedGeneration set correctly.
diff --git a/internal/controller/workloaddeployment_controller.go b/internal/controller/workloaddeployment_controller.go
index 5e3de448..94277a50 100644
--- a/internal/controller/workloaddeployment_controller.go
+++ b/internal/controller/workloaddeployment_controller.go
@@ -11,6 +11,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -60,6 +61,21 @@ type WorkloadDeploymentReconciler struct {
enableReferencedDataGate bool
}
+func effectiveDesiredReplicas(deployment *computev1alpha.WorkloadDeployment) int32 {
+ if !deployment.DeletionTimestamp.IsZero() {
+ return 0
+ }
+ if deployment.Spec.Replicas != nil {
+ return *deployment.Spec.Replicas
+ }
+ return deployment.Spec.ScaleSettings.MinReplicas
+}
+
+func workloadDeploymentPodSelector(deployment *computev1alpha.WorkloadDeployment) string {
+ set := labels.Set{computev1alpha.WorkloadDeploymentUIDLabel: string(deployment.GetUID())}
+ return set.AsSelector().String()
+}
+
// +kubebuilder:rbac:groups=compute.datumapis.com,resources=workloaddeployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=compute.datumapis.com,resources=workloaddeployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=compute.datumapis.com,resources=workloaddeployments/finalizers,verbs=update
@@ -104,6 +120,14 @@ func (r *WorkloadDeploymentReconciler) Reconcile(ctx context.Context, req mcreco
if !deployment.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil
}
+ if deployment.Spec.Replicas == nil {
+ base := deployment.DeepCopy()
+ deployment.Spec.Replicas = new(deployment.Spec.ScaleSettings.MinReplicas)
+ if err := cl.GetClient().Patch(ctx, &deployment, client.MergeFrom(base)); err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed initializing deployment replicas: %w", err)
+ }
+ return ctrl.Result{Requeue: true}, nil
+ }
logger.Info("reconciling deployment")
defer logger.Info("reconcile complete")
@@ -116,6 +140,7 @@ func (r *WorkloadDeploymentReconciler) Reconcile(ctx context.Context, req mcreco
listOpts := client.MatchingLabels{
computev1alpha.WorkloadDeploymentUIDLabel: string(deployment.GetUID()),
}
+ desiredReplicas := effectiveDesiredReplicas(&deployment)
var instances computev1alpha.InstanceList
if err := cl.GetClient().List(ctx, &instances, listOpts); err != nil {
@@ -127,7 +152,7 @@ func (r *WorkloadDeploymentReconciler) Reconcile(ctx context.Context, req mcreco
EnableReferencedDataGate: r.enableReferencedDataGate,
})
- actions, err := instanceControl.GetActions(ctx, cl.GetScheme(), &deployment, instances.Items)
+ actions, err := instanceControl.GetActions(ctx, cl.GetScheme(), &deployment, desiredReplicas, instances.Items)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed getting instance control actions: %w", err)
}
@@ -177,10 +202,6 @@ func (r *WorkloadDeploymentReconciler) Reconcile(ctx context.Context, req mcreco
// their gates removed.
replicas := len(instances.Items)
- desiredReplicas := deployment.Spec.ScaleSettings.MinReplicas
- if dt := deployment.DeletionTimestamp; !dt.IsZero() {
- desiredReplicas = 0
- }
currentReplicas, updatedReplicas, readyReplicas, quotaBlockedReplicas, referencedDataBlockedReplicas, err := r.reconcileInstanceGates(ctx, cl.GetClient(), &deployment, instances.Items, networkReady)
if err != nil {
@@ -192,6 +213,7 @@ func (r *WorkloadDeploymentReconciler) Reconcile(ctx context.Context, req mcreco
deployment.Status.UpdatedReplicas = int32(updatedReplicas)
deployment.Status.DesiredReplicas = desiredReplicas
deployment.Status.ReadyReplicas = int32(readyReplicas)
+ deployment.Status.Selector = workloadDeploymentPodSelector(&deployment)
deployment.Status.ObservedGeneration = deployment.Generation
switch {
diff --git a/internal/controller/workloaddeployment_controller_test.go b/internal/controller/workloaddeployment_controller_test.go
index 45dfbea6..fb877c05 100644
--- a/internal/controller/workloaddeployment_controller_test.go
+++ b/internal/controller/workloaddeployment_controller_test.go
@@ -55,6 +55,7 @@ func wdControllerTestDeployment(minReplicas int32) *computev1alpha.WorkloadDeplo
CityCode: wdControllerTestCityCode,
PlacementName: testDefaultPlacement,
WorkloadRef: computev1alpha.WorkloadReference{Name: wdControllerTestWorkload},
+ Replicas: new(minReplicas),
ScaleSettings: computev1alpha.HorizontalScaleSettings{
MinReplicas: minReplicas,
// Always present in production: the API server defaults the policy
@@ -374,6 +375,62 @@ func TestReconcileInstanceGates_ClearsNetworkSchedulingGate(t *testing.T) {
})
}
+func TestEffectiveDesiredReplicas(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ mutate func(*computev1alpha.WorkloadDeployment)
+ wantCount int32
+ }{
+ {
+ name: "falls back to minReplicas when unset",
+ mutate: func(deployment *computev1alpha.WorkloadDeployment) { deployment.Spec.Replicas = nil },
+ wantCount: 2,
+ },
+ {
+ name: "uses spec replicas when set",
+ mutate: func(deployment *computev1alpha.WorkloadDeployment) {
+ deployment.Spec.Replicas = new(int32(5))
+ },
+ wantCount: 5,
+ },
+ {
+ name: "deletion scales to zero",
+ mutate: func(deployment *computev1alpha.WorkloadDeployment) {
+ deployment.Spec.Replicas = new(int32(5))
+ now := metav1.Now()
+ deployment.DeletionTimestamp = &now
+ },
+ wantCount: 0,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ deployment := wdControllerTestDeployment(2)
+ if tt.mutate != nil {
+ tt.mutate(deployment)
+ }
+
+ assert.Equal(t, tt.wantCount, effectiveDesiredReplicas(deployment))
+ })
+ }
+}
+
+func TestWorkloadDeploymentPodSelector(t *testing.T) {
+ t.Parallel()
+
+ deployment := wdControllerTestDeployment(1)
+
+ assert.Equal(t,
+ "compute.datumapis.com/workload-deployment-uid=wd-uid-test",
+ workloadDeploymentPodSelector(deployment),
+ )
+}
+
// newTestWDReconciler builds a WorkloadDeploymentReconciler wired to a fake
// project cluster with the controller finalizer pre-registered, mirroring
// SetupWithManager. Networking is disabled so Reconcile treats the network as
@@ -421,7 +478,7 @@ func TestWorkloadDeploymentReconcile_FinalizerAddRequeues(t *testing.T) {
// Second reconcile (post-requeue) proceeds past the finalizer branch and
// publishes status: ObservedGeneration tracks the deployment generation and
- // DesiredReplicas reflects scale settings.
+ // DesiredReplicas reflects the effective desired count.
result, err = r.Reconcile(context.Background(), req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
@@ -429,10 +486,61 @@ func TestWorkloadDeploymentReconcile_FinalizerAddRequeues(t *testing.T) {
require.NoError(t, cl.Get(context.Background(), req.NamespacedName, &updated))
assert.Equal(t, updated.Generation, updated.Status.ObservedGeneration)
assert.Equal(t, int32(1), updated.Status.DesiredReplicas)
+ assert.Equal(t, workloadDeploymentPodSelector(&updated), updated.Status.Selector)
assert.True(t, apimeta.IsStatusConditionTrue(updated.Status.Conditions, computev1alpha.WorkloadDeploymentReplicasReady),
"no instances are quota-blocked, so ReplicasReady must be true")
}
+func TestWorkloadDeploymentReconcile_UsesSpecReplicas(t *testing.T) {
+ t.Parallel()
+
+ deployment := wdControllerTestDeployment(1)
+ deployment.Spec.Replicas = new(int32(3))
+ cl := newProjectFakeClient(deployment)
+ r := newTestWDReconciler(cl)
+ req := mcreconcile.Request{
+ ClusterName: testCluster,
+ Request: ctrl.Request{
+ NamespacedName: types.NamespacedName{Name: wdControllerTestName, Namespace: wdControllerTestNS},
+ },
+ }
+
+ _, err := r.Reconcile(context.Background(), req)
+ require.NoError(t, err)
+ _, err = r.Reconcile(context.Background(), req)
+ require.NoError(t, err)
+
+ var updated computev1alpha.WorkloadDeployment
+ require.NoError(t, cl.Get(context.Background(), req.NamespacedName, &updated))
+ assert.Equal(t, int32(3), updated.Status.DesiredReplicas)
+ assert.Equal(t, workloadDeploymentPodSelector(&updated), updated.Status.Selector)
+}
+
+func TestWorkloadDeploymentReconcile_InitializesReplicas(t *testing.T) {
+ t.Parallel()
+
+ deployment := wdControllerTestDeployment(2)
+ deployment.Spec.Replicas = nil
+ deployment.Finalizers = []string{workloadControllerFinalizer}
+ cl := newProjectFakeClient(deployment)
+ r := newTestWDReconciler(cl)
+ req := mcreconcile.Request{
+ ClusterName: testCluster,
+ Request: ctrl.Request{
+ NamespacedName: types.NamespacedName{Name: wdControllerTestName, Namespace: wdControllerTestNS},
+ },
+ }
+
+ result, err := r.Reconcile(context.Background(), req)
+ require.NoError(t, err)
+ assert.Equal(t, ctrl.Result{Requeue: true}, result)
+
+ var updated computev1alpha.WorkloadDeployment
+ require.NoError(t, cl.Get(context.Background(), req.NamespacedName, &updated))
+ require.NotNil(t, updated.Spec.Replicas)
+ assert.Equal(t, int32(2), *updated.Spec.Replicas)
+}
+
// ─── wdRefDataCondChanged tests ───────────────────────────────────────────────
// TestWdRefDataCondChanged_BothNil verifies that two nil conditions are treated
diff --git a/internal/controller/workloaddeployment_location_test.go b/internal/controller/workloaddeployment_location_test.go
index dd41381a..d8b6aa8e 100644
--- a/internal/controller/workloaddeployment_location_test.go
+++ b/internal/controller/workloaddeployment_location_test.go
@@ -174,6 +174,7 @@ func TestWorkloadDeploymentReconcile_NoMatchingLocation_SetsCondition(t *testing
Spec: computev1alpha.WorkloadDeploymentSpec{
CityCode: locTestCityCode,
WorkloadRef: computev1alpha.WorkloadReference{Name: "location-test-workload"},
+ Replicas: new(int32(1)),
ScaleSettings: computev1alpha.HorizontalScaleSettings{
MinReplicas: 1,
// Production deployments always carry the kubebuilder-defaulted