Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions api/v1alpha1/conditions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package v1alpha1

// Condition type constants.
//
// These conditions follow the conventions of kstatus
// (https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus), the
// library that Helm --wait, Argo CD, Argo Rollouts, Flux, and kubectl wait
// --for=jsonpath use to decide whether a custom resource is ready.
//
// Ready and Progressing remain the conditions for human-facing status and for
// automation written against this controller. Reconciling and Stalled exist so
// generic tooling can read the same state without custom health checks.
const (
// ConditionReady is True for WorkerDeployment when the Temporal
// connection is reachable and the target version is the current version in Temporal.
Expand All @@ -11,6 +20,42 @@ const (
// ConditionProgressing is True while a rollout is actively in-flight —
// i.e., the target version has not yet been promoted to current.
ConditionProgressing = "Progressing"

// ConditionStalled is True when reconciliation cannot progress and only a spec
// change can resolve it — an invalid spec, or a connection kind this controller
// cannot read. kstatus
// (https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus) reports
// Failed when it is True, so Argo Rollouts and Helm --wait abort instead of
// waiting out their timeout.
//
// kstatus's own convention is that such a condition is absent while things are
// normal, but it only ever tests for True, so this controller writes the
// condition on every path and sets it False when nothing is stalled. That reads
// identically to kstatus and keeps every condition the controller owns visible
// in kubectl describe, consistent with Ready and Progressing.
//
// It is set only for failures decidable from information already in hand.
// Failures that are waiting on another object to exist (a missing Connection or
// credential Secret) and transient infrastructure failures report Reconciling
// instead, because neither can be told apart from a normal few-second gap
// during a deploy. See stalledReasons in the controller package.
ConditionStalled = "Stalled"

// ConditionReconciling is True while the controller is still working toward the
// spec, and False once it has caught up. kstatus reports InProgress when it is
// True, which is the path kstatus intends for custom resources. Without it,
// kstatus has to infer the same answer from Ready=False, a fallback its own
// documentation flags as unreliable.
//
// It is close to the inverse of Progressing but not identical: a transient
// blocking error sets Progressing=False (blocked) and Reconciling=True (still
// retrying), because those two vocabularies disagree about what a retry is.
//
// Reconciling and Stalled must never both be True on the same object.
// kstatus scans status.conditions in array order and returns on the first
// match, so the verdict would depend on insertion order. The controller
// writes both on every path and sets at most one of them to True.
ConditionReconciling = "Reconciling"
)

// Deprecated condition type constants. Maintained for backward compatibility with
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/workerdeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ const (
// when the target version has been successfully registered as the current version.
ReasonRolloutComplete = "RolloutComplete"

// ReasonReconcileSucceeded is set on ConditionStalled=False after any reconcile
// that completed without a blocking error, whatever stage the rollout is at.
// It is what allows a WorkerDeployment that recovers from a blocking error to
// stop reporting Failed to kstatus consumers.
ReasonReconcileSucceeded = "ReconcileSucceeded"

// ReasonWaitingForPollers is set on ConditionProgressing=True when workers are
// not yet (or are no longer) actively polling Temporal. This covers both:
// (1) the target version's Kubernetes Deployment has been created but the
Expand Down
10 changes: 10 additions & 0 deletions api/v1alpha1/workerresourcetemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ type WorkerResourceTemplateStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`

// ObservedGeneration is the .metadata.generation the controller last
// reconciled. Compare against .metadata.generation to tell whether the
// controller has caught up with the latest spec change.
//
// This is the only generation field kstatus consults; the per-entry
// observedGeneration carried on each condition is not read by it.
// +optional
// +kubebuilder:validation:Minimum=0
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down
64 changes: 50 additions & 14 deletions docs/cd-rollouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ For migration help, see [migration-to-versioned.md](migration-to-versioned.md).

## Understanding the conditions

The `WorkerDeployment` resource exposes two standard conditions on `status.conditions` that CD tools and scripts can consume.
The `WorkerDeployment` resource exposes four standard conditions on `status.conditions` that CD tools and scripts can consume.

`Ready` and `Progressing` describe the rollout in the controller's own terms. They are the ones to read in a script, a dashboard, or `kubectl describe`, and their `reason` fields describe why the controller has set the condition's `Status` field to `True` or `False`.

`Stalled` and `Reconciling` say the same thing in the vocabulary [kstatus](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus) understands, the library behind Helm 4 `--wait` and Flux health assessment. They follow kstatus's "abnormal-true" convention: each is present and `True` only while something unusual is happening, and absent otherwise. You rarely need to read them yourself; they exist so those tools reach the right verdict without a custom health check.

### `Ready`

Expand Down Expand Up @@ -43,6 +47,34 @@ When `Progressing=False` due to an error, the `reason` field identifies what wen

Once the underlying problem is fixed, the next successful reconcile will restore `Progressing` and `Ready` to the correct state.

### `Stalled` and `Reconciling`

`Reconciling=True` means the controller is still working toward the spec. kstatus-based tools report the resource as **in progress** and keep waiting. `Stalled=True` means reconciliation cannot proceed and waiting will not help. The kstatus tools report **failed** and stop. Both are absent once a rollout is complete, and only one is ever set at a time.

`Stalled` is set to `True` only for failures that are decidable from information already in hand, where nothing arriving later could change the answer. The `Reason` field is used to describe why that decision cannot change.
* `InvalidSpec` is used when the spec you just applied is not valid.
* `ClusterConnectionUnsupported` is used when the type of Connection is not supported by the controller.

`Reconciling` is set to `True` when the controller is still working to make the observed state of the resource match the desired state of the resource, with the `Reason` field used to provide more detail about why `Reconciling=True`.

* `ConnectionNotFound` is used when the `Connection` may not exist *yet* or is simply absent.
* `AuthSecretInvalid` a credential Secret may not exist *yet* or is simply absent.
* `TemporalClientCreationFailed` is used when the Temporal Server is unreachable.
* `TemporalStateFetchFailed` is used when rate-limiting is encountered.
* `PlanGenerationFailed`, `PlanExecutionFailed` is used when errors occurred during plan generation or execution that can be potentially be recovered from in the next reconciliation loop.

The reason a missing `Connection` is not treated as terminal is ordering. Applying a `WorkerDeployment` alongside its `Connection` and credentials in one release gives no guarantee about which lands first, so a missing reference is frequently a normal gap of a few seconds rather than a mistake.

The trade-off is that an incorrect `connectionRef` or a `Connection` that was never created keeps reporting *in progress* until your tool's timeout expires rather than failing immediately. Set timeouts you are willing to wait out, and read the `reason` on `Ready`/`Progressing` (or the resource's Kubernetes Events) to see what is actually blocking.

`WorkerResourceTemplate` follows the same pattern: a template that cannot render, or that the API server rejects outright, sets `Stalled`. A `WorkerResourceTemplate` waiting for its `WorkerDeployment` to appear, or retrying a transient apply failure, sets `Reconciling`.

### `Connection` and `ClusterConnection`

`Connection` and `ClusterConnection` are configuration-only resources. They have no controller of their own and expose no conditions, so tools that assess health from conditions like Helm `--wait`, Flux, and anything else built on [kstatus](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus) treat them as healthy as soon as they exist. This is intentional as there is no reconcile loop behind them and therefore nothing to wait for. Kubernetes treats `ConfigMap` and `Secret` the same way.

A broken connection is still reported, just on the `WorkerDeployment` that references it rather than on the connection itself (see the `ConnectionNotFound` and `AuthSecretInvalid` reasons above). Your rollouts should wait on the `WorkerDeployment` as waiting on a `Connection` does not necessarily tell you that the credentials in it work.

## Triggering a rollout

A rollout starts when you change the pod template in your `WorkerDeployment` spec — a changed pod spec produces a new Build ID, which the controller treats as a new version to roll out.
Expand Down Expand Up @@ -97,7 +129,7 @@ Set `--timeout` to exceed the longest expected rollout time — for progressive

### Helm 4

Helm 4 uses [kstatus](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus) for its `--wait` implementation ([HIP-0022](https://helm.sh/community/hips/hip-0022/)). kstatus understands the standard Kubernetes conditions contract and should block until `Ready=True` on your `WorkerDeployment`:
Helm 4 uses [kstatus](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus) for its `--wait` implementation ([HIP-0022](https://helm.sh/community/hips/hip-0022/)). kstatus understands the standard Kubernetes conditions contract and should block until `Ready=True` on your `WorkerDeployment`. Because the controller also emits `Stalled` (see above), a rollout blocked by an invalid spec fails the release immediately instead of waiting out the timeout:

```bash
helm upgrade my-worker ./chart --values values.yaml --wait --timeout 10m
Expand All @@ -121,38 +153,42 @@ kubectl wait workerdeployment/my-worker \

ArgoCD does not have a generic fallback that automatically checks `status.conditions` on unknown CRD types. For any resource whose group (`temporal.io`) is not in ArgoCD's built-in health check registry, ArgoCD silently skips that resource when computing application health. A [custom Lua health check](https://argo-cd.readthedocs.io/en/stable/operator-manual/health/) is the standard mechanism for teaching ArgoCD how to assess a CRD's health.

The two standard conditions (`Ready`, `Progressing`) keep the Lua simple — it only needs to read the condition type and status, not any controller-specific status fields. The following script is a starting point; adapt it to your ArgoCD version and any site-specific requirements:
The standard conditions keep the Lua simple — it only needs to read condition types and statuses, not any controller-specific status fields. Reading `Stalled` and `Reconciling` rather than `Progressing` also makes ArgoCD agree with Helm and Flux about what counts as a failure, instead of showing **Degraded** for a `Connection` that is a second away from existing. The following script is a starting point; adapt it to your ArgoCD version and any site-specific requirements:

```yaml
# In your argocd-cm ConfigMap
data:
resource.customizations.health.temporal.io_WorkerDeployment: |
local ready = nil
local progressing = nil
local stalled = nil
local reconciling = nil
if obj.status ~= nil and obj.status.conditions ~= nil then
for _, c in ipairs(obj.status.conditions) do
if c.type == "Ready" then ready = c end
if c.type == "Progressing" then progressing = c end
if c.type == "Stalled" then stalled = c end
if c.type == "Reconciling" then reconciling = c end
end
end
-- Check Stalled first: it is the only condition that means waiting will not help.
if stalled ~= nil and stalled.status == "True" then
return {status = "Degraded", message = stalled.message}
end
if ready ~= nil and ready.status == "True" then
return {status = "Healthy", message = ready.message}
end
if progressing ~= nil then
if progressing.status == "True" then
return {status = "Progressing", message = progressing.message}
else
return {status = "Degraded", message = progressing.message}
end
if reconciling ~= nil and reconciling.status == "True" then
return {status = "Progressing", message = reconciling.message}
end
return {status = "Progressing", message = "Waiting for conditions"}
```

With a health check like this in place:

- ArgoCD shows **Degraded** when reconciliation is stalled (`Stalled=True`) — an invalid spec, or a connection kind this controller cannot read.
- ArgoCD shows **Healthy** once `Ready=True`.
- ArgoCD shows **Progressing** while a rollout is in-flight (`Progressing=True`).
- ArgoCD shows **Degraded** when progress is blocked (`Progressing=False` with an error reason).
- ArgoCD shows **Progressing** while a rollout is in-flight, and also while the controller is retrying a recoverable problem such as a `Connection` that does not exist yet. Read the `reason` on `Ready` to tell those apart.

The same script works for `WorkerResourceTemplate` — register it under `resource.customizations.health.temporal.io_WorkerResourceTemplate` as well, since it emits the same three conditions.

If you use [sync waves](https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/) and workers must be fully rolled out before a dependent service is updated, place the `WorkerDeployment` in an earlier wave.

Expand All @@ -162,7 +198,7 @@ If you use [sync waves](https://argo-cd.readthedocs.io/en/stable/user-guide/sync

### Kustomization

Flux's `Kustomization` controller uses kstatus to assess resource health. Because `WorkerDeployment` emits a standard `Ready` condition, Flux should treat it as healthy when `Ready=True`. Adding an explicit `healthChecks` entry makes the dependency visible and ensures Flux waits on the `WorkerDeployment` before marking the Kustomization as ready:
Flux's `Kustomization` controller uses kstatus to assess resource health. Because `WorkerDeployment` emits the standard `Ready`, `Reconciling`, and `Stalled` conditions, Flux should treat it as healthy when `Ready=True`, keep waiting while `Reconciling=True`, and fail the health check rather than wait out the timeout when `Stalled=True`. Adding an explicit `healthChecks` entry makes the dependency visible and ensures Flux waits on the `WorkerDeployment` before marking the Kustomization as ready:

```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
Expand Down
4 changes: 4 additions & 0 deletions docs/migration-crd-rename.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ message: "Migration complete. Delete this TemporalWorkerDeployment."

For `TemporalConnection`, the same `Deprecated` → `MigratedToConnection` pattern applies (there is no ownership transfer step, so there is no intermediate state).

> **CD health checks:** because these resources never report `Ready=True`, tools that assess health from conditions (Helm `--wait`, Flux, and anything else built on [kstatus](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus)) treat an unmigrated `TemporalWorkerDeployment` or `TemporalConnection` as not-ready for as long as it exists — completing the migration is what resolves it.

> **NOTE**: A resource already marked for deletion reports `Terminating` instead, so following the migration steps prevents a stuck release.

## Deletion protection

After upgrading to v1.7, the controller adds a `temporal.io/migration-guard` finalizer to every `TemporalWorkerDeployment` and `TemporalConnection`. This finalizer prevents the resource from being fully deleted until migration is confirmed:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
k8s.io/apimachinery v0.36.3
k8s.io/client-go v0.36.3
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5
sigs.k8s.io/cli-utils v0.37.2
sigs.k8s.io/controller-runtime v0.24.0
sigs.k8s.io/yaml v1.6.0
)
Expand Down Expand Up @@ -80,7 +81,6 @@ require (
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,8 @@ modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg=
sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco=
sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4=
sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
observedGeneration:
format: int64
minimum: 0
type: integer
versions:
items:
properties:
Expand Down
Loading
Loading