Skip to content

Commit 040976c

Browse files
committed
Add AppCred controller foundation
Signed-off-by: Veronika Fisarova <[email protected]>
1 parent 6db2b5a commit 040976c

9 files changed

+1038
-1
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
userName:
74+
description: UserName - the Keystone user under which this AC is created
75+
type: string
76+
required:
77+
- userName
78+
type: object
79+
x-kubernetes-validations:
80+
- message: gracePeriodDays must be smaller than expirationDays
81+
rule: self.gracePeriodDays < self.expirationDays
82+
status:
83+
description: ApplicationCredentialStatus defines the observed state
84+
properties:
85+
acID:
86+
description: ACID - the ID in Keystone for this AC
87+
type: string
88+
conditions:
89+
description: Conditions
90+
items:
91+
description: Condition defines an observation of a API resource
92+
operational state.
93+
properties:
94+
lastTransitionTime:
95+
description: |-
96+
Last time the condition transitioned from one status to another.
97+
This should be when the underlying condition changed. If that is not known, then using the time when
98+
the API field changed is acceptable.
99+
format: date-time
100+
type: string
101+
message:
102+
description: A human readable message indicating details about
103+
the transition.
104+
type: string
105+
reason:
106+
description: The reason for the condition's last transition
107+
in CamelCase.
108+
type: string
109+
severity:
110+
description: |-
111+
Severity provides a classification of Reason code, so the current situation is immediately
112+
understandable and could act accordingly.
113+
It is meant for situations where Status=False and it should be indicated if it is just
114+
informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue
115+
and no actions to automatically resolve the issue can/should be done).
116+
For conditions where Status=Unknown or Status=True the Severity should be SeverityNone.
117+
type: string
118+
status:
119+
description: Status of the condition, one of True, False, Unknown.
120+
type: string
121+
type:
122+
description: Type of condition in CamelCase.
123+
type: string
124+
required:
125+
- lastTransitionTime
126+
- status
127+
- type
128+
type: object
129+
type: array
130+
createdAt:
131+
description: CreatedAt - timestap of creation
132+
format: date-time
133+
type: string
134+
expiresAt:
135+
description: ExpiresAt - time of validity expiration
136+
format: date-time
137+
type: string
138+
secretName:
139+
description: SecretName - name of the k8s Secret storing the AC secret
140+
type: string
141+
type: object
142+
type: object
143+
served: true
144+
storage: true
145+
subresources:
146+
status: {}

api/v1beta1/keystoneapi.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"fmt"
2121
"net/url"
22+
"strings"
2223
"time"
2324

2425
"github.com/gophercloud/gophercloud"
@@ -92,6 +93,115 @@ func GetAdminServiceClient(
9293
return os, ctrlResult, nil
9394
}
9495

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

0 commit comments

Comments
 (0)