Skip to content

Commit

Permalink
Add support for unlinking component from service (#3651)
Browse files Browse the repository at this point in the history
* Add support for unlinking component from service

* Changes based on PR feedback
  • Loading branch information
dharmit authored Aug 25, 2020
1 parent 2e46179 commit 552aad7
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 4 deletions.
20 changes: 20 additions & 0 deletions pkg/envinfo/envinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,26 @@ func (esi *EnvSpecificInfo) DeleteURL(parameter string) error {
return esi.writeToFile()
}

func (esi *EnvSpecificInfo) DeleteLink(parameter string) error {
index := -1

for i, link := range *esi.componentSettings.Link {
if link.Name == parameter {
index = i
break
}
}

if index != -1 {
s := *esi.componentSettings.Link
s = append(s[:index], s[index+1:]...)
esi.componentSettings.Link = &s
return esi.writeToFile()
} else {
return nil
}
}

// GetComponentSettings returns the componentSettings from envinfo
func (esi *EnvSpecificInfo) GetComponentSettings() ComponentSettings {
return esi.componentSettings
Expand Down
81 changes: 78 additions & 3 deletions pkg/odo/cli/component/common_link.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var (
sbrResource = "servicebindingrequests"
)

const unlink = "unlink"

type commonLinkOptions struct {
wait bool
port string
Expand Down Expand Up @@ -74,22 +76,27 @@ func (o *commonLinkOptions) complete(name string, cmd *cobra.Command, args []str
}

if !sboSupport {
return fmt.Errorf("Please install Service Binding Operator to be able to create a link")
return fmt.Errorf("please install Service Binding Operator to be able to create/delete a link")
}

o.serviceType, o.serviceName, err = svc.IsOperatorServiceNameValid(suppliedName)
if err != nil {
return err
}

if o.operationName == unlink {
// rest of the code is specific to link operation
return nil
}

componentName := o.EnvSpecificInfo.GetName()

// Assign static/hardcoded values to SBR
o.sbr.Kind = sbrKind
o.sbr.APIVersion = strings.Join([]string{sbrGroup, sbrVersion}, "/")

// service binding request name will be like <component-name>-<service-type>-<service-name>. For example: nodejs-etcdcluster-example
o.sbr.Name = strings.Join([]string{componentName, strings.ToLower(o.serviceType), o.serviceName}, "-")
o.sbr.Name = getSBRName(componentName, o.serviceType, o.serviceName)
o.sbr.Namespace = o.EnvSpecificInfo.GetNamespace()
o.sbr.Spec.DetectBindingResources = true // because we want the operator what to bind from the service

Expand Down Expand Up @@ -172,6 +179,35 @@ func (o *commonLinkOptions) validate(wait bool) (err error) {
return fmt.Errorf("Couldn't find service named %q. Refer %q to see list of running services", svcFullName, "odo service list")
}

if o.operationName == unlink {
componentName := o.EnvSpecificInfo.GetName()
sbrName := getSBRName(componentName, o.serviceType, o.serviceName)
links := o.EnvSpecificInfo.GetLink()

linked := isComponentLinked(sbrName, links)
if !linked {
// user's trying to unlink a service that's not linked with the component
return fmt.Errorf("failed to unlink the service %q since it's not linked with the component %q", svcFullName, componentName)
}

// Verify if the underlying service binding request actually exists
sbrSvcFullName := strings.Join([]string{sbrKind, sbrName}, "/")
sbrExists, err := svc.OperatorSvcExists(o.KClient, sbrSvcFullName)
if err != nil {
return err
}
if !sbrExists {
// This could have happened if the service binding request was deleted outside odo workflow (eg: oc delete sbr/<sbr-name>)
// we must remove entry of the link from env.yaml in this case
err = o.Context.EnvSpecificInfo.DeleteLink(sbrName)
if err != nil {
return fmt.Errorf("component's link with %q has been deleted outside odo; unable to delete odo's state of the link", svcFullName)
}
return fmt.Errorf("component's link with %q has been deleted outside odo", svcFullName)
}
return nil
}

// since the service exists, let's get more info to populate service binding request
// first get the CR itself
cr, err := o.KClient.GetCustomResource(o.serviceType)
Expand Down Expand Up @@ -228,6 +264,24 @@ func (o *commonLinkOptions) validate(wait bool) (err error) {

func (o *commonLinkOptions) run() (err error) {
if experimental.IsExperimentalModeEnabled() {
if o.operationName == unlink {
sbrName := getSBRName(o.EnvSpecificInfo.GetName(), o.serviceType, o.serviceName)
svcFullName := getSvcFullName(sbrKind, sbrName)
err = svc.DeleteServiceBindingRequest(o.KClient, svcFullName)
if err != nil {
return err
}

err = o.Context.EnvSpecificInfo.DeleteLink(sbrName)
if err != nil {
return err
}

log.Successf("Successfully unlinked component %q from service %q\n", o.Context.EnvSpecificInfo.GetName(), o.suppliedName)
log.Italic("To apply the changes, please use `odo push`")

return
}
// convert service binding request into a ma[string]interface{} type so
// as to use it with dynamic client
sbrMap := make(map[string]interface{})
Expand All @@ -242,7 +296,7 @@ func (o *commonLinkOptions) run() (err error) {
err = o.KClient.CreateDynamicResource(sbrMap, sbrGroup, sbrVersion, sbrResource)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("Component %q is already linked with the service %q\n", o.Context.EnvSpecificInfo.GetName(), o.suppliedName)
return fmt.Errorf("component %q is already linked with the service %q\n", o.Context.EnvSpecificInfo.GetName(), o.suppliedName)
}
return err
}
Expand Down Expand Up @@ -343,3 +397,24 @@ func (o *commonLinkOptions) waitForLinkToComplete() (err error) {
_, err = o.Client.WaitAndGetPod(podSelector, corev1.PodRunning, "Waiting for component to start")
return err
}

// getSvcFullName returns service name in the format <service-type>/<service-name>
func getSvcFullName(serviceType, serviceName string) string {
return strings.Join([]string{serviceType, serviceName}, "/")
}

// getSBRName creates a name to be used for creation/deletion of SBR during link/unlink operations
func getSBRName(componentName, serviceType, serviceName string) string {
return strings.Join([]string{componentName, strings.ToLower(serviceType), serviceName}, "-")
}

// isComponentLinked checks if link with "sbrName" exists in the component's
// config. It confirms if the component is linked with the service
func isComponentLinked(sbrName string, links []envinfo.EnvInfoLink) bool {
for _, link := range links {
if link.Name == sbrName {
return true
}
}
return false
}
6 changes: 6 additions & 0 deletions pkg/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ func DeleteServiceAndUnlinkComponents(client *occlient.Client, serviceName strin
return nil
}

// DeleteServiceBindingRequest deletes a service binding request (when user
// does odo unlink). It's just a wrapper on DeleteOperatorService
func DeleteServiceBindingRequest(client *kclient.Client, serviceName string) error {
return DeleteOperatorService(client, serviceName)
}

// DeleteOperatorService deletes an Operator backed service
// TODO: make it unlink the service from component as a part of
// https://github.com/openshift/odo/issues/3563
Expand Down
18 changes: 17 additions & 1 deletion tests/integration/operatorhub/cmd_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ spec:
Expect(stdOut).To(ContainSubstring("Couldn't find service named %q", "EtcdCluster/example"))
})

It("should successfully connect a component with an existing service", func() {
It("should successfully connect and disconnect a component with an existing service", func() {
if os.Getenv("KUBERNETES") == "true" {
Skip("This is a OpenShift specific scenario, skipping")
}
Expand All @@ -433,6 +433,22 @@ spec:

stdOut := helper.CmdShouldPass("odo", "link", "EtcdCluster/example")
Expect(stdOut).To(ContainSubstring("Successfully created link between component"))
stdOut = helper.CmdShouldFail("odo", "link", "EtcdCluster/example")
Expect(stdOut).To(ContainSubstring("already linked with the service"))

stdOut = helper.CmdShouldPass("odo", "unlink", "EtcdCluster/example")
Expect(stdOut).To(ContainSubstring("Successfully unlinked component"))

// verify that sbr is deleted
stdOut = helper.CmdShouldFail("odo", "unlink", "EtcdCluster/example")
Expect(stdOut).To(ContainSubstring("failed to unlink the service"))

// next, delete a link outside of odo (using oc) and ensure that it throws an error
helper.CmdShouldPass("odo", "link", "EtcdCluster/example")
sbrName := strings.Join([]string{componentName, "etcdcluster", "example"}, "-")
helper.CmdShouldPass("oc", "delete", fmt.Sprintf("ServiceBindingRequest/%s", sbrName))
stdOut = helper.CmdShouldFail("odo", "unlink", "EtcdCluster/example")
helper.MatchAllInOutput(stdOut, []string{"component's link with", "has been deleted outside odo"})
})
})
})

0 comments on commit 552aad7

Please sign in to comment.