diff --git a/internal/resources/frontproxy/certificates.go b/internal/resources/frontproxy/certificates.go index 91af0eb7..3819d6d3 100644 --- a/internal/resources/frontproxy/certificates.go +++ b/internal/resources/frontproxy/certificates.go @@ -72,29 +72,7 @@ func (r *reconciler) serverCertificateReconciler() reconciling.NamedCertificateR name := r.certName(certKind) template := r.certTemplateMap().CertificateTemplate(certKind) - fpService := r.serviceName() - - dnsNames := []string{ - fpService, - fmt.Sprintf("%s.%s", fpService, r.rootShard.Namespace), - fmt.Sprintf("%s.%s.svc.cluster.local", fpService, r.rootShard.Namespace), - } - - if r.frontProxy != nil { - // only add the external hostname if this is not reconciling the rootshard-internal-only proxy - dnsNames = append(dnsNames, r.rootShard.Spec.External.Hostname) - if r.rootShard.Spec.External.PrivateHostname != "" { - dnsNames = append(dnsNames, r.rootShard.Spec.External.PrivateHostname) - } - - // DEPRECATED: keep support for the deprecated ExternalHostname field for now - // to not break existing front-proxy installations. - if r.frontProxy.Spec.ExternalHostname != "" { //nolint:staticcheck - dnsNames = append(dnsNames, r.frontProxy.Spec.ExternalHostname) //nolint:staticcheck - } else if r.frontProxy.Spec.External.Hostname != "" { - dnsNames = append(dnsNames, r.frontProxy.Spec.External.Hostname) - } - } + dnsNames := r.serverDNSNames() return func() (string, reconciling.CertificateReconciler) { return name, func(cert *certmanagerv1.Certificate) (*certmanagerv1.Certificate, error) { @@ -132,6 +110,38 @@ func (r *reconciler) serverCertificateReconciler() reconciling.NamedCertificateR } } +// serverDNSNames returns the DNS names for the proxy's server certificate: the Service +// short forms plus the fully qualified Service name, which lives in the cluster domain +// configured on the RootShard. The operator itself addresses the proxy through exactly +// that name (see GetRootShardProxyBaseURL), so it has to be covered by the certificate. +func (r *reconciler) serverDNSNames() []string { + serviceName := r.serviceName() + + dnsNames := []string{ + serviceName, + fmt.Sprintf("%s.%s", serviceName, r.rootShard.Namespace), + fmt.Sprintf("%s.%s.svc.%s", serviceName, r.rootShard.Namespace, resources.GetRootShardClusterDomain(r.rootShard)), + } + + if r.frontProxy != nil { + // only add the external hostname if this is not reconciling the rootshard-internal-only proxy + dnsNames = append(dnsNames, r.rootShard.Spec.External.Hostname) + if r.rootShard.Spec.External.PrivateHostname != "" { + dnsNames = append(dnsNames, r.rootShard.Spec.External.PrivateHostname) + } + + // DEPRECATED: keep support for the deprecated ExternalHostname field for now + // to not break existing front-proxy installations. + if r.frontProxy.Spec.ExternalHostname != "" { //nolint:staticcheck + dnsNames = append(dnsNames, r.frontProxy.Spec.ExternalHostname) //nolint:staticcheck + } else if r.frontProxy.Spec.External.Hostname != "" { + dnsNames = append(dnsNames, r.frontProxy.Spec.External.Hostname) + } + } + + return dnsNames +} + func (r *reconciler) kubeconfigCertificateReconciler() reconciling.NamedCertificateReconcilerFactory { const certKind = operatorv1alpha1.KubeconfigCertificate diff --git a/internal/resources/frontproxy/certificates_test.go b/internal/resources/frontproxy/certificates_test.go new file mode 100644 index 00000000..e97f97cd --- /dev/null +++ b/internal/resources/frontproxy/certificates_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2026 The kcp Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package frontproxy + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1" +) + +func TestServerDNSNames(t *testing.T) { + tests := []struct { + name string + reconciler func() *reconciler + expected []string + }{ + { + name: "root shard proxy with default cluster domain", + reconciler: func() *reconciler { + return NewRootShardProxy(&operatorv1alpha1.RootShard{ + ObjectMeta: metav1.ObjectMeta{Name: "root", Namespace: "kcp"}, + }) + }, + expected: []string{ + "root-proxy", + "root-proxy.kcp", + "root-proxy.kcp.svc.cluster.local", + }, + }, + { + name: "root shard proxy with custom cluster domain", + reconciler: func() *reconciler { + return NewRootShardProxy(&operatorv1alpha1.RootShard{ + ObjectMeta: metav1.ObjectMeta{Name: "root", Namespace: "kcp"}, + Spec: operatorv1alpha1.RootShardSpec{ + CommonShardSpec: operatorv1alpha1.CommonShardSpec{ + ClusterDomain: "cozy.local", + }, + }, + }) + }, + expected: []string{ + "root-proxy", + "root-proxy.kcp", + "root-proxy.kcp.svc.cozy.local", + }, + }, + { + name: "front proxy with custom cluster domain and external hostname", + reconciler: func() *reconciler { + return NewFrontProxy( + &operatorv1alpha1.FrontProxy{ + ObjectMeta: metav1.ObjectMeta{Name: "kcp", Namespace: "kcp"}, + }, + &operatorv1alpha1.RootShard{ + ObjectMeta: metav1.ObjectMeta{Name: "root", Namespace: "kcp"}, + Spec: operatorv1alpha1.RootShardSpec{ + CommonShardSpec: operatorv1alpha1.CommonShardSpec{ + ClusterDomain: "cozy.local", + }, + External: operatorv1alpha1.ExternalConfig{ + Hostname: "kcp.example.com", + }, + }, + }, + nil, + ) + }, + expected: []string{ + "kcp-front-proxy", + "kcp-front-proxy.kcp", + "kcp-front-proxy.kcp.svc.cozy.local", + "kcp.example.com", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.expected, tt.reconciler().serverDNSNames()) + }) + } +} diff --git a/internal/resources/resources.go b/internal/resources/resources.go index c55597ea..ad3e74a4 100644 --- a/internal/resources/resources.go +++ b/internal/resources/resources.go @@ -248,22 +248,22 @@ func GetNamedVirtualWorkspaceResourceLabels(vw deployv1alpha1.NamedVirtualWorksp return getResourceLabels(vw.Name, "virtual-workspace") } -func GetRootShardBaseHost(r *operatorv1alpha1.RootShard) string { - clusterDomain := r.Spec.ClusterDomain - if clusterDomain == "" { - clusterDomain = defaultClusterDomain +// GetRootShardClusterDomain returns the DNS domain of the cluster the RootShard runs in, +// defaulting to the cluster default when the spec does not configure one. +func GetRootShardClusterDomain(r *operatorv1alpha1.RootShard) string { + if r.Spec.ClusterDomain != "" { + return r.Spec.ClusterDomain } - return fmt.Sprintf("%s-kcp.%s.svc.%s", r.Name, r.Namespace, clusterDomain) + return defaultClusterDomain } -func GetRootShardProxyBaseHost(r *operatorv1alpha1.RootShard) string { - clusterDomain := r.Spec.ClusterDomain - if clusterDomain == "" { - clusterDomain = defaultClusterDomain - } +func GetRootShardBaseHost(r *operatorv1alpha1.RootShard) string { + return fmt.Sprintf("%s-kcp.%s.svc.%s", r.Name, r.Namespace, GetRootShardClusterDomain(r)) +} - return fmt.Sprintf("%s-proxy.%s.svc.%s", r.Name, r.Namespace, clusterDomain) +func GetRootShardProxyBaseHost(r *operatorv1alpha1.RootShard) string { + return fmt.Sprintf("%s-proxy.%s.svc.%s", r.Name, r.Namespace, GetRootShardClusterDomain(r)) } func GetCompiledRootShardBaseHost(r *deployv1alpha1.CompiledRootShard) string {