diff --git a/api/go.mod b/api/go.mod index dfe7ad8a..b41858e1 100644 --- a/api/go.mod +++ b/api/go.mod @@ -95,3 +95,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.13 //allow-merging replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging + +replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63 diff --git a/controllers/cinder_controller.go b/controllers/cinder_controller.go index 51960a0e..8e6f6f99 100644 --- a/controllers/cinder_controller.go +++ b/controllers/cinder_controller.go @@ -1031,6 +1031,17 @@ func (r *CinderReconciler) generateServiceConfigs( templateParameters["ServicePassword"] = string(ospSecret.Data[instance.Spec.PasswordSelectors.Service]) templateParameters["KeystoneInternalURL"] = keystoneInternalURL templateParameters["KeystonePublicURL"] = keystonePublicURL + + // Check for Application Credentials + Log := r.GetLogger(ctx) + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, h.GetClient(), instance.Namespace, cinder.ServiceName); err != nil { + Log.Error(err, "Failed to get ApplicationCredential for service", "service", cinder.ServiceName) + return err + } else if acData != nil { + templateParameters["ApplicationCredentialID"] = acData.ID + templateParameters["ApplicationCredentialSecret"] = acData.Secret + Log.Info("Using ApplicationCredentials auth", "service", cinder.ServiceName) + } templateParameters["TransportURL"] = transportURLSecretData templateParameters["DatabaseConnection"] = fmt.Sprintf("mysql+pymysql://%s:%s@%s/%s?read_default_file=/etc/my.cnf", databaseAccount.Spec.UserName, diff --git a/controllers/cinderapi_controller.go b/controllers/cinderapi_controller.go index 82e2ba82..19da752b 100644 --- a/controllers/cinderapi_controller.go +++ b/controllers/cinderapi_controller.go @@ -338,6 +338,42 @@ func (r *CinderAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man return err } + // 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 cinder AC secret by name pattern (ac-cinder-secret) + expectedSecretName := keystonev1.GetACSecretName("cinder") + if name == expectedSecretName { + // get all CinderAPI CRs in this namespace + cinderAPIs := &cinderv1beta1.CinderAPIList{} + listOpts := []client.ListOption{ + client.InNamespace(ns), + } + if err := r.List(context.Background(), cinderAPIs, listOpts...); err != nil { + return nil + } + + // Enqueue reconcile for all cinder API instances + for _, cr := range cinderAPIs.Items { + objKey := client.ObjectKey{ + Namespace: ns, + Name: cr.Name, + } + result = append(result, reconcile.Request{NamespacedName: objKey}) + } + } + + return result + } + return ctrl.NewControllerManagedBy(mgr). For(&cinderv1beta1.CinderAPI{}). Owns(&keystonev1.KeystoneService{}). @@ -352,6 +388,8 @@ func (r *CinderAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(acSecretFn)). Watches(&topologyv1.Topology{}, handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.GenerationChangedPredicate{})). @@ -701,6 +739,11 @@ func (r *CinderAPIReconciler) reconcileNormal(ctx context.Context, instance *cin return ctrlResult, nil } + // Check for Application Credentials + if ctrlResult, err = keystonev1.VerifyApplicationCredentialsForService(ctx, r.Client, instance.Namespace, "cinder", &configVars, cinder.NormalDuration); err != nil || ctrlResult.RequeueAfter > 0 { + return ctrlResult, err + } + // // check for required Transport URL and config secrets // diff --git a/go.mod b/go.mod index dc542539..33162624 100644 --- a/go.mod +++ b/go.mod @@ -116,3 +116,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.13 //allow-merging replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging + +replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63 diff --git a/go.sum b/go.sum index 208cb929..93f00760 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63 h1:ug2YPMQJ/+0ifOjFyaPx1YtX0zsVnL02pB2ngacYviw= +github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63/go.mod h1:FMFoO4MjEQ85JpdLtDHxYSZxvJ9KzHua+HdKhpl0KRI= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -100,8 +102,6 @@ github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyU github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo= github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35 h1:QFFGu93A+XCvDUxZIgfBE4gB5hEdVQAIw+E8dF1kP/E= github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35/go.mod h1:qq8BCRxTEmLRriUsQ4HeDUzqltWg32MQPDTMhgbBGK4= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1 h1:QohvX44nxoV2GwvvOURGXYyDuCn4SCrnwubTKJtzehY= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1/go.mod h1:FMFoO4MjEQ85JpdLtDHxYSZxvJ9KzHua+HdKhpl0KRI= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00 h1:Xih6tYYqiDVllo4fDGHqTPL+M2biO5YLOUmbiTqrW/I= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00/go.mod h1:PMoNILOdQ1Ij7DyrKgljN6RAiq8pFM2AGsUb6mcxe98= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251021145236-2b84ec9fd9bb h1:wToXqX7AS1JV3Kna7RcJfkRart8rSGun2biKNfyY6Zg= diff --git a/templates/cinder/config/00-global-defaults.conf b/templates/cinder/config/00-global-defaults.conf index af4912a3..96e987cf 100644 --- a/templates/cinder/config/00-global-defaults.conf +++ b/templates/cinder/config/00-global-defaults.conf @@ -76,12 +76,18 @@ auth_url = {{ .KeystoneInternalURL }} memcached_servers = {{ .MemcachedServers }} memcache_pool_dead_retry = 10 memcache_pool_conn_get_timeout = 2 +{{ if (index . "ApplicationCredentialID") -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ApplicationCredentialID }} +application_credential_secret = {{ .ApplicationCredentialSecret }} +{{ else -}} auth_type = password +username = {{ .ServiceUser }} +password = {{ .ServicePassword }} +{{ end -}} project_domain_name = Default user_domain_name = Default project_name = service -username = {{ .ServiceUser }} -password = {{ .ServicePassword }} service_token_roles_required = true interface = internal {{if (index . "MemcachedAuthCert")}} @@ -93,10 +99,16 @@ memcache_tls_enabled = true [nova] interface = internal +{{ if (index . "ApplicationCredentialID") -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ApplicationCredentialID }} +application_credential_secret = {{ .ApplicationCredentialSecret }} +{{ else -}} auth_type = password -auth_url = {{ .KeystoneInternalURL }} username = {{ .ServiceUser }} password = {{ .ServicePassword }} +{{ end -}} +auth_url = {{ .KeystoneInternalURL }} user_domain_name = Default project_name = service project_domain_name = Default @@ -104,9 +116,15 @@ project_domain_name = Default [service_user] send_service_user_token = True 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 }} +{{ end -}} project_domain_name = Default user_domain_name = Default project_name = service -username = {{ .ServiceUser }} -password = {{ .ServicePassword }} diff --git a/test/functional/cinder_controller_test.go b/test/functional/cinder_controller_test.go index 23a48a7d..32c631da 100644 --- a/test/functional/cinder_controller_test.go +++ b/test/functional/cinder_controller_test.go @@ -29,6 +29,7 @@ import ( corev1 "k8s.io/api/core/v1" k8s_errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" @@ -37,9 +38,11 @@ import ( "github.com/openstack-k8s-operators/cinder-operator/pkg/cinder" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" + keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" util "github.com/openstack-k8s-operators/lib-common/modules/common/util" mariadb_test "github.com/openstack-k8s-operators/mariadb-operator/api/test/helpers" + mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" ) var _ = Describe("Cinder controller", func() { @@ -1744,4 +1747,125 @@ var _ = Describe("Cinder Webhook", func() { return instance, fmt.Sprintf("cinderVolumes[%s].topologyRef", instance) }), ) + + When("An ApplicationCredential is created for Cinder", func() { + var ( + acName string + acSecretName string + servicePasswordSecret string + passwordSelector string + ) + BeforeEach(func() { + servicePasswordSecret = "ac-test-osp-secret" //nolint:gosec // G101 + passwordSelector = "CinderPassword" + + DeferCleanup(k8sClient.Delete, ctx, + CreateCinderMessageBusSecret( + cinderTest.Instance.Namespace, + cinderTest.RabbitmqSecretName, + ), + ) + DeferCleanup(k8sClient.Delete, ctx, + CreateCinderSecret( + cinderTest.Instance.Namespace, servicePasswordSecret)) + // Create Cinder using the service password secret + spec := GetDefaultCinderSpec() + spec["secret"] = servicePasswordSecret + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}})) + DeferCleanup(keystone.DeleteKeystoneAPI, + keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace), + ) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(cinderTest.Instance.Namespace, MemcachedInstance, memcachedv1.MemcachedSpec{})) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + + // Create MariaDB account and database + acc, accSecret := mariadb.CreateMariaDBAccountAndSecret(cinderTest.Database, mariadbv1.MariaDBAccountSpec{}) + DeferCleanup(k8sClient.Delete, ctx, acc) + DeferCleanup(k8sClient.Delete, ctx, accSecret) + mariadb.CreateMariaDBDatabase(cinderTest.Database.Namespace, cinderTest.Database.Name, mariadbv1.MariaDBDatabaseSpec{}) + DeferCleanup(k8sClient.Delete, ctx, mariadb.GetMariaDBDatabase(cinderTest.Database)) + + acName = fmt.Sprintf("ac-%s", cinder.ServiceName) + acSecretName = acName + "-secret" + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cinderTest.Instance.Namespace, + Name: acSecretName, + }, + Data: map[string][]byte{ + "AC_ID": []byte("test-ac-id"), + "AC_SECRET": []byte("test-ac-secret"), + }, + } + DeferCleanup(k8sClient.Delete, ctx, secret) + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + ac := &keystonev1.KeystoneApplicationCredential{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cinderTest.Instance.Namespace, + Name: acName, + }, + Spec: keystonev1.KeystoneApplicationCredentialSpec{ + UserName: cinder.ServiceName, + Secret: servicePasswordSecret, + PasswordSelector: passwordSelector, + Roles: []string{"admin", "member"}, + AccessRules: []keystonev1.ACRule{{Service: "identity", Method: "POST", Path: "/auth/tokens"}}, + ExpirationDays: 30, + GracePeriodDays: 5, + }, + } + DeferCleanup(k8sClient.Delete, ctx, ac) + Expect(k8sClient.Create(ctx, ac)).To(Succeed()) + + fetched := &keystonev1.KeystoneApplicationCredential{} + key := types.NamespacedName{Namespace: ac.Namespace, Name: ac.Name} + Expect(k8sClient.Get(ctx, key, fetched)).To(Succeed()) + + fetched.Status.SecretName = acSecretName + now := metav1.Now() + readyCond := condition.Condition{ + Type: condition.ReadyCondition, + Status: corev1.ConditionTrue, + Reason: condition.ReadyReason, + Message: condition.ReadyMessage, + LastTransitionTime: now, + } + fetched.Status.Conditions = condition.Conditions{readyCond} + Expect(k8sClient.Status().Update(ctx, fetched)).To(Succeed()) + + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + + th.SimulateJobSuccess(cinderTest.CinderDBSync) + + keystone.SimulateKeystoneEndpointReady(cinderTest.CinderKeystoneEndpoint) + }) + + It("should render ApplicationCredential auth in 00-global-defaults.conf", func() { + keystone.SimulateKeystoneEndpointReady(cinderTest.CinderKeystoneEndpoint) + + Eventually(func(g Gomega) { + cfgSecret := th.GetSecret(cinderTest.CinderConfigSecret) + g.Expect(cfgSecret).NotTo(BeNil()) + + conf := string(cfgSecret.Data["00-global-defaults.conf"]) + + g.Expect(conf).To(ContainSubstring( + "application_credential_id = test-ac-id"), + ) + g.Expect(conf).To(ContainSubstring( + "application_credential_secret = test-ac-secret"), + ) + }, timeout, interval).Should(Succeed()) + }) + }) })