Files
kubesphere/pkg/kapis/cluster/v1alpha1/handler.go

519 lines
16 KiB
Go

/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/emicklei/go-restful"
"gopkg.in/yaml.v2"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/cli-runtime/pkg/printers"
k8sinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
v1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/rest"
"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/utils/k8sutil"
"kubesphere.io/kubesphere/pkg/version"
)
const (
defaultAgentImage = "kubesphere/tower:v1.0"
defaultTimeout = 10 * time.Second
KubesphereNamespace = "kubesphere-system"
KubeSphereConfigName = "kubesphere-config"
KubeSphereApiServer = "ks-apiserver"
)
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
proxyService string
proxyAddress string
agentImage string
yamlPrinter *printers.YAMLPrinter
}
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(),
proxyService: proxyService,
proxyAddress: proxyAddress,
agentImage: agentImage,
yamlPrinter: &printers.YAMLPrinter{},
}
}
// generateAgentDeployment will return a deployment yaml for proxy connection type cluster
// ProxyPublishAddress takes high precedence over proxyPublishService, use proxyPublishService ingress
// address only when proxyPublishAddress is not provided.
func (h *handler) generateAgentDeployment(request *restful.Request, response *restful.Response) {
clusterName := request.PathParameter("cluster")
cluster, err := h.clusterLister.Get(clusterName)
if err != nil {
if errors.IsNotFound(err) {
api.HandleNotFound(response, request, err)
return
} else {
api.HandleInternalError(response, request, err)
return
}
}
if cluster.Spec.Connection.Type != v1alpha1.ConnectionTypeProxy {
api.HandleNotFound(response, request, fmt.Errorf("cluster %s is not using proxy connection", cluster.Name))
return
}
// use service ingress address
if len(h.proxyAddress) == 0 {
err = h.populateProxyAddress()
if err != nil {
api.HandleNotFound(response, request, err)
return
}
}
var buf bytes.Buffer
err = h.generateDefaultDeployment(cluster, &buf)
if err != nil {
api.HandleInternalError(response, request, err)
return
}
response.Write(buf.Bytes())
}
func (h *handler) populateProxyAddress() error {
if len(h.proxyService) == 0 {
return fmt.Errorf("neither proxy address nor proxy service provided")
}
namespace := "kubesphere-system"
parts := strings.Split(h.proxyService, ".")
if len(parts) > 1 && len(parts[1]) != 0 {
namespace = parts[1]
}
service, err := h.serviceLister.Services(namespace).Get(parts[0])
if err != nil {
return fmt.Errorf("service %s not found in namespace %s", parts[0], namespace)
}
if len(service.Spec.Ports) == 0 {
return fmt.Errorf("there are no ports in proxy service %s spec", h.proxyService)
}
port := service.Spec.Ports[0].Port
var serviceAddress string
for _, ingress := range service.Status.LoadBalancer.Ingress {
if len(ingress.Hostname) != 0 {
serviceAddress = fmt.Sprintf("http://%s:%d", ingress.Hostname, port)
}
if len(ingress.IP) != 0 {
serviceAddress = fmt.Sprintf("http://%s:%d", ingress.IP, port)
}
}
if len(serviceAddress) == 0 {
return fmt.Errorf("cannot generate agent deployment yaml for member cluster "+
" because %s service has no public address, please check %s status, or set address "+
" mannually in ClusterConfiguration", h.proxyService, h.proxyService)
}
h.proxyAddress = serviceAddress
return nil
}
// Currently, this method works because of serviceaccount/clusterrole/clusterrolebinding already
// created by kubesphere, we don't need to create them again. And it's a little bit inconvenient
// if we want to change the template.
// TODO(jeff): load template from configmap
func (h *handler) generateDefaultDeployment(cluster *v1alpha1.Cluster, w io.Writer) error {
_, err := url.Parse(h.proxyAddress)
if err != nil {
return fmt.Errorf("invalid proxy address %s, should format like http[s]://1.2.3.4:123", h.proxyAddress)
}
if cluster.Spec.Connection.Type == v1alpha1.ConnectionTypeDirect {
return errClusterConnectionIsNotProxy
}
agent := appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-agent",
Namespace: "kubesphere-system",
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "agent",
"app.kubernetes.io/part-of": "tower",
},
},
Strategy: appsv1.DeploymentStrategy{},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "agent",
"app.kubernetes.io/part-of": "tower",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "agent",
Command: []string{
"/agent",
fmt.Sprintf("--name=%s", cluster.Name),
fmt.Sprintf("--token=%s", cluster.Spec.Connection.Token),
fmt.Sprintf("--proxy-server=%s", h.proxyAddress),
fmt.Sprintf("--keepalive=10s"),
fmt.Sprintf("--kubesphere-service=ks-apiserver.kubesphere-system.svc:80"),
fmt.Sprintf("--kubernetes-service=kubernetes.default.svc:443"),
fmt.Sprintf("--v=0"),
},
Image: h.agentImage,
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("200M"),
},
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("100M"),
},
},
},
},
ServiceAccountName: "kubesphere",
},
},
},
}
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 := k8sutil.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
err := request.ReadEntity(&cluster)
if err != nil {
api.HandleBadRequest(response, request, err)
return
}
if cluster.Spec.Connection.Type != v1alpha1.ConnectionTypeDirect {
api.HandleBadRequest(response, request, fmt.Errorf("cluster connection type MUST be direct"))
return
}
if len(cluster.Spec.Connection.KubeConfig) == 0 {
api.HandleBadRequest(response, request, fmt.Errorf("cluster kubeconfig MUST NOT be empty"))
return
}
err = h.validateKubeConfig(cluster.Spec.Connection.KubeConfig)
if 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))
}
response.WriteHeader(http.StatusOK)
}
// validateKubeConfig takes base64 encoded kubeconfig and check its validity
func (h *handler) validateKubeConfig(kubeconfig []byte) error {
config, err := k8sutil.LoadKubeConfigFromBytes(kubeconfig)
if err != nil {
return err
}
clusters, err := h.clusterLister.List(labels.Everything())
if err != nil {
return err
}
// clusters with the exactly same KubernetesAPIEndpoint considered to be one
// MUST not import the same cluster twice
for _, cluster := range clusters {
if len(cluster.Spec.Connection.KubernetesAPIEndpoint) != 0 && cluster.Spec.Connection.KubernetesAPIEndpoint == config.Host {
return fmt.Errorf("existing cluster %s with the exacty same server address, MUST not import the same cluster twice", cluster.Name)
}
}
config.Timeout = defaultTimeout
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
return err
}
_, err = clientSet.Discovery().ServerVersion()
return err
}
// 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) {
if len(ksEndpoint) == 0 && len(kubeconfig) == 0 {
return nil, fmt.Errorf("neither kubesphere api endpoint nor kubeconfig was provided")
}
client := http.Client{
Timeout: defaultTimeout,
}
path := fmt.Sprintf("%s/kapis/version", ksEndpoint)
if len(ksEndpoint) != 0 {
_, err := url.Parse(ksEndpoint)
if err != nil {
return nil, err
}
} else {
config, err := k8sutil.LoadKubeConfigFromBytes(kubeconfig)
if err != nil {
return nil, err
}
transport, err := rest.TransportFor(config)
if err != nil {
return nil, err
}
client.Transport = transport
path = fmt.Sprintf("%s/api/v1/namespaces/%s/services/:%s:/proxy/kapis/version", config.Host, KubesphereNamespace, KubeSphereApiServer)
}
response, err := client.Get(path)
if err != nil {
return nil, err
}
responseBytes, _ := ioutil.ReadAll(response.Body)
responseBody := string(responseBytes)
response.Body = ioutil.NopCloser(bytes.NewBuffer(responseBytes))
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("invalid response: %s , please make sure %s.%s.svc of member cluster is up and running", KubeSphereApiServer, KubesphereNamespace, responseBody)
}
ver := version.Info{}
err = json.NewDecoder(response.Body).Decode(&ver)
if err != nil {
return nil, fmt.Errorf("invalid response: %s , please make sure %s.%s.svc of member cluster is up and running", KubeSphereApiServer, KubesphereNamespace, responseBody)
}
return &ver, nil
}
// validateMemberClusterConfiguration compares host and member cluster jwt, if they are not same, it changes member
// cluster jwt to host's, then restart member cluster ks-apiserver.
func (h *handler) validateMemberClusterConfiguration(memberKubeconfig []byte) error {
hConfig, err := h.getHostClusterConfig()
if err != nil {
return err
}
mConfig, err := h.getMemberClusterConfig(memberKubeconfig)
if err != nil {
return err
}
if hConfig.AuthenticationOptions.JwtSecret != mConfig.AuthenticationOptions.JwtSecret {
return fmt.Errorf("hostcluster Jwt is not equal to member cluster jwt, please edit the member cluster cluster config")
}
return nil
}
// getMemberClusterConfig returns KubeSphere running config by the given member cluster kubeconfig
func (h *handler) getMemberClusterConfig(kubeconfig []byte) (*config.Config, error) {
config, err := k8sutil.LoadKubeConfigFromBytes(kubeconfig)
if err != nil {
return nil, err
}
config.Timeout = defaultTimeout
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
memberCm, err := clientSet.CoreV1().ConfigMaps(KubesphereNamespace).Get(context.Background(), KubeSphereConfigName, metav1.GetOptions{})
if err != nil {
return nil, err
}
return getConfigFromCm(memberCm)
}
// getHostClusterConfig returns KubeSphere running config from host cluster ConfigMap
func (h *handler) getHostClusterConfig() (*config.Config, error) {
hostCm, err := h.configMapLister.ConfigMaps(KubesphereNamespace).Get(KubeSphereConfigName)
if err != nil {
return nil, fmt.Errorf("failed to get host cluster %s/configmap/%s, err: %s", KubesphereNamespace, KubeSphereConfigName, err)
}
return getConfigFromCm(hostCm)
}
// getConfigFromCm returns KubeSphere ruuning config by the given ConfigMap.
func getConfigFromCm(cm *corev1.ConfigMap) (*config.Config, error) {
Config := config.New()
value, ok := cm.Data["kubesphere.yaml"]
if !ok {
return nil, fmt.Errorf("failed to get %s/configmap/%s kubesphere.yaml value", KubesphereNamespace, KubeSphereConfigName)
}
err := yaml.Unmarshal([]byte(value), Config)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal value from %s/configmap/%s. err: %s", KubesphereNamespace, KubeSphereConfigName, err)
}
return Config, nil
}