Skip to content

Commit 15fcdb9

Browse files
committed
Add AppCred types and controller support
Signed-off-by: Veronika Fisarova <[email protected]>
1 parent 6db2b5a commit 15fcdb9

10 files changed

+1058
-2
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
annotations:
6+
controller-gen.kubebuilder.io/version: v0.14.0
7+
name: applicationcredentials.keystone.openstack.org
8+
spec:
9+
group: keystone.openstack.org
10+
names:
11+
kind: ApplicationCredential
12+
listKind: ApplicationCredentialList
13+
plural: applicationcredentials
14+
shortNames:
15+
- appcred
16+
singular: applicationcredential
17+
scope: Namespaced
18+
versions:
19+
- additionalPrinterColumns:
20+
- description: Keystone AC ID
21+
jsonPath: .status.acID
22+
name: ACID
23+
type: string
24+
- description: Secret holding AC secret
25+
jsonPath: .status.secretName
26+
name: SecretName
27+
type: string
28+
- description: Status
29+
jsonPath: .status.conditions[0].status
30+
name: Status
31+
type: string
32+
- description: Message
33+
jsonPath: .status.conditions[0].message
34+
name: Message
35+
type: string
36+
name: v1beta1
37+
schema:
38+
openAPIV3Schema:
39+
description: ApplicationCredential is the Schema for the applicationcredentials
40+
API
41+
properties:
42+
apiVersion:
43+
description: |-
44+
APIVersion defines the versioned schema of this representation of an object.
45+
Servers should convert recognized schemas to the latest internal value, and
46+
may reject unrecognized values.
47+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
48+
type: string
49+
kind:
50+
description: |-
51+
Kind is a string value representing the REST resource this object represents.
52+
Servers may infer this from the endpoint the client submits requests to.
53+
Cannot be updated.
54+
In CamelCase.
55+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
56+
type: string
57+
metadata:
58+
type: object
59+
spec:
60+
description: ApplicationCredentialSpec defines what the user can set
61+
properties:
62+
expirationDays:
63+
default: 14
64+
description: ExpirationDays sets the lifetime in days for the AC
65+
minimum: 2
66+
type: integer
67+
gracePeriodDays:
68+
default: 7
69+
description: GracePeriodDays sets how many days before expiration
70+
the AC should be rotated
71+
minimum: 1
72+
type: integer
73+
passwordSelector:
74+
description: PasswordSelector for extracting the service password
75+
type: string
76+
secret:
77+
default: osp-secret
78+
description: Secret containing service user password
79+
type: string
80+
userName:
81+
description: UserName - the Keystone user under which this AC is created
82+
type: string
83+
required:
84+
- userName
85+
type: object
86+
x-kubernetes-validations:
87+
- message: gracePeriodDays must be smaller than expirationDays
88+
rule: self.gracePeriodDays < self.expirationDays
89+
status:
90+
description: ApplicationCredentialStatus defines the observed state
91+
properties:
92+
acID:
93+
description: ACID - the ID in Keystone for this AC
94+
type: string
95+
conditions:
96+
description: Conditions
97+
items:
98+
description: Condition defines an observation of a API resource
99+
operational state.
100+
properties:
101+
lastTransitionTime:
102+
description: |-
103+
Last time the condition transitioned from one status to another.
104+
This should be when the underlying condition changed. If that is not known, then using the time when
105+
the API field changed is acceptable.
106+
format: date-time
107+
type: string
108+
message:
109+
description: A human readable message indicating details about
110+
the transition.
111+
type: string
112+
reason:
113+
description: The reason for the condition's last transition
114+
in CamelCase.
115+
type: string
116+
severity:
117+
description: |-
118+
Severity provides a classification of Reason code, so the current situation is immediately
119+
understandable and could act accordingly.
120+
It is meant for situations where Status=False and it should be indicated if it is just
121+
informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue
122+
and no actions to automatically resolve the issue can/should be done).
123+
For conditions where Status=Unknown or Status=True the Severity should be SeverityNone.
124+
type: string
125+
status:
126+
description: Status of the condition, one of True, False, Unknown.
127+
type: string
128+
type:
129+
description: Type of condition in CamelCase.
130+
type: string
131+
required:
132+
- lastTransitionTime
133+
- status
134+
- type
135+
type: object
136+
type: array
137+
createdAt:
138+
description: CreatedAt - timestap of creation
139+
format: date-time
140+
type: string
141+
expiresAt:
142+
description: ExpiresAt - time of validity expiration
143+
format: date-time
144+
type: string
145+
secretName:
146+
description: SecretName - name of the k8s Secret storing the AC secret
147+
type: string
148+
type: object
149+
type: object
150+
served: true
151+
storage: true
152+
subresources:
153+
status: {}

