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
8 changes: 8 additions & 0 deletions api/bases/glance.openstack.org_glanceapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ spec:
- single
- edge
type: string
auth:
description: Auth - Parameters related to authentication
properties:
applicationCredentialSecret:
description: ApplicationCredentialSecret - Secret containing Application
Credential ID and Secret
type: string
type: object
containerImage:
description: ContainerImage - GlanceAPI Container Image URL
type: string
Expand Down
8 changes: 8 additions & 0 deletions api/bases/glance.openstack.org_glances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,14 @@ spec:
APITimeout
minimum: 1
type: integer
auth:
description: Auth - Parameters related to authentication
properties:
applicationCredentialSecret:
description: ApplicationCredentialSecret - Secret containing
Application Credential ID and Secret
type: string
type: object
customServiceConfig:
description: |-
CustomServiceConfig - customize the service config using this parameter to change service defaults,
Expand Down
17 changes: 15 additions & 2 deletions api/v1beta1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ package v1beta1
import (
"strings"

topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)

const (
Expand Down Expand Up @@ -100,6 +100,11 @@ type GlanceAPITemplate struct {
// TLS - Parameters related to the TLS
TLS tls.API `json:"tls,omitempty"`

// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec
// Auth - Parameters related to authentication
Auth AuthSpec `json:"auth,omitempty"`

// ImageCache - It represents the struct to expose the ImageCache related
// parameters (size of the PVC and cronJob schedule)
// +kubebuilder:validation:Optional
Expand Down Expand Up @@ -147,6 +152,14 @@ type APIOverrideSpec struct {
Service map[service.Endpoint]service.RoutedOverrideSpec `json:"service,omitempty"`
}

// AuthSpec defines authentication parameters
type AuthSpec struct {
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec
// ApplicationCredentialSecret - Secret containing Application Credential ID and Secret
ApplicationCredentialSecret string `json:"applicationCredentialSecret,omitempty"`
}

// SetupDefaults - initializes any CRD field defaults based on environment variables (the defaulting mechanism itself is implemented via webhooks)
func SetupDefaults() {
// Acquire environmental defaults and initialize Glance defaults with them
Expand Down
3 changes: 1 addition & 2 deletions api/v1beta1/glance_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"strings"

"github.com/google/go-cmp/cmp"
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -29,7 +30,6 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"

common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook"
)
Expand Down Expand Up @@ -157,7 +157,6 @@ func (r *GlanceSpecCore) Default() {
}
}


// Check if File is used as a backend for Glance
func isFileBackend(customServiceConfig string, topLevel bool) bool {
availableBackends := GetEnabledBackends(customServiceConfig)
Expand Down
7 changes: 4 additions & 3 deletions api/v1beta1/glanceapi_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ limitations under the License.
package v1beta1

import (
"fmt"

"github.com/google/go-cmp/cmp"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"fmt"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/google/go-cmp/cmp"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)

// GlanceAPIDefaults -
Expand Down
16 changes: 16 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions config/crd/bases/glance.openstack.org_glanceapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ spec:
- single
- edge
type: string
auth:
description: Auth - Parameters related to authentication
properties:
applicationCredentialSecret:
description: ApplicationCredentialSecret - Secret containing Application
Credential ID and Secret
type: string
type: object
containerImage:
description: ContainerImage - GlanceAPI Container Image URL
type: string
Expand Down
8 changes: 8 additions & 0 deletions config/crd/bases/glance.openstack.org_glances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,14 @@ spec:
APITimeout
minimum: 1
type: integer
auth:
description: Auth - Parameters related to authentication
properties:
applicationCredentialSecret:
description: ApplicationCredentialSecret - Secret containing
Application Credential ID and Secret
type: string
type: object
customServiceConfig:
description: |-
CustomServiceConfig - customize the service config using this parameter to change service defaults,
Expand Down
4 changes: 4 additions & 0 deletions internal/controller/glance_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import (
// Common static errors for glance controllers
var (
ErrNetworkAttachmentConfig = errors.New("not all pods have interfaces with ips as configured in NetworkAttachments")
ErrACSecretNotFound = errors.New("ApplicationCredential secret not found")
ErrACSecretMissingKeys = errors.New("ApplicationCredential secret missing required keys")
)

// fields to index to reconcile when change
Expand All @@ -58,6 +60,7 @@ const (
tlsAPIPublicField = ".spec.tls.api.public.secretName"
topologyField = ".spec.topologyRef.Name"
notificationBusSecretField = ".spec.notificationBusSecret"
authAppCredSecretField = ".spec.auth.applicationCredentialSecret" // #nosec G101
)

var (
Expand All @@ -71,6 +74,7 @@ var (
tlsAPIPublicField,
topologyField,
notificationBusSecretField,
authAppCredSecretField,
}
)

Expand Down
34 changes: 34 additions & 0 deletions internal/controller/glanceapi_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,18 @@ func (r *GlanceAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
return err
}

// index authAppCredSecretField
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &glancev1.GlanceAPI{}, authAppCredSecretField, func(rawObj client.Object) []string {
Comment thread
fmount marked this conversation as resolved.
// Extract the application credential secret name from the spec, if one is provided
cr := rawObj.(*glancev1.GlanceAPI)
if cr.Spec.Auth.ApplicationCredentialSecret == "" {
return nil
}
return []string{cr.Spec.Auth.ApplicationCredentialSecret}
}); err != nil {
return err
}

// Watch for changes to any CustomServiceConfigSecrets. Global secrets
svcSecretFn := func(_ context.Context, o client.Object) []reconcile.Request {
var namespace = o.GetNamespace()
Expand Down Expand Up @@ -1285,6 +1297,28 @@ func (r *GlanceAPIReconciler) generateServiceConfig(
"Wsgi": wsgi,
}

// Try to get Application Credential from the secret specified in the CR
if instance.Spec.Auth.ApplicationCredentialSecret != "" {
acSecretObj, _, err := secret.GetSecret(ctx, h, instance.Spec.Auth.ApplicationCredentialSecret, instance.Namespace)
if err != nil {
if k8s_errors.IsNotFound(err) {
Log.Info("ApplicationCredential secret not found, waiting", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
return fmt.Errorf("%w: %s", ErrACSecretNotFound, instance.Spec.Auth.ApplicationCredentialSecret)
}
Log.Error(err, "Failed to get ApplicationCredential secret", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
return err
}
acID, okID := acSecretObj.Data[keystonev1.ACIDSecretKey]
acSecretData, okSecret := acSecretObj.Data[keystonev1.ACSecretSecretKey]
if okID && len(acID) > 0 && okSecret && len(acSecretData) > 0 {
templateParameters["ApplicationCredentialID"] = string(acID)
templateParameters["ApplicationCredentialSecret"] = string(acSecretData)
Log.Info("Using ApplicationCredentials auth", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
} else {
return fmt.Errorf("%w: %s", ErrACSecretMissingKeys, instance.Spec.Auth.ApplicationCredentialSecret)
}
}

// (OSPRH-18291)Only set EndpointID parameter when the Endpoint has been
// created and the associated ID is set in the keystoneapi CR. Because we
// have the Keystone CR, we get the Region parameter mirrored in its
Expand Down
29 changes: 24 additions & 5 deletions templates/common/config/00-config.conf
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@ default_backend=default_backend
[keystone_authtoken]
www_authenticate_uri={{ .KeystonePublicURL }}
auth_url={{ .KeystoneInternalURL }}
auth_type=password
username={{ .ServiceUser }}
{{ if (index . "ApplicationCredentialID") -}}
auth_type = v3applicationcredential
application_credential_id = {{ .ApplicationCredentialID }}
application_credential_secret = {{ .ApplicationCredentialSecret }}
{{- else -}}
auth_type = password
username = {{ .ServiceUser }}
password = {{ .ServicePassword }}
project_domain_name = Default
user_domain_name = Default
project_name = service
{{- end }}
{{ if (index . "MemcachedServers") }}
memcached_servers = {{ .MemcachedServers }}
memcache_pool_dead_retry = 10
Expand All @@ -55,15 +64,19 @@ memcache_tls_keyfile = {{ .MemcachedAuthKey }}
memcache_tls_cafile = {{ .MemcachedAuthCa }}
memcache_tls_enabled = true
{{end}}
project_domain_name=Default
user_domain_name=Default
project_name=service
{{ if (index . "Region") -}}
region_name = {{ .Region }}
{{ end -}}

[service_user]
{{ if (index . "ApplicationCredentialID") -}}
auth_type = v3applicationcredential
application_credential_id = {{ .ApplicationCredentialID }}
application_credential_secret = {{ .ApplicationCredentialSecret }}
{{ else -}}
auth_type = password
password = {{ .ServicePassword }}
{{- end }}

[oslo_messaging_notifications]
{{ if (index . "TransportURL") -}}
Expand Down Expand Up @@ -97,11 +110,17 @@ filesystem_store_datadir = /var/lib/glance/os_glance_tasks_store/

[oslo_limit]
auth_url={{ .KeystoneInternalURL }}
{{ if (index . "ApplicationCredentialID") -}}
auth_type = v3applicationcredential
application_credential_id = {{ .ApplicationCredentialID }}
application_credential_secret = {{ .ApplicationCredentialSecret }}
{{ else -}}
auth_type = password
username={{ .ServiceUser }}
password = {{ .ServicePassword }}
system_scope = all
user_domain_id = default
{{- end }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Application credentials cannot be used for oslo_limit because they are
unscoped by design, while oslo_limit requires system-scoped access
(system_scope=all). Unscoped tokens cannot access system-scoped resources,
so password auth is required to obtain system-scoped tokens.

This means we still need manual update for oslo_limit if password is changed for service user.

Copy link
Copy Markdown
Contributor

@konan-abhi konan-abhi Dec 22, 2025

Choose a reason for hiding this comment

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

So I was doing some R & D with oslo_limit and application credentials and found that if we set system_scope = unscoped and add endpoint_id of keystone service the quotas works as expected but on the other hand I am not sure the impact of changing system_scope from all to unscoped.

Required configs for oslo_limit to work with application credentials:

[oslo_limit]
auth_type = v3applicationcredential
application_credential_id = 8584854904234ef194c343e408791aa9
application_credential_secret = zk8dINEEMIIfQFgxr1tX-neAsHIvOzs7_HvnBmeMBX4gNS8tuSHkW7CNu0MIaDq0C_H052LioPpbDcEUUFA6lw
auth_url = https://10.0.110.74/identity
user_domain_name = Default
system_scope = unscoped
endpoint_id = b7426e96bc5c4cdf9916950ac8d22454 // Endpoint ID of keystone service

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hello @konan-abhi, thanks a lot for providing details! So, for now we should remove the application credential completely from the oslo_limit config section and keep only the password auth option for now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey @Deydra71
I don't think removing application credential from oslo limit will help us from achieving complete ZDPR. We have couple of options;

  1. Do not use quotas (which uses oslo_limit) if application credentials will be used
  2. Add support for AC for oslo_limit (this will need inputs from keystone as oslo limit uses system scope and AC are for project specific). I have WIP patch for it but it need attention from keystone folks

keystone patch: https://review.opendev.org/c/openstack/keystone/+/971735
Devstack support: https://review.opendev.org/c/openstack/devstack/+/971680
Glance Upstream depending on both above to confirm it works: https://review.opendev.org/c/openstack/glance/+/971682

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Application credentials cannot be used for oslo_limit because they are
unscoped by design

This is incorrect. Application Credentials are scoped to a specific project and have role assignments only on that project. The issue with GET /v3/limits is that results were being filtered based on the scope of the token. For project-scoped tokens, the results were always being filtered regardless of role assignment.

This was fixed in: https://opendev.org/openstack/keystone/commit/8a42793e714efb0b5c1bb07bcb81d2b4ab59b600

{{ if (index . "EndpointID") -}}
endpoint_id = {{ .EndpointID }}
{{ end -}}
Expand Down
51 changes: 51 additions & 0 deletions test/functional/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (

. "github.com/onsi/gomega" //revive:disable:dot-imports
glancev1 "github.com/openstack-k8s-operators/glance-operator/api/v1beta1"
"github.com/openstack-k8s-operators/glance-operator/internal/glance"
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -446,3 +448,52 @@ func CreateGlanceMessageBusSecret(namespace string, name string) *corev1.Secret
logger.Info("Secret created", "name", name)
return s
}

// GetGlanceAPISpecWithAC returns a GlanceAPI spec with Application Credential configured
func GetGlanceAPISpecWithAC(apiType APIType, acSecretName string) map[string]interface{} {
spec := CreateGlanceAPISpec(apiType)
spec["secret"] = ACTestServicePasswordSecret
spec["auth"] = map[string]interface{}{
"applicationCredentialSecret": acSecretName,
}
return spec
}

// GetDefaultGlanceAC returns a default KeystoneApplicationCredential spec for testing
func GetDefaultGlanceAC(namespace string, acName string) *keystonev1.KeystoneApplicationCredential {
return &keystonev1.KeystoneApplicationCredential{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: acName,
},
Spec: keystonev1.KeystoneApplicationCredentialSpec{
UserName: glance.ServiceName,
Secret: ACTestServicePasswordSecret,
PasswordSelector: ACTestPasswordSelector,
Roles: []string{"admin", "member"},
AccessRules: []keystonev1.ACRule{{Service: "identity", Method: "POST", Path: "/auth/tokens"}},
ExpirationDays: 30,
GracePeriodDays: 5,
},
}
}

// CreateACSecret creates an Application Credential secret for testing
func CreateACSecret(namespace string, secretName string) *corev1.Secret {
return th.CreateSecret(
types.NamespacedName{Namespace: namespace, Name: secretName},
map[string][]byte{
keystonev1.ACIDSecretKey: []byte("test-ac-id"),
keystonev1.ACSecretSecretKey: []byte("test-ac-secret"),
},
)
}

// GetKeystoneAC fetches a KeystoneApplicationCredential by name
func GetKeystoneAC(name types.NamespacedName) *keystonev1.KeystoneApplicationCredential {
instance := &keystonev1.KeystoneApplicationCredential{}
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed())
}, timeout, interval).Should(Succeed())
return instance
}
4 changes: 4 additions & 0 deletions test/functional/glance_test_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const (
GlanceCephExtraMountsPath = "/etc/ceph"
// GlanceCephExtraMountsSecretName -
GlanceCephExtraMountsSecretName = "ceph"
// ACTestServicePasswordSecret - secret name for AC test service password
ACTestServicePasswordSecret = "ac-test-osp-secret" // #nosec G101
// ACTestPasswordSelector - password selector for AC test
ACTestPasswordSelector = "GlancePassword"
)

// GlanceTestData is the data structure used to provide input data to envTest
Expand Down
Loading