github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/services.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"github.com/juju/errors"
    11  	core "k8s.io/api/core/v1"
    12  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    13  	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    15  
    16  	"github.com/juju/juju/caas/kubernetes/provider/application"
    17  	"github.com/juju/juju/caas/kubernetes/provider/constants"
    18  	k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs"
    19  	"github.com/juju/juju/caas/kubernetes/provider/utils"
    20  	k8sannotations "github.com/juju/juju/core/annotations"
    21  )
    22  
    23  func getServiceLabels(appName string, legacy bool) map[string]string {
    24  	return utils.LabelsForApp(appName, legacy)
    25  }
    26  
    27  func (k *kubernetesClient) ensureServicesForApp(appName, deploymentName string, annotations k8sannotations.Annotation, services []k8sspecs.K8sService) (cleanUps []func(), err error) {
    28  	for _, v := range services {
    29  		if v.Name == deploymentName {
    30  			return cleanUps, errors.NewNotValid(nil, fmt.Sprintf("%q is a reserved service name", deploymentName))
    31  		}
    32  		spec := &core.Service{
    33  			ObjectMeta: meta.ObjectMeta{
    34  				Name:        v.Name,
    35  				Namespace:   k.namespace,
    36  				Labels:      utils.LabelsMerge(v.Labels, getServiceLabels(appName, k.IsLegacyLabels())),
    37  				Annotations: annotations.Copy().Merge(v.Annotations),
    38  			},
    39  			Spec: v.Spec,
    40  		}
    41  		serviceCleanup, err := k.ensureK8sService(spec)
    42  		cleanUps = append(cleanUps, serviceCleanup)
    43  		if err != nil {
    44  			return cleanUps, errors.Trace(err)
    45  		}
    46  	}
    47  	return cleanUps, nil
    48  }
    49  
    50  // ensureK8sService ensures a k8s service resource.
    51  func (k *kubernetesClient) ensureK8sService(spec *core.Service) (func(), error) {
    52  	cleanUp := func() {}
    53  	if k.namespace == "" {
    54  		return cleanUp, errNoNamespace
    55  	}
    56  
    57  	api := k.client().CoreV1().Services(k.namespace)
    58  	// Set any immutable fields if the service already exists.
    59  	existing, err := api.Get(context.TODO(), spec.Name, meta.GetOptions{})
    60  	if err == nil {
    61  		spec.Spec.ClusterIP = existing.Spec.ClusterIP
    62  		spec.ObjectMeta.ResourceVersion = existing.ObjectMeta.ResourceVersion
    63  	}
    64  	_, err = api.Update(context.TODO(), spec, meta.UpdateOptions{})
    65  	if k8serrors.IsNotFound(err) {
    66  		var svcCreated *core.Service
    67  		svcCreated, err = api.Create(context.TODO(), spec, meta.CreateOptions{})
    68  		if err == nil {
    69  			cleanUp = func() { _ = k.deleteService(svcCreated.GetName()) }
    70  		}
    71  	}
    72  	return cleanUp, errors.Trace(err)
    73  }
    74  
    75  // deleteService deletes a service resource.
    76  func (k *kubernetesClient) deleteService(serviceName string) error {
    77  	if k.namespace == "" {
    78  		return errNoNamespace
    79  	}
    80  	services := k.client().CoreV1().Services(k.namespace)
    81  	err := services.Delete(context.TODO(), serviceName, meta.DeleteOptions{
    82  		PropagationPolicy: constants.DefaultPropagationPolicy(),
    83  	})
    84  	if k8serrors.IsNotFound(err) {
    85  		return nil
    86  	}
    87  	return errors.Trace(err)
    88  }
    89  
    90  func (k *kubernetesClient) deleteServices(appName string) error {
    91  	if k.namespace == "" {
    92  		return errNoNamespace
    93  	}
    94  	// Service API does not have `DeleteCollection` implemented, so we have to do it like this.
    95  	api := k.client().CoreV1().Services(k.namespace)
    96  	services, err := api.List(context.TODO(),
    97  		meta.ListOptions{
    98  			LabelSelector: utils.LabelsToSelector(
    99  				getServiceLabels(appName, k.IsLegacyLabels())).String(),
   100  		},
   101  	)
   102  	if err != nil {
   103  		return errors.Trace(err)
   104  	}
   105  	for _, svc := range services.Items {
   106  		if err := k.deleteService(svc.GetName()); err != nil {
   107  			if errors.IsNotFound(err) {
   108  				continue
   109  			}
   110  			return errors.Trace(err)
   111  		}
   112  	}
   113  	return nil
   114  }
   115  
   116  func findServiceForApplication(
   117  	ctx context.Context,
   118  	serviceI corev1.ServiceInterface,
   119  	appName string,
   120  	legacyLabels bool,
   121  ) (*core.Service, error) {
   122  	labels := utils.LabelsForApp(appName, legacyLabels)
   123  	servicesList, err := serviceI.List(context.TODO(), meta.ListOptions{
   124  		LabelSelector: utils.LabelsToSelector(labels).String(),
   125  	})
   126  
   127  	if err != nil {
   128  		return nil, errors.Annotatef(err, "finding service for application %s", appName)
   129  	}
   130  
   131  	if len(servicesList.Items) == 0 {
   132  		return nil, errors.NotFoundf("finding service for application %s", appName)
   133  	}
   134  
   135  	services := []core.Service{}
   136  	endpointSvcName := application.HeadlessServiceName(appName)
   137  	// We want to filter out the endpoints services made by juju as they should
   138  	// not be considered.
   139  	for _, svc := range servicesList.Items {
   140  		if svc.Name != endpointSvcName {
   141  			services = append(services, svc)
   142  		}
   143  	}
   144  
   145  	if len(services) != 1 {
   146  		return nil, errors.NotValidf("unable to handle mutiple services %d for application %s", len(servicesList.Items), appName)
   147  	}
   148  
   149  	return &services[0], nil
   150  }