Skip to content

Commit 7ab1409

Browse files
authored
Merge pull request #2628 from bnallapeta/bnr/2386
proposal: add new CRD OpenStackClusterIdentity
2 parents b3412b5 + 4af2ff4 commit 7ab1409

File tree

1 file changed

+341
-0
lines changed

1 file changed

+341
-0
lines changed
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
# OpenStackClusterIdentity for Centralized Credential Management
2+
3+
## Metadata
4+
5+
- **Authors**: @bnallapeta
6+
- **Reviewers**: CAPO maintainers (@mdbooth)
7+
- **Status**: Proposed
8+
- **Creation Date**: 2025-07-22
9+
- **Last Updated**: 2025-07-29
10+
11+
## Summary
12+
13+
This proposal introduces `OpenStackClusterIdentity`, a cluster-scoped resource for centralized OpenStack credential management in CAPO. This enables multi-tenant environments to share credentials across namespaces while maintaining proper access controls, following patterns from AWS and Azure Cluster API providers.
14+
15+
## Motivation
16+
17+
### Goals
18+
- Enable centralized storage of OpenStack credentials in cluster-scoped resources
19+
- Provide fine-grained namespace access controls for credential usage
20+
- Maintain 100% backward compatibility with existing OpenStackCluster resources
21+
- Support manual, gradual migration without breaking existing deployments
22+
- Follow established patterns from other Cluster API providers (AWS, Azure)
23+
24+
### Non-Goals
25+
- Automatic migration of existing deployments
26+
- Integration with external secret management systems
27+
- Breaking changes to existing API or functionality
28+
29+
### User Stories
30+
31+
#### Story 1: Platform Administrator
32+
As a platform administrator managing multiple tenant namespaces, I want to store OpenStack credentials centrally in a secure namespace (e.g., `capo-system`), control which tenant namespaces can use specific credentials, and rotate credentials in one place without updating every namespace.
33+
34+
#### Story 2: Tenant User
35+
As a tenant user in namespace `team-a`, I want to create OpenStack clusters using centrally managed credentials without managing OpenStack secrets in my namespace, with clear error messages if I don't have permission to use specific credentials.
36+
37+
#### Story 3: Multi-Region Setup
38+
As an administrator managing clusters across multiple OpenStack regions, I want to create region-specific cluster identities with appropriate credentials and allow tenants to use different regional credentials based on their needs.
39+
40+
### API Design
41+
42+
#### New OpenStackClusterIdentity Resource (Cluster-scoped)
43+
44+
```go
45+
type OpenStackClusterIdentity struct {
46+
metav1.TypeMeta `json:",inline"`
47+
metav1.ObjectMeta `json:"metadata,omitempty"`
48+
Spec OpenStackClusterIdentitySpec `json:"spec,omitempty"`
49+
}
50+
51+
type OpenStackClusterIdentitySpec struct {
52+
// SecretRef references the secret containing OpenStack credentials
53+
SecretRef OpenStackCredentialSecretReference `json:"secretRef"`
54+
55+
// NamespaceSelector selects allowed namespaces via labels
56+
// All namespaces have a kubernetes.io/metadata.name label containing their name
57+
// +optional
58+
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
59+
}
60+
61+
type OpenStackCredentialSecretReference struct {
62+
Name string `json:"name"`
63+
Namespace string `json:"namespace"`
64+
}
65+
```
66+
67+
#### Enhanced OpenStackIdentityReference (Discriminated Union)
68+
69+
We extend the existing `identityRef` to support multiple types using a discriminated union pattern:
70+
71+
```go
72+
type OpenStackIdentityReference struct {
73+
// Type specifies the identity reference type
74+
// +kubebuilder:validation:Enum=Secret;ClusterIdentity
75+
// +kubebuilder:default=Secret
76+
// +kubebuilder:validation:XValidation:rule="self == 'Secret' ? has(self.cloudName) : !has(self.cloudName)",message="cloudName required for Secret type, forbidden for ClusterIdentity type"
77+
// +kubebuilder:validation:XValidation:rule="has(self.name)",message="name is required"
78+
// +optional
79+
Type string `json:"type,omitempty"`
80+
81+
// Name of the secret (type=Secret) or cluster identity (type=ClusterIdentity)
82+
// +optional
83+
Name string `json:"name,omitempty"`
84+
85+
// CloudName required for Secret type, forbidden for ClusterIdentity type
86+
// +optional
87+
CloudName string `json:"cloudName,omitempty"`
88+
89+
// Region applies to both types
90+
// +optional
91+
Region string `json:"region,omitempty"`
92+
}
93+
```
94+
95+
### Implementation Details
96+
97+
#### Credential Resolution Logic
98+
The scope factory will implement type-based credential resolution:
99+
100+
```go
101+
func (f *providerScopeFactory) resolveCredentials(identityRef *infrav1.OpenStackIdentityReference) {
102+
switch identityRef.Type {
103+
case "ClusterIdentity":
104+
return f.newScopeFromClusterIdentity(identityRef.Name)
105+
case "Secret", "": // Default to Secret for backward compatibility
106+
return f.newScopeFromSecretIdentity(identityRef)
107+
default:
108+
return fmt.Errorf("unsupported identity type: %s", identityRef.Type)
109+
}
110+
}
111+
```
112+
113+
#### Permission and Access Control
114+
115+
This feature involves two distinct types of permissions:
116+
117+
**1. Controller RBAC (Kubernetes-level permissions)**
118+
The CAPO controller already has cluster-wide secret access. We only need to add:
119+
120+
```yaml
121+
# ADD to existing config/rbac/role.yaml
122+
- apiGroups: [""]
123+
resources: ["namespaces"]
124+
verbs: ["get"] # Read namespace metadata for validation
125+
- apiGroups: ["infrastructure.cluster.x-k8s.io"]
126+
resources: ["openstackclusteridentities"]
127+
verbs: ["get", "list", "watch"] # Manage cluster identities
128+
```
129+
130+
**2. Namespace Access Control (Application-level permissions)**
131+
The cluster identity defines which namespaces can use it via namespace selectors:
132+
133+
```go
134+
func (r *OpenStackClusterReconciler) validateNamespaceAccess(identity *OpenStackClusterIdentity, namespace string) error {
135+
// If no selector specified, allow all namespaces
136+
if identity.Spec.NamespaceSelector == nil {
137+
return nil
138+
}
139+
140+
// Get the namespace object
141+
ns := &corev1.Namespace{}
142+
err := r.Client.Get(ctx, types.NamespacedName{Name: namespace}, ns)
143+
if err != nil {
144+
return fmt.Errorf("failed to get namespace %s: %w", namespace, err)
145+
}
146+
147+
// Check if namespace matches the selector
148+
selector, err := metav1.LabelSelectorAsSelector(identity.Spec.NamespaceSelector)
149+
if err != nil {
150+
return fmt.Errorf("invalid namespace selector: %w", err)
151+
}
152+
153+
if !selector.Matches(labels.Set(ns.Labels)) {
154+
return fmt.Errorf("namespace %s not allowed to use cluster identity %s",
155+
namespace, identity.Name)
156+
}
157+
158+
return nil
159+
}
160+
```
161+
162+
**Key Edge Cases:**
163+
- Missing type field: Defaults to `Secret` behavior (100% backward compatible)
164+
- Invalid type: Clear error message
165+
- Invalid field combinations: CEL validation prevents misconfigurations
166+
- Namespace access denied: Clear error message with identity name and namespace
167+
168+
#### CEL Validation
169+
170+
We use CEL (Common Expression Language) for API validation, following existing CAPO patterns:
171+
172+
```go
173+
type OpenStackIdentityReference struct {
174+
// Type specifies the identity reference type
175+
// +kubebuilder:validation:Enum=Secret;ClusterIdentity
176+
// +kubebuilder:default=Secret
177+
// +kubebuilder:validation:XValidation:rule="self == 'Secret' ? has(self.cloudName) : !has(self.cloudName)",message="cloudName required for Secret type, forbidden for ClusterIdentity type"
178+
// +kubebuilder:validation:XValidation:rule="has(self.name)",message="name is required"
179+
// +optional
180+
Type string `json:"type,omitempty"`
181+
182+
// Name of the secret (type=Secret) or cluster identity (type=ClusterIdentity)
183+
// +optional
184+
Name string `json:"name,omitempty"`
185+
186+
// CloudName required for Secret type, forbidden for ClusterIdentity type
187+
// +optional
188+
CloudName string `json:"cloudName,omitempty"`
189+
190+
// Region applies to both types
191+
// +optional
192+
Region string `json:"region,omitempty"`
193+
}
194+
```
195+
196+
**CEL Validation Rules:**
197+
1. **Name Required**: `name` field is always required for both types
198+
2. **CloudName Logic**: Required for Secret type, forbidden for ClusterIdentity type
199+
3. **Type Safety**: Enum validation ensures only valid types are accepted
200+
201+
### Backward Compatibility
202+
203+
- **Perfect backward compatibility**: Existing `identityRef` configurations work unchanged
204+
- **Default behavior**: Missing `type` field defaults to `Secret` behavior
205+
- **No migration required**: Existing clusters continue working
206+
- **Gradual adoption**: Users can adopt `type: ClusterIdentity` when required
207+
208+
#### Migration Strategy
209+
- **No forced migration**: Existing deployments continue working indefinitely
210+
- **Manual opt-in**: Users add `type: ClusterIdentity` when ready
211+
- **Clear validation**: Type-specific field validation prevents misconfigurations
212+
213+
### Testing Strategy
214+
215+
**Unit Tests**: API validation for both identity types, credential resolution logic, namespace access validation
216+
**Integration Tests**: End-to-end credential resolution for both types, cross-namespace access validation
217+
**E2E Tests**: Full cluster lifecycle with both identity types, mixed deployments
218+
**Security Tests**: Unauthorized access attempts, namespace boundary enforcement
219+
220+
## Risks and Mitigations
221+
222+
| Risk | Mitigation |
223+
|------|------------|
224+
| Complex validation logic | Type-specific validation, comprehensive testing |
225+
| Field confusion | Clear documentation, validation webhooks |
226+
| Migration issues | Extensive backward compatibility testing |
227+
| Cross-namespace security | Strict namespace selector validation, audit logging |
228+
229+
## Alternatives
230+
231+
**Separate clusterIdentityRef field**: Requires dual fields, harder to maintain long-term
232+
**External Secret Operator**: More complex, adds external dependency
233+
**ConfigMap-based References**: No validation, security concerns
234+
235+
## Example Usage
236+
237+
### Create Cluster Identity
238+
```yaml
239+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
240+
kind: OpenStackClusterIdentity
241+
metadata:
242+
name: production-openstack
243+
spec:
244+
secretRef:
245+
name: openstack-credentials
246+
namespace: capo-system
247+
namespaceSelector:
248+
matchExpressions:
249+
- key: kubernetes.io/metadata.name
250+
operator: In
251+
values: [team-a, team-b]
252+
---
253+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
254+
kind: OpenStackClusterIdentity
255+
metadata:
256+
name: development-openstack
257+
spec:
258+
secretRef:
259+
name: dev-openstack-credentials
260+
namespace: capo-system
261+
namespaceSelector:
262+
matchLabels:
263+
environment: development
264+
```
265+
266+
### Current Secret-based Identity (unchanged)
267+
```yaml
268+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
269+
kind: OpenStackCluster
270+
metadata:
271+
name: cluster-a
272+
namespace: team-a
273+
spec:
274+
identityRef:
275+
# No type field = defaults to Secret behavior
276+
name: my-secret
277+
cloudName: openstack
278+
```
279+
280+
### New ClusterIdentity-based (new usage)
281+
```yaml
282+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
283+
kind: OpenStackCluster
284+
metadata:
285+
name: cluster-a
286+
namespace: team-a
287+
spec:
288+
identityRef:
289+
type: ClusterIdentity
290+
name: prod-openstack
291+
```
292+
293+
### Explicit Secret Type (optional)
294+
```yaml
295+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
296+
kind: OpenStackCluster
297+
metadata:
298+
name: cluster-a
299+
namespace: team-a
300+
spec:
301+
identityRef:
302+
type: Secret
303+
name: my-secret
304+
cloudName: openstack
305+
```
306+
307+
## Implementation Notes
308+
309+
**API Changes**:
310+
- Extend `OpenStackIdentityReference` with `type` field with `Secret` and `ClusterIdentity` as the only supported values
311+
- Add CEL validation rules for type-specific field combinations
312+
- Update CRD generation for new field
313+
314+
**RBAC Changes**:
315+
- Add namespace `get` permission to existing `config/rbac/role.yaml`
316+
- Add `openstackclusteridentities` resource permissions
317+
318+
**Controller Changes**:
319+
- Modify `pkg/scope/provider.go` for type-based credential resolution
320+
- Add namespace access validation logic in controllers
321+
- Add cluster identity lookup and permission checking
322+
- Implement comprehensive error handling with clear messages
323+
324+
**Backward Compatibility**:
325+
- Default `type` to `Secret` when not specified
326+
- CEL validation maintains existing field requirements for secret-based identities
327+
- Ensure all existing configurations continue working
328+
329+
**Security Considerations**:
330+
- Validate namespace access on every cluster reconciliation
331+
- Log access attempts for audit purposes
332+
- Clear error messages for permission denials
333+
- Fail securely when cluster identity is not accessible
334+
335+
**Broader Impact**:
336+
This change automatically enables cluster identity support for:
337+
- `OpenStackCluster` resources
338+
- `OpenStackMachine` resources
339+
- `OpenStackServer` resources
340+
341+
This proposal provides centralized credential management while maintaining full backward compatibility and following established Kubernetes patterns (discriminated union).

0 commit comments

Comments
 (0)