github.com/oam-dev/kubevela@v1.9.11/pkg/velaql/providers/query/endpoint.go (about)

     1  /*
     2   Copyright 2022 The KubeVela Authors.
     3  
     4   Licensed under the Apache License, Version 2.0 (the "License");
     5   you may not use this file except in compliance with the License.
     6   You may obtain a copy of the License at
     7  
     8   	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10   Unless required by applicable law or agreed to in writing, software
    11   distributed under the License is distributed on an "AS IS" BASIS,
    12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   See the License for the specific language governing permissions and
    14   limitations under the License.
    15  */
    16  
    17  package query
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/kubevela/pkg/util/slices"
    27  	corev1 "k8s.io/api/core/v1"
    28  	v1 "k8s.io/api/networking/v1"
    29  	networkv1beta1 "k8s.io/api/networking/v1beta1"
    30  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/klog/v2"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
    36  
    37  	monitorContext "github.com/kubevela/pkg/monitor/context"
    38  	wfContext "github.com/kubevela/workflow/pkg/context"
    39  	"github.com/kubevela/workflow/pkg/cue/model/value"
    40  	"github.com/kubevela/workflow/pkg/types"
    41  
    42  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    43  	apis "github.com/oam-dev/kubevela/apis/types"
    44  	"github.com/oam-dev/kubevela/pkg/multicluster"
    45  	querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types"
    46  )
    47  
    48  // CollectServiceEndpoints generator service endpoints is available for common component type,
    49  // such as webservice or helm
    50  // it can not support the cloud service component currently
    51  func (h *provider) CollectServiceEndpoints(ctx monitorContext.Context, _ wfContext.Context, v *value.Value, _ types.Action) error {
    52  	val, err := v.LookupValue("app")
    53  	if err != nil {
    54  		return err
    55  	}
    56  	opt := Option{}
    57  	if err = val.UnmarshalTo(&opt); err != nil {
    58  		return err
    59  	}
    60  	app := new(v1beta1.Application)
    61  	err = findResource(ctx, h.cli, app, opt.Name, opt.Namespace, "")
    62  	if err != nil {
    63  		return fmt.Errorf("query app failure %w", err)
    64  	}
    65  	serviceEndpoints := make([]querytypes.ServiceEndpoint, 0)
    66  	var clusterGatewayNodeIP = make(map[string]string)
    67  	collector := NewAppCollector(h.cli, opt)
    68  	resources, err := collector.ListApplicationResources(ctx, app)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	for i, resource := range resources {
    73  		cluster := resources[i].Cluster
    74  		cachedSelectorNodeIP := func() string {
    75  			if ip, exist := clusterGatewayNodeIP[cluster]; exist {
    76  				return ip
    77  			}
    78  			ip := selectorNodeIP(ctx, cluster, h.cli)
    79  			if ip != "" {
    80  				clusterGatewayNodeIP[cluster] = ip
    81  			}
    82  			return ip
    83  		}
    84  		if resource.ResourceTree != nil {
    85  			serviceEndpoints = append(serviceEndpoints, getEndpointFromNode(ctx, h.cli, resource.ResourceTree, resource.Component, cachedSelectorNodeIP)...)
    86  		} else {
    87  			serviceEndpoints = append(serviceEndpoints, getServiceEndpoints(ctx, h.cli, resource.GroupVersionKind(), resource.Name, resource.Namespace, resource.Cluster, resource.Component, cachedSelectorNodeIP)...)
    88  		}
    89  
    90  	}
    91  	return fillQueryResult(v, serviceEndpoints, "list")
    92  }
    93  
    94  func getEndpointFromNode(ctx context.Context, cli client.Client, node *querytypes.ResourceTreeNode, component string, cachedSelectorNodeIP func() string) []querytypes.ServiceEndpoint {
    95  	if node == nil {
    96  		return nil
    97  	}
    98  	var serviceEndpoints []querytypes.ServiceEndpoint
    99  	serviceEndpoints = append(serviceEndpoints, getServiceEndpoints(ctx, cli, node.GroupVersionKind(), node.Name, node.Namespace, node.Cluster, component, cachedSelectorNodeIP)...)
   100  	for _, child := range node.LeafNodes {
   101  		serviceEndpoints = append(serviceEndpoints, getEndpointFromNode(ctx, cli, child, component, cachedSelectorNodeIP)...)
   102  	}
   103  	return serviceEndpoints
   104  }
   105  
   106  func getServiceEndpoints(ctx context.Context, cli client.Client, gvk schema.GroupVersionKind, name, namespace, cluster, component string, cachedSelectorNodeIP func() string) []querytypes.ServiceEndpoint {
   107  	var serviceEndpoints []querytypes.ServiceEndpoint
   108  	switch gvk.Kind {
   109  	case "Ingress":
   110  		if gvk.Group == networkv1beta1.GroupName && (gvk.Version == "v1beta1" || gvk.Version == "v1") {
   111  			var ingress v1.Ingress
   112  			ingress.SetGroupVersionKind(gvk)
   113  			if err := findResource(ctx, cli, &ingress, name, namespace, cluster); err != nil {
   114  				klog.Error(err, fmt.Sprintf("find v1 Ingress %s/%s from cluster %s failure", name, namespace, cluster))
   115  				return nil
   116  			}
   117  			serviceEndpoints = append(serviceEndpoints, generatorFromIngress(ingress, cluster, component)...)
   118  		} else {
   119  			klog.Warning("not support ingress version", "version", gvk)
   120  		}
   121  	case "Service":
   122  		var service corev1.Service
   123  		service.SetGroupVersionKind(gvk)
   124  		if err := findResource(ctx, cli, &service, name, namespace, cluster); err != nil {
   125  			klog.Error(err, fmt.Sprintf("find v1 Service %s/%s from cluster %s failure", name, namespace, cluster))
   126  			return nil
   127  		}
   128  		serviceEndpoints = append(serviceEndpoints, generatorFromService(service, cachedSelectorNodeIP, cluster, component, "")...)
   129  	case "SeldonDeployment":
   130  		obj := new(unstructured.Unstructured)
   131  		obj.SetGroupVersionKind(gvk)
   132  		if err := findResource(ctx, cli, obj, name, namespace, cluster); err != nil {
   133  			klog.Error(err, fmt.Sprintf("find v1 Seldon Deployment %s/%s from cluster %s failure", name, namespace, cluster))
   134  			return nil
   135  		}
   136  		anno := obj.GetAnnotations()
   137  		serviceName := "ambassador"
   138  		serviceNS := apis.DefaultKubeVelaNS
   139  		if anno != nil {
   140  			if anno[annoAmbassadorServiceName] != "" {
   141  				serviceName = anno[annoAmbassadorServiceName]
   142  			}
   143  			if anno[annoAmbassadorServiceNamespace] != "" {
   144  				serviceNS = anno[annoAmbassadorServiceNamespace]
   145  			}
   146  		}
   147  		var service corev1.Service
   148  		if err := findResource(ctx, cli, &service, serviceName, serviceNS, cluster); err != nil {
   149  			klog.Error(err, fmt.Sprintf("find v1 Service %s/%s from cluster %s failure", serviceName, serviceNS, cluster))
   150  			return nil
   151  		}
   152  		serviceEndpoints = append(serviceEndpoints, generatorFromService(service, cachedSelectorNodeIP, cluster, component, fmt.Sprintf("/seldon/%s/%s", namespace, name))...)
   153  	case "HTTPRoute":
   154  		var route gatewayv1beta1.HTTPRoute
   155  		route.SetGroupVersionKind(gvk)
   156  		if err := findResource(ctx, cli, &route, name, namespace, cluster); err != nil {
   157  			klog.Error(err, fmt.Sprintf("find HTTPRoute %s/%s from cluster %s failure", name, namespace, cluster))
   158  			return nil
   159  		}
   160  		serviceEndpoints = append(serviceEndpoints, generatorFromHTTPRoute(ctx, cli, route, cluster, component)...)
   161  	}
   162  	return serviceEndpoints
   163  }
   164  
   165  func findResource(ctx context.Context, cli client.Client, obj client.Object, name, namespace, cluster string) error {
   166  	obj.SetNamespace(namespace)
   167  	obj.SetName(name)
   168  	gctx, cancel := context.WithTimeout(ctx, time.Second*10)
   169  	defer cancel()
   170  	if err := cli.Get(multicluster.ContextWithClusterName(gctx, cluster),
   171  		client.ObjectKeyFromObject(obj), obj); err != nil {
   172  		if kerrors.IsNotFound(err) {
   173  			return nil
   174  		}
   175  		return err
   176  	}
   177  	return nil
   178  }
   179  
   180  func generatorFromService(service corev1.Service, selectorNodeIP func() string, cluster, component, path string) []querytypes.ServiceEndpoint {
   181  	var serviceEndpoints []querytypes.ServiceEndpoint
   182  
   183  	var objRef = corev1.ObjectReference{
   184  		Kind:            "Service",
   185  		Namespace:       service.ObjectMeta.Namespace,
   186  		Name:            service.ObjectMeta.Name,
   187  		UID:             service.UID,
   188  		APIVersion:      service.APIVersion,
   189  		ResourceVersion: service.ResourceVersion,
   190  	}
   191  
   192  	formatEndpoint := func(host, appProtocol string, portName string, portProtocol corev1.Protocol, portNum int32, inner bool) querytypes.ServiceEndpoint {
   193  		return querytypes.ServiceEndpoint{
   194  			Endpoint: querytypes.Endpoint{
   195  				Protocol:    portProtocol,
   196  				AppProtocol: &appProtocol,
   197  				Host:        host,
   198  				Port:        int(portNum),
   199  				PortName:    portName,
   200  				Path:        path,
   201  				Inner:       inner,
   202  			},
   203  			Ref:       objRef,
   204  			Cluster:   cluster,
   205  			Component: component,
   206  		}
   207  	}
   208  	switch service.Spec.Type {
   209  	case corev1.ServiceTypeLoadBalancer:
   210  		for _, port := range service.Spec.Ports {
   211  			appp := judgeAppProtocol(port.Port)
   212  			for _, ingress := range service.Status.LoadBalancer.Ingress {
   213  				if ingress.Hostname != "" {
   214  					serviceEndpoints = append(serviceEndpoints, formatEndpoint(ingress.Hostname, appp, port.Name, port.Protocol, port.Port, false))
   215  				}
   216  				if ingress.IP != "" {
   217  					serviceEndpoints = append(serviceEndpoints, formatEndpoint(ingress.IP, appp, port.Name, port.Protocol, port.Port, false))
   218  				}
   219  			}
   220  		}
   221  	case corev1.ServiceTypeNodePort:
   222  		for _, port := range service.Spec.Ports {
   223  			appp := judgeAppProtocol(port.Port)
   224  			serviceEndpoints = append(serviceEndpoints, formatEndpoint(selectorNodeIP(), appp, port.Name, port.Protocol, port.NodePort, false))
   225  		}
   226  	case corev1.ServiceTypeClusterIP, corev1.ServiceTypeExternalName:
   227  		for _, port := range service.Spec.Ports {
   228  			appp := judgeAppProtocol(port.Port)
   229  			serviceEndpoints = append(serviceEndpoints, formatEndpoint(fmt.Sprintf("%s.%s", service.Name, service.Namespace), appp, port.Name, port.Protocol, port.Port, true))
   230  		}
   231  	}
   232  	return serviceEndpoints
   233  }
   234  
   235  func generatorFromIngress(ingress v1.Ingress, cluster, component string) (serviceEndpoints []querytypes.ServiceEndpoint) {
   236  	getAppProtocol := func(host string) string {
   237  		if len(ingress.Spec.TLS) > 0 {
   238  			for _, tls := range ingress.Spec.TLS {
   239  				if len(tls.Hosts) > 0 && slices.Contains(tls.Hosts, host) {
   240  					return querytypes.HTTPS
   241  				}
   242  				if len(tls.Hosts) == 0 {
   243  					return querytypes.HTTPS
   244  				}
   245  			}
   246  		}
   247  		return querytypes.HTTP
   248  	}
   249  	// It depends on the Ingress Controller
   250  	getEndpointPort := func(appProtocol string) int {
   251  		if appProtocol == querytypes.HTTPS {
   252  			if port, err := strconv.Atoi(ingress.Annotations[apis.AnnoIngressControllerHTTPSPort]); port > 0 && err == nil {
   253  				return port
   254  			}
   255  			return 443
   256  		}
   257  		if port, err := strconv.Atoi(ingress.Annotations[apis.AnnoIngressControllerHTTPPort]); port > 0 && err == nil {
   258  			return port
   259  		}
   260  		return 80
   261  	}
   262  
   263  	// The host in rule maybe empty, means access the application by the Gateway Host(IP)
   264  	getHost := func(host string) string {
   265  		if host != "" {
   266  			return host
   267  		}
   268  		return ingress.Annotations[apis.AnnoIngressControllerHost]
   269  	}
   270  
   271  	for _, rule := range ingress.Spec.Rules {
   272  		var appProtocol = getAppProtocol(rule.Host)
   273  		var appPort = getEndpointPort(appProtocol)
   274  		if rule.HTTP != nil {
   275  			for _, path := range rule.HTTP.Paths {
   276  				serviceEndpoints = append(serviceEndpoints, querytypes.ServiceEndpoint{
   277  					Endpoint: querytypes.Endpoint{
   278  						Protocol:    corev1.ProtocolTCP,
   279  						AppProtocol: &appProtocol,
   280  						Host:        getHost(rule.Host),
   281  						Path:        path.Path,
   282  						Port:        appPort,
   283  					},
   284  					Ref: corev1.ObjectReference{
   285  						Kind:            "Ingress",
   286  						Namespace:       ingress.ObjectMeta.Namespace,
   287  						Name:            ingress.ObjectMeta.Name,
   288  						UID:             ingress.UID,
   289  						APIVersion:      ingress.APIVersion,
   290  						ResourceVersion: ingress.ResourceVersion,
   291  					},
   292  					Cluster:   cluster,
   293  					Component: component,
   294  				})
   295  			}
   296  		}
   297  	}
   298  	return serviceEndpoints
   299  }
   300  
   301  func getGatewayPortAndProtocol(ctx context.Context, cli client.Client, defaultNamespace, cluster string, parents []gatewayv1beta1.ParentReference) (string, int) {
   302  	for _, parent := range parents {
   303  		if parent.Kind != nil && *parent.Kind == "Gateway" {
   304  			var gateway gatewayv1beta1.Gateway
   305  			namespace := defaultNamespace
   306  			if parent.Namespace != nil {
   307  				namespace = string(*parent.Namespace)
   308  			}
   309  			if err := findResource(ctx, cli, &gateway, string(parent.Name), namespace, cluster); err != nil {
   310  				klog.Errorf("query the Gateway %s/%s/%s failure %s", cluster, namespace, string(parent.Name), err.Error())
   311  			}
   312  			var listener *gatewayv1beta1.Listener
   313  			if parent.SectionName != nil {
   314  				for i, lis := range gateway.Spec.Listeners {
   315  					if lis.Name == *parent.SectionName {
   316  						listener = &gateway.Spec.Listeners[i]
   317  						break
   318  					}
   319  				}
   320  			} else if len(gateway.Spec.Listeners) > 0 {
   321  				listener = &gateway.Spec.Listeners[0]
   322  			}
   323  			if listener != nil {
   324  				var protocol = querytypes.HTTP
   325  				if listener.Protocol == gatewayv1beta1.HTTPSProtocolType {
   326  					protocol = querytypes.HTTPS
   327  				}
   328  				var port = int(listener.Port)
   329  				// The gateway listener port may not be the externally exposed port.
   330  				// For example, the traefik addon has a default port mapping configuration of 8443->443 8000->80
   331  				// So users could set the `ports-mapping` annotation.
   332  				if mapping := gateway.Annotations["ports-mapping"]; mapping != "" {
   333  					for _, portItem := range strings.Split(mapping, ",") {
   334  						if portMap := strings.Split(portItem, ":"); len(portMap) == 2 {
   335  							if portMap[0] == fmt.Sprintf("%d", listener.Port) {
   336  								newPort, err := strconv.Atoi(portMap[1])
   337  								if err == nil {
   338  									port = newPort
   339  								}
   340  							}
   341  						}
   342  					}
   343  				}
   344  				return protocol, port
   345  			}
   346  		}
   347  	}
   348  	return querytypes.HTTP, 80
   349  }
   350  
   351  func generatorFromHTTPRoute(ctx context.Context, cli client.Client, route gatewayv1beta1.HTTPRoute, cluster, component string) []querytypes.ServiceEndpoint {
   352  	existPath := make(map[string]bool)
   353  	var serviceEndpoints []querytypes.ServiceEndpoint
   354  	for _, rule := range route.Spec.Rules {
   355  		for _, host := range route.Spec.Hostnames {
   356  			appProtocol, appPort := getGatewayPortAndProtocol(ctx, cli, route.Namespace, cluster, route.Spec.ParentRefs)
   357  			for _, match := range rule.Matches {
   358  				path := ""
   359  				if match.Path != nil && (match.Path.Type == nil || string(*match.Path.Type) == string(gatewayv1beta1.PathMatchPathPrefix)) {
   360  					path = *match.Path.Value
   361  				}
   362  				if !existPath[path] {
   363  					existPath[path] = true
   364  					serviceEndpoints = append(serviceEndpoints, querytypes.ServiceEndpoint{
   365  						Endpoint: querytypes.Endpoint{
   366  							Protocol:    corev1.ProtocolTCP,
   367  							AppProtocol: &appProtocol,
   368  							Host:        string(host),
   369  							Path:        path,
   370  							Port:        appPort,
   371  						},
   372  						Ref: corev1.ObjectReference{
   373  							Kind:            route.Kind,
   374  							Namespace:       route.ObjectMeta.Namespace,
   375  							Name:            route.ObjectMeta.Name,
   376  							UID:             route.UID,
   377  							APIVersion:      route.APIVersion,
   378  							ResourceVersion: route.ResourceVersion,
   379  						},
   380  						Cluster:   cluster,
   381  						Component: component,
   382  					})
   383  				}
   384  			}
   385  		}
   386  	}
   387  	return serviceEndpoints
   388  }
   389  
   390  func selectorNodeIP(ctx context.Context, clusterName string, client client.Client) string {
   391  	ctx, cancel := context.WithTimeout(ctx, time.Second*10)
   392  	defer cancel()
   393  	var nodes corev1.NodeList
   394  	if err := client.List(multicluster.ContextWithClusterName(ctx, clusterName), &nodes); err != nil {
   395  		return ""
   396  	}
   397  	if len(nodes.Items) == 0 {
   398  		return ""
   399  	}
   400  	return selectGatewayIP(nodes.Items)
   401  }
   402  
   403  // judgeAppProtocol  RFC-6335 and http://www.iana.org/assignments/service-names).
   404  func judgeAppProtocol(port int32) string {
   405  	switch port {
   406  	case 80, 8080:
   407  		return querytypes.HTTP
   408  	case 443:
   409  		return querytypes.HTTPS
   410  	case 3306:
   411  		return querytypes.Mysql
   412  	case 6379:
   413  		return querytypes.Redis
   414  	default:
   415  		return ""
   416  	}
   417  }
   418  
   419  // selectGatewayIP will choose one gateway IP from all nodes, it will pick up external IP first. If there isn't any, it will pick the first node's internal IP.
   420  func selectGatewayIP(nodes []corev1.Node) string {
   421  	var gatewayNode *corev1.Node
   422  	var workerNodes []corev1.Node
   423  	for i, node := range nodes {
   424  		if _, exist := node.Labels[apis.LabelNodeRoleGateway]; exist {
   425  			gatewayNode = &nodes[i]
   426  			break
   427  		} else if _, exist := node.Labels[apis.LabelNodeRoleWorker]; exist {
   428  			workerNodes = append(workerNodes, nodes[i])
   429  		}
   430  	}
   431  	var candidates = nodes
   432  	if gatewayNode != nil {
   433  		candidates = []corev1.Node{*gatewayNode}
   434  	} else if len(workerNodes) > 0 {
   435  		candidates = workerNodes
   436  	}
   437  
   438  	if len(candidates) == 0 {
   439  		return ""
   440  	}
   441  	var addressMaps = make([]map[corev1.NodeAddressType]string, 0)
   442  	for _, node := range candidates {
   443  		var addressMap = make(map[corev1.NodeAddressType]string)
   444  		for _, address := range node.Status.Addresses {
   445  			addressMap[address.Type] = address.Address
   446  		}
   447  		// first get external ip
   448  		if ip, exist := addressMap[corev1.NodeExternalIP]; exist {
   449  			return ip
   450  		}
   451  		addressMaps = append(addressMaps, addressMap)
   452  	}
   453  	return addressMaps[0][corev1.NodeInternalIP]
   454  }