Skip to content

Commit b1499cb

Browse files
authored
feat: add clusters marshal (#64)
1 parent 568d3c7 commit b1499cb

File tree

6 files changed

+297
-0
lines changed

6 files changed

+297
-0
lines changed

pkg/clusteraccess/access.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"time"
77

8+
"sigs.k8s.io/yaml"
9+
810
authenticationv1 "k8s.io/api/authentication/v1"
911
corev1 "k8s.io/api/core/v1"
1012
rbacv1 "k8s.io/api/rbac/v1"
@@ -341,3 +343,138 @@ func ComputeTokenRenewalTimeWithRatio(creationTime, expirationTime time.Time, ra
341343
renewalAt := creationTime.Add(renewalAfter)
342344
return renewalAt
343345
}
346+
347+
// oidcTrustConfig represents the configuration for an OIDC trust relationship.
348+
// It includes the host of the Kubernetes API server, CA data for TLS verification,
349+
// and the audience for the OIDC tokens.
350+
type oidcTrustConfig struct {
351+
// Host is the URL of the Kubernetes API server.
352+
Host string `json:"host,omitempty"`
353+
// CAData is the base64-encoded CA certificate data used to verify the server's TLS certificate.
354+
CAData []byte `json:"caData,omitempty"`
355+
}
356+
357+
// WriteOIDCConfigFromRESTConfig converts a RESTConfig to an OIDC trust configuration format.
358+
// When creating a Kubernetes deployment, this configuration is used to set up the trust relationship to
359+
// the target cluster.
360+
// Example:
361+
//
362+
// spec:
363+
//
364+
// template:
365+
// spec:
366+
// volumes:
367+
// - name: oidc-trust-config
368+
// projected:
369+
// sources:
370+
// - secret:
371+
// name: oidc-trust-config
372+
// items:
373+
// - key: host
374+
// path: cluster/host
375+
// - key: caData
376+
// path: cluster/ca.crt
377+
// - serviceAccountToken:
378+
// audience: target-cluster
379+
// path: cluster/token
380+
// expirationSeconds: 3600
381+
//
382+
// volumeMounts:
383+
// - name: oidc-trust-config
384+
// mountPath: /var/run/secrets/oidc-trust-config
385+
// readOnly: true
386+
func WriteOIDCConfigFromRESTConfig(restConfig *rest.Config) ([]byte, error) {
387+
oidcConfig := &oidcTrustConfig{
388+
Host: restConfig.Host,
389+
CAData: restConfig.CAData,
390+
}
391+
392+
configMarshaled, err := yaml.Marshal(oidcConfig)
393+
if err != nil {
394+
return nil, fmt.Errorf("failed to write OIDC trust config: %w", err)
395+
}
396+
397+
return configMarshaled, nil
398+
}
399+
400+
// WriteKubeconfigFromRESTConfig converts the RESTConfig to a kubeconfig format.
401+
// Supported authentication methods are Bearer Token, Username/Password and Client Certificate.
402+
func WriteKubeconfigFromRESTConfig(restConfig *rest.Config) ([]byte, error) {
403+
var authInfo *clientcmdapi.AuthInfo
404+
405+
id := "cluster"
406+
407+
type authType string
408+
const (
409+
authTypeBearerToken authType = "BearerToken"
410+
authTypeBasicAuth authType = "BasicAuth"
411+
authTypeClientCert authType = "ClientCert"
412+
)
413+
availableAuthTypes := make(map[authType]interface{})
414+
if restConfig.BearerToken != "" {
415+
availableAuthTypes[authTypeBearerToken] = nil
416+
}
417+
418+
if restConfig.Username != "" && restConfig.Password != "" {
419+
availableAuthTypes[authTypeBasicAuth] = nil
420+
}
421+
422+
if restConfig.CertData != nil && restConfig.KeyData != nil {
423+
availableAuthTypes[authTypeClientCert] = nil
424+
}
425+
426+
if len(availableAuthTypes) == 0 {
427+
return nil, fmt.Errorf("cannot write to kubeconfig when RESTConfig does not contain any supported authentication information")
428+
}
429+
430+
if _, ok := availableAuthTypes[authTypeBearerToken]; ok {
431+
authInfo = &clientcmdapi.AuthInfo{
432+
Token: restConfig.BearerToken,
433+
}
434+
}
435+
436+
if _, ok := availableAuthTypes[authTypeBasicAuth]; ok {
437+
authInfo = &clientcmdapi.AuthInfo{
438+
Username: restConfig.Username,
439+
Password: restConfig.Password,
440+
}
441+
}
442+
443+
if _, ok := availableAuthTypes[authTypeClientCert]; ok {
444+
authInfo = &clientcmdapi.AuthInfo{
445+
ClientCertificateData: restConfig.CertData,
446+
ClientKeyData: restConfig.KeyData,
447+
}
448+
}
449+
450+
server := restConfig.Host
451+
if restConfig.APIPath != "" {
452+
server = fmt.Sprint(server, "/", restConfig.APIPath)
453+
}
454+
455+
kubeConfig := clientcmdapi.Config{
456+
CurrentContext: id,
457+
Contexts: map[string]*clientcmdapi.Context{
458+
id: {
459+
AuthInfo: id,
460+
Cluster: id,
461+
},
462+
},
463+
Clusters: map[string]*clientcmdapi.Cluster{
464+
id: {
465+
Server: server,
466+
CertificateAuthorityData: restConfig.CAData,
467+
},
468+
},
469+
AuthInfos: map[string]*clientcmdapi.AuthInfo{
470+
id: authInfo,
471+
},
472+
}
473+
474+
configMarshaled, err := clientcmd.Write(kubeConfig)
475+
if err != nil {
476+
return nil, fmt.Errorf("failed to write RESTConfig to kubeconfig: %w", err)
477+
}
478+
479+
return configMarshaled, nil
480+
}

pkg/clusteraccess/access_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package clusteraccess_test
22

33
import (
4+
"fmt"
5+
"os"
6+
47
. "github.com/onsi/ginkgo/v2"
58
. "github.com/onsi/gomega"
9+
"k8s.io/client-go/rest"
10+
"k8s.io/client-go/tools/clientcmd"
611
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/yaml"
713

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

389395
})
390396

