Skip to content

feat: add clusters marshal #64

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
137 changes: 137 additions & 0 deletions pkg/clusteraccess/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"time"

"sigs.k8s.io/yaml"

authenticationv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
Expand Down Expand Up @@ -341,3 +343,138 @@ func ComputeTokenRenewalTimeWithRatio(creationTime, expirationTime time.Time, ra
renewalAt := creationTime.Add(renewalAfter)
return renewalAt
}

// oidcTrustConfig represents the configuration for an OIDC trust relationship.
// It includes the host of the Kubernetes API server, CA data for TLS verification,
// and the audience for the OIDC tokens.
type oidcTrustConfig struct {
// Host is the URL of the Kubernetes API server.
Host string `json:"host,omitempty"`
// CAData is the base64-encoded CA certificate data used to verify the server's TLS certificate.
CAData []byte `json:"caData,omitempty"`
}

// WriteOIDCConfigFromRESTConfig converts a RESTConfig to an OIDC trust configuration format.
// When creating a Kubernetes deployment, this configuration is used to set up the trust relationship to
// the target cluster.
// Example:
//
// spec:
//
// template:
// spec:
// volumes:
// - name: oidc-trust-config
// projected:
// sources:
// - secret:
// name: oidc-trust-config
// items:
// - key: host
// path: cluster/host
// - key: caData
// path: cluster/ca.crt
// - serviceAccountToken:
// audience: target-cluster
// path: cluster/token
// expirationSeconds: 3600
//
// volumeMounts:
// - name: oidc-trust-config
// mountPath: /var/run/secrets/oidc-trust-config
// readOnly: true
func WriteOIDCConfigFromRESTConfig(restConfig *rest.Config) ([]byte, error) {
oidcConfig := &oidcTrustConfig{
Host: restConfig.Host,
CAData: restConfig.CAData,
}

configMarshaled, err := yaml.Marshal(oidcConfig)
if err != nil {
return nil, fmt.Errorf("failed to write OIDC trust config: %w", err)
}

return configMarshaled, nil
}

// WriteKubeconfigFromRESTConfig converts the RESTConfig to a kubeconfig format.
// Supported authentication methods are Bearer Token, Username/Password and Client Certificate.
func WriteKubeconfigFromRESTConfig(restConfig *rest.Config) ([]byte, error) {
var authInfo *clientcmdapi.AuthInfo

id := "cluster"

type authType string
const (
authTypeBearerToken authType = "BearerToken"
authTypeBasicAuth authType = "BasicAuth"
authTypeClientCert authType = "ClientCert"
)
availableAuthTypes := make(map[authType]interface{})
if restConfig.BearerToken != "" {
availableAuthTypes[authTypeBearerToken] = nil
}

if restConfig.Username != "" && restConfig.Password != "" {
availableAuthTypes[authTypeBasicAuth] = nil
}

if restConfig.CertData != nil && restConfig.KeyData != nil {
availableAuthTypes[authTypeClientCert] = nil
}

if len(availableAuthTypes) == 0 {
return nil, fmt.Errorf("cannot write to kubeconfig when RESTConfig does not contain any supported authentication information")
}

if _, ok := availableAuthTypes[authTypeBearerToken]; ok {
authInfo = &clientcmdapi.AuthInfo{
Token: restConfig.BearerToken,
}
}

if _, ok := availableAuthTypes[authTypeBasicAuth]; ok {
authInfo = &clientcmdapi.AuthInfo{
Username: restConfig.Username,
Password: restConfig.Password,
}
}

if _, ok := availableAuthTypes[authTypeClientCert]; ok {
authInfo = &clientcmdapi.AuthInfo{
ClientCertificateData: restConfig.CertData,
ClientKeyData: restConfig.KeyData,
}
}

server := restConfig.Host
if restConfig.APIPath != "" {
server = fmt.Sprint(server, "/", restConfig.APIPath)
}

kubeConfig := clientcmdapi.Config{
CurrentContext: id,
Contexts: map[string]*clientcmdapi.Context{
id: {
AuthInfo: id,
Cluster: id,
},
},
Clusters: map[string]*clientcmdapi.Cluster{
id: {
Server: server,
CertificateAuthorityData: restConfig.CAData,
},
},
AuthInfos: map[string]*clientcmdapi.AuthInfo{
id: authInfo,
},
}

configMarshaled, err := clientcmd.Write(kubeConfig)
if err != nil {
return nil, fmt.Errorf("failed to write RESTConfig to kubeconfig: %w", err)
}

return configMarshaled, nil
}
74 changes: 74 additions & 0 deletions pkg/clusteraccess/access_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package clusteraccess_test

