mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
* Add integration test case Co-authored-by: Anand Kumar Singh <anandrkskd@gmail.com> Co-authored-by: Philippe Martin <phmartin@redhat.com> * Add ApplyOpenShift method to handler * Test openhift component with odo dev * Rename GetKubernetesComponentsToPush to GetK8sAndOcComponentsToPush and modify if to obtain both k8s and oc components Signed-off-by: Parthvi Vala <pvala@redhat.com> * Fix unit test failures with delete_test Signed-off-by: Parthvi Vala <pvala@redhat.com> * update ListClusterResourcesToDeleteFromDevfile to fetch openshift component,Add ListOpenShiftComponents Signed-off-by: anandrkskd <anandrkskd@gmail.com> * fix testcase 'should have deleted the old resource and created the new resource' and add helper function ReplaceStrings Signed-off-by: anandrkskd <anandrkskd@gmail.com> * fix debug test to check openshift component Signed-off-by: anandrkskd <anandrkskd@gmail.com> * Update GetBindingsFromDevfile to include openshift components * Update offline tests * Add openshift component to devfiles * Update tests * Fix binding tests * Fix RemoveBinding unit tests * Handle OpenShift components when removing binding * odo describe component displaysOpenShift components * Remove unused function --------- Signed-off-by: Parthvi Vala <pvala@redhat.com> Signed-off-by: anandrkskd <anandrkskd@gmail.com> Co-authored-by: Anand Kumar Singh <anandrkskd@gmail.com> Co-authored-by: Philippe Martin <phmartin@redhat.com> Co-authored-by: Parthvi Vala <pvala@redhat.com>
379 lines
12 KiB
Go
379 lines
12 KiB
Go
package binding
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
|
|
"k8s.io/klog"
|
|
|
|
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/redhat-developer/odo/pkg/project"
|
|
|
|
"github.com/devfile/library/v2/pkg/devfile/parser"
|
|
devfilefs "github.com/devfile/library/v2/pkg/testingutil/filesystem"
|
|
"gopkg.in/yaml.v2"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
devfilev1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
|
|
parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
|
|
|
|
"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"
|
|
"github.com/redhat-developer/odo/pkg/libdevfile"
|
|
clierrors "github.com/redhat-developer/odo/pkg/odo/cli/errors"
|
|
)
|
|
|
|
type BindingClient struct {
|
|
// Backends
|
|
flagsBackend *backendpkg.FlagsBackend
|
|
interactiveBackend *backendpkg.InteractiveBackend
|
|
|
|
// Clients
|
|
kubernetesClient kclient.ClientInterface
|
|
}
|
|
|
|
var _ Client = (*BindingClient)(nil)
|
|
|
|
func NewBindingClient(projectClient project.Client, kubernetesClient kclient.ClientInterface) *BindingClient {
|
|
// We create the asker client and the backends here and not at the CLI level, as we want to hide these details to the CLI
|
|
askerClient := asker.NewSurveyAsker()
|
|
return &BindingClient{
|
|
flagsBackend: backendpkg.NewFlagsBackend(),
|
|
interactiveBackend: backendpkg.NewInteractiveBackend(askerClient, projectClient, kubernetesClient),
|
|
kubernetesClient: kubernetesClient,
|
|
}
|
|
}
|
|
|
|
// GetFlags gets the flag specific to add binding operation so that it can correctly decide on the backend to be used
|
|
// It ignores all the flags except the ones specific to add binding operation, for e.g. verbosity flag
|
|
func (o *BindingClient) GetFlags(flags map[string]string) map[string]string {
|
|
bindingFlags := map[string]string{}
|
|
for flag, value := range flags {
|
|
if flag == backendpkg.FLAG_NAME ||
|
|
flag == backendpkg.FLAG_WORKLOAD ||
|
|
flag == backendpkg.FLAG_SERVICE_NAMESPACE ||
|
|
flag == backendpkg.FLAG_SERVICE ||
|
|
flag == backendpkg.FLAG_BIND_AS_FILES ||
|
|
flag == backendpkg.FLAG_NAMING_STRATEGY {
|
|
bindingFlags[flag] = value
|
|
}
|
|
}
|
|
return bindingFlags
|
|
}
|
|
|
|
func (o *BindingClient) GetServiceInstances(namespace string) (map[string]unstructured.Unstructured, error) {
|
|
err := o.checkServiceBindingOperatorInstalled()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the BindableKinds/bindable-kinds object
|
|
bindableKind, err := o.kubernetesClient.GetBindableKinds()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get a list of restMappings of all the GVKs present in bindableKind's Status
|
|
bindableKindRestMappings, err := o.kubernetesClient.GetBindableKindStatusRestMapping(bindableKind.Status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var bindableObjectMap = map[string]unstructured.Unstructured{}
|
|
for _, restMapping := range bindableKindRestMappings {
|
|
// TODO: Debug into why List returns all the versions instead of the GVR version
|
|
// List all the instances of the restMapping object
|
|
resources, err := o.kubernetesClient.ListDynamicResources(namespace, restMapping.Resource, "")
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) || kerrors.IsForbidden(err) {
|
|
// Assume the namespace is deleted or being terminated, hence user can't list its resources
|
|
klog.V(3).Infoln(err)
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
for _, item := range resources.Items {
|
|
// format: `<name> (<kind>.<group>)`
|
|
serviceName := fmt.Sprintf("%s (%s.%s)", item.GetName(), item.GetKind(), item.GroupVersionKind().Group)
|
|
bindableObjectMap[serviceName] = item
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
// The function also returns status information of the binding in the cluster, if accessible, or a warning if the cluster is not accessible
|
|
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
|
|
}
|
|
ocpComponents, err := devfileObj.Data.GetComponents(parsercommon.DevfileOptions{
|
|
ComponentOptions: parsercommon.ComponentOptions{
|
|
ComponentType: devfilev1alpha2.OpenshiftComponentType,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allComponents := make([]devfilev1alpha2.Component, 0, len(kubeComponents)+len(ocpComponents))
|
|
allComponents = append(allComponents, kubeComponents...)
|
|
allComponents = append(allComponents, ocpComponents...)
|
|
|
|
var warning error
|
|
for _, component := range allComponents {
|
|
strCRD, err := libdevfile.GetK8sManifestsWithVariablesSubstituted(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 := kclient.ConvertUnstructuredToResource(u, &sbo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sb, err := kclient.APIServiceBindingFromBinding(sbo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sb.Status, err = o.getStatusFromBinding(sb.Name)
|
|
if err != nil {
|
|
warning = clierrors.NewWarning(kclient.NewNoConnectionError().Error(), err)
|
|
}
|
|
|
|
result = append(result, sb)
|
|
|
|
case specApi.GroupVersion.WithKind("ServiceBinding"):
|
|
|
|
var sbc specApi.ServiceBinding
|
|
err := kclient.ConvertUnstructuredToResource(u, &sbc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sb := kclient.APIServiceBindingFromSpec(sbc)
|
|
sb.Status, err = o.getStatusFromSpec(sb.Name)
|
|
if err != nil {
|
|
warning = clierrors.NewWarning(kclient.NewNoConnectionError().Error(), err)
|
|
}
|
|
|
|
result = append(result, sb)
|
|
|
|
}
|
|
}
|
|
return result, warning
|
|
}
|
|
|
|
// 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 {
|
|
var sb api.ServiceBinding
|
|
sb, err = kclient.APIServiceBindingFromBinding(bindingSB)
|
|
if err != nil {
|
|
return api.ServiceBinding{}, err
|
|
}
|
|
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 := kclient.APIServiceBindingFromSpec(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) {
|
|
if o.kubernetesClient == nil {
|
|
return nil, nil
|
|
}
|
|
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
|
|
}
|
|
|
|
func (o *BindingClient) checkServiceBindingOperatorInstalled() error {
|
|
isServiceBindingInstalled, err := o.kubernetesClient.IsServiceBindingSupported()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isServiceBindingInstalled {
|
|
//revive:disable:error-strings This is a top-level error message displayed as is to the end user
|
|
return fmt.Errorf("Service Binding Operator is not installed on the cluster, please ensure it is installed before proceeding. " +
|
|
"See installation instructions: https://odo.dev/docs/command-reference/add-binding#installing-the-service-binding-operator")
|
|
//revive:enable:error-strings
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *BindingClient) CheckServiceBindingsInjectionDone(componentName string, appName string) (bool, error) {
|
|
|
|
deployment, err := o.kubernetesClient.GetOneDeployment(componentName, appName, true)
|
|
if err != nil {
|
|
// If not deployment yet => all bindings are done
|
|
if _, ok := err.(*kclient.DeploymentNotFoundError); ok {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
}
|
|
deploymentName := deployment.GetName()
|
|
|
|
specList, bindingList, err := o.kubernetesClient.ListServiceBindingsFromAllGroups()
|
|
if err != nil {
|
|
// If ServiceBinding kind is not registered => all bindings are done
|
|
if runtime.IsNotRegisteredError(err) {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
for _, binding := range bindingList {
|
|
app := binding.Spec.Application
|
|
if app.Group != appsv1.SchemeGroupVersion.Group ||
|
|
app.Version != appsv1.SchemeGroupVersion.Version ||
|
|
(app.Kind != "Deployment" && app.Resource != "deployments") {
|
|
continue
|
|
}
|
|
if app.Name != deploymentName {
|
|
continue
|
|
}
|
|
if injected := meta.IsStatusConditionTrue(binding.Status.Conditions, bindingApis.InjectionReady); !injected {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
for _, binding := range specList {
|
|
app := binding.Spec.Workload
|
|
if app.APIVersion != appsv1.SchemeGroupVersion.String() ||
|
|
app.Kind != "Deployment" {
|
|
continue
|
|
}
|
|
if app.Name != deploymentName {
|
|
continue
|
|
}
|
|
if injected := meta.IsStatusConditionTrue(binding.Status.Conditions, bindingApis.InjectionReady); !injected {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|