From 883097a3adabb2989a373a41494cfed5ec5dfd2d Mon Sep 17 00:00:00 2001 From: Xinzhao Xu Date: Tue, 21 Dec 2021 17:33:21 +0800 Subject: [PATCH] Add update cluster kubeconfig API --- pkg/api/cluster/v1alpha1/types.go | 5 ++ pkg/apiserver/apiserver.go | 4 +- pkg/kapis/cluster/v1alpha1/handler.go | 74 +++++++++++++++++++++- pkg/kapis/cluster/v1alpha1/handler_test.go | 6 +- pkg/kapis/cluster/v1alpha1/register.go | 11 +++- tools/cmd/doc-gen/main.go | 2 +- 6 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 pkg/api/cluster/v1alpha1/types.go diff --git a/pkg/api/cluster/v1alpha1/types.go b/pkg/api/cluster/v1alpha1/types.go new file mode 100644 index 000000000..83e4d5899 --- /dev/null +++ b/pkg/api/cluster/v1alpha1/types.go @@ -0,0 +1,5 @@ +package v1alpha1 + +type UpdateClusterRequest struct { + KubeConfig []byte `json:"kubeconfig"` +} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 6ee16480e..1354ef6ed 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -22,6 +22,7 @@ import ( "fmt" "net/http" rt "runtime" + "strconv" "time" "kubesphere.io/kubesphere/pkg/utils/iputil" @@ -34,8 +35,6 @@ import ( openpitrixv2alpha1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v2alpha1" - "strconv" - "github.com/emicklei/go-restful" "k8s.io/apimachinery/pkg/runtime/schema" urlruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -228,6 +227,7 @@ func (s *APIServer) installKubeSphereAPIs() { s.KubernetesClient.KubeSphere(), s.EventsClient, s.LoggingClient, s.AuditingClient, amOperator, rbacAuthorizer, s.MonitoringClient, s.RuntimeCache, s.Config.MeteringOptions)) urlruntime.Must(terminalv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), rbacAuthorizer, s.KubernetesClient.Config())) urlruntime.Must(clusterkapisv1alpha1.AddToContainer(s.container, + s.KubernetesClient.KubeSphere(), s.InformerFactory.KubernetesSharedInformerFactory(), s.InformerFactory.KubeSphereSharedInformerFactory(), s.Config.MultiClusterOptions.ProxyPublishService, diff --git a/pkg/kapis/cluster/v1alpha1/handler.go b/pkg/kapis/cluster/v1alpha1/handler.go index b60123d80..9762a3482 100644 --- a/pkg/kapis/cluster/v1alpha1/handler.go +++ b/pkg/kapis/cluster/v1alpha1/handler.go @@ -46,7 +46,9 @@ import ( "kubesphere.io/api/cluster/v1alpha1" "kubesphere.io/kubesphere/pkg/api" + clusterv1alpha1 "kubesphere.io/kubesphere/pkg/api/cluster/v1alpha1" "kubesphere.io/kubesphere/pkg/apiserver/config" + 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/version" @@ -63,6 +65,7 @@ const ( var errClusterConnectionIsNotProxy = fmt.Errorf("cluster is not using proxy connection") type handler struct { + ksclient kubesphere.Interface serviceLister v1.ServiceLister clusterLister clusterlister.ClusterLister configMapLister v1.ConfigMapLister @@ -73,13 +76,14 @@ type handler struct { yamlPrinter *printers.YAMLPrinter } -func newHandler(k8sInformers k8sinformers.SharedInformerFactory, ksInformers externalversions.SharedInformerFactory, proxyService, proxyAddress, agentImage string) *handler { +func newHandler(ksclient kubesphere.Interface, k8sInformers k8sinformers.SharedInformerFactory, ksInformers externalversions.SharedInformerFactory, proxyService, proxyAddress, agentImage string) *handler { if len(agentImage) == 0 { agentImage = defaultAgentImage } return &handler{ + ksclient: ksclient, serviceLister: k8sInformers.Core().V1().Services().Lister(), clusterLister: ksInformers.Cluster().V1alpha1().Clusters().Lister(), configMapLister: k8sInformers.Core().V1().ConfigMaps().Lister(), @@ -133,7 +137,6 @@ func (h *handler) generateAgentDeployment(request *restful.Request, response *re response.Write(buf.Bytes()) } -// func (h *handler) populateProxyAddress() error { if len(h.proxyService) == 0 { return fmt.Errorf("neither proxy address nor proxy service provided") @@ -251,6 +254,73 @@ func (h *handler) generateDefaultDeployment(cluster *v1alpha1.Cluster, w io.Writ return h.yamlPrinter.PrintObj(&agent, w) } +// updateKubeConfig updates the kubeconfig of the specific cluster, this API is used to update expired kubeconfig. +func (h *handler) updateKubeConfig(request *restful.Request, response *restful.Response) { + var req clusterv1alpha1.UpdateClusterRequest + if err := request.ReadEntity(&req); err != nil { + api.HandleBadRequest(response, request, err) + return + } + + clusterName := request.PathParameter("cluster") + obj, err := h.clusterLister.Get(clusterName) + if err != nil { + api.HandleBadRequest(response, request, err) + return + } + cluster := obj.DeepCopy() + if _, ok := cluster.Labels[v1alpha1.HostCluster]; ok { + api.HandleBadRequest(response, request, fmt.Errorf("update kubeconfig of the host cluster is not allowed")) + return + } + // For member clusters that use proxy mode, we don't need to update the kubeconfig, + // if the certs expired, just restart the tower component in the host cluster, it will renew the cert. + if cluster.Spec.Connection.Type == v1alpha1.ConnectionTypeProxy { + api.HandleBadRequest(response, request, fmt.Errorf( + "update kubeconfig of member clusters which using proxy mode is not allowed, their certs are managed and will be renewed by tower", + )) + return + } + + if len(req.KubeConfig) == 0 { + api.HandleBadRequest(response, request, fmt.Errorf("cluster kubeconfig MUST NOT be empty")) + return + } + config, err := loadKubeConfigFromBytes(req.KubeConfig) + if err != nil { + api.HandleBadRequest(response, request, err) + return + } + config.Timeout = defaultTimeout + clientSet, err := kubernetes.NewForConfig(config) + if err != nil { + api.HandleBadRequest(response, request, err) + return + } + if _, err = clientSet.Discovery().ServerVersion(); err != nil { + api.HandleBadRequest(response, request, err) + return + } + + _, err = validateKubeSphereAPIServer(cluster.Spec.Connection.KubeSphereAPIEndpoint, cluster.Spec.Connection.KubeConfig) + if err != nil { + api.HandleBadRequest(response, request, fmt.Errorf("unable validate kubesphere endpoint, %v", err)) + return + } + + err = h.validateMemberClusterConfiguration(cluster.Spec.Connection.KubeConfig) + if err != nil { + api.HandleBadRequest(response, request, fmt.Errorf("failed to validate member cluster configuration, err: %v", err)) + } + + cluster.Spec.Connection.KubeConfig = req.KubeConfig + if _, err = h.ksclient.ClusterV1alpha1().Clusters().Update(context.TODO(), cluster, metav1.UpdateOptions{}); err != nil { + api.HandleBadRequest(response, request, err) + return + } + response.WriteHeader(http.StatusOK) +} + // ValidateCluster validate cluster kubeconfig and kubesphere apiserver address, check their accessibility func (h *handler) validateCluster(request *restful.Request, response *restful.Response) { var cluster v1alpha1.Cluster diff --git a/pkg/kapis/cluster/v1alpha1/handler_test.go b/pkg/kapis/cluster/v1alpha1/handler_test.go index 570ce4338..07cb215fb 100644 --- a/pkg/kapis/cluster/v1alpha1/handler_test.go +++ b/pkg/kapis/cluster/v1alpha1/handler_test.go @@ -252,7 +252,7 @@ func TestGeranteAgentDeployment(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.description, func(t *testing.T) { - h := newHandler(informersFactory.KubernetesSharedInformerFactory(), + h := newHandler(ksclient, informersFactory.KubernetesSharedInformerFactory(), informersFactory.KubeSphereSharedInformerFactory(), proxyService, "", @@ -333,7 +333,7 @@ func TestValidateKubeConfig(t *testing.T) { informersFactory.KubernetesSharedInformerFactory().Core().V1().Services().Informer().GetIndexer().Add(service) informersFactory.KubeSphereSharedInformerFactory().Cluster().V1alpha1().Clusters().Informer().GetIndexer().Add(cluster) - h := newHandler(informersFactory.KubernetesSharedInformerFactory(), + h := newHandler(ksclient, informersFactory.KubernetesSharedInformerFactory(), informersFactory.KubeSphereSharedInformerFactory(), proxyService, "", @@ -409,7 +409,7 @@ func TestValidateMemberClusterConfiguration(t *testing.T) { informersFactory.KubeSphereSharedInformerFactory().Cluster().V1alpha1().Clusters().Informer().GetIndexer().Add(cluster) informersFactory.KubernetesSharedInformerFactory().Core().V1().ConfigMaps().Informer().GetIndexer().Add(hostCm) - h := newHandler(informersFactory.KubernetesSharedInformerFactory(), + h := newHandler(ksclient, informersFactory.KubernetesSharedInformerFactory(), informersFactory.KubeSphereSharedInformerFactory(), proxyService, "", diff --git a/pkg/kapis/cluster/v1alpha1/register.go b/pkg/kapis/cluster/v1alpha1/register.go index 6b48374bb..955484819 100644 --- a/pkg/kapis/cluster/v1alpha1/register.go +++ b/pkg/kapis/cluster/v1alpha1/register.go @@ -26,6 +26,7 @@ import ( "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/apiserver/runtime" + kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" "kubesphere.io/kubesphere/pkg/client/informers/externalversions" "kubesphere.io/kubesphere/pkg/constants" ) @@ -37,6 +38,7 @@ const ( var GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} func AddToContainer(container *restful.Container, + ksclient kubesphere.Interface, k8sInformers k8sinformers.SharedInformerFactory, ksInformers externalversions.SharedInformerFactory, proxyService string, @@ -44,7 +46,7 @@ func AddToContainer(container *restful.Container, agentImage string) error { webservice := runtime.NewWebService(GroupVersion) - h := newHandler(k8sInformers, ksInformers, proxyService, proxyAddress, agentImage) + h := newHandler(ksclient, k8sInformers, ksInformers, proxyService, proxyAddress, agentImage) // returns deployment yaml for cluster agent webservice.Route(webservice.GET("/clusters/{cluster}/agent/deployment"). @@ -61,6 +63,13 @@ func AddToContainer(container *restful.Container, Returns(http.StatusOK, api.StatusOK, nil). Metadata(restfulspec.KeyOpenAPITags, []string{constants.MultiClusterTag})) + webservice.Route(webservice.PUT("/clusters/{cluster}/kubeconfig"). + Doc("Update cluster kubeconfig."). + Param(webservice.PathParameter("cluster", "Name of the cluster.").Required(true)). + To(h.updateKubeConfig). + Returns(http.StatusOK, api.StatusOK, nil). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.MultiClusterTag})) + container.Add(webservice) return nil diff --git a/tools/cmd/doc-gen/main.go b/tools/cmd/doc-gen/main.go index 06e1c6fa7..3dc7db970 100644 --- a/tools/cmd/doc-gen/main.go +++ b/tools/cmd/doc-gen/main.go @@ -118,7 +118,7 @@ func generateSwaggerJson() []byte { informerFactory := informers.NewNullInformerFactory() urlruntime.Must(oauth.AddToContainer(container, nil, nil, nil, nil, nil, nil)) - urlruntime.Must(clusterkapisv1alpha1.AddToContainer(container, informerFactory.KubernetesSharedInformerFactory(), + urlruntime.Must(clusterkapisv1alpha1.AddToContainer(container, clientsets.KubeSphere(), informerFactory.KubernetesSharedInformerFactory(), informerFactory.KubeSphereSharedInformerFactory(), "", "", "")) urlruntime.Must(devopsv1alpha2.AddToContainer(container, "")) urlruntime.Must(devopsv1alpha3.AddToContainer(container, ""))