Skip to content
Merged
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
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
1 change: 1 addition & 0 deletions api/v1beta1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,5 @@ const (

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

)
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 interfaces to their corresponding key in the CA bundle secret
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 == "" {
Comment thread
vakwetu marked this conversation as resolved.
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