package handler
import (
"context"
"sort"
"github.com/goodrain/rainbond/pkg/component/k8s"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type WorkloadDetailSummary struct {
Name string `json:"name"`
Kind string `json:"kind"`
Namespace string `json:"namespace"`
Status string `json:"status"`
Replicas int64 `json:"replicas,omitempty"`
ReadyReplicas int64 `json:"ready_replicas,omitempty"`
CreatedAt string `json:"created_at"`
Selector map[string]string `json:"selector,omitempty"`
}
type WorkloadDetail struct {
Summary WorkloadDetailSummary `json:"summary"`
Workload *unstructured.Unstructured `json:"workload"`
Pods []corev1.Pod `json:"pods"`
Services []corev1.Service `json:"services"`
Ingresses []networkingv1.Ingress `json:"ingresses"`
}
type PodContainerInfo struct {
Name string `json:"name"`
Image string `json:"image,omitempty"`
Ready bool `json:"ready"`
RestartCount int32 `json:"restart_count,omitempty"`
}
type PodDetailSummary struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Phase string `json:"phase,omitempty"`
NodeName string `json:"node_name,omitempty"`
PodIP string `json:"pod_ip,omitempty"`
CreatedAt string `json:"created_at"`
}
type PodResourceDetail struct {
Summary PodDetailSummary `json:"summary"`
Pod *corev1.Pod `json:"pod"`
Detail interface{} `json:"detail,omitempty"`
Containers []PodContainerInfo `json:"containers"`
Services []corev1.Service `json:"services"`
Ingresses []networkingv1.Ingress `json:"ingresses"`
}
type ResourceEventInfo struct {
Type string `json:"type"`
Reason string `json:"reason"`
Message string `json:"message"`
Count int32 `json:"count"`
LastTimestamp string `json:"last_timestamp"`
}
type ResourceCenterHandler struct{}
func (h *ResourceCenterHandler) GetWorkloadDetail(tenantName, group, version, resource, name string) (*WorkloadDetail, error) {
if err := validateGVRParams(group, version, resource); err != nil {
return nil, err
}
ns, err := GetNsResourceHandler().getTenantNamespace(tenantName)
if err != nil {
return nil, err
}
gvr := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
workload, err := k8s.Default().DynamicClient.Resource(gvr).Namespace(ns).Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
return nil, err
}
selector := extractWorkloadSelector(workload)
pods, err := h.listWorkloadPods(ns, selector)
if err != nil {
return nil, err
}
services, err := h.listRelatedServices(ns, selector)
if err != nil {
return nil, err
}
ingresses, err := h.listRelatedIngresses(ns, services)
if err != nil {
return nil, err
}
summary := WorkloadDetailSummary{
Name: workload.GetName(),
Kind: workload.GetKind(),
Namespace: ns,
Status: computeNsResourceStatus(*workload),
CreatedAt: workload.GetCreationTimestamp().String(),
Selector: selector,
Replicas: extractWorkloadReplicas(workload),
ReadyReplicas: extractWorkloadReadyReplicas(workload),
}
return &WorkloadDetail{
Summary: summary,
Workload: workload,
Pods: pods,
Services: services,
Ingresses: ingresses,
}, nil
}
func (h *ResourceCenterHandler) GetPodDetail(tenantName, podName string) (*PodResourceDetail, error) {
ns, err := GetNsResourceHandler().getTenantNamespace(tenantName)
if err != nil {
return nil, err
}
pod, err := k8s.Default().Clientset.CoreV1().Pods(ns).Get(context.Background(), podName, metav1.GetOptions{})
if err != nil {
return nil, err
}
detail, err := GetPodHandler().PodDetail(ns, podName)
if err != nil {
return nil, err
}
services, err := h.listRelatedServices(ns, pod.Labels)
if err != nil {
return nil, err
}
ingresses, err := h.listRelatedIngresses(ns, services)
if err != nil {
return nil, err
}
containers := make([]PodContainerInfo, 0, len(pod.Spec.Containers))
statusByName := make(map[string]corev1.ContainerStatus, len(pod.Status.ContainerStatuses))
for _, status := range pod.Status.ContainerStatuses {
statusByName[status.Name] = status
}
for _, container := range pod.Spec.Containers {
info := PodContainerInfo{
Name: container.Name,
Image: container.Image,
}
if status, ok := statusByName[container.Name]; ok {
info.Ready = status.Ready
info.RestartCount = status.RestartCount
}
containers = append(containers, info)
}
return &PodResourceDetail{
Summary: PodDetailSummary{
Name: pod.Name,
Namespace: pod.Namespace,
Phase: string(pod.Status.Phase),
NodeName: pod.Spec.NodeName,
PodIP: pod.Status.PodIP,
CreatedAt: pod.CreationTimestamp.String(),
},
Pod: pod,
Detail: detail,
Containers: containers,
Services: services,
Ingresses: ingresses,
}, nil
}
func (h *ResourceCenterHandler) ListEvents(tenantName, namespace, kind, name string) ([]ResourceEventInfo, error) {
ns := namespace
if ns == "" {
var err error
ns, err = GetNsResourceHandler().getTenantNamespace(tenantName)
if err != nil {
return nil, err
}
}
selector := fields.Set{
"involvedObject.kind": kind,
"involvedObject.name": name,
}.String()
list, err := k8s.Default().Clientset.CoreV1().Events(ns).List(context.Background(), metav1.ListOptions{
FieldSelector: selector,
})
if err != nil {
return nil, err
}
items := make([]ResourceEventInfo, 0, len(list.Items))
for _, event := range list.Items {
items = append(items, toResourceEventInfo(event))
}
sort.Slice(items, func(i, j int) bool {
return items[i].LastTimestamp > items[j].LastTimestamp
})
return items, nil
}
func (h *ResourceCenterHandler) listWorkloadPods(namespace string, selector map[string]string) ([]corev1.Pod, error) {
if len(selector) == 0 {
return []corev1.Pod{}, nil
}
list, err := k8s.Default().Clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
LabelSelector: labels.Set(selector).String(),
})
if err != nil {
return nil, err
}
return list.Items, nil
}
func (h *ResourceCenterHandler) listRelatedServices(namespace string, selector map[string]string) ([]corev1.Service, error) {
list, err := k8s.Default().Clientset.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
var matched []corev1.Service
for _, svc := range list.Items {
if labelsMatchSelector(svc.Spec.Selector, selector) {
matched = append(matched, svc)
}
}
return matched, nil
}
func (h *ResourceCenterHandler) listRelatedIngresses(namespace string, services []corev1.Service) ([]networkingv1.Ingress, error) {
if len(services) == 0 {
return []networkingv1.Ingress{}, nil
}
serviceNames := make(map[string]struct{}, len(services))
for _, svc := range services {
serviceNames[svc.Name] = struct{}{}
}
list, err := k8s.Default().Clientset.NetworkingV1().Ingresses(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
var matched []networkingv1.Ingress
for _, ing := range list.Items {
for _, serviceName := range collectIngressServiceNames(ing) {
if _, ok := serviceNames[serviceName]; ok {
matched = append(matched, ing)
break
}
}
}
return matched, nil
}
func extractWorkloadSelector(workload *unstructured.Unstructured) map[string]string {
if selector, ok, _ := unstructured.NestedStringMap(workload.Object, "spec", "selector", "matchLabels"); ok && len(selector) > 0 {
return selector
}
if selector, ok, _ := unstructured.NestedStringMap(workload.Object, "spec", "jobTemplate", "spec", "template", "metadata", "labels"); ok && len(selector) > 0 {
return selector
}
if selector, ok, _ := unstructured.NestedStringMap(workload.Object, "spec", "template", "metadata", "labels"); ok && len(selector) > 0 {
return selector
}
return nil
}
func extractWorkloadReplicas(workload *unstructured.Unstructured) int64 {
switch workload.GetKind() {
case "DaemonSet":
value, _, _ := unstructured.NestedInt64(workload.Object, "status", "desiredNumberScheduled")
return value
case "CronJob":
active, _, _ := unstructured.NestedSlice(workload.Object, "status", "active")
return int64(len(active))
default:
value, _, _ := unstructured.NestedInt64(workload.Object, "spec", "replicas")
return value
}
}
func extractWorkloadReadyReplicas(workload *unstructured.Unstructured) int64 {
switch workload.GetKind() {
case "DaemonSet":
value, _, _ := unstructured.NestedInt64(workload.Object, "status", "numberReady")
return value
case "CronJob":
active, _, _ := unstructured.NestedSlice(workload.Object, "status", "active")
return int64(len(active))
default:
value, _, _ := unstructured.NestedInt64(workload.Object, "status", "readyReplicas")
return value
}
}
func labelsMatchSelector(selector map[string]string, resourceLabels map[string]string) bool {
if len(selector) == 0 || len(resourceLabels) == 0 {
return false
}
for key, value := range selector {
if resourceLabels[key] != value {
return false
}
}
return true
}
func collectIngressServiceNames(ingress networkingv1.Ingress) []string {
names := make([]string, 0, 4)
if ingress.Spec.DefaultBackend != nil && ingress.Spec.DefaultBackend.Service != nil {
names = append(names, ingress.Spec.DefaultBackend.Service.Name)
}
for _, rule := range ingress.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
if path.Backend.Service != nil {
names = append(names, path.Backend.Service.Name)
}
}
}
return uniqueStrings(names)
}
func uniqueStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func toResourceEventInfo(event corev1.Event) ResourceEventInfo {
lastTimestamp := event.LastTimestamp.String()
if lastTimestamp == "" || lastTimestamp == "<nil>" {
lastTimestamp = event.EventTime.String()
}
if lastTimestamp == "" || lastTimestamp == "<nil>" {
lastTimestamp = event.FirstTimestamp.String()
}
return ResourceEventInfo{
Type: event.Type,
Reason: event.Reason,
Message: event.Message,
Count: event.Count,
LastTimestamp: lastTimestamp,
}
}
var resourceCenterHandler *ResourceCenterHandler
func GetResourceCenterHandler() *ResourceCenterHandler {
if resourceCenterHandler == nil {
resourceCenterHandler = &ResourceCenterHandler{}
}
return resourceCenterHandler
}