322 lines
8.7 KiB
Go
322 lines
8.7 KiB
Go
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
|
|
}
|