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  }