Skip to content

Commit 36e4343

Browse files
committed
Add OpenStackAuthenticationSucceded condition
- Sets OpenStackAuthenticationSucceededCondition to True/False based on credential validity during reconciliation. - Adds tests for condition handling on credential errors and missing secrets. The condition is set for OpenStackCluster, OpenStackMachine, OpenStackServer and OpenStackFloatingIPPool. OpenStackMachineTemplate is currently missing conditions completely. These will be added separately. Signed-off-by: Lennart Jern <lennart.jern@est.tech>
1 parent 9e848d9 commit 36e4343

9 files changed

Lines changed: 689 additions & 23 deletions

api/v1beta1/conditions_consts.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ const (
7272
PortCreateFailedReason = "PortCreateFailed"
7373
)
7474

75+
const (
76+
// OpenStackAuthenticationSucceeded reports on the current status of the OpenStack credentials.
77+
OpenStackAuthenticationSucceeded clusterv1beta1.ConditionType = "OpenStackAuthenticationSucceeded"
78+
79+
// OpenStackAuthenticationFailedReason is used when the controller fails to authenticate with OpenStack.
80+
OpenStackAuthenticationFailedReason = "OpenStackAuthenticationFailed"
81+
)
82+
7583
const (
7684
// NetworkReadyCondition reports on the current status of the cluster network infrastructure.
7785
// Ready indicates that the network, subnets, and related resources have been successfully provisioned.

controllers/openstackcluster_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ func (r *OpenStackClusterReconciler) Reconcile(ctx context.Context, req ctrl.Req
124124

125125
clientScope, err := r.ScopeFactory.NewClientScopeFromObject(ctx, r.Client, r.CaCertificates, log, openStackCluster)
126126
if err != nil {
127+
v1beta1conditions.MarkFalse(openStackCluster, infrav1.OpenStackAuthenticationSucceeded, infrav1.OpenStackAuthenticationFailedReason, clusterv1beta1.ConditionSeverityError, "Failed to create OpenStack client scope: %v", err)
127128
return reconcile.Result{}, err
128129
}
130+
v1beta1conditions.MarkTrue(openStackCluster, infrav1.OpenStackAuthenticationSucceeded)
129131
scope := scope.NewWithLogger(clientScope, log)
130132

131133
// Handle deleted clusters

controllers/openstackcluster_controller_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ var _ = Describe("OpenStackCluster controller", func() {
224224
Expect(created.Spec.IdentityRef.Region).To(Equal("RegionOne"))
225225
})
226226

227-
It("should fail when namespace is denied access to ClusterIdentity", func() {
227+
It("should fail when namespace is denied access to ClusterIdentity and set OpenStackAuthenticationSucceededCondition to False", func() {
228228
testCluster.SetName("identity-access-denied")
229229
testCluster.Spec.IdentityRef = infrav1.OpenStackIdentityReference{
230230
Type: "ClusterIdentity",
@@ -248,6 +248,53 @@ var _ = Describe("OpenStackCluster controller", func() {
248248

249249
Expect(err).To(MatchError(identityAccessErr))
250250
Expect(result).To(Equal(reconcile.Result{}))
251+
252+
// Fetch the updated OpenStackCluster to verify the condition was set
253+
updatedCluster := &infrav1.OpenStackCluster{}
254+
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: testCluster.Name, Namespace: testCluster.Namespace}, updatedCluster)).To(Succeed())
255+
256+
// Verify OpenStackAuthenticationSucceededCondition is set to False
257+
Expect(v1beta1conditions.IsFalse(updatedCluster, infrav1.OpenStackAuthenticationSucceeded)).To(BeTrue())
258+
condition := v1beta1conditions.Get(updatedCluster, infrav1.OpenStackAuthenticationSucceeded)
259+
Expect(condition).ToNot(BeNil())
260+
Expect(condition.Reason).To(Equal(infrav1.OpenStackAuthenticationFailedReason))
261+
Expect(condition.Severity).To(Equal(clusterv1beta1.ConditionSeverityError))
262+
Expect(condition.Message).To(ContainSubstring("Failed to create OpenStack client scope"))
263+
})
264+
265+
It("should set OpenStackAuthenticationSucceededCondition to False when credentials secret is missing", func() {
266+
testCluster.SetName("missing-credentials")
267+
testCluster.Spec.IdentityRef = infrav1.OpenStackIdentityReference{
268+
Type: "Secret",
269+
Name: "non-existent-secret",
270+
CloudName: "openstack",
271+
}
272+
273+
err := k8sClient.Create(ctx, testCluster)
274+
Expect(err).To(BeNil())
275+
err = k8sClient.Create(ctx, capiCluster)
276+
Expect(err).To(BeNil())
277+
278+
credentialsErr := fmt.Errorf("secret not found: non-existent-secret")
279+
mockScopeFactory.SetClientScopeCreateError(credentialsErr)
280+
281+
req := createRequestFromOSCluster(testCluster)
282+
result, err := reconciler.Reconcile(ctx, req)
283+
284+
Expect(err).To(MatchError(credentialsErr))
285+
Expect(result).To(Equal(reconcile.Result{}))
286+
287+
// Fetch the updated OpenStackCluster to verify the condition was set
288+
updatedCluster := &infrav1.OpenStackCluster{}
289+
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: testCluster.Name, Namespace: testCluster.Namespace}, updatedCluster)).To(Succeed())
290+
291+
// Verify OpenStackAuthenticationSucceededCondition is set to False
292+
Expect(v1beta1conditions.IsFalse(updatedCluster, infrav1.OpenStackAuthenticationSucceeded)).To(BeTrue())
293+
condition := v1beta1conditions.Get(updatedCluster, infrav1.OpenStackAuthenticationSucceeded)
294+
Expect(condition).ToNot(BeNil())
295+
Expect(condition.Reason).To(Equal(infrav1.OpenStackAuthenticationFailedReason))
296+
Expect(condition.Severity).To(Equal(clusterv1beta1.ConditionSeverityError))
297+
Expect(condition.Message).To(ContainSubstring("Failed to create OpenStack client scope"))
251298
})
252299

253300
It("should reject updates that modify identityRef.region (immutable)", func() {

controllers/openstackfloatingippool_controller.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,25 @@ func (r *OpenStackFloatingIPPoolReconciler) Reconcile(ctx context.Context, req c
8080
return ctrl.Result{}, client.IgnoreNotFound(err)
8181
}
8282

83+
patchHelper, err := patch.NewHelper(pool, r.Client)
84+
if err != nil {
85+
return ctrl.Result{}, err
86+
}
87+
88+
defer func() {
89+
if err := patchHelper.Patch(ctx, pool); err != nil {
90+
if reterr == nil {
91+
reterr = fmt.Errorf("error patching OpenStackFloatingIPPool %s/%s: %w", pool.Namespace, pool.Name, err)
92+
}
93+
}
94+
}()
95+
8396
clientScope, err := r.ScopeFactory.NewClientScopeFromObject(ctx, r.Client, r.CaCertificates, log, pool)
8497
if err != nil {
98+
v1beta1conditions.MarkFalse(pool, infrav1.OpenStackAuthenticationSucceeded, infrav1.OpenStackAuthenticationFailedReason, clusterv1beta1.ConditionSeverityError, "Failed to create OpenStack client scope: %v", err)
8599
return reconcile.Result{}, err
86100
}
101+
v1beta1conditions.MarkTrue(pool, infrav1.OpenStackAuthenticationSucceeded)
87102
scope := scope.NewWithLogger(clientScope, log)
88103

89104
// This is done before deleting the pool, because we want to handle deleted IPs before we delete the pool
@@ -101,19 +116,6 @@ func (r *OpenStackFloatingIPPoolReconciler) Reconcile(ctx context.Context, req c
101116
return ctrl.Result{}, r.reconcileDelete(ctx, scope, pool)
102117
}
103118

104-
patchHelper, err := patch.NewHelper(pool, r.Client)
105-
if err != nil {
106-
return ctrl.Result{}, err
107-
}
108-
109-
defer func() {
110-
if err := patchHelper.Patch(ctx, pool); err != nil {
111-
if reterr == nil {
112-
reterr = fmt.Errorf("error patching OpenStackFloatingIPPool %s/%s: %w", pool.Namespace, pool.Name, err)
113-
}
114-
}
115-
}()
116-
117119
if err := r.reconcileFloatingIPNetwork(scope, pool); err != nil {
118120
return ctrl.Result{}, err
119121
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"fmt"
21+
22+
. "github.com/onsi/ginkgo/v2" //nolint:revive
23+
. "github.com/onsi/gomega" //nolint:revive
24+
"go.uber.org/mock/gomock"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1"
27+
"sigs.k8s.io/cluster-api/test/framework"
28+
v1beta1conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions"
29+
"sigs.k8s.io/cluster-api/util/patch"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
32+
33+
infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
34+
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
35+
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
36+
)
37+
38+
var _ = Describe("OpenStackFloatingIPPool controller", func() {
39+
var (
40+
testPool *infrav1alpha1.OpenStackFloatingIPPool
41+
testNamespace string
42+
poolReconciler *OpenStackFloatingIPPoolReconciler
43+
poolMockCtrl *gomock.Controller
44+
poolMockFactory *scope.MockScopeFactory
45+
testNum int
46+
)
47+
48+
BeforeEach(func() {
49+
testNum++
50+
testNamespace = fmt.Sprintf("pool-test-%d", testNum)
51+
52+
testPool = &infrav1alpha1.OpenStackFloatingIPPool{
53+
TypeMeta: metav1.TypeMeta{
54+
APIVersion: infrav1alpha1.SchemeGroupVersion.Group + "/" + infrav1alpha1.SchemeGroupVersion.Version,
55+
Kind: "OpenStackFloatingIPPool",
56+
},
57+
ObjectMeta: metav1.ObjectMeta{
58+
Name: "test-pool",
59+
Namespace: testNamespace,
60+
},
61+
Spec: infrav1alpha1.OpenStackFloatingIPPoolSpec{
62+
IdentityRef: infrav1.OpenStackIdentityReference{
63+
Name: "test-creds",
64+
CloudName: "openstack",
65+
},
66+
ReclaimPolicy: infrav1alpha1.ReclaimDelete,
67+
},
68+
}
69+
70+
input := framework.CreateNamespaceInput{
71+
Creator: k8sClient,
72+
Name: testNamespace,
73+
}
74+
framework.CreateNamespace(ctx, input)
75+
76+
poolMockCtrl = gomock.NewController(GinkgoT())
77+
poolMockFactory = scope.NewMockScopeFactory(poolMockCtrl, "")
78+
poolReconciler = &OpenStackFloatingIPPoolReconciler{
79+
Client: k8sClient,
80+
ScopeFactory: poolMockFactory,
81+
}
82+
})
83+
84+
AfterEach(func() {
85+
orphan := metav1.DeletePropagationOrphan
86+
deleteOptions := client.DeleteOptions{
87+
PropagationPolicy: &orphan,
88+
}
89+
90+
// Remove finalizers and delete openstackfloatingippool
91+
patchHelper, err := patch.NewHelper(testPool, k8sClient)
92+
Expect(err).To(BeNil())
93+
testPool.SetFinalizers([]string{})
94+
err = patchHelper.Patch(ctx, testPool)
95+
Expect(err).To(BeNil())
96+
err = k8sClient.Delete(ctx, testPool, &deleteOptions)
97+
Expect(err).To(BeNil())
98+
})
99+
100+
It("should set OpenStackAuthenticationSucceededCondition to False when credentials secret is missing", func() {
101+
testPool.SetName("missing-pool-credentials")
102+
testPool.Spec.IdentityRef = infrav1.OpenStackIdentityReference{
103+
Type: "Secret",
104+
Name: "non-existent-secret",
105+
CloudName: "openstack",
106+
}
107+
108+
err := k8sClient.Create(ctx, testPool)
109+
Expect(err).To(BeNil())
110+
111+
credentialsErr := fmt.Errorf("secret not found: non-existent-secret")
112+
poolMockFactory.SetClientScopeCreateError(credentialsErr)
113+
114+
req := reconcile.Request{
115+
NamespacedName: client.ObjectKey{
116+
Name: testPool.Name,
117+
Namespace: testPool.Namespace,
118+
},
119+
}
120+
result, err := poolReconciler.Reconcile(ctx, req)
121+
122+
Expect(err).To(MatchError(credentialsErr))
123+
Expect(result).To(Equal(reconcile.Result{}))
124+
125+
// Fetch the updated OpenStackFloatingIPPool to verify the condition was set
126+
updatedPool := &infrav1alpha1.OpenStackFloatingIPPool{}
127+
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: testPool.Name, Namespace: testPool.Namespace}, updatedPool)).To(Succeed())
128+
129+
// Verify OpenStackAuthenticationSucceededCondition is set to False
130+
Expect(v1beta1conditions.IsFalse(updatedPool, infrav1.OpenStackAuthenticationSucceeded)).To(BeTrue())
131+
condition := v1beta1conditions.Get(updatedPool, infrav1.OpenStackAuthenticationSucceeded)
132+
Expect(condition).ToNot(BeNil())
133+
Expect(condition.Reason).To(Equal(infrav1.OpenStackAuthenticationFailedReason))
134+
Expect(condition.Severity).To(Equal(clusterv1beta1.ConditionSeverityError))
135+
Expect(condition.Message).To(ContainSubstring("Failed to create OpenStack client scope"))
136+
})
137+
138+
It("should set OpenStackAuthenticationSucceededCondition to False when namespace is denied access to ClusterIdentity", func() {
139+
testPool.SetName("identity-access-denied-pool")
140+
testPool.Spec.IdentityRef = infrav1.OpenStackIdentityReference{
141+
Type: "ClusterIdentity",
142+
Name: "test-cluster-identity",
143+
CloudName: "openstack",
144+
}
145+
146+
err := k8sClient.Create(ctx, testPool)
147+
Expect(err).To(BeNil())
148+
149+
identityAccessErr := &scope.IdentityAccessDeniedError{
150+
IdentityName: "test-cluster-identity",
151+
RequesterNamespace: testNamespace,
152+
}
153+
poolMockFactory.SetClientScopeCreateError(identityAccessErr)
154+
155+
req := reconcile.Request{
156+
NamespacedName: client.ObjectKey{
157+
Name: testPool.Name,
158+
Namespace: testPool.Namespace,
159+
},
160+
}
161+
result, err := poolReconciler.Reconcile(ctx, req)
162+
163+
Expect(err).To(MatchError(identityAccessErr))
164+
Expect(result).To(Equal(reconcile.Result{}))
165+
166+
// Fetch the updated OpenStackFloatingIPPool to verify the condition was set
167+
updatedPool := &infrav1alpha1.OpenStackFloatingIPPool{}
168+
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: testPool.Name, Namespace: testPool.Namespace}, updatedPool)).To(Succeed())
169+
170+
// Verify OpenStackAuthenticationSucceededCondition is set to False
171+
Expect(v1beta1conditions.IsFalse(updatedPool, infrav1.OpenStackAuthenticationSucceeded)).To(BeTrue())
172+
condition := v1beta1conditions.Get(updatedPool, infrav1.OpenStackAuthenticationSucceeded)
173+
Expect(condition).ToNot(BeNil())
174+
Expect(condition.Reason).To(Equal(infrav1.OpenStackAuthenticationFailedReason))
175+
Expect(condition.Severity).To(Equal(clusterv1beta1.ConditionSeverityError))
176+
Expect(condition.Message).To(ContainSubstring("Failed to create OpenStack client scope"))
177+
})
178+
})

controllers/openstackmachine_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ func (r *OpenStackMachineReconciler) Reconcile(ctx context.Context, req ctrl.Req
150150

151151
clientScope, err := r.ScopeFactory.NewClientScopeFromObject(ctx, r.Client, r.CaCertificates, log, openStackMachine, infraCluster)
152152
if err != nil {
153+
v1beta1conditions.MarkFalse(openStackMachine, infrav1.OpenStackAuthenticationSucceeded, infrav1.OpenStackAuthenticationFailedReason, clusterv1beta1.ConditionSeverityError, "Failed to create OpenStack client scope: %v", err)
153154
return reconcile.Result{}, err
154155
}
156+
v1beta1conditions.MarkTrue(openStackMachine, infrav1.OpenStackAuthenticationSucceeded)
155157
scope := scope.NewWithLogger(clientScope, log)
156158

157159
clusterResourceName := fmt.Sprintf("%s-%s", cluster.Namespace, cluster.Name)

0 commit comments

Comments
 (0)