From 8d4909590e36fe6807c6f1d9c1a63ab73fa5fdf9 Mon Sep 17 00:00:00 2001 From: Peter Sutter Date: Wed, 7 Feb 2024 13:42:01 +0100 Subject: [PATCH] Fetch cluster CA via `ConfigMap` --- internal/client/garden/client_test.go | 130 +++++++++++++++++-------- internal/client/garden/shoot_client.go | 38 ++++++-- pkg/cmd/ssh/ssh_test.go | 8 +- pkg/target/manager_test.go | 8 +- 4 files changed, 128 insertions(+), 56 deletions(-) diff --git a/internal/client/garden/client_test.go b/internal/client/garden/client_test.go index 38841f49..b97c84c0 100644 --- a/internal/client/garden/client_test.go +++ b/internal/client/garden/client_test.go @@ -148,9 +148,10 @@ var _ = Describe("Client", func() { k8sVersionLegacy = "1.19.0" // legacy kubeconfig should be rendered ) var ( - testShoot1 *gardencorev1beta1.Shoot - caSecret *corev1.Secret - ca *secrets.Certificate + testShoot1 *gardencorev1beta1.Shoot + caConfigMap *corev1.ConfigMap + caSecret *corev1.Secret + ca *secrets.Certificate ) BeforeEach(func() { @@ -188,6 +189,16 @@ var _ = Describe("Client", func() { ca, err = csc.GenerateCertificate() Expect(err).NotTo(HaveOccurred()) + caConfigMap = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: testShoot1.Name + ".ca-cluster", + Namespace: testShoot1.Namespace, + }, + Data: map[string]string{ + "ca.crt": string(ca.CertificatePEM), + }, + } + caSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: testShoot1.Name + ".ca-cluster", @@ -203,46 +214,87 @@ var _ = Describe("Client", func() { JustBeforeEach(func() { gardenClient = clientgarden.NewClient( nil, - fake.NewClientWithObjects(testShoot1, caSecret), + fake.NewClientWithObjects(testShoot1, caConfigMap), gardenName, ) }) - It("it should return the client config", func() { - gardenClient = clientgarden.NewClient( - nil, - fake.NewClientWithObjects(testShoot1, caSecret), - gardenName, - ) + Context("when ca-cluster configmap exists", func() { + It("it should return the client config", func() { + gardenClient = clientgarden.NewClient( + nil, + fake.NewClientWithObjects(testShoot1, caSecret), + gardenName, + ) + + clientConfig, err := gardenClient.GetShootClientConfig(ctx, namespace, shootName) + Expect(err).NotTo(HaveOccurred()) + + rawConfig, err := clientConfig.RawConfig() + Expect(err).NotTo(HaveOccurred()) + Expect(rawConfig.Clusters).To(HaveLen(2)) + context := rawConfig.Contexts[rawConfig.CurrentContext] + cluster := rawConfig.Clusters[context.Cluster] + Expect(cluster.Server).To(Equal("https://api." + domain)) + Expect(cluster.CertificateAuthorityData).To(Equal(ca.CertificatePEM)) + + extension := &clientgarden.ExecPluginConfig{} + extension.GardenClusterIdentity = gardenName + extension.ShootRef.Namespace = namespace + extension.ShootRef.Name = shootName + + Expect(cluster.Extensions["client.authentication.k8s.io/exec"]).To(Equal(extension.ToRuntimeObject())) + + Expect(rawConfig.Contexts).To(HaveLen(2)) + + Expect(rawConfig.AuthInfos).To(HaveLen(1)) + authInfo := rawConfig.AuthInfos[context.AuthInfo] + Expect(authInfo.Exec.APIVersion).To(Equal(clientauthenticationv1.SchemeGroupVersion.String())) + Expect(authInfo.Exec.Command).To(Equal("kubectl-gardenlogin")) + Expect(authInfo.Exec.Args).To(Equal([]string{ + "get-client-certificate", + })) + Expect(authInfo.Exec.InstallHint).ToNot(BeEmpty()) + }) + }) - clientConfig, err := gardenClient.GetShootClientConfig(ctx, namespace, shootName) - Expect(err).NotTo(HaveOccurred()) - - rawConfig, err := clientConfig.RawConfig() - Expect(err).NotTo(HaveOccurred()) - Expect(rawConfig.Clusters).To(HaveLen(2)) - context := rawConfig.Contexts[rawConfig.CurrentContext] - cluster := rawConfig.Clusters[context.Cluster] - Expect(cluster.Server).To(Equal("https://api." + domain)) - Expect(cluster.CertificateAuthorityData).To(Equal(ca.CertificatePEM)) - - extension := &clientgarden.ExecPluginConfig{} - extension.GardenClusterIdentity = gardenName - extension.ShootRef.Namespace = namespace - extension.ShootRef.Name = shootName - - Expect(cluster.Extensions["client.authentication.k8s.io/exec"]).To(Equal(extension.ToRuntimeObject())) - - Expect(rawConfig.Contexts).To(HaveLen(2)) - - Expect(rawConfig.AuthInfos).To(HaveLen(1)) - authInfo := rawConfig.AuthInfos[context.AuthInfo] - Expect(authInfo.Exec.APIVersion).To(Equal(clientauthenticationv1.SchemeGroupVersion.String())) - Expect(authInfo.Exec.Command).To(Equal("kubectl-gardenlogin")) - Expect(authInfo.Exec.Args).To(Equal([]string{ - "get-client-certificate", - })) - Expect(authInfo.Exec.InstallHint).ToNot(BeEmpty()) + Context("when ca-cluster secret exists", func() { + It("it should return the client config", func() { + gardenClient = clientgarden.NewClient( + nil, + fake.NewClientWithObjects(testShoot1, caSecret), + gardenName, + ) + + clientConfig, err := gardenClient.GetShootClientConfig(ctx, namespace, shootName) + Expect(err).NotTo(HaveOccurred()) + + rawConfig, err := clientConfig.RawConfig() + Expect(err).NotTo(HaveOccurred()) + Expect(rawConfig.Clusters).To(HaveLen(2)) + context := rawConfig.Contexts[rawConfig.CurrentContext] + cluster := rawConfig.Clusters[context.Cluster] + Expect(cluster.Server).To(Equal("https://api." + domain)) + Expect(cluster.CertificateAuthorityData).To(Equal(ca.CertificatePEM)) + + extension := &clientgarden.ExecPluginConfig{} + extension.GardenClusterIdentity = gardenName + extension.ShootRef.Namespace = namespace + extension.ShootRef.Name = shootName + + Expect(cluster.Extensions["client.authentication.k8s.io/exec"]).To(Equal(extension.ToRuntimeObject())) + + Expect(rawConfig.Contexts).To(HaveLen(2)) + + Expect(rawConfig.AuthInfos).To(HaveLen(1)) + authInfo := rawConfig.AuthInfos[context.AuthInfo] + Expect(authInfo.Exec.APIVersion).To(Equal(clientauthenticationv1.SchemeGroupVersion.String())) + Expect(authInfo.Exec.Command).To(Equal("kubectl-gardenlogin")) + Expect(authInfo.Exec.Args).To(Equal([]string{ + "get-client-certificate", + })) + Expect(authInfo.Exec.InstallHint).ToNot(BeEmpty()) + }) }) Context("legacy kubeconfig", func() { @@ -281,7 +333,7 @@ var _ = Describe("Client", func() { }) }) - Context("when the ca-cluster secret does not exist", func() { + Context("when the ca-cluster does not exist", func() { BeforeEach(func() { gardenClient = clientgarden.NewClient( nil, diff --git a/internal/client/garden/shoot_client.go b/internal/client/garden/shoot_client.go index cbdf174d..0c4d1682 100644 --- a/internal/client/garden/shoot_client.go +++ b/internal/client/garden/shoot_client.go @@ -16,6 +16,7 @@ import ( gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" seedmanagementv1alpha1 "github.com/gardener/gardener/pkg/apis/seedmanagement/v1alpha1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -32,9 +33,12 @@ func init() { } const ( + // ShootProjectConfigMapSuffixCACluster is a constant for a shoot project config map with suffix 'ca-cluster'. + ShootProjectConfigMapSuffixCACluster = "ca-cluster" // ShootProjectSecretSuffixCACluster is a constant for a shoot project secret with suffix 'ca-cluster'. + // Deprecated: This constant is deprecated in favor of ShootProjectConfigMapSuffixCACluster. ShootProjectSecretSuffixCACluster = "ca-cluster" - // DataKeyCertificateCA is the key in a secret data holding the CA certificate. + // DataKeyCertificateCA is the key in a secret or config map data holding the CA certificate. DataKeyCertificateCA = "ca.crt" ) @@ -219,16 +223,32 @@ func (g *clientImpl) GetShootClientConfig(ctx context.Context, namespace, name s } // fetch cluster ca - caClusterSecret := corev1.Secret{} - caClusterSecretName := fmt.Sprintf("%s.%s", name, ShootProjectSecretSuffixCACluster) + caClusterConfigMap := corev1.ConfigMap{} + caClusterConfigName := fmt.Sprintf("%s.%s", name, ShootProjectConfigMapSuffixCACluster) - if err := g.c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: caClusterSecretName}, &caClusterSecret); err != nil { - return nil, err - } + err := g.c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: caClusterConfigName}, &caClusterConfigMap) + + var caCert []byte + // TODO(petersutter): Remove this fallback of reading the `.ca-cluster` Secret when Gardener no longer reconciles it, presumably with Gardener v1.97. + if apierrors.IsNotFound(err) { //nolint:gocritic // Rewriting the if-else to a switch statement does not provide significant improvement in this case. We will soon remove the switch once we stop reading the Secret. + caClusterSecret := corev1.Secret{} + caClusterSecretName := fmt.Sprintf("%s.%s", name, ShootProjectSecretSuffixCACluster) + + if err := g.c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: caClusterSecretName}, &caClusterSecret); err != nil { + return nil, fmt.Errorf("could not get cluster CA secret: %w", err) + } - caCert, ok := caClusterSecret.Data[DataKeyCertificateCA] - if !ok || len(caCert) == 0 { - return nil, fmt.Errorf("%s of secret %s is empty", DataKeyCertificateCA, caClusterSecretName) + caCert = caClusterSecret.Data[DataKeyCertificateCA] + if len(caCert) == 0 { + return nil, fmt.Errorf("%s of secret %s is empty", DataKeyCertificateCA, caClusterSecretName) + } + } else if err != nil { + return nil, fmt.Errorf("could not get cluster CA config map: %w", err) + } else { + caCert = []byte(caClusterConfigMap.Data[DataKeyCertificateCA]) + if len(caCert) == 0 { + return nil, fmt.Errorf("%s of config map %s is empty", DataKeyCertificateCA, caClusterConfigName) + } } kubeconfigRequest := shootKubeconfigRequest{ diff --git a/pkg/cmd/ssh/ssh_test.go b/pkg/cmd/ssh/ssh_test.go index df0c3492..ad35dbe2 100644 --- a/pkg/cmd/ssh/ssh_test.go +++ b/pkg/cmd/ssh/ssh_test.go @@ -233,13 +233,13 @@ var _ = Describe("SSH Command", func() { ca, err := csc.GenerateCertificate() Expect(err).NotTo(HaveOccurred()) - caSecret := &corev1.Secret{ + caConfigMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: testShoot.Name + ".ca-cluster", Namespace: testShoot.Namespace, }, - Data: map[string][]byte{ - "ca.crt": ca.CertificatePEM, + Data: map[string]string{ + "ca.crt": string(ca.CertificatePEM), }, } @@ -251,7 +251,7 @@ var _ = Describe("SSH Command", func() { testShoot, testShootKeypair, seedKubeconfigSecret, - caSecret, + caConfigMap, ). WithStatusSubresource(&operationsv1alpha1.Bastion{}). Build()) diff --git a/pkg/target/manager_test.go b/pkg/target/manager_test.go index c63d39e4..8d23392c 100644 --- a/pkg/target/manager_test.go +++ b/pkg/target/manager_test.go @@ -188,13 +188,13 @@ var _ = Describe("Target Manager", func() { ca, err := csc.GenerateCertificate() Expect(err).NotTo(HaveOccurred()) - prod1GoldenShootCaSecret := &corev1.Secret{ + prod1GoldenShootCaConfigMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: prod1GoldenShoot.Name + ".ca-cluster", Namespace: *prod1Project.Spec.Namespace, }, - Data: map[string][]byte{ - "ca.crt": ca.CertificatePEM, + Data: map[string]string{ + "ca.crt": string(ca.CertificatePEM), }, } @@ -209,7 +209,7 @@ var _ = Describe("Target Manager", func() { prod2AmbiguousShoot, prod1PendingShoot, namespace, - prod1GoldenShootCaSecret, + prod1GoldenShootCaConfigMap, ) ctrl = gomock.NewController(GinkgoT())