Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions api/bases/keystone.openstack.org_keystoneapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ spec:
description: EnableSecureRBAC - Enable Consistent and Secure RBAC
policies
type: boolean
externalKeystoneAPI:
default: false
description: ExternalKeystoneAPI - Enable use of external Keystone
API endpoints instead of deploying a local Keystone API
type: boolean
extraMounts:
default: []
description: ExtraMounts containing conf files
Expand Down
21 changes: 21 additions & 0 deletions api/v1beta1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,25 @@ const (

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

//
// External Keystone API condition messages
//
// ExternalKeystoneAPIDBMessage
ExternalKeystoneAPIDBMessage = "External Keystone API configured - database is not managed by this operator"

// ExternalKeystoneAPIDBAccountMessage
ExternalKeystoneAPIDBAccountMessage = "External Keystone API configured - database account is not managed by this operator"

// ExternalKeystoneAPIRabbitMQTransportURLMessage
ExternalKeystoneAPIRabbitMQTransportURLMessage = "External Keystone API configured - RabbitMQ is not managed by this operator"

// ExternalKeystoneAPIMemcachedReadyMessage
ExternalKeystoneAPIMemcachedReadyMessage = "External Keystone API configured - memcached is not managed by this operator"

// ExternalKeystoneAPIServiceConfigReadyMessage
ExternalKeystoneAPIServiceMessage = "External Keystone API configured - service is not managed by this operator"

// ExternalKeystoneAPINetworkAttachmentsReadyMessage
ExternalKeystoneAPINetworkAttachmentsReadyMessage = "External Keystone API configured - network attachments are not managed by this operator"
)
12 changes: 8 additions & 4 deletions api/v1beta1/keystoneapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/openstack-k8s-operators/lib-common/modules/common/endpoint"
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
"github.com/openstack-k8s-operators/lib-common/modules/common/secret"
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
Expand Down Expand Up @@ -145,8 +144,13 @@ func GetScopedAdminServiceClient(
keystoneAPI *KeystoneAPI,
scope *gophercloud.AuthScope,
) (*openstack.OpenStack, ctrl.Result, error) {
// get public endpoint as authurl from keystone instance
authURL, err := keystoneAPI.GetEndpoint(endpoint.EndpointInternal)
// get endpoint as authurl from keystone instance
// default to internal endpoint if not specified
epInterface := endpoint.EndpointInternal
if keystoneAPI.Spec.ExternalKeystoneAPI {
epInterface = endpoint.Endpoint(endpoint.EndpointPublic)
}
authURL, err := keystoneAPI.GetEndpoint(epInterface)
if err != nil {
return nil, ctrl.Result{}, err
}
Expand All @@ -163,7 +167,7 @@ func GetScopedAdminServiceClient(
h,
keystoneAPI.Spec.TLS.CaBundleSecretName,
10*time.Second,
tls.InternalCABundleKey)
interfaceBundleKeys[epInterface])
if err != nil {
return nil, ctrl.Result{}, err
}
Expand Down
13 changes: 13 additions & 0 deletions api/v1beta1/keystoneapi_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ const (
APIDefaultTimeout = 60
)

var (
// interfaceBundleKeys maps endpoint winterfaces to their corresponding key in the CA bundle secret
Copy link
Contributor Author

Choose a reason for hiding this comment

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

s/winterfaces/interfaces/

interfaceBundleKeys = map[endpoint.Endpoint]string{
endpoint.EndpointInternal: tls.InternalCABundleKey,
endpoint.EndpointPublic: tls.CABundleKey,
}
)

