Repository: openshift/odo
Pull request #4650 (2021-04-21 12:40:13)

Creates service on cluster upon "odo push"
**What type of PR is this?**
/kind feature

**What does this PR do / why we need it**:
This PR ensures that an Opreator backed service is created on the cluster only when user does `odo push` and not when they do `odo service create`.

**Which issue(s) this PR fixes**:

Fixes part of the scope mentioned in https://github.com/openshift/odo/issues/4160#issuecomment-809366184

**PR acceptance criteria**:

- [ ] Unit test 

- [x] Integration test 

- [ ] Documentation 

- [ ] I have read the [test guidelines](https://github.com/openshift/odo/blob/master/docs/dev/test-architecture.adoc)

**How to test changes / Special notes to the reviewer**:
Notes:
* This PR is targeted only for `odo service create; odo push` workflow for Operator backed services. Adding labels to the service, deleting service upon doing `odo push` and Service Catalog related changes will come in subsequent PRs after this one is merged. Doing this to break big piece into smaller chunks.
* ~I need to add integration tests to the PR.~ Integration tests have been modified to use `odo push` to actually create service on the cluster. Can't think of a new scenario that needs to be added. Open to ideas.

How to test:
* For Operator backed services, `odo service create` and `odo push`. Subsequent `odo push` would silently pass without any service info (because the service already exists on cluster.)



openshift-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/openshift/odo/pull/4650#" title="Author self-approved">dharmit</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openshift%2Fodo).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/openshift/odo/blob/master/OWNERS)~~ [dharmit]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/go.mod b/go.mod
index 577c31dbb..bcdf51d85 100644
--- a/go.mod
+++ b/go.mod
@@ -64,6 +64,7 @@ require (
 	k8s.io/cli-runtime v0.17.0
 	k8s.io/client-go v12.0.0+incompatible
 	k8s.io/klog v1.0.0
+	k8s.io/klog/v2 v2.0.0
 	k8s.io/kubectl v0.0.0
 	sigs.k8s.io/yaml v1.2.0
 )
diff --git a/openshift-ci/build-root/Dockerfile.travislogin b/openshift-ci/build-root/Dockerfile.travislogin
deleted file mode 100644
index ce06f6f22..000000000
--- a/openshift-ci/build-root/Dockerfile.travislogin
+++ /dev/null
@@ -1,39 +0,0 @@
-# Dockerfile to bootstrap build and test in openshift-ci
-
-FROM registry.svc.ci.openshift.org/openshift/release:golang-1.13
-
-RUN yum install sudo -y \
-    make \
-    wget \
-    gcc \
-    git \
-    httpd-tools \
-    ruby \
-    ruby-devel \
-    gcc-c++ \
-    patch \
-    readline \
-    readline-devel \
-    zlib \
-    zlib-devel \
-    libffi-devel \
-    openssl-devel \
-    bzip2 \
-    autoconf \
-    automake \
-    libtool \
-    bison \
-    sqlite-devel
-
-RUN sudo curl -sSL https://rvm.io/mpapis.asc | gpg2 --import -
-RUN sleep 5
-RUN sudo curl -sSL https://rvm.io/pkuczynski.asc | gpg2 --import -
-
-RUN curl -L get.rvm.io | bash -s stable
-
-RUN source /etc/profile.d/rvm.sh
-RUN /bin/bash -l -c "rvm reload && \
-    rvm requirements run && \
-    rvm install 2.7 && \
-    ruby --version && \
-    gem install travis --pre --no-document"
diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go
index af4c31785..3a9391c35 100644
--- a/pkg/devfile/adapters/kubernetes/component/adapter.go
+++ b/pkg/devfile/adapters/kubernetes/component/adapter.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/openshift/odo/pkg/service"
+
 	"github.com/devfile/library/pkg/devfile/generator"
 	componentlabels "github.com/openshift/odo/pkg/component/labels"
 	"github.com/openshift/odo/pkg/envinfo"
@@ -172,6 +174,26 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
 		return errors.Wrap(err, "unable to create or update component")
 	}
 
