From 417b91559d6f426905d603e147984861ba95f110 Mon Sep 17 00:00:00 2001 From: Cyrill Berg Date: Wed, 17 Dec 2025 20:33:32 +0100 Subject: [PATCH 1/2] fix: respect caBundleSecretRef on shard resources for externalLogicalClusterAdminKubeconfig Signed-off-by: Cyrill Berg --- internal/controller/shard/controller.go | 2 +- internal/resources/shard/kubeconfigs.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/controller/shard/controller.go b/internal/controller/shard/controller.go index 1fd52766..bac2c078 100644 --- a/internal/controller/shard/controller.go +++ b/internal/controller/shard/controller.go @@ -194,7 +194,7 @@ func (r *ShardReconciler) reconcileStatus(ctx context.Context, oldShard *operato availableCond := apimeta.FindStatusCondition(newShard.Status.Conditions, string(operatorv1alpha1.ConditionTypeAvailable)) switch { - case availableCond.Status == metav1.ConditionTrue: + case availableCond != nil && availableCond.Status == metav1.ConditionTrue: newShard.Status.Phase = operatorv1alpha1.ShardPhaseRunning case newShard.DeletionTimestamp != nil: diff --git a/internal/resources/shard/kubeconfigs.go b/internal/resources/shard/kubeconfigs.go index a25b3b15..bfdb9174 100644 --- a/internal/resources/shard/kubeconfigs.go +++ b/internal/resources/shard/kubeconfigs.go @@ -149,8 +149,8 @@ func ExternalLogicalClusterAdminKubeconfigReconciler(shard *operatorv1alpha1.Sha config = &clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ serverName: { - Server: fmt.Sprintf("https://%s:%d", rootShard.Spec.External.Hostname, rootShard.Spec.External.Port), - CertificateAuthority: getCAMountPath(operatorv1alpha1.ServerCA) + "/tls.crt", + Server: fmt.Sprintf("https://%s:%d", rootShard.Spec.External.Hostname, rootShard.Spec.External.Port), + // CertificateAuthority will be configured below to respect CABundleSecretRef property in the shard spec }, }, Contexts: map[string]*clientcmdapi.Context{ @@ -168,6 +168,12 @@ func ExternalLogicalClusterAdminKubeconfigReconciler(shard *operatorv1alpha1.Sha CurrentContext: contextName, } + if shard.Spec.CABundleSecretRef == nil { + config.Clusters[serverName].CertificateAuthority = getCAMountPath(operatorv1alpha1.ServerCA) + "/tls.crt" + } else { + config.Clusters[serverName].CertificateAuthority = getCAMountPath(operatorv1alpha1.CABundleCA) + "/tls.crt" + } + data, err := clientcmd.Write(*config) if err != nil { return nil, err From e9626931e550124aaf394563a7895fdedac10bc7 Mon Sep 17 00:00:00 2001 From: Cyrill Berg Date: Wed, 17 Dec 2025 20:35:09 +0100 Subject: [PATCH 2/2] test: enhance shard controller tests to check for created kubeconfigs Signed-off-by: Cyrill Berg --- internal/controller/shard/controller_test.go | 130 ++++++++++++++++++- 1 file changed, 125 insertions(+), 5 deletions(-) diff --git a/internal/controller/shard/controller_test.go b/internal/controller/shard/controller_test.go index ca4617e1..2cd75a56 100644 --- a/internal/controller/shard/controller_test.go +++ b/internal/controller/shard/controller_test.go @@ -23,9 +23,12 @@ import ( certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" ctrlruntimefakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -37,9 +40,11 @@ func TestReconciling(t *testing.T) { const namespace = "shard-tests" testcases := []struct { - name string - rootShard *operatorv1alpha1.RootShard - shard *operatorv1alpha1.Shard + name string + rootShard *operatorv1alpha1.RootShard + shard *operatorv1alpha1.Shard + extraObjects []ctrlruntimeclient.Object + checkFunc func(t *testing.T, client ctrlruntimeclient.Client, shard *operatorv1alpha1.Shard) }{ { name: "vanilla", @@ -78,20 +83,131 @@ func TestReconciling(t *testing.T) { }, }, }, + extraObjects: nil, + checkFunc: func(t *testing.T, client ctrlruntimeclient.Client, shard *operatorv1alpha1.Shard) { + // Check that the external logical cluster admin kubeconfig uses the server CA + _ = api.Config{} + kubeconfigSecret := &corev1.Secret{} + err := client.Get(context.Background(), ctrlruntimeclient.ObjectKey{ + Name: shard.Name + "-external-logical-cluster-admin-kubeconfig", + Namespace: shard.Namespace, + }, kubeconfigSecret) + require.NoError(t, err) + + kubeconfigData, exists := kubeconfigSecret.Data["kubeconfig"] + require.True(t, exists, "kubeconfig data should exist") + + config, err := clientcmd.Load(kubeconfigData) + require.NoError(t, err) + + cluster, exists := config.Clusters["external-logical-cluster:admin"] + require.True(t, exists, "external-logical-cluster:admin cluster should exist") + + require.Equal(t, "/etc/kcp/tls/ca/server/tls.crt", cluster.CertificateAuthority, "should use server CA path") + }, + }, + { + name: "with-ca-bundle-secret-ref", + rootShard: &operatorv1alpha1.RootShard{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rooty-ca", + Namespace: namespace, + }, + Spec: operatorv1alpha1.RootShardSpec{ + External: operatorv1alpha1.ExternalConfig{ + Hostname: "example.kcp.io", + Port: 6443, + }, + CommonShardSpec: operatorv1alpha1.CommonShardSpec{ + Etcd: operatorv1alpha1.EtcdConfig{ + Endpoints: []string{"https://localhost:2379"}, + }, + }, + }, + }, + shard: &operatorv1alpha1.Shard{ + ObjectMeta: metav1.ObjectMeta{ + Name: "shardy-ca", + Namespace: namespace, + }, + Spec: operatorv1alpha1.ShardSpec{ + CommonShardSpec: operatorv1alpha1.CommonShardSpec{ + Etcd: operatorv1alpha1.EtcdConfig{ + Endpoints: []string{"https://localhost:2379"}, + }, + CABundleSecretRef: &corev1.LocalObjectReference{ + Name: "custom-ca", + }, + }, + RootShard: operatorv1alpha1.RootShardConfig{ + Reference: &corev1.LocalObjectReference{ + Name: "rooty-ca", + }, + }, + }, + }, + extraObjects: []ctrlruntimeclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-ca", + Namespace: namespace, + }, + Data: map[string][]byte{ + "tls.crt": []byte("custom-ca-cert"), + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "shardy-ca-server", + Namespace: namespace, + }, + Data: map[string][]byte{ + "tls.crt": []byte("server-ca-cert"), + }, + }, + }, + checkFunc: func(t *testing.T, client ctrlruntimeclient.Client, shard *operatorv1alpha1.Shard) { + // Check that the external logical cluster admin kubeconfig uses the merged CA bundle + _ = api.Config{} + kubeconfigSecret := &corev1.Secret{} + err := client.Get(context.Background(), ctrlruntimeclient.ObjectKey{ + Name: shard.Name + "-external-logical-cluster-admin-kubeconfig", + Namespace: shard.Namespace, + }, kubeconfigSecret) + require.NoError(t, err) + + kubeconfigData, exists := kubeconfigSecret.Data["kubeconfig"] + require.True(t, exists, "kubeconfig data should exist") + + config, err := clientcmd.Load(kubeconfigData) + require.NoError(t, err) + + cluster, exists := config.Clusters["external-logical-cluster:admin"] + require.True(t, exists, "external-logical-cluster:admin cluster should exist") + + require.Equal(t, "/etc/kcp/tls/ca/ca-bundle/tls.crt", cluster.CertificateAuthority, "should use merged CA bundle path") + }, }, } scheme := runtime.NewScheme() + require.Nil(t, corev1.AddToScheme(scheme)) + require.Nil(t, appsv1.AddToScheme(scheme)) require.Nil(t, operatorv1alpha1.AddToScheme(scheme)) require.Nil(t, certmanagerv1.AddToScheme(scheme)) for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { + objects := []ctrlruntimeclient.Object{testcase.rootShard, testcase.shard} + if testcase.extraObjects != nil { + objects = append(objects, testcase.extraObjects...) + } + client := ctrlruntimefakeclient. NewClientBuilder(). WithScheme(scheme). WithStatusSubresource(testcase.rootShard, testcase.shard). - WithObjects(testcase.rootShard, testcase.shard). + WithObjects(objects...). Build() ctx := context.Background() @@ -102,9 +218,13 @@ func TestReconciling(t *testing.T) { } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: ctrlruntimeclient.ObjectKeyFromObject(testcase.rootShard), + NamespacedName: ctrlruntimeclient.ObjectKeyFromObject(testcase.shard), }) require.NoError(t, err) + + if testcase.checkFunc != nil { + testcase.checkFunc(t, client, testcase.shard) + } }) } }