From 562969fc933b373ec4116972cd20bbec278913cc Mon Sep 17 00:00:00 2001 From: Robert Graeff Date: Wed, 18 Sep 2024 07:36:59 +0200 Subject: [PATCH] OIDC targets --- .../targettypes/kubernetes_cluster.go | 19 +++- .../targettypes/kubernetes_cluster_test.go | 107 ++++++++++++++++++ apis/go.mod | 4 + apis/go.sum | 1 - pkg/deployer/lib/utils.go | 65 +++++++++-- 5 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go diff --git a/apis/core/v1alpha1/targettypes/kubernetes_cluster.go b/apis/core/v1alpha1/targettypes/kubernetes_cluster.go index da8b22c0ce..8916ddf60b 100644 --- a/apis/core/v1alpha1/targettypes/kubernetes_cluster.go +++ b/apis/core/v1alpha1/targettypes/kubernetes_cluster.go @@ -20,6 +20,8 @@ const KubernetesClusterTargetType v1alpha1.TargetType = core.GroupName + "/kuber type KubernetesClusterTargetConfig struct { // Kubeconfig defines kubeconfig as string. Kubeconfig ValueRef `json:"kubeconfig"` + + OIDCConfig *OIDCConfig `json:"oidcConfig,omitempty"` } // DefaultKubeconfigKey is the default that is used to hold a kubeconfig. @@ -35,7 +37,8 @@ type ValueRef struct { // kubeconfigJSON is a helper struct for decoding. type kubeconfigJSON struct { - Kubeconfig *ValueRef `json:"kubeconfig"` + Kubeconfig *ValueRef `json:"kubeconfig"` + OIDCConfig *OIDCConfig `json:"oidcConfig,omitempty"` } // valueRefJSON is a helper struct to decode json into a secret ref object. @@ -77,9 +80,12 @@ func (v *ValueRef) UnmarshalJSON(data []byte) error { func (kc *KubernetesClusterTargetConfig) UnmarshalJSON(data []byte) error { kj := &kubeconfigJSON{} err := json.Unmarshal(data, kj) - if err == nil && kj.Kubeconfig != nil { + if err == nil && (kj.Kubeconfig != nil || kj.OIDCConfig != nil) { // parsing was successful - kc.Kubeconfig = *kj.Kubeconfig + if kj.Kubeconfig != nil { + kc.Kubeconfig = *kj.Kubeconfig + } + kc.OIDCConfig = kj.OIDCConfig return nil } return kc.Kubeconfig.UnmarshalJSON(data) @@ -92,3 +98,10 @@ func (v ValueRef) OpenAPISchemaType() []string { } } func (v ValueRef) OpenAPISchemaFormat() string { return "" } + +type OIDCConfig struct { + Server string `json:"server,omitempty"` + CACertificate []byte `json:"caCertificate,omitempty"` + ServiceAccount string `json:"serviceAccount,omitempty"` + Audience []string `json:"audience,omitempty"` +} diff --git a/apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go b/apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go new file mode 100644 index 0000000000..7b214327d9 --- /dev/null +++ b/apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go @@ -0,0 +1,107 @@ +package targettypes_test + +import ( + "encoding/json" + "github.com/gardener/landscaper/apis/core/v1alpha1" + "github.com/gardener/landscaper/apis/core/v1alpha1/targettypes" + "k8s.io/utils/ptr" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Types Testing") +} + +var _ = Describe("Kubernetes Cluster Target Types", func() { + + It("should marshal a kubeconfig", func() { + targetConfig := &targettypes.KubernetesClusterTargetConfig{ + Kubeconfig: targettypes.ValueRef{ + StrVal: ptr.To("test-kubeconfig"), + }, + } + targetConfigJSON, err := json.Marshal(targetConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(targetConfigJSON).To(MatchJSON(`{"kubeconfig":"test-kubeconfig"}`)) + }) + + It("should unmarshal a kubeconfig", func() { + configJSON := []byte(`{"kubeconfig":"test-kubeconfig"}`) + config := &targettypes.KubernetesClusterTargetConfig{} + Expect(json.Unmarshal(configJSON, config)).To(Succeed()) + Expect(config).To(Equal(&targettypes.KubernetesClusterTargetConfig{ + Kubeconfig: targettypes.ValueRef{ + StrVal: ptr.To("test-kubeconfig"), + }, + })) + }) + + It("should marshal an old secretRef", func() { + targetConfig := &targettypes.KubernetesClusterTargetConfig{ + Kubeconfig: targettypes.ValueRef{ + SecretRef: &v1alpha1.SecretReference{ + ObjectReference: v1alpha1.ObjectReference{ + Name: "test-secret-name", + Namespace: "test-namespace", + }, + Key: "test-key", + }, + }, + } + targetConfigBytes, err := json.Marshal(targetConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(targetConfigBytes).To(MatchJSON(`{"kubeconfig":{"secretRef":{"name":"test-secret-name","namespace":"test-namespace","key":"test-key"}}}`)) + }) + + It("should unmarshal an old secretRef", func() { + configJSON := []byte(`{"kubeconfig":{"secretRef":{"name":"test-secret-name","namespace":"test-namespace","key":"test-key"}}}`) + config := &targettypes.KubernetesClusterTargetConfig{} + Expect(json.Unmarshal(configJSON, config)).To(Succeed()) + Expect(config).To(Equal(&targettypes.KubernetesClusterTargetConfig{ + Kubeconfig: targettypes.ValueRef{ + SecretRef: &v1alpha1.SecretReference{ + ObjectReference: v1alpha1.ObjectReference{ + Name: "test-secret-name", + Namespace: "test-namespace", + }, + Key: "test-key", + }, + }, + })) + }) + + It("should marshal an oidc config", func() { + targetConfig := &targettypes.KubernetesClusterTargetConfig{ + OIDCConfig: &targettypes.OIDCConfig{ + Server: "test-server", + CACertificate: []byte("test-cert"), + ServiceAccount: "test-account", + Audience: []string{"test-audience"}, + }, + } + targetConfigJSON, err := json.Marshal(targetConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(targetConfigJSON).To(MatchJSON(`{"kubeconfig":{},"oidcConfig":{"server":"test-server","caCertificate":"dGVzdC1jZXJ0","serviceAccount":"test-account","audience":["test-audience"]}}`)) + }) + + It("should unmarshal an oidc config", func() { + configJSON := []byte(`{"kubeconfig":{},"oidcConfig":{"server":"test-server","caCertificate":"dGVzdC1jZXJ0","serviceAccount":"test-account","audience":["test-audience"]}}`) + config := &targettypes.KubernetesClusterTargetConfig{} + Expect(json.Unmarshal(configJSON, config)).To(Succeed()) + Expect(config).To(Equal(&targettypes.KubernetesClusterTargetConfig{ + Kubeconfig: targettypes.ValueRef{ + StrVal: ptr.To("{}"), + }, + OIDCConfig: &targettypes.OIDCConfig{ + Server: "test-server", + CACertificate: []byte("test-cert"), + ServiceAccount: "test-account", + Audience: []string{"test-audience"}, + }, + })) + }) +}) diff --git a/apis/go.mod b/apis/go.mod index 2309da080c..583ddfad9e 100644 --- a/apis/go.mod +++ b/apis/go.mod @@ -5,6 +5,7 @@ go 1.22.4 require ( github.com/gardener/component-spec/bindings-go v0.0.98 github.com/onsi/ginkgo v1.16.5 + github.com/onsi/ginkgo/v2 v2.20.1 github.com/onsi/gomega v1.34.2 github.com/robfig/cron/v3 v3.0.1 k8s.io/api v0.30.3 @@ -24,11 +25,13 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -41,6 +44,7 @@ require ( golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.24.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/apis/go.sum b/apis/go.sum index 97a64e0f09..77dc351205 100644 --- a/apis/go.sum +++ b/apis/go.sum @@ -21,7 +21,6 @@ github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDsl github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= diff --git a/pkg/deployer/lib/utils.go b/pkg/deployer/lib/utils.go index 9b43d9ff59..c98d2217db 100644 --- a/pkg/deployer/lib/utils.go +++ b/pkg/deployer/lib/utils.go @@ -11,6 +11,9 @@ import ( "fmt" "reflect" + authenticationv1 "k8s.io/api/authentication/v1" + "k8s.io/utils/ptr" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,24 +40,62 @@ import ( "github.com/gardener/landscaper/pkg/utils/read_write_layer" ) -func GetRestConfigAndClientAndClientSet(ctx context.Context, resolvedTarget *lsv1alpha1.ResolvedTarget, lsUncachedClient client.Client) (*rest.Config, client.Client, kubernetes.Interface, error) { +func GetRestConfigAndClientAndClientSet(ctx context.Context, resolvedTarget *lsv1alpha1.ResolvedTarget, lsUncachedClient client.Client) (_ *rest.Config, _ client.Client, _ kubernetes.Interface, err error) { + var restConfig *rest.Config + + if resolvedTarget.Target == nil { + return nil, nil, nil, fmt.Errorf("resolved target does not contain the original target") + } + targetConfig := &targettypes.KubernetesClusterTargetConfig{} if err := yaml.Unmarshal([]byte(resolvedTarget.Content), targetConfig); err != nil { return nil, nil, nil, fmt.Errorf("unable to parse target confĂ­guration: %w", err) } - kubeconfigBytes, err := GetKubeconfigFromTargetConfig(ctx, targetConfig, resolvedTarget.Namespace, lsUncachedClient) - if err != nil { - return nil, nil, nil, err - } + if targetConfig.Kubeconfig.StrVal != nil || targetConfig.Kubeconfig.SecretRef != nil { + kubeconfigBytes, err := GetKubeconfigFromTargetConfig(ctx, targetConfig, resolvedTarget.Namespace, lsUncachedClient) + if err != nil { + return nil, nil, nil, err + } - kubeconfig, err := clientcmd.NewClientConfigFromBytes(kubeconfigBytes) - if err != nil { - return nil, nil, nil, err - } - restConfig, err := kubeconfig.ClientConfig() - if err != nil { - return nil, nil, nil, err + kubeconfig, err := clientcmd.NewClientConfigFromBytes(kubeconfigBytes) + if err != nil { + return nil, nil, nil, err + } + + restConfig, err = kubeconfig.ClientConfig() + if err != nil { + return nil, nil, nil, err + } + + } else if targetConfig.OIDCConfig != nil { + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: resolvedTarget.Namespace, + Name: targetConfig.OIDCConfig.ServiceAccount, + }, + } + tokenRequest := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: targetConfig.OIDCConfig.Audience, + ExpirationSeconds: ptr.To[int64](86400), + }, + } + + if err = lsUncachedClient.SubResource("token").Create(ctx, serviceAccount, tokenRequest); err != nil { + return nil, nil, nil, fmt.Errorf("unable to create token: %w", err) + } + + restConfig = &rest.Config{ + Host: targetConfig.OIDCConfig.Server, + BearerToken: tokenRequest.Status.Token, + TLSClientConfig: rest.TLSClientConfig{ + CAData: targetConfig.OIDCConfig.CACertificate, + }, + } + + } else { + return nil, nil, nil, fmt.Errorf("unable build rest config from resolved target") } kubeClient, err := client.New(restConfig, client.Options{})