397+
Context("Marshal RESTConfig", func() {
398+
readRESTConfigFromKubeconfig := func(kubeconfig string) *rest.Config {
399+
data, err := os.ReadFile(fmt.Sprint("./testdata/kubeconfig/", kubeconfig))
400+
Expect(err).ToNot(HaveOccurred(), "failed to read kubeconfig file")
401+
402+
config, err := clientcmd.RESTConfigFromKubeConfig(data)
403+
Expect(err).ToNot(HaveOccurred(), "failed to parse kubeconfig file")
404+
return config
405+
}
406+
407+
It("should create an OIDC config", func() {
408+
restConfig := readRESTConfigFromKubeconfig("kubeconfig-token.yaml")
409+
410+
oidcConfigRaw, err := clusteraccess.WriteOIDCConfigFromRESTConfig(restConfig)
411+
Expect(err).ToNot(HaveOccurred())
412+
Expect(oidcConfigRaw).ToNot(BeEmpty())
413+
414+
var oidcConfig map[string]string
415+
Expect(yaml.Unmarshal(oidcConfigRaw, &oidcConfig)).ToNot(HaveOccurred())
416+
Expect(oidcConfig).To(HaveKeyWithValue("host", "https://test-server"))
417+
Expect(oidcConfig)
418+
})
419+
420+
It("should create a kubeconfig with token", func() {
421+
restConfig := readRESTConfigFromKubeconfig("kubeconfig-token.yaml")
422+
423+
kubeconfigRaw, err := clusteraccess.WriteKubeconfigFromRESTConfig(restConfig)
424+
Expect(err).ToNot(HaveOccurred())
425+
Expect(kubeconfigRaw).ToNot(BeEmpty())
426+
427+
config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigRaw)
428+
Expect(err).ToNot(HaveOccurred())
429+
Expect(config.Host).To(Equal("https://test-server"))
430+
Expect(config.TLSClientConfig.CAData).ToNot(BeEmpty())
431+
Expect(config.BearerToken).To(Equal("dGVzdC10b2tlbg=="))
432+
})
433+
434+
It("should create a kubeconfig with basic auth", func() {
435+
restConfig := readRESTConfigFromKubeconfig("kubeconfig-basicauth.yaml")
436+
437+
kubeconfigRaw, err := clusteraccess.WriteKubeconfigFromRESTConfig(restConfig)
438+
Expect(err).ToNot(HaveOccurred())
439+
Expect(kubeconfigRaw).ToNot(BeEmpty())
440+
441+
config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigRaw)
442+
Expect(err).ToNot(HaveOccurred())
443+
Expect(config.Host).To(Equal("https://test-server"))
444+
Expect(config.TLSClientConfig.CAData).ToNot(BeEmpty())
445+
Expect(config.Username).To(Equal("foo"))
446+
Expect(config.Password).To(Equal("bar"))
447+
})
448+
449+
It("should create a kubeconfig with client tls", func() {
450+
restConfig := readRESTConfigFromKubeconfig("kubeconfig-tls.yaml")
451+
452+
kubeconfigRaw, err := clusteraccess.WriteKubeconfigFromRESTConfig(restConfig)
453+
Expect(err).ToNot(HaveOccurred())
454+
Expect(kubeconfigRaw).ToNot(BeEmpty())
455+
456+
config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigRaw)
457+
Expect(err).ToNot(HaveOccurred())
458+
Expect(config.Host).To(Equal("https://test-server"))
459+
Expect(config.TLSClientConfig.CAData).ToNot(BeEmpty())
460+
Expect(config.TLSClientConfig.CertData).ToNot(BeEmpty())
461+
Expect(config.TLSClientConfig.KeyData).ToNot(BeEmpty())
462+
})
463+
})
464+
391465
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: v1
2+
kind: Config
3+
clusters:
4+
- name: test-cluster
5+
cluster:
6+
server: https://test-server
7+
certificate-authority-data: dGVzdC1jYS1kYXRh
8+
9+
contexts:
10+
- name: test-context
11+
context:
12+
cluster: test-cluster
13+
user: test-auth
14+
15+
current-context: test-context
16+
17+
users:
18+
- name: test-auth
19+
user:
20+
username: foo
21+
password: bar
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: v1
2+
kind: Config
3+
clusters:
4+
- name: test-cluster
5+
cluster:
6+
server: https://test-server
7+
certificate-authority-data: dGVzdC1jYS1kYXRh
8+
9+
contexts:
10+
- name: test-context
11+
context:
12+
cluster: test-cluster
13+
user: test-auth
14+
15+
current-context: test-context
16+
17+
users:
18+
- name: test-auth
19+
user:
20+
client-certificate-data: dGVzdC1jYS1jZXJ0aWZpY2F0ZQ==
21+
client-key-data: dGVzdC1jYS1rZXk=
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: v1
2+
kind: Config
3+
clusters:
4+
- name: test-cluster
5+
cluster:
6+
server: https://test-server
7+
certificate-authority-data: dGVzdC1jYS1kYXRh
8+
9+
contexts:
10+
- name: test-context
11+
context:
12+
cluster: test-cluster
13+
user: test-auth
14+
15+
current-context: test-context
16+
17+
users:
18+
- name: test-auth
19+
user:
20+
token: dGVzdC10b2tlbg==

