diff --git a/pkg/controller/cluster/cluster_controller.go b/pkg/controller/cluster/cluster_controller.go index 3323742a7..49a911e8e 100644 --- a/pkg/controller/cluster/cluster_controller.go +++ b/pkg/controller/cluster/cluster_controller.go @@ -19,7 +19,9 @@ package cluster import ( "bytes" "context" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "net/http" "reflect" @@ -52,6 +54,7 @@ import ( clusterclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/cluster/v1alpha1" clusterinformer "kubesphere.io/kubesphere/pkg/client/informers/externalversions/cluster/v1alpha1" clusterlister "kubesphere.io/kubesphere/pkg/client/listers/cluster/v1alpha1" + "kubesphere.io/kubesphere/pkg/utils/k8sutil" "kubesphere.io/kubesphere/pkg/version" ) @@ -614,6 +617,11 @@ func (c *clusterController) syncCluster(key string) error { } c.updateClusterCondition(cluster, readyConditon) + if err = c.updateKubeConfigExpirationDateCondition(cluster); err != nil { + klog.Errorf("sync KubeConfig expiration date for cluster %s failed: %v", cluster.Name, err) + return err + } + if !reflect.DeepEqual(oldCluster, cluster) { _, err = c.clusterClient.Update(context.TODO(), cluster, metav1.UpdateOptions{}) if err != nil { @@ -823,3 +831,52 @@ func (c *clusterController) unJoinFederation(clusterConfig *rest.Config, unjoini } } } + +func parseKubeConfigExpirationDate(kubeconfig []byte) (time.Time, error) { + config, err := k8sutil.LoadKubeConfigFromBytes(kubeconfig) + if err != nil { + return time.Time{}, err + } + if config.CertData == nil { + return time.Time{}, fmt.Errorf("empty CertData") + } + block, _ := pem.Decode(config.CertData) + if block == nil { + return time.Time{}, fmt.Errorf("pem.Decode failed, got empty block data") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return time.Time{}, err + } + return cert.NotAfter, nil +} + +func (c *clusterController) updateKubeConfigExpirationDateCondition(cluster *clusterv1alpha1.Cluster) error { + if _, ok := cluster.Labels[clusterv1alpha1.HostCluster]; ok { + return nil + } + // we don't need to check member clusters which using proxy mode, their certs are managed and will be renewed by tower. + if cluster.Spec.Connection.Type == clusterv1alpha1.ConnectionTypeProxy { + return nil + } + + klog.V(5).Infof("sync KubeConfig expiration date for cluster %s", cluster.Name) + notAfter, err := parseKubeConfigExpirationDate(cluster.Spec.Connection.KubeConfig) + if err != nil { + return fmt.Errorf("parseKubeConfigExpirationDate for cluster %s failed: %v", cluster.Name, err) + } + expiresInSevenDays := v1.ConditionFalse + if time.Now().AddDate(0, 0, 7).Sub(notAfter) > 0 { + expiresInSevenDays = v1.ConditionTrue + } + + c.updateClusterCondition(cluster, clusterv1alpha1.ClusterCondition{ + Type: clusterv1alpha1.ClusterKubeConfigCertExpiresInSevenDays, + Status: expiresInSevenDays, + LastUpdateTime: metav1.Now(), + LastTransitionTime: metav1.Now(), + Reason: string(clusterv1alpha1.ClusterKubeConfigCertExpiresInSevenDays), + Message: notAfter.String(), + }) + return nil +} diff --git a/pkg/kapis/cluster/v1alpha1/handler.go b/pkg/kapis/cluster/v1alpha1/handler.go index 9762a3482..be3cb7f66 100644 --- a/pkg/kapis/cluster/v1alpha1/handler.go +++ b/pkg/kapis/cluster/v1alpha1/handler.go @@ -41,8 +41,6 @@ import ( "k8s.io/client-go/kubernetes" v1 "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "kubesphere.io/api/cluster/v1alpha1" "kubesphere.io/kubesphere/pkg/api" @@ -51,6 +49,7 @@ import ( kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" "kubesphere.io/kubesphere/pkg/client/informers/externalversions" clusterlister "kubesphere.io/kubesphere/pkg/client/listers/cluster/v1alpha1" + "kubesphere.io/kubesphere/pkg/utils/k8sutil" "kubesphere.io/kubesphere/pkg/version" ) @@ -286,7 +285,7 @@ func (h *handler) updateKubeConfig(request *restful.Request, response *restful.R api.HandleBadRequest(response, request, fmt.Errorf("cluster kubeconfig MUST NOT be empty")) return } - config, err := loadKubeConfigFromBytes(req.KubeConfig) + config, err := k8sutil.LoadKubeConfigFromBytes(req.KubeConfig) if err != nil { api.HandleBadRequest(response, request, err) return @@ -363,7 +362,7 @@ func (h *handler) validateCluster(request *restful.Request, response *restful.Re // validateKubeConfig takes base64 encoded kubeconfig and check its validity func (h *handler) validateKubeConfig(kubeconfig []byte) error { - config, err := loadKubeConfigFromBytes(kubeconfig) + config, err := k8sutil.LoadKubeConfigFromBytes(kubeconfig) if err != nil { return err } @@ -393,20 +392,6 @@ func (h *handler) validateKubeConfig(kubeconfig []byte) error { return err } -func loadKubeConfigFromBytes(kubeconfig []byte) (*rest.Config, error) { - clientConfig, err := clientcmd.NewClientConfigFromBytes(kubeconfig) - if err != nil { - return nil, err - } - - config, err := clientConfig.ClientConfig() - if err != nil { - return nil, err - } - - return config, nil -} - // validateKubeSphereAPIServer uses version api to check the accessibility // If kubesphere apiserver endpoint is not provided, use kube-apiserver proxy instead func validateKubeSphereAPIServer(ksEndpoint string, kubeconfig []byte) (*version.Info, error) { @@ -426,7 +411,7 @@ func validateKubeSphereAPIServer(ksEndpoint string, kubeconfig []byte) (*version return nil, err } } else { - config, err := loadKubeConfigFromBytes(kubeconfig) + config, err := k8sutil.LoadKubeConfigFromBytes(kubeconfig) if err != nil { return nil, err } @@ -485,7 +470,7 @@ func (h *handler) validateMemberClusterConfiguration(memberKubeconfig []byte) er // getMemberClusterConfig returns KubeSphere running config by the given member cluster kubeconfig func (h *handler) getMemberClusterConfig(kubeconfig []byte) (*config.Config, error) { - config, err := loadKubeConfigFromBytes(kubeconfig) + config, err := k8sutil.LoadKubeConfigFromBytes(kubeconfig) if err != nil { return nil, err } diff --git a/pkg/kapis/cluster/v1alpha1/handler_test.go b/pkg/kapis/cluster/v1alpha1/handler_test.go index 07cb215fb..f671b75da 100644 --- a/pkg/kapis/cluster/v1alpha1/handler_test.go +++ b/pkg/kapis/cluster/v1alpha1/handler_test.go @@ -41,6 +41,7 @@ import ( "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake" "kubesphere.io/kubesphere/pkg/informers" + "kubesphere.io/kubesphere/pkg/utils/k8sutil" "kubesphere.io/kubesphere/pkg/version" ) @@ -339,7 +340,7 @@ func TestValidateKubeConfig(t *testing.T) { "", agentImage) - config, err := loadKubeConfigFromBytes([]byte(base64EncodedKubeConfig)) + config, err := k8sutil.LoadKubeConfigFromBytes([]byte(base64EncodedKubeConfig)) if err != nil { t.Fatal(err) } @@ -415,7 +416,7 @@ func TestValidateMemberClusterConfiguration(t *testing.T) { "", agentImage) - config, err := loadKubeConfigFromBytes([]byte(base64EncodedKubeConfig)) + config, err := k8sutil.LoadKubeConfigFromBytes([]byte(base64EncodedKubeConfig)) if err != nil { t.Fatal(err) } diff --git a/pkg/utils/k8sutil/k8sutil.go b/pkg/utils/k8sutil/k8sutil.go index c771002d4..219b57339 100644 --- a/pkg/utils/k8sutil/k8sutil.go +++ b/pkg/utils/k8sutil/k8sutil.go @@ -18,6 +18,8 @@ package k8sutil import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" tenantv1alpha1 "kubesphere.io/api/tenant/v1alpha1" tenantv1alpha2 "kubesphere.io/api/tenant/v1alpha2" @@ -55,3 +57,18 @@ func GetWorkspaceOwnerName(ownerReferences []metav1.OwnerReference) string { } return "" } + +// LoadKubeConfigFromBytes parses the kubeconfig yaml data to the rest.Config struct. +func LoadKubeConfigFromBytes(kubeconfig []byte) (*rest.Config, error) { + clientConfig, err := clientcmd.NewClientConfigFromBytes(kubeconfig) + if err != nil { + return nil, err + } + + config, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + + return config, nil +} diff --git a/staging/src/kubesphere.io/api/cluster/v1alpha1/cluster_types.go b/staging/src/kubesphere.io/api/cluster/v1alpha1/cluster_types.go index 137762f35..fab11ab3a 100644 --- a/staging/src/kubesphere.io/api/cluster/v1alpha1/cluster_types.go +++ b/staging/src/kubesphere.io/api/cluster/v1alpha1/cluster_types.go @@ -121,6 +121,9 @@ const ( // Openpitrix runtime is created ClusterOpenPitrixRuntimeReady ClusterConditionType = "OpenPitrixRuntimeReady" + + // ClusterKubeConfigCertExpiresInSevenDays indicates that the cluster certificate is about to expire. + ClusterKubeConfigCertExpiresInSevenDays ClusterConditionType = "KubeConfigCertExpiresInSevenDays" ) type ClusterCondition struct {