import (
"fmt"
"os"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"

corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
Expand Down Expand Up @@ -388,4 +394,72 @@ var _ = Describe("ClusterAccess", func() {

})

Context("Marshal RESTConfig", func() {
readRESTConfigFromKubeconfig := func(kubeconfig string) *rest.Config {
data, err := os.ReadFile(fmt.Sprint("./testdata/kubeconfig/", kubeconfig))
Expect(err).ToNot(HaveOccurred(), "failed to read kubeconfig file")

config, err := clientcmd.RESTConfigFromKubeConfig(data)
Expect(err).ToNot(HaveOccurred(), "failed to parse kubeconfig file")
return config
}

It("should create an OIDC config", func() {
restConfig := readRESTConfigFromKubeconfig("kubeconfig-token.yaml")

oidcConfigRaw, err := clusteraccess.WriteOIDCConfigFromRESTConfig(restConfig)
Expect(err).ToNot(HaveOccurred())
Expect(oidcConfigRaw).ToNot(BeEmpty())

var oidcConfig map[string]string
Expect(yaml.Unmarshal(oidcConfigRaw, &oidcConfig)).ToNot(HaveOccurred())
Expect(oidcConfig).To(HaveKeyWithValue("host", "https://test-server"))
Expect(oidcConfig)
})

It("should create a kubeconfig with token", func() {
restConfig := readRESTConfigFromKubeconfig("kubeconfig-token.yaml")

kubeconfigRaw, err := clusteraccess.WriteKubeconfigFromRESTConfig(restConfig)
Expect(err).ToNot(HaveOccurred())
Expect(kubeconfigRaw).ToNot(BeEmpty())

config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigRaw)
Expect(err).ToNot(HaveOccurred())
Expect(config.Host).To(Equal("https://test-server"))
Expect(config.TLSClientConfig.CAData).ToNot(BeEmpty())
Expect(config.BearerToken).To(Equal("dGVzdC10b2tlbg=="))
})

It("should create a kubeconfig with basic auth", func() {
restConfig := readRESTConfigFromKubeconfig("kubeconfig-basicauth.yaml")

kubeconfigRaw, err := clusteraccess.WriteKubeconfigFromRESTConfig(restConfig)
Expect(err).ToNot(HaveOccurred())
Expect(kubeconfigRaw).ToNot(BeEmpty())

config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigRaw)
Expect(err).ToNot(HaveOccurred())
Expect(config.Host).To(Equal("https://test-server"))
Expect(config.TLSClientConfig.CAData).ToNot(BeEmpty())
Expect(config.Username).To(Equal("foo"))
Expect(config.Password).To(Equal("bar"))
})

It("should create a kubeconfig with client tls", func() {
restConfig := readRESTConfigFromKubeconfig("kubeconfig-tls.yaml")

kubeconfigRaw, err := clusteraccess.WriteKubeconfigFromRESTConfig(restConfig)
Expect(err).ToNot(HaveOccurred())
Expect(kubeconfigRaw).ToNot(BeEmpty())

config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigRaw)
Expect(err).ToNot(HaveOccurred())
Expect(config.Host).To(Equal("https://test-server"))
Expect(config.TLSClientConfig.CAData).ToNot(BeEmpty())
Expect(config.TLSClientConfig.CertData).ToNot(BeEmpty())
Expect(config.TLSClientConfig.KeyData).ToNot(BeEmpty())
})
})

})
21 changes: 21 additions & 0 deletions pkg/clusteraccess/testdata/kubeconfig/kubeconfig-basicauth.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://test-server
certificate-authority-data: dGVzdC1jYS1kYXRh

contexts:
- name: test-context
context:
cluster: test-cluster
user: test-auth

current-context: test-context

users:
- name: test-auth
user:
username: foo
password: bar
21 changes: 21 additions & 0 deletions pkg/clusteraccess/testdata/kubeconfig/kubeconfig-tls.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://test-server
certificate-authority-data: dGVzdC1jYS1kYXRh

contexts:
- name: test-context
context:
cluster: test-cluster
user: test-auth

current-context: test-context

users:
- name: test-auth
user:
client-certificate-data: dGVzdC1jYS1jZXJ0aWZpY2F0ZQ==
client-key-data: dGVzdC1jYS1rZXk=
20 changes: 20 additions & 0 deletions pkg/clusteraccess/testdata/kubeconfig/kubeconfig-token.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://test-server
certificate-authority-data: dGVzdC1jYS1kYXRh

contexts:
- name: test-context
context:
cluster: test-cluster
user: test-auth

current-context: test-context

users:
- name: test-auth
user:
token: dGVzdC10b2tlbg==
24 changes: 24 additions & 0 deletions pkg/clusters/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package clusters
import (
"fmt"

"github.com/openmcp-project/controller-utils/pkg/clusteraccess"

flag "github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -235,3 +237,25 @@ func (c *Cluster) APIServerEndpoint() string {
}
return c.restCfg.Host
}

/////////////////
// Serializing //
/////////////////

// WriteKubeconfig writes the cluster's kubeconfig to a byte slice.
// see clusteraccess.WriteKubeconfigFromRESTConfig for details.
func (c *Cluster) WriteKubeconfig() ([]byte, error) {
if c.restCfg == nil {
return nil, fmt.Errorf("cannot write kubeconfig for cluster when REST config is not set")
}
return clusteraccess.WriteKubeconfigFromRESTConfig(c.restCfg)
}

// WriteOIDCConfig writes the cluster's OIDC config to a byte slice.
// see clusteraccess.WriteOIDCConfigFromRESTConfig for details.
func (c *Cluster) WriteOIDCConfig() ([]byte, error) {
if c.restCfg == nil {
return nil, fmt.Errorf("cannot write OIDC config for cluster when REST config is not set")
}
return clusteraccess.WriteOIDCConfigFromRESTConfig(c.restCfg)
}