package k8s import ( "context" "fmt" "strings" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/ocdp/cluster-service/internal/domain/entity" "github.com/ocdp/cluster-service/internal/domain/repository" ) // EntryClient 使用 Kubernetes API 查询实例相关 Service/Ingress type EntryClient struct{} // NewEntryClient 创建 EntryClient func NewEntryClient() repository.InstanceEntryClient { return &EntryClient{} } // ListEntries 查询实例的 Service/Ingress 入口 func (c *EntryClient) ListEntries( ctx context.Context, cluster *entity.Cluster, instance *entity.Instance, ) ([]*entity.InstanceEntry, error) { clientset, err := c.createClientset(cluster) if err != nil { return nil, err } selector := fmt.Sprintf("app.kubernetes.io/instance=%s", instance.Name) serviceEntries, err := c.collectServiceEntries(ctx, clientset, instance, selector) if err != nil { return nil, err } ingressEntries, err := c.collectIngressEntries(ctx, clientset, instance, selector) if err != nil { return nil, err } return append(serviceEntries, ingressEntries...), nil } func (c *EntryClient) collectServiceEntries( ctx context.Context, clientset *kubernetes.Clientset, instance *entity.Instance, selector string, ) ([]*entity.InstanceEntry, error) { services, err := c.listServices(ctx, clientset, instance.Namespace, selector) if err != nil { return nil, err } entries := convertServicesToEntries(services, instance, selector == "") if len(entries) == 0 && selector != "" { // Fallback: widen the search scope and filter manually. services, err = c.listServices(ctx, clientset, instance.Namespace, "") if err != nil { return nil, err } entries = convertServicesToEntries(services, instance, true) } return entries, nil } func (c *EntryClient) collectIngressEntries( ctx context.Context, clientset *kubernetes.Clientset, instance *entity.Instance, selector string, ) ([]*entity.InstanceEntry, error) { ingresses, err := c.listIngresses(ctx, clientset, instance.Namespace, selector) if err != nil { return nil, err } entries := convertIngressesToEntries(ingresses, instance, selector == "") if len(entries) == 0 && selector != "" { ingresses, err = c.listIngresses(ctx, clientset, instance.Namespace, "") if err != nil { return nil, err } entries = convertIngressesToEntries(ingresses, instance, true) } return entries, nil } func (c *EntryClient) listServices( ctx context.Context, clientset *kubernetes.Clientset, namespace, selector string, ) ([]corev1.Service, error) { listOptions := metav1.ListOptions{} if selector != "" { listOptions.LabelSelector = selector } services, err := clientset.CoreV1(). Services(namespace). List(ctx, listOptions) if err != nil { return nil, fmt.Errorf("failed to list services: %w", err) } return services.Items, nil } func (c *EntryClient) listIngresses( ctx context.Context, clientset *kubernetes.Clientset, namespace, selector string, ) ([]networkingv1.Ingress, error) { listOptions := metav1.ListOptions{} if selector != "" { listOptions.LabelSelector = selector } ingresses, err := clientset.NetworkingV1(). Ingresses(namespace). List(ctx, listOptions) if err != nil { return nil, fmt.Errorf("failed to list ingresses: %w", err) } return ingresses.Items, nil } func convertServicesToEntries(services []corev1.Service, instance *entity.Instance, enforceMatch bool) []*entity.InstanceEntry { entries := make([]*entity.InstanceEntry, 0, len(services)) for _, svc := range services { if enforceMatch && !resourceMatchesInstance(svc.ObjectMeta, instance) { continue } entries = append(entries, convertServiceToEntry(&svc)) } return entries } func convertIngressesToEntries(ingresses []networkingv1.Ingress, instance *entity.Instance, enforceMatch bool) []*entity.InstanceEntry { entries := make([]*entity.InstanceEntry, 0, len(ingresses)) for _, ing := range ingresses { if enforceMatch && !resourceMatchesInstance(ing.ObjectMeta, instance) { continue } entries = append(entries, convertIngressToEntry(&ing)) } return entries } func (c *EntryClient) createClientset(cluster *entity.Cluster) (*kubernetes.Clientset, error) { config, err := clientcmd.RESTConfigFromKubeConfig([]byte(cluster.GetKubeConfig())) if err != nil { config = &rest.Config{ Host: cluster.Host, TLSClientConfig: rest.TLSClientConfig{ CAData: []byte(cluster.CAData), CertData: []byte(cluster.CertData), KeyData: []byte(cluster.KeyData), }, BearerToken: cluster.Token, } } clientset, err := kubernetes.NewForConfig(config) if err != nil { return nil, fmt.Errorf("failed to create kubernetes client: %w", err) } return clientset, nil } func convertServiceToEntry(svc *corev1.Service) *entity.InstanceEntry { clusterIP := svc.Spec.ClusterIP if clusterIP == corev1.ClusterIPNone { clusterIP = "" } lbIngress := make([]string, 0, len(svc.Status.LoadBalancer.Ingress)) for _, ing := range svc.Status.LoadBalancer.Ingress { if ing.IP != "" { lbIngress = append(lbIngress, ing.IP) } if ing.Hostname != "" { lbIngress = append(lbIngress, ing.Hostname) } } ports := make([]entity.InstanceEntryPort, 0, len(svc.Spec.Ports)) for _, port := range svc.Spec.Ports { ports = append(ports, entity.InstanceEntryPort{ Name: port.Name, Protocol: string(port.Protocol), Port: port.Port, TargetPort: intOrStringToString(port.TargetPort), NodePort: port.NodePort, }) } return &entity.InstanceEntry{ Kind: "Service", Name: svc.Name, Namespace: svc.Namespace, Type: string(svc.Spec.Type), ClusterIP: clusterIP, ExternalIPs: append([]string{}, svc.Spec.ExternalIPs...), LoadBalancerIngress: lbIngress, Ports: ports, } } func convertIngressToEntry(ing *networkingv1.Ingress) *entity.InstanceEntry { lbIngress := make([]string, 0, len(ing.Status.LoadBalancer.Ingress)) for _, addr := range ing.Status.LoadBalancer.Ingress { if addr.IP != "" { lbIngress = append(lbIngress, addr.IP) } if addr.Hostname != "" { lbIngress = append(lbIngress, addr.Hostname) } } hosts := make([]entity.InstanceEntryHost, 0, len(ing.Spec.Rules)) for _, rule := range ing.Spec.Rules { hostEntry := entity.InstanceEntryHost{ Host: rule.Host, } if rule.HTTP != nil { paths := make([]entity.InstanceEntryPath, 0, len(rule.HTTP.Paths)) for _, path := range rule.HTTP.Paths { name := "" port := "" if path.Backend.Service != nil { name = path.Backend.Service.Name port = serviceBackendPortString(path.Backend.Service.Port) } paths = append(paths, entity.InstanceEntryPath{ Path: path.Path, ServiceName: name, ServicePort: port, }) } hostEntry.Paths = paths } hosts = append(hosts, hostEntry) } tlsEntries := make([]entity.InstanceEntryTLS, 0, len(ing.Spec.TLS)) for _, tls := range ing.Spec.TLS { tlsEntries = append(tlsEntries, entity.InstanceEntryTLS{ Hosts: append([]string{}, tls.Hosts...), SecretName: tls.SecretName, }) } entryType := "Ingress" if ing.Spec.IngressClassName != nil { entryType = *ing.Spec.IngressClassName } return &entity.InstanceEntry{ Kind: "Ingress", Name: ing.Name, Namespace: ing.Namespace, Type: entryType, LoadBalancerIngress: lbIngress, Hosts: hosts, TLS: tlsEntries, } } func intOrStringToString(v intstr.IntOrString) string { if v.Type == intstr.String { return v.StrVal } return fmt.Sprintf("%d", v.IntValue()) } func serviceBackendPortString(port networkingv1.ServiceBackendPort) string { if port.Name != "" { return port.Name } if port.Number != 0 { return fmt.Sprintf("%d", port.Number) } return "" } func resourceMatchesInstance(meta metav1.ObjectMeta, instance *entity.Instance) bool { if instance == nil { return false } labels := meta.GetLabels() if labels != nil { if labels["app.kubernetes.io/instance"] == instance.Name { return true } labelKeys := []string{"app", "app.kubernetes.io/name", "app.kubernetes.io/component", "release"} for _, key := range labelKeys { if labels[key] == instance.Name { return true } } } annotations := meta.GetAnnotations() if annotations != nil { if annotations["meta.helm.sh/release-name"] == instance.Name { if ns := annotations["meta.helm.sh/release-namespace"]; ns == "" || ns == instance.Namespace { return true } } } name := meta.GetName() if name == instance.Name || strings.HasPrefix(name, instance.Name+"-") { return true } return false }