Skip to content
Merged
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: 81 additions & 8 deletions api/v1/clusterobjectset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,12 +555,13 @@ type ClusterObjectSetStatus struct {
// ready, meaning it had successfully rolled out and all of its objects passed
// their probes. It is set once and is immutable thereafter.
//
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf == null",message="completedAt is immutable"
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf == null",message="completedAt is immutable, once set"
// +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, 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 modified. Nil means the phase has never been
// Ready.
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf == null",message="completedAt is immutable, once set"
CompletedAt metav1.Time `json:"completedAt,omitzero"`

// message is a phase-level message providing context about the
// current status (e.g. a validation error for InvalidSpecification
// 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,omitzero"`

// 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,52 @@ 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.
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

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.

Here's where I used it in orb-operator: https://github.com/joelanford/orb-operator/blob/76c7c2316efdacf9a3eeb8a68719df9781dacf1e/internal/status/cos/status.go#L163-L186

Can't remember if I make any specific decisions on this, or if this is what fell out of the LLM and it seemed fine. One thing that occurs to me looking at the orb-operator code is that maybe we should specifically try to evaluate all phases even if some other phases are blocked by preflight validation failures?

Unknown seems like it would be something along the lines of "couldn't read our cache" or similar.

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 think we do. The only thing is we ignore validation failures if other phases have them? or aren't yet complete. The reason is those dry-run operations can fail because, e.g. the namespace that's supposed to be created in a previous phase hasn't been created yet.

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'd say we could use Unknown for now as the initial status when we derive the digest of the phase. Then, if there's a revision validation error, or some other precondition not met, it will come up.


// 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

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.

To me, Superseded feels like "all objects from this phase that needed to be adopted by the next revision have been adopted by the next revision". An outcome of this would mean that a phase that has no objects in a higher revision is Superseded as soon as we process that phase and see that there is nothing to hand over.

And if we need a present tense phase, then Superseding would be "at least one object from this phase has been adopted by a higher revision"

Archiving/Archived is different. That is about cleaning up any leftover objects after everything shared has been handed over.

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.

Re: superseded versus archived

  • COS controller is what initiates supersession.
  • COD controller (or other COS owner) initiates archival/deletion

So we definitely need the COS controller to distinguish between those two things. It is totally possible for a fully superseded COS to remain active indefinitely.

@perdasilva perdasilva Oct 26, 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.

yeah - superseded really should be what you said. The problem is that there's no way for the COS to know when that's happened (unless its also trolling throw the statuses of the later revisions). Feels like a bit of extra complexity for now. But, maybe it wouldn't be too bad 🤔


// PhaseStatusArchiving indicates the controller is actively deleting
// objects in this phase.
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