github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/k8s/services.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package k8s provides a client for interacting with a Kubernetes cluster. 5 package k8s 6 7 import ( 8 "context" 9 "fmt" 10 "net/url" 11 "regexp" 12 "strconv" 13 14 "github.com/defenseunicorns/pkg/helpers" 15 corev1 "k8s.io/api/core/v1" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 ) 18 19 // See https://regex101.com/r/OWVfAO/1. 20 const serviceURLPattern = `^(?P<name>[^\.]+)\.(?P<namespace>[^\.]+)\.svc\.cluster\.local$` 21 22 // ServiceInfo contains information necessary for connecting to a cluster service. 23 type ServiceInfo struct { 24 Namespace string 25 Name string 26 Port int 27 } 28 29 // ReplaceService deletes and re-creates a service. 30 func (k *K8s) ReplaceService(service *corev1.Service) (*corev1.Service, error) { 31 if err := k.DeleteService(service.Namespace, service.Name); err != nil { 32 return nil, err 33 } 34 35 return k.CreateService(service) 36 } 37 38 // GenerateService returns a K8s service struct without writing to the cluster. 39 func (k *K8s) GenerateService(namespace, name string) *corev1.Service { 40 service := &corev1.Service{ 41 TypeMeta: metav1.TypeMeta{ 42 APIVersion: corev1.SchemeGroupVersion.String(), 43 Kind: "Service", 44 }, 45 ObjectMeta: metav1.ObjectMeta{ 46 Name: name, 47 Namespace: namespace, 48 Annotations: make(Labels), 49 }, 50 } 51 52 // Merge in common labels so that later modifications to the service can't mutate them 53 service.ObjectMeta.Labels = helpers.MergeMap[string](k.Labels, service.ObjectMeta.Labels) 54 55 return service 56 } 57 58 // DeleteService removes a service from the cluster by namespace and name. 59 func (k *K8s) DeleteService(namespace, name string) error { 60 return k.Clientset.CoreV1().Services(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) 61 } 62 63 // CreateService creates the given service in the cluster. 64 func (k *K8s) CreateService(service *corev1.Service) (*corev1.Service, error) { 65 createOptions := metav1.CreateOptions{} 66 return k.Clientset.CoreV1().Services(service.Namespace).Create(context.TODO(), service, createOptions) 67 } 68 69 // GetService returns a Kubernetes service resource in the provided namespace with the given name. 70 func (k *K8s) GetService(namespace, serviceName string) (*corev1.Service, error) { 71 return k.Clientset.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) 72 } 73 74 // GetServices returns a list of services in the provided namespace. To search all namespaces, pass "" in the namespace arg. 75 func (k *K8s) GetServices(namespace string) (*corev1.ServiceList, error) { 76 return k.Clientset.CoreV1().Services(namespace).List(context.TODO(), metav1.ListOptions{}) 77 } 78 79 // GetServicesByLabel returns a list of matched services given a label and value. To search all namespaces, pass "" in the namespace arg. 80 func (k *K8s) GetServicesByLabel(namespace, label, value string) (*corev1.ServiceList, error) { 81 // Create the selector and add the requirement 82 labelSelector, _ := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{ 83 MatchLabels: Labels{ 84 label: value, 85 }, 86 }) 87 88 // Run the query with the selector and return as a ServiceList 89 return k.Clientset.CoreV1().Services(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) 90 } 91 92 // GetServicesByLabelExists returns a list of matched services given a label. To search all namespaces, pass "" in the namespace arg. 93 func (k *K8s) GetServicesByLabelExists(namespace, label string) (*corev1.ServiceList, error) { 94 // Create the selector and add the requirement 95 labelSelector, _ := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{ 96 MatchExpressions: []metav1.LabelSelectorRequirement{{ 97 Key: label, 98 Operator: metav1.LabelSelectorOpExists, 99 }}, 100 }) 101 102 // Run the query with the selector and return as a ServiceList 103 return k.Clientset.CoreV1().Services(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) 104 } 105 106 // ServiceInfoFromNodePortURL takes a nodePortURL and parses it to find the service info for connecting to the cluster. The string is expected to follow the following format: 107 // Example nodePortURL: 127.0.0.1:{PORT}. 108 func (k *K8s) ServiceInfoFromNodePortURL(nodePortURL string) (*ServiceInfo, error) { 109 // Attempt to parse as normal, if this fails add a scheme to the URL (docker registries don't use schemes) 110 parsedURL, err := url.Parse(nodePortURL) 111 if err != nil { 112 parsedURL, err = url.Parse("scheme://" + nodePortURL) 113 if err != nil { 114 return nil, err 115 } 116 } 117 118 // Match hostname against localhost ip/hostnames 119 hostname := parsedURL.Hostname() 120 if hostname != helpers.IPV4Localhost && hostname != "localhost" { 121 return nil, fmt.Errorf("node port services should be on localhost") 122 } 123 124 // Get the node port from the nodeportURL. 125 nodePort, err := strconv.Atoi(parsedURL.Port()) 126 if err != nil { 127 return nil, err 128 } 129 if nodePort < 30000 || nodePort > 32767 { 130 return nil, fmt.Errorf("node port services should use the port range 30000-32767") 131 } 132 133 services, err := k.GetServices("") 134 if err != nil { 135 return nil, err 136 } 137 138 for _, svc := range services.Items { 139 if svc.Spec.Type == "NodePort" { 140 for _, port := range svc.Spec.Ports { 141 if int(port.NodePort) == nodePort { 142 return &ServiceInfo{ 143 Namespace: svc.Namespace, 144 Name: svc.Name, 145 Port: int(port.Port), 146 }, nil 147 } 148 } 149 } 150 } 151 152 return nil, fmt.Errorf("no matching node port services found") 153 } 154 155 // ServiceInfoFromServiceURL takes a serviceURL and parses it to find the service info for connecting to the cluster. The string is expected to follow the following format: 156 // Example serviceURL: http://{SERVICE_NAME}.{NAMESPACE}.svc.cluster.local:{PORT}. 157 func ServiceInfoFromServiceURL(serviceURL string) (*ServiceInfo, error) { 158 parsedURL, err := url.Parse(serviceURL) 159 if err != nil { 160 return nil, err 161 } 162 163 // Get the remote port from the serviceURL. 164 remotePort, err := strconv.Atoi(parsedURL.Port()) 165 if err != nil { 166 return nil, err 167 } 168 169 // Match hostname against local cluster service format. 170 pattern := regexp.MustCompile(serviceURLPattern) 171 get, err := helpers.MatchRegex(pattern, parsedURL.Hostname()) 172 173 // If incomplete match, return an error. 174 if err != nil { 175 return nil, err 176 } 177 178 return &ServiceInfo{ 179 Namespace: get("namespace"), 180 Name: get("name"), 181 Port: remotePort, 182 }, nil 183 }