package handler import ( "context" "fmt" "sort" "strings" "sync" "github.com/goodrain/rainbond/api/client/prometheus" "github.com/goodrain/rainbond/config/configs" "github.com/goodrain/rainbond/pkg/component/k8s" "github.com/goodrain/rainbond/pkg/component/prom" k8sutil "github.com/goodrain/rainbond/util/k8s" "github.com/pquerna/ffjson/ffjson" "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" policyv1beta1 "k8s.io/api/policy/v1beta1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" utilversion "k8s.io/apimachinery/pkg/util/version" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "github.com/goodrain/rainbond/api/model" "sigs.k8s.io/controller-runtime/pkg/client" "time" ) const ( // NodeRolesLabelPrefix - NodeRolesLabelPrefix = "node-role.kubernetes.io" // NodeRolesLabel - NodeRolesLabel = "kubernetes.io/role" // NodeInternalIP - NodeInternalIP = "InternalIP" // NodeExternalIP - NodeExternalIP = "ExternalIP" // UnSchedulAble - UnSchedulAble = "unschedulable" // ReSchedulAble - ReSchedulAble = "reschedulable" // NodeUp - NodeUp = "up" // NodeDown - NodeDown = "down" // Evict - Evict = "evict" //EvictionKind - EvictionKind = "Eviction" //EvictionSubresource - EvictionSubresource = "pods/eviction" ) // NodesHandler - type NodesHandler interface { ListNodes(ctx context.Context) ([]model.NodeInfo, error) ListNodesWithOptions(ctx context.Context, includeStats bool) ([]model.NodeInfo, error) ListChaosNodeArch(ctx context.Context) ([]string, error) GetNodeInfo(ctx context.Context, nodeName string) (model.NodeInfo, error) NodeAction(ctx context.Context, nodeName, action string) error ListLabels(ctx context.Context, nodeName string) (map[string]string, error) UpdateLabels(ctx context.Context, nodeName string, labels map[string]string) (map[string]string, error) ListTaints(ctx context.Context, nodeName string) ([]v1.Taint, error) UpdateTaints(ctx context.Context, nodeName string, taints []v1.Taint) ([]v1.Taint, error) } // NewNodesHandler - func NewNodesHandler() NodesHandler { return &nodesHandle{ namespace: configs.Default().PublicConfig.RbdNamespace, clientset: k8s.Default().Clientset, config: k8s.Default().RestConfig, mapper: k8s.Default().Mapper, prometheusCli: prom.Default().PrometheusCli, nodeFilesystemStatsTimeout: defaultNodeFilesystemStatsTimeout, } } type nodesHandle struct { namespace string clientset *kubernetes.Clientset clusterInfoCache *model.ClusterResource cacheTime time.Time config *rest.Config mapper meta.RESTMapper client client.Client prometheusCli prometheus.Interface nodeFilesystemStatsTimeout time.Duration } // ListNodes - func (n *nodesHandle) ListNodes(ctx context.Context) (res []model.NodeInfo, err error) { return n.ListNodesWithOptions(ctx, true) } // ListNodesWithOptions skips optional kubelet and Prometheus work for health-only callers. func (n *nodesHandle) ListNodesWithOptions(ctx context.Context, includeStats bool) (res []model.NodeInfo, err error) { nodeList, err := n.clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) if err != nil { logrus.Errorf("get node list error: %v", err) return nil, err } res = make([]model.NodeInfo, 0, len(nodeList.Items)) for _, node := range nodeList.Items { nodeInfo, err := n.handleNodeInfo(node, false) if err != nil { logrus.Error("get node info handle error:", err) return res, err } res = append(res, nodeInfo) } if !includeStats || len(res) == 0 { return res, ctx.Err() } n.setNodesFilesystemStats(ctx, res) if err := ctx.Err(); err != nil { return nil, err } n.setNodesRequestedResources(res) return res, ctx.Err() } // ListChaosNodeArch - func (n *nodesHandle) ListChaosNodeArch(ctx context.Context) ([]string, error) { chaosPods, err := n.clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{ LabelSelector: "name=rbd-chaos", }) if err != nil { logrus.Errorf("get chaos list error: %v", err) return nil, err } chaosNodeIP := make(map[string]int) for _, chaosPod := range chaosPods.Items { chaosNodeIP[chaosPod.Status.HostIP] = 1 } nodes, err := n.clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) if err != nil { logrus.Error("get node info handle error:", err) return nil, err } var nodeArchs []string for _, node := range nodes.Items { address := node.Status.Addresses for _, addr := range address { if addr.Type == NodeInternalIP && chaosNodeIP[addr.Address] == 1 { nodeArchs = append(nodeArchs, node.Status.NodeInfo.Architecture) } } } return nodeArchs, nil } // GetNodeInfo - func (n *nodesHandle) GetNodeInfo(ctx context.Context, nodeName string) (res model.NodeInfo, err error) { node, err := n.clientset.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { logrus.Error("get node info error:", err) return res, err } res, err = n.HandleNodeInfo(*node) if err != nil { logrus.Error("get node info handle error:", err) return res, err } statsErr := n.setNodeFilesystemStats(ctx, nodeName, &res.Resource) res.DiskStatsStatus = nodeDiskStatsStatus(res.Resource, statsErr) if statsErr != nil { logrus.Warnf("get kubelet filesystem stats for node %s: %v", nodeName, statsErr) } return res, nil } // HandleNode - func (n *nodesHandle) HandleNodeInfo(node v1.Node) (nodeinfo model.NodeInfo, err error) { return n.handleNodeInfo(node, true) } func (n *nodesHandle) handleNodeInfo(node v1.Node, includeRequestedResources bool) (nodeinfo model.NodeInfo, err error) { nodeinfo.DiskStatsStatus = "not_requested" var condition model.NodeCondition for _, addr := range node.Status.Addresses { switch addr.Type { case NodeInternalIP: nodeinfo.InternalIP = addr.Address case NodeExternalIP: nodeinfo.ExternalIP = addr.Address default: continue } } for _, cond := range node.Status.Conditions { condition.Type = string(cond.Type) condition.Status = string(cond.Status) condition.Message = cond.Message condition.Reason = cond.Reason condition.LastHeartbeatTime = cond.LastHeartbeatTime condition.LastTransitionTime = cond.LastTransitionTime nodeinfo.Conditions = append(nodeinfo.Conditions, condition) } // get node roles var roles []string for k, _ := range node.Labels { if strings.HasPrefix(k, NodeRolesLabelPrefix) { // string handle : node-role.kubernetes.io/worker: "true" role := strings.Split(k, "/")[1] if role == "master" { continue } roles = append(roles, role) } } sort.Strings(roles) if includeRequestedResources { n.setNodeRequestedResources(node.Name, &nodeinfo.Resource) } // cap resource nodeinfo.Resource.CapMemory = int(node.Status.Capacity.Memory().Value()) / 1024 / 1024 nodeinfo.Resource.CapCPU = int(node.Status.Capacity.Cpu().Value()) nodeinfo.Resource.CapStorageEq = int(node.Status.Capacity.StorageEphemeral().Value()) / 1024 / 1024 nodeinfo.Name = node.Name nodeinfo.Unschedulable = node.Spec.Unschedulable nodeinfo.KernelVersion = node.Status.NodeInfo.KernelVersion nodeinfo.ContainerRunTime = node.Status.NodeInfo.ContainerRuntimeVersion nodeinfo.Architecture = node.Status.NodeInfo.Architecture nodeinfo.OperatingSystem = node.Status.NodeInfo.OperatingSystem nodeinfo.CreateTime = node.CreationTimestamp.Time nodeinfo.OSVersion = node.Status.NodeInfo.OSImage nodeinfo.Roles = roles return nodeinfo, nil } func (n *nodesHandle) setNodeRequestedResources(nodeName string, resource *model.Resource) { queries := []prometheusQuery[func(float64)]{ { expr: fmt.Sprintf(`sum(rbd_api_exporter_cluster_pod_memory{node_name="%v"}) by (instance)`, nodeName), apply: func(value float64) { resource.ReqMemory = int(value) / 1024 / 1024 }, }, { expr: fmt.Sprintf(`sum(rbd_api_exporter_cluster_pod_cpu{node_name="%v"}) by (instance)`, nodeName), apply: func(value float64) { resource.ReqCPU = float32(value) / 1000 }, }, { expr: fmt.Sprintf(`sum(rbd_api_exporter_cluster_pod_ephemeral_storage{node_name="%v"}) by (instance)`, nodeName), apply: func(value float64) { resource.ReqStorageEq = float32(value) / 1024 / 1024 }, }, } metrics := n.queryMetrics(queriesToExpressions(queries)) for i, query := range queries { metric := metrics[i] for _, value := range metric.MetricData.MetricValues { if value.Sample != nil { query.apply(value.Sample.Value()) break } } } } func (n *nodesHandle) setNodesFilesystemStats(ctx context.Context, nodes []model.NodeInfo) { const maxWorkers = 8 workerCount := maxWorkers if len(nodes) < workerCount { workerCount = len(nodes) } if workerCount == 0 { return } jobs := make(chan int) var wg sync.WaitGroup for i := 0; i < workerCount; i++ { wg.Add(1) go func() { defer wg.Done() for index := range jobs { err := n.setNodeFilesystemStats(ctx, nodes[index].Name, &nodes[index].Resource) nodes[index].DiskStatsStatus = nodeDiskStatsStatus(nodes[index].Resource, err) if err != nil { logrus.Warnf("get kubelet filesystem stats for node %s: %v", nodes[index].Name, err) } } }() } queueJobs: for i := range nodes { select { case jobs <- i: case <-ctx.Done(): break queueJobs } } close(jobs) wg.Wait() } func (n *nodesHandle) setNodesRequestedResources(nodes []model.NodeInfo) { nodeIndexes := make(map[string]int, len(nodes)) for i := range nodes { nodeIndexes[nodes[i].Name] = i } queries := []prometheusQuery[func(*model.Resource, float64)]{ { expr: `sum(rbd_api_exporter_cluster_pod_memory) by (node_name, instance)`, apply: func(resource *model.Resource, value float64) { resource.ReqMemory = int(value) / 1024 / 1024 }, }, { expr: `sum(rbd_api_exporter_cluster_pod_cpu) by (node_name, instance)`, apply: func(resource *model.Resource, value float64) { resource.ReqCPU = float32(value) / 1000 }, }, { expr: `sum(rbd_api_exporter_cluster_pod_ephemeral_storage) by (node_name, instance)`, apply: func(resource *model.Resource, value float64) { resource.ReqStorageEq = float32(value) / 1024 / 1024 }, }, } metrics := n.queryMetrics(queriesToExpressions(queries)) for i, query := range queries { metric := metrics[i] for _, value := range metric.MetricData.MetricValues { index, ok := nodeIndexes[value.Metadata["node_name"]] if !ok || value.Sample == nil { continue } query.apply(&nodes[index].Resource, value.Sample.Value()) } } } type prometheusQuery[T any] struct { expr string apply T } func queriesToExpressions[T any](queries []prometheusQuery[T]) []string { expressions := make([]string, len(queries)) for i := range queries { expressions[i] = queries[i].expr } return expressions } func (n *nodesHandle) queryMetrics(expressions []string) []prometheus.Metric { metrics := make([]prometheus.Metric, len(expressions)) var wg sync.WaitGroup for i := range expressions { wg.Add(1) go func(index int) { defer wg.Done() metrics[index] = n.prometheusCli.GetMetric(expressions[index], time.Now()) }(i) } wg.Wait() return metrics } // NodeAction - func (n *nodesHandle) NodeAction(ctx context.Context, nodeName, action string) error { var data string switch action { case UnSchedulAble: // true can't scheduler ,false can scheduler data = fmt.Sprintf(`{"spec":{"unschedulable":%t}}`, true) case ReSchedulAble: // true can't scheduler ,false can scheduler data = fmt.Sprintf(`{"spec":{"unschedulable":%t}}`, false) case Evict: // unschedulable data = fmt.Sprintf(`{"spec":{"unschedulable":%t}}`, true) default: logrus.Info("not support this action") return nil } _, err := n.clientset.CoreV1().Nodes().Patch(ctx, nodeName, types.StrategicMergePatchType, []byte(data), metav1.PatchOptions{}) if err != nil { logrus.Error("action patch error:", err) return err } if action == Evict { err = n.DeleteOrEvictPodsSimple(nodeName) if err != nil { logrus.Error("delete or evict pods error:", err) return err } } return nil } // DeleteOrEvictPodsSimple Evict the Pod from a node func (n *nodesHandle) DeleteOrEvictPodsSimple(nodeName string) error { nodePods, err := n.GetNodePods(nodeName) if err != nil { return err } policyGroupVersion, err := n.SupportEviction() if err != nil { return err } if policyGroupVersion == "" { return fmt.Errorf("the server can not support eviction subresource") } for _, v := range nodePods { n.evictPod(v, policyGroupVersion) } return nil } // SupportEviction uses Discovery API to find out if the server support eviction subresource // If support, it will return its groupVersion; Otherwise, it will return "" func (n *nodesHandle) SupportEviction() (string, error) { discoveryClient := n.clientset.Discovery() groupList, err := discoveryClient.ServerGroups() if err != nil { return "", err } foundPolicyGroup := false var policyGroupVersion string for _, group := range groupList.Groups { if group.Name == "policy" { foundPolicyGroup = true policyGroupVersion = group.PreferredVersion.GroupVersion break } } if !foundPolicyGroup { return "", nil } resourceList, err := discoveryClient.ServerResourcesForGroupVersion("v1") if err != nil { return "", err } for _, resource := range resourceList.APIResources { if resource.Name == EvictionSubresource && resource.Kind == EvictionKind { return policyGroupVersion, nil } } return "", nil } // GetNodePods - func (n *nodesHandle) GetNodePods(nodeName string) (pods []v1.Pod, err error) { podList, err := n.clientset.CoreV1().Pods(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{ FieldSelector: fields.SelectorFromSet(fields.Set{"spec.nodeName": nodeName}).String()}) if err != nil { return nil, err } return podList.Items, err } // GetNodeScheduler Scheduler status func (n *nodesHandle) GetNodeScheduler(ctx context.Context, nodeName string) (status bool, err error) { node, err := n.clientset.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { logrus.Error("get node scheduler status error:", err) return false, err } return node.Spec.Unschedulable, err } // evictPod - func (n *nodesHandle) evictPod(pod v1.Pod, policyGroupVersion string) error { deleteOptions := &metav1.DeleteOptions{} if k8sutil.GetKubeVersion().AtLeast(utilversion.MustParseSemantic("v1.21.0")) { eviction := &policyv1.Eviction{ TypeMeta: metav1.TypeMeta{ APIVersion: policyGroupVersion, Kind: EvictionKind, }, ObjectMeta: metav1.ObjectMeta{ Name: pod.Name, Namespace: pod.Namespace, }, DeleteOptions: deleteOptions, } // Remember to change change the URL manipulation func when Evction's version change return n.clientset.PolicyV1().Evictions(eviction.Namespace).Evict(context.Background(), eviction) } eviction := &policyv1beta1.Eviction{ TypeMeta: metav1.TypeMeta{ APIVersion: policyGroupVersion, Kind: EvictionKind, }, ObjectMeta: metav1.ObjectMeta{ Name: pod.Name, Namespace: pod.Namespace, }, DeleteOptions: deleteOptions, } // Remember to change change the URL manipulation func when Evction's version change return n.clientset.PolicyV1beta1().Evictions(eviction.Namespace).Evict(context.Background(), eviction) } // GetLabels - func (n *nodesHandle) ListLabels(ctx context.Context, nodeName string) (map[string]string, error) { node, err := n.clientset.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { logrus.Error("[GetLabels] get node error:", err) return nil, err } return node.Labels, err } // UpdateLabels - func (n *nodesHandle) UpdateLabels(ctx context.Context, nodeName string, labels map[string]string) (map[string]string, error) { node, err := n.clientset.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { logrus.Error("[UpdateLabels] update node labels error:", err) return nil, err } node.Labels = labels res, err := n.clientset.CoreV1().Nodes().Update(ctx, node, metav1.UpdateOptions{}) if err != nil { logrus.Error("[UpdateLabels] update node labels error:", err) return nil, err } return res.Labels, nil } // GetTaint - func (n *nodesHandle) ListTaints(ctx context.Context, nodeName string) ([]v1.Taint, error) { node, err := n.clientset.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { logrus.Error("[GetTaint] get node error:", err) return nil, err } return node.Spec.Taints, err } // UpdateTaint - func (n *nodesHandle) UpdateTaints(ctx context.Context, nodeName string, taints []v1.Taint) ([]v1.Taint, error) { taintsByte, err := ffjson.Marshal(taints) if err != nil { return nil, err } data := fmt.Sprintf(`{"spec":{"taints":%s}}`, string(taintsByte)) node, err := n.clientset.CoreV1().Nodes().Patch(ctx, nodeName, types.StrategicMergePatchType, []byte(data), metav1.PatchOptions{}) if err != nil { logrus.Error("[UpdateTaints] update node taints error:", err) return nil, err } return node.Spec.Taints, err }