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 }