feat: add observedgeneration to status object of all crds (#1930)

* feat: add observedgeneration to status object of all crds

Signed-off-by: sandert-k8s <sandert98@gmail.com>

* chore(api): rename rulestatusspec to rulestatusstatus

Signed-off-by: sandert-k8s <sandert98@gmail.com>

---------

Signed-off-by: sandert-k8s <sandert98@gmail.com>
This commit is contained in:
Sander Tervoert
2026-06-01 06:51:46 +02:00
committed by GitHub
parent eaee31765a
commit d255c33f5d
30 changed files with 350 additions and 7 deletions
@@ -9,6 +9,9 @@ import (
// CapsuleConfigurationStatus defines the Capsule configuration status.
type CapsuleConfigurationStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Users which are considered Capsule Users and are bound to the Capsule Tenant construct.
Users rbac.UserListSpec `json:"users,omitempty"`
}
+3
View File
@@ -14,6 +14,9 @@ import (
// CustomQuotaStatus defines the observed state of GlobalResourceQuota.
type CustomQuotaStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Usage measurements
// +optional
Usage CustomQuotaStatusUsage `json:"usage,omitempty"`
+3
View File
@@ -13,6 +13,9 @@ import (
// GlobalResourceQuotaStatus defines the observed state of GlobalResourceQuota.
type ResourcePoolStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// How many namespaces are considered
// +kubebuilder:default=0
NamespaceSize uint `json:"namespaceCount,omitempty"`
+3
View File
@@ -21,6 +21,9 @@ type ResourcePoolClaimSpec struct {
// ResourceQuotaClaimStatus defines the observed state of ResourceQuotaClaim.
type ResourcePoolClaimStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Reference to the GlobalQuota being claimed from
// +optional
Pool meta.LocalRFC1123ObjectReferenceWithUID `json:"pool,omitzero"`
+5 -2
View File
@@ -12,7 +12,10 @@ import (
// RuleStatus contains the accumulated rules applying to namespace it's deployed in.
// +kubebuilder:object:generate=true
type RuleStatusSpec struct {
type RuleStatusStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Managed Enforcement properties per Namespace (aggregated from rules)
//+optional
Rule api.NamespaceRuleBodyNamespace `json:"rule,omitzero"`
@@ -34,7 +37,7 @@ type RuleStatus struct {
Spec []*api.NamespaceRuleBodyNamespace `json:"spec,omitzero"`
// +optional
Status RuleStatusSpec `json:"status,omitzero"`
Status RuleStatusStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
+4
View File
@@ -25,6 +25,10 @@ type TenantStatus struct {
// Allowed Cluster Objects within Tenant
TenantAvailableStatus `json:",inline"`
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Collected owners for this tenant
Owners rbac.OwnerStatusListSpec `json:"owners,omitempty"`
// Promoted ServiceAccounts across the Tenant
+5 -1
View File
@@ -22,7 +22,11 @@ type TenantOwnerSpec struct {
}
// TenantOwnerStatus defines the observed state of TenantOwner.
type TenantOwnerStatus struct{}
type TenantOwnerStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
+3
View File
@@ -13,6 +13,9 @@ import (
)
type TenantResourceCommonStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Condition of the GlobalTenantResource.
Conditions meta.ConditionList `json:"conditions,omitempty"`
+4 -4
View File
@@ -1626,7 +1626,7 @@ func (in *RuleStatusList) DeepCopyObject() runtime.Object {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RuleStatusSpec) DeepCopyInto(out *RuleStatusSpec) {
func (in *RuleStatusStatus) DeepCopyInto(out *RuleStatusStatus) {
*out = *in
in.Rule.DeepCopyInto(&out.Rule)
if in.Conditions != nil {
@@ -1638,12 +1638,12 @@ func (in *RuleStatusSpec) DeepCopyInto(out *RuleStatusSpec) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleStatusSpec.
func (in *RuleStatusSpec) DeepCopy() *RuleStatusSpec {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleStatusStatus.
func (in *RuleStatusStatus) DeepCopy() *RuleStatusStatus {
if in == nil {
return nil
}
out := new(RuleStatusSpec)
out := new(RuleStatusStatus)
in.DeepCopyInto(out)
return out
}
@@ -1250,6 +1250,11 @@ spec:
description: CapsuleConfigurationStatus defines the Capsule configuration
status.
properties:
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
users:
description: Users which are considered Capsule Users and are bound
to the Capsule Tenant construct.
@@ -329,6 +329,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
targets:
description: Targeting GVK
items:
@@ -384,6 +384,11 @@ spec:
items:
type: string
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
targets:
description: Targeting GVK
items:
@@ -521,6 +521,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
processedItems:
description: List of the replicated resources for the given TenantResource.
items:
@@ -230,6 +230,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
pool:
description: Reference to the GlobalQuota being claimed from
properties:
@@ -389,6 +389,11 @@ spec:
items:
type: string
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
required:
- conditions
type: object
@@ -145,6 +145,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
rule:
description: Managed Enforcement properties per Namespace (aggregated
from rules)
@@ -74,6 +74,12 @@ spec:
type: object
status:
description: status defines the observed state of TenantOwner.
properties:
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
type: object
required:
- spec
@@ -456,6 +456,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
processedItems:
description: List of the replicated resources for the given TenantResource.
items:
@@ -2949,6 +2949,11 @@ spec:
items:
type: string
type: array
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
owners:
description: Collected owners for this tenant
items:
+249
View File
@@ -0,0 +1,249 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
capmeta "github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
)
var _ = Describe("observedGeneration is tracked in status", Ordered, Label("observedGeneration"), func() {
var tnt *capsulev1beta2.Tenant
JustBeforeEach(func() {
tnt = &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-observed-generation",
},
Spec: capsulev1beta2.TenantSpec{
Owners: rbac.OwnerListSpec{
{
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: "e2e-observed-generation-owner",
Kind: "User",
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
EventuallyDeletion(tnt)
})
It("sets observedGeneration after initial reconciliation", func() {
By("waiting for the tenant to be ready", func() {
TenantReadyTrue(tnt)
})
By("verifying observedGeneration equals metadata.generation", func() {
Eventually(func(g Gomega) {
current := &capsulev1beta2.Tenant{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, current)).To(Succeed())
g.Expect(current.Status.ObservedGeneration).To(
Equal(current.GetGeneration()),
"expected status.observedGeneration (%d) to equal metadata.generation (%d) after initial reconciliation",
current.Status.ObservedGeneration,
current.GetGeneration(),
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
It("updates observedGeneration after a spec change", func() {
By("waiting for the tenant to be ready at generation 1", func() {
TenantReadyTrue(tnt)
})
var genBeforeUpdate int64
By("recording the current generation", func() {
current := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, current)).To(Succeed())
genBeforeUpdate = current.GetGeneration()
Expect(genBeforeUpdate).To(BeNumerically(">=", int64(1)))
})
By("mutating the spec to increment metadata.generation", func() {
UpdateTenantEventually(tnt, func(t *capsulev1beta2.Tenant) {
t.Spec.Cordoned = true
})
})
By("verifying observedGeneration advances to the new generation", func() {
Eventually(func(g Gomega) {
current := &capsulev1beta2.Tenant{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, current)).To(Succeed())
g.Expect(current.GetGeneration()).To(
BeNumerically(">", genBeforeUpdate),
"expected metadata.generation to have incremented after spec update",
)
g.Expect(current.Status.ObservedGeneration).To(
Equal(current.GetGeneration()),
"expected status.observedGeneration (%d) to match updated metadata.generation (%d)",
current.Status.ObservedGeneration,
current.GetGeneration(),
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
})
var _ = Describe("CapsuleConfiguration observedGeneration is tracked in status", Serial, Ordered, Label("observedGeneration"), func() {
It("observedGeneration matches metadata.generation for the default CapsuleConfiguration", func() {
Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: defaultConfigurationName}, cfg)).To(Succeed())
g.Expect(cfg.Status.ObservedGeneration).To(
Equal(cfg.GetGeneration()),
"expected status.observedGeneration (%d) to equal metadata.generation (%d)",
cfg.Status.ObservedGeneration,
cfg.GetGeneration(),
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("advances observedGeneration after a CapsuleConfiguration spec change", func() {
var genBefore int64
By("recording the current generation", func() {
cfg := &capsulev1beta2.CapsuleConfiguration{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: defaultConfigurationName}, cfg)).To(Succeed())
genBefore = cfg.GetGeneration()
})
By("mutating CapsuleConfiguration to increment metadata.generation", func() {
ModifyCapsuleConfigurationOpts(func(cfg *capsulev1beta2.CapsuleConfiguration) {
cfg.Spec.IgnoreUserWithGroups = append(cfg.Spec.IgnoreUserWithGroups, "e2e-observed-gen-dummy-group")
})
})
DeferCleanup(func() {
ModifyCapsuleConfigurationOpts(func(cfg *capsulev1beta2.CapsuleConfiguration) {
groups := cfg.Spec.IgnoreUserWithGroups
filtered := groups[:0]
for _, g := range groups {
if g != "e2e-observed-gen-dummy-group" {
filtered = append(filtered, g)
}
}
cfg.Spec.IgnoreUserWithGroups = filtered
})
})
By("verifying observedGeneration advances to the new generation", func() {
Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: defaultConfigurationName}, cfg)).To(Succeed())
g.Expect(cfg.GetGeneration()).To(
BeNumerically(">", genBefore),
"expected metadata.generation to increment after spec update",
)
g.Expect(cfg.Status.ObservedGeneration).To(
Equal(cfg.GetGeneration()),
"expected status.observedGeneration (%d) to match updated metadata.generation (%d)",
cfg.Status.ObservedGeneration,
cfg.GetGeneration(),
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
})
var _ = Describe("RuleStatus observedGeneration is tracked in status", Ordered, Label("observedGeneration"), func() {
ctx := context.TODO()
var (
tnt *capsulev1beta2.Tenant
ns *corev1.Namespace
)
JustBeforeEach(func() {
tnt = &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-rulestatus-observed-gen",
},
Spec: capsulev1beta2.TenantSpec{
Owners: rbac.OwnerListSpec{
{
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: "e2e-rulestatus-observed-gen-owner",
Kind: "User",
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(ctx, tnt)
}).Should(Succeed())
TenantReadyTrue(tnt)
ns = NewNamespace("e2e-rulestatus-observed-gen-ns", map[string]string{
capmeta.TenantLabel: tnt.GetName(),
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
})
JustAfterEach(func() {
if ns != nil {
_ = k8sClient.Delete(ctx, ns)
}
EventuallyDeletion(tnt)
})
It("sets observedGeneration on RuleStatus after reconciliation", func() {
By("waiting for RuleStatus to exist and have observedGeneration set", func() {
Eventually(func(g Gomega) {
ruleStatus := &capsulev1beta2.RuleStatus{}
g.Expect(k8sClient.Get(ctx, client.ObjectKey{
Name: capmeta.NameForManagedRuleStatus(),
Namespace: ns.Name,
}, ruleStatus)).To(Succeed())
g.Expect(ruleStatus.Status.ObservedGeneration).To(
BeNumerically(">", int64(0)),
"expected status.observedGeneration to be non-zero",
)
g.Expect(ruleStatus.Status.ObservedGeneration).To(
Equal(ruleStatus.GetGeneration()),
"expected status.observedGeneration (%d) to equal metadata.generation (%d)",
ruleStatus.Status.ObservedGeneration,
ruleStatus.GetGeneration(),
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
})
+8
View File
@@ -1086,6 +1086,14 @@ func TenantReady(
current.Status.Owners,
)
}
g.Expect(current.Status.ObservedGeneration).To(
Equal(current.GetGeneration()),
"expected Tenant %q status.observedGeneration (%d) to equal metadata.generation (%d)",
tnt.GetName(),
current.Status.ObservedGeneration,
current.GetGeneration(),
)
}, timeoutInterval, defaultPollInterval).Should(Succeed())
}
@@ -194,6 +194,7 @@ func (r *Manager) updateConfigStatus(
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
return r.Client.Status().Update(ctx, latest)
})
@@ -298,6 +298,7 @@ func (r *customQuotaClaimController) updateStatus(
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
// Set Ready Condition
readyCondition := meta.NewReadyCondition(instance)
@@ -390,6 +390,7 @@ func (r *clusterCustomQuotaClaimController) updateStatus(
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
// Set Ready Condition
readyCondition := meta.NewReadyCondition(instance)
@@ -281,6 +281,7 @@ func (r *resourceClaimController) updateStatus(
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
readyCondition := meta.NewReadyCondition(latest)
if reconcileError != nil {
@@ -912,6 +912,7 @@ func (r *resourcePoolController) updateStatus(ctx context.Context, instance *cap
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
// Set Ready Condition
readyCondition := meta.NewReadyCondition(instance)
+1
View File
@@ -644,6 +644,7 @@ func (r *globalResourceController) updateStatus(ctx context.Context, instance *c
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
// Set Ready Condition
readyCondition := meta.NewReadyCondition(instance)
@@ -648,6 +648,7 @@ func (r *namespacedResourceController) updateStatus(ctx context.Context, instanc
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
// Set Ready Condition
readyCondition := meta.NewReadyCondition(instance)
@@ -162,6 +162,7 @@ func (r *Manager) updateStatus(ctx context.Context, instance *capsulev1beta2.Rul
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
// Set Ready Condition
readyCondition := meta.NewReadyCondition(instance)
+1
View File
@@ -56,6 +56,7 @@ func (r *Manager) updateTenantStatus(ctx context.Context, instance *capsulev1bet
}
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
setTenantStatusState(latest)
readyCondition := capmeta.NewReadyCondition(instance)