github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/kube/services/services.go (about)

     1  package services
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/jenkins-x/jx-logging/pkg/log"
    12  	"github.com/olli-ai/jx/v2/pkg/kube"
    13  
    14  	"github.com/olli-ai/jx/v2/pkg/util"
    15  	"github.com/pkg/errors"
    16  	v1 "k8s.io/api/core/v1"
    17  	"k8s.io/api/extensions/v1beta1"
    18  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/fields"
    20  	"k8s.io/apimachinery/pkg/util/wait"
    21  	"k8s.io/apimachinery/pkg/watch"
    22  	"k8s.io/client-go/kubernetes"
    23  	tools_watch "k8s.io/client-go/tools/watch"
    24  )
    25  
    26  const (
    27  	ExposeAnnotation             = "fabric8.io/expose"
    28  	ExposeURLAnnotation          = "fabric8.io/exposeUrl"
    29  	ExposeGeneratedByAnnotation  = "fabric8.io/generated-by"
    30  	ExposeIngressName            = "fabric8.io/ingress.name"
    31  	JenkinsXSkipTLSAnnotation    = "jenkins-x.io/skip.tls"
    32  	ExposeIngressAnnotation      = "fabric8.io/ingress.annotations"
    33  	CertManagerAnnotation        = "certmanager.k8s.io/issuer"
    34  	CertManagerClusterAnnotation = "certmanager.k8s.io/cluster-issuer"
    35  	ServiceAppLabel              = "app"
    36  )
    37  
    38  type ServiceURL struct {
    39  	Name string
    40  	URL  string
    41  }
    42  
    43  func GetServices(client kubernetes.Interface, ns string) (map[string]*v1.Service, error) {
    44  	answer := map[string]*v1.Service{}
    45  	list, err := client.CoreV1().Services(ns).List(meta_v1.ListOptions{})
    46  	if err != nil {
    47  		return answer, fmt.Errorf("failed to load Services %s", err)
    48  	}
    49  	for _, r := range list.Items {
    50  		name := r.Name
    51  		copy := r
    52  		answer[name] = &copy
    53  	}
    54  	return answer, nil
    55  }
    56  
    57  // GetServicesByName returns a list of Service objects from a list of service names
    58  func GetServicesByName(client kubernetes.Interface, ns string, services []string) ([]*v1.Service, error) {
    59  	answer := make([]*v1.Service, 0)
    60  	svcList, err := client.CoreV1().Services(ns).List(meta_v1.ListOptions{})
    61  	if err != nil {
    62  		return answer, errors.Wrapf(err, "listing the services in namespace %q", ns)
    63  	}
    64  	for _, s := range svcList.Items {
    65  		i := util.StringArrayIndex(services, s.GetName())
    66  		if i > 0 {
    67  			copy := s
    68  			answer = append(answer, &copy)
    69  		}
    70  	}
    71  	return answer, nil
    72  }
    73  
    74  func GetServiceNames(client kubernetes.Interface, ns string, filter string) ([]string, error) {
    75  	names := []string{}
    76  	list, err := client.CoreV1().Services(ns).List(meta_v1.ListOptions{})
    77  	if err != nil {
    78  		return names, fmt.Errorf("failed to load Services %s", err)
    79  	}
    80  	for _, r := range list.Items {
    81  		name := r.Name
    82  		if filter == "" || strings.Contains(name, filter) {
    83  			names = append(names, name)
    84  		}
    85  	}
    86  	sort.Strings(names)
    87  	return names, nil
    88  }
    89  
    90  func GetServiceURLFromMap(services map[string]*v1.Service, name string) string {
    91  	return GetServiceURL(services[name])
    92  }
    93  
    94  func FindServiceURL(client kubernetes.Interface, namespace string, name string) (string, error) {
    95  	log.Logger().Debugf("Finding service url for %s in namespace %s", name, namespace)
    96  	svc, err := client.CoreV1().Services(namespace).Get(name, meta_v1.GetOptions{})
    97  	if err != nil {
    98  		return "", errors.Wrapf(err, "finding the service %s in namespace %s", name, namespace)
    99  	}
   100  	answer := GetServiceURL(svc)
   101  	if answer != "" {
   102  		log.Logger().Debugf("Found service url %s", answer)
   103  		return answer, nil
   104  	}
   105  
   106  	log.Logger().Debugf("Couldn't find service url, attempting to look up via ingress")
   107  
   108  	// lets try find the service via Ingress
   109  	ing, err := client.ExtensionsV1beta1().Ingresses(namespace).Get(name, meta_v1.GetOptions{})
   110  	if err != nil {
   111  		log.Logger().Debugf("Unable to finding ingress for %s in namespace %s - err %s", name, namespace, err)
   112  		return "", errors.Wrapf(err, "getting ingress for service %q in namespace %s", name, namespace)
   113  	}
   114  
   115  	url := IngressURL(ing)
   116  	if url == "" {
   117  		log.Logger().Debugf("Unable to find service url via ingress for %s in namespace %s", name, namespace)
   118  	}
   119  	return url, nil
   120  }
   121  
   122  func FindIngressURL(client kubernetes.Interface, namespace string, name string) (string, error) {
   123  	log.Logger().Debugf("Finding ingress url for %s in namespace %s", name, namespace)
   124  	// lets try find the service via Ingress
   125  	ing, err := client.ExtensionsV1beta1().Ingresses(namespace).Get(name, meta_v1.GetOptions{})
   126  	if err != nil {
   127  		log.Logger().Debugf("Error finding ingress for %s in namespace %s - err %s", name, namespace, err)
   128  		return "", nil
   129  	}
   130  
   131  	url := IngressURL(ing)
   132  	if url == "" {
   133  		log.Logger().Debugf("Unable to find url via ingress for %s in namespace %s", name, namespace)
   134  	}
   135  	return url, nil
   136  }
   137  
   138  // IngressURL returns the URL for the ingres
   139  func IngressURL(ing *v1beta1.Ingress) string {
   140  	if ing != nil {
   141  		if len(ing.Spec.Rules) > 0 {
   142  			rule := ing.Spec.Rules[0]
   143  			hostname := rule.Host
   144  			for _, tls := range ing.Spec.TLS {
   145  				for _, h := range tls.Hosts {
   146  					if h != "" {
   147  						url := "https://" + h
   148  						log.Logger().Debugf("found service url %s", url)
   149  						return url
   150  					}
   151  				}
   152  			}
   153  			if hostname != "" {
   154  				url := "http://" + hostname
   155  				log.Logger().Debugf("found service url %s", url)
   156  				return url
   157  			}
   158  		}
   159  	}
   160  	return ""
   161  }
   162  
   163  // IngressHost returns the host for the ingres
   164  func IngressHost(ing *v1beta1.Ingress) string {
   165  	if ing != nil {
   166  		if len(ing.Spec.Rules) > 0 {
   167  			rule := ing.Spec.Rules[0]
   168  			hostname := rule.Host
   169  			for _, tls := range ing.Spec.TLS {
   170  				for _, h := range tls.Hosts {
   171  					if h != "" {
   172  						return h
   173  					}
   174  				}
   175  			}
   176  			if hostname != "" {
   177  				return hostname
   178  			}
   179  		}
   180  	}
   181  	return ""
   182  }
   183  
   184  // IngressProtocol returns the scheme (https / http) for the Ingress
   185  func IngressProtocol(ing *v1beta1.Ingress) string {
   186  	if ing != nil && len(ing.Spec.TLS) == 0 {
   187  		return "http"
   188  	}
   189  	return "https"
   190  }
   191  
   192  func FindServiceHostname(client kubernetes.Interface, namespace string, name string) (string, error) {
   193  	// lets try find the service via Ingress
   194  	ing, err := client.ExtensionsV1beta1().Ingresses(namespace).Get(name, meta_v1.GetOptions{})
   195  	if ing != nil && err == nil {
   196  		if len(ing.Spec.Rules) > 0 {
   197  			rule := ing.Spec.Rules[0]
   198  			hostname := rule.Host
   199  			for _, tls := range ing.Spec.TLS {
   200  				for _, h := range tls.Hosts {
   201  					if h != "" {
   202  						return h, nil
   203  					}
   204  				}
   205  			}
   206  			if hostname != "" {
   207  				return hostname, nil
   208  			}
   209  		}
   210  	}
   211  	return "", nil
   212  }
   213  
   214  // FindService looks up a service by name across all namespaces
   215  func FindService(client kubernetes.Interface, name string) (*v1.Service, error) {
   216  	nsl, err := client.CoreV1().Namespaces().List(meta_v1.ListOptions{})
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	for _, ns := range nsl.Items {
   221  		svc, err := client.CoreV1().Services(ns.GetName()).Get(name, meta_v1.GetOptions{})
   222  		if err == nil {
   223  			return svc, nil
   224  		}
   225  	}
   226  	return nil, errors.New("Service not found!")
   227  }
   228  
   229  // GetServiceURL returns the
   230  func GetServiceURL(svc *v1.Service) string {
   231  	url := ""
   232  	if svc != nil && svc.Annotations != nil {
   233  		url = svc.Annotations[ExposeURLAnnotation]
   234  	}
   235  	if url == "" {
   236  		scheme := "http"
   237  		for _, port := range svc.Spec.Ports {
   238  			if port.Port == 443 {
   239  				scheme = "https"
   240  				break
   241  			}
   242  		}
   243  
   244  		// lets check if its a LoadBalancer
   245  		if svc.Spec.Type == v1.ServiceTypeLoadBalancer {
   246  			for _, ing := range svc.Status.LoadBalancer.Ingress {
   247  				if ing.IP != "" {
   248  					return scheme + "://" + ing.IP + "/"
   249  				}
   250  				if ing.Hostname != "" {
   251  					return scheme + "://" + ing.Hostname + "/"
   252  				}
   253  			}
   254  		}
   255  	}
   256  	return url
   257  }
   258  
   259  // FindServiceSchemePort parses the service definition and interprets http scheme in the absence of an external ingress
   260  func FindServiceSchemePort(client kubernetes.Interface, namespace string, name string) (string, string, error) {
   261  	svc, err := client.CoreV1().Services(namespace).Get(name, meta_v1.GetOptions{})
   262  	if err != nil {
   263  		return "", "", errors.Wrapf(err, "failed to find service %s in namespace %s", name, namespace)
   264  	}
   265  	return ExtractServiceSchemePort(svc)
   266  }
   267  
   268  func GetServiceURLFromName(c kubernetes.Interface, name, ns string) (string, error) {
   269  	return FindServiceURL(c, ns, name)
   270  }
   271  
   272  func FindServiceURLs(client kubernetes.Interface, namespace string) ([]ServiceURL, error) {
   273  	options := meta_v1.ListOptions{}
   274  	urls := []ServiceURL{}
   275  	svcs, err := client.CoreV1().Services(namespace).List(options)
   276  	if err != nil {
   277  		return urls, err
   278  	}
   279  	for _, s := range svcs.Items {
   280  		svc := s
   281  		url := GetServiceURL(&svc)
   282  		if url == "" {
   283  			url, _ = FindServiceURL(client, namespace, svc.Name)
   284  		}
   285  		if len(url) > 0 {
   286  			urls = append(urls, ServiceURL{
   287  				Name: svc.Name,
   288  				URL:  url,
   289  			})
   290  		}
   291  	}
   292  	return urls, nil
   293  }
   294  
   295  // WaitForExternalIP waits for the pods of a deployment to become ready
   296  func WaitForExternalIP(client kubernetes.Interface, name, namespace string, timeout time.Duration) error {
   297  
   298  	options := meta_v1.ListOptions{
   299  		FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(),
   300  	}
   301  
   302  	w, err := client.CoreV1().Services(namespace).Watch(options)
   303  
   304  	if err != nil {
   305  		return err
   306  	}
   307  	defer w.Stop()
   308  
   309  	condition := func(event watch.Event) (bool, error) {
   310  		svc := event.Object.(*v1.Service)
   311  		return HasExternalAddress(svc), nil
   312  	}
   313  
   314  	ctx, _ := context.WithTimeout(context.Background(), timeout)
   315  	_, err = tools_watch.UntilWithoutRetry(ctx, w, condition)
   316  
   317  	if err == wait.ErrWaitTimeout {
   318  		return fmt.Errorf("service %s never became ready", name)
   319  	}
   320  	return nil
   321  }
   322  
   323  // WaitForService waits for a service to become ready
   324  func WaitForService(client kubernetes.Interface, name, namespace string, timeout time.Duration) error {
   325  	options := meta_v1.ListOptions{
   326  		FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(),
   327  	}
   328  	w, err := client.CoreV1().Services(namespace).Watch(options)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	defer w.Stop()
   333  
   334  	condition := func(event watch.Event) (bool, error) {
   335  		svc := event.Object.(*v1.Service)
   336  		return svc.GetName() == name, nil
   337  	}
   338  	ctx, _ := context.WithTimeout(context.Background(), timeout)
   339  	_, err = tools_watch.UntilWithoutRetry(ctx, w, condition)
   340  
   341  	if err == wait.ErrWaitTimeout {
   342  		return fmt.Errorf("service %s never became ready", name)
   343  	}
   344  
   345  	return nil
   346  }
   347  
   348  func HasExternalAddress(svc *v1.Service) bool {
   349  	for _, v := range svc.Status.LoadBalancer.Ingress {
   350  		if v.IP != "" || v.Hostname != "" {
   351  			return true
   352  		}
   353  	}
   354  	return false
   355  }
   356  
   357  func CreateServiceLink(client kubernetes.Interface, currentNamespace, targetNamespace, serviceName, externalURL string) error {
   358  	annotations := make(map[string]string)
   359  	annotations[ExposeURLAnnotation] = externalURL
   360  
   361  	svc := v1.Service{
   362  		ObjectMeta: meta_v1.ObjectMeta{
   363  			Name:        serviceName,
   364  			Namespace:   currentNamespace,
   365  			Annotations: annotations,
   366  		},
   367  		Spec: v1.ServiceSpec{
   368  			Type:         v1.ServiceTypeExternalName,
   369  			ExternalName: fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, targetNamespace),
   370  		},
   371  	}
   372  
   373  	_, err := client.CoreV1().Services(currentNamespace).Create(&svc)
   374  	if err != nil {
   375  		return err
   376  	}
   377  
   378  	return nil
   379  }
   380  
   381  func DeleteService(client *kubernetes.Clientset, namespace string, serviceName string) error {
   382  	return client.CoreV1().Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{})
   383  }
   384  
   385  func GetService(client kubernetes.Interface, currentNamespace, targetNamespace, serviceName string) error {
   386  	svc := v1.Service{
   387  		ObjectMeta: meta_v1.ObjectMeta{
   388  			Name:      serviceName,
   389  			Namespace: currentNamespace,
   390  		},
   391  		Spec: v1.ServiceSpec{
   392  			Type:         v1.ServiceTypeExternalName,
   393  			ExternalName: fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, targetNamespace),
   394  		},
   395  	}
   396  	_, err := client.CoreV1().Services(currentNamespace).Create(&svc)
   397  	if err != nil {
   398  		return err
   399  	}
   400  	return nil
   401  }
   402  
   403  func IsServicePresent(c kubernetes.Interface, name, ns string) (bool, error) {
   404  	svc, err := c.CoreV1().Services(ns).Get(name, meta_v1.GetOptions{})
   405  	if err != nil || svc == nil {
   406  		return false, err
   407  	}
   408  	return true, nil
   409  }
   410  
   411  // GetServiceAppName retrieves the application name from the service labels
   412  func GetServiceAppName(c kubernetes.Interface, name, ns string) (string, error) {
   413  	svc, err := c.CoreV1().Services(ns).Get(name, meta_v1.GetOptions{})
   414  	if err != nil || svc == nil {
   415  		return "", errors.Wrapf(err, "retrieving service %q", name)
   416  	}
   417  	return ServiceAppName(svc), nil
   418  }
   419  
   420  // ServiceAppName retrives the application name from service labels. If no app lable exists,
   421  // it returns the service name
   422  func ServiceAppName(service *v1.Service) string {
   423  	if annotations := service.Annotations; annotations != nil {
   424  		ingName, ok := annotations[ExposeIngressName]
   425  		if ok {
   426  			return ingName
   427  		}
   428  	}
   429  	if labels := service.Labels; labels != nil {
   430  		app, ok := labels[ServiceAppLabel]
   431  		if ok {
   432  			return app
   433  		}
   434  	}
   435  	return service.GetName()
   436  }
   437  
   438  // AnnotateServicesWithCertManagerIssuer adds the cert-manager annotation to the services from the given namespace. If a list of
   439  // services is provided, it will apply the annotation only to that specific services.
   440  func AnnotateServicesWithCertManagerIssuer(c kubernetes.Interface, ns, issuer string, clusterIssuer bool, services ...string) ([]*v1.Service, error) {
   441  	result := make([]*v1.Service, 0)
   442  	svcList, err := GetServices(c, ns)
   443  	if err != nil {
   444  		return result, err
   445  	}
   446  
   447  	for _, s := range svcList {
   448  		// annotate only the services present in the list, if the list is empty annotate all services
   449  		if len(services) > 0 {
   450  			i := util.StringArrayIndex(services, s.GetName())
   451  			if i < 0 {
   452  				continue
   453  			}
   454  		}
   455  		if s.Annotations[ExposeAnnotation] == "true" && s.Annotations[JenkinsXSkipTLSAnnotation] != "true" {
   456  			existingAnnotations, _ := s.Annotations[ExposeIngressAnnotation]
   457  			// if no existing `fabric8.io/ingress.annotations` initialise and add else update with ClusterIssuer
   458  			certManagerAnnotation := CertManagerAnnotation
   459  			if clusterIssuer == true {
   460  				certManagerAnnotation = CertManagerClusterAnnotation
   461  			}
   462  			if len(existingAnnotations) > 0 {
   463  				s.Annotations[ExposeIngressAnnotation] = existingAnnotations + "\n" + certManagerAnnotation + ": " + issuer
   464  			} else {
   465  				s.Annotations[ExposeIngressAnnotation] = certManagerAnnotation + ": " + issuer
   466  			}
   467  			s, err = c.CoreV1().Services(ns).Update(s)
   468  			if err != nil {
   469  				return result, fmt.Errorf("failed to annotate and update service %s in namespace %s: %v", s.Name, ns, err)
   470  			}
   471  			result = append(result, s)
   472  		}
   473  	}
   474  	return result, nil
   475  }
   476  
   477  // AnnotateServicesWithBasicAuth annotates the services with nginx baisc auth annotations
   478  func AnnotateServicesWithBasicAuth(client kubernetes.Interface, ns string, services ...string) error {
   479  	if len(services) == 0 {
   480  		return nil
   481  	}
   482  	svcList, err := GetServices(client, ns)
   483  	if err != nil {
   484  		return errors.Wrapf(err, "retrieving the services from namespace %q", ns)
   485  	}
   486  	for _, service := range svcList {
   487  		// Check if the service is in the white-list
   488  		idx := util.StringArrayIndex(services, service.GetName())
   489  		if idx < 0 {
   490  			continue
   491  		}
   492  		if service.Annotations == nil {
   493  			service.Annotations = map[string]string{}
   494  		}
   495  		// Add the required basic authentication annotation for nginx-ingress controller
   496  		ingressAnnotations := service.Annotations[ExposeIngressAnnotation]
   497  		basicAuthAnnotations := fmt.Sprintf(
   498  			"nginx.ingress.kubernetes.io/auth-type: basic\nnginx.ingress.kubernetes.io/auth-secret: %s\nnginx.ingress.kubernetes.io/auth-realm: Authentication is required to access this service",
   499  			kube.SecretBasicAuth)
   500  		if ingressAnnotations != "" {
   501  			ingressAnnotations = ingressAnnotations + "\n" + basicAuthAnnotations
   502  		} else {
   503  			ingressAnnotations = basicAuthAnnotations
   504  		}
   505  		service.Annotations[ExposeIngressAnnotation] = ingressAnnotations
   506  		_, err = client.CoreV1().Services(ns).Update(service)
   507  		if err != nil {
   508  			return errors.Wrapf(err, "updating the service %q in namesapce %q", service.GetName(), ns)
   509  		}
   510  	}
   511  	return nil
   512  }
   513  
   514  func CleanServiceAnnotations(c kubernetes.Interface, ns string, services ...string) error {
   515  	svcList, err := GetServices(c, ns)
   516  	if err != nil {
   517  		return err
   518  	}
   519  	for _, s := range svcList {
   520  		// clear the annotations only for the services provided in the list if the list
   521  		// is not empty, otherwise clear the annotations of all services
   522  		if len(services) > 0 {
   523  			i := util.StringArrayIndex(services, s.GetName())
   524  			if i < 0 {
   525  				continue
   526  			}
   527  		}
   528  		if s.Annotations[ExposeAnnotation] == "true" && s.Annotations[JenkinsXSkipTLSAnnotation] != "true" {
   529  			// if no existing `fabric8.io/ingress.annotations` initialise and add else update with ClusterIssuer
   530  			annotationsForIngress := s.Annotations[ExposeIngressAnnotation]
   531  			if len(annotationsForIngress) > 0 {
   532  
   533  				var newAnnotations []string
   534  				annotations := strings.Split(annotationsForIngress, "\n")
   535  				for _, element := range annotations {
   536  					annotation := strings.SplitN(element, ":", 2)
   537  					key, _ := annotation[0], strings.TrimSpace(annotation[1])
   538  					if key != CertManagerAnnotation && key != CertManagerClusterAnnotation {
   539  						newAnnotations = append(newAnnotations, element)
   540  					}
   541  				}
   542  				annotationsForIngress = ""
   543  				for _, v := range newAnnotations {
   544  					if len(annotationsForIngress) > 0 {
   545  						annotationsForIngress = annotationsForIngress + "\n" + v
   546  					} else {
   547  						annotationsForIngress = v
   548  					}
   549  				}
   550  				s.Annotations[ExposeIngressAnnotation] = annotationsForIngress
   551  
   552  			}
   553  			delete(s.Annotations, ExposeURLAnnotation)
   554  
   555  			_, err = c.CoreV1().Services(ns).Update(s)
   556  			if err != nil {
   557  				return fmt.Errorf("failed to clean service %s annotations in namespace %s: %v", s.Name, ns, err)
   558  			}
   559  		}
   560  	}
   561  	return nil
   562  }
   563  
   564  // ExtractServiceSchemePort is a utility function to interpret http scheme and port information from k8s service definitions
   565  func ExtractServiceSchemePort(svc *v1.Service) (string, string, error) {
   566  	scheme := ""
   567  	port := ""
   568  
   569  	found := false
   570  
   571  	// Search in order of degrading priority
   572  	for _, p := range svc.Spec.Ports {
   573  		if p.Port == 443 { // Prefer 443/https if found
   574  			scheme = "https"
   575  			port = "443"
   576  			found = true
   577  			break
   578  		}
   579  	}
   580  
   581  	if !found {
   582  		for _, p := range svc.Spec.Ports {
   583  			if p.Port == 80 { // Use 80/http if found
   584  				scheme = "http"
   585  				port = "80"
   586  				found = true
   587  			}
   588  		}
   589  	}
   590  
   591  	if !found { // No conventional ports, so search for named https ports
   592  		for _, p := range svc.Spec.Ports {
   593  			if p.Protocol == "TCP" {
   594  				if p.Name == "https" {
   595  					scheme = "https"
   596  					port = strconv.FormatInt(int64(p.Port), 10)
   597  					found = true
   598  					break
   599  				}
   600  			}
   601  		}
   602  	}
   603  
   604  	if !found { // No conventional ports, so search for named http ports
   605  		for _, p := range svc.Spec.Ports {
   606  			if p.Name == "http" {
   607  				scheme = "http"
   608  				port = strconv.FormatInt(int64(p.Port), 10)
   609  				found = true
   610  				break
   611  			}
   612  		}
   613  	}
   614  
   615  	return scheme, port, nil
   616  }