+	// fetch the "kubernetes inlined components" to create them on cluster
+	// from odo standpoint, these components contain yaml manifest of an odo service or an odo link
+	k8sComponents, err := a.Devfile.Data.GetComponents(parsercommon.DevfileOptions{
+		ComponentOptions: parsercommon.ComponentOptions{ComponentType: devfilev1.KubernetesComponentType},
+	})
+	if err != nil {
+		return errors.Wrap(err, "error while trying to fetch service(s) from devfile")
+	}
+	// create the Kubernetes objects from the manifest
+	services, err := service.CreateServiceFromKubernetesInlineComponents(a.Client.GetKubeClient(), k8sComponents)
+	if err != nil {
+		return errors.Wrap(err, "failed to create service(s) associated with the component")
+	}
+
+	if len(services) == 1 {
+		log.Infof("Created service %q on the cluster; refer %q to know how to link it to the component", services[0], "odo link -h")
+	} else if len(services) > 1 {
+		log.Infof("Created services %q on the cluster; refer %q to know how to link them to the component", strings.Join(services, ", "), "odo link -h")
+	}
+
 	deployment, err := a.Client.GetKubeClient().WaitForDeploymentRollout(a.ComponentName)
 	if err != nil {
 		return errors.Wrap(err, "error while waiting for deployment rollout")
@@ -376,7 +398,7 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
 	}
 
 	if len(containers) == 0 {
-		return fmt.Errorf("No valid components found in the devfile")
+		return fmt.Errorf("no valid components found in the devfile")
 	}
 
 	// Add the project volume before generating init containers
@@ -471,7 +493,7 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
 		ObjectMeta:     objectMeta,
 		SelectorLabels: selectorLabels,
 	}
-	service, err := generator.GetService(a.Devfile, serviceParams, parsercommon.DevfileOptions{})
+	svc, err := generator.GetService(a.Devfile, serviceParams, parsercommon.DevfileOptions{})
 	if err != nil {
 		return err
 	}
@@ -488,21 +510,21 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
 		klog.V(2).Infof("Successfully updated component %v", componentName)
 		oldSvc, err := a.Client.GetKubeClient().KubeClient.CoreV1().Services(a.Client.Namespace).Get(componentName, metav1.GetOptions{})
 		ownerReference := generator.GetOwnerReference(deployment)
