Files
odo/pkg/binding/binding.go
Armel Soro eeda644cc9 Add support for OpenShift Devfile components (#6548)
* 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>
2023-02-03 14:30:24 +01:00

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
}