Skip to content

Commit 1f6fd26

Browse files
committed
tests: unit tests for provider.go
Signed-off-by: Bharath Nallapeta <[email protected]>
1 parent 2702e07 commit 1f6fd26

File tree

2 files changed

+479
-0
lines changed

2 files changed

+479
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
Copyright 2025 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 scope
18+
19+
import (
20+
"context"
21+
"errors"
22+
"strings"
23+
"testing"
24+
25+
"github.com/go-logr/logr"
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
"k8s.io/client-go/kubernetes/scheme"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
32+
33+
infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
34+
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
35+
)
36+
37+
const (
38+
testNSA = "ns-a"
39+
testNSAllowed = "allowed-team"
40+
testNSTeamY = "team-y"
41+
testNSTeamZ = "team-z"
42+
testNSTeamW = "team-w"
43+
testNSTeamA = "team-a"
44+
testNSCapo = "capo-system"
45+
resTestCloudName = "mycloud"
46+
testProdCloud = "prodcloud"
47+
)
48+
49+
var (
50+
testValidCloudsYAML = []byte(`clouds:
51+
mycloud:
52+
auth:
53+
auth_url: https://keystone.example.com/
54+
application_credential_id: id
55+
application_credential_secret: secret
56+
region_name: RegionOne
57+
`)
58+
testProdCloudsYAML = []byte(`clouds:
59+
prodcloud:
60+
auth:
61+
auth_url: https://keystone.prod.com/
62+
application_credential_id: prod-id
63+
application_credential_secret: prod-secret
64+
region_name: RegionOne
65+
`)
66+
testEmptyCloudsYAML = []byte("clouds: {}\n")
67+
testDefaultCloudsYAML = []byte("clouds: { default: {} }\n")
68+
)
69+
70+
func ensureSchemes(t *testing.T) *runtime.Scheme {
71+
t.Helper()
72+
local := runtime.NewScheme()
73+
if err := scheme.AddToScheme(local); err != nil {
74+
t.Fatalf("failed to add core scheme: %v", err)
75+
}
76+
if err := infrav1.AddToScheme(local); err != nil {
77+
t.Fatalf("failed to add v1beta1 scheme: %v", err)
78+
}
79+
if err := infrav1alpha1.AddToScheme(local); err != nil {
80+
t.Fatalf("failed to add v1alpha1 scheme: %v", err)
81+
}
82+
return local
83+
}
84+
85+
func createResTestSecret(namespace, name string, data map[string][]byte) *corev1.Secret {
86+
secret := &corev1.Secret{}
87+
secret.Namespace = namespace
88+
secret.Name = name
89+
secret.Data = data
90+
return secret
91+
}
92+
93+
func createTestNamespace(name string, labels map[string]string) *corev1.Namespace {
94+
ns := &corev1.Namespace{}
95+
ns.Name = name
96+
ns.Labels = labels
97+
return ns
98+
}
99+
100+
func createTestClusterIdentity(name string, selector *metav1.LabelSelector) *infrav1alpha1.OpenStackClusterIdentity {
101+
identity := &infrav1alpha1.OpenStackClusterIdentity{}
102+
identity.Name = name
103+
identity.Spec.SecretRef = infrav1alpha1.OpenStackCredentialSecretReference{
104+
Name: "creds",
105+
Namespace: testNSCapo,
106+
}
107+
identity.Spec.NamespaceSelector = selector
108+
return identity
109+
}
110+
111+
// newFakeClient creates a fake client with the provided scheme and objects.
112+
func newFakeClient(sch *runtime.Scheme, objs ...client.Object) client.Client {
113+
return fake.NewClientBuilder().WithScheme(sch).WithObjects(objs...).Build()
114+
}
115+
116+
// assertResolutionReached ensures the error is not caused by credential resolution (missing secret/namespace or access denied).
117+
// We still expect an error (typically from OpenStack auth) when running full scope creation with the real factory.
118+
func assertResolutionReached(t *testing.T, err error) {
119+
t.Helper()
120+
if err == nil {
121+
t.Fatalf("expected OpenStack auth error, got success")
122+
}
123+
if strings.Contains(err.Error(), "secret") && strings.Contains(err.Error(), "not found") {
124+
t.Fatalf("credential resolution failed: %v", err)
125+
}
126+
var denied *IdentityAccessDeniedError
127+
if errors.As(err, &denied) {
128+
t.Fatalf("credential resolution failed: %v", err)
129+
}
130+
}
131+
132+
func assertDenied(t *testing.T, err error) {
133+
t.Helper()
134+
var denied *IdentityAccessDeniedError
135+
if err == nil || !errors.As(err, &denied) {
136+
t.Fatalf("expected IdentityAccessDeniedError, got %T %v", err, err)
137+
}
138+
}
139+
140+
func assertNotDenied(t *testing.T, err error) {
141+
t.Helper()
142+
var denied *IdentityAccessDeniedError
143+
if errors.As(err, &denied) {
144+
t.Fatalf("did not expect IdentityAccessDeniedError, got %v", err)
145+
}
146+
}
147+
148+
func TestNewClientScopeFromObject_Resolution(t *testing.T) {
149+
t.Parallel()
150+
localScheme := ensureSchemes(t)
151+
type testCase struct {
152+
name string
153+
objects []client.Object
154+
namespace string
155+
identity infrav1.OpenStackIdentityReference
156+
assertErr func(*testing.T, error)
157+
}
158+
159+
cases := []testCase{
160+
{
161+
name: "secret path returns scope",
162+
objects: []client.Object{
163+
createResTestSecret(testNSA, "valid-creds", map[string][]byte{CloudsSecretKey: testValidCloudsYAML}),
164+
},
165+
namespace: testNSA,
166+
identity: infrav1.OpenStackIdentityReference{Name: "valid-creds", CloudName: resTestCloudName},
167+
assertErr: func(t *testing.T, err error) {
168+
t.Helper()
169+
assertResolutionReached(t, err)
170+
},
171+
},
172+
{
173+
name: "clusteridentity returns scope when selector allows",
174+
objects: []client.Object{
175+
createTestNamespace(testNSAllowed, map[string]string{"env": "prod"}),
176+
createTestClusterIdentity("prod-id", &metav1.LabelSelector{MatchLabels: map[string]string{"env": "prod"}}),
177+
createResTestSecret(testNSCapo, "creds", map[string][]byte{CloudsSecretKey: testProdCloudsYAML}),
178+
},
179+
namespace: testNSAllowed,
180+
identity: infrav1.OpenStackIdentityReference{Type: "ClusterIdentity", Name: "prod-id", CloudName: testProdCloud},
181+
assertErr: func(t *testing.T, err error) {
182+
t.Helper()
183+
assertResolutionReached(t, err)
184+
},
185+
},
186+
{
187+
name: "secret path: missing secret returns error",
188+
objects: []client.Object{},
189+
namespace: testNSA,
190+
identity: infrav1.OpenStackIdentityReference{Name: "missing", CloudName: "cloudA"},
191+
assertErr: func(t *testing.T, err error) {
192+
t.Helper()
193+
if err == nil {
194+
t.Fatalf("expected error")
195+
}
196+
},
197+
},
198+
{
199+
name: "secret path: empty cloudName returns error",
200+
objects: []client.Object{createResTestSecret(testNSA, "creds", map[string][]byte{CloudsSecretKey: testEmptyCloudsYAML})},
201+
namespace: testNSA,
202+
identity: infrav1.OpenStackIdentityReference{Name: "creds", CloudName: ""},
203+
assertErr: func(t *testing.T, err error) {
204+
t.Helper()
205+
if err == nil {
206+
t.Fatalf("expected error")
207+
}
208+
},
209+
},
210+
{
211+
name: "clusteridentity: identity not found",
212+
objects: []client.Object{},
213+
namespace: "team-x",
214+
identity: infrav1.OpenStackIdentityReference{Type: "ClusterIdentity", Name: "missing-id", CloudName: "cloudA"},
215+
assertErr: func(t *testing.T, err error) {
216+
t.Helper()
217+
if err == nil {
218+
t.Fatalf("expected error")
219+
}
220+
},
221+
},
222+
{
223+
name: "clusteridentity: selector denies -> access denied",
224+
objects: []client.Object{
225+
createTestNamespace(testNSTeamY, nil),
226+
createTestClusterIdentity("prod-id", &metav1.LabelSelector{MatchLabels: map[string]string{"allowed": "true"}}),
227+
createResTestSecret(testNSCapo, "creds", map[string][]byte{CloudsSecretKey: testEmptyCloudsYAML}),
228+
},
229+
namespace: testNSTeamY,
230+
identity: infrav1.OpenStackIdentityReference{Type: "ClusterIdentity", Name: "prod-id", CloudName: "cloudA"},
231+
assertErr: func(t *testing.T, err error) {
232+
t.Helper()
233+
assertDenied(t, err)
234+
},
235+
},
236+
{
237+
name: "clusteridentity: selector nil allows (not denied)",
238+
objects: []client.Object{
239+
createTestNamespace(testNSTeamZ, nil),
240+
createTestClusterIdentity("any-id", nil),
241+
createResTestSecret(testNSCapo, "creds", map[string][]byte{CloudsSecretKey: testDefaultCloudsYAML}),
242+
},
243+
namespace: testNSTeamZ,
244+
identity: infrav1.OpenStackIdentityReference{Type: "ClusterIdentity", Name: "any-id", CloudName: "default"},
245+
assertErr: func(t *testing.T, err error) {
246+
t.Helper()
247+
assertNotDenied(t, err)
248+
},
249+
},
250+
{
251+
name: "clusteridentity: empty selector matches all (not denied)",
252+
objects: []client.Object{
253+
createTestNamespace(testNSTeamW, nil),
254+
createTestClusterIdentity("empty-selector-id", &metav1.LabelSelector{}),
255+
createResTestSecret(testNSCapo, "creds", map[string][]byte{CloudsSecretKey: testDefaultCloudsYAML}),
256+
},
257+
namespace: testNSTeamW,
258+
identity: infrav1.OpenStackIdentityReference{Type: "ClusterIdentity", Name: "empty-selector-id", CloudName: "default"},
259+
assertErr: func(t *testing.T, err error) {
260+
t.Helper()
261+
assertNotDenied(t, err)
262+
},
263+
},
264+
{
265+
name: "clusteridentity: cross-namespace secret allowed (not denied)",
266+
objects: []client.Object{
267+
createTestNamespace(testNSTeamA, nil),
268+
createTestNamespace(testNSCapo, nil),
269+
createTestClusterIdentity("cross-ns-id", nil),
270+
createResTestSecret(testNSCapo, "creds", map[string][]byte{CloudsSecretKey: testDefaultCloudsYAML}),
271+
},
272+
namespace: testNSTeamA,
273+
identity: infrav1.OpenStackIdentityReference{Type: "ClusterIdentity", Name: "cross-ns-id", CloudName: "default"},
274+
assertErr: func(t *testing.T, err error) {
275+
t.Helper()
276+
assertNotDenied(t, err)
277+
},
278+
},
279+
}
280+
281+
for _, tc := range cases {
282+
t.Run(tc.name, func(t *testing.T) {
283+
ctx := context.Background()
284+
c := newFakeClient(localScheme, tc.objects...)
285+
// Always use real factory with fake k8s client - this tests credential resolution
286+
// without making OpenStack API calls
287+
factory := &providerScopeFactory{}
288+
289+
srv := &infrav1alpha1.OpenStackServer{}
290+
srv.Namespace = tc.namespace
291+
srv.Spec.IdentityRef = tc.identity
292+
293+
_, err := factory.NewClientScopeFromObject(ctx, c, nil, logr.Discard(), srv)
294+
tc.assertErr(t, err)
295+
})
296+
}
297+
}

0 commit comments

Comments
 (0)