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))
|
||||
- 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))
|
||||
- `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
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -344,7 +344,7 @@ openshiftci-presubmit-unittests:
|
||||
|
||||
.PHONY: test-operator-hub
|
||||
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
|
||||
test-cmd-devfile-describe:
|
||||
|
||||
@@ -9,11 +9,10 @@ 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"
|
||||
"github.com/openshift/odo/pkg/service"
|
||||
"github.com/openshift/odo/pkg/util"
|
||||
|
||||
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
|
||||
// Once the component has started, it will sync the source code to it.
|
||||
func (a Adapter) Push(parameters common.PushParameters) (err error) {
|
||||
|
||||
a.deployment, err = a.Client.GetKubeClient().GetOneDeployment(a.ComponentName, a.AppName)
|
||||
if err != nil {
|
||||
if _, ok := err.(*kclient.DeploymentNotFoundError); !ok {
|
||||
@@ -160,6 +160,29 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
|
||||
}
|
||||
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)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
ownerReference := generator.GetOwnerReference(a.deployment)
|
||||
// update the owner reference of the PVCs with the deployment
|
||||
for i := range pvcs {
|
||||
if pvcs[i].OwnerReferences != nil || pvcs[i].DeletionTimestamp != nil {
|
||||
continue
|
||||
}
|
||||
err = a.Client.GetKubeClient().UpdateStorageOwnerReference(&pvcs[i], generator.GetOwnerReference(a.deployment))
|
||||
err = a.Client.GetKubeClient().UpdateStorageOwnerReference(&pvcs[i], ownerReference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = service.UpdateKubernetesInlineComponentsOwnerReferences(a.Client.GetKubeClient(), k8sComponents, ownerReference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parameters.EnvSpecificInfo.SetDevfileObj(a.Devfile)
|
||||
err = component.ApplyConfig(&a.Client, config.LocalConfigInfo{}, parameters.EnvSpecificInfo, color.Output, componentExists, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -36,8 +36,7 @@ type ComponentSettings struct {
|
||||
URL *[]localConfigProvider.LocalURL `yaml:"Url,omitempty" json:"url,omitempty"`
|
||||
// AppName is the application name. Application is a virtual concept present in odo used
|
||||
// for grouping of components. A namespace can contain multiple applications
|
||||
AppName string `yaml:"AppName,omitempty" json:"appName,omitempty"`
|
||||
Link *[]EnvInfoLink `yaml:"Link,omitempty" json:"link,omitempty"`
|
||||
AppName string `yaml:"AppName,omitempty" json:"appName,omitempty"`
|
||||
|
||||
// DebugPort controls the port used by the pod to run the debugging agent on
|
||||
DebugPort *int `yaml:"DebugPort,omitempty" json:"debugPort,omitempty"`
|
||||
@@ -88,15 +87,6 @@ type EnvSpecificInfo struct {
|
||||
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 {
|
||||
return JSONEnvInfoRepr{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -216,14 +206,6 @@ func (esi *EnvSpecificInfo) SetConfiguration(parameter string, value interface{}
|
||||
} else {
|
||||
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()
|
||||
@@ -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
|
||||
func (esi *EnvSpecificInfo) GetComponentSettings() ComponentSettings {
|
||||
return esi.componentSettings
|
||||
@@ -431,26 +393,6 @@ func (ei *EnvInfo) SetIsRouteSupported(isRouteSupported bool) {
|
||||
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 (
|
||||
// Name is the name of the setting controlling the component name
|
||||
Name = "Name"
|
||||
@@ -472,10 +414,6 @@ const (
|
||||
Push = "PUSH"
|
||||
// PushDescription is the description of push parameter
|
||||
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 (
|
||||
@@ -485,7 +423,6 @@ var (
|
||||
DebugPort: DebugPortDescription,
|
||||
URL: URLDescription,
|
||||
Push: PushDescription,
|
||||
Link: LinkDescription,
|
||||
}
|
||||
|
||||
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 {
|
||||
if !create {
|
||||
return nil
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"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.
|
||||
func (c *Client) WaitForDeploymentRollout(deploymentName string) (*appsv1.Deployment, error) {
|
||||
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.
|
||||
func (c *Client) ApplyDeployment(deploy appsv1.Deployment) (*appsv1.Deployment, error) {
|
||||
data, err := json.Marshal(deploy)
|
||||
|
||||
klog.V(5).Infoln("Applying Deployment via server-side apply:")
|
||||
klog.V(5).Infoln(resourceAsJson(deploy))
|
||||
|
||||
if err != nil {
|
||||
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)})
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (c *Client) DeleteDeployment(labels map[string]string) error {
|
||||
if labels == nil {
|
||||
@@ -299,6 +373,17 @@ func (c *Client) GetDynamicResource(group, version, resource, name string) (*uns
|
||||
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
|
||||
func (c *Client) DeleteDynamicResource(name, group, version, resource string) error {
|
||||
deploymentRes := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
|
||||
|
||||
@@ -114,8 +114,8 @@ func TestCreateDeployment(t *testing.T) {
|
||||
|
||||
if err == nil {
|
||||
|
||||
if len(fkclientset.Kubernetes.Actions()) != 1 {
|
||||
t.Errorf("expected 1 action in StartDeployment got: %v", fkclientset.Kubernetes.Actions())
|
||||
if len(fkclientset.Kubernetes.Actions()) != 2 {
|
||||
t.Errorf("expected 2 action in StartDeployment got %d: %v", len(fkclientset.Kubernetes.Actions()), fkclientset.Kubernetes.Actions())
|
||||
} else {
|
||||
if createdDeployment.Name != tt.deploymentName {
|
||||
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 len(fkclientset.Kubernetes.Actions()) != 1 {
|
||||
t.Errorf("expected 1 action in UpdateDeployment got: %v", fkclientset.Kubernetes.Actions())
|
||||
if len(fkclientset.Kubernetes.Actions()) != 2 {
|
||||
t.Errorf("expected 2 action in UpdateDeployment got %d: %v", len(fkclientset.Kubernetes.Actions()), fkclientset.Kubernetes.Actions())
|
||||
} else {
|
||||
if updatedDeployment.Name != tt.deploymentName {
|
||||
t.Errorf("deployment name does not match the expected name, expected: %s, got %s", tt.deploymentName, updatedDeployment.Name)
|
||||
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/devfile/library/pkg/devfile/generator"
|
||||
"github.com/openshift/odo/pkg/component"
|
||||
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/log"
|
||||
"github.com/openshift/odo/pkg/occlient"
|
||||
@@ -18,6 +16,7 @@ import (
|
||||
"github.com/openshift/odo/pkg/util"
|
||||
servicebinding "github.com/redhat-developer/service-binding-operator/api/v1alpha1"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -53,8 +52,16 @@ func newCommonLinkOptions() *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
|
||||
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.operationName = name
|
||||
@@ -71,7 +78,11 @@ func (o *commonLinkOptions) complete(name string, cmd *cobra.Command, args []str
|
||||
// and must create s2i context instead
|
||||
o.Context, err = genericclioptions.NewContextCreatingAppIfNeeded(cmd)
|
||||
} else {
|
||||
o.Context, err = genericclioptions.NewDevfileContext(cmd)
|
||||
o.Context, err = genericclioptions.New(genericclioptions.CreateParameters{
|
||||
Cmd: cmd,
|
||||
DevfilePath: component.DevfilePath,
|
||||
ComponentContext: context,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -163,11 +174,6 @@ func (o *commonLinkOptions) run() (err error) {
|
||||
return o.linkOperator()
|
||||
}
|
||||
|
||||
linkType := "Component"
|
||||
if o.isTargetAService {
|
||||
linkType = "Service"
|
||||
}
|
||||
|
||||
var component string
|
||||
if o.Context.EnvSpecificInfo != nil {
|
||||
component = o.EnvSpecificInfo.GetName()
|
||||
@@ -183,9 +189,9 @@ func (o *commonLinkOptions) run() (err error) {
|
||||
|
||||
switch o.operationName {
|
||||
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":
|
||||
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:
|
||||
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}, "-")
|
||||
}
|
||||
|
||||
// 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
|
||||
func (o *commonLinkOptions) completeForOperator() (err error) {
|
||||
serviceBindingSupport, err := o.Client.GetKubeClient().IsServiceBindingSupported()
|
||||
@@ -309,9 +310,6 @@ func (o *commonLinkOptions) completeForOperator() (err error) {
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -323,8 +321,7 @@ func (o *commonLinkOptions) completeForOperator() (err error) {
|
||||
Kind: kclient.ServiceBindingKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: o.getServiceBindingName(componentName),
|
||||
Namespace: o.EnvSpecificInfo.GetNamespace(),
|
||||
Name: o.getServiceBindingName(componentName),
|
||||
},
|
||||
Spec: servicebinding.ServiceBindingSpec{
|
||||
DetectBindingResources: true,
|
||||
@@ -339,13 +336,13 @@ func (o *commonLinkOptions) completeForOperator() (err error) {
|
||||
},
|
||||
},
|
||||
}
|
||||
o.serviceBinding.SetOwnerReferences(append(o.serviceBinding.GetOwnerReferences(), ownerReference))
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateForOperator validates the options when svc is supported
|
||||
func (o *commonLinkOptions) validateForOperator() (err error) {
|
||||
var svcFullName string
|
||||
|
||||
if o.isTargetAService {
|
||||
// let's validate if the service exists
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
o.serviceType = "Service"
|
||||
svcFullName = o.serviceName
|
||||
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)
|
||||
@@ -371,29 +374,12 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
||||
}
|
||||
|
||||
if o.operationName == unlink {
|
||||
serviceBindingName, found := o.EnvSpecificInfo.SearchLinkName(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)
|
||||
_, found, err := svc.FindDevfileServiceBinding(o.EnvSpecificInfo.GetDevfileObj(), o.serviceType, o.serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !serviceBindingExists {
|
||||
// This could have happened if the service binding was deleted outside odo workflow (eg: oc delete sb/<sb-name>)
|
||||
// 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)
|
||||
if !found {
|
||||
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())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -421,7 +407,6 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
||||
Kind: kind,
|
||||
Name: o.serviceName,
|
||||
},
|
||||
Namespace: &o.KClient.Namespace,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@@ -432,7 +417,6 @@ func (o *commonLinkOptions) validateForOperator() (err error) {
|
||||
Kind: "Service",
|
||||
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
|
||||
// and stores the link info in the env
|
||||
func (o *commonLinkOptions) linkOperator() (err error) {
|
||||
// convert service binding request into a map[string]interface{} type so
|
||||
// as to use it with dynamic client
|
||||
serviceBindingMap := make(map[string]interface{})
|
||||
// Convert ServiceBinding -> JSON -> Map -> YAML
|
||||
// JSON conversion step is necessary to inline TypeMeta
|
||||
|
||||
intermediate, err := json.Marshal(o.serviceBinding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceBindingMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(intermediate, &serviceBindingMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this creates a link by creating a service of type
|
||||
// "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})
|
||||
yamlDesc, err := yaml.Marshal(serviceBindingMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetType := "component"
|
||||
if o.isTargetAService {
|
||||
targetType = "service"
|
||||
_, found, err := svc.FindDevfileServiceBinding(o.EnvSpecificInfo.GetDevfileObj(), o.serviceType, o.serviceName)
|
||||
if err != nil {
|
||||
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`")
|
||||
return err
|
||||
}
|
||||
|
||||
// unlinkOperator deletes the service binding resource from the cluster
|
||||
// and deletes the link info from the env
|
||||
// unlinkOperator deletes the service binding resource from the devfile
|
||||
func (o *commonLinkOptions) unlinkOperator() (err error) {
|
||||
serviceBindingName, found := o.EnvSpecificInfo.SearchLinkName(o.serviceType, o.serviceName)
|
||||
if !found {
|
||||
return fmt.Errorf("failed to unlink the service %q of type %q since no link found in env file", o.serviceName, o.serviceType)
|
||||
}
|
||||
svcFullName := getSvcFullName(kclient.ServiceBindingKind, serviceBindingName)
|
||||
err = svc.DeleteServiceBindingRequest(o.KClient, svcFullName)
|
||||
|
||||
// We already tested `found` in `validateForOperator`
|
||||
name, _, err := svc.FindDevfileServiceBinding(o.EnvSpecificInfo.GetDevfileObj(), o.serviceType, o.serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = o.Context.EnvSpecificInfo.DeleteLink(serviceBindingName)
|
||||
err = svc.DeleteKubernetesComponentFromDevfile(name, o.EnvSpecificInfo.GetDevfileObj())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetType := "component"
|
||||
if o.isTargetAService {
|
||||
targetType = "service"
|
||||
}
|
||||
|
||||
log.Successf("Successfully unlinked component %q from %s %q\n", o.Context.EnvSpecificInfo.GetName(), targetType, o.suppliedName)
|
||||
log.Successf("Successfully unlinked component %q from %s %q\n", o.Context.EnvSpecificInfo.GetName(), o.getLinkType(), o.suppliedName)
|
||||
log.Italic("To apply the changes, please use `odo push`")
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -234,12 +234,6 @@ func (do *DeleteOptions) DevFileRun() (err error) {
|
||||
if err != nil {
|
||||
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 {
|
||||
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.csvSupport, _ = svc.IsCSVSupported()
|
||||
|
||||
err = o.complete(name, cmd, args)
|
||||
err = o.complete(name, cmd, args, o.componentContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func NewUnlinkOptions() *UnlinkOptions {
|
||||
// Complete completes UnlinkOptions after they've been created
|
||||
func (o *UnlinkOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
|
||||
o.commonLinkOptions.csvSupport, _ = svc.IsCSVSupported()
|
||||
err = o.complete(name, cmd, args)
|
||||
err = o.complete(name, cmd, args, o.componentContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/devfile/library/pkg/devfile/parser"
|
||||
servicebinding "github.com/redhat-developer/service-binding-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
const provisionedAndBoundStatus = "ProvisionedAndBound"
|
||||
@@ -148,12 +149,6 @@ 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
|
||||
@@ -735,6 +730,44 @@ func ListDevfileServices(devfileObj parser.DevfileObj) ([]string, error) {
|
||||
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
|
||||
func AddKubernetesComponentToDevfile(crd, name string, devfileObj parser.DevfileObj) error {
|
||||
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
|
||||
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
|
||||
csvSupported, err := IsCSVSupported()
|
||||
if err != nil || !csvSupported {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
created := []string{}
|
||||
deleted := []string{}
|
||||
type DeployedInfo struct {
|
||||
DoesDeleteRestartsComponent bool
|
||||
Kind string
|
||||
Name string
|
||||
}
|
||||
|
||||
deployed := map[string]struct{}{}
|
||||
deployed := map[string]DeployedInfo{}
|
||||
|
||||
deployedServices, _, err := ListOperatorServices(client)
|
||||
if err != nil && err != kclient.ErrNoSuchOperator {
|
||||
// We ignore ErrNoSuchOperator error as we can deduce Operator Services are not installed
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
for _, svc := range deployedServices {
|
||||
name := svc.GetName()
|
||||
kind := svc.GetKind()
|
||||
deployedLabels := svc.GetLabels()
|
||||
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 {
|
||||
// get the string representation of the YAML definition of a CRD
|
||||
strCRD := c.Kubernetes.Inlined
|
||||
@@ -901,10 +1036,10 @@ func PushServiceFromKubernetesInlineComponents(client *kclient.Client, k8sCompon
|
||||
return err
|
||||
}
|
||||
|
||||
var group, version, kind, resource string
|
||||
var group, version, resource string
|
||||
for _, crd := range csv.Spec.CustomResourceDefinitions.Owned {
|
||||
if crd.Kind == cr {
|
||||
group, version, kind, resource, err = getGVKRFromCR(crd)
|
||||
group, version, _, resource, err = getGVKRFromCR(crd)
|
||||
if err != nil {
|
||||
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)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
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 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)
|
||||
u, err := client.GetDynamicResource(group, version, resource, crdName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
}
|
||||
deleted = append(deleted, key)
|
||||
}
|
||||
|
||||
if len(created) == 1 {
|
||||
log.Successf("Created service %q on the cluster; refer %q to know how to link it to the component", created[0], "odo link -h")
|
||||
} else if len(created) > 1 {
|
||||
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 := false
|
||||
for _, ownerRef := range u.GetOwnerReferences() {
|
||||
if ownerRef.UID == ownerReference.UID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
u.SetOwnerReferences(append(u.GetOwnerReferences(), ownerReference))
|
||||
|
||||
if len(deleted) == 1 {
|
||||
log.Successf("Deleted service %q from the cluster", deleted[0])
|
||||
} else if len(deleted) > 1 {
|
||||
log.Successf("Deleted services %q from the cluster", strings.Join(deleted, ", "))
|
||||
err = client.UpdateDynamicResource(group, version, resource, crdName, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -973,3 +1088,7 @@ func getCRDName(crd map[string]interface{}) (string, bool) {
|
||||
}
|
||||
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 {
|
||||
if _, ok := validVolumeMounts[volumeMount.Name]; !ok {
|
||||
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() {
|
||||
linkName = "link-" + helper.RandString(6)
|
||||
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() {
|
||||
// delete the link
|
||||
helper.Cmd("odo", "unlink", "EtcdCluster/"+name).ShouldPass()
|
||||
helper.Cmd("odo", "push").ShouldPass()
|
||||
})
|
||||
|
||||
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() {
|
||||
linkName = "link-" + helper.RandString(6)
|
||||
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() {
|
||||
// delete the link
|
||||
helper.Cmd("odo", "unlink", "EtcdCluster/"+name).ShouldPass()
|
||||
helper.Cmd("odo", "push").ShouldPass()
|
||||
})
|
||||
|
||||
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() {
|
||||
|
||||
helper.Cmd("odo", "link", cmp1, "--context", context0).ShouldPass()
|
||||
helper.Cmd("odo", "push", "--context", context0).ShouldPass()
|
||||
|
||||
// check the link exists with the specific name
|
||||
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}, "-"))
|
||||
})
|
||||
|
||||
// delete the link
|
||||
// delete the link and undeploy it
|
||||
helper.Cmd("odo", "unlink", cmp1, "--context", context0).ShouldPass()
|
||||
|
||||
helper.Cmd("odo", "push", "--context", context0).ShouldPass()
|
||||
commonVar.CliRunner.WaitAndCheckForTerminatingState("servicebinding", commonVar.Project, 1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user