Skip to content

Commit 3feafed

Browse files
committed
feat: add tenant list to status of capsuleconfiguration
Signed-off-by: sandert-k8s <sandert98@gmail.com>
1 parent d255c33 commit 3feafed

8 files changed

Lines changed: 299 additions & 6 deletions

File tree

DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ kubectl patch crd capsuleconfigurations.capsule.clastix.io \
172172
When the Development Environment is set up, we can run Capsule controllers with webhooks outside of the Kubernetes cluster:
173173

174174
```bash
175-
$ export NAMESPACE=capsule-system && export TMPDIR=/tmp/
175+
$ export NAMESPACE=capsule-system && export TMPDIR=/tmp/ && export SERVICE_ACCOUNT=capsule
176176
$ go run .
177177
```
178178

api/v1beta2/capsuleconfiguration_status.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,25 @@
44
package v1beta2
55

66
import (
7+
"github.com/projectcapsule/capsule/pkg/api/meta"
78
"github.com/projectcapsule/capsule/pkg/api/rbac"
89
)
910

1011
// CapsuleConfigurationStatus defines the Capsule configuration status.
1112
type CapsuleConfigurationStatus struct {
13+
// Users which are considered Capsule Users and are bound to the Capsule Tenant construct.
14+
Users rbac.UserListSpec `json:"users,omitempty"`
15+
// Conditions holds the reconciliation conditions for this CapsuleConfiguration.
16+
// Includes a Ready condition indicating whether the configuration was
17+
// successfully validated and applied.
18+
// +optional
19+
Conditions meta.ConditionList `json:"conditions,omitempty"`
20+
// Tenants is the sorted list of Tenant names currently present in the cluster.
21+
// The total count is available via len(Tenants).
22+
// +listType=atomic
23+
// +optional
24+
Tenants []string `json:"tenants,omitempty"`
1225
// ObservedGeneration is the most recent generation the controller has observed.
1326
// +optional
1427
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
15-
// Users which are considered Capsule Users and are bound to the Capsule Tenant construct.
16-
Users rbac.UserListSpec `json:"users,omitempty"`
1728
}

api/v1beta2/capsuleconfiguration_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ type ServiceAccountClient struct {
172172
// +kubebuilder:subresource:status
173173
// +kubebuilder:resource:scope=Cluster
174174
// +kubebuilder:storageversion
175+
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile status"
176+
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
175177

176178
// CapsuleConfiguration is the Schema for the Capsule configuration API.
177179
type CapsuleConfiguration struct {

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ spec:
1414
singular: capsuleconfiguration
1515
scope: Cluster
1616
versions:
17-
- name: v1beta2
17+
- additionalPrinterColumns:
18+
- description: Reconcile status
19+
jsonPath: .status.conditions[?(@.type=="Ready")].status
20+
name: Ready
21+
type: string
22+
- jsonPath: .metadata.creationTimestamp
23+
name: Age
24+
type: date
25+
name: v1beta2
1826
schema:
1927
openAPIV3Schema:
2028
description: CapsuleConfiguration is the Schema for the Capsule configuration
@@ -1250,11 +1258,79 @@ spec:
12501258
description: CapsuleConfigurationStatus defines the Capsule configuration
12511259
status.
12521260
properties:
1261+
conditions:
1262+
description: |-
1263+
Conditions holds the reconciliation conditions for this CapsuleConfiguration.
1264+
Includes a Ready condition indicating whether the configuration was
1265+
successfully validated and applied.
1266+
items:
1267+
description: Condition contains details for one aspect of the current
1268+
state of this API Resource.
1269+
properties:
1270+
lastTransitionTime:
1271+
description: |-
1272+
lastTransitionTime is the last time the condition transitioned from one status to another.
1273+
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
1274+
format: date-time
1275+
type: string
1276+
message:
1277+
description: |-
1278+
message is a human readable message indicating details about the transition.
1279+
This may be an empty string.
1280+
maxLength: 32768
1281+
type: string
1282+
observedGeneration:
1283+
description: |-
1284+
observedGeneration represents the .metadata.generation that the condition was set based upon.
1285+
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
1286+
with respect to the current state of the instance.
1287+
format: int64
1288+
minimum: 0
1289+
type: integer
1290+
reason:
1291+
description: |-
1292+
reason contains a programmatic identifier indicating the reason for the condition's last transition.
1293+
Producers of specific condition types may define expected values and meanings for this field,
1294+
and whether the values are considered a guaranteed API.
1295+
The value should be a CamelCase string.
1296+
This field may not be empty.
1297+
maxLength: 1024
1298+
minLength: 1
1299+
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
1300+
type: string
1301+
status:
1302+
description: status of the condition, one of True, False, Unknown.
1303+
enum:
1304+
- "True"
1305+
- "False"
1306+
- Unknown
1307+
type: string
1308+
type:
1309+
description: type of condition in CamelCase or in foo.example.com/CamelCase.
1310+
maxLength: 316
1311+
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])$
1312+
type: string
1313+
required:
1314+
- lastTransitionTime
1315+
- message
1316+
- reason
1317+
- status
1318+
- type
1319+
type: object
1320+
type: array
12531321
observedGeneration:
12541322
description: ObservedGeneration is the most recent generation the
12551323
controller has observed.
12561324
format: int64
12571325
type: integer
1326+
tenants:
1327+
description: |-
1328+
Tenants is the sorted list of Tenant names currently present in the cluster.
1329+
The total count is available via len(Tenants).
1330+
items:
1331+
type: string
1332+
type: array
1333+
x-kubernetes-list-type: atomic
12581334
users:
12591335
description: Users which are considered Capsule Users and are bound
12601336
to the Capsule Tenant construct.

