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

Generate Shoot gardenlogin kubeconfig locally #149

Merged
merged 12 commits into from
Apr 21, 2022
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/gardener/gardenctl-v2
go 1.17

require (
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/sprig/v3 v3.2.2
github.com/fatih/color v1.13.0
github.com/gardener/gardener v1.40.0
Expand Down Expand Up @@ -30,7 +31,6 @@ require (
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand Down
29 changes: 6 additions & 23 deletions internal/gardenclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,16 @@ type Client interface {

type clientImpl struct {
c client.Client

// name is a unique identifier of this Garden client
name string
}

// NewGardenClient returns a new gardenclient
func NewGardenClient(client client.Client) Client {
func NewGardenClient(client client.Client, name string) Client {
return &clientImpl{
c: client,
c: client,
name: name,
}
}

Expand Down Expand Up @@ -283,27 +287,6 @@ func (g *clientImpl) GetSeedClientConfig(ctx context.Context, name string) (clie
return config, nil
}

func (g *clientImpl) GetShootClientConfig(ctx context.Context, namespace, name string) (clientcmd.ClientConfig, error) {
key := types.NamespacedName{Namespace: namespace, Name: name}

cm, err := g.GetConfigMap(ctx, namespace, name+".kubeconfig")
if err != nil {
return nil, fmt.Errorf("failed to get kubeconfig for shoot %v: %w", key, err)
}

value, ok := cm.Data["kubeconfig"]
if !ok {
return nil, fmt.Errorf("invalid kubeconfig configmap for shoot %v", key)
}

config, err := clientcmd.NewClientConfigFromBytes([]byte(value))
if err != nil {
return nil, fmt.Errorf("failed to deserialize kubeconfig for shoot %v: %w", key, err)
}

return config, nil
}

func (g *clientImpl) GetCloudProfile(ctx context.Context, name string) (*gardencorev1beta1.CloudProfile, error) {
cloudProfile := &gardencorev1beta1.CloudProfile{}
key := types.NamespacedName{Name: name}
Expand Down
175 changes: 170 additions & 5 deletions internal/gardenclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ package gardenclient_test

import (
"context"
"fmt"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/gardener/gardener/pkg/utils/secrets"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
Expand All @@ -23,12 +26,16 @@ import (
)

var _ = Describe("Client", func() {
Describe("GetSeedClientConfig", func() {
var (
ctx context.Context
gardenClient gardenclient.Client
)
const (
gardenName = "my-garden"
)

var (
ctx context.Context
gardenClient gardenclient.Client
)

Describe("GetSeedClientConfig", func() {
BeforeEach(func() {
ctx = context.Background()
oidcSecret := &corev1.Secret{
Expand All @@ -51,6 +58,7 @@ var _ = Describe("Client", func() {
}
gardenClient = gardenclient.NewGardenClient(
fake.NewClientWithObjects(oidcSecret, loginSecret),
gardenName,
)
})

Expand All @@ -74,6 +82,163 @@ var _ = Describe("Client", func() {
})
})
})

Describe("GetShootClientConfig", func() {
const (
shootName = "test-shoot1"
namespace = "garden-prod1"
domain = "foo.bar.baz"

k8sVersion = "1.20.0"
k8sVersionLegacy = "1.19.0" // legacy kubeconfig should be rendered
)
var (
testShoot1 *gardencorev1beta1.Shoot
caSecret *corev1.Secret
ca *secrets.Certificate
)

BeforeEach(func() {
ctx = context.Background()
testShoot1 = &gardencorev1beta1.Shoot{
ObjectMeta: metav1.ObjectMeta{
Name: shootName,
Namespace: namespace,
},
Spec: gardencorev1beta1.ShootSpec{
Kubernetes: gardencorev1beta1.Kubernetes{
Version: k8sVersion,
},
},
Status: gardencorev1beta1.ShootStatus{
AdvertisedAddresses: []gardencorev1beta1.ShootAdvertisedAddress{
{
Name: "shoot-address1",
URL: "https://api." + domain,
},
{
Name: "shoot-address2",
URL: "https://api2." + domain,
},
},
},
}

csc := &secrets.CertificateSecretConfig{
Name: "ca-test",
CommonName: "ca-test",
CertType: secrets.CACert,
}
var err error
ca, err = csc.GenerateCertificate()
Expect(err).NotTo(HaveOccurred())

caSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: testShoot1.Name + ".ca-cluster",
Namespace: testShoot1.Namespace,
},
Data: map[string][]byte{
"ca.crt": ca.CertificatePEM,
},
}
})

Context("good case", func() {
JustBeforeEach(func() {
gardenClient = gardenclient.NewGardenClient(
fake.NewClientWithObjects(testShoot1, caSecret),
gardenName,
)
})

It("it should return the client config", func() {
gardenClient = gardenclient.NewGardenClient(
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 := &gardenclient.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.Command).To(Equal("kubectl"))
Expect(authInfo.Exec.Args).To(Equal([]string{
"gardenlogin",
"get-client-certificate",
}))
})

Context("legacy kubeconfig", func() {
BeforeEach(func() {
By("having shoot kubernetes version < v1.20.0")
testShoot1.Spec.Kubernetes.Version = k8sVersionLegacy
})

It("should create legacy kubeconfig configMap", func() {
clientConfig, err := gardenClient.GetShootClientConfig(ctx, namespace, shootName)
Expect(err).NotTo(HaveOccurred())

rawConfig, err := clientConfig.RawConfig()
Expect(err).ToNot(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))
Expect(cluster.Extensions).To(BeEmpty())

Expect(rawConfig.Contexts).To(HaveLen(2))

Expect(rawConfig.AuthInfos).To(HaveLen(1))
authInfo := rawConfig.AuthInfos[context.AuthInfo]
Expect(authInfo.Exec.Command).To(Equal("kubectl"))
Expect(authInfo.Exec.Args).To(Equal([]string{
"gardenlogin",
"get-client-certificate",
fmt.Sprintf("--name=%s", shootName),
fmt.Sprintf("--namespace=%s", namespace),
fmt.Sprintf("--garden-cluster-identity=%s", gardenName),
}))
})
})
})

Context("when the ca-cluster secret does not exist", func() {
BeforeEach(func() {
gardenClient = gardenclient.NewGardenClient(
fake.NewClientWithObjects(testShoot1),
gardenName,
)
})

It("it should fail with not found error", func() {
_, err := gardenClient.GetShootClientConfig(ctx, namespace, shootName)
Expect(err).To(HaveOccurred())
Expect(apierrors.IsNotFound(err)).To(BeTrue())
Expect(err.Error()).To(ContainSubstring(shootName + ".ca-cluster"))
})
})
})
})

// TODO copied from target_suite_test. Move into a test helper package for better reuse
Expand Down
17 changes: 17 additions & 0 deletions internal/gardenclient/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Gardener contributors

SPDX-License-Identifier: Apache-2.0
*/

package gardenclient

import "k8s.io/apimachinery/pkg/runtime"

type ExecPluginConfig struct {
execPluginConfig
}

func (e *ExecPluginConfig) ToRuntimeObject() runtime.Object {
return &e.execPluginConfig
}
7 changes: 7 additions & 0 deletions internal/gardenclient/gardenclient_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ package gardenclient_test
import (
"testing"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes/scheme"
)

func init() {
utilruntime.Must(gardencorev1beta1.AddToScheme(scheme.Scheme))
}

func TestCloudEnvCommand(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Gardenclient Test Suite")
Expand Down