Skip to content
Open
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
2 changes: 2 additions & 0 deletions api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 54 additions & 0 deletions controllers/neutronapi_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{})).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down Expand Up @@ -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=
Expand Down
30 changes: 24 additions & 6 deletions templates/neutronapi/config/01-neutron.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand Down
54 changes: 54 additions & 0 deletions test/functional/neutronapi_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
})
}
}

Expand Down