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
17 changes: 15 additions & 2 deletions api/v1beta2/capsuleconfiguration_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,27 @@
package v1beta2

import (
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
)

// CapsuleConfigurationStatus defines the Capsule configuration status.
type CapsuleConfigurationStatus struct {
// Users which are considered Capsule Users and are bound to the Capsule Tenant construct.
Users rbac.UserListSpec `json:"users,omitempty"`
// Conditions holds the reconciliation conditions for this CapsuleConfiguration.
// Includes a Ready condition indicating whether the configuration was
// successfully validated and applied.
// +optional
Conditions meta.ConditionList `json:"conditions,omitempty"`
// TenantCount is the total number of Tenant objects currently present in the cluster.
// +optional
TenantCount *int64 `json:"tenantCount,omitempty"`
// ManagedNamespaceCount is the total number of namespaces currently under Capsule
// management, aggregated from status.size across all Tenants.
// +optional
ManagedNamespaceCount *int64 `json:"managedNamespaceCount,omitempty"`
// 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"`
}
4 changes: 4 additions & 0 deletions api/v1beta2/capsuleconfiguration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ type ServiceAccountClient struct {
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:storageversion
// +kubebuilder:printcolumn:name="Tenants",type="integer",JSONPath=".status.tenantCount",description="Total number of Tenants"
// +kubebuilder:printcolumn:name="Namespaces",type="integer",JSONPath=".status.managedNamespaceCount",description="Total managed namespaces"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile status"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

// CapsuleConfiguration is the Schema for the Capsule configuration API.
type CapsuleConfiguration struct {
Expand Down
17 changes: 17 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,23 @@ spec:
singular: capsuleconfiguration
scope: Cluster
versions:
- name: v1beta2
- additionalPrinterColumns:
- description: Total number of Tenants
jsonPath: .status.tenantCount
name: Tenants
type: integer
- description: Total managed namespaces
jsonPath: .status.managedNamespaceCount
name: Namespaces
type: integer
- description: Reconcile status
jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1beta2
schema:
openAPIV3Schema:
description: CapsuleConfiguration is the Schema for the Capsule configuration
Expand Down Expand Up @@ -1250,11 +1266,82 @@ spec:
description: CapsuleConfigurationStatus defines the Capsule configuration
status.
properties:
conditions:
description: |-
Conditions holds the reconciliation conditions for this CapsuleConfiguration.
Includes a Ready condition indicating whether the configuration was
successfully validated and applied.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
managedNamespaceCount:
description: |-
ManagedNamespaceCount is the total number of namespaces currently under Capsule
management, aggregated from status.size across all Tenants.
format: int64
type: integer
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
tenantCount:
description: TenantCount is the total number of Tenant objects currently
present in the cluster.
format: int64
type: integer
users:
description: Users which are considered Capsule Users and are bound
to the Capsule Tenant construct.
Expand Down
186 changes: 186 additions & 0 deletions e2e/config_status_counters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// 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"
"sigs.k8s.io/controller-runtime/pkg/client"

capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api/rbac"
)

// configStatusCounters returns the current TenantCount and ManagedNamespaceCount
// from the default CapsuleConfiguration, treating a nil pointer as zero.
func configStatusCounters(g Gomega) (tenantCount, namespaceCount int64) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
g.Expect(k8sClient.Get(context.TODO(), client.ObjectKey{Name: defaultConfigurationName}, cfg)).To(Succeed())

if cfg.Status.TenantCount != nil {
tenantCount = *cfg.Status.TenantCount
}

if cfg.Status.ManagedNamespaceCount != nil {
namespaceCount = *cfg.Status.ManagedNamespaceCount
}

return tenantCount, namespaceCount
}

var _ = Describe("CapsuleConfiguration status counters", Ordered, Label("config", "status", "counters"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-cfg-counters-tnt",
Labels: map[string]string{
"env": "e2e",
},
},
Spec: capsulev1beta2.TenantSpec{
Owners: rbac.OwnerListSpec{
{
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: "e2e-cfg-counters-owner",
Kind: "User",
},
},
},
},
},
}
Comment on lines +37 to +56

