diff --git a/api/go.mod b/api/go.mod index 7ae5820d..679a2420 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-20251110074936-69e69f698212 diff --git a/controllers/neutronapi_controller.go b/controllers/neutronapi_controller.go index 38d45b30..f7da8a5c 100644 --- a/controllers/neutronapi_controller.go +++ b/controllers/neutronapi_controller.go @@ -245,6 +245,41 @@ var allWatchFields = []string{ topologyField, } +// Application Credential secret watching function +func (r *NeutronAPIReconciler) acSecretFn(_ 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 neutron AC secret by name pattern (ac-neutron-secret) + expectedSecretName := keystonev1.GetACSecretName(neutronapi.ServiceName) + if name == expectedSecretName { + // get all NeutronAPI CRs in this namespace + neutronAPIs := &neutronv1beta1.NeutronAPIList{} + listOpts := []client.ListOption{ + client.InNamespace(ns), + } + if err := r.List(context.Background(), neutronAPIs, listOpts...); err != nil { + return nil + } + + for _, cr := range neutronAPIs.Items { + result = append(result, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: cr.Namespace, + Name: cr.Name, + }, + }) + } + } + return result +} + // SetupWithManager - func (r *NeutronAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { // index passwordSecretField @@ -341,6 +376,8 @@ func (r *NeutronAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.acSecretFn)). Watches(&topologyv1.Topology{}, handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.GenerationChangedPredicate{})). @@ -501,6 +538,11 @@ func (r *NeutronAPIReconciler) reconcileInit( // Create Secrets required as input for the Service and calculate an overall hash of hashes // + // Verify Application Credentials if available + if result, err := keystonev1.VerifyApplicationCredentialsForService(ctx, r.Client, instance.Namespace, neutronapi.ServiceName, &secretVars, 10*time.Second); err != nil || result.RequeueAfter > 0 { + return result, err + } + // // create Secret required for neutronapi and dbsync input. It contains minimal neutron config required // to get the service up, user can add additional files to be added to the service. @@ -1854,6 +1896,18 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( servicePassword := string(ospSecret.Data[instance.Spec.PasswordSelectors.Service]) templateParameters["ServicePassword"] = servicePassword + templateParameters["UseApplicationCredentials"] = false + // Try to get Application Credential for this service + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, neutronapi.ServiceName); err != nil { + h.GetLogger().Error(err, "Failed to get ApplicationCredential for service", "service", neutronapi.ServiceName) + return err + } else if acData != nil { + templateParameters["UseApplicationCredentials"] = true + templateParameters["ACID"] = acData.ID + templateParameters["ACSecret"] = acData.Secret + h.GetLogger().Info("Using ApplicationCredentials auth", "service", neutronapi.ServiceName) + } + // Database databaseAccount := db.GetAccount() dbSecret := db.GetSecret() diff --git a/go.mod b/go.mod index 6c0774b6..ce620e80 100644 --- a/go.mod +++ b/go.mod @@ -117,3 +117,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-20251110074936-69e69f698212 diff --git a/go.sum b/go.sum index 896b060c..3c640d1e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Deydra71/keystone-operator/api v0.0.0-20251110074936-69e69f698212 h1:m/OIYRcJvoKnfhoJCAJDwN54cnu495rgQAngyGPTKvc= +github.com/Deydra71/keystone-operator/api v0.0.0-20251110074936-69e69f698212/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/neutronapi/config/01-neutron.conf b/templates/neutronapi/config/01-neutron.conf index ea7e0754..a69c308b 100644 --- a/templates/neutronapi/config/01-neutron.conf +++ b/templates/neutronapi/config/01-neutron.conf @@ -58,12 +58,18 @@ auth_url = {{ .KeystoneInternalURL }} memcached_servers={{ .MemcachedServers }} memcache_pool_dead_retry = 10 memcache_pool_conn_get_timeout = 2 +{{ if .UseApplicationCredentials -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else -}} auth_type = password +username = {{ .ServiceUser }} +password = {{ .ServicePassword }} +{{- end }} project_domain_name = Default user_domain_name = Default project_name = service -username = {{ .ServiceUser }} -password = {{ .ServicePassword }} interface = internal {{if (index . "MemcachedAuthCert")}} memcache_tls_certfile = {{ .MemcachedAuthCert }} @@ -74,25 +80,37 @@ memcache_tls_enabled = true [nova] auth_url = {{ .KeystoneInternalURL }} +{{ if .UseApplicationCredentials -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else -}} auth_type = password +username = {{ .ServiceUser }} +password = {{ .ServicePassword }} +{{- end }} project_domain_name = Default user_domain_name = Default region_name = regionOne project_name = service -username = {{ .ServiceUser }} endpoint_type = internal -password = {{ .ServicePassword }} [placement] auth_url = {{ .KeystoneInternalURL }} +{{ if .UseApplicationCredentials -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else -}} auth_type = password +username = {{ .ServiceUser }} +password = {{ .ServicePassword }} +{{- end }} project_domain_name = Default user_domain_name = Default region_name = regionOne project_name = service -username = {{ .ServiceUser }} endpoint_type = internal -password = {{ .ServicePassword }} [oslo_concurrency] lock_path = /var/lib/neutron/tmp diff --git a/test/functional/neutronapi_controller_test.go b/test/functional/neutronapi_controller_test.go index 57387cb8..c75f9679 100644 --- a/test/functional/neutronapi_controller_test.go +++ b/test/functional/neutronapi_controller_test.go @@ -1957,6 +1957,60 @@ func getNeutronAPIControllerSuite(ml2MechanismDrivers []string) func() { }, ).Spec.Template.Spec.Containers[0].Env, "CONFIG_HASH", "") }) + + When("an ApplicationCredential is created for Neutron", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateNeutronAPISecret(namespace, SecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + infra.SimulateMemcachedReady(memcachedName) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetNeutronAPI(neutronAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + SimulateTransportURLReady(apiTransportURLName) + mariadb.SimulateMariaDBAccountCompleted(types.NamespacedName{Namespace: namespace, Name: GetNeutronAPI(neutronAPIName).Spec.DatabaseAccount}) + mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: neutronapi.DatabaseCRName}) + + if isOVNEnabled { + DeferCleanup(DeleteOVNDBClusters, CreateOVNDBClusters(namespace)) + } + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(namespace)) + + // Create AC secret + acSecretName := fmt.Sprintf("ac-%s-secret", neutronapi.ServiceName) + acSecret := th.CreateSecret( + types.NamespacedName{Namespace: neutronAPIName.Namespace, Name: acSecretName}, + map[string][]byte{ + "AC_ID": []byte("test-ac-id"), + "AC_SECRET": []byte("test-ac-secret"), + }, + ) + DeferCleanup(th.DeleteInstance, acSecret) + }) + + It("should render ApplicationCredential auth in 01-neutron.conf", func() { + configSecretName := types.NamespacedName{ + Namespace: neutronAPIName.Namespace, + Name: fmt.Sprintf("%s-config", neutronAPIName.Name), + } + + Eventually(func(g Gomega) { + configSecret := th.GetSecret(configSecretName) + g.Expect(configSecret.Data).ShouldNot(BeNil()) + + conf := string(configSecret.Data["01-neutron.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()) + }) + }) } }