e2e/config_status_tenants_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2020-2026 Project Capsule Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package e2e
5+
6+
import (
7+
"context"
8+
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
14+
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
15+
capmeta "github.com/projectcapsule/capsule/pkg/api/meta"
16+
"github.com/projectcapsule/capsule/pkg/api/rbac"
17+
)
18+
19+
// configStatus returns the current CapsuleConfiguration status.
20+
func configStatus(g Gomega) capsulev1beta2.CapsuleConfigurationStatus {
21+
cfg := &capsulev1beta2.CapsuleConfiguration{}
22+
g.Expect(k8sClient.Get(context.TODO(), client.ObjectKey{Name: defaultConfigurationName}, cfg)).To(Succeed())
23+
24+
return cfg.Status
25+
}
26+
27+
// configTenantStatus returns the current Tenants list from the default CapsuleConfiguration.
28+
func configTenantStatus(g Gomega) []string {
29+
return configStatus(g).Tenants
30+
}
31+
32+
var _ = Describe("CapsuleConfiguration status tenants", Ordered, Label("config", "status", "tenants"), func() {
33+
tnt := &capsulev1beta2.Tenant{
34+
ObjectMeta: metav1.ObjectMeta{
35+
Name: "e2e-cfg-tenants-tnt",
36+
Labels: map[string]string{
37+
"env": "e2e",
38+
},
39+
},
40+
Spec: capsulev1beta2.TenantSpec{
41+
Owners: rbac.OwnerListSpec{
42+
{
43+
CoreOwnerSpec: rbac.CoreOwnerSpec{
44+
UserSpec: rbac.UserSpec{
45+
Name: "e2e-cfg-tenants-owner",
46+
Kind: "User",
47+
},
48+
},
49+
},
50+
},
51+
},
52+
}
53+
54+
JustAfterEach(func() {
55+
// Safety-net cleanup; EventuallyDeletion is idempotent for already-deleted objects.
56+
EventuallyDeletion(tnt)
57+
})
58+
59+
It("reflects Tenant create/delete in status.tenants list", func() {
60+
var baseNames []string
61+
62+
// Wait for the controller to have populated the tenants list at least once.
63+
Eventually(func(g Gomega) {
64+
baseNames = configTenantStatus(g)
65+
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
66+
67+
By("creating a Tenant and asserting its name appears in status.tenants")
68+
69+
EventuallyCreation(func() error {
70+
tnt.ResourceVersion = ""
71+
72+
return k8sClient.Create(context.TODO(), tnt)
73+
}).Should(Succeed())
74+
75+
TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)
76+
77+
Eventually(func(g Gomega) {
78+
names := configTenantStatus(g)
79+
g.Expect(names).To(HaveLen(len(baseNames)+1), "status.tenants should grow by one after Tenant creation")
80+
g.Expect(names).To(ContainElement(tnt.Name), "status.tenants should contain the new Tenant name")
81+
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
82+
83+
By("deleting the Tenant and asserting its name is removed from status.tenants")
84+
85+
EventuallyDeletion(tnt)
86+
87+
Eventually(func(g Gomega) {
88+
names := configTenantStatus(g)
89+
g.Expect(names).To(HaveLen(len(baseNames)), "status.tenants should return to baseline after Tenant deletion")
90+
g.Expect(names).NotTo(ContainElement(tnt.Name), "status.tenants must not contain the deleted Tenant name")
91+
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
92+
})
93+
})
94+
95+
var _ = Describe("CapsuleConfiguration status Ready condition", Ordered, Label("config", "status", "ready"), func() {
96+
It("has Ready=True with Reason=Succeeded after a successful reconcile", func() {
97+
// After the controller has reconciled at least once the Ready condition
98+
// must be True with the Succeeded reason. This guards against regressions
99+
// where a prior failure leaves the condition stuck at Ready=False.
100+
Eventually(func(g Gomega) {
101+
st := configStatus(g)
102+
cond := st.Conditions.GetConditionByType(capmeta.ReadyCondition)
103+
g.Expect(cond).NotTo(BeNil(), "Ready condition must be present")
104+
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue), "Ready condition must be True after successful reconcile")
105+
g.Expect(cond.Reason).To(Equal(capmeta.SucceededReason), "Ready condition reason must be Succeeded")
106+
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
107+
})
108+
})

0 commit comments

Comments
 (0)