Skip to content

Commit ea12f78

Browse files
authored
Merge pull request #2985 from Nordix/lentzi90/credentials-condition
🌱 Add OpenStackAuthenticationSucceeded condition
2 parents 9e848d9 + 36e4343 commit ea12f78

9 files changed

+689
-23
lines changed

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)