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.
+ + + + + + + + + + @@ -536,6 +553,30 @@ used by the instance.
The name of the container.
+ + + + + + + + + + @@ -545,6 +586,18 @@ used by the instance.
so replicate the structure here too.
+ + + + + @@ -590,7 +643,8 @@ EnvVar represents an environment variable present in a Container. @@ -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.
+ + + + + @@ -751,6 +813,66 @@ spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podI
true
controllerobject + Controller contains settings driven by the controller managing the instance.
+
false
locationobject + 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
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 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 []objectname 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
false
fileKeyRefobject + FileKeyRef selects a key of the env file. +Requires the EnvFiles feature gate to be enabled.
+
false
resourceFieldRef object
+### 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + 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
pathstring + The path within the volume from which to select the file. +Must be relative and may not contain the '..' path or start with '..'.
+
true
volumeNamestring + The name of the volume mount containing the env file.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
configMapRefobject + The ConfigMap to select from.
+
false
prefixstring + An optional identifier to prepend to each key in the referenced +ConfigMap or Secret. Must be a valid C_IDENTIFIER.
+
false
secretRefobject + The Secret to select from.
+
false
+ + +### Instance.spec.runtime.sandbox.containers[index].envFrom[index].configMapRef +[↩ Parent](#instancespecruntimesandboxcontainersindexenvfromindex) + + + +The ConfigMap to select from. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of the ConfigMap in the same namespace as the Workload.
+
true
optionalboolean + Specify whether the ConfigMap must be defined.
+
false
+ + +### Instance.spec.runtime.sandbox.containers[index].envFrom[index].secretRef +[↩ Parent](#instancespecruntimesandboxcontainersindexenvfromindex) + + + +The Secret to select from. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of the Secret in the same namespace as the Workload.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
templateHashstring + 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) + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + The name of the gate.
+
true
+ + +### Instance.spec.location +[↩ Parent](#instancespec) + + + +The location which the instance has been scheduled to + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of a datum location
+
true
namespacestring + 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"
+ + + + + @@ -1797,6 +2133,33 @@ with respect to the current state of the instance.
false
controllerobject + Controller contains status information about the controller managing the instance.
+
false
networkInterfaces []object
+### Instance.status.controller +[↩ Parent](#instancestatus) + + + +Controller contains status information about the controller managing the instance. + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
observedTemplateHashstring + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + 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
pathstring + The path within the volume from which to select the file. +Must be relative and may not contain the '..' path or start with '..'.
+
true
volumeNamestring + The name of the volume mount containing the env file.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
configMapRefobject + The ConfigMap to select from.
+
false
prefixstring + An optional identifier to prepend to each key in the referenced +ConfigMap or Secret. Must be a valid C_IDENTIFIER.
+
false
secretRefobject + The Secret to select from.
+
false
+ + +### WorkloadDeployment.spec.template.spec.runtime.sandbox.containers[index].envFrom[index].configMapRef +[↩ Parent](#workloaddeploymentspectemplatespecruntimesandboxcontainersindexenvfromindex) + + + +The ConfigMap to select from. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of the ConfigMap in the same namespace as the Workload.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of the Secret in the same namespace as the Workload.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
templateHashstring + 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) + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + The name of the gate.
+
true
+ + +### WorkloadDeployment.spec.template.spec.location +[↩ Parent](#workloaddeploymentspectemplatespec) + + + +The location which the instance has been scheduled to + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of a datum location
+
true
namespacestring + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + 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
pathstring + The path within the volume from which to select the file. +Must be relative and may not contain the '..' path or start with '..'.
+
true
volumeNamestring + The name of the volume mount containing the env file.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
configMapRefobject + The ConfigMap to select from.
+
false
prefixstring + An optional identifier to prepend to each key in the referenced +ConfigMap or Secret. Must be a valid C_IDENTIFIER.
+
false
secretRefobject + The Secret to select from.
+
false
+ + +### Workload.spec.template.spec.runtime.sandbox.containers[index].envFrom[index].configMapRef +[↩ Parent](#workloadspectemplatespecruntimesandboxcontainersindexenvfromindex) + + + +The ConfigMap to select from. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of the ConfigMap in the same namespace as the Workload.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of the Secret in the same namespace as the Workload.
+
true
optionalboolean + 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. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
templateHashstring + 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) + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + The name of the gate.
+
true
+ + +### Workload.spec.template.spec.location +[↩ Parent](#workloadspectemplatespec) + + + +The location which the instance has been scheduled to + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name of a datum location
+
true
namespacestring + 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