mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
odo link and odo unlink write to devfile without deploying to cluster (#4819)
* Do not deploy sbr at `odo link` time, save it in devfile * Changelog * Fix remove duplicates * Do not set owner references again * Fix wait for pod terminating * Simplify env vars deduplication * Add comment * Dont use oc * link type * fix * Fix spinner * Fix tests * Fic error message for link/unlink * Review * No spinner
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
- `odo push` undeploys Operator backed services marked as managed by the current devfile not present in this devfile anymore ([#4761](https://github.com/openshift/odo/pull/4761))
|
- `odo push` undeploys Operator backed services marked as managed by the current devfile not present in this devfile anymore ([#4761](https://github.com/openshift/odo/pull/4761))
|
||||||
- param based `odo service create` for operator backed services ([#4704](https://github.com/openshift/odo/pull/4704))
|
- param based `odo service create` for operator backed services ([#4704](https://github.com/openshift/odo/pull/4704))
|
||||||
- add `odo catalog describe service <operator> --example` ([#4821](https://github.com/openshift/odo/pull/4821))
|
- add `odo catalog describe service <operator> --example` ([#4821](https://github.com/openshift/odo/pull/4821))
|
||||||
|
- `odo link` and `odo unlink` write to devfile without deploying to cluster. Deploying happens when running `odo push` ([#4819](https://github.com/openshift/odo/pull/4819))
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -344,7 +344,7 @@ openshiftci-presubmit-unittests:
|
|||||||
|
|
||||||
.PHONY: test-operator-hub
|
.PHONY: test-operator-hub
|
||||||
test-operator-hub: ## Run OperatorHub tests
|
test-operator-hub: ## Run OperatorHub tests
|
||||||
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo service command tests" tests/integration/operatorhub/
|
$(RUN_GINKGO) $(GINKGO_FLAGS) tests/integration/operatorhub/
|
||||||
|
|
||||||
.PHONY: test-cmd-devfile-describe
|
.PHONY: test-cmd-devfile-describe
|
||||||
test-cmd-devfile-describe:
|
test-cmd-devfile-describe:
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/openshift/odo/pkg/service"
|
|
||||||
|
|
||||||
"github.com/devfile/library/pkg/devfile/generator"
|
"github.com/devfile/library/pkg/devfile/generator"
|
||||||
componentlabels "github.com/openshift/odo/pkg/component/labels"
|
componentlabels "github.com/openshift/odo/pkg/component/labels"
|
||||||
"github.com/openshift/odo/pkg/envinfo"
|
"github.com/openshift/odo/pkg/envinfo"
|
||||||
|
"github.com/openshift/odo/pkg/service"
|
||||||
"github.com/openshift/odo/pkg/util"
|
"github.com/openshift/odo/pkg/util"
|
||||||
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
@@ -111,6 +110,7 @@ type Adapter struct {
|
|||||||
// Push updates the component if a matching component exists or creates one if it doesn't exist
|
// Push updates the component if a matching component exists or creates one if it doesn't exist
|
||||||
// Once the component has started, it will sync the source code to it.
|
// Once the component has started, it will sync the source code to it.
|
||||||
func (a Adapter) Push(parameters common.PushParameters) (err error) {
|
func (a Adapter) Push(parameters common.PushParameters) (err error) {
|
||||||
|
|
||||||
a.deployment, err = a.Client.GetKubeClient().GetOneDeployment(a.ComponentName, a.AppName)
|
a.deployment, err = a.Client.GetKubeClient().GetOneDeployment(a.ComponentName, a.AppName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*kclient.DeploymentNotFoundError); !ok {
|
if _, ok := err.(*kclient.DeploymentNotFoundError); !ok {
|
||||||
@@ -160,6 +160,29 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
|
|||||||
}
|
}
|
||||||
s.End(true)
|
s.End(true)
|
||||||
|
|
||||||
|
log.Info("\nUpdating services")
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
labels := componentlabels.GetLabels(a.ComponentName, a.AppName, true)
|
||||||
|
// create the Kubernetes objects from the manifest and delete the ones not in the devfile
|
||||||
|
needRestart, err := service.PushServiceFromKubernetesInlineComponents(a.Client.GetKubeClient(), k8sComponents, labels)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create service(s) associated with the component")
|
||||||
|
}
|
||||||
|
|
||||||
|
if componentExists && needRestart {
|
||||||
|
err = a.Client.GetKubeClient().WaitForPodNotReady(podName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("\nCreating Kubernetes resources for component %s", a.ComponentName)
|
log.Infof("\nCreating Kubernetes resources for component %s", a.ComponentName)
|
||||||
|
|
||||||
previousMode := parameters.EnvSpecificInfo.GetRunMode()
|
previousMode := parameters.EnvSpecificInfo.GetRunMode()
|
||||||
@@ -183,21 +206,6 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
|
|||||||
return errors.Wrap(err, "unable to create or update component")
|
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")
|
|
||||||
}
|
|
||||||
labels := componentlabels.GetLabels(a.ComponentName, a.AppName, true)
|
|
||||||
// create the Kubernetes objects from the manifest and delete the ones not in the devfile
|
|
||||||
err = service.PushServiceFromKubernetesInlineComponents(a.Client.GetKubeClient(), k8sComponents, labels)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to create service(s) associated with the component")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.deployment, err = a.Client.GetKubeClient().WaitForDeploymentRollout(a.deployment.Name)
|
a.deployment, err = a.Client.GetKubeClient().WaitForDeploymentRollout(a.deployment.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "error while waiting for deployment rollout")
|
return errors.Wrap(err, "error while waiting for deployment rollout")
|
||||||
@@ -215,17 +223,23 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ownerReference := generator.GetOwnerReference(a.deployment)
|
||||||
// update the owner reference of the PVCs with the deployment
|
// update the owner reference of the PVCs with the deployment
|
||||||
for i := range pvcs {
|
for i := range pvcs {
|
||||||
if pvcs[i].OwnerReferences != nil || pvcs[i].DeletionTimestamp != nil {
|
if pvcs[i].OwnerReferences != nil || pvcs[i].DeletionTimestamp != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = a.Client.GetKubeClient().UpdateStorageOwnerReference(&pvcs[i], generator.GetOwnerReference(a.deployment))
|
err = a.Client.GetKubeClient().UpdateStorageOwnerReference(&pvcs[i], ownerReference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = service.UpdateKubernetesInlineComponentsOwnerReferences(a.Client.GetKubeClient(), k8sComponents, ownerReference)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
parameters.EnvSpecificInfo.SetDevfileObj(a.Devfile)
|
parameters.EnvSpecificInfo.SetDevfileObj(a.Devfile)
|
||||||
err = component.ApplyConfig(&a.Client, config.LocalConfigInfo{}, parameters.EnvSpecificInfo, color.Output, componentExists, false)
|
err = component.ApplyConfig(&a.Client, config.LocalConfigInfo{}, parameters.EnvSpecificInfo, color.Output, componentExists, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ type ComponentSettings struct {
|
|||||||
URL *[]localConfigProvider.LocalURL `yaml:"Url,omitempty" json:"url,omitempty"`
|
URL *[]localConfigProvider.LocalURL `yaml:"Url,omitempty" json:"url,omitempty"`
|
||||||
// AppName is the application name. Application is a virtual concept present in odo used
|
// AppName is the application name. Application is a virtual concept present in odo used
|
||||||
// for grouping of components. A namespace can contain multiple applications
|
// for grouping of components. A namespace can contain multiple applications
|
||||||
AppName string `yaml:"AppName,omitempty" json:"appName,omitempty"`
|
AppName string `yaml:"AppName,omitempty" json:"appName,omitempty"`
|
||||||
Link *[]EnvInfoLink `yaml:"Link,omitempty" json:"link,omitempty"`
|
|
||||||
|
|
||||||
// DebugPort controls the port used by the pod to run the debugging agent on
|
// DebugPort controls the port used by the pod to run the debugging agent on
|
||||||
DebugPort *int `yaml:"DebugPort,omitempty" json:"debugPort,omitempty"`
|
DebugPort *int `yaml:"DebugPort,omitempty" json:"debugPort,omitempty"`
|
||||||
@@ -88,15 +87,6 @@ type EnvSpecificInfo struct {
|
|||||||
envinfoFileExists bool
|
envinfoFileExists bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnvInfoLink struct {
|
|
||||||
// Name of link (same as name of k8s secret)
|
|
||||||
Name string `yaml:"Name,omitempty" json:"name,omitempty"`
|
|
||||||
// Kind of service with which the component is linked
|
|
||||||
ServiceKind string `yaml:"ServiceKind,omitempty" json:"serviceKind,omitempty"`
|
|
||||||
// Name of the instance of the ServiceKind that component is linked with
|
|
||||||
ServiceName string `yaml:"ServiceName,omitempty" json:"serviceName,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func WrapForJSONOutput(compSettings ComponentSettings) JSONEnvInfoRepr {
|
func WrapForJSONOutput(compSettings ComponentSettings) JSONEnvInfoRepr {
|
||||||
return JSONEnvInfoRepr{
|
return JSONEnvInfoRepr{
|
||||||
TypeMeta: metav1.TypeMeta{
|
TypeMeta: metav1.TypeMeta{
|
||||||
@@ -216,14 +206,6 @@ func (esi *EnvSpecificInfo) SetConfiguration(parameter string, value interface{}
|
|||||||
} else {
|
} else {
|
||||||
esi.componentSettings.URL = &[]localConfigProvider.LocalURL{urlValue}
|
esi.componentSettings.URL = &[]localConfigProvider.LocalURL{urlValue}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "link":
|
|
||||||
linkValue := value.(EnvInfoLink)
|
|
||||||
if esi.componentSettings.Link != nil {
|
|
||||||
*esi.componentSettings.Link = append(*esi.componentSettings.Link, linkValue)
|
|
||||||
} else {
|
|
||||||
esi.componentSettings.Link = &[]EnvInfoLink{linkValue}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return esi.writeToFile()
|
return esi.writeToFile()
|
||||||
@@ -305,26 +287,6 @@ func (esi *EnvSpecificInfo) DeleteConfiguration(parameter string) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// GetComponentSettings returns the componentSettings from envinfo
|
||||||
func (esi *EnvSpecificInfo) GetComponentSettings() ComponentSettings {
|
func (esi *EnvSpecificInfo) GetComponentSettings() ComponentSettings {
|
||||||
return esi.componentSettings
|
return esi.componentSettings
|
||||||
@@ -431,26 +393,6 @@ func (ei *EnvInfo) SetIsRouteSupported(isRouteSupported bool) {
|
|||||||
ei.isRouteSupported = isRouteSupported
|
ei.isRouteSupported = isRouteSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLink returns the EnvInfoLink, returns default if nil
|
|
||||||
func (ei *EnvInfo) GetLink() []EnvInfoLink {
|
|
||||||
if ei.componentSettings.Link == nil {
|
|
||||||
return []EnvInfoLink{}
|
|
||||||
}
|
|
||||||
return *ei.componentSettings.Link
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchLinkName searches for a Link with given service kind and service name
|
|
||||||
// and returns its name if found
|
|
||||||
func (ei *EnvInfo) SearchLinkName(serviceKind, serviceName string) (string, bool) {
|
|
||||||
links := ei.GetLink()
|
|
||||||
for _, link := range links {
|
|
||||||
if link.ServiceKind == serviceKind && link.ServiceName == serviceName {
|
|
||||||
return link.Name, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Name is the name of the setting controlling the component name
|
// Name is the name of the setting controlling the component name
|
||||||
Name = "Name"
|
Name = "Name"
|
||||||
@@ -472,10 +414,6 @@ const (
|
|||||||
Push = "PUSH"
|
Push = "PUSH"
|
||||||
// PushDescription is the description of push parameter
|
// PushDescription is the description of push parameter
|
||||||
PushDescription = "Push parameter is the action to write devfile commands to env.yaml"
|
PushDescription = "Push parameter is the action to write devfile commands to env.yaml"
|
||||||
// Link parameter
|
|
||||||
Link = "LINK"
|
|
||||||
// LinkDescription is the description of Link
|
|
||||||
LinkDescription = "Link to an Operator backed service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -485,7 +423,6 @@ var (
|
|||||||
DebugPort: DebugPortDescription,
|
DebugPort: DebugPortDescription,
|
||||||
URL: URLDescription,
|
URL: URLDescription,
|
||||||
Push: PushDescription,
|
Push: PushDescription,
|
||||||
Link: LinkDescription,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lowerCaseLocalParameters = util.GetLowerCaseParameters(GetLocallySupportedParameters())
|
lowerCaseLocalParameters = util.GetLowerCaseParameters(GetLocallySupportedParameters())
|
||||||
|
|||||||
@@ -893,75 +893,6 @@ func TestRemoveEndpointInDevfile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchLinkName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ei *EnvInfo
|
|
||||||
serviceKind string
|
|
||||||
serviceName string
|
|
||||||
want string
|
|
||||||
wantFound bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Case 1: Existing link",
|
|
||||||
ei: &EnvInfo{
|
|
||||||
componentSettings: ComponentSettings{
|
|
||||||
Link: &[]EnvInfoLink{
|
|
||||||
{
|
|
||||||
Name: "a first name",
|
|
||||||
ServiceKind: "a first kind",
|
|
||||||
ServiceName: "a first service name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "a name",
|
|
||||||
ServiceKind: "a kind",
|
|
||||||
ServiceName: "a service name",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
serviceKind: "a kind",
|
|
||||||
serviceName: "a service name",
|
|
||||||
want: "a name",
|
|
||||||
wantFound: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Case 2: Non existing link",
|
|
||||||
ei: &EnvInfo{
|
|
||||||
componentSettings: ComponentSettings{
|
|
||||||
Link: &[]EnvInfoLink{
|
|
||||||
{
|
|
||||||
Name: "a first name",
|
|
||||||
ServiceKind: "a first kind",
|
|
||||||
ServiceName: "a first service name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "a name",
|
|
||||||
ServiceKind: "a kind",
|
|
||||||
ServiceName: "a service name",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
serviceKind: "an unknown kind",
|
|
||||||
serviceName: "a service name",
|
|
||||||
wantFound: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result, found := tt.ei.SearchLinkName(tt.serviceKind, tt.serviceName)
|
|
||||||
if found != tt.wantFound {
|
|
||||||
t.Errorf("Expected found %v, got %v", tt.wantFound, found)
|
|
||||||
}
|
|
||||||
if found && result != tt.want {
|
|
||||||
t.Errorf("Expected %q, got %q", tt.want, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDirectoryAndFile(create bool, fs filesystem.Filesystem, odoDir string) error {
|
func createDirectoryAndFile(create bool, fs filesystem.Filesystem, odoDir string) error {
|
||||||
if !create {
|
if !create {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
@@ -115,6 +116,40 @@ func (c *Client) ListDeployments(selector string) (*appsv1.DeploymentList, error
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForPodNotReady waits for the status of the given pod to be not ready, or the pod to be deleted
|
||||||
|
func (c *Client) WaitForPodNotReady(name string) error {
|
||||||
|
watch, err := c.KubeClient.CoreV1().Pods(c.Namespace).Watch(context.TODO(), metav1.ListOptions{FieldSelector: "metadata.name=" + name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer watch.Stop()
|
||||||
|
|
||||||
|
if _, err = c.KubeClient.CoreV1().Pods(c.Namespace).Get(context.TODO(), name, metav1.GetOptions{}); kerrors.IsNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Minute):
|
||||||
|
return fmt.Errorf("timeout while waiting for %q pod to stop", name)
|
||||||
|
|
||||||
|
case val, ok := <-watch.ResultChan():
|
||||||
|
if !ok {
|
||||||
|
return errors.New("error getting value from resultchan")
|
||||||
|
}
|
||||||
|
if pod, ok := val.Object.(*corev1.Pod); ok {
|
||||||
|
for _, cond := range pod.Status.Conditions {
|
||||||
|
if cond.Type == "Ready" {
|
||||||
|
if cond.Status == corev1.ConditionFalse {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WaitForDeploymentRollout waits for deployment to finish rollout. Returns the state of the deployment after rollout.
|
// WaitForDeploymentRollout waits for deployment to finish rollout. Returns the state of the deployment after rollout.
|
||||||
func (c *Client) WaitForDeploymentRollout(deploymentName string) (*appsv1.Deployment, error) {
|
func (c *Client) WaitForDeploymentRollout(deploymentName string) (*appsv1.Deployment, error) {
|
||||||
klog.V(3).Infof("Waiting for %s deployment rollout", deploymentName)
|
klog.V(3).Infof("Waiting for %s deployment rollout", deploymentName)
|
||||||
@@ -225,13 +260,17 @@ func (c *Client) UpdateDeployment(deploy appsv1.Deployment) (*appsv1.Deployment,
|
|||||||
// odo overrides it with the value it expects instead of failing due to conflict.
|
// odo overrides it with the value it expects instead of failing due to conflict.
|
||||||
func (c *Client) ApplyDeployment(deploy appsv1.Deployment) (*appsv1.Deployment, error) {
|
func (c *Client) ApplyDeployment(deploy appsv1.Deployment) (*appsv1.Deployment, error) {
|
||||||
data, err := json.Marshal(deploy)
|
data, err := json.Marshal(deploy)
|
||||||
|
|
||||||
klog.V(5).Infoln("Applying Deployment via server-side apply:")
|
|
||||||
klog.V(5).Infoln(resourceAsJson(deploy))
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "unable to marshal deployment")
|
return nil, errors.Wrapf(err, "unable to marshal deployment")
|
||||||
}
|
}
|
||||||
|
klog.V(5).Infoln("Applying Deployment via server-side apply:")
|
||||||
|
klog.V(5).Infoln(resourceAsJson(deploy))
|
||||||
|
|
||||||
|
err = c.removeDuplicateEnv(deploy.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
deployment, err := c.KubeClient.AppsV1().Deployments(c.Namespace).Patch(context.TODO(), deploy.Name, types.ApplyPatchType, data, metav1.PatchOptions{FieldManager: FieldManager, Force: boolPtr(true)})
|
deployment, err := c.KubeClient.AppsV1().Deployments(c.Namespace).Patch(context.TODO(), deploy.Name, types.ApplyPatchType, data, metav1.PatchOptions{FieldManager: FieldManager, Force: boolPtr(true)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "unable to update Deployment %s", deploy.Name)
|
return nil, errors.Wrapf(err, "unable to update Deployment %s", deploy.Name)
|
||||||
@@ -239,6 +278,41 @@ func (c *Client) ApplyDeployment(deploy appsv1.Deployment) (*appsv1.Deployment,
|
|||||||
return deployment, nil
|
return deployment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeDuplicateEnv removes duplicate environment variables from containers, due to a bug in Service Binding Operator:
|
||||||
|
// https://github.com/redhat-developer/service-binding-operator/issues/983
|
||||||
|
func (c *Client) removeDuplicateEnv(deploymentName string) error {
|
||||||
|
deployment, err := c.KubeClient.AppsV1().Deployments(c.Namespace).Get(context.Background(), deploymentName, metav1.GetOptions{})
|
||||||
|
if kerrors.IsNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
changes := false
|
||||||
|
containers := deployment.Spec.Template.Spec.Containers
|
||||||
|
for i := range containers {
|
||||||
|
found := map[string]bool{}
|
||||||
|
var newEnv []corev1.EnvVar
|
||||||
|
for _, env := range containers[i].Env {
|
||||||
|
if _, ok := found[env.Name]; !ok {
|
||||||
|
found[env.Name] = true
|
||||||
|
newEnv = append(newEnv, env)
|
||||||
|
} else {
|
||||||
|
changes = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containers[i].Env = newEnv
|
||||||
|
}
|
||||||
|
if changes {
|
||||||
|
_, err = c.KubeClient.AppsV1().Deployments(c.Namespace).Update(context.Background(), deployment, metav1.UpdateOptions{})
|
||||||
|
if kerrors.IsNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteDeployment deletes the deployments with the given selector
|
// DeleteDeployment deletes the deployments with the given selector
|
||||||
func (c *Client) DeleteDeployment(labels map[string]string) error {
|
func (c *Client) DeleteDeployment(labels map[string]string) error {
|
||||||
if labels == nil {
|
if labels == nil {
|
||||||
@@ -299,6 +373,17 @@ func (c *Client) GetDynamicResource(group, version, resource, name string) (*uns
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDynamicResource updates a dynamic resource
|
||||||
|
func (c *Client) UpdateDynamicResource(group, version, resource, name string, u *unstructured.Unstructured) error {
|
||||||
|
deploymentRes := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
|
||||||
|
|
||||||
|
_, err := c.DynamicClient.Resource(deploymentRes).Namespace(c.Namespace).Update(context.TODO(), u, metav1.UpdateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteDynamicResource deletes an instance, specified by name, of a Custom Resource
|
// DeleteDynamicResource deletes an instance, specified by name, of a Custom Resource
|
||||||
func (c *Client) DeleteDynamicResource(name, group, version, resource string) error {
|
func (c *Client) DeleteDynamicResource(name, group, version, resource string) error {
|
||||||
deploymentRes := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
|
deploymentRes := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ func TestCreateDeployment(t *testing.T) {
|
|||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
||||||
if len(fkclientset.Kubernetes.Actions()) != 1 {
|
if len(fkclientset.Kubernetes.Actions()) != 2 {
|
||||||
t.Errorf("expected 1 action in StartDeployment got: %v", fkclientset.Kubernetes.Actions())
|
t.Errorf("expected 2 action in StartDeployment got %d: %v", len(fkclientset.Kubernetes.Actions()), fkclientset.Kubernetes.Actions())
|
||||||
} else {
|
} else {
|
||||||
if createdDeployment.Name != tt.deploymentName {
|
if createdDeployment.Name != tt.deploymentName {
|
||||||
t.Errorf("deployment name does not match the expected name, expected: %s, got %s", tt.deploymentName, createdDeployment.Name)
|
t.Errorf("deployment name does not match the expected name, expected: %s, got %s", tt.deploymentName, createdDeployment.Name)
|
||||||
@@ -261,8 +261,8 @@ func TestUpdateDeployment(t *testing.T) {
|
|||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
||||||
if len(fkclientset.Kubernetes.Actions()) != 1 {
|
if len(fkclientset.Kubernetes.Actions()) != 2 {
|
||||||
t.Errorf("expected 1 action in UpdateDeployment got: %v", fkclientset.Kubernetes.Actions())
|
t.Errorf("expected 2 action in UpdateDeployment got %d: %v", len(fkclientset.Kubernetes.Actions()), fkclientset.Kubernetes.Actions())
|
||||||
} else {
|
} else {
|
||||||
if updatedDeployment.Name != tt.deploymentName {
|
if updatedDeployment.Name != tt.deploymentName {
|
||||||
t.Errorf("deployment name does not match the expected name, expected: %s, got %s", tt.deploymentName, updatedDeployment.Name)
|
t.Errorf("deployment name does not match the expected name, expected: %s, got %s", tt.deploymentName, updatedDeployment.Name)
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/devfile/library/pkg/devfile/generator"
|
|
||||||
"github.com/openshift/odo/pkg/component"
|
"github.com/openshift/odo/pkg/component"
|
||||||
componentlabels "github.com/openshift/odo/pkg/component/labels"
|
componentlabels "github.com/openshift/odo/pkg/component/labels"
|
||||||
"github.com/openshift/odo/pkg/envinfo"
|
|
||||||
"github.com/openshift/odo/pkg/kclient"
|
"github.com/openshift/odo/pkg/kclient"
|
||||||
"github.com/openshift/odo/pkg/log"
|
"github.com/openshift/odo/pkg/log"
|
||||||
"github.com/openshift/odo/pkg/occlient"
|
"github.com/openshift/odo/pkg/occlient"
|
||||||
@@ -18,6 +16,7 @@ import (
|
|||||||
"github.com/openshift/odo/pkg/util"
|
"github.com/openshift/odo/pkg/util"
|
||||||
servicebinding "github.com/redhat-developer/service-binding-operator/api/v1alpha1"
|
servicebinding "github.com/redhat-developer/service-binding-operator/api/v1alpha1"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@@ -53,8 +52,16 @@ func newCommonLinkOptions() *commonLinkOptions {
|
|||||||
return &commonLinkOptions{}
|
return &commonLinkOptions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *commonLinkOptions) getLinkType() string {
|
||||||
|
linkType := "component"
|
||||||
|
if o.isTargetAService {
|
||||||
|
linkType = "service"
|
||||||
|
}
|
||||||
|
return linkType
|
||||||
|
}
|
||||||
|
|
||||||
// Complete completes LinkOptions after they've been created
|
// Complete completes LinkOptions after they've been created
|
||||||
func (o *commonLinkOptions) complete(name string, cmd *cobra.Command, args []string) (err error) {
|
func (o *commonLinkOptions) complete(name string, cmd *cobra.Command, args []string, context string) (err error) {
|
||||||
o.csvSupport, _ = svc.IsCSVSupported()
|
o.csvSupport, _ = svc.IsCSVSupported()
|
||||||
|
|
||||||
o.operationName = name
|
o.operationName = name
|
||||||
@@ -71,7 +78,11 @@ func (o *commonLinkOptions) complete(name string, cmd *cobra.Command, args []str
|
|||||||
// and must create s2i context instead
|
// and must create s2i context instead
|
||||||
o.Context, err = genericclioptions.NewContextCreatingAppIfNeeded(cmd)
|
o.Context, err = genericclioptions.NewContextCreatingAppIfNeeded(cmd)
|
||||||
} else {
|
} else {
|
||||||
o.Context, err = genericclioptions.NewDevfileContext(cmd)
|
o.Context, err = genericclioptions.New(genericclioptions.CreateParameters{
|
||||||
|
Cmd: cmd,
|
||||||
|
DevfilePath: component.DevfilePath,
|
||||||
|
ComponentContext: context,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -163,11 +174,6 @@ func (o *commonLinkOptions) run() (err error) {
|
|||||||
return o.linkOperator()
|
return o.linkOperator()
|
||||||
}
|
}
|
||||||
|
|
||||||
linkType := "Component"
|
|
||||||
if o.isTargetAService {
|
|
||||||
linkType = "Service"
|
|
||||||
}
|
|
||||||
|
|
||||||
var component string
|
var component string
|
||||||
if o.Context.EnvSpecificInfo != nil {
|
if o.Context.EnvSpecificInfo != nil {
|
||||||
component = o.EnvSpecificInfo.GetName()
|
component = o.EnvSpecificInfo.GetName()
|
||||||
@@ -183,9 +189,9 @@ func (o *commonLinkOptions) run() (err error) {
|
|||||||
|
|
||||||
switch o.operationName {
|
switch o.operationName {
|
||||||
case "link":
|
case "link":
|
||||||
log.Successf("%s %s has been successfully linked to the component %s\n", linkType, o.suppliedName, component)
|
log.Successf("The %s %s has been successfully linked to the component %s\n", o.getLinkType(), o.suppliedName, component)
|
||||||
case "unlink":
|
case "unlink":
|
||||||
log.Successf("%s %s has been successfully unlinked from the component %s\n", linkType, o.suppliedName, component)
|
log.Successf("The %s %s has been successfully unlinked from the component %s\n", o.getLinkType(), o.suppliedName, component)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown operation %s", o.operationName)
|
return fmt.Errorf("unknown operation %s", o.operationName)
|
||||||
}
|
}
|
||||||
@@ -273,11 +279,6 @@ func (o *commonLinkOptions) getServiceBindingName(componentName string) string {
|
|||||||
return strings.Join([]string{componentName, strings.ToLower(o.serviceType), o.serviceName}, "-")
|
return strings.Join([]string{componentName, strings.ToLower(o.serviceType), o.serviceName}, "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSvcFullName returns service name in the format <service-type>/<service-name>
|
|
||||||
func getSvcFullName(serviceType, serviceName string) string {
|
|
||||||
return strings.Join([]string{serviceType, serviceName}, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeForOperator completes the options when svc is supported
|
// completeForOperator completes the options when svc is supported
|
||||||
func (o *commonLinkOptions) completeForOperator() (err error) {
|
func (o *commonLinkOptions) completeForOperator() (err error) {
|
||||||
serviceBindingSupport, err := o.Client.GetKubeClient().IsServiceBindingSupported()
|
serviceBindingSupport, err := o.Client.GetKubeClient().IsServiceBindingSupported()
|
||||||
@@ -309,9 +310,6 @@ func (o *commonLinkOptions) completeForOperator() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// make this deployment the owner of the link we're creating so that link gets deleted upon doing "odo delete"
|
|
||||||
ownerReference := generator.GetOwnerReference(deployment)
|
|
||||||
|
|
||||||
deploymentGVR, err := o.KClient.GetDeploymentAPIVersion()
|
deploymentGVR, err := o.KClient.GetDeploymentAPIVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -323,8 +321,7 @@ func (o *commonLinkOptions) completeForOperator() (err error) {
|
|||||||
Kind: kclient.ServiceBindingKind,
|
Kind: kclient.ServiceBindingKind,
|
||||||
},
|
},
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: o.getServiceBindingName(componentName),
|
Name: o.getServiceBindingName(componentName),
|
||||||
Namespace: o.EnvSpecificInfo.GetNamespace(),
|
|
||||||
},
|
},
|
||||||
Spec: servicebinding.ServiceBindingSpec{
|
Spec: servicebinding.ServiceBindingSpec{
|
||||||
DetectBindingResources: true,
|
DetectBindingResources: true,
|
||||||
@@ -339,13 +336,13 @@ func (o *commonLinkOptions) completeForOperator() (err error) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
o.serviceBinding.SetOwnerReferences(append(o.serviceBinding.GetOwnerReferences(), ownerReference))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateForOperator validates the options when svc is supported
|
// validateForOperator validates the options when svc is supported
|
||||||
func (o *commonLinkOptions) validateForOperator() (err error) {
|
func (o *commonLinkOptions) validateForOperator() (err error) {
|
||||||
var svcFullName string
|
var svcFullName string
|
||||||
|
|
||||||
if o.isTargetAService {
|
if o.isTargetAService {
|
||||||
// let's validate if the service exists
|
// let's validate if the service exists
|
||||||
svcFullName = strings.Join([]string{o.serviceType, o.serviceName}, "/")
|
svcFullName = strings.Join([]string{o.serviceType, o.serviceName}, "/")
|
||||||
@@ -357,8 +354,14 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
|||||||
return fmt.Errorf("couldn't find service named %q. Refer %q to see list of running services", svcFullName, "odo service list")
|
return fmt.Errorf("couldn't find service named %q. Refer %q to see list of running services", svcFullName, "odo service list")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
o.serviceType = "Service"
|
||||||
|
svcFullName = o.serviceName
|
||||||
if o.suppliedName == o.EnvSpecificInfo.GetName() {
|
if o.suppliedName == o.EnvSpecificInfo.GetName() {
|
||||||
return fmt.Errorf("the component %q cannot be linked with itself", o.suppliedName)
|
if o.operationName == unlink {
|
||||||
|
return fmt.Errorf("the component %q cannot be unlinked from itself", o.suppliedName)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("the component %q cannot be linked with itself", o.suppliedName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := o.Context.Client.GetKubeClient().GetService(o.suppliedName)
|
_, err := o.Context.Client.GetKubeClient().GetService(o.suppliedName)
|
||||||
@@ -371,29 +374,12 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if o.operationName == unlink {
|
if o.operationName == unlink {
|
||||||
serviceBindingName, found := o.EnvSpecificInfo.SearchLinkName(o.serviceType, o.serviceName)
|
_, found, err := svc.FindDevfileServiceBinding(o.EnvSpecificInfo.GetDevfileObj(), o.serviceType, o.serviceName)
|
||||||
if !found {
|
|
||||||
if o.isTargetAService {
|
|
||||||
return fmt.Errorf("failed to unlink the service %q since no link was found in the env file", svcFullName)
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("failed to unlink the component %q since no link was found in the env file", o.suppliedName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify if the underlying service binding request actually exists
|
|
||||||
serviceBindingSvcFullName := strings.Join([]string{kclient.ServiceBindingKind, serviceBindingName}, "/")
|
|
||||||
serviceBindingExists, err := svc.OperatorSvcExists(o.KClient, serviceBindingSvcFullName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !serviceBindingExists {
|
if !found {
|
||||||
// This could have happened if the service binding was deleted outside odo workflow (eg: oc delete sb/<sb-name>)
|
return fmt.Errorf("failed to unlink the %s %q since no link was found in the configuration referring this %s", o.getLinkType(), svcFullName, o.getLinkType())
|
||||||
// we must remove entry of the link from env.yaml in this case
|
|
||||||
err = o.Context.EnvSpecificInfo.DeleteLink(serviceBindingName)
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -421,7 +407,6 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
|||||||
Kind: kind,
|
Kind: kind,
|
||||||
Name: o.serviceName,
|
Name: o.serviceName,
|
||||||
},
|
},
|
||||||
Namespace: &o.KClient.Namespace,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -432,7 +417,6 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
|||||||
Kind: "Service",
|
Kind: "Service",
|
||||||
Name: o.serviceName,
|
Name: o.serviceName,
|
||||||
},
|
},
|
||||||
Namespace: &o.KClient.Namespace,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,70 +430,57 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
|||||||
// the current component with the given component's service
|
// the current component with the given component's service
|
||||||
// and stores the link info in the env
|
// and stores the link info in the env
|
||||||
func (o *commonLinkOptions) linkOperator() (err error) {
|
func (o *commonLinkOptions) linkOperator() (err error) {
|
||||||
// convert service binding request into a map[string]interface{} type so
|
// Convert ServiceBinding -> JSON -> Map -> YAML
|
||||||
// as to use it with dynamic client
|
// JSON conversion step is necessary to inline TypeMeta
|
||||||
serviceBindingMap := make(map[string]interface{})
|
|
||||||
intermediate, err := json.Marshal(o.serviceBinding)
|
intermediate, err := json.Marshal(o.serviceBinding)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceBindingMap := make(map[string]interface{})
|
||||||
err = json.Unmarshal(intermediate, &serviceBindingMap)
|
err = json.Unmarshal(intermediate, &serviceBindingMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// this creates a link by creating a service of type
|
yamlDesc, err := yaml.Marshal(serviceBindingMap)
|
||||||
// "ServiceBindingRequest" from the Operator "ServiceBindingOperator".
|
|
||||||
err = o.KClient.CreateDynamicResource(serviceBindingMap, kclient.ServiceBindingGroup, kclient.ServiceBindingVersion, kclient.ServiceBindingResource)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "already exists") {
|
|
||||||
return fmt.Errorf("component %q is already linked with the service %q", o.Context.EnvSpecificInfo.GetName(), o.suppliedName)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// once the link is created, we need to store the information in
|
|
||||||
// env.yaml so that subsequent odo push can create a new deployment
|
|
||||||
// based on it
|
|
||||||
err = o.Context.EnvSpecificInfo.SetConfiguration("link", envinfo.EnvInfoLink{Name: o.serviceBinding.GetName(), ServiceKind: o.serviceType, ServiceName: o.serviceName})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
targetType := "component"
|
_, found, err := svc.FindDevfileServiceBinding(o.EnvSpecificInfo.GetDevfileObj(), o.serviceType, o.serviceName)
|
||||||
if o.isTargetAService {
|
if err != nil {
|
||||||
targetType = "service"
|
return err
|
||||||
}
|
}
|
||||||
log.Successf("Successfully created link between component %q and %s %q\n", o.Context.EnvSpecificInfo.GetName(), targetType, o.suppliedName)
|
if found {
|
||||||
|
return fmt.Errorf("component %q is already linked with the %s %q", o.Context.EnvSpecificInfo.GetName(), o.getLinkType(), o.suppliedName)
|
||||||
|
}
|
||||||
|
err = svc.AddKubernetesComponentToDevfile(string(yamlDesc), o.serviceBinding.Name, o.EnvSpecificInfo.GetDevfileObj())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Successf("Successfully created link between component %q and %s %q\n", o.Context.EnvSpecificInfo.GetName(), o.getLinkType(), o.suppliedName)
|
||||||
log.Italic("To apply the link, please use `odo push`")
|
log.Italic("To apply the link, please use `odo push`")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// unlinkOperator deletes the service binding resource from the cluster
|
// unlinkOperator deletes the service binding resource from the devfile
|
||||||
// and deletes the link info from the env
|
|
||||||
func (o *commonLinkOptions) unlinkOperator() (err error) {
|
func (o *commonLinkOptions) unlinkOperator() (err error) {
|
||||||
serviceBindingName, found := o.EnvSpecificInfo.SearchLinkName(o.serviceType, o.serviceName)
|
|
||||||
if !found {
|
// We already tested `found` in `validateForOperator`
|
||||||
return fmt.Errorf("failed to unlink the service %q of type %q since no link found in env file", o.serviceName, o.serviceType)
|
name, _, err := svc.FindDevfileServiceBinding(o.EnvSpecificInfo.GetDevfileObj(), o.serviceType, o.serviceName)
|
||||||
}
|
|
||||||
svcFullName := getSvcFullName(kclient.ServiceBindingKind, serviceBindingName)
|
|
||||||
err = svc.DeleteServiceBindingRequest(o.KClient, svcFullName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = o.Context.EnvSpecificInfo.DeleteLink(serviceBindingName)
|
err = svc.DeleteKubernetesComponentFromDevfile(name, o.EnvSpecificInfo.GetDevfileObj())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
targetType := "component"
|
log.Successf("Successfully unlinked component %q from %s %q\n", o.Context.EnvSpecificInfo.GetName(), o.getLinkType(), o.suppliedName)
|
||||||
if o.isTargetAService {
|
|
||||||
targetType = "service"
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Successf("Successfully unlinked component %q from %s %q\n", o.Context.EnvSpecificInfo.GetName(), targetType, o.suppliedName)
|
|
||||||
log.Italic("To apply the changes, please use `odo push`")
|
log.Italic("To apply the changes, please use `odo push`")
|
||||||
|
return nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,12 +234,6 @@ func (do *DeleteOptions) DevFileRun() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error occurred while deleting component, cause: %v", err)
|
log.Errorf("error occurred while deleting component, cause: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the information about link of the components because deleting a component also deletes its links (Service Binding Requests)
|
|
||||||
err = do.EnvSpecificInfo.DeleteConfiguration("link")
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("error occurred while deleting environment specific information of the component, cause: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log.Error("Aborting deletion of component")
|
log.Error("Aborting deletion of component")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (o *LinkOptions) Complete(name string, cmd *cobra.Command, args []string) (
|
|||||||
o.commonLinkOptions.devfilePath = filepath.Join(o.componentContext, DevfilePath)
|
o.commonLinkOptions.devfilePath = filepath.Join(o.componentContext, DevfilePath)
|
||||||
o.commonLinkOptions.csvSupport, _ = svc.IsCSVSupported()
|
o.commonLinkOptions.csvSupport, _ = svc.IsCSVSupported()
|
||||||
|
|
||||||
err = o.complete(name, cmd, args)
|
err = o.complete(name, cmd, args, o.componentContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func NewUnlinkOptions() *UnlinkOptions {
|
|||||||
// Complete completes UnlinkOptions after they've been created
|
// Complete completes UnlinkOptions after they've been created
|
||||||
func (o *UnlinkOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
|
func (o *UnlinkOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
|
||||||
o.commonLinkOptions.csvSupport, _ = svc.IsCSVSupported()
|
o.commonLinkOptions.csvSupport, _ = svc.IsCSVSupported()
|
||||||
err = o.complete(name, cmd, args)
|
err = o.complete(name, cmd, args, o.componentContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/devfile/library/pkg/devfile/parser"
|
"github.com/devfile/library/pkg/devfile/parser"
|
||||||
|
servicebinding "github.com/redhat-developer/service-binding-operator/api/v1alpha1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const provisionedAndBoundStatus = "ProvisionedAndBound"
|
const provisionedAndBoundStatus = "ProvisionedAndBound"
|
||||||
@@ -148,12 +149,6 @@ func DeleteServiceAndUnlinkComponents(client *occlient.Client, serviceName strin
|
|||||||
return nil
|
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
|
// DeleteOperatorService deletes an Operator backed service
|
||||||
// TODO: make it unlink the service from component as a part of
|
// TODO: make it unlink the service from component as a part of
|
||||||
// https://github.com/openshift/odo/issues/3563
|
// https://github.com/openshift/odo/issues/3563
|
||||||
@@ -735,6 +730,44 @@ func ListDevfileServices(devfileObj parser.DevfileObj) ([]string, error) {
|
|||||||
return services, nil
|
return services, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindDevfileServiceBinding returns the name of the ServiceBinding defined in a Devfile matching kind and name
|
||||||
|
func FindDevfileServiceBinding(devfileObj parser.DevfileObj, kind string, name string) (string, bool, error) {
|
||||||
|
if devfileObj.Data == nil {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
components, err := devfileObj.Data.GetComponents(common.DevfileOptions{
|
||||||
|
ComponentOptions: parsercommon.ComponentOptions{ComponentType: devfile.KubernetesComponentType},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range components {
|
||||||
|
var u unstructured.Unstructured
|
||||||
|
err = yaml.Unmarshal([]byte(c.Kubernetes.Inlined), &u)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLinkResource(u.GetKind()) {
|
||||||
|
var sbr servicebinding.ServiceBinding
|
||||||
|
err = yaml.Unmarshal([]byte(c.Kubernetes.Inlined), &sbr)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
services := sbr.Spec.Services
|
||||||
|
if len(services) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
service := services[0]
|
||||||
|
if service.Kind == kind && service.Name == name {
|
||||||
|
return u.GetName(), true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddKubernetesComponentToDevfile adds service definition to devfile as an inlined Kubernetes component
|
// AddKubernetesComponentToDevfile adds service definition to devfile as an inlined Kubernetes component
|
||||||
func AddKubernetesComponentToDevfile(crd, name string, devfileObj parser.DevfileObj) error {
|
func AddKubernetesComponentToDevfile(crd, name string, devfileObj parser.DevfileObj) error {
|
||||||
err := devfileObj.Data.AddComponents([]devfile.Component{{
|
err := devfileObj.Data.AddComponents([]devfile.Component{{
|
||||||
@@ -857,34 +890,136 @@ func (d *DynamicCRD) AddComponentLabelsToCRD(labels map[string]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PushServiceFromKubernetesInlineComponents updates service(s) from Kubernetes Inlined component in a devfile by creating new ones or removing old ones
|
// PushServiceFromKubernetesInlineComponents updates service(s) from Kubernetes Inlined component in a devfile by creating new ones or removing old ones
|
||||||
func PushServiceFromKubernetesInlineComponents(client *kclient.Client, k8sComponents []devfile.Component, labels map[string]string) error {
|
// returns true if the component needs to be restarted (when a service binding has been created or deleted)
|
||||||
|
func PushServiceFromKubernetesInlineComponents(client *kclient.Client, k8sComponents []devfile.Component, labels map[string]string) (bool, error) {
|
||||||
|
|
||||||
// check csv support before proceeding
|
// check csv support before proceeding
|
||||||
csvSupported, err := IsCSVSupported()
|
csvSupported, err := IsCSVSupported()
|
||||||
if err != nil || !csvSupported {
|
if err != nil || !csvSupported {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
created := []string{}
|
type DeployedInfo struct {
|
||||||
deleted := []string{}
|
DoesDeleteRestartsComponent bool
|
||||||
|
Kind string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
deployed := map[string]struct{}{}
|
deployed := map[string]DeployedInfo{}
|
||||||
|
|
||||||
deployedServices, _, err := ListOperatorServices(client)
|
deployedServices, _, err := ListOperatorServices(client)
|
||||||
if err != nil && err != kclient.ErrNoSuchOperator {
|
if err != nil && err != kclient.ErrNoSuchOperator {
|
||||||
// We ignore ErrNoSuchOperator error as we can deduce Operator Services are not installed
|
// We ignore ErrNoSuchOperator error as we can deduce Operator Services are not installed
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
for _, svc := range deployedServices {
|
for _, svc := range deployedServices {
|
||||||
name := svc.GetName()
|
name := svc.GetName()
|
||||||
kind := svc.GetKind()
|
kind := svc.GetKind()
|
||||||
deployedLabels := svc.GetLabels()
|
deployedLabels := svc.GetLabels()
|
||||||
if deployedLabels[applabels.ManagedBy] == "odo" && deployedLabels[componentlabels.ComponentLabel] == labels[componentlabels.ComponentLabel] {
|
if deployedLabels[applabels.ManagedBy] == "odo" && deployedLabels[componentlabels.ComponentLabel] == labels[componentlabels.ComponentLabel] {
|
||||||
deployed[kind+"/"+name] = struct{}{}
|
deployed[kind+"/"+name] = DeployedInfo{
|
||||||
|
DoesDeleteRestartsComponent: isLinkResource(kind),
|
||||||
|
Kind: kind,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
needRestart := false
|
||||||
|
madeChange := false
|
||||||
|
|
||||||
|
// create an object on the kubernetes cluster for all the Kubernetes Inlined components
|
||||||
|
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 false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cr, csv, err := GetCSV(client, d.OriginalCRD)
|
||||||
|
if err != nil {
|
||||||
|
return false, 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 false, err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add labels to the CRD before creation
|
||||||
|
d.AddComponentLabelsToCRD(labels)
|
||||||
|
|
||||||
|
crdName, ok := getCRDName(d.OriginalCRD)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := deployed[cr+"/"+crdName]; !found && isLinkResource(cr) {
|
||||||
|
// If creating the ServiceBinding, the component will restart
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(deployed, cr+"/"+crdName)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
continue // this ensures that services slice is not updated
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name, _ := d.GetServiceNameFromCRD() // ignoring error because invalid yaml won't be inserted into devfile through odo
|
||||||
|
if isLinkResource(cr) {
|
||||||
|
log.Successf("Created link %q on the cluster; component will be restarted", name)
|
||||||
|
} else {
|
||||||
|
log.Successf("Created service %q on the cluster; refer %q to know how to link it to the component", strings.Join([]string{kind, name}, "/"), "odo link -h")
|
||||||
|
}
|
||||||
|
madeChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val := range deployed {
|
||||||
|
err = DeleteOperatorService(client, key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLinkResource(val.Kind) {
|
||||||
|
log.Successf("Deleted link %q on the cluster; component will be restarted", val.Name)
|
||||||
|
} else {
|
||||||
|
log.Successf("Deleted service %q from the cluster", key)
|
||||||
|
}
|
||||||
|
madeChange = true
|
||||||
|
|
||||||
|
if val.DoesDeleteRestartsComponent {
|
||||||
|
needRestart = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create an object on the kubernetes cluster for all the Kubernetes Inlined components
|
if !madeChange {
|
||||||
|
log.Success("Services and Links are in sync with the cluster, no changes are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return needRestart, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKubernetesInlineComponentsOwnerReferences adds an owner reference to an inlined Kubernetes resource
|
||||||
|
// if not already present in the list of owner references
|
||||||
|
func UpdateKubernetesInlineComponentsOwnerReferences(client *kclient.Client, k8sComponents []devfile.Component, ownerReference metav1.OwnerReference) error {
|
||||||
for _, c := range k8sComponents {
|
for _, c := range k8sComponents {
|
||||||
// get the string representation of the YAML definition of a CRD
|
// get the string representation of the YAML definition of a CRD
|
||||||
strCRD := c.Kubernetes.Inlined
|
strCRD := c.Kubernetes.Inlined
|
||||||
@@ -901,10 +1036,10 @@ func PushServiceFromKubernetesInlineComponents(client *kclient.Client, k8sCompon
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var group, version, kind, resource string
|
var group, version, resource string
|
||||||
for _, crd := range csv.Spec.CustomResourceDefinitions.Owned {
|
for _, crd := range csv.Spec.CustomResourceDefinitions.Owned {
|
||||||
if crd.Kind == cr {
|
if crd.Kind == cr {
|
||||||
group, version, kind, resource, err = getGVKRFromCR(crd)
|
group, version, _, resource, err = getGVKRFromCR(crd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -912,53 +1047,33 @@ func PushServiceFromKubernetesInlineComponents(client *kclient.Client, k8sCompon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add labels to the CRD before creation
|
|
||||||
d.AddComponentLabelsToCRD(labels)
|
|
||||||
|
|
||||||
crdName, ok := getCRDName(d.OriginalCRD)
|
crdName, ok := getCRDName(d.OriginalCRD)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(deployed, cr+"/"+crdName)
|
u, err := client.GetDynamicResource(group, version, resource, crdName)
|
||||||
|
|
||||||
// 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
|
|
||||||
continue // this ensures that services slice is not updated
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
name, _ := d.GetServiceNameFromCRD() // ignoring error because invalid yaml won't be inserted into devfile through odo
|
|
||||||
created = append(created, strings.Join([]string{kind, name}, "/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range deployed {
|
|
||||||
err = DeleteOperatorService(client, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
deleted = append(deleted, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(created) == 1 {
|
found := false
|
||||||
log.Successf("Created service %q on the cluster; refer %q to know how to link it to the component", created[0], "odo link -h")
|
for _, ownerRef := range u.GetOwnerReferences() {
|
||||||
} else if len(created) > 1 {
|
if ownerRef.UID == ownerReference.UID {
|
||||||
log.Successf("Created services %q on the cluster; refer %q to know how to link them to the component", strings.Join(created, ", "), "odo link -h")
|
found = true
|
||||||
}
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.SetOwnerReferences(append(u.GetOwnerReferences(), ownerReference))
|
||||||
|
|
||||||
if len(deleted) == 1 {
|
err = client.UpdateDynamicResource(group, version, resource, crdName, u)
|
||||||
log.Successf("Deleted service %q from the cluster", deleted[0])
|
if err != nil {
|
||||||
} else if len(deleted) > 1 {
|
return err
|
||||||
log.Successf("Deleted services %q from the cluster", strings.Join(deleted, ", "))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,3 +1088,7 @@ func getCRDName(crd map[string]interface{}) (string, bool) {
|
|||||||
}
|
}
|
||||||
return name, true
|
return name, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isLinkResource(kind string) bool {
|
||||||
|
return kind == "ServiceBinding"
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,6 +149,13 @@ func (k kubernetesClient) ListFromCluster() (StorageList, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// to track volumes created by Service Binding Operator
|
||||||
|
for _, volume := range pod.Spec.Volumes {
|
||||||
|
if volume.Secret != nil {
|
||||||
|
validVolumeMounts[volume.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, volumeMount := range volumeMounts {
|
for _, volumeMount := range volumeMounts {
|
||||||
if _, ok := validVolumeMounts[volumeMount.Name]; !ok {
|
if _, ok := validVolumeMounts[volumeMount.Name]; !ok {
|
||||||
return StorageList{}, fmt.Errorf("pvc not found for mount path %s", volumeMount.Name)
|
return StorageList{}, fmt.Errorf("pvc not found for mount path %s", volumeMount.Name)
|
||||||
|
|||||||
95
tests/examples/source/devfiles/nodejs/devfile-with-link.yaml
Normal file
95
tests/examples/source/devfiles/nodejs/devfile-with-link.yaml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
commands:
|
||||||
|
- exec:
|
||||||
|
commandLine: npm install
|
||||||
|
component: runtime
|
||||||
|
group:
|
||||||
|
isDefault: true
|
||||||
|
kind: build
|
||||||
|
workingDir: /project
|
||||||
|
id: install
|
||||||
|
- exec:
|
||||||
|
commandLine: npm start
|
||||||
|
component: runtime
|
||||||
|
group:
|
||||||
|
isDefault: true
|
||||||
|
kind: run
|
||||||
|
workingDir: /project
|
||||||
|
id: run
|
||||||
|
- exec:
|
||||||
|
commandLine: npm run debug
|
||||||
|
component: runtime
|
||||||
|
group:
|
||||||
|
isDefault: true
|
||||||
|
kind: debug
|
||||||
|
workingDir: /project
|
||||||
|
id: debug
|
||||||
|
- exec:
|
||||||
|
commandLine: npm test
|
||||||
|
component: runtime
|
||||||
|
group:
|
||||||
|
isDefault: true
|
||||||
|
kind: test
|
||||||
|
workingDir: /project
|
||||||
|
id: test
|
||||||
|
components:
|
||||||
|
- container:
|
||||||
|
endpoints:
|
||||||
|
- name: http-3000
|
||||||
|
targetPort: 3000
|
||||||
|
image: registry.access.redhat.com/ubi8/nodejs-14:latest
|
||||||
|
memoryLimit: 1024Mi
|
||||||
|
mountSources: true
|
||||||
|
sourceMapping: /project
|
||||||
|
name: runtime
|
||||||
|
- kubernetes:
|
||||||
|
inlined: |
|
||||||
|
apiVersion: etcd.database.coreos.com/v1beta2
|
||||||
|
kind: EtcdCluster
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
etcd.database.coreos.com/scope: clusterwide
|
||||||
|
name: myetcd
|
||||||
|
spec:
|
||||||
|
size: 1
|
||||||
|
version: 3.2.13
|
||||||
|
name: myetcd
|
||||||
|
- kubernetes:
|
||||||
|
inlined: |
|
||||||
|
apiVersion: binding.operators.coreos.com/v1alpha1
|
||||||
|
kind: ServiceBinding
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: etcd-link
|
||||||
|
spec:
|
||||||
|
application:
|
||||||
|
group: apps
|
||||||
|
name: api-app
|
||||||
|
resource: deployments
|
||||||
|
version: v1
|
||||||
|
bindAsFiles: true
|
||||||
|
detectBindingResources: true
|
||||||
|
services:
|
||||||
|
- group: etcd.database.coreos.com
|
||||||
|
kind: EtcdCluster
|
||||||
|
name: myetcd
|
||||||
|
version: v1beta2
|
||||||
|
status:
|
||||||
|
secret: ""
|
||||||
|
name: etcd-link
|
||||||
|
metadata:
|
||||||
|
description: Stack with Node.js 14
|
||||||
|
displayName: Node.js Runtime
|
||||||
|
language: nodejs
|
||||||
|
name: nodejs
|
||||||
|
projectType: nodejs
|
||||||
|
tags:
|
||||||
|
- NodeJS
|
||||||
|
- Express
|
||||||
|
- ubi8
|
||||||
|
version: 1.0.1
|
||||||
|
schemaVersion: 2.0.0
|
||||||
|
starterProjects:
|
||||||
|
- git:
|
||||||
|
remotes:
|
||||||
|
origin: https://github.com/odo-devfiles/nodejs-ex.git
|
||||||
|
name: nodejs-starter
|
||||||
149
tests/integration/operatorhub/cmd_link_test.go
Normal file
149
tests/integration/operatorhub/cmd_link_test.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/openshift/odo/tests/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("odo link command tests for OperatorHub", func() {
|
||||||
|
|
||||||
|
var commonVar helper.CommonVar
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
commonVar = helper.CommonBeforeEach()
|
||||||
|
helper.Chdir(commonVar.Context)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
helper.CommonAfterEach(commonVar)
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("Operators are installed in the cluster", func() {
|
||||||
|
|
||||||
|
var etcdOperator string
|
||||||
|
var etcdCluster string
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
// wait till odo can see that all operators installed by setup script in the namespace
|
||||||
|
odoArgs := []string{"catalog", "list", "services"}
|
||||||
|
operators := []string{"etcdoperator", "service-binding-operator"}
|
||||||
|
for _, operator := range operators {
|
||||||
|
helper.WaitForCmdOut("odo", odoArgs, 5, true, func(output string) bool {
|
||||||
|
return strings.Contains(output, operator)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
list := helper.Cmd("odo", "catalog", "list", "services").ShouldPass().Out()
|
||||||
|
etcdOperator = regexp.MustCompile(`etcdoperator\.*[a-z][0-9]\.[0-9]\.[0-9]-clusterwide`).FindString(list)
|
||||||
|
etcdCluster = fmt.Sprintf("%s/EtcdCluster", etcdOperator)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
helper.DeleteProject(commonVar.Project)
|
||||||
|
})
|
||||||
|
|
||||||
|
When("a component and a service are deployed", func() {
|
||||||
|
|
||||||
|
var componentName string
|
||||||
|
var svcFullName string
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context)
|
||||||
|
componentName = "cmp-" + helper.RandString(6)
|
||||||
|
helper.Cmd("odo", "create", "nodejs", componentName).ShouldPass()
|
||||||
|
|
||||||
|
serviceName := "service-" + helper.RandString(6)
|
||||||
|
svcFullName = strings.Join([]string{"EtcdCluster", serviceName}, "/")
|
||||||
|
helper.Cmd("odo", "service", "create", etcdCluster, serviceName, "--project", commonVar.Project).ShouldPass()
|
||||||
|
|
||||||
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
|
name := commonVar.CliRunner.GetRunningPodNameByComponent(componentName, commonVar.Project)
|
||||||
|
Expect(name).To(Not(BeEmpty()))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find files in component container", func() {
|
||||||
|
helper.Cmd("odo", "exec", "--", "ls", "/project/server.js").ShouldPass()
|
||||||
|
})
|
||||||
|
|
||||||
|
When("a link between the component and the service is created and deployed", func() {
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
helper.Cmd("odo", "link", svcFullName).ShouldPass()
|
||||||
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
|
name := commonVar.CliRunner.GetRunningPodNameByComponent(componentName, commonVar.Project)
|
||||||
|
Expect(name).To(Not(BeEmpty()))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find files in component container", func() {
|
||||||
|
helper.Cmd("odo", "exec", "--", "ls", "/project/server.js").ShouldPass()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find the link environment variable", func() {
|
||||||
|
stdOut := helper.Cmd("odo", "exec", "--", "sh", "-c", "echo $ETCDCLUSTER_CLUSTERIP").ShouldPass().Out()
|
||||||
|
Expect(stdOut).To(Not(BeEmpty()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("a link with between the component and the service is created with --bind-as-files and deployed", func() {
|
||||||
|
|
||||||
|
var bindingName string
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
bindingName = "sbr-" + helper.RandString(6)
|
||||||
|
helper.Cmd("odo", "link", svcFullName, "--bind-as-files", "--name", bindingName).ShouldPass()
|
||||||
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
|
name := commonVar.CliRunner.GetRunningPodNameByComponent(componentName, commonVar.Project)
|
||||||
|
Expect(name).To(Not(BeEmpty()))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find files in component container", func() {
|
||||||
|
helper.Cmd("odo", "exec", "--", "ls", "/project/server.js").ShouldPass()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find bindings for service", func() {
|
||||||
|
helper.Cmd("odo", "exec", "--", "ls", "/bindings/"+bindingName+"/clusterIP").ShouldPass()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("getting sources, a devfile defining a component, a service and a link, and executing odo push", func() {
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
componentName := "api" // this is the name of the component in the devfile
|
||||||
|
helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context)
|
||||||
|
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-with-link.yaml"), filepath.Join(commonVar.Context, "devfile.yaml"))
|
||||||
|
helper.Cmd("odo", "create", componentName).ShouldPass()
|
||||||
|
|
||||||
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
|
name := commonVar.CliRunner.GetRunningPodNameByComponent(componentName, commonVar.Project)
|
||||||
|
Expect(name).To(Not(BeEmpty()))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find files in component container", func() {
|
||||||
|
helper.Cmd("odo", "exec", "--", "ls", "/project/server.js").ShouldPass()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find bindings for service", func() {
|
||||||
|
helper.Cmd("odo", "exec", "--", "ls", "/bindings/etcd-link/clusterIP").ShouldPass()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should find owner references on link and service", func() {
|
||||||
|
ocArgs := []string{"get", "servicebinding", "etcd-link", "-o", "jsonpath='{.metadata.ownerReferences.*.name}'", "-n", commonVar.Project}
|
||||||
|
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
|
||||||
|
return strings.Contains(output, "api-app")
|
||||||
|
})
|
||||||
|
|
||||||
|
ocArgs = []string{"get", "etcdclusters.etcd.database.coreos.com", "myetcd", "-o", "jsonpath='{.metadata.ownerReferences.*.name}'", "-n", commonVar.Project}
|
||||||
|
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
|
||||||
|
return strings.Contains(output, "api-app")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -456,12 +456,13 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
|||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
linkName = "link-" + helper.RandString(6)
|
linkName = "link-" + helper.RandString(6)
|
||||||
helper.Cmd("odo", "link", "EtcdCluster/"+name, "--name", linkName).ShouldPass()
|
helper.Cmd("odo", "link", "EtcdCluster/"+name, "--name", linkName).ShouldPass()
|
||||||
// for the moment, odo push is not necessary to deploy the link
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
// delete the link
|
// delete the link
|
||||||
helper.Cmd("odo", "unlink", "EtcdCluster/"+name).ShouldPass()
|
helper.Cmd("odo", "unlink", "EtcdCluster/"+name).ShouldPass()
|
||||||
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should create the link with the specified name", func() {
|
It("should create the link with the specified name", func() {
|
||||||
@@ -479,12 +480,13 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
|||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
linkName = "link-" + helper.RandString(6)
|
linkName = "link-" + helper.RandString(6)
|
||||||
helper.Cmd("odo", "link", "EtcdCluster/"+name, "--name", linkName, "--bind-as-files").ShouldPass()
|
helper.Cmd("odo", "link", "EtcdCluster/"+name, "--name", linkName, "--bind-as-files").ShouldPass()
|
||||||
// for the moment, odo push is not necessary to deploy the link
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
// delete the link
|
// delete the link
|
||||||
helper.Cmd("odo", "unlink", "EtcdCluster/"+name).ShouldPass()
|
helper.Cmd("odo", "unlink", "EtcdCluster/"+name).ShouldPass()
|
||||||
|
helper.Cmd("odo", "push").ShouldPass()
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should create a servicebinding resource with bindAsFiles set to true", func() {
|
It("should create a servicebinding resource with bindAsFiles set to true", func() {
|
||||||
@@ -607,6 +609,7 @@ spec:
|
|||||||
It("should link the two components successfully", func() {
|
It("should link the two components successfully", func() {
|
||||||
|
|
||||||
helper.Cmd("odo", "link", cmp1, "--context", context0).ShouldPass()
|
helper.Cmd("odo", "link", cmp1, "--context", context0).ShouldPass()
|
||||||
|
helper.Cmd("odo", "push", "--context", context0).ShouldPass()
|
||||||
|
|
||||||
// check the link exists with the specific name
|
// check the link exists with the specific name
|
||||||
ocArgs := []string{"get", "servicebinding", strings.Join([]string{cmp0, cmp1}, "-"), "-o", "jsonpath='{.status.secret}'", "-n", commonVar.Project}
|
ocArgs := []string{"get", "servicebinding", strings.Join([]string{cmp0, cmp1}, "-"), "-o", "jsonpath='{.status.secret}'", "-n", commonVar.Project}
|
||||||
@@ -614,9 +617,9 @@ spec:
|
|||||||
return strings.Contains(output, strings.Join([]string{cmp0, cmp1}, "-"))
|
return strings.Contains(output, strings.Join([]string{cmp0, cmp1}, "-"))
|
||||||
})
|
})
|
||||||
|
|
||||||
// delete the link
|
// delete the link and undeploy it
|
||||||
helper.Cmd("odo", "unlink", cmp1, "--context", context0).ShouldPass()
|
helper.Cmd("odo", "unlink", cmp1, "--context", context0).ShouldPass()
|
||||||
|
helper.Cmd("odo", "push", "--context", context0).ShouldPass()
|
||||||
commonVar.CliRunner.WaitAndCheckForTerminatingState("servicebinding", commonVar.Project, 1)
|
commonVar.CliRunner.WaitAndCheckForTerminatingState("servicebinding", commonVar.Project, 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user