From d8cd7736fca21410cdc97fdecbf6f00e8b7f2f45 Mon Sep 17 00:00:00 2001 From: xuezhaojun Date: Wed, 8 Dec 2021 17:44:16 +0800 Subject: [PATCH] add sa syncer (#176) Signed-off-by: xuezhaojun --- pkg/helpers/sa_syncer.go | 111 ++++++++++++++++ pkg/helpers/sa_syncer_test.go | 242 ++++++++++++++++++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 pkg/helpers/sa_syncer.go create mode 100644 pkg/helpers/sa_syncer_test.go diff --git a/pkg/helpers/sa_syncer.go b/pkg/helpers/sa_syncer.go new file mode 100644 index 000000000..ab6ff1fb7 --- /dev/null +++ b/pkg/helpers/sa_syncer.go @@ -0,0 +1,111 @@ +package helpers + +import ( + "context" + "fmt" + "strings" + + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/resource/resourceapply" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +// EnsureSAToken get the saToken of target sa and ensure it was rendered as expected. +// A usage example combined with RenderToKubeconfigSecert would be like the following: +// ... +// saName := clusterManagerName+"-registration-controller-sa" +// secretName := saName+"-kubeconfig" +// err := helpers.EnsureSAToken(ctx, saName, clustermanagerNamespace, amdinKubeClient, +// helpers.RenderToKubeconfigSecret(secretName, clustermanagerNamespace, AdminKubeconfig, kubeClient, recorder)) // kubeClient used to create secret; normally recorder is from controller object. +// if err != nil { +// return err +// } +// ... +func EnsureSAToken(ctx context.Context, saName, saNamespace string, client kubernetes.Interface, renderSAToken func([]byte) error) error { + // get the service account + sa, err := client.CoreV1().ServiceAccounts(saNamespace).Get(ctx, saName, metav1.GetOptions{}) + if err != nil { + return err + } + if len(sa.Secrets) == 0 { + return fmt.Errorf("token secret for %s not exist yet", saName) + } + + prefix := saName + if len(prefix) > 63 { + prefix = prefix[:37] + } + + for _, secret := range sa.Secrets { + if strings.HasPrefix(secret.Name, prefix) && strings.Contains(secret.Name, "token") { + tokenSecretName := secret.Name + + // get the token secret + tokenSecret, err := client.CoreV1().Secrets(saNamespace).Get(ctx, tokenSecretName, metav1.GetOptions{}) + if err != nil { + return err + } + + if tokenSecret.Type != corev1.SecretTypeServiceAccountToken { + continue + } + + saToken, ok := tokenSecret.Data["token"] + if !ok { + return fmt.Errorf("no token in data for secret %s", tokenSecretName) + } + + return renderSAToken(saToken) + } + } + + return fmt.Errorf("no token secret for this service account %s", sa.Name) +} + +// RenderToKubeconfigSecret would render saToken to a secret. +func RenderToKubeconfigSecret(secretName, secretNamespace string, templateKubeconfig *rest.Config, client coreclientv1.SecretsGetter, recorder events.Recorder) func([]byte) error { + return func(saToken []byte) error { + kubeconfigContent, err := clientcmd.Write(clientcmdapi.Config{ + Kind: "Config", + APIVersion: "v1", + Clusters: map[string]*clientcmdapi.Cluster{ + "cluster": { + Server: templateKubeconfig.Host, + CertificateAuthorityData: templateKubeconfig.CAData, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "context": { + Cluster: "cluster", + AuthInfo: "user", + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "user": { + Token: string(saToken), + }, + }, + CurrentContext: "context", + }) + if err != nil { + return err + } + _, _, err = resourceapply.ApplySecret(client, recorder, &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: secretNamespace, + Name: secretName, + }, + Data: map[string][]byte{ + "kubeconfig": kubeconfigContent, + }, + }) + return err + } +} diff --git a/pkg/helpers/sa_syncer_test.go b/pkg/helpers/sa_syncer_test.go new file mode 100644 index 000000000..05ee96e99 --- /dev/null +++ b/pkg/helpers/sa_syncer_test.go @@ -0,0 +1,242 @@ +package helpers + +import ( + "context" + "fmt" + "testing" + + "github.com/openshift/library-go/pkg/operator/events" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + testclient "k8s.io/client-go/kubernetes/fake" + coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" +) + +func TestEnsureSAToken(t *testing.T) { + type args struct { + ctx context.Context + saName string + saNamespace string + renderSAToken func([]byte) error + client kubernetes.Interface + } + + simpleRender := func(data []byte) error { + expected := "sa-token" + if expected != string(data) { + return fmt.Errorf("render result not as expected, data: %q", data) + } + return nil + } + + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Secrets: []corev1.ObjectReference{ + { + Name: "test-token", + Namespace: "test", + }, + }, + } + + wrongNameSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Type: corev1.SecretTypeServiceAccountToken, + Data: map[string][]byte{ + "token": []byte("sa-token"), + }, + } + + wrongTypeSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-token", + Namespace: "test", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + "token": []byte("sa-token"), + }, + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-token", + Namespace: "test", + }, + Type: corev1.SecretTypeServiceAccountToken, + Data: map[string][]byte{ + "token": []byte("sa-token"), + }, + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "sa doesn't exist", + args: args{ + saName: "test", + saNamespace: "test", + renderSAToken: simpleRender, + client: testclient.NewSimpleClientset(), + }, + wantErr: true, + }, + { + name: "sa exist, but token secret doesn't exist", + args: args{ + saName: "test", + saNamespace: "test", + renderSAToken: simpleRender, + client: testclient.NewSimpleClientset(sa), + }, + wantErr: true, + }, { + name: "no token secret", + args: args{ + saName: "test", + saNamespace: "test", + renderSAToken: simpleRender, + client: testclient.NewSimpleClientset(sa, wrongNameSecret), + }, + wantErr: true, + }, { + name: "no token secret", + args: args{ + saName: "test", + saNamespace: "test", + renderSAToken: simpleRender, + client: testclient.NewSimpleClientset(sa, wrongTypeSecret), + }, + wantErr: true, + }, + { + name: "success", + args: args{ + saName: "test", + saNamespace: "test", + renderSAToken: simpleRender, + client: testclient.NewSimpleClientset(sa, secret), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := EnsureSAToken(tt.args.ctx, tt.args.saName, tt.args.saNamespace, tt.args.client, tt.args.renderSAToken); (err != nil) != tt.wantErr { + t.Errorf("EnsureKubeconfigFromSA() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestRenderToKubeconfigSecret(t *testing.T) { + type args struct { + secretName string + secretNamespace string + templateKubeconfig *rest.Config + client coreclientv1.SecretsGetter + recorder events.Recorder + } + + tkc := &rest.Config{ + Host: "host", + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte("caData"), + }, + } + + client := testclient.NewSimpleClientset(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secretNamespace", + }, + }) + + tests := []struct { + name string + args args + }{ + { + name: "the secret content should be the content of a kubeconfig", + args: args{ + secretName: "secretName", + secretNamespace: "secretNamespace", + templateKubeconfig: tkc, + client: client.CoreV1(), + recorder: newTestingEventRecorder(t), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + render := RenderToKubeconfigSecret(tt.args.secretName, tt.args.secretNamespace, tt.args.templateKubeconfig, tt.args.client, tt.args.recorder) + err := render([]byte("sa-token")) + if err != nil { + t.Errorf("renderSaToken, err should be nil but got %s", err.Error()) + return + } + secret, err := client.CoreV1().Secrets("secretNamespace").Get(context.Background(), "secretName", metav1.GetOptions{}) + if err != nil { + t.Errorf("get secret, err should be nil but got %s", err.Error()) + return + } + if kubeconfigContent, ok := secret.Data["kubeconfig"]; !ok { + t.Errorf("kubeconfig data not exist") + return + } else if string(kubeconfigContent) != "apiVersion: v1\nclusters:\n- cluster:\n certificate-authority-data: Y2FEYXRh\n server: host\n name: cluster\ncontexts:\n- context:\n cluster: cluster\n user: user\n name: context\ncurrent-context: context\nkind: Config\npreferences: {}\nusers:\n- name: user\n user:\n token: sa-token\n" { + t.Errorf("kubeconfig data doesn't correct, got %q", string(kubeconfigContent)) + return + } + }) + } +} + +type testingEventRecorder struct { + t *testing.T + component string +} + +// NewTestingEventRecorder provides event recorder that will log all recorded events to the error log. +func newTestingEventRecorder(t *testing.T) events.Recorder { + return &testingEventRecorder{t: t, component: "test"} +} + +func (r *testingEventRecorder) ComponentName() string { + return r.component +} + +func (r *testingEventRecorder) ForComponent(c string) events.Recorder { + return &testingEventRecorder{t: r.t, component: c} +} + +func (r *testingEventRecorder) Shutdown() {} + +func (r *testingEventRecorder) WithComponentSuffix(suffix string) events.Recorder { + return r.ForComponent(fmt.Sprintf("%s-%s", r.ComponentName(), suffix)) +} + +func (r *testingEventRecorder) Event(reason, message string) { + r.t.Logf("Event: %v: %v", reason, message) +} + +func (r *testingEventRecorder) Eventf(reason, messageFmt string, args ...interface{}) { + r.Event(reason, fmt.Sprintf(messageFmt, args...)) +} + +func (r *testingEventRecorder) Warning(reason, message string) { + r.t.Logf("Warning: %v: %v", reason, message) +} + +func (r *testingEventRecorder) Warningf(reason, messageFmt string, args ...interface{}) { + r.Warning(reason, fmt.Sprintf(messageFmt, args...)) +}