feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
249
pkg/controller/kubeconfig/kubeconfig_controller.go
Normal file
249
pkg/controller/kubeconfig/kubeconfig_controller.go
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/models/kubeconfig"
|
||||
"kubesphere.io/kubesphere/pkg/utils/pkiutil"
|
||||
|
||||
kscontroller "kubesphere.io/kubesphere/pkg/controller"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
controllerName = "kubeconfig"
|
||||
residual = 30 * 24 * time.Hour
|
||||
)
|
||||
|
||||
var _ kscontroller.Controller = &Reconciler{}
|
||||
var _ reconcile.Reconciler = &Reconciler{}
|
||||
|
||||
// Reconciler reconciles a User object
|
||||
type Reconciler struct {
|
||||
client.Client
|
||||
config *rest.Config
|
||||
}
|
||||
|
||||
func (r *Reconciler) Name() string {
|
||||
return controllerName
|
||||
}
|
||||
|
||||
func (r *Reconciler) SetupWithManager(mgr *kscontroller.Manager) error {
|
||||
r.Client = mgr.GetClient()
|
||||
r.config = mgr.K8sClient.Config()
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named(controllerName).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: 1}).
|
||||
For(&corev1.Secret{},
|
||||
builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
secret := object.(*corev1.Secret)
|
||||
return secret.Namespace == constants.KubeSphereNamespace &&
|
||||
secret.Type == kubeconfig.SecretTypeKubeConfig &&
|
||||
secret.Labels[constants.UsernameLabelKey] != ""
|
||||
}))).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
|
||||
secret := &corev1.Secret{}
|
||||
if err := r.Get(ctx, req.NamespacedName, secret); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if !secret.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if err := r.UpdateSecret(ctx, secret); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *Reconciler) UpdateSecret(ctx context.Context, secret *corev1.Secret) error {
|
||||
// already exist and cert will not expire in 3 days
|
||||
if isValid(secret) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// create a new CSR
|
||||
var ca []byte
|
||||
var err error
|
||||
if len(r.config.CAData) > 0 {
|
||||
ca = r.config.CAData
|
||||
} else {
|
||||
ca, err = os.ReadFile(kubeconfig.InClusterCAFilePath)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to read CA file: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
username := secret.Labels[constants.UsernameLabelKey]
|
||||
|
||||
currentContext := fmt.Sprintf("%s@%s", username, kubeconfig.DefaultClusterName)
|
||||
config := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Preferences: clientcmdapi.Preferences{},
|
||||
Clusters: map[string]*clientcmdapi.Cluster{kubeconfig.DefaultClusterName: {
|
||||
Server: r.config.Host,
|
||||
InsecureSkipTLSVerify: false,
|
||||
CertificateAuthorityData: ca,
|
||||
}},
|
||||
Contexts: map[string]*clientcmdapi.Context{currentContext: {
|
||||
Cluster: kubeconfig.DefaultClusterName,
|
||||
AuthInfo: username,
|
||||
Namespace: kubeconfig.DefaultNamespace,
|
||||
}},
|
||||
AuthInfos: make(map[string]*clientcmdapi.AuthInfo),
|
||||
CurrentContext: currentContext,
|
||||
}
|
||||
|
||||
data, err := clientcmd.Write(config)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to write kubeconfig for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
secret.Data = map[string][]byte{kubeconfig.FileName: data}
|
||||
if err = r.Update(ctx, secret); err != nil {
|
||||
klog.Errorf("Failed to update kubeconfig for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.createCSR(ctx, username); err != nil {
|
||||
klog.Errorf("Failed to create CSR for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValid(secret *corev1.Secret) bool {
|
||||
username := secret.Labels[constants.UsernameLabelKey]
|
||||
|
||||
data := secret.Data[kubeconfig.FileName]
|
||||
if len(data) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
config, err := clientcmd.Load(data)
|
||||
if err != nil {
|
||||
klog.Warningf("Failed to load kubeconfig for user %s: %v", username, err)
|
||||
return false
|
||||
}
|
||||
|
||||
if authInfo, ok := config.AuthInfos[username]; ok {
|
||||
clientCert, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData)
|
||||
if err != nil {
|
||||
klog.Warningf("Failed to parse client certificate for user %s: %v", username, err)
|
||||
return false
|
||||
}
|
||||
for _, cert := range clientCert {
|
||||
if cert.NotAfter.After(time.Now().Add(residual)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// in process
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Reconciler) createCSR(ctx context.Context, username string) error {
|
||||
csrConfig := &certutil.Config{
|
||||
CommonName: username,
|
||||
Organization: nil,
|
||||
AltNames: certutil.AltNames{},
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
|
||||
x509csr, x509key, err := pkiutil.NewCSRAndKey(csrConfig)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to create CSR and key for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var csrBuffer, keyBuffer bytes.Buffer
|
||||
if err = pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(x509key)}); err != nil {
|
||||
klog.Errorf("Failed to encode private key for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var csrBytes []byte
|
||||
if csrBytes, err = x509.CreateCertificateRequest(rand.Reader, x509csr, x509key); err != nil {
|
||||
klog.Errorf("Failed to create CSR for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = pem.Encode(&csrBuffer, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}); err != nil {
|
||||
klog.Errorf("Failed to encode CSR for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
csr := csrBuffer.Bytes()
|
||||
key := keyBuffer.Bytes()
|
||||
csrName := fmt.Sprintf("%s-csr-%d", username, time.Now().Unix())
|
||||
k8sCSR := &certificatesv1.CertificateSigningRequest{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "CertificateSigningRequest",
|
||||
APIVersion: "certificates.k8s.io/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: csrName,
|
||||
Labels: map[string]string{constants.UsernameLabelKey: username},
|
||||
Annotations: map[string]string{kubeconfig.PrivateKeyAnnotation: string(key)},
|
||||
},
|
||||
Spec: certificatesv1.CertificateSigningRequestSpec{
|
||||
Request: csr,
|
||||
SignerName: certificatesv1.KubeAPIServerClientSignerName,
|
||||
Usages: []certificatesv1.KeyUsage{certificatesv1.UsageKeyEncipherment, certificatesv1.UsageClientAuth, certificatesv1.UsageDigitalSignature},
|
||||
Username: username,
|
||||
Groups: []string{user.AllAuthenticated},
|
||||
},
|
||||
}
|
||||
|
||||
if err = r.Create(ctx, k8sCSR); err != nil {
|
||||
klog.Errorf("Failed to create CSR for user %s: %v", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user