Skip to content

Commit 46e453b

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

9 files changed

+1030
-1
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
status:
80+
description: ApplicationCredentialStatus defines the observed state
81+
properties:
82+
acID:
83+
description: ACID - the ID in Keystone for this AC
84+
type: string
85+
conditions:
86+
description: Conditions
87+
items:
88+
description: Condition defines an observation of a API resource
89+
operational state.
90+
properties:
91+
lastTransitionTime:
92+
description: |-
93+
Last time the condition transitioned from one status to another.
94+
This should be when the underlying condition changed. If that is not known, then using the time when
95+
the API field changed is acceptable.
96+
format: date-time
97+
type: string
98+
message:
99+
description: A human readable message indicating details about
100+
the transition.
101+
type: string
102+
reason:
103+
description: The reason for the condition's last transition
104+
in CamelCase.
105+
type: string
106+
severity:
107+
description: |-
108+
Severity provides a classification of Reason code, so the current situation is immediately
109+
understandable and could act accordingly.
110+
It is meant for situations where Status=False and it should be indicated if it is just
111+
informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue
112+
and no actions to automatically resolve the issue can/should be done).
113+
For conditions where Status=Unknown or Status=True the Severity should be SeverityNone.
114+
type: string
115+
status:
116+
description: Status of the condition, one of True, False, Unknown.
117+
type: string
118+
type:
119+
description: Type of condition in CamelCase.
120+
type: string
121+
required:
122+
- lastTransitionTime
123+
- status
124+
- type
125+
type: object
126+
type: array
127+
createdAt:
128+
description: CreatedAt - timestap of creation
129+
format: date-time
130+
type: string
131+
expiresAt:
132+
description: ExpiresAt - time of validity expiration
133+
format: date-time
134+
type: string
135+
secretName:
136+
description: SecretName - name of the k8s Secret storing the AC secret
137+
type: string
138+
type: object
139+
type: object
140+
served: true
141+
storage: true
142+
subresources:
143+
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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
// ApplicationCredentialSpec defines what the user can set
25+
type ApplicationCredentialSpec struct {
26+
// UserName - the Keystone user under which this AC is created
27+
UserName string `json:"userName"`
28+
29+
// ExpirationDays sets the lifetime (in days) for the AC
30+
// +kubebuilder:validation:Optional
31+
// +kubebuilder:default=14
32+
// +kubebuilder:validation:Minimum=2
33+
ExpirationDays int `json:"expirationDays"`
34+
35+
// GracePeriodDays sets how many days before expiration the AC should be rotated
36+
// +kubebuilder:validation:Optional
37+
// +kubebuilder:default=7
38+
// +kubebuilder:validation:Minimum=1
39+
GracePeriodDays int `json:"gracePeriodDays"`
40+
}
41+
42+
// ApplicationCredentialStatus defines the observed state
43+
type ApplicationCredentialStatus struct {
44+
// ACID - the ID in Keystone for this AC
45+
ACID string `json:"acID,omitempty"`
46+
47+
// SecretName - name of the k8s Secret storing the AC secret
48+
SecretName string `json:"secretName,omitempty"`
49+
50+
// Conditions
51+
Conditions condition.Conditions `json:"conditions,omitempty"`
52+
53+
// CreatedAt - timestap of creation
54+
CreatedAt *metav1.Time `json:"createdAt,omitempty"`
55+
56+
// ExpiresAt - time of validity expiration
57+
ExpiresAt *metav1.Time `json:"expiresAt,omitempty"`
58+
}
59+
60+
//+kubebuilder:object:root=true
61+
//+kubebuilder:subresource:status
62+
// +kubebuilder:resource:shortName=appcred
63+
//+kubebuilder:printcolumn:name="ACID",type="string",JSONPath=".status.acID",description="Keystone AC ID"
64+
//+kubebuilder:printcolumn:name="SecretName",type="string",JSONPath=".status.secretName",description="Secret holding AC secret"
65+
//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].status",description="Status"
66+
//+kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[0].message",description="Message"
67+
68+
// ApplicationCredential is the Schema for the applicationcredentials API
69+
type ApplicationCredential struct {
70+
metav1.TypeMeta `json:",inline"`
71+
metav1.ObjectMeta `json:"metadata,omitempty"`
72+
73+
Spec ApplicationCredentialSpec `json:"spec,omitempty"`
74+
Status ApplicationCredentialStatus `json:"status,omitempty"`
75+
}
76+
77+
//+kubebuilder:object:root=true
78+
79+
// ApplicationCredentialList contains a list of ApplicationCredential
80+
type ApplicationCredentialList struct {
81+
metav1.TypeMeta `json:",inline"`
82+
metav1.ListMeta `json:"metadata,omitempty"`
83+
Items []ApplicationCredential `json:"items"`
84+
}
85+
86+
func init() {
87+
SchemeBuilder.Register(&ApplicationCredential{}, &ApplicationCredentialList{})
88+
}
89+
90+
// IsReady - returns true if ApplicationCredential is reconciled successfully
91+
func (ac *ApplicationCredential) IsReady() bool {
92+
return ac.Status.Conditions.IsTrue(condition.ReadyCondition)
93+
}

0 commit comments

Comments
 (0)