ns := NewNamespace("e2e-cfg-counters-ns")

JustAfterEach(func() {
// Safety-net cleanup; EventuallyDeletion is idempotent for already-deleted objects.
EventuallyDeletion(ns)
EventuallyDeletion(tnt)
})

It("reflects Tenant create/delete and Namespace create/delete in status counters", func() {
var baseTenantCount, baseNSCount int64

// Wait for the controller to have populated the counters at least once.
Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
g.Expect(k8sClient.Get(context.TODO(), client.ObjectKey{Name: defaultConfigurationName}, cfg)).To(Succeed())
g.Expect(cfg.Status.TenantCount).NotTo(BeNil(), "TenantCount must be initialised by the controller before the test")

baseTenantCount = *cfg.Status.TenantCount

if cfg.Status.ManagedNamespaceCount != nil {
baseNSCount = *cfg.Status.ManagedNamespaceCount
}
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

By("creating a Tenant and asserting TenantCount increments by one")

EventuallyCreation(func() error {
tnt.ResourceVersion = ""

return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())

TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)

Eventually(func(g Gomega) {
tc, _ := configStatusCounters(g)
g.Expect(tc).To(Equal(baseTenantCount+1), "TenantCount should be baseTenantCount+1 after Tenant creation")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

By("creating a Namespace under the Tenant and asserting ManagedNamespaceCount increments by one")

NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceReady(tnt, ns, 1)

Eventually(func(g Gomega) {
_, nc := configStatusCounters(g)
g.Expect(nc).To(Equal(baseNSCount+1), "ManagedNamespaceCount should be baseNSCount+1 after Namespace creation")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

By("deleting the Namespace and asserting ManagedNamespaceCount returns to the baseline")

EventuallyDeletion(ns)

Eventually(func(g Gomega) {
_, nc := configStatusCounters(g)
g.Expect(nc).To(Equal(baseNSCount), "ManagedNamespaceCount should return to baseline after Namespace deletion")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

By("deleting the Tenant and asserting TenantCount returns to the baseline")

EventuallyDeletion(tnt)

Eventually(func(g Gomega) {
tc, _ := configStatusCounters(g)
g.Expect(tc).To(Equal(baseTenantCount), "TenantCount should return to baseline after Tenant deletion")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})

It("does not count Namespaces that belong to no Tenant", func() {
var baseNSCount int64

Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
g.Expect(k8sClient.Get(context.TODO(), client.ObjectKey{Name: defaultConfigurationName}, cfg)).To(Succeed())

if cfg.Status.ManagedNamespaceCount != nil {
baseNSCount = *cfg.Status.ManagedNamespaceCount
}
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

unmanaged := NewNamespace("e2e-cfg-counters-unmanaged")

DeferCleanup(func() {
EventuallyDeletion(unmanaged)
})

Expect(k8sClient.Create(context.TODO(), unmanaged)).To(Succeed())

// Wait long enough for a potential (spurious) reconcile to settle.
Consistently(func(g Gomega) {
_, nc := configStatusCounters(g)
g.Expect(nc).To(Equal(baseNSCount), "ManagedNamespaceCount must not change for unmanaged Namespaces")
}, "10s", defaultPollInterval).Should(Succeed())

EventuallyDeletion(unmanaged)
})

It("sets status.size on the Tenant reflecting the namespace count", func() {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""

return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
Comment on lines +156 to +160

TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)

ns1 := NewNamespace("e2e-cfg-counters-size-ns1")
ns2 := NewNamespace("e2e-cfg-counters-size-ns2")

owner := tnt.Spec.Owners[0].UserSpec

DeferCleanup(func() {
for _, n := range []*corev1.Namespace{ns1, ns2} {
EventuallyDeletion(n)
}
})

NamespaceCreation(ns1, owner, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceReady(tnt, ns1, 1)

NamespaceCreation(ns2, owner, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceReady(tnt, ns2, 2)

Eventually(func(g Gomega) {
_, nc := configStatusCounters(g)
g.Expect(nc).To(BeNumerically(">=", int64(2)), "ManagedNamespaceCount should include both namespaces")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
Loading
Loading