...

Source file src/github.com/redhat-developer/odo/pkg/service/service.go

Documentation: github.com/redhat-developer/odo/pkg/service

     1  package service
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/redhat-developer/odo/pkg/libdevfile"
     8  	"github.com/redhat-developer/odo/pkg/log"
     9  
    10  	devfile "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
    11  	"github.com/devfile/library/v2/pkg/devfile/parser"
    12  	devfilefs "github.com/devfile/library/v2/pkg/testingutil/filesystem"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    15  	"k8s.io/klog"
    16  
    17  	"github.com/redhat-developer/odo/pkg/kclient"
    18  	odolabels "github.com/redhat-developer/odo/pkg/labels"
    19  
    20  	olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
    21  )
    22  
    23  // LinkLabel is the name of the name of the link in the devfile
    24  const LinkLabel = "app.kubernetes.io/link-name"
    25  
    26  // ServiceLabel is the name of the service in the service binding object
    27  const ServiceLabel = "app.kubernetes.io/service-name"
    28  
    29  // ServiceKind is the kind of the service in the service binding object
    30  const ServiceKind = "app.kubernetes.io/service-kind"
    31  
    32  // IsLinkSecret helps in identifying if a secret is related to Service Binding
    33  func IsLinkSecret(labels map[string]string) bool {
    34  	_, hasLinkLabel := labels[LinkLabel]
    35  	_, hasServiceLabel := labels[ServiceLabel]
    36  	_, hasServiceKindLabel := labels[ServiceKind]
    37  	return hasLinkLabel && hasServiceLabel && hasServiceKindLabel
    38  }
    39  
    40  // DeleteOperatorService deletes an Operator backed service
    41  // TODO: make it unlink the service from component as a part of
    42  // https://github.com/redhat-developer/odo/issues/3563
    43  func DeleteOperatorService(client kclient.ClientInterface, serviceName string) error {
    44  	kind, name, err := SplitServiceKindName(serviceName)
    45  	if err != nil {
    46  		return fmt.Errorf("refer %q to see list of running services: %w", serviceName, err)
    47  	}
    48  
    49  	csv, err := client.GetCSVWithCR(kind)
    50  	if err != nil {
    51  		return err
    52  	}
    53  
    54  	if csv == nil {
    55  		return fmt.Errorf("unable to find any Operator providing the service %q", kind)
    56  	}
    57  
    58  	crs := client.GetCustomResourcesFromCSV(csv)
    59  	var cr *olm.CRDDescription
    60  
    61  	for _, c := range *crs {
    62  		customResource := c
    63  		if customResource.Kind == kind {
    64  			cr = &customResource
    65  			break
    66  		}
    67  	}
    68  
    69  	return client.DeleteDynamicResource(name, kclient.GetGVRFromCR(cr), false)
    70  }
    71  
    72  // ListOperatorServices lists all operator backed services.
    73  // It returns list of services, slice of services that it failed (if any) to list and error (if any)
    74  func ListOperatorServices(client kclient.ClientInterface) ([]unstructured.Unstructured, []string, error) {
    75  	klog.V(4).Info("Getting list of services")
    76  
    77  	// First let's get the list of all the operators in the namespace
    78  	csvs, err := client.ListClusterServiceVersions()
    79  	if err != nil {
    80  		return nil, nil, err
    81  	}
    82  
    83  	if err != nil {
    84  		return nil, nil, fmt.Errorf("unable to list operator backed services: %w", err)
    85  	}
    86  
    87  	var allCRInstances []unstructured.Unstructured
    88  	var failedListingCR []string
    89  
    90  	// let's get the Services a.k.a Custom Resources (CR) defined by each operator, one by one
    91  	for _, csv := range csvs.Items {
    92  		clusterServiceVersion := csv
    93  		klog.V(4).Infof("Getting services started from operator: %s", clusterServiceVersion.Name)
    94  		customResources := client.GetCustomResourcesFromCSV(&clusterServiceVersion)
    95  
    96  		// list and write active instances of each service/CR
    97  		var instances []unstructured.Unstructured
    98  		for _, cr := range *customResources {
    99  			customResource := cr
   100  
   101  			list, err := GetCRInstances(client, &customResource)
   102  			if err != nil {
   103  				crName := strings.Join([]string{csv.Name, cr.Kind}, "/")
   104  				klog.V(4).Infof("Failed to list instances of %q with error: %s", crName, err.Error())
   105  				failedListingCR = append(failedListingCR, crName)
   106  				continue
   107  			}
   108  
   109  			if len(list.Items) > 0 {
   110  				instances = append(instances, list.Items...)
   111  			}
   112  		}
   113  
   114  		// assuming there are more than one instances of a CR
   115  		allCRInstances = append(allCRInstances, instances...)
   116  	}
   117  
   118  	return allCRInstances, failedListingCR, nil
   119  }
   120  
   121  // GetCRInstances fetches and returns instances of the CR provided in the
   122  // "customResource" field. It also returns error (if any)
   123  func GetCRInstances(client kclient.ClientInterface, customResource *olm.CRDDescription) (*unstructured.UnstructuredList, error) {
   124  	klog.V(4).Infof("Getting instances of: %s\n", customResource.Name)
   125  
   126  	instances, err := client.ListDynamicResources("", kclient.GetGVRFromCR(customResource), "")
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	return instances, nil
   132  }
   133  
   134  // SplitServiceKindName splits the service name provided for deletion by the
   135  // user. It has to be of the format <service-kind>/<service-name>. Example: EtcdCluster/myetcd
   136  func SplitServiceKindName(serviceName string) (string, string, error) {
   137  	sn := strings.SplitN(serviceName, "/", 2)
   138  	if len(sn) != 2 || sn[0] == "" || sn[1] == "" {
   139  		return "", "", fmt.Errorf("couldn't split %q into exactly two", serviceName)
   140  	}
   141  
   142  	kind := sn[0]
   143  	name := sn[1]
   144  
   145  	return kind, name, nil
   146  }
   147  
   148  // PushKubernetesResources updates service(s) from Kubernetes Inlined component in a devfile by creating new ones or removing old ones
   149  func PushKubernetesResources(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponents []devfile.Component, labels map[string]string, annotations map[string]string, context, mode string, reference metav1.OwnerReference) error {
   150  	// check csv support before proceeding
   151  	csvSupported, err := client.IsCSVSupported()
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	var deployed map[string]DeployedInfo
   157  
   158  	if csvSupported {
   159  		deployed, err = ListDeployedServices(client, labels)
   160  		if err != nil {
   161  			return err
   162  		}
   163  
   164  		for key, deployedResource := range deployed {
   165  			if deployedResource.isLinkResource {
   166  				delete(deployed, key)
   167  			}
   168  		}
   169  	}
   170  
   171  	// create an object on the kubernetes cluster for all the Kubernetes Inlined components
   172  	for _, c := range k8sComponents {
   173  		uList, er := libdevfile.GetK8sComponentAsUnstructuredList(devfileObj, c.Name, context, devfilefs.DefaultFs{})
   174  		if er != nil {
   175  			return er
   176  		}
   177  		for _, u := range uList {
   178  			var found bool
   179  			currentOwnerReferences := u.GetOwnerReferences()
   180  			for _, ref := range currentOwnerReferences {
   181  				if ref.UID == reference.UID {
   182  					found = true
   183  					break
   184  				}
   185  			}
   186  			if !found {
   187  				currentOwnerReferences = append(currentOwnerReferences, reference)
   188  				u.SetOwnerReferences(currentOwnerReferences)
   189  			}
   190  			er = PushKubernetesResource(client, u, labels, annotations, mode)
   191  			if er != nil {
   192  				return er
   193  			}
   194  			if csvSupported {
   195  				delete(deployed, u.GetKind()+"/"+u.GetName())
   196  			}
   197  		}
   198  	}
   199  
   200  	if csvSupported {
   201  		for key, val := range deployed {
   202  			if isLinkResource(val.Kind) {
   203  				continue
   204  			}
   205  			err = DeleteOperatorService(client, key)
   206  			if err != nil {
   207  				return err
   208  
   209  			}
   210  		}
   211  	}
   212  
   213  	return nil
   214  }
   215  
   216  // PushKubernetesResource pushes a Kubernetes resource (u) to the cluster using client
   217  // adding labels to the resource
   218  func PushKubernetesResource(client kclient.ClientInterface, u unstructured.Unstructured, labels map[string]string, annotations map[string]string, mode string) error {
   219  	sboSupported, err := client.IsServiceBindingSupported()
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	// If the component is of Kind: ServiceBinding, trying to run in Dev mode and SBO is not installed, run it without operator.
   225  	if isLinkResource(u.GetKind()) && mode == odolabels.ComponentDevMode && !sboSupported {
   226  		// it's a service binding related resource
   227  		return pushLinksWithoutOperator(client, u, labels)
   228  	}
   229  
   230  	// Add all passed in labels to the k8s resource regardless if it's an operator or not
   231  	u.SetLabels(mergeMaps(u.GetLabels(), labels))
   232  
   233  	// Pass in all annotations to the k8s resource
   234  	u.SetAnnotations(mergeMaps(u.GetAnnotations(), annotations))
   235  
   236  	_, err = updateOperatorService(client, u)
   237  	return err
   238  }
   239  
   240  func mergeMaps(maps ...map[string]string) map[string]string {
   241  	mergedMaps := map[string]string{}
   242  
   243  	for _, l := range maps {
   244  		for k, v := range l {
   245  			mergedMaps[k] = v
   246  		}
   247  	}
   248  
   249  	return mergedMaps
   250  }
   251  
   252  // DeployedInfo holds information about the services present on the cluster
   253  type DeployedInfo struct {
   254  	Kind           string
   255  	Name           string
   256  	isLinkResource bool
   257  }
   258  
   259  func ListDeployedServices(client kclient.ClientInterface, labels map[string]string) (map[string]DeployedInfo, error) {
   260  	deployed := map[string]DeployedInfo{}
   261  
   262  	deployedServices, _, err := ListOperatorServices(client)
   263  	if err != nil {
   264  		// We ignore ErrNoSuchOperator error as we can deduce Operator Services are not installed
   265  		return nil, err
   266  	}
   267  	for _, svc := range deployedServices {
   268  		name := svc.GetName()
   269  		kind := svc.GetKind()
   270  		deployedLabels := svc.GetLabels()
   271  		if odolabels.IsManagedByOdo(deployedLabels) && odolabels.GetComponentName(deployedLabels) == odolabels.GetComponentName(labels) {
   272  			deployed[kind+"/"+name] = DeployedInfo{
   273  				Kind:           kind,
   274  				Name:           name,
   275  				isLinkResource: isLinkResource(kind),
   276  			}
   277  		}
   278  	}
   279  
   280  	return deployed, nil
   281  }
   282  
   283  func isLinkResource(kind string) bool {
   284  	return kind == "ServiceBinding"
   285  }
   286  
   287  // updateOperatorService creates the given operator on the cluster
   288  // it returns true if the generation of the resource increased or the resource is created
   289  func updateOperatorService(client kclient.ClientInterface, u unstructured.Unstructured) (bool, error) {
   290  
   291  	// Create the service on cluster
   292  	updated, err := client.PatchDynamicResource(u)
   293  	if err != nil {
   294  		return false, err
   295  	}
   296  
   297  	if updated {
   298  		createSpinner := log.Spinnerf("Creating resource %s/%s", u.GetKind(), u.GetName())
   299  		createSpinner.End(true)
   300  	}
   301  	return updated, err
   302  }
   303  

View as plain text