// KeystoneAPISpec defines the desired state of KeystoneAPI
type KeystoneAPISpec struct {
KeystoneAPISpecCore `json:",inline"`
Expand Down Expand Up @@ -213,6 +221,11 @@ type KeystoneAPISpecCore struct {
// This is only needed when multiple realms are federated.
// Config files mount path is set to /var/lib/httpd/metadata/
FederatedRealmConfig string `json:"federatedRealmConfig"`

// +kubebuilder:validation:Optional
// +kubebuilder:default=false
// ExternalKeystoneAPI - Enable use of external Keystone API endpoints instead of deploying a local Keystone API
ExternalKeystoneAPI bool `json:"externalKeystoneAPI"`
}

// APIOverrideSpec to override the generated manifest of several child resources.
Expand Down
100 changes: 100 additions & 0 deletions api/v1beta1/keystoneapi_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package v1beta1

import (
"fmt"
"net/url"

"github.com/openstack-k8s-operators/lib-common/modules/common/service"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -115,6 +116,9 @@ func (spec *KeystoneAPISpecCore) ValidateCreate(basePath *field.Path, namespace
// referenced because is not supported
allErrs = append(allErrs, spec.ValidateTopology(basePath, namespace)...)

// Validate external Keystone API configuration
allErrs = append(allErrs, spec.ValidateExternalKeystoneAPI(basePath)...)

return allErrs
}

Expand Down Expand Up @@ -158,6 +162,9 @@ func (spec *KeystoneAPISpecCore) ValidateUpdate(_ KeystoneAPISpecCore, basePath
// referenced because is not supported
allErrs = append(allErrs, spec.ValidateTopology(basePath, namespace)...)

// Validate external Keystone API configuration
allErrs = append(allErrs, spec.ValidateExternalKeystoneAPI(basePath)...)

return allErrs
}

Expand All @@ -169,6 +176,99 @@ func (r *KeystoneAPI) ValidateDelete() (admission.Warnings, error) {
return nil, nil
}

// ValidateExternalKeystoneAPI validates the external Keystone API configuration
func (spec *KeystoneAPISpecCore) ValidateExternalKeystoneAPI(basePath *field.Path) field.ErrorList {
var allErrs field.ErrorList

if !spec.ExternalKeystoneAPI {
// No validation needed when external Keystone API is not enabled
return allErrs
}

overridePath := basePath.Child("override").Child("service")

// Both public and internal endpoints must be defined with EndpointURL set
// This ensures services that depend on both endpoints (like Glance) don't fail
// when rendering templates
// Note: We don't check for nil or empty service override here because the
// hasPublic and hasInternal checks below will catch missing endpoints regardless
// of whether the map is nil, empty, or contains invalid keys.
hasPublic := false
hasInternal := false

for endpointType, overrideSpec := range spec.Override.Service {
endpointURLPath := overridePath.Key(string(endpointType)).Child("endpointURL")
if endpointType == service.EndpointPublic {
hasPublic = true
if overrideSpec.EndpointURL == nil || *overrideSpec.EndpointURL == "" {
allErrs = append(allErrs, field.Required(
endpointURLPath,
"external Keystone API requires endpointURL to be set for public endpoint",
))
} else {
// Validate URL format
parsedURL, err := url.Parse(*overrideSpec.EndpointURL)
if err != nil {
allErrs = append(allErrs, field.Invalid(
endpointURLPath,
*overrideSpec.EndpointURL,
fmt.Sprintf("invalid URL format: %v", err),
))
} else if parsedURL.Scheme == "" {
// Require a scheme (http:// or https://)
allErrs = append(allErrs, field.Invalid(
endpointURLPath,
*overrideSpec.EndpointURL,
"URL must include a scheme (e.g., http:// or https://)",
))
}
}
}
if endpointType == service.EndpointInternal {
hasInternal = true
if overrideSpec.EndpointURL == nil || *overrideSpec.EndpointURL == "" {
allErrs = append(allErrs, field.Required(
endpointURLPath,
"external Keystone API requires endpointURL to be set for internal endpoint",
))
} else {
// Validate URL format
parsedURL, err := url.Parse(*overrideSpec.EndpointURL)
if err != nil {
allErrs = append(allErrs, field.Invalid(
endpointURLPath,
*overrideSpec.EndpointURL,
fmt.Sprintf("invalid URL format: %v", err),
))
} else if parsedURL.Scheme == "" {
// Require a scheme (http:// or https://)
allErrs = append(allErrs, field.Invalid(
endpointURLPath,
*overrideSpec.EndpointURL,
"URL must include a scheme (e.g., http:// or https://)",
))
}
}
}
}

if !hasPublic {
allErrs = append(allErrs, field.Required(
overridePath,
fmt.Sprintf("external Keystone API requires %s endpoint to be defined", service.EndpointPublic),
))
}

if !hasInternal {
allErrs = append(allErrs, field.Required(
overridePath,
fmt.Sprintf("external Keystone API requires %s endpoint to be defined", service.EndpointInternal),
))
}

return allErrs
}

// SetDefaultRouteAnnotations sets HAProxy timeout values of the route
func (spec *KeystoneAPISpecCore) SetDefaultRouteAnnotations(annotations map[string]string) {
const haProxyAnno = "haproxy.router.openshift.io/timeout"
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/keystone.openstack.org_keystoneapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ spec:
description: EnableSecureRBAC - Enable Consistent and Secure RBAC
policies
type: boolean
externalKeystoneAPI:
default: false
description: ExternalKeystoneAPI - Enable use of external Keystone
API endpoints instead of deploying a local Keystone API
type: boolean
extraMounts:
default: []
description: ExtraMounts containing conf files
Expand Down
Loading