Skip to content

Commit c053c03

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

15 files changed

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

api/v1beta1/keystoneapi.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,99 @@ func GetAdminServiceClient(
139139
return os, ctrlResult, nil
140140
}
141141

142+
// GetUserServiceClient - returns an *openstack.OpenStack object scoped as the given service user
143+
func GetUserServiceClient(
144+
ctx context.Context,
145+
h *helper.Helper,
146+
keystoneAPI *KeystoneAPI,
147+
userName string,
148+
secretName string,
149+
passwordSelector string,
150+
) (*openstack.OpenStack, ctrl.Result, error) {
151+
152+
authURL, err := keystoneAPI.GetEndpoint(endpoint.EndpointInternal)
153+
if err != nil {
154+
return nil, ctrl.Result{}, err
155+
}
156+
157+
parsedAuthURL, err := url.Parse(authURL)
158+
if err != nil {
159+
return nil, ctrl.Result{}, err
160+
}
161+
162+
tlsConfig := &openstack.TLSConfig{}
163+
if parsedAuthURL.Scheme == "https" && keystoneAPI.Spec.TLS.CaBundleSecretName != "" {
164+
caCert, ctrlResult, err := secret.GetDataFromSecret(
165+
ctx,
166+
h,
167+
keystoneAPI.Spec.TLS.CaBundleSecretName,
168+
10*time.Second,
169+
tls.InternalCABundleKey)
170+
if err != nil {
171+
return nil, ctrlResult, err
172+
}
173+
if (ctrlResult != ctrl.Result{}) {
174+
return nil, ctrlResult,
175+
fmt.Errorf("CABundleSecret %s not found",
176+
keystoneAPI.Spec.TLS.CaBundleSecretName)
177+
}
178+
179+
tlsConfig = &openstack.TLSConfig{
180+
CACerts: []string{caCert},
181+
}
182+
}
183+
184+
password, err := getPasswordFromOSPSecret(ctx, h, secretName, passwordSelector)
185+
if err != nil {
186+
return nil, ctrl.Result{}, fmt.Errorf("failed to get password from osp-secret for user %q: %w", userName, err)
187+
}
188+
189+
scope := &gophercloud.AuthScope{
190+
ProjectName: "service",
191+
DomainName: "Default",
192+
}
193+
194+
osClient, err := openstack.NewOpenStack(
195+
h.GetLogger(),
196+
openstack.AuthOpts{
197+
AuthURL: authURL,
198+
Username: userName,
199+
Password: password,
200+
TenantName: "service",
201+
DomainName: "Default",
202+
Region: keystoneAPI.Spec.Region,
203+
TLS: tlsConfig,
204+
Scope: scope,
205+
},
206+
)
207+
if err != nil {
208+
return nil, ctrl.Result{}, err
209+
}
210+
211+
return osClient, ctrl.Result{}, nil
212+
}
213+
214+
func getPasswordFromOSPSecret(
215+
ctx context.Context,
216+
h *helper.Helper,
217+
ospSecretName, passwordSelector string,
218+
) (string, error) {
219+
data, res, err := secret.GetDataFromSecret(
220+
ctx,
221+
h,
222+
ospSecretName,
223+
10*time.Second,
224+
passwordSelector,
225+
)
226+
if err != nil {
227+
return "", fmt.Errorf("failed to get %q from Secret/%s: %w", passwordSelector, ospSecretName, err)
228+
}
229+
if res != (ctrl.Result{}) {
230+
return "", fmt.Errorf("secret/%s didn’t contain %q", ospSecretName, passwordSelector)
231+
}
232+
return data, nil
233+
}
234+
142235
// GetScopedAdminServiceClient - get a scoped admin serviceClient for the keystoneAPI instance
143236
func GetScopedAdminServiceClient(
144237
ctx context.Context,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
"context"
21+
"errors"
22+
"fmt"
23+
24+
corev1 "k8s.io/api/core/v1"
25+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
26+
"k8s.io/apimachinery/pkg/types"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
)
29+
30+
// ApplicationCredentialData contains AC ID/Secret extracted from a Secret
31+
// Used by service operators to get AC data from the Secret
32+
type ApplicationCredentialData struct {
33+
ID string
34+
Secret string
35+
}
36+
37+
// GetACSecretName returns the standard AC Secret name for a service
38+
func GetACSecretName(serviceName string) string {
39+
return fmt.Sprintf("ac-%s-secret", serviceName)
40+
}
41+
42+
// GetACCRName returns the standard AC CR name for a service
43+
func GetACCRName(serviceName string) string {
44+
return fmt.Sprintf("ac-%s", serviceName)
45+
}
46+
47+
var (
48+
// ErrACIDMissing indicates AC_ID key missing or empty in the Secret
49+
ErrACIDMissing = errors.New("applicationcredential secret missing AC_ID")
50+
// ErrACSecretMissing indicates AC_SECRET key missing or empty in the Secret
51+
ErrACSecretMissing = errors.New("applicationcredential secret missing AC_SECRET")
52+
)
53+
54+
// GetApplicationCredentialFromSecret fetches and validates AC data from the Secret
55+
func GetApplicationCredentialFromSecret(
56+
ctx context.Context,
57+
c client.Client,
58+
namespace string,
59+
serviceName string,
60+
) (*ApplicationCredentialData, error) {
61+
secret := &corev1.Secret{}
62+
key := types.NamespacedName{Namespace: namespace, Name: GetACSecretName(serviceName)}
63+
if err := c.Get(ctx, key, secret); err != nil {
64+
if k8s_errors.IsNotFound(err) {
65+
return nil, nil
66+
}
67+
return nil, fmt.Errorf("get applicationcredential secret %s: %w", key, err)
68+
}
69+
70+
acID, okID := secret.Data["AC_ID"]
71+
if !okID || len(acID) == 0 {
72+
return nil, fmt.Errorf("%w: %s", ErrACIDMissing, key.String())
73+
}
74+
acSecret, okSecret := secret.Data["AC_SECRET"]
75+
if !okSecret || len(acSecret) == 0 {
76+
return nil, fmt.Errorf("%w: %s", ErrACSecretMissing, key.String())
77+
}
78+
79+
return &ApplicationCredentialData{ID: string(acID), Secret: string(acSecret)}, nil
80+
}

0 commit comments

Comments
 (0)