...

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

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

     1  package component
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
    11  	"github.com/devfile/api/v2/pkg/devfile"
    12  	"github.com/devfile/library/v2/pkg/devfile/parser"
    13  	"github.com/devfile/library/v2/pkg/devfile/parser/data"
    14  	routev1 "github.com/openshift/api/route/v1"
    15  	"k8s.io/apimachinery/pkg/runtime"
    16  	"k8s.io/klog"
    17  
    18  	"github.com/redhat-developer/odo/pkg/alizer"
    19  	"github.com/redhat-developer/odo/pkg/api"
    20  	"github.com/redhat-developer/odo/pkg/kclient"
    21  	odolabels "github.com/redhat-developer/odo/pkg/labels"
    22  	"github.com/redhat-developer/odo/pkg/odo/commonflags"
    23  	"github.com/redhat-developer/odo/pkg/platform"
    24  	"github.com/redhat-developer/odo/pkg/podman"
    25  	"github.com/redhat-developer/odo/pkg/util"
    26  
    27  	corev1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  )
    30  
    31  const (
    32  	NotAvailable = "Not available"
    33  	UnknownValue = "Unknown"
    34  )
    35  
    36  // GetComponentTypeFromDevfileMetadata returns component type from the devfile metadata;
    37  // it could either be projectType or language, if neither of them are set, return 'Not available'
    38  func GetComponentTypeFromDevfileMetadata(metadata devfile.DevfileMetadata) string {
    39  	var componentType string
    40  	if metadata.ProjectType != "" {
    41  		componentType = metadata.ProjectType
    42  	} else if metadata.Language != "" {
    43  		componentType = metadata.Language
    44  	} else {
    45  		componentType = NotAvailable
    46  	}
    47  	return componentType
    48  }
    49  
    50  // GetComponentRuntimeFromDevfileMetadata returns the Project Type defined in the Devfile metadata
    51  // or if not set, the Language
    52  func GetComponentRuntimeFromDevfileMetadata(metadata devfile.DevfileMetadata) string {
    53  	if metadata.ProjectType != "" {
    54  		return metadata.ProjectType
    55  	}
    56  	return metadata.Language
    57  }
    58  
    59  // GatherName computes and returns what should be used as name for the Devfile object specified.
    60  //
    61  // If a non-blank name is available in the Devfile metadata (which is optional), it is sanitized and returned.
    62  //
    63  // Otherwise, it uses Alizer to detect the name, from the project build tools (pom.xml, package.json, ...),
    64  // or from the component directory name.
    65  func GatherName(contextDir string, devfileObj *parser.DevfileObj) (string, error) {
    66  	var name string
    67  	if devfileObj != nil {
    68  		name = devfileObj.GetMetadataName()
    69  		if name == "" || strings.TrimSpace(name) == "" {
    70  			// Use Alizer if Devfile has no (optional) metadata.name field.
    71  			// We need to pass in the Devfile base directory (not the path to the devfile.yaml).
    72  			// Name returned by alizer.DetectName is expected to be already sanitized.
    73  			alizerClient := alizer.Alizer{} // TODO(feloy) fix with DI
    74  			return alizerClient.DetectName(filepath.Dir(devfileObj.Ctx.GetAbsPath()))
    75  		}
    76  	} else {
    77  		// Fallback to the context dir name
    78  		baseDir, err := filepath.Abs(contextDir)
    79  		if err != nil {
    80  			return "", err
    81  		}
    82  		name = filepath.Base(baseDir)
    83  	}
    84  
    85  	// sanitize the name
    86  	s := util.GetDNS1123Name(name)
    87  	klog.V(3).Infof("name of component is %q, and sanitized name is %q", name, s)
    88  
    89  	return s, nil
    90  }
    91  
    92  // Log returns log from component
    93  func Log(platformClient platform.Client, componentName string, appName string, follow bool, command v1alpha2.Command) (io.ReadCloser, error) {
    94  
    95  	pod, err := platformClient.GetRunningPodFromSelector(odolabels.GetSelector(componentName, appName, odolabels.ComponentDevMode, false))
    96  	if err != nil {
    97  		return nil, fmt.Errorf("a running component %s doesn't exist on the cluster: %w", componentName, err)
    98  	}
    99  
   100  	containerName := command.Exec.Component
   101  
   102  	return platformClient.GetPodLogs(pod.Name, containerName, follow)
   103  }
   104  
   105  // ListAllClusterComponents returns a list of all "components" on a cluster
   106  // that are both odo and non-odo components.
   107  //
   108  // We then return a list of "components" intended for listing / output purposes specifically for commands such as:
   109  // `odo list`
   110  // that are both odo and non-odo components.
   111  func ListAllClusterComponents(client kclient.ClientInterface, namespace string) ([]api.ComponentAbstract, error) {
   112  
   113  	// Get all the dynamic resources available
   114  	resourceList, err := client.GetAllResourcesFromSelector("", namespace)
   115  	if err != nil {
   116  		return nil, fmt.Errorf("unable to list all dynamic resources required to find components: %w", err)
   117  	}
   118  
   119  	var components []api.ComponentAbstract
   120  
   121  	for _, resource := range resourceList {
   122  
   123  		// ignore "PackageManifest" as they are not components, it is just a record in OpenShift catalog.
   124  		if resource.GetKind() == "PackageManifest" {
   125  			continue
   126  		}
   127  
   128  		var labels, annotations map[string]string
   129  
   130  		// Retrieve the labels and annotations from the unstructured resource output
   131  		if resource.GetLabels() != nil {
   132  			labels = resource.GetLabels()
   133  		}
   134  		if resource.GetAnnotations() != nil {
   135  			annotations = resource.GetAnnotations()
   136  		}
   137  
   138  		// Figure out the correct name to use
   139  		// if there is no instance label (app.kubernetes.io/instance),
   140  		// we SKIP the resource as it is not a component essential for Kubernetes.
   141  		name := odolabels.GetComponentName(labels)
   142  		if name == "" {
   143  			continue
   144  		}
   145  
   146  		// Get the component type (if there is any..)
   147  		componentType, err := odolabels.GetProjectType(nil, annotations)
   148  		if err != nil || componentType == "" {
   149  			componentType = api.TypeUnknown
   150  		}
   151  
   152  		// Get the managedBy label
   153  		// IMPORTANT. If both "managed-by" and "instance" labels are BLANK, it is most likely an operator
   154  		// or a non-component. We do not want to show these in the list of components
   155  		// so we skip them if there is no "managed-by" label.
   156  
   157  		managedBy := odolabels.GetManagedBy(labels)
   158  		if managedBy == "" && name == "" {
   159  			continue
   160  		}
   161  
   162  		managedByVersion := odolabels.GetManagedByVersion(labels)
   163  
   164  		// Generate the appropriate "component" with all necessary information
   165  		component := api.ComponentAbstract{
   166  			Name:             name,
   167  			ManagedBy:        managedBy,
   168  			Type:             componentType,
   169  			ManagedByVersion: managedByVersion,
   170  			//lint:ignore SA1019 we need to output the deprecated value, before to remove it in a future release
   171  			RunningOn: commonflags.PlatformCluster,
   172  			Platform:  commonflags.PlatformCluster,
   173  		}
   174  		mode := odolabels.GetMode(labels)
   175  		componentFound := false
   176  		for v, otherCompo := range components {
   177  			if component.Name == otherCompo.Name {
   178  				componentFound = true
   179  				if mode != "" {
   180  					if components[v].RunningIn == nil {
   181  						components[v].RunningIn = api.NewRunningModes()
   182  					}
   183  					components[v].RunningIn.AddRunningMode(api.RunningMode(strings.ToLower(mode)))
   184  				}
   185  				if otherCompo.Type == api.TypeUnknown && component.Type != api.TypeUnknown {
   186  					components[v].Type = component.Type
   187  				}
   188  				if otherCompo.ManagedBy == api.TypeUnknown && component.ManagedBy != api.TypeUnknown {
   189  					components[v].ManagedBy = component.ManagedBy
   190  				}
   191  			}
   192  		}
   193  		if !componentFound {
   194  			if mode != "" {
   195  				if component.RunningIn == nil {
   196  					component.RunningIn = api.NewRunningModes()
   197  				}
   198  				component.RunningIn.AddRunningMode(api.RunningMode(strings.ToLower(mode)))
   199  			}
   200  			components = append(components, component)
   201  		}
   202  	}
   203  
   204  	return components, nil
   205  }
   206  
   207  func ListAllComponents(client kclient.ClientInterface, podmanClient podman.Client, namespace string, devObj *parser.DevfileObj, componentName string) ([]api.ComponentAbstract, string, error) {
   208  	var (
   209  		allComponents []api.ComponentAbstract
   210  	)
   211  
   212  	if client != nil {
   213  		clusterComponents, err := ListAllClusterComponents(client, namespace)
   214  		if err != nil {
   215  			return nil, "", err
   216  		}
   217  		allComponents = append(allComponents, clusterComponents...)
   218  	}
   219  
   220  	// PdomanClient can be nil if podman platform is not accessible
   221  	if podmanClient != nil {
   222  		podmanComponents, err := podmanClient.ListAllComponents()
   223  		if err != nil {
   224  			return nil, "", err
   225  		}
   226  		allComponents = append(allComponents, podmanComponents...)
   227  	}
   228  
   229  	localComponent := api.ComponentAbstract{
   230  		Name:      componentName,
   231  		ManagedBy: "",
   232  		RunningIn: api.NewRunningModes(),
   233  	}
   234  	if devObj != nil {
   235  		localComponent.Type = GetComponentTypeFromDevfileMetadata(devObj.Data.GetMetadata())
   236  	}
   237  
   238  	componentInDevfile := ""
   239  	if localComponent.Name != "" {
   240  		if !Contains(localComponent, allComponents) {
   241  			allComponents = append(allComponents, localComponent)
   242  		}
   243  		componentInDevfile = localComponent.Name
   244  	}
   245  	return allComponents, componentInDevfile, nil
   246  }
   247  
   248  func getResourcesForComponent(
   249  	ctx context.Context,
   250  	client platform.Client,
   251  	name string,
   252  	namespace string,
   253  ) ([]unstructured.Unstructured, error) {
   254  	selector := odolabels.GetNameSelector(name)
   255  	resourceList, err := client.GetAllResourcesFromSelector(selector, namespace)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	filteredList := []unstructured.Unstructured{}
   260  	for _, resource := range resourceList {
   261  		// ignore "PackageManifest" as they are not components, it is just a record in OpenShift catalog.
   262  		if resource.GetKind() == "PackageManifest" {
   263  			continue
   264  		}
   265  		filteredList = append(filteredList, resource)
   266  	}
   267  	return filteredList, nil
   268  }
   269  
   270  // GetRunningModes returns the list of modes on which a "name" component is deployed, by looking into namespace
   271  // the resources deployed with matching labels, based on the "odo.dev/mode" label
   272  func GetRunningModes(ctx context.Context, kubeClient kclient.ClientInterface, podmanClient podman.Client, name string) (map[platform.Client]api.RunningModes, error) {
   273  	var hasErr bool
   274  	var ns string
   275  	listByPlatform := make(map[platform.Client][]unstructured.Unstructured)
   276  	if kubeClient != nil {
   277  		ns = kubeClient.GetCurrentNamespace()
   278  		list, err := getResourcesForComponent(ctx, kubeClient, name, ns)
   279  		if err != nil {
   280  			klog.V(4).Infof("error while listing cluster components: %v", err)
   281  			hasErr = true
   282  		} else if len(list) > 0 {
   283  			listByPlatform[kubeClient] = list
   284  		}
   285  	}
   286  
   287  	if podmanClient != nil {
   288  		ns = ""
   289  		list, err := getResourcesForComponent(ctx, podmanClient, name, ns)
   290  		if err != nil {
   291  			klog.V(4).Infof("error while listing Podman components: %v", err)
   292  			hasErr = true
   293  		} else if len(list) > 0 {
   294  			listByPlatform[podmanClient] = list
   295  		}
   296  	}
   297  
   298  	if hasErr {
   299  		return nil, nil
   300  	}
   301  
   302  	if len(listByPlatform) == 0 {
   303  		return nil, NewNoComponentFoundError(name, ns)
   304  	}
   305  
   306  	result := make(map[platform.Client]api.RunningModes)
   307  	for plt, list := range listByPlatform {
   308  		mapResult := api.NewRunningModes()
   309  		for _, resource := range list {
   310  			resourceLabels := resource.GetLabels()
   311  			mode := odolabels.GetMode(resourceLabels)
   312  			if mode != "" {
   313  				mapResult.AddRunningMode(api.RunningMode(strings.ToLower(mode)))
   314  			}
   315  		}
   316  		result[plt] = mapResult
   317  	}
   318  
   319  	return result, nil
   320  }
   321  
   322  // Contains checks to see if the component exists in an array or not
   323  // by checking the name
   324  func Contains(component api.ComponentAbstract, components []api.ComponentAbstract) bool {
   325  	for _, comp := range components {
   326  		if component.Name == comp.Name {
   327  			return true
   328  		}
   329  	}
   330  	return false
   331  }
   332  
   333  // GetDevfileInfo extracts information from the labels and annotations of resources to rebuild a Devfile
   334  func GetDevfileInfo(ctx context.Context, kubeClient kclient.ClientInterface, podmanClient podman.Client, name string) (parser.DevfileObj, error) {
   335  	var ns string
   336  	listByPlatform := make(map[platform.Client][]unstructured.Unstructured)
   337  	if kubeClient != nil {
   338  		ns = kubeClient.GetCurrentNamespace()
   339  		list, err := getResourcesForComponent(ctx, kubeClient, name, ns)
   340  		if err != nil {
   341  			klog.V(4).Infof("error while listing cluster components: %v", err)
   342  		} else if len(list) > 0 {
   343  			listByPlatform[kubeClient] = list
   344  		}
   345  	}
   346  	if podmanClient != nil {
   347  		ns = ""
   348  		list, err := getResourcesForComponent(ctx, podmanClient, name, "")
   349  		if err != nil {
   350  			klog.V(4).Infof("error while listing Podman components: %v", err)
   351  		} else if len(list) > 0 {
   352  			listByPlatform[podmanClient] = list
   353  		}
   354  	}
   355  
   356  	if len(listByPlatform) == 0 {
   357  		return parser.DevfileObj{}, NewNoComponentFoundError(name, ns)
   358  	}
   359  
   360  	// If a same resource is found on both platforms, make sure it has the same labels.
   361  	// Otherwise, we don't know how to extract Devfile information from it.
   362  	kList := listByPlatform[kubeClient]
   363  	pList := listByPlatform[podmanClient]
   364  	if len(kList) > 0 {
   365  		err := checkLabelsForDevfileInfo(kList, pList)
   366  		if err != nil {
   367  			return parser.DevfileObj{}, err
   368  		}
   369  	}
   370  	if len(pList) > 0 {
   371  		err := checkLabelsForDevfileInfo(pList, kList)
   372  		if err != nil {
   373  			return parser.DevfileObj{}, err
   374  		}
   375  	}
   376  
   377  	if len(kList) > 0 {
   378  		return getDevfileInfoFromList(kList)
   379  	}
   380  	return getDevfileInfoFromList(pList)
   381  }
   382  
   383  func checkLabelsForDevfileInfo(l1 []unstructured.Unstructured, l2 []unstructured.Unstructured) error {
   384  	for _, k := range l1 {
   385  		var found bool
   386  		var (
   387  			kLabels      = k.GetLabels()
   388  			kAnnotations = k.GetAnnotations()
   389  			kName        = odolabels.GetComponentName(kLabels)
   390  		)
   391  		kProjectType, err := odolabels.GetProjectType(kLabels, kAnnotations)
   392  		if err != nil {
   393  			klog.V(7).Infof("error while working on cluster resource %q: %v", kName, err)
   394  			continue
   395  		}
   396  
   397  		var (
   398  			pName        string
   399  			pProjectType string
   400  		)
   401  		for _, p := range l2 {
   402  			pLabels := p.GetLabels()
   403  			pAnnotations := p.GetAnnotations()
   404  			pName = odolabels.GetComponentName(pLabels)
   405  			pProjectType, err = odolabels.GetProjectType(pLabels, pAnnotations)
   406  			if err != nil {
   407  				klog.V(7).Infof("error while working on resource %q: %v", pName, err)
   408  				continue
   409  			}
   410  			if kName == pName {
   411  				found = true
   412  				break
   413  			}
   414  		}
   415  		if found {
   416  			// Error out if there is a mismatch
   417  			if kProjectType != pProjectType {
   418  				return fmt.Errorf("found resource %q on both platforms, but with different project types: %q vs %q",
   419  					kName, kProjectType, pProjectType)
   420  			}
   421  		}
   422  	}
   423  	return nil
   424  }
   425  
   426  func getDevfileInfoFromList(list []unstructured.Unstructured) (parser.DevfileObj, error) {
   427  	devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
   428  	if err != nil {
   429  		return parser.DevfileObj{}, err
   430  	}
   431  	metadata := devfileData.GetMetadata()
   432  	metadata.Name = UnknownValue
   433  	metadata.DisplayName = UnknownValue
   434  	metadata.ProjectType = UnknownValue
   435  	metadata.Language = UnknownValue
   436  	metadata.Version = UnknownValue
   437  	metadata.Description = UnknownValue
   438  
   439  	for _, resource := range list {
   440  		labels := resource.GetLabels()
   441  		annotations := resource.GetAnnotations()
   442  		name := odolabels.GetComponentName(labels)
   443  		if len(name) > 0 && metadata.Name == UnknownValue {
   444  			metadata.Name = name
   445  		}
   446  		typ, err := odolabels.GetProjectType(labels, annotations)
   447  		if err != nil {
   448  			continue
   449  		}
   450  		if len(typ) > 0 && metadata.ProjectType == UnknownValue {
   451  			metadata.ProjectType = typ
   452  		}
   453  	}
   454  	devfileData.SetMetadata(metadata)
   455  	return parser.DevfileObj{
   456  		Data: devfileData,
   457  	}, nil
   458  }
   459  
   460  // ListRoutesAndIngresses lists routes and ingresses created by a component;
   461  // it only returns the resources created with Deploy mode;
   462  // it fetches resources from the cluster that match label and return.
   463  func ListRoutesAndIngresses(client kclient.ClientInterface, componentName, appName string) (ings []api.ConnectionData, routes []api.ConnectionData, err error) {
   464  	if client == nil {
   465  		return nil, nil, nil
   466  	}
   467  
   468  	selector := odolabels.GetNameSelector(componentName)
   469  
   470  	k8sIngresses, err := client.ListIngresses(client.GetCurrentNamespace(), selector)
   471  	if err != nil {
   472  		return nil, nil, err
   473  	}
   474  	for _, ing := range k8sIngresses.Items {
   475  		if ownerReferences := ing.GetOwnerReferences(); ownerReferences != nil {
   476  			klog.V(4).Infof("Skipping Ingress %q created/owned by another resource: %v", ing.GetName(), ownerReferences)
   477  			continue
   478  		}
   479  		ings = append(ings, api.ConnectionData{
   480  			Name: ing.GetName(),
   481  			Rules: func() (rules []api.Rules) {
   482  				for _, rule := range ing.Spec.Rules {
   483  					var paths []string
   484  					for _, path := range rule.HTTP.Paths {
   485  						paths = append(paths, path.Path)
   486  					}
   487  					host := rule.Host
   488  					if host == "" {
   489  						host = "*"
   490  					}
   491  					rules = append(rules, api.Rules{Host: host, Paths: paths})
   492  				}
   493  				if len(ing.Spec.Rules) == 0 {
   494  					rules = append(rules, api.Rules{Host: "*", Paths: []string{"/*"}})
   495  				}
   496  				return rules
   497  			}(),
   498  		})
   499  	}
   500  	// Return early if it is not an OpenShift cluster
   501  	if isOC, e := client.IsProjectSupported(); !isOC {
   502  		if e != nil {
   503  			klog.V(4).Infof("unable to detect project support: %s", e.Error())
   504  		}
   505  		return ings, nil, nil
   506  	}
   507  
   508  	routeGVR, err := client.GetGVRFromGVK(kclient.RouteGVK)
   509  	if err != nil {
   510  		return nil, nil, fmt.Errorf("unable to determine GVR for %s: %w", kclient.RouteGVK.String(), err)
   511  	}
   512  
   513  	ocRoutes, err := client.ListDynamicResources(client.GetCurrentNamespace(), routeGVR, selector)
   514  	if err != nil {
   515  		return nil, nil, err
   516  	}
   517  	for _, u := range ocRoutes.Items {
   518  		if ownerReferences := u.GetOwnerReferences(); ownerReferences != nil {
   519  			klog.V(4).Infof("Skipping Route %q created/owned by another resource: %v", u.GetName(), ownerReferences)
   520  			continue
   521  		}
   522  		route := routev1.Route{}
   523  		err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &route)
   524  		if err != nil {
   525  			return nil, nil, err
   526  		}
   527  		routes = append(routes, api.ConnectionData{
   528  			Name: route.GetName(),
   529  			Rules: []api.Rules{
   530  				{Host: route.Spec.Host, Paths: []string{route.Spec.Path}},
   531  			},
   532  		})
   533  	}
   534  
   535  	return ings, routes, nil
   536  }
   537  
   538  func GetContainersNames(pod *corev1.Pod) []string {
   539  	result := make([]string, 0, len(pod.Spec.Containers))
   540  	for _, container := range pod.Spec.Containers {
   541  		result = append(result, container.Name)
   542  	}
   543  	return result
   544  }
   545  

View as plain text