Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions api/bases/keystone.openstack.org_keystoneapplicationcredentials.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.18.0
name: keystoneapplicationcredentials.keystone.openstack.org
spec:
group: keystone.openstack.org
names:
kind: KeystoneApplicationCredential
listKind: KeystoneApplicationCredentialList
plural: keystoneapplicationcredentials
shortNames:
- appcred
singular: keystoneapplicationcredential
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Keystone ApplicationCredential ID
jsonPath: .status.acID
name: ACID
type: string
- description: Secret holding ApplicationCredential secret
jsonPath: .status.secretName
name: SecretName
type: string
- description: Last rotation time
format: date-time
jsonPath: .status.lastRotated
name: LastRotated
type: string
- description: When rotation becomes eligible
format: date-time
jsonPath: .status.rotationEligibleAt
name: RotationEligible
type: string
- description: Status
jsonPath: .status.conditions[0].status
name: Status
type: string
- description: Message
jsonPath: .status.conditions[0].message
name: Message
type: string
name: v1beta1
schema:
openAPIV3Schema:
description: KeystoneApplicationCredential is the Schema for the applicationcredentials
API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: KeystoneApplicationCredentialSpec defines what the user can
set
properties:
accessRules:
description: AccessRules defines which services the ApplicationCredential
is permitted to access
items:
description: ACRule defines a additional access rule for an ApplicationCredential
properties:
method:
description: Method is the HTTP verb to allow (defaults to all
if empty)
type: string
path:
description: Path is the API path to allow
type: string
service:
description: Service is the OpenStack service type
type: string
type: object
type: array
expirationDays:
default: 365
description: ExpirationDays sets the lifetime in days for the ApplicationCredential
minimum: 2
type: integer
gracePeriodDays:
default: 182
description: GracePeriodDays sets how many days before expiration
the ApplicationCredential should be rotated
minimum: 1
type: integer
passwordSelector:
description: PasswordSelector for extracting the service password
type: string
roles:
description: Roles to assign to the ApplicationCredential
items:
type: string
minItems: 1
type: array
secret:
description: Secret containing service user password
type: string
unrestricted:
default: false
description: Unrestricted indicates whether the ApplicationCredential
may be used to create or destroy other credentials or trusts
type: boolean
userName:
description: UserName - the Keystone user under which this ApplicationCredential
is created
type: string
required:
- passwordSelector
- roles
- secret
- userName
type: object
x-kubernetes-validations:
- message: gracePeriodDays must be smaller than expirationDays
rule: self.gracePeriodDays < self.expirationDays
status:
description: KeystoneApplicationCredentialStatus defines the observed
state
properties:
acID:
description: ACID - the ID in Keystone for this ApplicationCredential
type: string
conditions:
description: Conditions
items:
description: Condition defines an observation of a API resource
operational state.
properties:
lastTransitionTime:
description: |-
Last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when
the API field changed is acceptable.
format: date-time
type: string
message:
description: A human readable message indicating details about
the transition.
type: string
reason:
description: The reason for the condition's last transition
in CamelCase.
type: string
severity:
description: |-
Severity provides a classification of Reason code, so the current situation is immediately
understandable and could act accordingly.
It is meant for situations where Status=False and it should be indicated if it is just
informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue
and no actions to automatically resolve the issue can/should be done).
For conditions where Status=Unknown or Status=True the Severity should be SeverityNone.
type: string
status:
description: Status of the condition, one of True, False, Unknown.
type: string
type:
description: Type of condition in CamelCase.
type: string
required:
- lastTransitionTime
- status
- type
type: object
type: array
createdAt:
description: CreatedAt - timestap of creation
format: date-time
type: string
expiresAt:
description: ExpiresAt - time of validity expiration
format: date-time
type: string
lastRotated:
description: LastRotated - timestamp when credentials were last rotated
format: date-time
type: string
rotationEligibleAt:
description: |-
RotationEligibleAt indicates when rotation becomes eligible (start of grace period window).
Computed as ExpiresAt - GracePeriodDays. The AC can be rotated after this timestamp.
format: date-time
type: string
secretName:
description: SecretName - name of the k8s Secret storing the ApplicationCredential
secret
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
15 changes: 15 additions & 0 deletions api/v1beta1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const (

// KeystoneServiceOSUserReadyCondition Status=True condition which indicates if the service user got created in the keystone instance is ready/was successful
KeystoneServiceOSUserReadyCondition condition.Type = "KeystoneServiceOSUserReady"

// KeystoneApplicationCredentialReadyCondition Status=True condition which indicates if the ApplicationCredential has been created and is ready
KeystoneApplicationCredentialReadyCondition condition.Type = "KeystoneApplicationCredentialReady"
)

