github.com/terraform-modules-krish/terratest@v0.29.0/modules/k8s/service.go (about)

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/url"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/require"
    11  	corev1 "k8s.io/api/core/v1"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  
    14  	"github.com/terraform-modules-krish/terratest/modules/aws"
    15  	"github.com/terraform-modules-krish/terratest/modules/logger"
    16  	"github.com/terraform-modules-krish/terratest/modules/random"
    17  	"github.com/terraform-modules-krish/terratest/modules/retry"
    18  	"github.com/terraform-modules-krish/terratest/modules/testing"
    19  )
    20  
    21  // ListServices will look for services in the given namespace that match the given filters and return them. This will
    22  // fail the test if there is an error.
    23  func ListServices(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.Service {
    24  	service, err := ListServicesE(t, options, filters)
    25  	require.NoError(t, err)
    26  	return service
    27  }
    28  
    29  // ListServicesE will look for services in the given namespace that match the given filters and return them.
    30  func ListServicesE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.Service, error) {
    31  	clientset, err := GetKubernetesClientFromOptionsE(t, options)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	resp, err := clientset.CoreV1().Services(options.Namespace).List(context.Background(), filters)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  	return resp.Items, nil
    40  }
    41  
    42  // GetService returns a Kubernetes service resource in the provided namespace with the given name. This will
    43  // fail the test if there is an error.
    44  func GetService(t testing.TestingT, options *KubectlOptions, serviceName string) *corev1.Service {
    45  	service, err := GetServiceE(t, options, serviceName)
    46  	require.NoError(t, err)
    47  	return service
    48  }
    49  
    50  // GetServiceE returns a Kubernetes service resource in the provided namespace with the given name.
    51  func GetServiceE(t testing.TestingT, options *KubectlOptions, serviceName string) (*corev1.Service, error) {
    52  	clientset, err := GetKubernetesClientFromOptionsE(t, options)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	return clientset.CoreV1().Services(options.Namespace).Get(context.Background(), serviceName, metav1.GetOptions{})
    57  }
    58  
    59  // WaitUntilServiceAvailable waits until the service endpoint is ready to accept traffic.
    60  func WaitUntilServiceAvailable(t testing.TestingT, options *KubectlOptions, serviceName string, retries int, sleepBetweenRetries time.Duration) {
    61  	statusMsg := fmt.Sprintf("Wait for service %s to be provisioned.", serviceName)
    62  	message := retry.DoWithRetry(
    63  		t,
    64  		statusMsg,
    65  		retries,
    66  		sleepBetweenRetries,
    67  		func() (string, error) {
    68  			service, err := GetServiceE(t, options, serviceName)
    69  			if err != nil {
    70  				return "", err
    71  			}
    72  
    73  			isMinikube, err := IsMinikubeE(t, options)
    74  			if err != nil {
    75  				return "", err
    76  			}
    77  
    78  			// For minikube, all services will be available immediately so we only do the check if we are not on
    79  			// minikube.
    80  			if !isMinikube && !IsServiceAvailable(service) {
    81  				return "", NewServiceNotAvailableError(service)
    82  			}
    83  			return "Service is now available", nil
    84  		},
    85  	)
    86  	logger.Logf(t, message)
    87  }
    88  
    89  // IsServiceAvailable returns true if the service endpoint is ready to accept traffic. Note that for Minikube, this
    90  // function is moot as all services, even LoadBalancer, is available immediately.
    91  func IsServiceAvailable(service *corev1.Service) bool {
    92  	// Only the LoadBalancer type has a delay. All other service types are available if the resource exists.
    93  	switch service.Spec.Type {
    94  	case corev1.ServiceTypeLoadBalancer:
    95  		ingress := service.Status.LoadBalancer.Ingress
    96  		// The load balancer is ready if it has at least one ingress point
    97  		return len(ingress) > 0
    98  	default:
    99  		return true
   100  	}
   101  }
   102  
   103  // GetServiceEndpoint will return the service access point. If the service endpoint is not ready, will fail the test
   104  // immediately.
   105  func GetServiceEndpoint(t testing.TestingT, options *KubectlOptions, service *corev1.Service, servicePort int) string {
   106  	endpoint, err := GetServiceEndpointE(t, options, service, servicePort)
   107  	require.NoError(t, err)
   108  	return endpoint
   109  }
   110  
   111  // GetServiceEndpointE will return the service access point using the following logic:
   112  // - For ClusterIP service type, return the URL that maps to ClusterIP and Service Port
   113  // - For NodePort service type, identify the public IP of the node (if it exists, otherwise return the bound hostname),
   114  //   and the assigned node port for the provided service port, and return the URL that maps to node ip and node port.
   115  // - For LoadBalancer service type, return the publicly accessible hostname of the load balancer.
   116  //   If the hostname is empty, it will return the public IP of the LoadBalancer.
   117  // - All other service types are not supported.
   118  func GetServiceEndpointE(t testing.TestingT, options *KubectlOptions, service *corev1.Service, servicePort int) (string, error) {
   119  	switch service.Spec.Type {
   120  	case corev1.ServiceTypeClusterIP:
   121  		// ClusterIP service type will map directly to service port
   122  		return fmt.Sprintf("%s:%d", service.Spec.ClusterIP, servicePort), nil
   123  	case corev1.ServiceTypeNodePort:
   124  		return findEndpointForNodePortService(t, options, service, int32(servicePort))
   125  	case corev1.ServiceTypeLoadBalancer:
   126  		// For minikube, LoadBalancer service is exactly the same as NodePort service
   127  		isMinikube, err := IsMinikubeE(t, options)
   128  		if err != nil {
   129  			return "", err
   130  		}
   131  		if isMinikube {
   132  			return findEndpointForNodePortService(t, options, service, int32(servicePort))
   133  		}
   134  
   135  		ingress := service.Status.LoadBalancer.Ingress
   136  		if len(ingress) == 0 {
   137  			return "", NewServiceNotAvailableError(service)
   138  		}
   139  		if ingress[0].Hostname == "" {
   140  			return fmt.Sprintf("%s:%d", ingress[0].IP, servicePort), nil
   141  		}
   142  		// Load Balancer service type will map directly to service port
   143  		return fmt.Sprintf("%s:%d", ingress[0].Hostname, servicePort), nil
   144  	default:
   145  		return "", NewUnknownServiceTypeError(service)
   146  	}
   147  }
   148  
   149  // Extracts a endpoint that can be reached outside the kubernetes cluster. NodePort type needs to find the right
   150  // allocated node port mapped to the service port, as well as find out the externally reachable ip (if available).
   151  func findEndpointForNodePortService(
   152  	t testing.TestingT,
   153  	options *KubectlOptions,
   154  	service *corev1.Service,
   155  	servicePort int32,
   156  ) (string, error) {
   157  	nodePort, err := FindNodePortE(service, int32(servicePort))
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  	node, err := pickRandomNodeE(t, options)
   162  	if err != nil {
   163  		return "", err
   164  	}
   165  	nodeHostname, err := FindNodeHostnameE(t, node)
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	return fmt.Sprintf("%s:%d", nodeHostname, nodePort), nil
   170  }
   171  
   172  // Given the desired servicePort, return the allocated nodeport
   173  func FindNodePortE(service *corev1.Service, servicePort int32) (int32, error) {
   174  	for _, port := range service.Spec.Ports {
   175  		if port.Port == servicePort {
   176  			return port.NodePort, nil
   177  		}
   178  	}
   179  	return -1, NewUnknownServicePortError(service, servicePort)
   180  }
   181  
   182  // pickRandomNode will pick a random node in the kubernetes cluster
   183  func pickRandomNodeE(t testing.TestingT, options *KubectlOptions) (corev1.Node, error) {
   184  	nodes, err := GetNodesE(t, options)
   185  	if err != nil {
   186  		return corev1.Node{}, err
   187  	}
   188  	if len(nodes) == 0 {
   189  		return corev1.Node{}, NewNoNodesInKubernetesError()
   190  	}
   191  	index := random.Random(0, len(nodes)-1)
   192  	return nodes[index], nil
   193  }
   194  
   195  // Given a node, return the ip address, preferring the external IP
   196  func FindNodeHostnameE(t testing.TestingT, node corev1.Node) (string, error) {
   197  	nodeIDUri, err := url.Parse(node.Spec.ProviderID)
   198  	if err != nil {
   199  		return "", err
   200  	}
   201  	switch nodeIDUri.Scheme {
   202  	case "aws":
   203  		return findAwsNodeHostnameE(t, node, nodeIDUri)
   204  	default:
   205  		return findDefaultNodeHostnameE(node)
   206  	}
   207  }
   208  
   209  // findAwsNodeHostname will return the public ip of the node, assuming the node is an AWS EC2 instance.
   210  // If the instance does not have a public IP, will return the internal hostname as recorded on the Kubernetes node
   211  // object.
   212  func findAwsNodeHostnameE(t testing.TestingT, node corev1.Node, awsIDUri *url.URL) (string, error) {
   213  	// Path is /AVAILABILITY_ZONE/INSTANCE_ID
   214  	parts := strings.Split(awsIDUri.Path, "/")
   215  	if len(parts) != 3 {
   216  		return "", NewMalformedNodeIDError(&node)
   217  	}
   218  	instanceID := parts[2]
   219  	availabilityZone := parts[1]
   220  	// Availability Zone name is known to be region code + 1 letter
   221  	// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html
   222  	region := availabilityZone[:len(availabilityZone)-1]
   223  
   224  	ipMap, err := aws.GetPublicIpsOfEc2InstancesE(t, []string{instanceID}, region)
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  
   229  	publicIP, containsIP := ipMap[instanceID]
   230  	if !containsIP || publicIP == "" {
   231  		// return default hostname
   232  		return findDefaultNodeHostnameE(node)
   233  	}
   234  	return publicIP, nil
   235  }
   236  
   237  // findDefaultNodeHostname returns the hostname recorded on the Kubernetes node object.
   238  func findDefaultNodeHostnameE(node corev1.Node) (string, error) {
   239  	for _, address := range node.Status.Addresses {
   240  		if address.Type == corev1.NodeHostName {
   241  			return address.Address, nil
   242  		}
   243  	}
   244  	return "", NewNodeHasNoHostnameError(&node)
   245  }