From ba7f1a1cfcef6a9218178327d5415b0380665887 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 3 Jun 2022 14:46:44 +0200 Subject: [PATCH] odo describe binding (#5773) * List Servicebindings from SBO only * Support both implementations * Integration tests (TBC) * Human readable output * Support --name flag * Reference doc * fix * self review * Check for InjectionReady condition before to get bindings * Fix comments * Remove unnecessary error returned value * Display info when status is unknown * Sections in doc * Review --- .../command-reference/describe-binding.md | 107 ++++++++ .../{describe.md => describe-component.md} | 2 +- .../command-reference/json-output.md | 109 ++++++++ pkg/api/binding.go | 63 +++++ pkg/binding/binding.go | 191 ++++++++++++- pkg/binding/interface.go | 7 + pkg/binding/mock.go | 31 +++ pkg/kclient/binding.go | 83 ++++-- pkg/kclient/interface.go | 13 +- pkg/kclient/mock_Client.go | 33 ++- pkg/kclient/operators.go | 4 +- pkg/odo/cli/describe/binding.go | 177 ++++++++++++ pkg/odo/cli/describe/describe.go | 3 +- .../devfile-with-service-binding-envvars.yaml | 88 ++++++ .../devfile-with-service-binding-files.yaml | 88 ++++++ ...ile-with-spec-service-binding-envvars.yaml | 85 ++++++ .../devfile-with-spec-service-binding.yaml | 82 ++++++ .../devfile/cmd_describe_binding_test.go | 255 ++++++++++++++++++ ...test.go => cmd_describe_component_test.go} | 2 +- 19 files changed, 1387 insertions(+), 36 deletions(-) create mode 100644 docs/website/versioned_docs/version-3.0.0/command-reference/describe-binding.md rename docs/website/versioned_docs/version-3.0.0/command-reference/{describe.md => describe-component.md} (97%) create mode 100644 pkg/api/binding.go create mode 100644 pkg/odo/cli/describe/binding.go create mode 100644 tests/examples/source/devfiles/nodejs/devfile-with-service-binding-envvars.yaml create mode 100644 tests/examples/source/devfiles/nodejs/devfile-with-service-binding-files.yaml create mode 100644 tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding-envvars.yaml create mode 100644 tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding.yaml create mode 100644 tests/integration/devfile/cmd_describe_binding_test.go rename tests/integration/devfile/{cmd_describe_test.go => cmd_describe_component_test.go} (99%) diff --git a/docs/website/versioned_docs/version-3.0.0/command-reference/describe-binding.md b/docs/website/versioned_docs/version-3.0.0/command-reference/describe-binding.md new file mode 100644 index 00000000000..833398dbeae --- /dev/null +++ b/docs/website/versioned_docs/version-3.0.0/command-reference/describe-binding.md @@ -0,0 +1,107 @@ +--- +title: odo describe binding +--- + +## Description + +`odo describe binding` command is useful for getting information about service bindings. + +This command supports the service bindings added with the command `odo add binding` and bindings added manually to the Devfile, using a `ServiceBinding` resource from one of these apiVersion: +- `binding.operators.coreos.com/v1alpha1` +- `servicebinding.io/v1alpha3` + +## Running the Command + +There are 2 ways to describe a service binding: +- [Describe with access to Devfile](#describe-with-access-to-devfile) +- [Describe without access to Devfile](#describe-without-access-to-devfile) + +### Describe with access to Devfile + +This command returns information extracted from the Devfile and, if possible, from the cluster. + +The command lists the Kubernetes resources declared in the Devfile as a Kubernetes component, +with the kind `ServiceBinding` and one of these apiVersion: +- `binding.operators.coreos.com/v1alpha1` +- `servicebinding.io/v1alpha3` + +For each of these resources, the following information is displayed: +- the resource name, +- the list of the services to which the component is bound using this service binding, +- if the variables are bound as files or as environment variables, +- if the binding information is auto-detected. + +When the service binding are not deployed yet to the cluster: + +```shell +$ odo describe binding +ServiceBinding used by the current component: + +Service Binding Name: my-nodejs-app-cluster-sample +Services: + • cluster-sample (Cluster.postgresql.k8s.enterprisedb.io) +Bind as files: false +Detect binding resources: true +Available binding information: unknown + +Service Binding Name: my-nodejs-app-redis-standalone +Services: + • redis-standalone (Redis.redis.redis.opstreelabs.in) +Bind as files: false +Detect binding resources: true +Available binding information: unknown + +Binding information for one or more ServiceBinding is not available because they don't exist on the cluster yet. +Start "odo dev" first to see binding information. +``` + +When the resources have been deployed to the cluster, the command also extracts information from the status of the resources to display information about the variables that can be used from the component. + +```shell +$ odo describe binding +ServiceBinding used by the current component: + +Service Binding Name: my-nodejs-app-cluster-sample-2 +Services: + • cluster-sample-2 (Cluster.postgresql.k8s.enterprisedb.io) +Bind as files: false +Detect binding resources: true +Available binding information: + • CLUSTER_PASSWORD + • CLUSTER_PROVIDER + • CLUSTER_TLS.CRT + • CLUSTER_TLS.KEY + • CLUSTER_USERNAME + • CLUSTER_CA.KEY + • CLUSTER_CLUSTERIP + • CLUSTER_HOST + • CLUSTER_PGPASS + • CLUSTER_TYPE + • CLUSTER_CA.CRT + • CLUSTER_DATABASE + +Service Binding Name: my-nodejs-app-redis-standalone +Services: + • redis-standalone (Redis.redis.redis.opstreelabs.in) +Bind as files: false +Detect binding resources: true +Available binding information: + • REDIS_CLUSTERIP + • REDIS_HOST + • REDIS_PASSWORD + • REDIS_TYPE +``` + +### Describe without access to Devfile + +```shell +odo describe binding --name +``` + +The command extracts information from the cluster. + +The command searches for a resource in the current namespace with the given name, the kind `ServiceBinding` and one of these apiVersion: +- `binding.operators.coreos.com/v1alpha1` +- `servicebinding.io/v1alpha3` + +If a resource is found, it displays information about the service binding and the variables that can be used from the component. diff --git a/docs/website/versioned_docs/version-3.0.0/command-reference/describe.md b/docs/website/versioned_docs/version-3.0.0/command-reference/describe-component.md similarity index 97% rename from docs/website/versioned_docs/version-3.0.0/command-reference/describe.md rename to docs/website/versioned_docs/version-3.0.0/command-reference/describe-component.md index 1846e5c0133..d3f4d6aa4b0 100644 --- a/docs/website/versioned_docs/version-3.0.0/command-reference/describe.md +++ b/docs/website/versioned_docs/version-3.0.0/command-reference/describe-component.md @@ -1,5 +1,5 @@ --- -title: odo describe +title: odo describe component --- `odo describe component` command is useful for getting information about a component managed by `odo`. diff --git a/docs/website/versioned_docs/version-3.0.0/command-reference/json-output.md b/docs/website/versioned_docs/version-3.0.0/command-reference/json-output.md index d57abcdf60b..800df9f122a 100644 --- a/docs/website/versioned_docs/version-3.0.0/command-reference/json-output.md +++ b/docs/website/versioned_docs/version-3.0.0/command-reference/json-output.md @@ -255,4 +255,113 @@ $ odo registry --details -o json }, }, [...] ] +``` + +## odo describe binding -o json + +The `odo describe binding` command lists all the service binding resources declared in the devfile +and, if the resource is deployed to the cluster, also displays the variables that can be used from +the component. + +If a name is given, the command does not extract information from the Devfile, but instead extracts +information from the deployed resource with the given name. + +Without a name, the output of the command is a list of service binding details, for example: + +```shell +$ odo describe binding -o json +[ + { + "name": "my-first-binding", + "spec": { + "services": [ + { + "apiVersion": "postgresql.k8s.enterprisedb.io/v1", + "kind": "Cluster", + "name": "cluster-sample" + } + ], + "detectBindingResources": false, + "bindAsFiles": true + }, + "status": { + "bindingsFiles": [ + "${SERVICE_BINDING_ROOT}/my-first-binding/host", + "${SERVICE_BINDING_ROOT}/my-first-binding/password", + "${SERVICE_BINDING_ROOT}/my-first-binding/pgpass", + "${SERVICE_BINDING_ROOT}/my-first-binding/provider", + "${SERVICE_BINDING_ROOT}/my-first-binding/type", + "${SERVICE_BINDING_ROOT}/my-first-binding/username", + "${SERVICE_BINDING_ROOT}/my-first-binding/database" + ], + "bindingEnvVars": [ + "PASSWD" + ] + } + }, + { + "name": "my-second-binding", + "spec": { + "services": [ + { + "apiVersion": "postgresql.k8s.enterprisedb.io/v1", + "kind": "Cluster", + "name": "cluster-sample-2" + } + ], + "detectBindingResources": true, + "bindAsFiles": true + }, + "status": { + "bindingsFiles": [ + "${SERVICE_BINDING_ROOT}/my-second-binding/ca.crt", + "${SERVICE_BINDING_ROOT}/my-second-binding/clusterIP", + "${SERVICE_BINDING_ROOT}/my-second-binding/database", + "${SERVICE_BINDING_ROOT}/my-second-binding/host", + "${SERVICE_BINDING_ROOT}/my-second-binding/ca.key", + "${SERVICE_BINDING_ROOT}/my-second-binding/password", + "${SERVICE_BINDING_ROOT}/my-second-binding/pgpass", + "${SERVICE_BINDING_ROOT}/my-second-binding/provider", + "${SERVICE_BINDING_ROOT}/my-second-binding/tls.crt", + "${SERVICE_BINDING_ROOT}/my-second-binding/tls.key", + "${SERVICE_BINDING_ROOT}/my-second-binding/type", + "${SERVICE_BINDING_ROOT}/my-second-binding/username" + ] + } + } +] +``` + +When specifying a name, the output is a unique service binding: +```shell +$ odo describe binding --name my-first-binding -o json +{ + "name": "my-first-binding", + "spec": { + "services": [ + { + "apiVersion": "postgresql.k8s.enterprisedb.io/v1", + "kind": "Cluster", + "name": "cluster-sample" + } + ], + "detectBindingResources": false, + "bindAsFiles": true + }, + "status": { + "bindingsFiles": [ + "${SERVICE_BINDING_ROOT}/my-first-binding/host", + "${SERVICE_BINDING_ROOT}/my-first-binding/password", + "${SERVICE_BINDING_ROOT}/my-first-binding/pgpass", + "${SERVICE_BINDING_ROOT}/my-first-binding/provider", + "${SERVICE_BINDING_ROOT}/my-first-binding/type", + "${SERVICE_BINDING_ROOT}/my-first-binding/username", + "${SERVICE_BINDING_ROOT}/my-first-binding/database" + ], + "bindingEnvVars": [ + "PASSWD" + ] + } +} +``` diff --git a/pkg/api/binding.go b/pkg/api/binding.go new file mode 100644 index 00000000000..48fda1a3a0b --- /dev/null +++ b/pkg/api/binding.go @@ -0,0 +1,63 @@ +package api + +import ( + bindingApi "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" + specApi "github.com/redhat-developer/service-binding-operator/apis/spec/v1alpha3" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ServiceBinding describes a service binding, from group binding.operators.coreos.com/v1alpha1 or servicebinding.io/v1alpha3 +type ServiceBinding struct { + Name string `json:"name"` + Spec ServiceBindingSpec `json:"spec"` + Status *ServiceBindingStatus `json:"status,omitempty"` +} + +type ServiceBindingSpec struct { + Services []specApi.ServiceBindingServiceReference `json:"services"` + DetectBindingResources bool `json:"detectBindingResources"` + BindAsFiles bool `json:"bindAsFiles"` +} + +type ServiceBindingStatus struct { + BindingFiles []string `json:"bindingsFiles,omitempty"` + BindingEnvVars []string `json:"bindingEnvVars,omitempty"` +} + +// ServiceBindingFromBinding returns a common api.ServiceBinding structure +// from a ServiceBinding.binding.operators.coreos.com/v1alpha1 +func ServiceBindingFromBinding(binding bindingApi.ServiceBinding) ServiceBinding { + + var dstSvcs []specApi.ServiceBindingServiceReference + for _, srcSvc := range binding.Spec.Services { + dstSvc := specApi.ServiceBindingServiceReference{ + Name: srcSvc.Name, + } + dstSvc.APIVersion, dstSvc.Kind = schema.GroupVersion{ + Group: srcSvc.Group, + Version: srcSvc.Version, + }.WithKind(srcSvc.Kind).ToAPIVersionAndKind() + dstSvcs = append(dstSvcs, dstSvc) + } + return ServiceBinding{ + Name: binding.Name, + Spec: ServiceBindingSpec{ + Services: dstSvcs, + DetectBindingResources: binding.Spec.DetectBindingResources, + BindAsFiles: binding.Spec.BindAsFiles, + }, + } +} + +// ServiceBindingFromSpec returns a common api.ServiceBinding structure +// from a ServiceBinding.servicebinding.io/v1alpha3 +func ServiceBindingFromSpec(spec specApi.ServiceBinding) ServiceBinding { + return ServiceBinding{ + Name: spec.Name, + Spec: ServiceBindingSpec{ + Services: []specApi.ServiceBindingServiceReference{spec.Spec.Service}, + DetectBindingResources: false, + BindAsFiles: true, + }, + } +} diff --git a/pkg/binding/binding.go b/pkg/binding/binding.go index 5e24c60aab5..5b8dcea4eab 100644 --- a/pkg/binding/binding.go +++ b/pkg/binding/binding.go @@ -2,12 +2,23 @@ package binding import ( "fmt" + "path/filepath" + + bindingApis "github.com/redhat-developer/service-binding-operator/apis" + bindingApi "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" + specApi "github.com/redhat-developer/service-binding-operator/apis/spec/v1alpha3" - "github.com/devfile/library/pkg/devfile/parser" - sboApi "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" "gopkg.in/yaml.v2" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + devfilev1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser" + parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + devfilefs "github.com/devfile/library/pkg/testingutil/filesystem" + + "github.com/redhat-developer/odo/pkg/api" "github.com/redhat-developer/odo/pkg/binding/asker" backendpkg "github.com/redhat-developer/odo/pkg/binding/backend" "github.com/redhat-developer/odo/pkg/kclient" @@ -99,7 +110,7 @@ func (o *BindingClient) AddBinding(bindingName string, bindAsFiles bool, unstruc return obj, err } - serviceBinding := kclient.NewServiceBindingObject(bindingName, bindAsFiles, deploymentName, deploymentGVR, []sboApi.Mapping{}, []sboApi.Service{service}) + serviceBinding := kclient.NewServiceBindingObject(bindingName, bindAsFiles, deploymentName, deploymentGVR, []bindingApi.Mapping{}, []bindingApi.Service{service}) // Note: we cannot directly marshal the serviceBinding object to yaml because it doesn't do that in the correct k8s manifest format serviceBindingUnstructured, err := kclient.ConvertK8sResourceToUnstructured(serviceBinding) @@ -146,3 +157,177 @@ func (o *BindingClient) GetServiceInstances() (map[string]unstructured.Unstructu return bindableObjectMap, nil } + +// GetBindingsFromDevfile returns all ServiceBinding resources declared as Kubernertes component from a Devfile +// from group binding.operators.coreos.com/v1alpha1 or servicebinding.io/v1alpha3 +func (o *BindingClient) GetBindingsFromDevfile(devfileObj parser.DevfileObj, context string) ([]api.ServiceBinding, error) { + result := []api.ServiceBinding{} + kubeComponents, err := devfileObj.Data.GetComponents(parsercommon.DevfileOptions{ + ComponentOptions: parsercommon.ComponentOptions{ + ComponentType: devfilev1alpha2.KubernetesComponentType, + }, + }) + if err != nil { + return nil, err + } + + for _, component := range kubeComponents { + strCRD, err := libdevfile.GetK8sManifestWithVariablesSubstituted(devfileObj, component.Name, context, devfilefs.DefaultFs{}) + if err != nil { + return nil, err + } + + u := unstructured.Unstructured{} + if err := yaml.Unmarshal([]byte(strCRD), &u.Object); err != nil { + return nil, err + } + + switch u.GetObjectKind().GroupVersionKind() { + case bindingApi.GroupVersionKind: + + var sbo bindingApi.ServiceBinding + err := o.kubernetesClient.ConvertUnstructuredToResource(u, &sbo) + if err != nil { + return nil, err + } + + sb := api.ServiceBindingFromBinding(sbo) + sb.Status, err = o.getStatusFromBinding(sb.Name) + if err != nil { + return nil, err + } + + result = append(result, sb) + + case specApi.GroupVersion.WithKind("ServiceBinding"): + + var sbc specApi.ServiceBinding + err := o.kubernetesClient.ConvertUnstructuredToResource(u, &sbc) + if err != nil { + return nil, err + } + + sb := api.ServiceBindingFromSpec(sbc) + sb.Status, err = o.getStatusFromSpec(sb.Name) + if err != nil { + return nil, err + } + + result = append(result, sb) + + } + } + return result, nil +} + +// GetBindingFromCluster returns the ServiceBinding resource with the given name +// from the cluster, from group binding.operators.coreos.com/v1alpha1 or servicebinding.io/v1alpha3 +func (o *BindingClient) GetBindingFromCluster(name string) (api.ServiceBinding, error) { + + bindingSB, err := o.kubernetesClient.GetBindingServiceBinding(name) + if err == nil { + sb := api.ServiceBindingFromBinding(bindingSB) + sb.Status, err = o.getStatusFromBinding(bindingSB.Name) + if err != nil { + return api.ServiceBinding{}, err + } + return sb, nil + } + if err != nil && !kerrors.IsNotFound(err) { + return api.ServiceBinding{}, err + } + + specSB, err := o.kubernetesClient.GetSpecServiceBinding(name) + if err == nil { + sb := api.ServiceBindingFromSpec(specSB) + sb.Status, err = o.getStatusFromSpec(specSB.Name) + if err != nil { + return api.ServiceBinding{}, err + } + return sb, nil + } + + // In case of notFound error, this time we return the error + if kerrors.IsNotFound(err) { + return api.ServiceBinding{}, fmt.Errorf("ServiceBinding %q not found", name) + } + return api.ServiceBinding{}, err +} + +// getStatusFromBinding returns status information from a ServiceBinding in the cluster +// from group binding.operators.coreos.com/v1alpha1 +func (o *BindingClient) getStatusFromBinding(name string) (*api.ServiceBindingStatus, error) { + bindingSB, err := o.kubernetesClient.GetBindingServiceBinding(name) + if err != nil { + if kerrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + + if injected := meta.IsStatusConditionTrue(bindingSB.Status.Conditions, bindingApis.InjectionReady); !injected { + return nil, nil + } + + secretName := bindingSB.Status.Secret + secret, err := o.kubernetesClient.GetSecret(secretName, o.kubernetesClient.GetCurrentNamespace()) + if err != nil { + return nil, err + } + + bindings := make([]string, 0, len(secret.Data)) + if bindingSB.Spec.BindAsFiles { + for k := range secret.Data { + bindingName := filepath.ToSlash(filepath.Join("${SERVICE_BINDING_ROOT}", name, k)) + bindings = append(bindings, bindingName) + } + return &api.ServiceBindingStatus{ + BindingFiles: bindings, + }, nil + } + + for k := range secret.Data { + bindings = append(bindings, k) + } + return &api.ServiceBindingStatus{ + BindingEnvVars: bindings, + }, nil +} + +// getStatusFromSpec returns status information from a ServiceBinding in the cluster +// from group servicebinding.io/v1alpha3 +func (o *BindingClient) getStatusFromSpec(name string) (*api.ServiceBindingStatus, error) { + specSB, err := o.kubernetesClient.GetSpecServiceBinding(name) + if err != nil { + if kerrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + + if injected := meta.IsStatusConditionTrue(specSB.Status.Conditions, bindingApis.InjectionReady); !injected { + return nil, nil + } + + if specSB.Status.Binding == nil { + return nil, nil + } + secretName := specSB.Status.Binding.Name + secret, err := o.kubernetesClient.GetSecret(secretName, o.kubernetesClient.GetCurrentNamespace()) + if err != nil { + return nil, err + } + bindingFiles := make([]string, 0, len(secret.Data)) + bindingEnvVars := make([]string, 0, len(specSB.Spec.Env)) + for k := range secret.Data { + bindingName := filepath.ToSlash(filepath.Join("${SERVICE_BINDING_ROOT}", name, k)) + bindingFiles = append(bindingFiles, bindingName) + } + for _, env := range specSB.Spec.Env { + bindingEnvVars = append(bindingEnvVars, env.Name) + } + return &api.ServiceBindingStatus{ + BindingFiles: bindingFiles, + BindingEnvVars: bindingEnvVars, + }, nil +} diff --git a/pkg/binding/interface.go b/pkg/binding/interface.go index a7c099ea866..7be5d551e9b 100644 --- a/pkg/binding/interface.go +++ b/pkg/binding/interface.go @@ -2,6 +2,7 @@ package binding import ( "github.com/devfile/library/pkg/devfile/parser" + "github.com/redhat-developer/odo/pkg/api" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -21,4 +22,10 @@ type Client interface { AddBinding(bindingName string, bindAsFiles bool, unstructuredService unstructured.Unstructured, obj parser.DevfileObj, componentContext string) (parser.DevfileObj, error) // GetServiceInstances returns a map of bindable instance name with its unstructured.Unstructured object, and an error GetServiceInstances() (map[string]unstructured.Unstructured, error) + + // GetBindingsFromDevfile returns the bindings defined in the devfile with the status extracted from cluster + GetBindingsFromDevfile(devfileObj parser.DevfileObj, context string) ([]api.ServiceBinding, error) + + // GetBindingFromCluster returns information about a binding in the cluster (either from group binding.operators.coreos.com or servicebinding.io) + GetBindingFromCluster(name string) (api.ServiceBinding, error) } diff --git a/pkg/binding/mock.go b/pkg/binding/mock.go index c89d0cba24d..a07feeb5537 100644 --- a/pkg/binding/mock.go +++ b/pkg/binding/mock.go @@ -9,6 +9,7 @@ import ( parser "github.com/devfile/library/pkg/devfile/parser" gomock "github.com/golang/mock/gomock" + api "github.com/redhat-developer/odo/pkg/api" unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -80,6 +81,36 @@ func (mr *MockClientMockRecorder) AskBindingName(serviceName, componentName, fla return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AskBindingName", reflect.TypeOf((*MockClient)(nil).AskBindingName), serviceName, componentName, flags) } +// GetBinding mocks base method. +func (m *MockClient) GetBinding(name string) (api.ServiceBinding, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBinding", name) + ret0, _ := ret[0].(api.ServiceBinding) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBinding indicates an expected call of GetBinding. +func (mr *MockClientMockRecorder) GetBinding(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBinding", reflect.TypeOf((*MockClient)(nil).GetBinding), name) +} + +// GetBindingsFromDevfile mocks base method. +func (m *MockClient) GetBindingsFromDevfile(devfileObj parser.DevfileObj, context string) ([]api.ServiceBinding, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBindingsFromDevfile", devfileObj, context) + ret0, _ := ret[0].([]api.ServiceBinding) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBindingsFromDevfile indicates an expected call of GetBindingsFromDevfile. +func (mr *MockClientMockRecorder) GetBindingsFromDevfile(devfileObj, context interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBindingsFromDevfile", reflect.TypeOf((*MockClient)(nil).GetBindingsFromDevfile), devfileObj, context) +} + // GetFlags mocks base method. func (m *MockClient) GetFlags(flags map[string]string) map[string]string { m.ctrl.T.Helper() diff --git a/pkg/kclient/binding.go b/pkg/kclient/binding.go index 3939363941d..74579bb5ed0 100644 --- a/pkg/kclient/binding.go +++ b/pkg/kclient/binding.go @@ -4,7 +4,8 @@ import ( "context" "errors" - sboApi "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" + bindingApi "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" + specApi "github.com/redhat-developer/service-binding-operator/apis/spec/v1alpha3" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,24 +20,24 @@ const ( // IsServiceBindingSupported checks if resource of type service binding request present on the cluster func (c *Client) IsServiceBindingSupported() (bool, error) { - gvr := sboApi.GroupVersionResource + gvr := bindingApi.GroupVersionResource return c.IsResourceSupported(gvr.Group, gvr.Version, gvr.Resource) } // GetBindableKinds returns BindableKinds of name "bindable-kinds". // "bindable-kinds" is the default resource provided by SBO -func (c *Client) GetBindableKinds() (sboApi.BindableKinds, error) { +func (c *Client) GetBindableKinds() (bindingApi.BindableKinds, error) { if c.DynamicClient == nil { - return sboApi.BindableKinds{}, nil + return bindingApi.BindableKinds{}, nil } var ( unstructuredBK *unstructured.Unstructured - bindableKind sboApi.BindableKinds + bindableKind bindingApi.BindableKinds err error ) - unstructuredBK, err = c.DynamicClient.Resource(sboApi.GroupVersion.WithResource(BindableKindsResource)).Get(context.TODO(), "bindable-kinds", v1.GetOptions{}) + unstructuredBK, err = c.DynamicClient.Resource(bindingApi.GroupVersion.WithResource(BindableKindsResource)).Get(context.TODO(), "bindable-kinds", v1.GetOptions{}) if err != nil { if kerrors.IsNotFound(err) { //revive:disable:error-strings This is a top-level error message displayed as is to the end user @@ -46,7 +47,7 @@ func (c *Client) GetBindableKinds() (sboApi.BindableKinds, error) { return bindableKind, err } - err = c.ConvertUnstructuredToResource(unstructuredBK.UnstructuredContent(), &bindableKind) + err = c.ConvertUnstructuredToResource(*unstructuredBK, &bindableKind) if err != nil { return bindableKind, err } @@ -54,7 +55,7 @@ func (c *Client) GetBindableKinds() (sboApi.BindableKinds, error) { } // GetBindableKindStatusRestMapping retuns a list of *meta.RESTMapping of all the bindable kind operator CRD -func (c Client) GetBindableKindStatusRestMapping(bindableKindStatuses []sboApi.BindableKindsStatus) ([]*meta.RESTMapping, error) { +func (c Client) GetBindableKindStatusRestMapping(bindableKindStatuses []bindingApi.BindableKindsStatus) ([]*meta.RESTMapping, error) { var result []*meta.RESTMapping for _, bks := range bindableKindStatuses { if mappingContainsBKS(result, bks) { @@ -73,17 +74,17 @@ func (c Client) GetBindableKindStatusRestMapping(bindableKindStatuses []sboApi.B return result, nil } -// NewServiceBindingServiceObject returns the sboApi.Service object based on the RESTMapping -func (c *Client) NewServiceBindingServiceObject(unstructuredService unstructured.Unstructured, bindingName string) (sboApi.Service, error) { +// NewServiceBindingServiceObject returns the bindingApi.Service object based on the RESTMapping +func (c *Client) NewServiceBindingServiceObject(unstructuredService unstructured.Unstructured, bindingName string) (bindingApi.Service, error) { serviceRESTMapping, err := c.GetRestMappingFromUnstructured(unstructuredService) if err != nil { - return sboApi.Service{}, err + return bindingApi.Service{}, err } - return sboApi.Service{ + return bindingApi.Service{ Id: &bindingName, // Id field is helpful if user wants to inject mappings (custom binding data) - NamespacedRef: sboApi.NamespacedRef{ - Ref: sboApi.Ref{ + NamespacedRef: bindingApi.NamespacedRef{ + Ref: bindingApi.Ref{ Group: serviceRESTMapping.GroupVersionKind.Group, Version: serviceRESTMapping.GroupVersionKind.Version, Kind: serviceRESTMapping.GroupVersionKind.Kind, @@ -94,21 +95,21 @@ func (c *Client) NewServiceBindingServiceObject(unstructuredService unstructured }, nil } -// NewServiceBindingObject returns the sboApi.ServiceBinding object -func NewServiceBindingObject(bindingName string, bindAsFiles bool, deploymentName string, deploymentGVR v1.GroupVersionResource, mappings []sboApi.Mapping, services []sboApi.Service) *sboApi.ServiceBinding { - return &sboApi.ServiceBinding{ +// NewServiceBindingObject returns the bindingApi.ServiceBinding object +func NewServiceBindingObject(bindingName string, bindAsFiles bool, deploymentName string, deploymentGVR v1.GroupVersionResource, mappings []bindingApi.Mapping, services []bindingApi.Service) *bindingApi.ServiceBinding { + return &bindingApi.ServiceBinding{ TypeMeta: v1.TypeMeta{ - APIVersion: sboApi.GroupVersion.String(), + APIVersion: bindingApi.GroupVersion.String(), Kind: ServiceBindingKind, }, ObjectMeta: v1.ObjectMeta{ Name: bindingName, }, - Spec: sboApi.ServiceBindingSpec{ + Spec: bindingApi.ServiceBindingSpec{ DetectBindingResources: true, BindAsFiles: bindAsFiles, - Application: sboApi.Application{ - Ref: sboApi.Ref{ + Application: bindingApi.Application{ + Ref: bindingApi.Ref{ Name: deploymentName, Group: deploymentGVR.Group, Version: deploymentGVR.Version, @@ -121,7 +122,45 @@ func NewServiceBindingObject(bindingName string, bindAsFiles bool, deploymentNam } } -func mappingContainsBKS(bindableObjects []*meta.RESTMapping, bks sboApi.BindableKindsStatus) bool { +// GetBindingServiceBinding returns a ServiceBinding from group binding.operators.coreos.com/v1alpha1 +func (c Client) GetBindingServiceBinding(name string) (bindingApi.ServiceBinding, error) { + if c.DynamicClient == nil { + return bindingApi.ServiceBinding{}, nil + } + + u, err := c.GetDynamicResource(bindingApi.GroupVersionResource, name) + if err != nil { + return bindingApi.ServiceBinding{}, err + } + + var result bindingApi.ServiceBinding + err = c.ConvertUnstructuredToResource(*u, &result) + if err != nil { + return bindingApi.ServiceBinding{}, err + } + return result, nil +} + +// GetSpecServiceBinding returns a ServiceBinding from group servicebinding.io/v1alpha3 +func (c Client) GetSpecServiceBinding(name string) (specApi.ServiceBinding, error) { + if c.DynamicClient == nil { + return specApi.ServiceBinding{}, nil + } + + u, err := c.GetDynamicResource(specApi.GroupVersionResource, name) + if err != nil { + return specApi.ServiceBinding{}, err + } + + var result specApi.ServiceBinding + err = c.ConvertUnstructuredToResource(*u, &result) + if err != nil { + return specApi.ServiceBinding{}, err + } + return result, nil +} + +func mappingContainsBKS(bindableObjects []*meta.RESTMapping, bks bindingApi.BindableKindsStatus) bool { var gkAlreadyAdded bool // check every GroupKind only once for _, bo := range bindableObjects { diff --git a/pkg/kclient/interface.go b/pkg/kclient/interface.go index 15b1542c346..1efad0c25ed 100644 --- a/pkg/kclient/interface.go +++ b/pkg/kclient/interface.go @@ -7,7 +7,8 @@ import ( "github.com/go-openapi/spec" projectv1 "github.com/openshift/api/project/v1" olm "github.com/operator-framework/api/pkg/operators/v1alpha1" - sboApi "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" + bindingApi "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" + specApi "github.com/redhat-developer/service-binding-operator/apis/spec/v1alpha3" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -84,13 +85,15 @@ type ClientInterface interface { GetRestMappingFromUnstructured(unstructured.Unstructured) (*meta.RESTMapping, error) GetRestMappingFromGVK(gvk schema.GroupVersionKind) (*meta.RESTMapping, error) GetOperatorGVRList() ([]meta.RESTMapping, error) - ConvertUnstructuredToResource(u map[string]interface{}, obj interface{}) error + ConvertUnstructuredToResource(u unstructured.Unstructured, obj interface{}) error // binding.go IsServiceBindingSupported() (bool, error) - GetBindableKinds() (sboApi.BindableKinds, error) - GetBindableKindStatusRestMapping(bindableKindStatuses []sboApi.BindableKindsStatus) ([]*meta.RESTMapping, error) - NewServiceBindingServiceObject(unstructuredService unstructured.Unstructured, bindingName string) (sboApi.Service, error) + GetBindableKinds() (bindingApi.BindableKinds, error) + GetBindableKindStatusRestMapping(bindableKindStatuses []bindingApi.BindableKindsStatus) ([]*meta.RESTMapping, error) + GetBindingServiceBinding(name string) (bindingApi.ServiceBinding, error) + GetSpecServiceBinding(name string) (specApi.ServiceBinding, error) + NewServiceBindingServiceObject(unstructuredService unstructured.Unstructured, bindingName string) (bindingApi.Service, error) // owner_reference.go TryWithBlockOwnerDeletion(ownerReference metav1.OwnerReference, exec func(ownerReference metav1.OwnerReference) error) error diff --git a/pkg/kclient/mock_Client.go b/pkg/kclient/mock_Client.go index 5b776e74570..53890a75734 100644 --- a/pkg/kclient/mock_Client.go +++ b/pkg/kclient/mock_Client.go @@ -14,6 +14,7 @@ import ( v1 "github.com/openshift/api/project/v1" v1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" v1alpha10 "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1" + v1alpha3 "github.com/redhat-developer/service-binding-operator/apis/spec/v1alpha3" v10 "k8s.io/api/apps/v1" v11 "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/api/meta" @@ -78,7 +79,7 @@ func (mr *MockClientInterfaceMockRecorder) CollectEvents(selector, events, quit } // ConvertUnstructuredToResource mocks base method. -func (m *MockClientInterface) ConvertUnstructuredToResource(u map[string]interface{}, obj interface{}) error { +func (m *MockClientInterface) ConvertUnstructuredToResource(u unstructured.Unstructured, obj interface{}) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ConvertUnstructuredToResource", u, obj) ret0, _ := ret[0].(error) @@ -412,6 +413,21 @@ func (mr *MockClientInterfaceMockRecorder) GetBindableKinds() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBindableKinds", reflect.TypeOf((*MockClientInterface)(nil).GetBindableKinds)) } +// GetBindingServiceBinding mocks base method. +func (m *MockClientInterface) GetBindingServiceBinding(name string) (v1alpha10.ServiceBinding, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBindingServiceBinding", name) + ret0, _ := ret[0].(v1alpha10.ServiceBinding) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBindingServiceBinding indicates an expected call of GetBindingServiceBinding. +func (mr *MockClientInterfaceMockRecorder) GetBindingServiceBinding(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBindingServiceBinding", reflect.TypeOf((*MockClientInterface)(nil).GetBindingServiceBinding), name) +} + // GetCSVWithCR mocks base method. func (m *MockClientInterface) GetCSVWithCR(name string) (*v1alpha1.ClusterServiceVersion, error) { m.ctrl.T.Helper() @@ -855,6 +871,21 @@ func (mr *MockClientInterfaceMockRecorder) GetServerVersion(timeout interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerVersion", reflect.TypeOf((*MockClientInterface)(nil).GetServerVersion), timeout) } +// GetSpecServiceBinding mocks base method. +func (m *MockClientInterface) GetSpecServiceBinding(name string) (v1alpha3.ServiceBinding, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSpecServiceBinding", name) + ret0, _ := ret[0].(v1alpha3.ServiceBinding) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSpecServiceBinding indicates an expected call of GetSpecServiceBinding. +func (mr *MockClientInterfaceMockRecorder) GetSpecServiceBinding(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSpecServiceBinding", reflect.TypeOf((*MockClientInterface)(nil).GetSpecServiceBinding), name) +} + // IsCSVSupported mocks base method. func (m *MockClientInterface) IsCSVSupported() (bool, error) { m.ctrl.T.Helper() diff --git a/pkg/kclient/operators.go b/pkg/kclient/operators.go index d4f312d9da6..cce7919ce81 100644 --- a/pkg/kclient/operators.go +++ b/pkg/kclient/operators.go @@ -200,8 +200,8 @@ func (c *Client) GetRestMappingFromGVK(gvk schema.GroupVersionKind) (*meta.RESTM return mapper.RESTMapping(gvk.GroupKind(), gvk.Version) } -func (c *Client) ConvertUnstructuredToResource(u map[string]interface{}, obj interface{}) error { - return runtime.DefaultUnstructuredConverter.FromUnstructured(u, obj) +func (c *Client) ConvertUnstructuredToResource(u unstructured.Unstructured, obj interface{}) error { + return runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), obj) } // GetOperatorGVRList creates a slice of rest mappings that are provided by Operators (CSV) diff --git a/pkg/odo/cli/describe/binding.go b/pkg/odo/cli/describe/binding.go new file mode 100644 index 00000000000..ff18336b9e0 --- /dev/null +++ b/pkg/odo/cli/describe/binding.go @@ -0,0 +1,177 @@ +package describe + +import ( + "context" + "fmt" + "os" + "strconv" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime/schema" + ktemplates "k8s.io/kubectl/pkg/util/templates" + + "github.com/redhat-developer/odo/pkg/api" + "github.com/redhat-developer/odo/pkg/log" + "github.com/redhat-developer/odo/pkg/machineoutput" + "github.com/redhat-developer/odo/pkg/odo/cmdline" + "github.com/redhat-developer/odo/pkg/odo/genericclioptions" + "github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset" +) + +// BindingRecommendedCommandName is the recommended binding sub-command name +const BindingRecommendedCommandName = "binding" + +var describeBindingExample = ktemplates.Examples(` +# Describe the bindings in the current devfile +%[1]s + +# Describe a binding on the cluster +%[1]s --name frontend +`) + +type BindingOptions struct { + // nameFlag of the component to describe, optional + nameFlag string + + // Context + *genericclioptions.Context + + // Clients + clientset *clientset.Clientset + + // working directory + contextDir string +} + +// NewBindingOptions returns new instance of BindingOptions +func NewBindingOptions() *BindingOptions { + return &BindingOptions{} +} + +func (o *BindingOptions) SetClientset(clientset *clientset.Clientset) { + o.clientset = clientset +} + +func (o *BindingOptions) Complete(cmdline cmdline.Cmdline, args []string) (err error) { + if o.nameFlag == "" { + o.contextDir, err = os.Getwd() + if err != nil { + return err + } + + o.Context, err = genericclioptions.New(genericclioptions.NewCreateParameters(cmdline).NeedDevfile("")) + if err != nil { + return err + } + // this ensures that the namespace set in env.yaml is used + o.clientset.KubernetesClient.SetNamespace(o.GetProject()) + return nil + } + return nil +} + +func (o *BindingOptions) Validate() (err error) { + return nil +} + +func (o *BindingOptions) Run(ctx context.Context) error { + if o.nameFlag == "" { + bindings, err := o.runWithoutName() + if err != nil { + return err + } + printBindingsHumanReadableOutput(bindings) + return nil + } + + binding, err := o.runWithName() + if err != nil { + return err + } + printSingleBindingHumanReadableOutput(binding) + return nil +} + +// Run contains the logic for the odo command +func (o *BindingOptions) RunForJsonOutput(ctx context.Context) (out interface{}, err error) { + if o.nameFlag == "" { + return o.runWithoutName() + } + return o.runWithName() +} + +func (o *BindingOptions) runWithoutName() ([]api.ServiceBinding, error) { + return o.clientset.BindingClient.GetBindingsFromDevfile(o.EnvSpecificInfo.GetDevfileObj(), o.contextDir) +} + +func (o *BindingOptions) runWithName() (api.ServiceBinding, error) { + return o.clientset.BindingClient.GetBindingFromCluster(o.nameFlag) +} + +// NewCmdBinding implements the binding odo sub-command +func NewCmdBinding(name, fullName string) *cobra.Command { + o := NewBindingOptions() + + var bindingCmd = &cobra.Command{ + Use: name, + Short: "Describe bindings", + Long: "Describe bindings", + Args: cobra.NoArgs, + Example: fmt.Sprintf(describeBindingExample, fullName), + Run: func(cmd *cobra.Command, args []string) { + genericclioptions.GenericRun(o, cmd, args) + }, + } + bindingCmd.Flags().StringVar(&o.nameFlag, "name", "", "Name of the binding to describe, optional. By default, the bindings in the local devfile are described") + clientset.Add(bindingCmd, clientset.KUBERNETES, clientset.BINDING) + machineoutput.UsedByCommand(bindingCmd) + + return bindingCmd +} + +// printSingleBindingHumanReadableOutput prints information about a binding and returns true if status is unknown +func printSingleBindingHumanReadableOutput(binding api.ServiceBinding) bool { + log.Describef("Service Binding Name: ", binding.Name) + log.Info("Services:") + for _, service := range binding.Spec.Services { + gvk := schema.FromAPIVersionAndKind(service.APIVersion, service.Kind) + log.Printf("%s (%s.%s)", service.Name, gvk.Kind, gvk.Group) + } + log.Describef("Bind as files: ", strconv.FormatBool(binding.Spec.BindAsFiles)) + log.Describef("Detect binding resources: ", strconv.FormatBool(binding.Spec.DetectBindingResources)) + + if binding.Status == nil { + log.Describef("Available binding information: ", "unknown") + return true + } + log.Info("Available binding information:") + for _, info := range binding.Status.BindingFiles { + log.Printf(info) + } + for _, info := range binding.Status.BindingEnvVars { + log.Printf(info) + } + return false +} + +func printBindingsHumanReadableOutput(bindings []api.ServiceBinding) { + if len(bindings) == 0 { + log.Info("No ServiceBinding used by the current component") + return + } + + log.Info("ServiceBinding used by the current component:") + someStatusUnknown := false + for _, binding := range bindings { + fmt.Println() + statusUnknown := printSingleBindingHumanReadableOutput(binding) + if statusUnknown { + someStatusUnknown = true + } + } + if someStatusUnknown { + fmt.Println() + log.Info(`Binding information for one or more ServiceBinding is not available because they don't exist on the cluster yet. +Start "odo dev" first to see binding information.`) + } +} diff --git a/pkg/odo/cli/describe/describe.go b/pkg/odo/cli/describe/describe.go index d1af8442088..e87a6e96a6c 100644 --- a/pkg/odo/cli/describe/describe.go +++ b/pkg/odo/cli/describe/describe.go @@ -16,7 +16,8 @@ func NewCmdDescribe(name, fullName string) *cobra.Command { } componentCmd := NewCmdComponent(ComponentRecommendedCommandName, util.GetFullName(fullName, ComponentRecommendedCommandName)) - describeCmd.AddCommand(componentCmd) + bindingCmd := NewCmdBinding(BindingRecommendedCommandName, util.GetFullName(fullName, BindingRecommendedCommandName)) + describeCmd.AddCommand(componentCmd, bindingCmd) describeCmd.Annotations = map[string]string{"command": "main"} describeCmd.SetUsageTemplate(util.CmdUsageTemplate) diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-service-binding-envvars.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-service-binding-envvars.yaml new file mode 100644 index 00000000000..ddf978330aa --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-service-binding-envvars.yaml @@ -0,0 +1,88 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: test +components: +- container: + dedicatedPod: false + endpoints: + - name: http-3000 + secure: false + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + name: runtime +- kubernetes: + inlined: | + apiVersion: binding.operators.coreos.com/v1alpha1 + kind: ServiceBinding + metadata: + name: my-nodejs-app-cluster-sample + spec: + application: + group: apps + name: my-nodejs-app-app + resource: deployments + version: v1 + bindAsFiles: false + detectBindingResources: true + services: + - group: postgresql.k8s.enterprisedb.io + id: my-nodejs-app-cluster-sample + kind: Cluster + name: cluster-sample + resource: clusters + version: v1 + name: my-nodejs-app-cluster-sample +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: my-nodejs-app + 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 diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-service-binding-files.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-service-binding-files.yaml new file mode 100644 index 00000000000..a178806460c --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-service-binding-files.yaml @@ -0,0 +1,88 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: test +components: +- container: + dedicatedPod: false + endpoints: + - name: http-3000 + secure: false + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + name: runtime +- kubernetes: + inlined: | + apiVersion: binding.operators.coreos.com/v1alpha1 + kind: ServiceBinding + metadata: + name: my-nodejs-app-cluster-sample + spec: + application: + group: apps + name: my-nodejs-app-app + resource: deployments + version: v1 + bindAsFiles: true + detectBindingResources: true + services: + - group: postgresql.k8s.enterprisedb.io + id: my-nodejs-app-cluster-sample + kind: Cluster + name: cluster-sample + resource: clusters + version: v1 + name: my-nodejs-app-cluster-sample +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: my-nodejs-app + 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 diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding-envvars.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding-envvars.yaml new file mode 100644 index 00000000000..3547969937d --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding-envvars.yaml @@ -0,0 +1,85 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: test +components: +- container: + dedicatedPod: false + endpoints: + - name: http-3000 + secure: false + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + name: runtime +- kubernetes: + inlined: | + apiVersion: servicebinding.io/v1alpha3 + kind: ServiceBinding + metadata: + name: my-nodejs-app-cluster-sample + spec: + service: + apiVersion: postgresql.k8s.enterprisedb.io/v1 + kind: Cluster + name: cluster-sample + workload: + apiVersion: apps/v1 + kind: Deployment + name: my-nodejs-app-app + env: + - name: PASSWD + key: password + name: my-nodejs-app-cluster-sample +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: my-nodejs-app + 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 diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding.yaml new file mode 100644 index 00000000000..c4f52006826 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-spec-service-binding.yaml @@ -0,0 +1,82 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + hotReloadCapable: false + workingDir: ${PROJECT_SOURCE} + id: test +components: +- container: + dedicatedPod: false + endpoints: + - name: http-3000 + secure: false + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + name: runtime +- kubernetes: + inlined: | + apiVersion: servicebinding.io/v1alpha3 + kind: ServiceBinding + metadata: + name: my-nodejs-app-cluster-sample + spec: + service: + apiVersion: postgresql.k8s.enterprisedb.io/v1 + kind: Cluster + name: cluster-sample + workload: + apiVersion: apps/v1 + kind: Deployment + name: my-nodejs-app-app + name: my-nodejs-app-cluster-sample +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: my-nodejs-app + 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 diff --git a/tests/integration/devfile/cmd_describe_binding_test.go b/tests/integration/devfile/cmd_describe_binding_test.go new file mode 100644 index 00000000000..b5b048bf185 --- /dev/null +++ b/tests/integration/devfile/cmd_describe_binding_test.go @@ -0,0 +1,255 @@ +package devfile + +import ( + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/redhat-developer/odo/tests/helper" +) + +var _ = Describe("odo describe binding command tests", func() { + var commonVar helper.CommonVar + + // This is run before every Spec (It) + var _ = BeforeEach(func() { + if helper.IsKubernetesCluster() { + Skip("Operators have not been setup on Kubernetes cluster yet. Remove this once the issue has been fixed.") + } + commonVar = helper.CommonBeforeEach() + helper.Chdir(commonVar.Context) + }) + + // This is run after every Spec (It) + var _ = AfterEach(func() { + helper.CommonAfterEach(commonVar) + }) + + When("creating a component with a binding", func() { + cmpName := "my-nodejs-app" + BeforeEach(func() { + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-service-binding-files.yaml")).ShouldPass() + }) + + It("should describe the binding without running odo dev", func() { + By("JSON output", func() { + res := helper.Cmd("odo", "describe", "binding", "-o", "json").ShouldPass() + stdout, stderr := res.Out(), res.Err() + Expect(stderr).To(BeEmpty()) + Expect(helper.IsJSON(stdout)).To(BeTrue()) + helper.JsonPathContentIs(stdout, "0.name", "my-nodejs-app-cluster-sample") + helper.JsonPathContentIs(stdout, "0.spec.services.0.apiVersion", "postgresql.k8s.enterprisedb.io/v1") + helper.JsonPathContentIs(stdout, "0.spec.services.0.kind", "Cluster") + helper.JsonPathContentIs(stdout, "0.spec.services.0.name", "cluster-sample") + helper.JsonPathContentIs(stdout, "0.spec.detectBindingResources", "true") + helper.JsonPathContentIs(stdout, "0.spec.bindAsFiles", "true") + helper.JsonPathContentIs(stdout, "0.status", "") + }) + By("human readable output", func() { + res := helper.Cmd("odo", "describe", "binding").ShouldPass() + stdout, _ := res.Out(), res.Err() + Expect(stdout).To(ContainSubstring("ServiceBinding used by the current component")) + Expect(stdout).To(ContainSubstring("Service Binding Name: my-nodejs-app-cluster-sample")) + Expect(stdout).To(ContainSubstring("cluster-sample (Cluster.postgresql.k8s.enterprisedb.io)")) + Expect(stdout).To(ContainSubstring("Bind as files: true")) + Expect(stdout).To(ContainSubstring("Detect binding resources: true")) + Expect(stdout).To(ContainSubstring("Available binding information: unknown")) + Expect(stdout).To(ContainSubstring("Binding information for one or more ServiceBinding is not available")) + }) + }) + }) + + for _, ctx := range []struct { + title string + devfile string + assertJsonOutput func(list bool, stdout, stderr string) + assertHumanReadableOutput func(list bool, stdout, stderr string) + }{ + { + title: "creating a component with a binding as files", + devfile: helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-service-binding-files.yaml"), + assertJsonOutput: func(list bool, stdout, stderr string) { + prefix := "" + if list { + prefix = "0." + } + Expect(stderr).To(BeEmpty()) + Expect(helper.IsJSON(stdout)).To(BeTrue()) + helper.JsonPathContentIs(stdout, prefix+"name", "my-nodejs-app-cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.apiVersion", "postgresql.k8s.enterprisedb.io/v1") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.kind", "Cluster") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.name", "cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.detectBindingResources", "true") + helper.JsonPathContentIs(stdout, prefix+"spec.bindAsFiles", "true") + helper.JsonPathContentContain(stdout, prefix+"status.bindingsFiles", "${SERVICE_BINDING_ROOT}/my-nodejs-app-cluster-sample/password") + helper.JsonPathContentIs(stdout, prefix+"status.bindingEnvVars", "") + }, + assertHumanReadableOutput: func(list bool, stdout, stderr string) { + if list { + Expect(stdout).To(ContainSubstring("ServiceBinding used by the current component")) + } + Expect(stdout).To(ContainSubstring("Service Binding Name: my-nodejs-app-cluster-sample")) + Expect(stdout).To(ContainSubstring("cluster-sample (Cluster.postgresql.k8s.enterprisedb.io)")) + Expect(stdout).To(ContainSubstring("Bind as files: true")) + Expect(stdout).To(ContainSubstring("Detect binding resources: true")) + Expect(stdout).To(ContainSubstring("${SERVICE_BINDING_ROOT}/my-nodejs-app-cluster-sample/password")) + }, + }, + { + title: "creating a component with a binding as environment variables", + devfile: helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-service-binding-envvars.yaml"), + assertJsonOutput: func(list bool, stdout, stderr string) { + prefix := "" + if list { + prefix = "0." + } + Expect(stderr).To(BeEmpty()) + Expect(helper.IsJSON(stdout)).To(BeTrue()) + helper.JsonPathContentIs(stdout, prefix+"name", "my-nodejs-app-cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.apiVersion", "postgresql.k8s.enterprisedb.io/v1") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.kind", "Cluster") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.name", "cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.detectBindingResources", "true") + helper.JsonPathContentIs(stdout, prefix+"spec.bindAsFiles", "false") + helper.JsonPathContentIs(stdout, prefix+"status.bindingsFiles", "") + helper.JsonPathContentContain(stdout, prefix+"status.bindingEnvVars", "PASSWORD") + }, + assertHumanReadableOutput: func(list bool, stdout, stderr string) { + if list { + Expect(stdout).To(ContainSubstring("ServiceBinding used by the current component")) + } + Expect(stdout).To(ContainSubstring("Service Binding Name: my-nodejs-app-cluster-sample")) + Expect(stdout).To(ContainSubstring("cluster-sample (Cluster.postgresql.k8s.enterprisedb.io)")) + Expect(stdout).To(ContainSubstring("Bind as files: false")) + Expect(stdout).To(ContainSubstring("Detect binding resources: true")) + Expect(stdout).To(ContainSubstring("PASSWORD")) + }, + }, + { + title: "creating a component with a spec binding", + devfile: helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-spec-service-binding.yaml"), + assertJsonOutput: func(list bool, stdout, stderr string) { + prefix := "" + if list { + prefix = "0." + } + Expect(stderr).To(BeEmpty()) + Expect(helper.IsJSON(stdout)).To(BeTrue()) + helper.JsonPathContentIs(stdout, prefix+"name", "my-nodejs-app-cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.apiVersion", "postgresql.k8s.enterprisedb.io/v1") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.kind", "Cluster") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.name", "cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.detectBindingResources", "false") + helper.JsonPathContentIs(stdout, prefix+"spec.bindAsFiles", "true") + helper.JsonPathContentContain(stdout, prefix+"status.bindingsFiles", "${SERVICE_BINDING_ROOT}/my-nodejs-app-cluster-sample/password") + helper.JsonPathContentIs(stdout, prefix+"status.bindingEnvVars", "") + }, + assertHumanReadableOutput: func(list bool, stdout, stderr string) { + if list { + Expect(stdout).To(ContainSubstring("ServiceBinding used by the current component")) + } + Expect(stdout).To(ContainSubstring("Service Binding Name: my-nodejs-app-cluster-sample")) + Expect(stdout).To(ContainSubstring("cluster-sample (Cluster.postgresql.k8s.enterprisedb.io)")) + Expect(stdout).To(ContainSubstring("Bind as files: true")) + Expect(stdout).To(ContainSubstring("Detect binding resources: false")) + Expect(stdout).To(ContainSubstring("${SERVICE_BINDING_ROOT}/my-nodejs-app-cluster-sample/password")) + }, + }, + { + title: "creating a component with a spec binding and envvars", + devfile: helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-spec-service-binding-envvars.yaml"), + assertJsonOutput: func(list bool, stdout, stderr string) { + prefix := "" + if list { + prefix = "0." + } + Expect(stderr).To(BeEmpty()) + Expect(helper.IsJSON(stdout)).To(BeTrue()) + helper.JsonPathContentIs(stdout, prefix+"name", "my-nodejs-app-cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.apiVersion", "postgresql.k8s.enterprisedb.io/v1") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.kind", "Cluster") + helper.JsonPathContentIs(stdout, prefix+"spec.services.0.name", "cluster-sample") + helper.JsonPathContentIs(stdout, prefix+"spec.detectBindingResources", "false") + helper.JsonPathContentIs(stdout, prefix+"spec.bindAsFiles", "true") + helper.JsonPathContentContain(stdout, prefix+"status.bindingsFiles", "${SERVICE_BINDING_ROOT}/my-nodejs-app-cluster-sample/password") + helper.JsonPathContentContain(stdout, prefix+"status.bindingEnvVars", "PASSWD") + }, + assertHumanReadableOutput: func(list bool, stdout, stderr string) { + if list { + Expect(stdout).To(ContainSubstring("ServiceBinding used by the current component")) + } + Expect(stdout).To(ContainSubstring("Service Binding Name: my-nodejs-app-cluster-sample")) + Expect(stdout).To(ContainSubstring("cluster-sample (Cluster.postgresql.k8s.enterprisedb.io)")) + Expect(stdout).To(ContainSubstring("Bind as files: true")) + Expect(stdout).To(ContainSubstring("Detect binding resources: false")) + Expect(stdout).To(ContainSubstring("${SERVICE_BINDING_ROOT}/my-nodejs-app-cluster-sample/password")) + Expect(stdout).To(ContainSubstring("PASSWD")) + }, + }, + } { + When(ctx.title, func() { + cmpName := "my-nodejs-app" + BeforeEach(func() { + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", ctx.devfile).ShouldPass() + }) + + When("Starting a Pg service", func() { + BeforeEach(func() { + // Ensure that the operators are installed + commonVar.CliRunner.EnsureOperatorIsInstalled("service-binding-operator") + commonVar.CliRunner.EnsureOperatorIsInstalled("cloud-native-postgresql") + Eventually(func() string { + out, _ := commonVar.CliRunner.GetBindableKinds() + return out + }, 120, 3).Should(ContainSubstring("Cluster")) + addBindableKind := commonVar.CliRunner.Run("apply", "-f", helper.GetExamplePath("manifests", "bindablekind-instance.yaml")) + Expect(addBindableKind.ExitCode()).To(BeEquivalentTo(0)) + }) + + When("running dev session", func() { + var session helper.DevSession + BeforeEach(func() { + var err error + session, _, _, _, err = helper.StartDevMode() + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + session.Kill() + session.WaitEnd() + }) + + It("should describe the binding", func() { + By("JSON output", func() { + res := helper.Cmd("odo", "describe", "binding", "-o", "json").ShouldPass() + stdout, stderr := res.Out(), res.Err() + ctx.assertJsonOutput(true, stdout, stderr) + }) + By("human readable output", func() { + res := helper.Cmd("odo", "describe", "binding").ShouldPass() + stdout, stderr := res.Out(), res.Err() + ctx.assertHumanReadableOutput(true, stdout, stderr) + }) + + By("JSON output from another directory with name flag", func() { + err := os.Chdir("/") + Expect(err).ToNot(HaveOccurred()) + res := helper.Cmd("odo", "describe", "binding", "--name", "my-nodejs-app-cluster-sample", "-o", "json").ShouldPass() + stdout, stderr := res.Out(), res.Err() + ctx.assertJsonOutput(false, stdout, stderr) + }) + By("human readable output from another directory with name flag", func() { + err := os.Chdir("/") + Expect(err).ToNot(HaveOccurred()) + res := helper.Cmd("odo", "describe", "binding", "--name", "my-nodejs-app-cluster-sample").ShouldPass() + stdout, stderr := res.Out(), res.Err() + ctx.assertHumanReadableOutput(false, stdout, stderr) + }) + + }) + }) + }) + }) + } +}) diff --git a/tests/integration/devfile/cmd_describe_test.go b/tests/integration/devfile/cmd_describe_component_test.go similarity index 99% rename from tests/integration/devfile/cmd_describe_test.go rename to tests/integration/devfile/cmd_describe_component_test.go index 46ecb3fddba..01a0bf95e5d 100644 --- a/tests/integration/devfile/cmd_describe_test.go +++ b/tests/integration/devfile/cmd_describe_component_test.go @@ -10,7 +10,7 @@ import ( "github.com/redhat-developer/odo/tests/helper" ) -var _ = Describe("odo describe command tests", func() { +var _ = Describe("odo describe component command tests", func() { var commonVar helper.CommonVar var cmpName string