pkg/clusters/cluster.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package clusters
33
import (
44
"fmt"
55

6+
"github.com/openmcp-project/controller-utils/pkg/clusteraccess"
7+
68
flag "github.com/spf13/pflag"
79
"k8s.io/apimachinery/pkg/runtime"
810
"k8s.io/client-go/rest"
@@ -235,3 +237,25 @@ func (c *Cluster) APIServerEndpoint() string {
235237
}
236238
return c.restCfg.Host
237239
}
240+
241+
/////////////////
242+
// Serializing //
243+
/////////////////
244+
245+
// WriteKubeconfig writes the cluster's kubeconfig to a byte slice.
246+
// see clusteraccess.WriteKubeconfigFromRESTConfig for details.
247+
func (c *Cluster) WriteKubeconfig() ([]byte, error) {
248+
if c.restCfg == nil {
249+
return nil, fmt.Errorf("cannot write kubeconfig for cluster when REST config is not set")
250+
}
251+
return clusteraccess.WriteKubeconfigFromRESTConfig(c.restCfg)
252+
}
253+
254+
// WriteOIDCConfig writes the cluster's OIDC config to a byte slice.
255+
// see clusteraccess.WriteOIDCConfigFromRESTConfig for details.
256+
func (c *Cluster) WriteOIDCConfig() ([]byte, error) {
257+
if c.restCfg == nil {
258+
return nil, fmt.Errorf("cannot write OIDC config for cluster when REST config is not set")
259+
}
260+
return clusteraccess.WriteOIDCConfigFromRESTConfig(c.restCfg)
261+
}

0 commit comments

Comments
 (0)