api/v1beta1/keystoneapi.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,107 @@ func GetAdminServiceClient(
9292
return os, ctrlResult, nil
9393
}
9494

95+
// GetUserServiceClient - returns an *openstack.OpenStack object scoped as the given service user
96+
func GetUserServiceClient(
97+
ctx context.Context,
98+
h *helper.Helper,
99+
keystoneAPI *KeystoneAPI,
100+
userName string,
101+
secretName string,
102+
passwordSelector string,
103+
) (*openstack.OpenStack, ctrl.Result, error) {
104+
105+
authURL, err := keystoneAPI.GetEndpoint(endpoint.EndpointInternal)
106+
if err != nil {
107+
return nil, ctrl.Result{}, err
108+
}
109+
110+
parsedAuthURL, err := url.Parse(authURL)
111+
if err != nil {
112+
return nil, ctrl.Result{}, err
113+
}
114+
115+
tlsConfig := &openstack.TLSConfig{}
116+
if parsedAuthURL.Scheme == "https" && keystoneAPI.Spec.TLS.CaBundleSecretName != "" {
117+
caCert, ctrlResult, err := secret.GetDataFromSecret(
118+
ctx,
119+
h,
120+
keystoneAPI.Spec.TLS.CaBundleSecretName,
121+
10*time.Second,
122+
tls.InternalCABundleKey)
123+
if err != nil {
124+
return nil, ctrlResult, err
125+
}
126+
if (ctrlResult != ctrl.Result{}) {
127+
return nil, ctrlResult,
128+
fmt.Errorf("CABundleSecret %s not found",
129+
keystoneAPI.Spec.TLS.CaBundleSecretName)
130+
}
131+
132+
tlsConfig = &openstack.TLSConfig{
133+
CACerts: []string{caCert},
134+
}
135+
}
136+
137+
password, err := getPasswordFromOSPSecret(ctx, h, secretName, passwordSelector)
138+
if err != nil {
139+
return nil, ctrl.Result{}, fmt.Errorf("failed to get password from osp-secret for user %q: %w", userName, err)
140+
}
141+
142+
scope := &gophercloud.AuthScope{
143+
ProjectName: "service",
144+
DomainName: "Default",
145+
}
146+
147+
osClient, err := openstack.NewOpenStack(
148+
h.GetLogger(),
149+
openstack.AuthOpts{
150+
AuthURL: authURL,
151+
Username: userName,
152+
Password: password,
153+
TenantName: "service",
154+
DomainName: "Default",
155+
Region: keystoneAPI.Spec.Region,
156+
TLS: tlsConfig,
157+
Scope: scope,
158+
},
159+
)
160+
if err != nil {
161+
return nil, ctrl.Result{}, err
162+
}
163+
164+
// DEBUG
165+
logger := h.GetLogger()
166+
logger.Info("GetUserServiceClient debug",
167+
"authURL", authURL,
168+
"region", keystoneAPI.Spec.Region,
169+
"userName", userName,
170+
)
171+
172+
return osClient, ctrl.Result{}, nil
173+
}
174+
175+
func getPasswordFromOSPSecret(
176+
ctx context.Context,
177+
h *helper.Helper,
178+
ospSecretName, passwordSelector string,
179+
) (string, error) {
180+
data, res, err := secret.GetDataFromSecret(
181+
ctx,
182+
h,
183+
ospSecretName,
184+
10*time.Second,
185+
passwordSelector,
186+
)
187+
if err != nil {
188+
return "", fmt.Errorf("failed to get %q from Secret/%s: %w", passwordSelector, ospSecretName, err)
189+
}
190+
if res != (ctrl.Result{}) {
191+
return "", fmt.Errorf("secret/%s didn’t contain %q", ospSecretName, passwordSelector)
192+
}
193+
return data, nil
194+
}
195+
95196
// GetScopedAdminServiceClient - get a scoped admin serviceClient for the keystoneAPI instance
96197
func GetScopedAdminServiceClient(
97198
ctx context.Context,
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2025.
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 v1beta1
18+
19+
import (
20+
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
)
23+
24+
// +kubebuilder:validation:XValidation:rule="self.gracePeriodDays < self.expirationDays",message="gracePeriodDays must be smaller than expirationDays"
25+
// ApplicationCredentialSpec defines what the user can set
26+
type ApplicationCredentialSpec struct {
27+
28+
// Secret containing service user password
29+
// +kubebuilder:validation:Optional
30+
// +kubebuilder:default=osp-secret
31+
Secret string `json:"secret"`
32+
33+
// PasswordSelector for extracting the service password
34+
// +kubebuilder:validation:Optional
35+
PasswordSelector string `json:"passwordSelector"`
36+
37+
// UserName - the Keystone user under which this AC is created
38+
UserName string `json:"userName"`
39+
40+
// ExpirationDays sets the lifetime in days for the AC
41+
// +kubebuilder:validation:Optional
42+
// +kubebuilder:default=14
43+
// +kubebuilder:validation:Minimum=2
44+
ExpirationDays int `json:"expirationDays"`
45+
46+
// GracePeriodDays sets how many days before expiration the AC should be rotated
47+
// +kubebuilder:validation:Optional
48+
// +kubebuilder:default=7
49+
// +kubebuilder:validation:Minimum=1
50+
GracePeriodDays int `json:"gracePeriodDays"`
51+
}
52+
53+
// ApplicationCredentialStatus defines the observed state
54+
type ApplicationCredentialStatus struct {
55+
// ACID - the ID in Keystone for this AC
56+
ACID string `json:"acID,omitempty"`
57+
58+
// SecretName - name of the k8s Secret storing the AC secret
59+
SecretName string `json:"secretName,omitempty"`
60+
61+
// Conditions
62+
Conditions condition.Conditions `json:"conditions,omitempty"`
63+
64+
// CreatedAt - timestap of creation
65+
CreatedAt *metav1.Time `json:"createdAt,omitempty"`
66+
67+
// ExpiresAt - time of validity expiration
68+
ExpiresAt *metav1.Time `json:"expiresAt,omitempty"`
69+
}
70+
71+
//+kubebuilder:object:root=true
72+
//+kubebuilder:subresource:status
73+
// +kubebuilder:resource:shortName=appcred
74+
//+kubebuilder:printcolumn:name="ACID",type="string",JSONPath=".status.acID",description="Keystone AC ID"
75+
//+kubebuilder:printcolumn:name="SecretName",type="string",JSONPath=".status.secretName",description="Secret holding AC secret"
76+
//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].status",description="Status"
77+
//+kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[0].message",description="Message"
78+
79+
// ApplicationCredential is the Schema for the applicationcredentials API
80+
type ApplicationCredential struct {
81+
metav1.TypeMeta `json:",inline"`
82+
metav1.ObjectMeta `json:"metadata,omitempty"`
83+
84+
Spec ApplicationCredentialSpec `json:"spec,omitempty"`
85+
Status ApplicationCredentialStatus `json:"status,omitempty"`
86+
}
87+
88+
//+kubebuilder:object:root=true
89+
90+
// ApplicationCredentialList contains a list of ApplicationCredential
91+
type ApplicationCredentialList struct {
92+
metav1.TypeMeta `json:",inline"`
93+
metav1.ListMeta `json:"metadata,omitempty"`
94+
Items []ApplicationCredential `json:"items"`
95+
}
96+
97+
func init() {
98+
SchemeBuilder.Register(&ApplicationCredential{}, &ApplicationCredentialList{})
99+
}
100+
101+
// IsReady - returns true if ApplicationCredential is reconciled successfully
102+
func (ac *ApplicationCredential) IsReady() bool {
103+
return ac.Status.Conditions.IsTrue(condition.ReadyCondition)
104+
}

0 commit comments

Comments
 (0)