From b7fa7eee0bf6978bea1317dcaaf69c5d23b36085 Mon Sep 17 00:00:00 2001 From: Veronika Fisarova Date: Mon, 15 Dec 2025 15:01:37 +0100 Subject: [PATCH] Application Credential Support Adds the end-to-end support for consuming Keystone ApplicationCredentials (AC) in the watcher-operator, enabling WatcherAPI, WatcherApplier, and WatcherDecisionEngine pods to use AC-based authentication when available. Signed-off-by: Veronika Fisarova --- api/go.mod | 14 +- api/go.sum | 16 +- go.mod | 20 +-- go.sum | 28 ++-- internal/controller/watcherapi_controller.go | 47 ++++++ .../controller/watcherapplier_controller.go | 46 ++++++ .../watcherdecisionengine_controller.go | 46 ++++++ templates/00-default.conf | 16 +- test/functional/watcherapi_controller_test.go | 144 ++++++++++++++++++ .../watcherapplier_controller_test.go | 132 ++++++++++++++++ .../watcherdecisionengine_controller_test.go | 141 +++++++++++++++++ 11 files changed, 611 insertions(+), 39 deletions(-) diff --git a/api/go.mod b/api/go.mod index c4033656..2697190d 100644 --- a/api/go.mod +++ b/api/go.mod @@ -7,8 +7,8 @@ toolchain go1.24.6 require ( github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251110170511-c2d4a351a7c3 github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef - k8s.io/api v0.31.13 - k8s.io/apimachinery v0.31.13 + k8s.io/api v0.31.14 + k8s.io/apimachinery v0.31.14 k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d sigs.k8s.io/controller-runtime v0.19.7 ) @@ -54,16 +54,16 @@ require ( golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.33.2 // indirect - k8s.io/client-go v0.31.13 // indirect + k8s.io/client-go v0.31.14 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect @@ -93,3 +93,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-20251211085602-3e1a3e022c81 diff --git a/api/go.sum b/api/go.sum index 8c5ac0ee..bcb7f861 100644 --- a/api/go.sum +++ b/api/go.sum @@ -137,19 +137,19 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/go.mod b/go.mod index aa8f3b3c..6eb287d7 100644 --- a/go.mod +++ b/go.mod @@ -16,9 +16,9 @@ require ( github.com/openstack-k8s-operators/watcher-operator/api v0.0.0-00010101000000-000000000000 go.uber.org/zap v1.27.1 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.31.13 - k8s.io/apimachinery v0.31.13 - k8s.io/client-go v0.31.13 + k8s.io/api v0.31.14 + k8s.io/apimachinery v0.31.14 + k8s.io/client-go v0.31.14 k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d sigs.k8s.io/controller-runtime v0.19.7 ) @@ -62,8 +62,8 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251021145236-2b84ec9fd9bb // indirect - github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077 // indirect + github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251103072528-9eb684fef4ef // indirect + github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251103072528-9eb684fef4ef // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -90,10 +90,10 @@ require ( golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.36.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect @@ -142,3 +142,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-20251211085602-3e1a3e022c81 diff --git a/go.sum b/go.sum index 06805af9..ccaed6c0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Deydra71/keystone-operator/api v0.0.0-20251211085602-3e1a3e022c81 h1:plax+NFgJJL1SrERyXAnf3jOHRhLTtBlJ2oc7d84EoU= +github.com/Deydra71/keystone-operator/api v0.0.0-20251211085602-3e1a3e022c81/go.mod h1:b98Jl8eyUw8V07l9YiuQnoMlnWC748oV8IhXH15NCC4= 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/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= @@ -120,14 +122,12 @@ 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.20251110170511-c2d4a351a7c3 h1:gKazSLpq0Ytn4OLzNtSKQpLswAdki8u8mXZgpJy83bE= github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251110170511-c2d4a351a7c3/go.mod h1:Y9LqOS1wYhn7RT4jFknINdWa+ziYEIOU1jLNxkxiCsw= -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.20251103072528-9eb684fef4ef h1:1j7kk+D4ZdIXm6C/IwEjuTzIuvWUytxO39E/x94JY7k= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:kUT/SyuxZiOcX8ZuvpFN3PaQa2V8uQon8YwY+1RoQWM= -github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251021145236-2b84ec9fd9bb h1:wToXqX7AS1JV3Kna7RcJfkRart8rSGun2biKNfyY6Zg= -github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251021145236-2b84ec9fd9bb/go.mod h1:yf13jWb60XV26eA7A8o86ZCXNWBLNK9dPkTSWFaTPCw= -github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077 h1:9tpPDBV2RLXMDgt13ec8XR2OatFriItseqg+Oyvx9GA= -github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077/go.mod h1:JPQHkExlxeT6MU3DNJgXXJJG0NMQHlZwxxfbYRaP3eg= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251103072528-9eb684fef4ef h1:Ql4G7sRHpqWFGwXypN7MorDGUWv4jz5n34ayzVt3R9E= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:yf13jWb60XV26eA7A8o86ZCXNWBLNK9dPkTSWFaTPCw= +github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251103072528-9eb684fef4ef h1:VMwP0988m1VCjpVn+MxHt7i3B0OuBhQnM5akKt4taVA= +github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:jl+SNs7K7XBx5jVbUJwWV0NRDfM8LyeV4AsGAroP8XA= github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef h1:U9cgXJs/GuO6/0bRn6oaS7ovDrabyGPZpmZyAWksUuQ= github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:lgYyrXEYA2BPsq4Kg6dqa+QsHgOjMPyOsEYrvyYW3jk= github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251110170510-e669472c745c h1:Fx2ZD3jNoqprCJSEkvU59vnkNNluLH1QUstpZN6M6JU= @@ -224,19 +224,19 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/controller/watcherapi_controller.go b/internal/controller/watcherapi_controller.go index f2e38822..d1aa13c0 100644 --- a/internal/controller/watcherapi_controller.go +++ b/internal/controller/watcherapi_controller.go @@ -472,6 +472,18 @@ func (r *WatcherAPIReconciler) generateServiceConfigs( if string(secret.Data[NotificationURLSelector]) != "" { templateParameters["NotificationURL"] = string(secret.Data[NotificationURLSelector]) } + + // Check for Application Credentials + templateParameters["UseApplicationCredentials"] = false + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, watcher.ServiceName); err != nil { + Log.Error(err, "Failed to get ApplicationCredential for service", "service", watcher.ServiceName) + } else if acData != nil { + templateParameters["UseApplicationCredentials"] = true + templateParameters["ACID"] = acData.ID + templateParameters["ACSecret"] = acData.Secret + Log.Info("Using ApplicationCredentials auth", "service", watcher.ServiceName) + } + // MTLS if memcachedInstance.GetMemcachedMTLSSecret() != "" { templateParameters["MemcachedAuthCert"] = fmt.Sprint(memcachedv1.CertMountPath()) @@ -929,6 +941,36 @@ func (r *WatcherAPIReconciler) SetupWithManager(mgr ctrl.Manager) error { return err } + // Application Credential secret watching function + acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request { + result := []reconcile.Request{} + + // Check if this is a watcher AC secret by name pattern (ac-watcher-secret) + expectedSecretName := keystonev1.GetACSecretName(watcher.ServiceName) + if o.GetName() == expectedSecretName { + // get all WatcherAPI CRs in this namespace + watcherAPIs := &watcherv1beta1.WatcherAPIList{} + listOpts := []client.ListOption{ + client.InNamespace(o.GetNamespace()), + } + if err := r.Client.List(context.Background(), watcherAPIs, listOpts...); err != nil { + return nil + } + + // Enqueue reconcile for all WatcherAPI instances + for _, cr := range watcherAPIs.Items { + result = append(result, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: o.GetNamespace(), + Name: cr.Name, + }, + }) + } + } + + return result + } + return ctrl.NewControllerManagedBy(mgr). For(&watcherv1beta1.WatcherAPI{}). Owns(&corev1.Secret{}). @@ -940,6 +982,11 @@ func (r *WatcherAPIReconciler) SetupWithManager(mgr ctrl.Manager) error { handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(acSecretFn), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). Watches( &memcachedv1.Memcached{}, handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), diff --git a/internal/controller/watcherapplier_controller.go b/internal/controller/watcherapplier_controller.go index aece0d3b..16b439e7 100644 --- a/internal/controller/watcherapplier_controller.go +++ b/internal/controller/watcherapplier_controller.go @@ -435,6 +435,17 @@ func (r *WatcherApplierReconciler) generateServiceConfigs( templateParameters["NotificationURL"] = string(secret.Data[NotificationURLSelector]) } + // Check for Application Credentials + templateParameters["UseApplicationCredentials"] = false + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, watcher.ServiceName); err != nil { + Log.Error(err, "Failed to get ApplicationCredential for service", "service", watcher.ServiceName) + } else if acData != nil { + templateParameters["UseApplicationCredentials"] = true + templateParameters["ACID"] = acData.ID + templateParameters["ACSecret"] = acData.Secret + Log.Info("Using ApplicationCredentials auth", "service", watcher.ServiceName) + } + // MTLS if memcachedInstance.GetMemcachedMTLSSecret() != "" { templateParameters["MemcachedAuthCert"] = fmt.Sprint(memcachedv1.CertMountPath()) @@ -506,6 +517,36 @@ func (r *WatcherApplierReconciler) SetupWithManager(mgr ctrl.Manager) error { return err } + // Application Credential secret watching function + acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request { + result := []reconcile.Request{} + + // Check if this is a watcher AC secret by name pattern (ac-watcher-secret) + expectedSecretName := keystonev1.GetACSecretName(watcher.ServiceName) + if o.GetName() == expectedSecretName { + // get all WatcherApplier CRs in this namespace + watcherAppliers := &watcherv1beta1.WatcherApplierList{} + listOpts := []client.ListOption{ + client.InNamespace(o.GetNamespace()), + } + if err := r.Client.List(context.Background(), watcherAppliers, listOpts...); err != nil { + return nil + } + + // Enqueue reconcile for all WatcherApplier instances + for _, cr := range watcherAppliers.Items { + result = append(result, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: o.GetNamespace(), + Name: cr.Name, + }, + }) + } + } + + return result + } + return ctrl.NewControllerManagedBy(mgr). For(&watcherv1beta1.WatcherApplier{}). Owns(&corev1.Secret{}). @@ -515,6 +556,11 @@ func (r *WatcherApplierReconciler) SetupWithManager(mgr ctrl.Manager) error { handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(acSecretFn), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). Watches( &memcachedv1.Memcached{}, handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), diff --git a/internal/controller/watcherdecisionengine_controller.go b/internal/controller/watcherdecisionengine_controller.go index 9bb31712..07d6a26e 100644 --- a/internal/controller/watcherdecisionengine_controller.go +++ b/internal/controller/watcherdecisionengine_controller.go @@ -404,6 +404,36 @@ func (r *WatcherDecisionEngineReconciler) SetupWithManager(mgr ctrl.Manager) err return err } + // Application Credential secret watching function + acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request { + result := []reconcile.Request{} + + // Check if this is a watcher AC secret by name pattern (ac-watcher-secret) + expectedSecretName := keystonev1.GetACSecretName(watcher.ServiceName) + if o.GetName() == expectedSecretName { + // get all WatcherDecisionEngine CRs in this namespace + watcherDEs := &watcherv1beta1.WatcherDecisionEngineList{} + listOpts := []client.ListOption{ + client.InNamespace(o.GetNamespace()), + } + if err := r.Client.List(context.Background(), watcherDEs, listOpts...); err != nil { + return nil + } + + // Enqueue reconcile for all WatcherDecisionEngine instances + for _, cr := range watcherDEs.Items { + result = append(result, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: o.GetNamespace(), + Name: cr.Name, + }, + }) + } + } + + return result + } + return ctrl.NewControllerManagedBy(mgr). For(&watcherv1beta1.WatcherDecisionEngine{}). Owns(&corev1.Secret{}). @@ -413,6 +443,11 @@ func (r *WatcherDecisionEngineReconciler) SetupWithManager(mgr ctrl.Manager) err handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(acSecretFn), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). Watches( &memcachedv1.Memcached{}, handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), @@ -586,6 +621,17 @@ func (r *WatcherDecisionEngineReconciler) generateServiceConfigs( templateParameters["NotificationURL"] = string(secret.Data[NotificationURLSelector]) } + // Check for Application Credentials + templateParameters["UseApplicationCredentials"] = false + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, watcher.ServiceName); err != nil { + Log.Error(err, "Failed to get ApplicationCredential for service", "service", watcher.ServiceName) + } else if acData != nil { + templateParameters["UseApplicationCredentials"] = true + templateParameters["ACID"] = acData.ID + templateParameters["ACSecret"] = acData.Secret + Log.Info("Using ApplicationCredentials auth", "service", watcher.ServiceName) + } + // MTLS if memcachedInstance.GetMemcachedMTLSSecret() != "" { templateParameters["MemcachedAuthCert"] = fmt.Sprint(memcachedv1.CertMountPath()) diff --git a/templates/00-default.conf b/templates/00-default.conf index f4d969fa..0d72fb16 100644 --- a/templates/00-default.conf +++ b/templates/00-default.conf @@ -48,14 +48,20 @@ memcache_tls_keyfile = {{ .MemcachedAuthKey }} memcache_tls_cafile = {{ .MemcachedAuthCa }} memcache_tls_enabled = true {{ end }} +{{ if .UseApplicationCredentials }} +auth_type = v3applicationcredential +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else }} project_domain_name = Default project_name = service user_domain_name = Default password = {{ .ServicePassword }} username = {{ .ServiceUser }} +auth_type = password +{{ end }} auth_url = {{ .KeystoneAuthURL }} interface = internal -auth_type = password {{ if .CaFilePath }} cafile = {{ .CaFilePath }} {{ end }} @@ -63,14 +69,20 @@ cafile = {{ .CaFilePath }} {{ if (index . "KeystoneAuthURL") }} [watcher_clients_auth] +{{ if .UseApplicationCredentials }} +auth_type = v3applicationcredential +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else }} project_domain_name = Default project_name = service user_domain_name = Default password = {{ .ServicePassword }} username = {{ .ServiceUser }} +auth_type = password +{{ end }} auth_url = {{ .KeystoneAuthURL }} interface = internal -auth_type = password {{ if .CaFilePath }} cafile = {{ .CaFilePath }} {{ end }} diff --git a/test/functional/watcherapi_controller_test.go b/test/functional/watcherapi_controller_test.go index 3baabdb3..b1f3ed85 100644 --- a/test/functional/watcherapi_controller_test.go +++ b/test/functional/watcherapi_controller_test.go @@ -9,11 +9,14 @@ import ( //revive:disable-next-line:dot-imports 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" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" watcherv1beta1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" + "github.com/openstack-k8s-operators/watcher-operator/internal/watcher" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" ) @@ -1582,4 +1585,145 @@ heartbeat_in_pthread=false`, }, timeout, interval).Should(Succeed()) }) }) + + When("An ApplicationCredential is created for Watcher", func() { + var ( + acName string + acSecretName string + servicePasswordSecret string + passwordSelector string + ) + BeforeEach(func() { + servicePasswordSecret = SecretName + passwordSelector = "WatcherPassword" + + secret := CreateInternalTopLevelSecretNotification() + DeferCleanup(k8sClient.Delete, ctx, secret) + + prometheusSecret := th.CreateSecret( + watcherTest.PrometheusSecretName, + map[string][]byte{ + "host": []byte("prometheus.example.com"), + "port": []byte("9090"), + }, + ) + DeferCleanup(k8sClient.Delete, ctx, prometheusSecret) + + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + watcherTest.WatcherAPI.Namespace, + "openstack", + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + + mariadb.CreateMariaDBAccountAndSecret( + watcherTest.WatcherDatabaseAccount, + mariadbv1.MariaDBAccountSpec{ + UserName: "watcher", + }, + ) + mariadb.CreateMariaDBDatabase( + watcherTest.WatcherAPI.Namespace, + "watcher", + mariadbv1.MariaDBDatabaseSpec{ + Name: "watcher", + }, + ) + mariadb.SimulateMariaDBAccountCompleted(watcherTest.WatcherDatabaseAccount) + mariadb.SimulateMariaDBDatabaseCompleted(watcherTest.WatcherDatabaseName) + + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(watcherTest.WatcherAPI.Namespace)) + + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(1)), + }, + } + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(watcherTest.WatcherAPI.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(watcherTest.MemcachedNamespace) + + // Create AC secret with test credentials + acName = fmt.Sprintf("ac-%s", watcher.ServiceName) + acSecretName = acName + "-secret" + acSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: watcherTest.WatcherAPI.Namespace, + Name: acSecretName, + }, + Data: map[string][]byte{ + "AC_ID": []byte("test-ac-id"), + "AC_SECRET": []byte("test-ac-secret"), + }, + } + DeferCleanup(k8sClient.Delete, ctx, acSecret) + Expect(k8sClient.Create(ctx, acSecret)).To(Succeed()) + + // Create AC CR + ac := &keystonev1.KeystoneApplicationCredential{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: watcherTest.WatcherAPI.Namespace, + Name: acName, + }, + Spec: keystonev1.KeystoneApplicationCredentialSpec{ + UserName: watcher.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()) + + // Simulate AC controller updating the status + 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()) + + // Create WatcherAPI + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) + th.SimulateStatefulSetReplicaReady(watcherTest.WatcherAPIStatefulSet) + keystone.SimulateKeystoneEndpointReady(watcherTest.WatcherKeystoneEndpointName) + }) + + It("should render ApplicationCredential auth in 00-default.conf", func() { + keystone.SimulateKeystoneEndpointReady(watcherTest.WatcherKeystoneEndpointName) + + // Wait for the config to be generated and updated with AC auth + Eventually(func(g Gomega) { + cfgSecret := th.GetSecret(watcherTest.WatcherAPIConfigSecret) + g.Expect(cfgSecret).NotTo(BeNil()) + + conf := string(cfgSecret.Data["00-default.conf"]) + + // AC auth is configured + g.Expect(conf).To(ContainSubstring("auth_type = v3applicationcredential")) + g.Expect(conf).To(ContainSubstring("application_credential_id = test-ac-id")) + g.Expect(conf).To(ContainSubstring("application_credential_secret = test-ac-secret")) + + // Password auth fields should not be present + g.Expect(conf).NotTo(ContainSubstring("auth_type = password")) + g.Expect(conf).NotTo(ContainSubstring("username = watcher")) + g.Expect(conf).NotTo(ContainSubstring("project_name = service")) + }, timeout, interval).Should(Succeed()) + }) + }) }) diff --git a/test/functional/watcherapplier_controller_test.go b/test/functional/watcherapplier_controller_test.go index 0e6954b8..2a6314e5 100644 --- a/test/functional/watcherapplier_controller_test.go +++ b/test/functional/watcherapplier_controller_test.go @@ -9,11 +9,14 @@ import ( //revive:disable-next-line:dot-imports 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" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" watcherv1beta1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" + "github.com/openstack-k8s-operators/watcher-operator/internal/watcher" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" ) @@ -1127,4 +1130,133 @@ heartbeat_in_pthread=false`, }, timeout, interval).Should(Succeed()) }) }) + + When("An ApplicationCredential is created for Watcher", func() { + var ( + acName string + acSecretName string + servicePasswordSecret string + passwordSelector string + ) + BeforeEach(func() { + servicePasswordSecret = SecretName + passwordSelector = "WatcherPassword" + + secret := CreateInternalTopLevelSecret() + DeferCleanup(k8sClient.Delete, ctx, secret) + + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + watcherTest.WatcherApplier.Namespace, + "openstack", + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + + mariadb.CreateMariaDBAccountAndSecret( + watcherTest.WatcherDatabaseAccount, + mariadbv1.MariaDBAccountSpec{ + UserName: "watcher", + }, + ) + mariadb.CreateMariaDBDatabase( + watcherTest.WatcherApplier.Namespace, + "watcher", + mariadbv1.MariaDBDatabaseSpec{ + Name: "watcher", + }, + ) + mariadb.SimulateMariaDBAccountCompleted(watcherTest.WatcherDatabaseAccount) + mariadb.SimulateMariaDBDatabaseCompleted(watcherTest.WatcherDatabaseName) + + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(watcherTest.WatcherApplier.Namespace)) + + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(1)), + }, + } + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(watcherTest.WatcherApplier.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(watcherTest.MemcachedNamespace) + + // Create AC secret with test credentials + acName = fmt.Sprintf("ac-%s", watcher.ServiceName) + acSecretName = acName + "-secret" + acSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: watcherTest.WatcherApplier.Namespace, + Name: acSecretName, + }, + Data: map[string][]byte{ + "AC_ID": []byte("test-ac-id"), + "AC_SECRET": []byte("test-ac-secret"), + }, + } + DeferCleanup(k8sClient.Delete, ctx, acSecret) + Expect(k8sClient.Create(ctx, acSecret)).To(Succeed()) + + // Create AC CR + ac := &keystonev1.KeystoneApplicationCredential{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: watcherTest.WatcherApplier.Namespace, + Name: acName, + }, + Spec: keystonev1.KeystoneApplicationCredentialSpec{ + UserName: watcher.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()) + + // Simulate AC controller updating the status + 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()) + + // Create WatcherApplier + DeferCleanup(th.DeleteInstance, CreateWatcherApplier(watcherTest.WatcherApplier, GetDefaultWatcherApplierSpec())) + th.SimulateStatefulSetReplicaReady(watcherTest.WatcherApplierStatefulSet) + }) + + It("should render ApplicationCredential auth in 00-default.conf", func() { + // Wait for the config to be generated and updated with AC auth + Eventually(func(g Gomega) { + cfgSecret := th.GetSecret(watcherTest.WatcherApplierConfigSecret) + g.Expect(cfgSecret).NotTo(BeNil()) + + conf := string(cfgSecret.Data["00-default.conf"]) + + // AC auth is configured + g.Expect(conf).To(ContainSubstring("auth_type = v3applicationcredential")) + g.Expect(conf).To(ContainSubstring("application_credential_id = test-ac-id")) + g.Expect(conf).To(ContainSubstring("application_credential_secret = test-ac-secret")) + + // Password auth fields should not be present + g.Expect(conf).NotTo(ContainSubstring("auth_type = password")) + g.Expect(conf).NotTo(ContainSubstring("username = watcher")) + g.Expect(conf).NotTo(ContainSubstring("project_name = service")) + }, timeout, interval).Should(Succeed()) + }) + }) }) diff --git a/test/functional/watcherdecisionengine_controller_test.go b/test/functional/watcherdecisionengine_controller_test.go index 620aa47d..32750761 100644 --- a/test/functional/watcherdecisionengine_controller_test.go +++ b/test/functional/watcherdecisionengine_controller_test.go @@ -9,11 +9,14 @@ import ( //revive:disable-next-line:dot-imports 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" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" watcherv1beta1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" + "github.com/openstack-k8s-operators/watcher-operator/internal/watcher" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" ) @@ -1253,4 +1256,142 @@ heartbeat_in_pthread=false`, }, timeout, interval).Should(Succeed()) }) }) + + When("An ApplicationCredential is created for Watcher", func() { + var ( + acName string + acSecretName string + servicePasswordSecret string + passwordSelector string + ) + BeforeEach(func() { + servicePasswordSecret = SecretName + passwordSelector = "WatcherPassword" + + secret := CreateInternalTopLevelSecret() + DeferCleanup(k8sClient.Delete, ctx, secret) + + prometheusSecret := th.CreateSecret( + watcherTest.PrometheusSecretName, + map[string][]byte{ + "host": []byte("prometheus.example.com"), + "port": []byte("9090"), + }, + ) + DeferCleanup(k8sClient.Delete, ctx, prometheusSecret) + + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + watcherTest.WatcherDecisionEngine.Namespace, + "openstack", + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + + mariadb.CreateMariaDBAccountAndSecret( + watcherTest.WatcherDatabaseAccount, + mariadbv1.MariaDBAccountSpec{ + UserName: "watcher", + }, + ) + mariadb.CreateMariaDBDatabase( + watcherTest.WatcherDecisionEngine.Namespace, + "watcher", + mariadbv1.MariaDBDatabaseSpec{ + Name: "watcher", + }, + ) + mariadb.SimulateMariaDBAccountCompleted(watcherTest.WatcherDatabaseAccount) + mariadb.SimulateMariaDBDatabaseCompleted(watcherTest.WatcherDatabaseName) + + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(watcherTest.WatcherDecisionEngine.Namespace)) + + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(1)), + }, + } + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(watcherTest.WatcherDecisionEngine.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(watcherTest.MemcachedNamespace) + + // Create AC secret with test credentials + acName = fmt.Sprintf("ac-%s", watcher.ServiceName) + acSecretName = acName + "-secret" + acSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: watcherTest.WatcherDecisionEngine.Namespace, + Name: acSecretName, + }, + Data: map[string][]byte{ + "AC_ID": []byte("test-ac-id"), + "AC_SECRET": []byte("test-ac-secret"), + }, + } + DeferCleanup(k8sClient.Delete, ctx, acSecret) + Expect(k8sClient.Create(ctx, acSecret)).To(Succeed()) + + // Create AC CR + ac := &keystonev1.KeystoneApplicationCredential{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: watcherTest.WatcherDecisionEngine.Namespace, + Name: acName, + }, + Spec: keystonev1.KeystoneApplicationCredentialSpec{ + UserName: watcher.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()) + + // Simulate AC controller updating the status + 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()) + + // Create WatcherDecisionEngine + DeferCleanup(th.DeleteInstance, CreateWatcherDecisionEngine(watcherTest.WatcherDecisionEngine, GetDefaultWatcherDecisionEngineSpec())) + th.SimulateStatefulSetReplicaReady(watcherTest.WatcherDecisionEngineStatefulSet) + }) + + It("should render ApplicationCredential auth in 00-default.conf", func() { + // Wait for the config to be generated and updated with AC auth + Eventually(func(g Gomega) { + cfgSecret := th.GetSecret(watcherTest.WatcherDecisionEngineConfigSecret) + g.Expect(cfgSecret).NotTo(BeNil()) + + conf := string(cfgSecret.Data["00-default.conf"]) + + // AC auth is configured + g.Expect(conf).To(ContainSubstring("auth_type = v3applicationcredential")) + g.Expect(conf).To(ContainSubstring("application_credential_id = test-ac-id")) + g.Expect(conf).To(ContainSubstring("application_credential_secret = test-ac-secret")) + + // Password auth fields should not be present + g.Expect(conf).NotTo(ContainSubstring("auth_type = password")) + g.Expect(conf).NotTo(ContainSubstring("username = watcher")) + g.Expect(conf).NotTo(ContainSubstring("project_name = service")) + }, timeout, interval).Should(Succeed()) + }) + }) })