-		service.OwnerReferences = append(service.OwnerReferences, ownerReference)
+		svc.OwnerReferences = append(svc.OwnerReferences, ownerReference)
 		if err != nil {
 			// no old service was found, create a new one
-			if len(service.Spec.Ports) > 0 {
-				_, err = a.Client.GetKubeClient().CreateService(*service)
+			if len(svc.Spec.Ports) > 0 {
+				_, err = a.Client.GetKubeClient().CreateService(*svc)
 				if err != nil {
 					return err
 				}
 				klog.V(2).Infof("Successfully created Service for component %s", componentName)
 			}
 		} else {
-			if len(service.Spec.Ports) > 0 {
-				service.Spec.ClusterIP = oldSvc.Spec.ClusterIP
-				service.ResourceVersion = oldSvc.GetResourceVersion()
-				_, err = a.Client.GetKubeClient().UpdateService(*service)
+			if len(svc.Spec.Ports) > 0 {
+				svc.Spec.ClusterIP = oldSvc.Spec.ClusterIP
+				svc.ResourceVersion = oldSvc.GetResourceVersion()
+				_, err = a.Client.GetKubeClient().UpdateService(*svc)
 				if err != nil {
 					return err
 				}
@@ -521,9 +543,9 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
 		}
 		klog.V(2).Infof("Successfully created component %v", componentName)
 		ownerReference := generator.GetOwnerReference(deployment)
-		service.OwnerReferences = append(service.OwnerReferences, ownerReference)
-		if len(service.Spec.Ports) > 0 {
-			_, err = a.Client.GetKubeClient().CreateService(*service)
+		svc.OwnerReferences = append(svc.OwnerReferences, ownerReference)
+		if len(svc.Spec.Ports) > 0 {
+			_, err = a.Client.GetKubeClient().CreateService(*svc)
 			if err != nil {
 				return err
 			}
@@ -549,7 +571,7 @@ func getFirstContainerWithSourceVolume(containers []corev1.Container) (string, s
 		}
 	}
 
-	return "", "", fmt.Errorf("In order to sync files, odo requires at least one component in a devfile to set 'mountSources: true'")
+	return "", "", fmt.Errorf("in order to sync files, odo requires at least one component in a devfile to set 'mountSources: true'")
 }
 
 // Delete deletes the component
diff --git a/pkg/odo/cli/component/devfile.go b/pkg/odo/cli/component/devfile.go
index de958ce1b..d80681c83 100644
--- a/pkg/odo/cli/component/devfile.go
+++ b/pkg/odo/cli/component/devfile.go
@@ -83,11 +83,11 @@ func (po *PushOptions) devfilePushInner() (err error) {
 
 	// Parse devfile and validate
 	devObj, err := devfile.ParseDevfileAndValidate(parser.ParserArgs{Path: po.DevfilePath})
-
 	if err != nil {
 		return err
 	}
 
+	// odo specific validations
 	err = validate.ValidateDevfileData(devObj.Data)
 	if err != nil {
 		return err
@@ -138,12 +138,12 @@ func (po *PushOptions) devfilePushInner() (err error) {
 	// Start or update the component
 	err = devfileHandler.Push(pushParams)
 	if err != nil {
-		err = errors.Errorf("Failed to start component with name %s. Error: %v",
+		err = errors.Errorf("Failed to start component with name %q. Error: %v",
 			componentName,
 			err,
 		)
 	} else {
-		log.Infof("\nPushing devfile component %s", componentName)
+		log.Infof("\nPushing devfile component %q", componentName)
 		log.Success("Changes successfully pushed to component")
 	}
 
diff --git a/pkg/odo/cli/component/push.go b/pkg/odo/cli/component/push.go
index 4bd537361..84ba68a7a 100644
--- a/pkg/odo/cli/component/push.go
+++ b/pkg/odo/cli/component/push.go
@@ -4,14 +4,15 @@ import (
 	"fmt"
 	"path/filepath"
 
+	"github.com/openshift/odo/pkg/component"
+	"github.com/openshift/odo/pkg/log"
+
 	"github.com/devfile/library/pkg/devfile"
 	"github.com/openshift/odo/pkg/devfile/validate"
 	"github.com/openshift/odo/pkg/envinfo"
 	ktemplates "k8s.io/kubectl/pkg/util/templates"
 
 	"github.com/devfile/library/pkg/devfile/parser"
-	"github.com/openshift/odo/pkg/component"
-	"github.com/openshift/odo/pkg/log"
 	"github.com/openshift/odo/pkg/occlient"
 	projectCmd "github.com/openshift/odo/pkg/odo/cli/project"
 	"github.com/openshift/odo/pkg/odo/genericclioptions"
diff --git a/pkg/odo/cli/service/create.go b/pkg/odo/cli/service/create.go
index 4fc0af38c..4cda9a4bc 100644
--- a/pkg/odo/cli/service/create.go
+++ b/pkg/odo/cli/service/create.go
@@ -126,20 +126,6 @@ func (o *CreateOptions) Complete(name string, cmd *cobra.Command, args []string)
 	return o.Backend.CompleteServiceCreate(o, cmd, args)
 }
 
-// outputNonInteractiveEquivalent outputs the populated options as the equivalent command that would be used in non-interactive mode
-func (o *CreateOptions) outputNonInteractiveEquivalent() string {
-	if o.outputCLI {
-		var tpl bytes.Buffer
-		t := template.Must(template.New("service-create-cli").Parse(equivalentTemplate))
-		e := t.Execute(&tpl, o)
-		if e != nil {
-			panic(e) // shouldn't happen
-		}
-		return strings.TrimSpace(tpl.String())
-	}
-	return ""
-}
-
 // Validate validates the CreateOptions based on completed values
 func (o *CreateOptions) Validate() (err error) {
 	// if we are in interactive mode, all values are already valid
@@ -159,7 +145,7 @@ func (o *CreateOptions) Run() (err error) {
 
 	// Information on what to do next; don't do this if "--dry-run" was requested as it gets appended to the file
 	if !o.DryRun {
-		log.Infof("You can now link the service to a component using 'odo link'; check 'odo link -h'")
+		log.Info("Successfully added service to the configuration; do 'odo push' to create service on the cluster")
 	}
 
 	equivalent := o.outputNonInteractiveEquivalent()
@@ -169,6 +155,20 @@ func (o *CreateOptions) Run() (err error) {
 	return
 }
 
+// outputNonInteractiveEquivalent outputs the populated options as the equivalent command that would be used in non-interactive mode
+func (o *CreateOptions) outputNonInteractiveEquivalent() string {
+	if o.outputCLI {
+		var tpl bytes.Buffer
+		t := template.Must(template.New("service-create-cli").Parse(equivalentTemplate))
+		e := t.Execute(&tpl, o)
+		if e != nil {
+			panic(e) // shouldn't happen
+		}
+		return strings.TrimSpace(tpl.String())
+	}
+	return ""
+}
+
 // NewCmdServiceCreate implements the odo service create command.
 func NewCmdServiceCreate(name, fullName string) *cobra.Command {
 	o := NewCreateOptions()
diff --git a/pkg/odo/cli/service/operator_backend.go b/pkg/odo/cli/service/operator_backend.go
index d9b4a6011..c56e99e2e 100644
--- a/pkg/odo/cli/service/operator_backend.go
+++ b/pkg/odo/cli/service/operator_backend.go
@@ -19,63 +19,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-// DynamicCRD holds the original CR obtained from the Operator (a CSV), or user
-// (when they use --from-file flag), and few other attributes that are likely
-// to be used to validate a CRD before creating a service from it
-type DynamicCRD struct {
-	// contains the CR as obtained from CSV or user
-	OriginalCRD map[string]interface{}
-}
-
-func NewDynamicCRD() *DynamicCRD {
-	return &DynamicCRD{}
-}
-
-// validateMetadataInCRD validates if the CRD has metadata.name field and returns an error
-func (d *DynamicCRD) validateMetadataInCRD() error {
-	metadata, ok := d.OriginalCRD["metadata"].(map[string]interface{})
-	if !ok {
-		// this condition is satisfied if there's no metadata at all in the provided CRD
-		return fmt.Errorf("couldn't find \"metadata\" in the yaml; need metadata start the service")
-	}
-
-	if _, ok := metadata["name"].(string); ok {
-		// found the metadata.name; no error
-		return nil
-	}
-	return fmt.Errorf("couldn't find metadata.name in the yaml; provide a name for the service")
-}
-
-// setServiceName modifies the CRD to contain user provided name on the CLI
-// instead of using the default one in almExample
-func (d *DynamicCRD) setServiceName(name string) {
-	metaMap := d.OriginalCRD["metadata"].(map[string]interface{})
-
-	for k := range metaMap {
-		if k == "name" {
-			metaMap[k] = name
-			return
-		}
-		// if metadata doesn't have 'name' field, we set it up
-		metaMap["name"] = name
-	}
-}
-
-// getServiceNameFromCRD fetches the service name from metadata.name field of the CRD
-func (d *DynamicCRD) getServiceNameFromCRD() (string, error) {
-	metadata, ok := d.OriginalCRD["metadata"].(map[string]interface{})
-	if !ok {
-		// this condition is satisfied if there's no metadata at all in the provided CRD
-		return "", fmt.Errorf("couldn't find \"metadata\" in the yaml; need metadata.name to start the service")
-	}
-
-	if name, ok := metadata["name"].(string); ok {
-		// found the metadata.name; no error
-		return name, nil
-	}
-	return "", fmt.Errorf("couldn't find metadata.name in the yaml; provide a name for the service")
-}
-
 // This CompleteServiceCreate contains logic to complete the "odo service create" call for the case of Operator backend
 func (b *OperatorBackend) CompleteServiceCreate(o *CreateOptions, cmd *cobra.Command, args []string) (err error) {
 	// since interactive mode is not supported for Operators yet, set it to false
@@ -109,7 +52,7 @@ func (b *OperatorBackend) CompleteServiceCreate(o *CreateOptions, cmd *cobra.Com
 }
 
 func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
-	d := NewDynamicCRD()
+	d := svc.NewDynamicCRD()
 	// if the user wants to create service from a file, we check for
 	// existence of file and validate if the requested operator and CR
 	// exist on the cluster
@@ -142,7 +85,7 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
 			return err
 		}
 
-		err = d.validateMetadataInCRD()
+		err = d.ValidateMetadataInCRD()
 		if err != nil {
 			return err
 		}
@@ -158,9 +101,9 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
 				return fmt.Errorf("service %q already exists; please provide a different name or delete the existing service first", svcFullName)
 			}
 
-			d.setServiceName(o.ServiceName)
+			d.SetServiceName(o.ServiceName)
 		} else {
-			o.ServiceName, err = d.getServiceNameFromCRD()
+			o.ServiceName, err = d.GetServiceNameFromCRD()
 			if err != nil {
 				return err
 			}
@@ -202,10 +145,10 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
 				return fmt.Errorf("service %q already exists; please provide a different name or delete the existing service first", svcFullName)
 			}
 
-			d.setServiceName(o.ServiceName)
+			d.SetServiceName(o.ServiceName)
 		}
 
-		err = d.validateMetadataInCRD()
+		err = d.ValidateMetadataInCRD()
 		if err != nil {
 			return err
 		}
@@ -214,7 +157,7 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
 		b.CustomResourceDefinition = d.OriginalCRD
 
 		if o.ServiceName == "" {
-			o.ServiceName, err = d.getServiceNameFromCRD()
+			o.ServiceName, err = d.GetServiceNameFromCRD()
 			if err != nil {
 				return err
 			}
@@ -235,18 +178,6 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
 func (b *OperatorBackend) RunServiceCreate(o *CreateOptions) (err error) {
 	s := &log.Status{}
 
-	// in case of an Operator backed service, name of the service is
-	// provided by the yaml specification in alm-examples. It might also
-	// happen that a user wants to spin up Service Catalog based service in
-	// spite of having 4.x cluster mode but we're not supporting
-	// interacting with both Operator Hub and Service Catalog on 4.x. So
-	// the user won't get to see service name in the log message
-	if !o.DryRun {
-		log.Infof("Deploying service %q of type: %q", o.ServiceName, b.CustomResource)
-		s = log.Spinner("Deploying service")
-		defer s.End(false)
-	}
-
 	// if cluster has resources of type CSV and o.CustomResource is not
 	// empty, we're expected to create an Operator backed service
 	if o.DryRun {
@@ -266,15 +197,6 @@ func (b *OperatorBackend) RunServiceCreate(o *CreateOptions) (err error) {
 
 		return nil
 	} else {
-		err = svc.CreateOperatorService(o.KClient, b.group, b.version, b.resource, b.CustomResourceDefinition)
-		if err != nil {
-			// TODO: logic to remove CRD info from devfile because service creation failed.
-			return err
-		} else {
-			s.End(true)
-			log.Successf(`Service %q was created`, o.ServiceName)
-		}
-
 		crdYaml, err := yaml.Marshal(b.CustomResourceDefinition)
 		if err != nil {
 			return err
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 3d17ed6a2..e9e42cb45 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/ghodss/yaml"
+
 	devfile "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
 	"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
 	"github.com/openshift/odo/pkg/kclient"
@@ -97,7 +99,6 @@ func doesCRExist(kind string, csvs *olm.ClusterServiceVersionList) (olm.ClusterS
 		}
 	}
 	return olm.ClusterServiceVersion{}, errors.New("could not find the requested cluster resource")
-
 }
 
 // CreateOperatorService creates new service (actually a Deployment) from OperatorHub
@@ -484,9 +485,9 @@ func updateStatusIfMatchingDeploymentExists(dcs []appsv1.DeploymentConfig, secre
 	}
 }
 
-// IsValidOperatorServiceName checks if the provided name follows
+// IsOperatorServiceNameValid checks if the provided name follows
 // <service-type>/<service-name> format. For example: "EtcdCluster/example" is
-// a valid servicename but "EtcdCluster/", "EtcdCluster", "example" aren't.
+// a valid service name but "EtcdCluster/", "EtcdCluster", "example" aren't.
 func IsOperatorServiceNameValid(name string) (string, string, error) {
 	checkName := strings.SplitN(name, "/", 2)
 
@@ -738,3 +739,116 @@ func DeleteKubernetesComponentFromDevfile(name string, devfileObj parser.Devfile
 
 	return devfileObj.WriteYamlDevfile()
 }
+
+// DynamicCRD holds the original CR obtained from the Operator (a CSV), or user
+// (when they use --from-file flag), and few other attributes that are likely
+// to be used to validate a CRD before creating a service from it
+type DynamicCRD struct {
+	// contains the CR as obtained from CSV or user
+	OriginalCRD map[string]interface{}
+}
+
+func NewDynamicCRD() *DynamicCRD {
+	return &DynamicCRD{}
+}
+
+// ValidateMetadataInCRD validates if the CRD has metadata.name field and returns an error
+func (d *DynamicCRD) ValidateMetadataInCRD() error {
+	metadata, ok := d.OriginalCRD["metadata"].(map[string]interface{})
+	if !ok {
+		// this condition is satisfied if there's no metadata at all in the provided CRD
+		return fmt.Errorf("couldn't find \"metadata\" in the yaml; need metadata start the service")
+	}
+
+	if _, ok := metadata["name"].(string); ok {
+		// found the metadata.name; no error
+		return nil
+	}
+	return fmt.Errorf("couldn't find metadata.name in the yaml; provide a name for the service")
+}
+
+// SetServiceName modifies the CRD to contain user provided name on the CLI
+// instead of using the default one in almExample
+func (d *DynamicCRD) SetServiceName(name string) {
+	metaMap := d.OriginalCRD["metadata"].(map[string]interface{})
+
+	for k := range metaMap {
+		if k == "name" {
+			metaMap[k] = name
+			return
+		}
+		// if metadata doesn't have 'name' field, we set it up
+		metaMap["name"] = name
+	}
+}
+
+// GetServiceNameFromCRD fetches the service name from metadata.name field of the CRD
+func (d *DynamicCRD) GetServiceNameFromCRD() (string, error) {
+	metadata, ok := d.OriginalCRD["metadata"].(map[string]interface{})
+	if !ok {
+		// this condition is satisfied if there's no metadata at all in the provided CRD
+		return "", fmt.Errorf("couldn't find \"metadata\" in the yaml; need metadata.name to start the service")
+	}
+
+	if name, ok := metadata["name"].(string); ok {
+		// found the metadata.name; no error
+		return name, nil
+	}
+	return "", fmt.Errorf("couldn't find metadata.name in the yaml; provide a name for the service")
+}
+
+// CreateServiceFromKubernetesInlineComponents creates service(s) from Kubernetes Inlined component in a devfile
+func CreateServiceFromKubernetesInlineComponents(client *kclient.Client, k8sComponents []devfile.Component) ([]string, error) {
+	if len(k8sComponents) == 0 {
+		// if there's no Kubernetes Inlined component, there's nothing to do.
+		return []string{}, nil
+	}
+
+	// create an object on the kubernetes cluster for all the Kubernetes Inlined components
+	var services []string
+	for _, c := range k8sComponents {
+		// get the string representation of the YAML definition of a CRD
+		strCRD := c.Kubernetes.Inlined
+
+		// convert the YAML definition into map[string]interface{} since it's needed to create dynamic resource
+		d := NewDynamicCRD()
+		err := yaml.Unmarshal([]byte(strCRD), &d.OriginalCRD)
+		if err != nil {
+			return []string{}, err
+		}
+
+		cr, csv, err := GetCSV(client, d.OriginalCRD)
+		if err != nil {
+			return []string{}, err
+		}
+
+		var group, version, kind, resource string
+		for _, crd := range csv.Spec.CustomResourceDefinitions.Owned {
+			if crd.Kind == cr {
+				group, version, kind, resource, err = getGVKRFromCR(crd)
+				if err != nil {
+					return []string{}, err
+				}
+				break
+			}
+		}
+
+		// create the service on cluster
+		err = client.CreateDynamicResource(d.OriginalCRD, group, version, resource)
+		if err != nil {
+			if strings.Contains(err.Error(), "already exists") {
+				// this could be the case when "odo push" was executed after making change to code but there was no change to the service itself
+				// TODO: better way to handle this might be introduced by https://github.com/openshift/odo/issues/4553
+				err = nil
+				break // this ensures that services slice is not updated
+			} else {
+				return []string{}, err
+			}
+		}
+
+		name, _ := d.GetServiceNameFromCRD() // ignoring error because invalid yaml won't be inserted into devfile through odo
+		services = append(services, strings.Join([]string{kind, name}, "/"))
+	}
+
+	return services, nil
+}


Test patch:
diff --git a/tests/integration/operatorhub/cmd_service_test.go b/tests/integration/operatorhub/cmd_service_test.go
index e44a4b7bc..43466a703 100644
--- a/tests/integration/operatorhub/cmd_service_test.go
+++ b/tests/integration/operatorhub/cmd_service_test.go
@@ -83,7 +83,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
 			operators := helper.CmdShouldPass("odo", "catalog", "list", "services")
 			etcdOperator := regexp.MustCompile(`etcdoperator\.*[a-z][0-9]\.[0-9]\.[0-9]-clusterwide`).FindString(operators)
 			stdOut := helper.CmdShouldPass("odo", "service", "create", fmt.Sprintf("%s/EtcdCluster", etcdOperator), "--project", commonVar.Project)
-			Expect(stdOut).To(ContainSubstring("Service \"example\" was created"))
+			Expect(stdOut).To(ContainSubstring("Successfully added service to the configuration"))
 
 			// read the devfile.yaml to check if service definition was rightly inserted
 			devfilePath := filepath.Join(commonVar.Context, "devfile.yaml")
@@ -92,7 +92,8 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
 			matchInOutput := []string{"kubernetes", "inlined", "EtcdCluster", "example"}
 			helper.MatchAllInOutput(string(content), matchInOutput)
 
-			// now verify if the pods for the operator have started
+			// now create the service on cluster and verify if the pods for the operator have started
+			helper.CmdShouldPass("odo", "push")
 			pods := oc.GetAllPodsInNs(commonVar.Project)
 			// Look for pod with example name because that's the name etcd will give to the pods.
 			etcdPod := regexp.MustCompile(`example-.[a-z0-9]*`).FindString(pods)
@@ -126,6 +127,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
 			operators := helper.CmdShouldPass("odo", "catalog", "list", "services")
 			etcdOperator := regexp.MustCompile(`etcdoperator\.*[a-z][0-9]\.[0-9]\.[0-9]-clusterwide`).FindString(operators)
 			helper.CmdShouldPass("odo", "service", "create", fmt.Sprintf("%s/EtcdCluster", etcdOperator), name, "--project", commonVar.Project)
+			helper.CmdShouldPass("odo", "push")
 
 			// now verify if the pods for the operator have started
 			pods := oc.GetAllPodsInNs(commonVar.Project)
@@ -217,6 +219,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
 
 			// now create operator backed service
 			helper.CmdShouldPass("odo", "service", "create", "--from-file", fileName, "--project", commonVar.Project)
+			helper.CmdShouldPass("odo", "push")
 
 			// now verify if the pods for the operator have started
 			pods := oc.GetAllPodsInNs(commonVar.Project)
@@ -250,6 +253,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
 
 			// now create operator backed service
 			helper.CmdShouldPass("odo", "service", "create", "--from-file", fileName, name, "--project", commonVar.Project)
+			helper.CmdShouldPass("odo", "push")
 
 			// Attempting to create service with same name should fail
 			stdOut = helper.CmdShouldFail("odo", "service", "create", "--from-file", fileName, name, "--project", commonVar.Project)
@@ -342,6 +346,7 @@ spec:
 			operators := helper.CmdShouldPass("odo", "catalog", "list", "services")
 			etcdOperator := regexp.MustCompile(`etcdoperator\.*[a-z][0-9]\.[0-9]\.[0-9]-clusterwide`).FindString(operators)
 			helper.CmdShouldPass("odo", "service", "create", fmt.Sprintf("%s/EtcdCluster", etcdOperator), "--project", commonVar.Project)
+			helper.CmdShouldPass("odo", "push")
 
 			// now verify if the pods for the operator have started
 			pods := oc.GetAllPodsInNs(commonVar.Project)
@@ -426,6 +431,7 @@ spec:
 			operators := helper.CmdShouldPass("odo", "catalog", "list", "services")
 			etcdOperator := regexp.MustCompile(`etcdoperator\.*[a-z][0-9]\.[0-9]\.[0-9]-clusterwide`).FindString(operators)
 			helper.CmdShouldPass("odo", "service", "create", fmt.Sprintf("%s/EtcdCluster", etcdOperator), "--project", commonVar.Project)
+			helper.CmdShouldPass("odo", "push")
 
 			// now verify if the pods for the operator have started
 			pods := oc.GetAllPodsInNs(commonVar.Project)
@@ -459,39 +465,5 @@ spec:
 			stdOut = helper.CmdShouldFail("odo", "unlink", "EtcdCluster/example")
 			Expect(stdOut).To(ContainSubstring("failed to unlink the service"))
 		})
-
-		It("should fail if we delete a link outside of odo (using oc)", func() {
-			if os.Getenv("KUBERNETES") == "true" {
-				Skip("This is a OpenShift specific scenario, skipping")
-			}
-
-			componentName := helper.RandString(6)
-			helper.CmdShouldPass("odo", "create", "nodejs", componentName)
-			helper.CmdShouldPass("odo", "push")
-
-			// start the Operator backed service first
-			operators := helper.CmdShouldPass("odo", "catalog", "list", "services")
-			etcdOperator := regexp.MustCompile(`etcdoperator\.*[a-z][0-9]\.[0-9]\.[0-9]-clusterwide`).FindString(operators)
-			helper.CmdShouldPass("odo", "service", "create", fmt.Sprintf("%s/EtcdCluster", etcdOperator), "--project", commonVar.Project)
-
-			// now verify if the pods for the operator have started
-			pods := helper.CmdShouldPass("oc", "get", "pods", "-n", commonVar.Project)
-			// Look for pod with example name because that's the name etcd will give to the pods.
-			etcdPod := regexp.MustCompile(`example-.[a-z0-9]*`).FindString(pods)
-
-			ocArgs := []string{"get", "pods", etcdPod, "-o", "template=\"{{.status.phase}}\"", "-n", commonVar.Project}
-			helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
-				return strings.Contains(output, "Running")
-			})
-
-			stdOut := helper.CmdShouldPass("odo", "link", "EtcdCluster/example")
-			Expect(stdOut).To(ContainSubstring("Successfully created link between component"))
-			helper.CmdShouldPass("odo", "push")
-
-			sbrName := strings.Join([]string{componentName, "etcdcluster", "example"}, "-")
-			helper.CmdShouldPass("oc", "delete", fmt.Sprintf("ServiceBinding/%s", sbrName))
-			stdOut = helper.CmdShouldFail("odo", "unlink", "EtcdCluster/example")
-			helper.MatchAllInOutput(stdOut, []string{"component's link with", "has been deleted outside odo"})
-		})
 	})
 })