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
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ kubectl patch crd capsuleconfigurations.capsule.clastix.io \
When the Development Environment is set up, we can run Capsule controllers with webhooks outside of the Kubernetes cluster:

```bash
$ export NAMESPACE=capsule-system && export TMPDIR=/tmp/
$ export NAMESPACE=capsule-system && export TMPDIR=/tmp/ && export SERVICE_ACCOUNT=capsule
$ go run .
```

Expand Down
15 changes: 13 additions & 2 deletions api/v1beta2/capsuleconfiguration_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@
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"`
Comment thread
sandert-k8s marked this conversation as resolved.
Comment thread
sandert-k8s marked this conversation as resolved.
Comment thread
sandert-k8s marked this conversation as resolved.
// Tenants is the sorted list of Tenant names currently present in the cluster.
// The total count is available via len(Tenants).
// +listType=atomic
// +optional
Tenants []string `json:"tenants,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"`
}
2 changes: 2 additions & 0 deletions api/v1beta2/capsuleconfiguration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ type ServiceAccountClient struct {
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:storageversion
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile status"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
Comment on lines +175 to +176

// CapsuleConfiguration is the Schema for the Capsule configuration API.
type CapsuleConfiguration struct {
Expand Down
12 changes: 12 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,15 @@ spec:
singular: capsuleconfiguration
scope: Cluster
versions:
- name: v1beta2
- additionalPrinterColumns:
- 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 +1258,79 @@ 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:
Comment on lines +1261 to +1266
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
Comment on lines +1319 to +1320
observedGeneration:
description: ObservedGeneration is the most recent generation the
controller has observed.
format: int64
type: integer
tenants:
description: |-
Tenants is the sorted list of Tenant names currently present in the cluster.
The total count is available via len(Tenants).
items:
type: string
type: array
x-kubernetes-list-type: atomic
users:
description: Users which are considered Capsule Users and are bound
to the Capsule Tenant construct.
Expand Down
111 changes: 111 additions & 0 deletions e2e/config_status_tenants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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"
)

// configStatus returns the current CapsuleConfiguration status.
func configStatus(g Gomega) capsulev1beta2.CapsuleConfigurationStatus {
cfg := &capsulev1beta2.CapsuleConfiguration{}
g.Expect(k8sClient.Get(context.TODO(), client.ObjectKey{Name: defaultConfigurationName}, cfg)).To(Succeed())

return cfg.Status
}

// configTenantStatus returns the current Tenants list from the default CapsuleConfiguration.
func configTenantStatus(g Gomega) []string {
return configStatus(g).Tenants
}

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

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

It("reflects Tenant create/delete in status.tenants list", func() {
var baseNames []string

// Wait for the controller to have populated the tenants list at least once.
// status.tenants is nil until the first reconcile runs; a non-nil slice
// (even empty) means the controller has initialised it.
Eventually(func(g Gomega) {
baseNames = configTenantStatus(g)
g.Expect(baseNames).NotTo(BeNil(), "status.tenants must be initialized before taking baseline")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
Comment thread
sandert-k8s marked this conversation as resolved.

By("creating a Tenant and asserting its name appears in status.tenants")

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

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

TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)

Eventually(func(g Gomega) {
names := configTenantStatus(g)
g.Expect(names).To(HaveLen(len(baseNames)+1), "status.tenants should grow by one after Tenant creation")
g.Expect(names).To(ContainElement(tnt.Name), "status.tenants should contain the new Tenant name")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

By("deleting the Tenant and asserting its name is removed from status.tenants")

EventuallyDeletion(tnt)

Eventually(func(g Gomega) {
names := configTenantStatus(g)
g.Expect(names).To(HaveLen(len(baseNames)), "status.tenants should return to baseline after Tenant deletion")
g.Expect(names).NotTo(ContainElement(tnt.Name), "status.tenants must not contain the deleted Tenant name")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})

var _ = Describe("CapsuleConfiguration status Ready condition", Ordered, Label("config", "status", "ready"), func() {
It("has Ready=True with Reason=Succeeded after a successful reconcile", func() {
// After the controller has reconciled at least once the Ready condition
// must be True with the Succeeded reason. This guards against regressions
// where a prior failure leaves the condition stuck at Ready=False.
Eventually(func(g Gomega) {
st := configStatus(g)
cond := st.Conditions.GetConditionByType(capmeta.ReadyCondition)
g.Expect(cond).NotTo(BeNil(), "Ready condition must be present")
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue), "Ready condition must be True after successful reconcile")
g.Expect(cond.Reason).To(Equal(capmeta.SucceededReason), "Ready condition reason must be Succeeded")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
Loading
Loading