Skip to content
Open
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
89 changes: 82 additions & 7 deletions api/v1/clusterobjectset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,11 @@ type ClusterObjectSetStatus struct {
//
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf == null",message="completedAt is immutable"
// +optional
CompletedAt metav1.Time `json:"completedAt,omitempty,omitzero"`
CompletedAt metav1.Time `json:"completedAt,omitzero"`
}

// ObservedPhase records the observed content digest of a resolved phase.
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.completedAt) || has(self.completedAt)",message="completedAt cannot be removed once set"
type ObservedPhase struct {
// name is the phase name matching a phase in spec.phases.
//
Expand All @@ -570,6 +571,28 @@ type ObservedPhase struct {
// +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="the value must consist of only lowercase alphanumeric characters and hyphens, and must start and end with an alphanumeric character."
Name string `json:"name"`

// status is the current state of this phase in the rollout. Must be one of
// InvalidSpecification, PreconditionsNotMet, Pending, Reconciling,
// Stabilizing, Ready, Unknown, Superseded, Archiving, or Archived.
// +required
Comment thread
perdasilva marked this conversation as resolved.
Status PhaseStatus `json:"status"`

// completedAt is the timestamp when this phase first became Ready.
// Set once and never cleared. Nil means the phase has never been
// Ready.
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf == null",message="completedAt is immutable"

@perdasilva perdasilva Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think oldSelf is ever null - maybe "self == oldSelf" is enough? Does the object-scoped rule already cover this?

CompletedAt metav1.Time `json:"completedAt,omitzero"`

// message is a phase-level message providing context about the
// current status (e.g. a validation error for Invalid phases, or
// a summary for Pending phases). At most 1024 characters; longer
// messages are truncated by the controller.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=1024
// +optional
Message string `json:"message,omitempty"`
Comment thread
dtfranz marked this conversation as resolved.

// digest is the digest of the phase's resolved object content
// at first successful resolution, in the format "<algorithm>:<hex>".
//
Expand All @@ -581,10 +604,14 @@ type ObservedPhase struct {
Digest string `json:"digest"`

// objectCounts reports the number of objects in this phase by state.
// It is omitted until counts have been computed; when present, all count
// fields are reported (including zeros).
// +optional
ObjectCounts ObjectCounts `json:"objectCounts,omitzero"`

// lastTransitionTime is the last time the phase transitioned from one status to another.
// +required
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Format=date-time
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitzero"`
}

// ObjectCounts reports the number of objects in a ClusterObjectSet or phase by state.
Expand All @@ -596,16 +623,16 @@ type ObjectCounts struct {
Total int64 `json:"total"`

// present is the number of objects in this phase that exist on the
// cluster. During reconcile, this counts objects that the controller
// has found or created. During teardown, this decrements toward zero
// as objects are deleted.
// cluster and are owned by this object set, or another in the same group.
// During reconcile, this counts objects that the controller has found or
// created. During teardown, this decrements toward zero as objects are deleted.
// +required
// +kubebuilder:validation:Minimum:=0
// +kubebuilder:validation:Maximum:=1000
Present int64 `json:"present"`

// synced is the number of objects in this phase whose cluster state
// matches the desired state.
// matches the desired state. Set to zero during teardown.
// +required
// +kubebuilder:validation:Minimum:=0
// +kubebuilder:validation:Maximum:=1000
Expand All @@ -619,6 +646,54 @@ type ObjectCounts struct {
Ready int64 `json:"ready"`
}

// PhaseStatus describes the current state of a phase in the rollout.
//
// +kubebuilder:validation:Enum=InvalidSpecification;Pending;Reconciling;Stabilizing;Ready;Unknown;Superseded;Archiving;Archived
type PhaseStatus string

const (
// PhaseStatusInvalidSpecification indicates the phase failed preflight validation
// in a permanent way (e.g. cross-phase duplication).
PhaseStatusInvalidSpecification PhaseStatus = "InvalidSpecification"

// PhaseStatusPending indicates the phase is not being actively
// reconciled, but the controller has checked and some objects do not
// match the desired state. The objectDetails field lists what would
// need to change.
PhaseStatusPending PhaseStatus = "Pending"

// PhaseStatusReconciling indicates the controller is actively applying
// objects in this phase and not all objects are synced yet.
PhaseStatusReconciling PhaseStatus = "Reconciling"

// PhaseStatusStabilizing indicates all objects in this phase are
// synced but some assertions are not yet passing. The controller is
// not actively writing; it is waiting for other controllers or
// external actions to bring the objects into the expected state.
PhaseStatusStabilizing PhaseStatus = "Stabilizing"

// PhaseStatusReady indicates all objects in this phase are synced
// and pass their assertions.
PhaseStatusReady PhaseStatus = "Ready"

// PhaseStatusUnknown indicates this phase was not evaluated during
// the most recent reconcile.
PhaseStatusUnknown PhaseStatus = "Unknown"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're currently not setting this status in the code


// PhaseStatusSuperseded indicates all objects in this phase have been
// adopted by a newer revision.
PhaseStatusSuperseded PhaseStatus = "Superseded"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, in Ready, Superseded gets set once any object has transitioned. We should also make a decision here. The issue at the revision/object set level is that we may not ever have all objects transitioned (some may not be present in the next revision)...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you're saying that in a given phase we may have both objects that have been superseded by the subsequent revision and some that have been archived instead? If we aren't avoiding the use of Progressed terminology we could set that as a more broad condition when both Archived and Superseded apply to the same phase.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @dtfranz. The assumption here is that there is no use to differentiate between "Superseded" and "Archived", as once the objects are either taken over or deleted it changes nothing for the Phase: seems clear that the assumption holds to me, making it explicit to double check.
For the term, I would consider keeping "Superseded" but with the meaning of Daniel's "Progressed".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, for example, rev-1.phase-1 has objects {a, b, c, d} and rev-2.phase-1 has objects {a, b, c}

rev-1.phase-1 will never has a HasTransitioned() == true because object d isn't present in rev-2. So, I'm wondering whether a phase is superseded once any of its objects is now owned by a subsequent revision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can stick to the current semantic - but this is something we'll probably want to revisit


// PhaseStatusArchiving indicates the controller is actively deleting
// objects in this phase. Objects still awaiting deletion are listed
// in objectDetails.
PhaseStatusArchiving PhaseStatus = "Archiving"

// PhaseStatusArchived indicates all objects in this phase have
// been deleted from the cluster.
PhaseStatusArchived PhaseStatus = "Archived"
)

// TODO(COD): Update the archival description below when archival decisions move
// to ClusterObjectDeployment reconciliation.

Expand Down
8 changes: 5 additions & 3 deletions api/v1/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,11 @@ func TestClusterObjectSetDigestImmutable(t *testing.T) {
// A phase digest may be set once, from empty.
cos.Status.ObservedPhases = []ObservedPhase{
{
Name: "deploy",
Digest: "sha256:abc123",
ObjectCounts: ObjectCounts{Total: 1},
Name: "deploy",
Digest: "sha256:abc123",
ObjectCounts: ObjectCounts{Total: 1},
Status: PhaseStatusReady,
LastTransitionTime: metav1.Now(),
},
}
require.NoError(t, c.Status().Update(t.Context(), cos))
Expand Down
6 changes: 5 additions & 1 deletion api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions applyconfigurations/api/v1/objectcounts.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 52 additions & 2 deletions applyconfigurations/api/v1/observedphase.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions applyconfigurations/internal/internal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/api-reference/olmv1-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,8 @@ _Appears in:_
| `key` _string_ | key is the data key within the referenced Secret containing the<br />object manifest content. The value at this key must be a<br />JSON-serialized Kubernetes object manifest. | | MaxLength: 253 <br />MinLength: 1 <br />Required: \{\} <br /> |




#### PreflightConfig


Expand Down
Loading
Loading