From 139670c587293c22044cd1a712eadb252613e74e Mon Sep 17 00:00:00 2001 From: Veronika Fisarova Date: Thu, 2 Oct 2025 15:55:39 +0200 Subject: [PATCH] Application Credential support --- config/rbac/role.yaml | 1 + controllers/swift_common.go | 6 +- controllers/swift_controller.go | 2 + controllers/swiftproxy_controller.go | 117 +++++++++++++++++- pkg/swiftproxy/templates.go | 10 ++ .../swiftproxy/config/00-proxy-server.conf | 12 ++ 6 files changed, 142 insertions(+), 6 deletions(-) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 03744b8c..729bec18 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -102,6 +102,7 @@ rules: - keystone.openstack.org resources: - keystoneapis + - keystoneapplicationcredentials verbs: - get - list diff --git a/controllers/swift_common.go b/controllers/swift_common.go index a56148c1..5d1ec0c7 100644 --- a/controllers/swift_common.go +++ b/controllers/swift_common.go @@ -22,14 +22,14 @@ import ( "fmt" "time" + topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/env" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" "github.com/openstack-k8s-operators/lib-common/modules/common/secret" - "k8s.io/apimachinery/pkg/types" - topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" - "github.com/openstack-k8s-operators/lib-common/modules/common/env" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" diff --git a/controllers/swift_controller.go b/controllers/swift_controller.go index fa1197bc..cf31b01a 100644 --- a/controllers/swift_controller.go +++ b/controllers/swift_controller.go @@ -59,6 +59,8 @@ func (r *SwiftReconciler) GetLogger(ctx context.Context) logr.Logger { //+kubebuilder:rbac:groups=swift.openstack.org,resources=swifts,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=swift.openstack.org,resources=swifts/status,verbs=get;update;patch //+kubebuilder:rbac:groups=swift.openstack.org,resources=swifts/finalizers,verbs=update;patch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +//+kubebuilder:rbac:groups=keystone.openstack.org,resources=keystoneapplicationcredentials,verbs=get;list;watch // service account, role, rolebinding // +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch diff --git a/controllers/swiftproxy_controller.go b/controllers/swiftproxy_controller.go index 509d62da..92ff6d2c 100644 --- a/controllers/swiftproxy_controller.go +++ b/controllers/swiftproxy_controller.go @@ -512,6 +512,19 @@ func (r *SwiftProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrlResult, err } + // Check for Application Credentials + ctrlResult, err = r.verifyApplicationCredentials( + ctx, + r.GetLogger(ctx), + helper.GetClient(), + instance.Namespace, + "swift", + &envVars, + ) + if (err != nil || ctrlResult != ctrl.Result{}) { + return ctrlResult, err + } + // Get the service password and pass it to the template sps, _, err := secret.GetSecret(ctx, helper, instance.Spec.Secret, instance.Namespace) if err != nil { @@ -578,6 +591,20 @@ func (r *SwiftProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, err } + // Get Application Credential data if available + useAC := false + acID := "" + acSecret := "" + // Try to get Application Credential for this service (via keystone api helper) + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, swift.ServiceName); err != nil { + Log.Error(err, "Failed to get ApplicationCredential for service", "service", swift.ServiceName) + } else if acData != nil { + useAC = true + acID = acData.ID + acSecret = acData.Secret + Log.Info("Using ApplicationCredentials auth", "service", swift.ServiceName) + } + // Create a Secret populated with content from templates/ tpl := swiftproxy.SecretTemplates( instance, @@ -591,6 +618,9 @@ func (r *SwiftProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request) os.GetRegion(), transportURLString, instance.Spec.APITimeout, + useAC, + acID, + acSecret, ) err = secret.EnsureSecrets(ctx, helper, instance, tpl, &envVars) if err != nil { @@ -846,7 +876,43 @@ func (r *SwiftProxyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma return nil } - return ctrl.NewControllerManagedBy(mgr). + // Application Credential secret watching function + acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request { + name := o.GetName() + ns := o.GetNamespace() + result := []reconcile.Request{} + + // Only handle Secret objects + if _, isSecret := o.(*corev1.Secret); !isSecret { + return nil + } + + // Check if this is a swift AC secret by name pattern (ac-swift-secret) + expectedSecretName := keystonev1.GetACSecretName("swift") + if name == expectedSecretName { + // get all SwiftProxy CRs in this namespace + swiftProxies := &swiftv1beta1.SwiftProxyList{} + listOpts := []client.ListOption{ + client.InNamespace(ns), + } + if err := r.List(context.Background(), swiftProxies, listOpts...); err != nil { + return nil + } + + // Enqueue reconcile for all swift proxy instances + for _, cr := range swiftProxies.Items { + objKey := client.ObjectKey{ + Namespace: ns, + Name: cr.Name, + } + result = append(result, reconcile.Request{NamespacedName: objKey}) + } + } + + return result + } + + b := ctrl.NewControllerManagedBy(mgr). For(&swiftv1beta1.SwiftProxy{}). Owns(&corev1.Secret{}). Owns(&keystonev1.KeystoneService{}). @@ -859,6 +925,8 @@ func (r *SwiftProxyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(acSecretFn)). Watches(&memcachedv1.Memcached{}, handler.EnqueueRequestsFromMapFunc(memcachedFn)). Watches(&topologyv1.Topology{}, @@ -866,8 +934,8 @@ func (r *SwiftProxyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma builder.WithPredicates(predicate.GenerationChangedPredicate{})). Watches(&keystonev1.KeystoneAPI{}, handler.EnqueueRequestsFromMapFunc(r.findObjectForSrc), - builder.WithPredicates(keystonev1.KeystoneAPIStatusChangedPredicate)). - Complete(r) + builder.WithPredicates(keystonev1.KeystoneAPIStatusChangedPredicate)) + return b.Complete(r) } func (r *SwiftProxyReconciler) findObjectsForSrc(ctx context.Context, src client.Object) []reconcile.Request { @@ -1031,3 +1099,46 @@ func (r *SwiftProxyReconciler) transportURLCreateOrUpdate( return transportURL, op, err } + +// verifyApplicationCredentials checks if ApplicationCredential secret exists and adds it to configVars +// The AC secret is created by the keystone-operator's AC controller when the AC is ready. +// If the secret exists and is valid, we use AC auth. Otherwise, we fall back to password auth. +func (r *SwiftProxyReconciler) verifyApplicationCredentials( + ctx context.Context, + log logr.Logger, + client client.Client, + namespace string, + serviceName string, + configVars *map[string]env.Setter, +) (ctrl.Result, error) { + // Check if AC secret exists (created by keystone AC controller) + acSecretName := keystonev1.GetACSecretName(serviceName) + secretKey := types.NamespacedName{Namespace: namespace, Name: acSecretName} + + hash, res, err := secret.VerifySecret( + ctx, + secretKey, + []string{"AC_ID", "AC_SECRET"}, + client, + 10*time.Second, + ) + + // VerifySecret returns res.RequeueAfter > 0 when secret not found (not an error) + // For AC, this is optional, so we just skip it instead of requeueing + if res.RequeueAfter > 0 { + log.V(1).Info("ApplicationCredential secret not found, using password auth") + return ctrl.Result{}, nil + } + + if err != nil { + // Actual error (not NotFound) - log and continue with password auth + log.Info("ApplicationCredential secret verification failed, continuing with password auth", "error", err.Error()) + return ctrl.Result{}, nil + } + + // AC secret exists and is valid - add to configVars for hash tracking + (*configVars)["secret-"+acSecretName] = env.SetValue(hash) + log.Info("Using ApplicationCredential authentication") + + return ctrl.Result{}, nil +} diff --git a/pkg/swiftproxy/templates.go b/pkg/swiftproxy/templates.go index 307c80ed..1829929b 100644 --- a/pkg/swiftproxy/templates.go +++ b/pkg/swiftproxy/templates.go @@ -40,6 +40,9 @@ func SecretTemplates( keystoneRegion string, transportURL string, apiTimeout int, + useApplicationCredentials bool, + applicationCredentialID string, + applicationCredentialSecret string, ) []util.Template { templateParameters := make(map[string]any) templateParameters["ServiceUser"] = instance.Spec.ServiceUser @@ -54,6 +57,13 @@ func SecretTemplates( templateParameters["TransportURL"] = transportURL templateParameters["APITimeout"] = apiTimeout + // Application Credential parameters + templateParameters["UseApplicationCredentials"] = useApplicationCredentials + if useApplicationCredentials { + templateParameters["ApplicationCredentialID"] = applicationCredentialID + templateParameters["ApplicationCredentialSecret"] = applicationCredentialSecret + } + // MTLS params if mc.Status.MTLSCert != "" { templateParameters["MemcachedAuthCert"] = fmt.Sprint(memcachedv1.CertMountPath()) diff --git a/templates/swiftproxy/config/00-proxy-server.conf b/templates/swiftproxy/config/00-proxy-server.conf index 9aef5b10..f98cf11e 100644 --- a/templates/swiftproxy/config/00-proxy-server.conf +++ b/templates/swiftproxy/config/00-proxy-server.conf @@ -80,12 +80,18 @@ project_reader_roles = SwiftProjectReader paste.filter_factory = keystonemiddleware.auth_token:filter_factory www_authenticate_uri = {{ .KeystonePublicURL }} auth_url = {{ .KeystonePublicURL }} +{{ if .UseApplicationCredentials -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ApplicationCredentialID }} +application_credential_secret = {{ .ApplicationCredentialSecret }} +{{- else -}} auth_plugin=password project_domain_id = default user_domain_id = default project_name = service username = {{ .ServiceUser }} password = {{ .ServicePassword }} +{{- end }} delay_auth_decision = True [filter:s3api] @@ -108,8 +114,14 @@ use = egg:swift#encryption [filter:ceilometer] paste.filter_factory = ceilometermiddleware.swift:filter_factory auth_url = {{ .KeystonePublicURL }} +{{ if .UseApplicationCredentials -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ApplicationCredentialID }} +application_credential_secret = {{ .ApplicationCredentialSecret }} +{{- else -}} password = {{ .ServicePassword }} username = {{ .ServiceUser }} +{{- end }} region_name = {{ .KeystoneRegion }} url = {{ .TransportURL }} project_name = service