// Common Messages used by API objects.
Expand Down Expand Up @@ -111,4 +114,16 @@ const (

// KeystoneServiceOSUserReadyErrorMessage
KeystoneServiceOSUserReadyErrorMessage = "Keystone Service user error occured %s"

//
// KeystoneApplicationCredentialReady condition messages
//
// KeystoneApplicationCredentialReadyInitMessage
KeystoneApplicationCredentialReadyInitMessage = "ApplicationCredential not yet created"

// KeystoneApplicationCredentialReadyMessage
KeystoneApplicationCredentialReadyMessage = "ApplicationCredential ready"

// KeystoneApplicationCredentialReadyErrorMessage
KeystoneApplicationCredentialReadyErrorMessage = "ApplicationCredential error occurred: %s"
)
106 changes: 106 additions & 0 deletions api/v1beta1/keystoneapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,112 @@ func GetAdminServiceClient(
return os, ctrlResult, nil
}

// GetUserServiceClient - returns an *openstack.OpenStack object scoped as the given service user
func GetUserServiceClient(
ctx context.Context,
h *helper.Helper,
keystoneAPI *KeystoneAPI,
userName string,
secretName string,
passwordSelector string,
) (*openstack.OpenStack, ctrl.Result, error) {

authURL, err := keystoneAPI.GetEndpoint(endpoint.EndpointInternal)
if err != nil {
return nil, ctrl.Result{}, err
}

parsedAuthURL, err := url.Parse(authURL)
if err != nil {
return nil, ctrl.Result{}, err
}

tlsConfig := &openstack.TLSConfig{}
if parsedAuthURL.Scheme == "https" && keystoneAPI.Spec.TLS.CaBundleSecretName != "" {
caCert, ctrlResult, err := secret.GetDataFromSecret(
ctx,
h,
keystoneAPI.Spec.TLS.CaBundleSecretName,
10*time.Second,
tls.InternalCABundleKey)
if err != nil {
return nil, ctrlResult, err
}
if (ctrlResult != ctrl.Result{}) {
return nil, ctrlResult,
fmt.Errorf("CABundleSecret %s not found",
keystoneAPI.Spec.TLS.CaBundleSecretName)
}

tlsConfig = &openstack.TLSConfig{
CACerts: []string{caCert},
}
}

password, res, err := getPasswordFromOSPSecret(ctx, h, secretName, passwordSelector)
if err != nil {
return nil, ctrl.Result{}, fmt.Errorf("failed to get password from osp-secret for user %q: %w", userName, err)
}
if res != (ctrl.Result{}) {
return nil, res, nil
}

scope := &gophercloud.AuthScope{
ProjectName: "service",
DomainName: "Default",
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very wary about hard-coding stuff here. I think that;

  1. there may be cases where either the service project or DomainName may be different
  2. we might be interested in obtaining an app cred for a non-service user or for a different project. For example, I know that there is work ongoing upstream to do manila share encryption where the user would use app creds to retrieve a barbican secret.

In any case, I think it makes sense to make this function more generic - maybe call it GetUserClient() and pass in the domain and project name. You could then add these parameters to the app cred spec, and default to "service" and "Default". This will future-proof the interface a bit.

Copy link
Contributor Author

@Deydra71 Deydra71 May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, but right now we don't have any spec.ServiceProject or spec.ServiceDomain. So, that's why this serves as a mean to only get scoped token for our service.

Since FR3 is explicitly about wiring up service-account auth, I suggest we leave this helper as is for now, and add a // TODO pointing at the future work to extend the CRD with project/domain fields and refactor into a generic GetUserClient(…, project, domain…)

The domain name is actually hard coded for admin as well - https://github.com/openstack-k8s-operators/keystone-operator/blob/main/api/v1beta1/keystoneapi.go#L157

Copy link
Contributor Author

@Deydra71 Deydra71 May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there may be cases where either the service project or DomainName may be different

Is this still true if we take into account only service users?


osClient, err := openstack.NewOpenStack(
ctx,
h.GetLogger(),
openstack.AuthOpts{
AuthURL: authURL,
Username: userName,
Password: password,
TenantName: "service",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto comment as above.

DomainName: "Default",
Region: keystoneAPI.Spec.Region,
TLS: tlsConfig,
Scope: scope,
},
)
if err != nil {
return nil, ctrl.Result{}, err
}

return osClient, ctrl.Result{}, nil
}

func getPasswordFromOSPSecret(
ctx context.Context,
h *helper.Helper,
ospSecretName, passwordSelector string,
) (string, ctrl.Result, error) {
if ospSecretName == "" {
return "", ctrl.Result{}, fmt.Errorf("secret name is empty, cannot retrieve password for selector %q", passwordSelector)
}
if passwordSelector == "" {
return "", ctrl.Result{}, fmt.Errorf("password selector is empty, cannot retrieve password from secret %q", ospSecretName)
}
data, res, err := secret.GetDataFromSecret(
ctx,
h,
ospSecretName,
10*time.Second,
passwordSelector,
)
if err != nil {
return "", ctrl.Result{}, fmt.Errorf("failed to get %q from Secret/%s: %w", passwordSelector, ospSecretName, err)
}
if res != (ctrl.Result{}) {
return "", res, nil
}
if data == "" {
return "", ctrl.Result{}, fmt.Errorf("password selector %q in secret %q is empty", passwordSelector, ospSecretName)
}
return data, ctrl.Result{}, nil
}

// GetScopedAdminServiceClient - get a scoped admin serviceClient for the keystoneAPI instance
func GetScopedAdminServiceClient(
ctx context.Context,
Expand Down
Loading