Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support viewer to target Shoot cluster by fetching cluster CA via ConfigMap #380

Merged
merged 2 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Fetch cluster CA via ConfigMap
  • Loading branch information
petersutter committed Feb 7, 2024
commit 8d4909590e36fe6807c6f1d9c1a63ab73fa5fdf9
130 changes: 91 additions & 39 deletions internal/client/garden/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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",
Expand All @@ -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() {
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 29 additions & 9 deletions internal/client/garden/shoot_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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 `<shoot-name>.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{
Expand Down
8 changes: 4 additions & 4 deletions pkg/cmd/ssh/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
}

Expand All @@ -251,7 +251,7 @@ var _ = Describe("SSH Command", func() {
testShoot,
testShootKeypair,
seedKubeconfigSecret,
caSecret,
caConfigMap,
).
WithStatusSubresource(&operationsv1alpha1.Bastion{}).
Build())
Expand Down
8 changes: 4 additions & 4 deletions pkg/target/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
}

Expand All @@ -209,7 +209,7 @@ var _ = Describe("Target Manager", func() {
prod2AmbiguousShoot,
prod1PendingShoot,
namespace,
prod1GoldenShootCaSecret,
prod1GoldenShootCaConfigMap,
)

ctrl = gomock.NewController(GinkgoT())
Expand Down
Loading