diff --git a/api/go.mod b/api/go.mod index 8e6d4702..876e0c99 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,9 +5,9 @@ go 1.24.4 require ( github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251103072528-9eb684fef4ef - k8s.io/api v0.31.13 - k8s.io/apimachinery v0.31.13 - k8s.io/client-go v0.31.13 // indirect + k8s.io/api v0.31.14 + k8s.io/apimachinery v0.31.14 + k8s.io/client-go v0.31.14 // indirect sigs.k8s.io/controller-runtime v0.19.7 ) @@ -48,6 +48,7 @@ require ( github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect @@ -92,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-20251201081347-2823ce61c025 diff --git a/api/go.sum b/api/go.sum index fd5ccf21..eaf9c999 100644 --- a/api/go.sum +++ b/api/go.sum @@ -113,8 +113,7 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/go.mod b/go.mod index 3222ed95..8010bbfb 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,9 @@ require ( github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251106091552-44d4046a8d6c gopkg.in/yaml.v2 v2.4.0 - 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 ) @@ -88,7 +88,7 @@ require ( go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.42.0 // indirect @@ -149,3 +149,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-20251201081347-2823ce61c025 diff --git a/go.sum b/go.sum index 0ea6b1f1..b027ddf4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Deydra71/keystone-operator/api v0.0.0-20251201081347-2823ce61c025 h1:9tVdplONUg5JpgAczs7wV4Swkc0mqAueWVT58cin9ZY= +github.com/Deydra71/keystone-operator/api v0.0.0-20251201081347-2823ce61c025/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= @@ -132,8 +134,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.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.20251103072528-9eb684fef4ef h1:Ql4G7sRHpqWFGwXypN7MorDGUWv4jz5n34ayzVt3R9E= @@ -210,8 +210,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/internal/controller/designateapi_controller.go b/internal/controller/designateapi_controller.go index 5f3dc085..9a9a12e1 100644 --- a/internal/controller/designateapi_controller.go +++ b/internal/controller/designateapi_controller.go @@ -372,6 +372,40 @@ func (r *DesignateAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl. return nil } + // 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 designate AC secret by name pattern (ac-designate-secret) + expectedSecretName := keystonev1.GetACSecretName("designate") + if name == expectedSecretName { + // get all DesignateAPI CRs in this namespace + designateAPIs := &designatev1beta1.DesignateAPIList{} + listOpts := []client.ListOption{ + client.InNamespace(ns), + } + if err := r.List(context.Background(), designateAPIs, listOpts...); err != nil { + return nil + } + + for _, cr := range designateAPIs.Items { + name := client.ObjectKey{ + Namespace: ns, + Name: cr.Name, + } + result = append(result, reconcile.Request{NamespacedName: name}) + } + } + return result + } + return ctrl.NewControllerManagedBy(mgr). For(&designatev1beta1.DesignateAPI{}). Owns(&keystonev1.KeystoneService{}). @@ -385,6 +419,8 @@ func (r *DesignateAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl. handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(acSecretFn)). Watches(&networkv1.NetworkAttachmentDefinition{}, handler.EnqueueRequestsFromMapFunc(nadFn)). Watches(&topologyv1.Topology{}, @@ -741,6 +777,19 @@ func (r *DesignateAPIReconciler) reconcileNormal(ctx context.Context, instance * // run check OpenStack secret - end + // Verify Application Credentials if available + ctrlResult, err = keystonev1.VerifyApplicationCredentialsForService( + ctx, + r.Client, + instance.Namespace, + designate.ServiceName, + &configMapVars, + 10*time.Second, + ) + if (err != nil || ctrlResult != ctrl.Result{}) { + return ctrlResult, err + } + // // TLS input validation // @@ -1198,6 +1247,18 @@ func (r *DesignateAPIReconciler) generateServiceConfigMaps( } templateParameters["AdminPassword"] = string(adminPasswordSecret.Data["DesignatePassword"]) + templateParameters["UseApplicationCredentials"] = false + // Try to get Application Credential for this service (via keystone api helper) + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, designate.ServiceName); err != nil { + Log.Error(err, "Failed to get ApplicationCredential for service", "service", designate.ServiceName) + return err + } else if acData != nil { + templateParameters["UseApplicationCredentials"] = true + templateParameters["ACID"] = acData.ID + templateParameters["ACSecret"] = acData.Secret + Log.Info("Using ApplicationCredentials auth", "service", designate.ServiceName) + } + cms := []util.Template{ // Custom ConfigMap { diff --git a/templates/designateapi/config/designate.conf b/templates/designateapi/config/designate.conf index 04c12e56..b17fd23b 100644 --- a/templates/designateapi/config/designate.conf +++ b/templates/designateapi/config/designate.conf @@ -7,12 +7,17 @@ enable_host_header=True enabled_extensions_admin=quotas [keystone_authtoken] +auth_type={{ if .UseApplicationCredentials }}v3applicationcredential{{ else }}password{{ end }} +{{ if .UseApplicationCredentials -}} +application_credential_id={{ .ACID }} +application_credential_secret={{ .ACSecret }} +{{- else -}} username={{ .ServiceUser }} +password={{ .AdminPassword }} +user_domain_name=Default project_name=service project_domain_name=Default -user_domain_name=Default -auth_type=password -password={{ .AdminPassword }} +{{- end }} region_name=regionOne www_authenticate_uri={{ .KeystonePublicURL }} auth_url={{ .KeystoneInternalURL }} diff --git a/test/functional/designateapi_controller_test.go b/test/functional/designateapi_controller_test.go index 69b6c881..117e0a82 100644 --- a/test/functional/designateapi_controller_test.go +++ b/test/functional/designateapi_controller_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports . "github.com/onsi/gomega" //revive:disable:dot-imports + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" corev1 "k8s.io/api/core/v1" @@ -29,6 +30,7 @@ import ( "github.com/openstack-k8s-operators/lib-common/modules/common/condition" //revive:disable-next-line:dot-imports "github.com/openstack-k8s-operators/designate-operator/internal/designate" + keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" @@ -235,6 +237,116 @@ var _ = Describe("DesignateAPI controller", func() { }) }) + When("An ApplicationCredential is created for Designate", func() { + var ( + acName string + acSecretName string + servicePasswordSecret string + passwordSelector string + keystoneAPIName types.NamespacedName + ) + BeforeEach(func() { + servicePasswordSecret = SecretName + passwordSelector = "DesignatePassword" + + keystoneAPIName = keystone.CreateKeystoneAPI(namespace) + DeferCleanup(keystone.DeleteKeystoneAPI, keystoneAPIName) + keystoneInternalEndpoint := fmt.Sprintf("http://keystone-for-%s-internal", designateAPIName.Name) + keystonePublicEndpoint := fmt.Sprintf("http://keystone-for-%s-public", designateAPIName.Name) + SimulateKeystoneReady(keystoneAPIName, keystonePublicEndpoint, keystoneInternalEndpoint) + + DeferCleanup(k8sClient.Delete, ctx, CreateDesignateSecret(namespace)) + DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName)) + + spec := GetDefaultDesignateAPISpec() + spec["secret"] = servicePasswordSecret + spec["transportURLSecret"] = RabbitmqSecretName + DeferCleanup(th.DeleteInstance, + CreateDesignateAPI(designateAPIName, spec)) + + mariaDBDatabaseName := mariadb.CreateMariaDBDatabase(namespace, designate.DatabaseCRName, mariadbv1.MariaDBDatabaseSpec{}) + mariaDBDatabase := mariadb.GetMariaDBDatabase(mariaDBDatabaseName) + DeferCleanup(k8sClient.Delete, ctx, mariaDBDatabase) + + designateAPI := GetDesignateAPI(designateAPIName) + apiMariaDBAccount, apiMariaDBSecret := mariadb.CreateMariaDBAccountAndSecret( + types.NamespacedName{ + Namespace: namespace, + Name: designateAPI.Spec.DatabaseAccount, + }, mariadbv1.MariaDBAccountSpec{}) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBAccount) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBSecret) + + acName = fmt.Sprintf("ac-%s", designate.ServiceName) + acSecretName = acName + "-secret" + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: acSecretName, + Namespace: namespace, + }, + 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: namespace, + Name: acName, + }, + Spec: keystonev1.KeystoneApplicationCredentialSpec{ + UserName: designate.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()) + }) + + It("should render ApplicationCredential auth in designate.conf", func() { + Eventually(func(g Gomega) { + cfgSecret := th.GetSecret(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-config-data", designateAPIName.Name), + }) + g.Expect(cfgSecret).NotTo(BeNil()) + + conf := string(cfgSecret.Data["designate.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()) + }) + }) + // NAD // Networks Annotation