mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-05-11 11:48:33 +00:00
111
pkg/helpers/sa_syncer.go
Normal file
111
pkg/helpers/sa_syncer.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
242
pkg/helpers/sa_syncer_test.go
Normal file
242
pkg/helpers/sa_syncer_test.go
Normal file
@@ -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...))
|
||||
}
|
||||
Reference in New Issue
Block a user