diff --git a/.editorconfig b/.editorconfig index 55f806080c..48c1ed8542 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,7 +12,13 @@ indent_size = 4 max_line_length = 101 -[*.yml,*.yaml,*.toml] +[*.yml] +indent_size = 2 + +[*.yaml] +indent_size = 2 + +[*.toml] indent_size = 2 [*.md] diff --git a/deploy/crds/apps.openshift.io_servicebindingrequests_crd.yaml b/deploy/crds/apps.openshift.io_servicebindingrequests_crd.yaml new file mode 100644 index 0000000000..5219df3ce9 --- /dev/null +++ b/deploy/crds/apps.openshift.io_servicebindingrequests_crd.yaml @@ -0,0 +1,232 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: servicebindingrequests.apps.openshift.io +spec: + group: apps.openshift.io + names: + kind: ServiceBindingRequest + listKind: ServiceBindingRequestList + plural: servicebindingrequests + shortNames: + - sbr + - sbrs + singular: servicebindingrequest + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: ServiceBindingRequest is the Schema for the servicebindings API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServiceBindingRequestSpec defines the desired state of ServiceBindingRequest + properties: + applicationSelector: + description: ApplicationSelector is used to identify the application + connecting to the backing service operator. + properties: + group: + type: string + matchLabels: + additionalProperties: + type: string + type: object + resource: + type: string + resourceRef: + type: string + version: + type: string + required: + - matchLabels + - resource + - resourceRef + - version + type: object + backingServiceSelector: + description: BackingServiceSelector is used to identify the backing + service operator. + properties: + group: + type: string + kind: + type: string + resourceRef: + type: string + version: + type: string + required: + - group + - kind + - resourceRef + - version + type: object + backingServiceSelectors: + description: BackingServiceSelectors is an slice of BackingServiceSelector + items: + description: BackingServiceSelector defines the selector based on + resource name, version, and resource kind + properties: + group: + type: string + kind: + type: string + resourceRef: + type: string + version: + type: string + required: + - group + - kind + - resourceRef + - version + type: object + type: array + customEnvVar: + description: Custom env variables + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) are expanded using + the previous defined environment variables in the container + and any service environment variables. If a variable cannot + be resolved, the reference in the input string will be unchanged. + The $(VAR_NAME) syntax can be escaped with a double $$, ie: + $$(VAR_NAME). Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, metadata.labels, metadata.annotations, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP.' + properties: + apiVersion: + description: Version of the schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: only resources + limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, optional + for env vars' + type: string + divisor: + description: Specifies the output format of the exposed + resources, defaults to "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + detectBindingResources: + description: DetectBindingResources is flag used to bind all non-bindable + variables from different subresources owned by backing operator CR. + type: boolean + envVarPrefix: + description: EnvVarPrefix is the prefix for environment variables + type: string + mountPathPrefix: + description: MountPathPrefix is the prefix for volume mount + type: string + required: + - applicationSelector + - backingServiceSelector + - backingServiceSelectors + type: object + status: + description: ServiceBindingRequestStatus defines the observed state of ServiceBindingRequest + properties: + applicationObjects: + description: ApplicationObjects contains all the application objects + filtered by label + items: + type: string + type: array + bindingStatus: + description: BindingStatus is the status of the service binding request. + type: string + secret: + description: Secret is the name of the intermediate secret + type: string + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true diff --git a/go.mod b/go.mod index 5eecce00bc..0c49128580 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( // Pinned to kubernetes-1.15.4 replace ( + github.com/openshift/api => github.com/openshift/api v0.0.0-20190424152011-77b8897ec79a k8s.io/api => k8s.io/api v0.0.0-20190918195907-bd6ac527cfd2 k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918201827-3de75813f604 k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d diff --git a/go.sum b/go.sum index 7b8cbebaa4..2a92f95d91 100644 --- a/go.sum +++ b/go.sum @@ -439,6 +439,8 @@ github.com/opencontainers/image-spec v0.0.0-20170604055404-372ad780f634/go.mod h github.com/opencontainers/runc v0.0.0-20181113202123-f000fe11ece1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runtime-spec v1.0.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v0.0.0-20170621221121-4a2974bf1ee9/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs= +github.com/openshift/api v0.0.0-20190424152011-77b8897ec79a h1:zJauc4Mzrbn2C+G6cMwvvMCGWQZoyHaDlhoP6AjQDs8= +github.com/openshift/api v0.0.0-20190424152011-77b8897ec79a/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= github.com/openshift/api v3.9.1-0.20190424152011-77b8897ec79a+incompatible h1:q2JBuObKafI7B4Eli6eLd+2T5JsU9ioWZ82zQwyjJPg= github.com/openshift/api v3.9.1-0.20190424152011-77b8897ec79a+incompatible/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= github.com/openshift/client-go v0.0.0-20190401163519-84c2b942258a/go.mod h1:6rzn+JTr7+WYS2E1TExP4gByoABxMznR6y2SnUIkmxk= @@ -459,7 +461,7 @@ github.com/operator-framework/operator-registry v1.0.1/go.mod h1:1xEdZjjUg2hPEd5 github.com/operator-framework/operator-registry v1.0.4/go.mod h1:hve6YwcjM2nGVlscLtNsp9sIIBkNZo6jlJgzWw7vP9s= github.com/operator-framework/operator-registry v1.1.1/go.mod h1:7D4WEwL+EKti5npUh4/u64DQhawCBRugp8Ql20duUb4= github.com/operator-framework/operator-sdk v0.8.2-0.20190522220659-031d71ef8154/go.mod h1:iVyukRkam5JZa8AnjYf+/G3rk7JI1+M6GsU0sq0B9NA= -github.com/operator-framework/operator-sdk v0.12.0 h1:9eAD1L8e6pPCpFCAacBUVf2eloDkRuVm29GTCOktLqQ= +github.com/operator-framework/operator-sdk v0.12.0 h1:aD4qPbSAbZgRj1jFdFLq/dBI4P4aKX8d4rJowyQtTYM= github.com/operator-framework/operator-sdk v0.12.0/go.mod h1:mW8isQxiXlLCVf2E+xqflkQAVLOTbiqjndKdkKIrR0M= github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= diff --git a/pkg/apis/apps/v1alpha1/servicebindingrequest_types.go b/pkg/apis/apps/v1alpha1/servicebindingrequest_types.go index c6c116431b..64530a9e29 100644 --- a/pkg/apis/apps/v1alpha1/servicebindingrequest_types.go +++ b/pkg/apis/apps/v1alpha1/servicebindingrequest_types.go @@ -3,6 +3,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" ) // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file @@ -28,6 +29,9 @@ type ServiceBindingRequestSpec struct { // BackingServiceSelector is used to identify the backing service operator. BackingServiceSelector BackingServiceSelector `json:"backingServiceSelector"` + // BackingServiceSelectors is used to identify multiple backing services. + BackingServiceSelectors []BackingServiceSelector `json:"backingServiceSelectors"` + // ApplicationSelector is used to identify the application connecting to the // backing service operator. ApplicationSelector ApplicationSelector `json:"applicationSelector"` @@ -52,10 +56,8 @@ type ServiceBindingRequestStatus struct { // BackingServiceSelector defines the selector based on resource name, version, and resource kind // +k8s:openapi-gen=true type BackingServiceSelector struct { - Group string `json:"group"` - Version string `json:"version"` - Kind string `json:"kind"` - ResourceRef string `json:"resourceRef"` + schema.GroupVersionKind `json:",inline" yaml:",inline"` + ResourceRef string `json:"resourceRef"` } // ApplicationSelector defines the selector based on labels and GVR diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go index 8086e9d2b4..71d4b7f778 100644 --- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go @@ -35,6 +35,7 @@ func (in *ApplicationSelector) DeepCopy() *ApplicationSelector { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackingServiceSelector) DeepCopyInto(out *BackingServiceSelector) { *out = *in + out.GroupVersionKind = in.GroupVersionKind return } @@ -120,6 +121,11 @@ func (in *ServiceBindingRequestSpec) DeepCopyInto(out *ServiceBindingRequestSpec } } out.BackingServiceSelector = in.BackingServiceSelector + if in.BackingServiceSelectors != nil { + in, out := &in.BackingServiceSelectors, &out.BackingServiceSelectors + *out = make([]BackingServiceSelector, len(*in)) + copy(*out, *in) + } in.ApplicationSelector.DeepCopyInto(&out.ApplicationSelector) return } diff --git a/pkg/apis/apps/v1alpha1/zz_generated.openapi.go b/pkg/apis/apps/v1alpha1/zz_generated.openapi.go index 19d74f4279..65ae3ea5f3 100644 --- a/pkg/apis/apps/v1alpha1/zz_generated.openapi.go +++ b/pkg/apis/apps/v1alpha1/zz_generated.openapi.go @@ -193,6 +193,19 @@ func schema_pkg_apis_apps_v1alpha1_ServiceBindingRequestSpec(ref common.Referenc Ref: ref("github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1.BackingServiceSelector"), }, }, + "backingServiceSelectors": { + SchemaProps: spec.SchemaProps{ + Description: "BackingServiceSelectors is an slice of BackingServiceSelector", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1.BackingServiceSelector"), + }, + }, + }, + }, + }, "applicationSelector": { SchemaProps: spec.SchemaProps{ Description: "ApplicationSelector is used to identify the application connecting to the backing service operator.", @@ -207,7 +220,7 @@ func schema_pkg_apis_apps_v1alpha1_ServiceBindingRequestSpec(ref common.Referenc }, }, }, - Required: []string{"backingServiceSelector", "applicationSelector"}, + Required: []string{"backingServiceSelector", "backingServiceSelectors", "applicationSelector"}, }, }, Dependencies: []string{ diff --git a/pkg/controller/servicebindingrequest/bind_custom_resources.go b/pkg/controller/servicebindingrequest/bind_custom_resources.go index 19469e9983..432ea80868 100644 --- a/pkg/controller/servicebindingrequest/bind_custom_resources.go +++ b/pkg/controller/servicebindingrequest/bind_custom_resources.go @@ -1,11 +1,12 @@ package servicebindingrequest import ( - "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + + "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" ) var ( diff --git a/pkg/controller/servicebindingrequest/binding.go b/pkg/controller/servicebindingrequest/binding.go new file mode 100644 index 0000000000..4a7a429543 --- /dev/null +++ b/pkg/controller/servicebindingrequest/binding.go @@ -0,0 +1,330 @@ +package servicebindingrequest + +import ( + "context" + "errors" + "fmt" + + "gotest.tools/assert/cmp" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" + "github.com/redhat-developer/service-binding-operator/pkg/converter" + "github.com/redhat-developer/service-binding-operator/pkg/log" +) + +const ( + // bindingInProgress binding is in progress + bindingInProgress = "InProgress" + // bindingFail binding has failed + bindingFail = "Fail" + // time in seconds to wait before requeuing requests + requeueAfter int64 = 45 + // Finalizer annotation used in finalizer steps + Finalizer = "finalizer.servicebindingrequest.openshift.io" +) + +// GroupVersion represents the service binding request resource's group version. +var GroupVersion = v1alpha1.SchemeGroupVersion.WithResource(ServiceBindingRequestResource) + +// ServiceBinderOptions is BuildServiceBinder arguments. +type ServiceBinderOptions struct { + Logger *log.Log + DynClient dynamic.Interface + DetectBindingResources bool + EnvVarPrefix string + SBR *v1alpha1.ServiceBindingRequest + Client client.Client +} + +// Valid returns whether the options are valid. +func (o *ServiceBinderOptions) Valid() bool { + return o.SBR != nil && o.DynClient != nil && o.Client != nil +} + +// ServiceBinder manages binding for a Service Binding Request and associated objects. +type ServiceBinder struct { + // Binder is responsible for interacting with the cluster and apply binding related changes. + Binder *Binder + // Data is the collection of all data read by the manager. + Data map[string][]byte + // DynClient is the Kubernetes dynamic client used to interact with the cluster. + DynClient dynamic.Interface + // Logger provides logging facilities for internal components. + Logger *log.Log + // Objects is a list of additional unstructured objects related to the Service Binding Request. + Objects []*unstructured.Unstructured + // SBR is the ServiceBindingRequest associated with binding.. + SBR *v1alpha1.ServiceBindingRequest + // Secret is the Secret associated with the Service Binding Request. + Secret *Secret +} + +// removeStringSlice given a string slice and a string, returns a new slice without given string. +func removeStringSlice(slice []string, str string) []string { + var cleanSlice []string + for _, s := range slice { + if str != s { + cleanSlice = append(cleanSlice, s) + } + } + return cleanSlice +} + +// updateServiceBindingRequest execute update API call on a SBR request. It can return errors from +// this action. +func (b *ServiceBinder) updateServiceBindingRequest( + sbr *v1alpha1.ServiceBindingRequest, +) (*v1alpha1.ServiceBindingRequest, error) { + u, err := converter.ToUnstructured(sbr) + if err != nil { + return nil, err + } + + u, err = b.DynClient. + Resource(GroupVersion). + Namespace(sbr.GetNamespace()). + Update(u, v1.UpdateOptions{}) + + if err != nil { + return nil, err + } + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sbr) + if err != nil { + return nil, err + } + return sbr, nil +} + +// Unbind removes the relationship between a Service Binding Request and its related objects. +func (b *ServiceBinder) Unbind() (reconcile.Result, error) { + logger := b.Logger.WithName("Unbind") + + logger.Info("Cleaning related objects from operator's annotations...") + if err := RemoveSBRAnnotations(b.DynClient, b.Objects); err != nil { + logger.Error(err, "On removing annotations from related objects.") + return RequeueError(err) + } + + if err := b.Binder.Unbind(); err != nil { + logger.Error(err, "On unbinding related objects") + return RequeueError(err) + } + + logger.Info("Deleting intermediary secret") + if err := b.Secret.Delete(); err != nil { + logger.Error(err, "On deleting intermediary secret.") + return RequeueError(err) + } + + logger.Debug("Removing resource finalizers...") + b.SBR.SetFinalizers(removeStringSlice(b.SBR.GetFinalizers(), Finalizer)) + if _, err := b.updateServiceBindingRequest(b.SBR); err != nil { + return NoRequeue(err) + } + + return Done() +} + +// updateStatusServiceBindingRequest updates the Service Binding Request's status field. +func (b *ServiceBinder) updateStatusServiceBindingRequest( + sbr *v1alpha1.ServiceBindingRequest, + sbrStatus *v1alpha1.ServiceBindingRequestStatus, +) ( + *v1alpha1.ServiceBindingRequest, + error, +) { + // do not update if both statuses are equal + if result := cmp.DeepEqual(sbr.Status, sbrStatus)(); result.Success() { + return sbr, nil + } + + // coping status over informed object + sbr.Status = *sbrStatus + + // converting object into unstructured + u, err := converter.ToUnstructured(sbr) + if err != nil { + return nil, err + } + + gr := v1alpha1.SchemeGroupVersion.WithResource(ServiceBindingRequestResource) + resourceClient := b.DynClient.Resource(gr).Namespace(sbr.GetNamespace()) + u, err = resourceClient.UpdateStatus(u, v1.UpdateOptions{}) + if err != nil { + return nil, err + } + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sbr) + if err != nil { + return nil, err + } + return sbr, nil +} + +// onError comprise the update of ServiceBindingRequest status to set error flag, and inspect +// informed error to apply a different behavior for not-founds. +func (b *ServiceBinder) onError( + err error, + sbr *v1alpha1.ServiceBindingRequest, + sbrStatus *v1alpha1.ServiceBindingRequestStatus, + objs []*unstructured.Unstructured, +) (reconcile.Result, error) { + sbrStatus.BindingStatus = bindingFail + + if objs != nil { + sbrStatus.BindingStatus = BindingSuccess + b.setApplicationObjects(sbrStatus, objs) + } + + _, errStatus := b.updateStatusServiceBindingRequest(sbr, sbrStatus) + if errStatus != nil { + return RequeueError(errStatus) + } + + return RequeueOnNotFound(err, requeueAfter) +} + +// Bind configures binding between the Service Binding Request and its related objects. +func (b *ServiceBinder) Bind() (reconcile.Result, error) { + sbrStatus := b.SBR.Status.DeepCopy() + + b.Logger.Info("Saving data on intermediary secret...") + secretObj, err := b.Secret.Commit(b.Data) + if err != nil { + b.Logger.Error(err, "On saving secret data..") + return b.onError(err, b.SBR, sbrStatus, nil) + } + + // update status information + sbrStatus.BindingStatus = bindingInProgress + sbrStatus.Secret = secretObj.GetName() + + updatedObjects, err := b.Binder.Bind() + if err != nil { + b.Logger.Error(err, "On binding application.") + return b.onError(err, b.SBR, sbrStatus, updatedObjects) + } + + // saving on status the list of objects that have been touched + sbrStatus.BindingStatus = BindingSuccess + b.setApplicationObjects(sbrStatus, updatedObjects) + + // annotating objects related to binding + namespacedName := types.NamespacedName{Namespace: b.SBR.GetNamespace(), Name: b.SBR.GetName()} + if err = SetSBRAnnotations(b.DynClient, namespacedName, b.Objects); err != nil { + b.Logger.Error(err, "On setting annotations in related objects.") + return b.onError(err, b.SBR, sbrStatus, updatedObjects) + } + + // updating status of request instance + sbr, err := b.updateStatusServiceBindingRequest(b.SBR, sbrStatus) + if err != nil { + return RequeueOnConflict(err) + } + + // appending finalizer, should be later removed upon resource deletion + sbr.SetFinalizers(append(sbr.GetFinalizers(), Finalizer)) + if _, err = b.updateServiceBindingRequest(sbr); err != nil { + return NoRequeue(err) + } + + b.Logger.Info("All done!") + return Done() +} + +// setApplicationObjects replaces the Status's equivalent field. +func (b *ServiceBinder) setApplicationObjects( + sbrStatus *v1alpha1.ServiceBindingRequestStatus, + objs []*unstructured.Unstructured, +) { + names := []string{} + for _, obj := range objs { + names = append(names, fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())) + } + sbrStatus.ApplicationObjects = names +} + +// buildPlan creates a new plan. +func buildPlan( + ctx context.Context, + dynClient dynamic.Interface, + sbr *v1alpha1.ServiceBindingRequest, +) (*Plan, error) { + planner := NewPlanner(ctx, dynClient, sbr) + return planner.Plan() +} + +// InvalidOptionsErr is returned when ServiceBinderOptions are not valid. +var InvalidOptionsErr = errors.New("invalid options") + +// BuildServiceBinder creates a new binding manager according to options. +func BuildServiceBinder(options *ServiceBinderOptions) (*ServiceBinder, error) { + if !options.Valid() { + return nil, InvalidOptionsErr + } + + // objs groups all extra objects related to the informed SBR + objs := make([]*unstructured.Unstructured, 0, 0) + + // plan is a source of information regarding the binding process + ctx := context.Background() + plan, err := buildPlan(ctx, options.DynClient, options.SBR) + if err != nil { + return nil, err + } + + rs := plan.GetCRs() + // append all SBR related CRs + objs = append(objs, rs...) + + // retriever is responsible for gathering data related to the given plan. + retriever := NewRetriever(options.DynClient, plan, options.EnvVarPrefix) + + // read bindable data from the specified resources + if options.DetectBindingResources { + err := retriever.ReadBindableResourcesData(&plan.SBR, rs) + if err != nil { + return nil, err + } + } + + // read bindable data from the CRDDescription found by the planner + for _, r := range plan.GetRelatedResources() { + err = retriever.ReadCRDDescriptionData(r.CR, r.CRDDescription) + if err != nil { + return nil, err + } + } + + // gather retriever's read data + // TODO: do not return error + retrievedData, err := retriever.Retrieve() + if err != nil { + return nil, err + } + + // gather related secret, again only appending it if there's a value. + secret := NewSecret(options.DynClient, plan) + secretObj, found, err := secret.Get() + if found { + objs = append(objs, secretObj) + } + + return &ServiceBinder{ + Logger: options.Logger, + Binder: NewBinder(ctx, options.Client, options.DynClient, options.SBR, retriever.VolumeKeys), + DynClient: options.DynClient, + SBR: options.SBR, + Objects: objs, + Data: retrievedData, + Secret: secret, + }, nil +} diff --git a/pkg/controller/servicebindingrequest/binding_test.go b/pkg/controller/servicebindingrequest/binding_test.go new file mode 100644 index 0000000000..0f2a622245 --- /dev/null +++ b/pkg/controller/servicebindingrequest/binding_test.go @@ -0,0 +1,289 @@ +package servicebindingrequest + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" + k8stesting "k8s.io/client-go/testing" + + "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" + "github.com/redhat-developer/service-binding-operator/pkg/log" + "github.com/redhat-developer/service-binding-operator/test/mocks" +) + +// TestServiceBinder_Bind exercises scenarios regarding binding SBR and its related resources. +func TestServiceBinder_Bind(t *testing.T) { + // wantedAction represents an action issued by the component that is required to exist after it + // finished the operation + type wantedAction struct { + verb string + resource string + name string + } + + // args are the test arguments + type args struct { + // options inform the test how to build the ServiceBinder. + options *ServiceBinderOptions + // wantBuildErr informs the test an error is wanted at build phase. + wantBuildErr error + // wantErr informs the test an error is wanted at ServiceBinder's bind phase. + wantErr error + // wantedActions informs the test all the actions that should have been issued by + // ServiceBinder. + wantedActions []wantedAction + } + + // assertBind exercises the bind functionality + assertBind := func(args args) func(*testing.T) { + return func(t *testing.T) { + sb, err := BuildServiceBinder(args.options) + if args.wantBuildErr != nil { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + + res, err := sb.Bind() + + if args.wantErr != nil { + require.Error(t, err) + require.Equal(t, args.wantErr, err) + require.Nil(t, res) + } else { + require.NoError(t, err) + require.NotNil(t, res) + } + + // extract actions from the dynamic client, regardless of the bind status; it is expected + // that failures also issue updates for ServiceBindingRequest objects + dynClient, ok := sb.DynClient.(*fake.FakeDynamicClient) + require.True(t, ok) + actions := dynClient.Actions() + require.NotNil(t, actions) + + // regardless of the result, verify the actions expected by the reconciliation + // process have been issued if user has specified wanted actions + if len(args.wantedActions) > 0 { + // proceed to find whether actions match wanted actions + for _, w := range args.wantedActions { + var match bool + // search for each wanted action in the slice of actions issued by ServiceBinder + for _, a := range actions { + // match will be updated in the switch branches below + if match { + break + } + + if a.Matches(w.verb, w.resource) { + // there are several action types; here it is required to 'type + // switch' it and perform the right check. + switch v := a.(type) { + case k8stesting.GetAction: + match = v.GetName() == w.name + case k8stesting.UpdateAction: + obj := v.GetObject() + uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + require.NoError(t, err) + u := &unstructured.Unstructured{Object: uObj} + match = w.name == u.GetName() + } + } + + // short circuit to the end of collected actions if the action has matched. + if match { + break + } + } + require.True(t, match, "expected action %+v not found", w) + } + } + } + } + + matchLabels := map[string]string{ + "connects-to": "database", + } + + f := mocks.NewFake(t, reconcilerName) + f.S.AddKnownTypes(v1alpha1.SchemeGroupVersion, &v1alpha1.ServiceBindingRequest{}) + f.S.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.ConfigMap{}) + + d := f.AddMockedUnstructuredDeployment(reconcilerName, matchLabels) + f.AddMockedUnstructuredDatabaseCRD() + f.AddMockedUnstructuredConfigMap("db1") + f.AddMockedUnstructuredConfigMap("db2") + + // create and munge a Database CR since there's no "Status" field in + // databases.postgres.baiju.dev, requiring us to add the field directly in the unstructured + // object + db1 := f.AddMockedUnstructuredPostgresDatabaseCR("db1") + { + runtimeStatus := map[string]interface{}{ + "dbConfigMap": "db1", + } + err := unstructured.SetNestedMap(db1.Object, runtimeStatus, "status") + require.NoError(t, err) + } + + db2 := f.AddMockedUnstructuredPostgresDatabaseCR("db2") + { + runtimeStatus := map[string]interface{}{ + "dbConfigMap": "db2", + } + err := unstructured.SetNestedMap(db2.Object, runtimeStatus, "status") + require.NoError(t, err) + } + + // create the ServiceBindingRequest + sbrSingleService := &v1alpha1.ServiceBindingRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps.openshift.io/v1alpha1", + Kind: "ServiceBindingRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "single-sbr", + }, + Spec: v1alpha1.ServiceBindingRequestSpec{ + ApplicationSelector: v1alpha1.ApplicationSelector{ + MatchLabels: matchLabels, + Group: d.GetObjectKind().GroupVersionKind().Group, + Version: d.GetObjectKind().GroupVersionKind().Version, + Resource: "deployments", + ResourceRef: d.GetName(), + }, + BackingServiceSelectors: []v1alpha1.BackingServiceSelector{ + { + GroupVersionKind: db1.GetObjectKind().GroupVersionKind(), + ResourceRef: db1.GetName(), + }, + }, + }, + Status: v1alpha1.ServiceBindingRequestStatus{}, + } + f.AddMockResource(sbrSingleService) + + // create the ServiceBindingRequest + sbrMultipleServices := &v1alpha1.ServiceBindingRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps.openshift.io/v1alpha1", + Kind: "ServiceBindingRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "multiple-sbr", + }, + Spec: v1alpha1.ServiceBindingRequestSpec{ + ApplicationSelector: v1alpha1.ApplicationSelector{ + MatchLabels: matchLabels, + Group: d.GetObjectKind().GroupVersionKind().Group, + Version: d.GetObjectKind().GroupVersionKind().Version, + Resource: "deployments", + ResourceRef: d.GetName(), + }, + BackingServiceSelectors: []v1alpha1.BackingServiceSelector{ + { + GroupVersionKind: db1.GetObjectKind().GroupVersionKind(), + ResourceRef: db1.GetName(), + }, + { + GroupVersionKind: db2.GetObjectKind().GroupVersionKind(), + ResourceRef: "db2", + }, + }, + }, + Status: v1alpha1.ServiceBindingRequestStatus{}, + } + f.AddMockResource(sbrMultipleServices) + + logger := log.NewLog("service-binder") + t.Run("single bind golden path", assertBind(args{ + options: &ServiceBinderOptions{ + Logger: logger, + DynClient: f.FakeDynClient(), + DetectBindingResources: false, + EnvVarPrefix: "", + SBR: sbrSingleService, + Client: f.FakeClient(), + }, + wantedActions: []wantedAction{ + { + resource: "servicebindingrequests", + verb: "update", + name: sbrSingleService.GetName(), + }, + { + resource: "secrets", + verb: "update", + name: sbrSingleService.GetName(), + }, + { + resource: "databases", + verb: "update", + name: db1.GetName(), + }, + }, + })) + + t.Run("bind with binding resource detection", assertBind(args{ + options: &ServiceBinderOptions{ + Logger: logger, + DynClient: f.FakeDynClient(), + DetectBindingResources: true, + EnvVarPrefix: "", + SBR: sbrSingleService, + Client: f.FakeClient(), + }, + })) + + // Missing SBR returns an InvalidOptionsErr + t.Run("bind missing SBR", assertBind(args{ + options: &ServiceBinderOptions{ + Logger: logger, + DynClient: f.FakeDynClient(), + DetectBindingResources: false, + EnvVarPrefix: "", + SBR: nil, + Client: f.FakeClient(), + }, + wantBuildErr: InvalidOptionsErr, + })) + + t.Run("multiple services bind golden path", assertBind(args{ + options: &ServiceBinderOptions{ + Logger: logger, + DynClient: f.FakeDynClient(), + DetectBindingResources: false, + EnvVarPrefix: "", + SBR: sbrMultipleServices, + Client: f.FakeClient(), + }, + wantedActions: []wantedAction{ + { + resource: "servicebindingrequests", + verb: "update", + name: sbrMultipleServices.GetName(), + }, + { + resource: "secrets", + verb: "update", + name: sbrMultipleServices.GetName(), + }, + { + resource: "databases", + verb: "update", + name: db1.GetName(), + }, + { + resource: "databases", + verb: "update", + name: db2.GetName(), + }, + }, + })) +} diff --git a/pkg/controller/servicebindingrequest/common.go b/pkg/controller/servicebindingrequest/common.go index 5984217936..43f0f54c00 100644 --- a/pkg/controller/servicebindingrequest/common.go +++ b/pkg/controller/servicebindingrequest/common.go @@ -78,14 +78,3 @@ func containsStringSlice(slice []string, str string) bool { } return false } - -// removeStringSlice given a string slice and a string, returns a new slice without given string. -func removeStringSlice(slice []string, str string) []string { - var cleanSlice []string - for _, s := range slice { - if str != s { - cleanSlice = append(cleanSlice, s) - } - } - return cleanSlice -} diff --git a/pkg/controller/servicebindingrequest/plan_decoupled.go b/pkg/controller/servicebindingrequest/plan_decoupled.go new file mode 100644 index 0000000000..9252cfaa3d --- /dev/null +++ b/pkg/controller/servicebindingrequest/plan_decoupled.go @@ -0,0 +1,13 @@ +package servicebindingrequest + +import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + +// GetCRs returns a slice of unstructured CRs contained in the internal related resources collection. +func (p *Plan) GetCRs() []*unstructured.Unstructured { + return p.RelatedResources.GetCRs() +} + +// GetRelatedResources returns the collection of related resources enumerated in the plan. +func (p *Plan) GetRelatedResources() RelatedResources { + return p.RelatedResources +} diff --git a/pkg/controller/servicebindingrequest/planner.go b/pkg/controller/servicebindingrequest/planner.go index 3766cfc165..ac2bb2b61a 100644 --- a/pkg/controller/servicebindingrequest/planner.go +++ b/pkg/controller/servicebindingrequest/planner.go @@ -2,9 +2,7 @@ package servicebindingrequest import ( "context" - "strings" - olmv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -30,86 +28,83 @@ type Planner struct { // Plan outcome, after executing planner. type Plan struct { - Ns string // namespace name - Name string // plan name, same than ServiceBindingRequest - CRDDescription *olmv1alpha1.CRDDescription // custom resource definition description - CR *unstructured.Unstructured // custom resource object - SBR v1alpha1.ServiceBindingRequest // service binding request + Ns string // namespace name + Name string // plan name, same than ServiceBindingRequest + SBR v1alpha1.ServiceBindingRequest // service binding request + RelatedResources RelatedResources // CR and CRDDescription pairs SBR related } // searchCR based on a CustomResourceDefinitionDescription and name, search for the object. -func (p *Planner) searchCR() (*unstructured.Unstructured, error) { - bss := p.sbr.Spec.BackingServiceSelector - gvk := schema.GroupVersionKind{Group: bss.Group, Version: bss.Version, Kind: bss.Kind} - gvr, _ := meta.UnsafeGuessKindToResource(gvk) - opts := metav1.GetOptions{} - - log := p.logger.WithValues("CR.GVK", gvk.String(), "CR.GVR", gvr.String()) - log.Debug("Searching for CR instance...") - - cr, err := p.client.Resource(gvr).Namespace(p.sbr.GetNamespace()).Get(bss.ResourceRef, opts) - - if err != nil { - log.Info("during reading CR") - return nil, err - } +func (p *Planner) searchCR(namespace string, selector v1alpha1.BackingServiceSelector) (*unstructured.Unstructured, error) { + // gvr is the plural guessed resource for the given selector + gvr, _ := meta.UnsafeGuessKindToResource(selector.GroupVersionKind) + // delegate the search selector's namespaced resource client + return p.client.Resource(gvr).Namespace(namespace).Get(selector.ResourceRef, metav1.GetOptions{}) +} - log.Debug("Found target CR!", "CR.Name", cr.GetName()) - return cr, nil +// CRDGVR is the plural GVR for Kubernetes CRDs. +var CRDGVR = schema.GroupVersionResource{ + Group: "apiextensions.k8s.io", + Version: "v1beta1", + Resource: "customresourcedefinitions", } -// searchCRD based on a CustomResourceDefinitionDescription and name, search for the object. -func (p *Planner) searchCRD() (*unstructured.Unstructured, error) { - bss := p.sbr.Spec.BackingServiceSelector - gvk := schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"} +// searchCRD returns the CRD related to the gvk. +func (p *Planner) searchCRD(gvk schema.GroupVersionKind) (*unstructured.Unstructured, error) { + // gvr is the plural guessed resource for the given GVK gvr, _ := meta.UnsafeGuessKindToResource(gvk) - opts := metav1.GetOptions{} - - logger := p.logger.WithValues("CR.GVK", gvk.String(), "CR.GVR", gvr.String(), "Kind", bss.Kind) - logger.Info("Searching for CRD instance...") - - // TODO: This hack should be removed! Probably the name should be prompted from user through SBR CR. - name := strings.ToLower(bss.Kind) + "s." + bss.Group - crd, err := p.client.Resource(gvr).Get(name, opts) - - if err != nil { - logger.Info("during reading CRD") - return nil, err - } - - logger.WithValues("CR.Name", crd.GetName()).Info("Found target CR!") - return crd, nil + // crdName is the string'fied GroupResource, e.g. "deployments.apps" + crdName := gvr.GroupResource().String() + // delegate the search to the CustomResourceDefinition resource client + return p.client.Resource(CRDGVR).Get(crdName, metav1.GetOptions{}) } // Plan by retrieving the necessary resources related to binding a service backend. func (p *Planner) Plan() (*Plan, error) { - bss := p.sbr.Spec.BackingServiceSelector - gvk := schema.GroupVersionKind{Group: bss.Group, Version: bss.Version, Kind: bss.Kind} - olm := NewOLM(p.client, p.sbr.GetNamespace()) - crd, err := p.searchCRD() - if err != nil { - return nil, err - } - - p.logger.Debug("After search crd", "CRD", crd) - - crdDescription, err := olm.SelectCRDByGVK(gvk, crd) - if err != nil { - return nil, err + ns := p.sbr.GetNamespace() + selectors := append([]v1alpha1.BackingServiceSelector{}, p.sbr.Spec.BackingServiceSelectors...) + selector := p.sbr.Spec.BackingServiceSelector + if len(selector.ResourceRef) > 0 { + selectors = append(selectors, selector) } - // retrieve the CR based on kind, api-version and name - cr, err := p.searchCR() - if err != nil { - return nil, err + relatedResources := make([]*RelatedResource, 0, 0) + for _, s := range selectors { + bssGVK := s.GroupVersionKind + + // resolve the CRD using the service's GVK + crd, err := p.searchCRD(bssGVK) + if err != nil { + return nil, err + } + p.logger.Debug("Resolved CRD", "CRD", crd) + + // resolve the CRDDescription based on the service's GVK and the resolved CRD + olm := NewOLM(p.client, ns) + crdDescription, err := olm.SelectCRDByGVK(bssGVK, crd) + if err != nil { + return nil, err + } + p.logger.Debug("Resolved CRDDescription", "CRDDescription", crdDescription) + + cr, err := p.searchCR(ns, s) + if err != nil { + return nil, err + } + + r := &RelatedResource{ + CRDDescription: crdDescription, + CR: cr, + } + relatedResources = append(relatedResources, r) + p.logger.Debug("Resolved related resource", "RelatedResource", r) } return &Plan{ - Ns: p.sbr.GetNamespace(), - Name: p.sbr.GetName(), - CRDDescription: crdDescription, - CR: cr, - SBR: *p.sbr, + Name: p.sbr.GetName(), + Ns: ns, + RelatedResources: relatedResources, + SBR: *p.sbr, }, nil } diff --git a/pkg/controller/servicebindingrequest/planner_test.go b/pkg/controller/servicebindingrequest/planner_test.go index cdc0c21f31..cea2693d66 100644 --- a/pkg/controller/servicebindingrequest/planner_test.go +++ b/pkg/controller/servicebindingrequest/planner_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" "github.com/redhat-developer/service-binding-operator/test/mocks" ) @@ -26,6 +27,9 @@ func TestPlannerNew(t *testing.T) { } f := mocks.NewFake(t, ns) sbr := f.AddMockedServiceBindingRequest(name, resourceRef, "", deploymentsGVR, matchLabels) + sbr.Spec.BackingServiceSelectors = []v1alpha1.BackingServiceSelector{ + sbr.Spec.BackingServiceSelector, + } f.AddMockedUnstructuredCSV("cluster-service-version") f.AddMockedDatabaseCR(resourceRef) f.AddMockedUnstructuredDatabaseCRD() @@ -34,7 +38,7 @@ func TestPlannerNew(t *testing.T) { require.NotNil(t, planner) t.Run("searchCR", func(t *testing.T) { - cr, err := planner.searchCR() + cr, err := planner.searchCR(ns, sbr.Spec.BackingServiceSelector) require.NoError(t, err) require.NotNil(t, cr) @@ -45,8 +49,7 @@ func TestPlannerNew(t *testing.T) { require.NoError(t, err) require.NotNil(t, plan) - require.NotNil(t, plan.CRDDescription) - require.NotNil(t, plan.CR) + require.NotEmpty(t, plan.RelatedResources) require.Equal(t, ns, plan.Ns) require.Equal(t, name, plan.Name) }) @@ -63,12 +66,13 @@ func TestPlannerAnnotation(t *testing.T) { f := mocks.NewFake(t, ns) sbr := f.AddMockedServiceBindingRequest(name, resourceRef, "", deploymentsGVR, matchLabels) f.AddMockedUnstructuredDatabaseCRD() + cr := f.AddMockedDatabaseCR("database") planner = NewPlanner(context.TODO(), f.FakeDynClient(), sbr) require.NotNil(t, planner) t.Run("searchCRD", func(t *testing.T) { - crd, err := planner.searchCRD() + crd, err := planner.searchCRD(cr.GetObjectKind().GroupVersionKind()) require.NoError(t, err) require.NotNil(t, crd) diff --git a/pkg/controller/servicebindingrequest/reconciler.go b/pkg/controller/servicebindingrequest/reconciler.go index 3b35909e9c..e29a50ffad 100644 --- a/pkg/controller/servicebindingrequest/reconciler.go +++ b/pkg/controller/servicebindingrequest/reconciler.go @@ -1,13 +1,9 @@ package servicebindingrequest import ( - "context" - "errors" "fmt" - "gotest.tools/assert/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" @@ -15,7 +11,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" - "github.com/redhat-developer/service-binding-operator/pkg/converter" "github.com/redhat-developer/service-binding-operator/pkg/log" ) @@ -27,44 +22,20 @@ type Reconciler struct { } const ( - // bindingInProgress binding is in progress - bindingInProgress = "InProgress" - // BindingSuccess binding has succeeded BindingSuccess = "Success" - // bindingFail binding has failed - bindingFail = "Fail" - // time in seconds to wait before requeuing requests - requeueAfter int64 = 45 - // sbrFinalizer annotation used in finalizer steps - sbrFinalizer = "finalizer.servicebindingrequest.openshift.io" ) // reconcilerLog local logger instance var reconcilerLog = log.NewLog("reconciler") -// setSecretName update the CR status field to "in progress", and setting secret name. -func (r *Reconciler) setSecretName(sbrStatus *v1alpha1.ServiceBindingRequestStatus, name string) { - sbrStatus.BindingStatus = bindingInProgress - sbrStatus.Secret = name -} - -// setStatus update the CR status field. -func (r *Reconciler) setStatus(sbrStatus *v1alpha1.ServiceBindingRequestStatus, status string) { - sbrStatus.BindingStatus = status -} - -// setApplicationObjects set the ApplicationObject status field, and also set the overall status as -// success, since it was able to bind applications. -func (r *Reconciler) setApplicationObjects( - sbrStatus *v1alpha1.ServiceBindingRequestStatus, - objs []*unstructured.Unstructured, -) { - names := []string{} - for _, obj := range objs { - names = append(names, fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())) +// validateServiceBindingRequest check for unsupported settings in SBR. +func (r *Reconciler) validateServiceBindingRequest(sbr *v1alpha1.ServiceBindingRequest) error { + // check if application ResourceRef and MatchLabels, one of them is required. + if sbr.Spec.ApplicationSelector.ResourceRef == "" && + sbr.Spec.ApplicationSelector.MatchLabels == nil { + return fmt.Errorf("both ResourceRef and MatchLabels are not set") } - sbrStatus.BindingStatus = BindingSuccess - sbrStatus.ApplicationObjects = names + return nil } // getServiceBindingRequest retrieve the SBR object based on namespaced-name. @@ -77,158 +48,35 @@ func (r *Reconciler) getServiceBindingRequest( if err != nil { return nil, err } - sbr := &v1alpha1.ServiceBindingRequest{} - err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sbr) - if err != nil { - return nil, err - } - return sbr, nil -} - -// updateStatusServiceBindingRequest update the status field of a ServiceBindingRequest. -func (r *Reconciler) updateStatusServiceBindingRequest( - sbr *v1alpha1.ServiceBindingRequest, - sbrStatus *v1alpha1.ServiceBindingRequestStatus, -) (*v1alpha1.ServiceBindingRequest, error) { - // do not update if both statuses are equal - if result := cmp.DeepEqual(sbr.Status, sbrStatus)(); result.Success() { - return sbr, nil - } - - // coping status over informed object - sbr.Status = *sbrStatus - - // converting object into unstructured - u, err := converter.ToUnstructured(sbr) - if err != nil { - return nil, err - } - - gr := v1alpha1.SchemeGroupVersion.WithResource(ServiceBindingRequestResource) - resourceClient := r.dynClient.Resource(gr).Namespace(sbr.GetNamespace()) - u, err = resourceClient.UpdateStatus(u, metav1.UpdateOptions{}) - if err != nil { - return nil, err - } + sbr := &v1alpha1.ServiceBindingRequest{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sbr) if err != nil { return nil, err } - return sbr, nil -} - -// updateServiceBindingRequest execute update API call on a SBR request. It can return errors from -// this action. -func (r *Reconciler) updateServiceBindingRequest( - sbr *v1alpha1.ServiceBindingRequest, -) (*v1alpha1.ServiceBindingRequest, error) { - u, err := converter.ToUnstructured(sbr) - if err != nil { - return nil, err - } - gr := v1alpha1.SchemeGroupVersion.WithResource(ServiceBindingRequestResource) - resourceClient := r.dynClient.Resource(gr).Namespace(sbr.GetNamespace()) - u, err = resourceClient.Update(u, metav1.UpdateOptions{}) - if err != nil { - return nil, err - } - err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sbr) - if err != nil { + if err = r.validateServiceBindingRequest(sbr); err != nil { return nil, err } return sbr, nil } -// onError comprise the update of ServiceBindingRequest status to set error flag, and inspect -// informed error to apply a different behavior for not-founds. -func (r *Reconciler) onError( - err error, - sbr *v1alpha1.ServiceBindingRequest, - sbrStatus *v1alpha1.ServiceBindingRequestStatus, - objs []*unstructured.Unstructured, -) (reconcile.Result, error) { - // settting overall status to failed - r.setStatus(sbrStatus, bindingFail) - // - if objs != nil { - r.setApplicationObjects(sbrStatus, objs) - } - _, errStatus := r.updateStatusServiceBindingRequest(sbr, sbrStatus) - if errStatus != nil { - return RequeueError(errStatus) - } - return RequeueOnNotFound(err, requeueAfter) -} - -// checkSBR checks the Service Binding Request -func checkSBR(sbr *v1alpha1.ServiceBindingRequest, log *log.Log) error { - // Check if application ResourceRef is present - if sbr.Spec.ApplicationSelector.ResourceRef == "" { - log.Debug("Spec.ApplicationSelector.ResourceRef not found") - - // Check if MatchLabels is present - if sbr.Spec.ApplicationSelector.MatchLabels == nil { - err := errors.New("NotFoundError") - log.Error(err, "Spec.ApplicationSelector.MatchLabels not found") - return err - } - } - return nil -} - // unbind removes the relationship between the given sbr and the manifests the operator has // previously modified. This process also deletes any manifests created to support the binding // functionality, such as ConfigMaps and Secrets. -func (r *Reconciler) unbind( - logger *log.Log, - binder *Binder, - secret *Secret, - sbr *v1alpha1.ServiceBindingRequest, - objectsToAnnotate []*unstructured.Unstructured, -) (reconcile.Result, error) { +func (r *Reconciler) unbind(logger *log.Log, bm *ServiceBinder) (reconcile.Result, error) { logger = logger.WithName("unbind") // when finalizer is not found anymore, it can be safely removed - if !containsStringSlice(sbr.GetFinalizers(), sbrFinalizer) { + if !containsStringSlice(bm.SBR.GetFinalizers(), Finalizer) { logger.Info("Resource can be safely deleted!") return Done() } - logger.Debug("Reading intermediary secret before deletion.") - secretObj, err := secret.Get() - if err != nil { - logger.Error(err, "On reading intermediary secret.") - return RequeueError(err) - } - - // adding secret on list of objects, to remove annotations from secret before deletion - objectsToAnnotate = append(objectsToAnnotate, secretObj) - - logger.Info("Cleaning related objects from operator's annotations...") - if err = RemoveSBRAnnotations(r.dynClient, objectsToAnnotate); err != nil { - logger.Error(err, "On removing annotations from related objects.") - return RequeueError(err) - } - logger.Info("Executing unbinding steps...") - err = binder.Unbind() - if err != nil { + if res, err := bm.Unbind(); err != nil { logger.Error(err, "On unbinding application.") - return RequeueError(err) - } - - logger.Info("Deleting intermediary secret...") - if err = secret.Delete(); err != nil { - logger.Error(err, "On deleting intermediary secret.") - return RequeueError(err) - } - - logger.Debug("Removing resource finalizers...") - sbr.SetFinalizers(removeStringSlice(sbr.GetFinalizers(), sbrFinalizer)) - if _, err = r.updateServiceBindingRequest(sbr); err != nil { - return NoRequeue(err) + return res, err } logger.Debug("Deletion done!") @@ -239,57 +87,16 @@ func (r *Reconciler) unbind( // in the common parts of the reconciler, and execute the final binding steps. func (r *Reconciler) bind( logger *log.Log, - binder *Binder, - secret *Secret, - retrievedData map[string][]byte, - sbr *v1alpha1.ServiceBindingRequest, + bm *ServiceBinder, sbrStatus *v1alpha1.ServiceBindingRequestStatus, - objectsToAnnotate []*unstructured.Unstructured, -) (reconcile.Result, error) { +) ( + reconcile.Result, + error, +) { logger = logger.WithName("bind") - logger.Info("Saving data on intermediary secret...") - secretObj, err := secret.Commit(retrievedData) - if err != nil { - logger.Error(err, "On saving secret data..") - return r.onError(err, sbr, sbrStatus, nil) - } - - // appending intermediary secret in the list of objects to annotate - objectsToAnnotate = append(objectsToAnnotate, secretObj) - // making sure secret name is part of status - r.setSecretName(sbrStatus, secretObj.GetName()) - logger.Info("Binding applications with intermediary secret...") - updatedObjects, err := binder.Bind() - if err != nil { - logger.Error(err, "On binding application.") - return r.onError(err, sbr, sbrStatus, updatedObjects) - } - - // saving on status the list of objects that have been touched - r.setApplicationObjects(sbrStatus, updatedObjects) - namespacedName := types.NamespacedName{Namespace: sbr.GetNamespace(), Name: sbr.GetName()} - - // annotating objects related to binding - if err = SetSBRAnnotations(r.dynClient, namespacedName, objectsToAnnotate); err != nil { - logger.Error(err, "On setting annotations in related objects.") - return r.onError(err, sbr, sbrStatus, updatedObjects) - } - - // updating status of request instance - if sbr, err = r.updateStatusServiceBindingRequest(sbr, sbrStatus); err != nil { - return RequeueOnConflict(err) - } - - // appending finalizer, should be later removed upon resource deletion - sbr.SetFinalizers(append(sbr.GetFinalizers(), sbrFinalizer)) - if _, err = r.updateServiceBindingRequest(sbr); err != nil { - return NoRequeue(err) - } - - logger.Info("All done!") - return Done() + return bm.Bind() } // Reconcile a ServiceBindingRequest by the following steps: @@ -303,9 +110,6 @@ func (r *Reconciler) bind( // 4. Search applications that are interested to bind with given service, by inspecting labels. The // Deployment (and other kinds) will be updated in "spec" level. func (r *Reconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { - ctx := context.TODO() - objectsToAnnotate := []*unstructured.Unstructured{} - logger := reconcilerLog.WithValues( "Request.Namespace", request.Namespace, "Request.Name", request.Name, @@ -313,7 +117,7 @@ func (r *Reconciler) Reconcile(request reconcile.Request) (reconcile.Result, err logger.Info("Reconciling ServiceBindingRequest...") - // fetch the ServiceBindingRequest instance + // fetch and validate namespaced ServiceBindingRequest instance sbr, err := r.getServiceBindingRequest(request.NamespacedName) if err != nil { logger.Error(err, "On retrieving service-binding-request instance.") @@ -326,53 +130,26 @@ func (r *Reconciler) Reconcile(request reconcile.Request) (reconcile.Result, err // splitting instance from it's status sbrStatus := &sbr.Status - // check Service Binding Request - if err = checkSBR(sbr, logger); err != nil { - return RequeueError(err) - } - - // - // Planing changes - // - - logger.Debug("Creating a plan based on OLM and CRD.") - planner := NewPlanner(ctx, r.dynClient, sbr) - plan, err := planner.Plan() - if err != nil { - logger.Error(err, "On creating a plan to bind applications.") - return r.onError(err, sbr, sbrStatus, nil) + options := &ServiceBinderOptions{ + Client: r.client, + DynClient: r.dynClient, + DetectBindingResources: sbr.Spec.DetectBindingResources, + EnvVarPrefix: sbr.Spec.EnvVarPrefix, + SBR: sbr, + Logger: logger, } - // storing CR in objects to annotate - objectsToAnnotate = append(objectsToAnnotate, plan.CR) - - // - // Retrieving data - // - - logger.Debug("Retrieving data to create intermediate secret.") - retriever := NewRetriever(r.dynClient, plan, sbr.Spec.EnvVarPrefix) - retrievedData, err := retriever.Retrieve() + bm, err := BuildServiceBinder(options) if err != nil { - logger.Error(err, "On retrieving binding data.") - return r.onError(err, sbr, sbrStatus, nil) + logger.Error(err, "Creating binding context") + return RequeueError(err) } - // storing objects used in Retriever - objectsToAnnotate = append(objectsToAnnotate, retriever.Objects...) - - // - // Binding and unbind intermediary secret - // - - secret := NewSecret(r.dynClient, plan) - binder := NewBinder(ctx, r.client, r.dynClient, sbr, retriever.volumeKeys) - if sbr.GetDeletionTimestamp() != nil { logger.Info("Resource is marked for deletion...") - return r.unbind(logger, binder, secret, sbr, objectsToAnnotate) + return r.unbind(logger, bm) } logger.Info("Starting the bind of application(s) with backing service...") - return r.bind(logger, binder, secret, retrievedData, sbr, sbrStatus, objectsToAnnotate) + return r.bind(logger, bm, sbrStatus) } diff --git a/pkg/controller/servicebindingrequest/reconciler_test.go b/pkg/controller/servicebindingrequest/reconciler_test.go index dc52b13872..e6c81163c0 100644 --- a/pkg/controller/servicebindingrequest/reconciler_test.go +++ b/pkg/controller/servicebindingrequest/reconciler_test.go @@ -46,7 +46,9 @@ func TestReconcilerReconcileError(t *testing.T) { "environment": "reconciler", } f := mocks.NewFake(t, reconcilerNs) + f.AddMockedUnstructuredDatabaseCRD() f.AddMockedUnstructuredServiceBindingRequest(reconcilerName, backingServiceResourceRef, "", deploymentsGVR, matchLabels) + f.AddMockedUnstructuredPostgresDatabaseCR("test-using-secret") fakeClient := f.FakeClient() fakeDynClient := f.FakeDynClient() @@ -54,15 +56,10 @@ func TestReconcilerReconcileError(t *testing.T) { res, err := reconciler.Reconcile(reconcileRequest()) - // FIXME: decide this test's fate - // I'm not very sure what this test was about, but in the case the SBR definition contains - // references to objects that do not exist, the reconciliation process is supposed to be - // successful. Commented below was the original test. - // - // require.Error(t, err) - // require.True(t, res.Requeue) - - require.NoError(t, err) + // currently this test passes because annotations present in the Databases CRD being currently + // used doesn't have a 'status' field in its definition; once it does and this code is updated ( + // since the Postgres CRD is being imported to be used in tests) this test will fail. + require.Error(t, err) require.True(t, res.Requeue) } diff --git a/pkg/controller/servicebindingrequest/related_resources.go b/pkg/controller/servicebindingrequest/related_resources.go new file mode 100644 index 0000000000..3f0dfb7210 --- /dev/null +++ b/pkg/controller/servicebindingrequest/related_resources.go @@ -0,0 +1,24 @@ +package servicebindingrequest + +import ( + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// RelatedResource represents a SBR related resource, composed by its CR and CRDDescription. +type RelatedResource struct { + CRDDescription *v1alpha1.CRDDescription + CR *unstructured.Unstructured +} + +// RelatedResources contains a collection of SBR related resources. +type RelatedResources []*RelatedResource + +// GetCRs returns a slice of unstructured CRs contained in the collection. +func (rr RelatedResources) GetCRs() []*unstructured.Unstructured { + var crs []*unstructured.Unstructured + for _, r := range rr { + crs = append(crs, r.CR) + } + return crs +} diff --git a/pkg/controller/servicebindingrequest/retrieve_decoupled.go b/pkg/controller/servicebindingrequest/retrieve_decoupled.go new file mode 100644 index 0000000000..ddf2c8d450 --- /dev/null +++ b/pkg/controller/servicebindingrequest/retrieve_decoupled.go @@ -0,0 +1,61 @@ +package servicebindingrequest + +import ( + "fmt" + + v1alpha12 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" +) + +// Retrieve returns the data read from related resources (see ReadBindableResourcesData and +// ReadCRDDescriptionData). +func (r *Retriever) Retrieve() (map[string][]byte, error) { + return r.data, nil +} + +// ReadBindableResourcesData reads all related resources of a given sbr +func (r *Retriever) ReadBindableResourcesData( + sbr *v1alpha1.ServiceBindingRequest, + crs []*unstructured.Unstructured, +) error { + r.logger.Info("Detecting extra resources for binding...") + for _, cr := range crs { + b := NewDetectBindableResources(sbr, cr, []schema.GroupVersionResource{ + {Group: "", Version: "v1", Resource: "configmaps"}, + {Group: "", Version: "v1", Resource: "services"}, + {Group: "route.openshift.io", Version: "v1", Resource: "routes"}, + }, r.client) + + vals, err := b.GetBindableVariables() + if err != nil { + return err + } + for k, v := range vals { + r.store(cr, k, []byte(fmt.Sprintf("%v", v))) + } + } + + return nil +} + +// ReadCRDDescriptionData reads data related to given crdDescription +func (r *Retriever) ReadCRDDescriptionData(u *unstructured.Unstructured, crdDescription *v1alpha12.CRDDescription) error { + r.logger.Info("Looking for spec-descriptors in 'spec'...") + for _, specDescriptor := range crdDescription.SpecDescriptors { + if err := r.read(u, "spec", specDescriptor.Path, specDescriptor.XDescriptors); err != nil { + return err + } + } + + r.logger.Info("Looking for status-descriptors in 'status'...") + for _, statusDescriptor := range crdDescription.StatusDescriptors { + if err := r.read(u, "status", statusDescriptor.Path, statusDescriptor.XDescriptors); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/controller/servicebindingrequest/retriever.go b/pkg/controller/servicebindingrequest/retriever.go index ae897b80ac..193c1cb597 100644 --- a/pkg/controller/servicebindingrequest/retriever.go +++ b/pkg/controller/servicebindingrequest/retriever.go @@ -20,7 +20,7 @@ type Retriever struct { Objects []*unstructured.Unstructured // list of objects employed client dynamic.Interface // Kubernetes API client plan *Plan // plan instance - volumeKeys []string // list of keys found + VolumeKeys []string // list of keys found bindingPrefix string // prefix for variable names cache map[string]interface{} // store visited paths } @@ -51,9 +51,9 @@ func (r *Retriever) getNestedValue(key string, sectionMap interface{}) (string, } // getCRKey retrieve key in section from CR object, part of the "plan" instance. -func (r *Retriever) getCRKey(section string, key string) (string, interface{}, error) { - obj := r.plan.CR.Object - objName := r.plan.CR.GetName() +func (r *Retriever) getCRKey(u *unstructured.Unstructured, section string, key string) (string, interface{}, error) { + obj := u.Object + objName := u.GetName() log := r.logger.WithValues("CR.Name", objName, "CR.section", section, "CR.key", key) log.Debug("Reading CR attributes...") @@ -76,7 +76,7 @@ func (r *Retriever) getCRKey(section string, key string) (string, interface{}, e // read attributes from CR, where place means which top level key name contains the "path" actual // value, and parsing x-descriptors in order to either directly read CR data, or read items from // a secret. -func (r *Retriever) read(place, path string, xDescriptors []string) error { +func (r *Retriever) read(cr *unstructured.Unstructured, place, path string, xDescriptors []string) error { log := r.logger.WithValues( "CR.Section", place, "CRDDescription.Path", path, @@ -89,13 +89,13 @@ func (r *Retriever) read(place, path string, xDescriptors []string) error { // holds the configMap name and items configMaps := make(map[string][]string) - pathValue, _, err := r.getCRKey(place, path) + pathValue, _, err := r.getCRKey(cr, place, path) + if err != nil { + return err + } for _, xDescriptor := range xDescriptors { log = log.WithValues("CRDDescription.xDescriptor", xDescriptor, "cache", r.cache) log.Debug("Inspecting xDescriptor...") - if err != nil { - return err - } if _, ok := r.cache[place].(map[string]interface{}); !ok { r.cache[place] = make(map[string]interface{}) @@ -112,9 +112,9 @@ func (r *Retriever) read(place, path string, xDescriptors []string) error { } else if strings.HasPrefix(xDescriptor, volumeMountSecretPrefix) { secrets[pathValue] = append(secrets[pathValue], r.extractSecretItemName(xDescriptor)) r.markVisitedPaths(r.extractSecretItemName(xDescriptor), pathValue, place) - r.volumeKeys = append(r.volumeKeys, pathValue) + r.VolumeKeys = append(r.VolumeKeys, pathValue) } else if strings.HasPrefix(xDescriptor, attributePrefix) { - r.store(path, []byte(pathValue)) + r.store(cr, path, []byte(pathValue)) } else { log.Debug("Defaulting....") } @@ -122,14 +122,14 @@ func (r *Retriever) read(place, path string, xDescriptors []string) error { for name, items := range secrets { // loading secret items all-at-once - err := r.readSecret(name, items, place, path) + err := r.readSecret(cr, name, items, place, path) if err != nil { return err } } for name, items := range configMaps { // add the function readConfigMap - err := r.readConfigMap(name, items, place, path) + err := r.readConfigMap(cr, name, items, place, path) if err != nil { return err } @@ -167,21 +167,17 @@ func (r *Retriever) markVisitedPaths(name, keyPath, fromPath string) { // readSecret based in secret name and list of items, read a secret from the same namespace informed // in plan instance. -func (r *Retriever) readSecret( - name string, - items []string, - fromPath string, - path string) error { +func (r *Retriever) readSecret(cr *unstructured.Unstructured, name string, items []string, fromPath string, path string) error { log := r.logger.WithValues("Secret.Name", name, "Secret.Items", items) log.Debug("Reading secret items...") gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"} - u, err := r.client.Resource(gvr).Namespace(r.plan.Ns).Get(name, metav1.GetOptions{}) + secret, err := r.client.Resource(gvr).Namespace(r.plan.Ns).Get(name, metav1.GetOptions{}) if err != nil { return err } - data, exists, err := unstructured.NestedMap(u.Object, []string{"data"}...) + data, exists, err := unstructured.NestedMap(secret.Object, []string{"data"}...) if err != nil { return err } @@ -201,21 +197,17 @@ func (r *Retriever) readSecret( // update cache after reading configmap/secret in cache r.cache[fromPath].(map[string]interface{})[path].(map[string]interface{})[k] = string(data) // making sure key name has a secret reference - r.store(fmt.Sprintf("configMap_%s", k), data) - r.store(fmt.Sprintf("secret_%s", k), data) + r.store(cr, fmt.Sprintf("configMap_%s", k), data) + r.store(cr, fmt.Sprintf("secret_%s", k), data) } - r.Objects = append(r.Objects, u) + r.Objects = append(r.Objects, secret) return nil } // readConfigMap based in configMap name and list of items, read a configMap from the same namespace informed // in plan instance. -func (r *Retriever) readConfigMap( - name string, - items []string, - fromPath string, - path string) error { +func (r *Retriever) readConfigMap(cr *unstructured.Unstructured, name string, items []string, fromPath string, path string) error { log := r.logger.WithValues("ConfigMap.Name", name, "ConfigMap.Items", items) log.Debug("Reading ConfigMap items...") @@ -244,7 +236,7 @@ func (r *Retriever) readConfigMap( // update cache after reading configmap/secret in cache r.cache[fromPath].(map[string]interface{})[path].(map[string]interface{})[k] = value // making sure key name has a configMap reference - r.store(fmt.Sprintf("configMap_%s", k), []byte(value)) + r.store(cr, fmt.Sprintf("configMap_%s", k), []byte(value)) } r.Objects = append(r.Objects, u) @@ -252,57 +244,18 @@ func (r *Retriever) readConfigMap( } // store key and value, formatting key to look like an environment variable. -func (r *Retriever) store(key string, value []byte) { +func (r *Retriever) store(u *unstructured.Unstructured, key string, value []byte) { key = strings.ReplaceAll(key, ":", "_") key = strings.ReplaceAll(key, ".", "_") if r.bindingPrefix == "" { - key = fmt.Sprintf("%s_%s", r.plan.CR.GetKind(), key) + key = fmt.Sprintf("%s_%s", u.GetKind(), key) } else { - key = fmt.Sprintf("%s_%s_%s", r.bindingPrefix, r.plan.CR.GetKind(), key) + key = fmt.Sprintf("%s_%s_%s", r.bindingPrefix, u.GetKind(), key) } key = strings.ToUpper(key) r.data[key] = value } -// Retrieve loop and read data pointed by the references in plan instance. Also runs through -// "bindable resources", gathering extra data. It can return error on retrieving and reading -// resources. -func (r *Retriever) Retrieve() (map[string][]byte, error) { - var err error - r.logger.Info("Looking for spec-descriptors in 'spec'...") - for _, specDescriptor := range r.plan.CRDDescription.SpecDescriptors { - if err = r.read("spec", specDescriptor.Path, specDescriptor.XDescriptors); err != nil { - return nil, err - } - } - - r.logger.Info("Looking for status-descriptors in 'status'...") - for _, statusDescriptor := range r.plan.CRDDescription.StatusDescriptors { - if err = r.read("status", statusDescriptor.Path, statusDescriptor.XDescriptors); err != nil { - return nil, err - } - } - - if r.plan.SBR.Spec.DetectBindingResources { - r.logger.Info("Detecting extra resources for binding...") - b := NewDetectBindableResources(&r.plan.SBR, r.plan.CR, []schema.GroupVersionResource{ - {Group: "", Version: "v1", Resource: "configmaps"}, - {Group: "", Version: "v1", Resource: "services"}, - {Group: "route.openshift.io", Version: "v1", Resource: "routes"}, - }, r.client) - - vals, err := b.GetBindableVariables() - if err != nil { - return nil, err - } - for k, v := range vals { - r.store(k, []byte(fmt.Sprintf("%v", v))) - } - } - - return r.data, nil -} - // NewRetriever instantiate a new retriever instance. func NewRetriever(client dynamic.Interface, plan *Plan, bindingPrefix string) *Retriever { return &Retriever{ @@ -311,7 +264,7 @@ func NewRetriever(client dynamic.Interface, plan *Plan, bindingPrefix string) *R Objects: []*unstructured.Unstructured{}, client: client, plan: plan, - volumeKeys: []string{}, + VolumeKeys: []string{}, bindingPrefix: bindingPrefix, cache: make(map[string]interface{}), } diff --git a/pkg/controller/servicebindingrequest/retriever_test.go b/pkg/controller/servicebindingrequest/retriever_test.go index 4e06058cae..8ca25c79fc 100644 --- a/pkg/controller/servicebindingrequest/retriever_test.go +++ b/pkg/controller/servicebindingrequest/retriever_test.go @@ -25,14 +25,24 @@ func TestRetriever(t *testing.T) { cr, err := mocks.UnstructuredDatabaseCRMock(ns, crName) require.NoError(t, err) - plan := &Plan{Ns: ns, Name: "retriever", CRDDescription: &crdDescription, CR: cr} + plan := &Plan{ + Ns: ns, + Name: "retriever", + RelatedResources: []*RelatedResource{ + { + CRDDescription: &crdDescription, + CR: cr, + }, + }, + } fakeDynClient := f.FakeDynClient() retriever = NewRetriever(fakeDynClient, plan, "SERVICE_BINDING") require.NotNil(t, retriever) - t.Run("retrive", func(t *testing.T) { + t.Run("retrieve", func(t *testing.T) { + _ = retriever.ReadCRDDescriptionData(cr, &crdDescription) objs, err := retriever.Retrieve() require.NoError(t, err) require.NotEmpty(t, retriever.data) @@ -40,14 +50,14 @@ func TestRetriever(t *testing.T) { }) t.Run("getCRKey", func(t *testing.T) { - imageName, _, err := retriever.getCRKey("spec", "imageName") + imageName, _, err := retriever.getCRKey(cr, "spec", "imageName") require.NoError(t, err) require.Equal(t, "postgres", imageName) }) t.Run("read", func(t *testing.T) { // reading from secret, from status attribute - err := retriever.read("status", "dbCredentials", []string{ + err := retriever.read(cr, "status", "dbCredentials", []string{ "binding:env:object:secret:user", "binding:env:object:secret:password", }) @@ -58,7 +68,7 @@ func TestRetriever(t *testing.T) { require.Contains(t, retriever.data, "SERVICE_BINDING_DATABASE_SECRET_PASSWORD") // reading from spec attribute - err = retriever.read("spec", "image", []string{ + err = retriever.read(cr, "spec", "image", []string{ "binding:env:attribute", }) require.NoError(t, err) @@ -76,7 +86,7 @@ func TestRetriever(t *testing.T) { t.Run("readSecret", func(t *testing.T) { retriever.data = make(map[string][]byte) - err := retriever.readSecret("db-credentials", []string{"user", "password"}, "spec", "dbConfigMap") + err := retriever.readSecret(cr, "db-credentials", []string{"user", "password"}, "spec", "dbConfigMap") require.NoError(t, err) require.Contains(t, retriever.data, "SERVICE_BINDING_DATABASE_SECRET_USER") @@ -84,7 +94,7 @@ func TestRetriever(t *testing.T) { }) t.Run("store", func(t *testing.T) { - retriever.store("test", []byte("test")) + retriever.store(cr, "test", []byte("test")) require.Contains(t, retriever.data, "SERVICE_BINDING_DATABASE_TEST") require.Equal(t, []byte("test"), retriever.data["SERVICE_BINDING_DATABASE_TEST"]) }) @@ -94,7 +104,7 @@ func TestRetriever(t *testing.T) { require.NotNil(t, retriever) retriever.data = make(map[string][]byte) - err := retriever.readSecret("db-credentials", []string{"user", "password"}, "spec", "dbConfigMap") + err := retriever.readSecret(cr, "db-credentials", []string{"user", "password"}, "spec", "dbConfigMap") require.NoError(t, err) require.Contains(t, retriever.data, "DATABASE_SECRET_USER") @@ -117,7 +127,16 @@ func TestRetrieverWithNestedCRKey(t *testing.T) { cr, err := mocks.UnstructuredNestedDatabaseCRMock(ns, crName) require.NoError(t, err) - plan := &Plan{Ns: ns, Name: "retriever", CRDDescription: &crdDescription, CR: cr} + plan := &Plan{ + Ns: ns, + Name: "retriever", + RelatedResources: []*RelatedResource{ + { + CRDDescription: &crdDescription, + CR: cr, + }, + }, + } fakeDynClient := f.FakeDynClient() @@ -125,7 +144,7 @@ func TestRetrieverWithNestedCRKey(t *testing.T) { require.NotNil(t, retriever) t.Run("Second level", func(t *testing.T) { - imageName, _, err := retriever.getCRKey("spec", "image.name") + imageName, _, err := retriever.getCRKey(cr, "spec", "image.name") require.NoError(t, err) require.Equal(t, "postgres", imageName) }) @@ -133,12 +152,12 @@ func TestRetrieverWithNestedCRKey(t *testing.T) { t.Run("Second level error", func(t *testing.T) { // FIXME: if attribute isn't available in CR we would not throw any error. t.Skip() - _, _, err := retriever.getCRKey("spec", "image..name") + _, _, err := retriever.getCRKey(cr, "spec", "image..name") require.NotNil(t, err) }) t.Run("Third level", func(t *testing.T) { - something, _, err := retriever.getCRKey("spec", "image.third.something") + something, _, err := retriever.getCRKey(cr, "spec", "image.third.something") require.NoError(t, err) require.Equal(t, "somevalue", something) }) @@ -154,7 +173,7 @@ func TestRetrieverWithConfigMap(t *testing.T) { f := mocks.NewFake(t, ns) f.AddMockedUnstructuredCSV("csv") - f.AddMockedConfigMap(crName) + f.AddMockedUnstructuredConfigMap(crName) f.AddMockedDatabaseCR(crName) crdDescription := mocks.CRDDescriptionConfigMapMock() @@ -162,7 +181,16 @@ func TestRetrieverWithConfigMap(t *testing.T) { cr, err := mocks.UnstructuredDatabaseConfigMapMock(ns, crName, crName) require.NoError(t, err) - plan := &Plan{Ns: ns, Name: "retriever", CRDDescription: &crdDescription, CR: cr} + plan := &Plan{ + Ns: ns, + Name: "retriever", + RelatedResources: []*RelatedResource{ + { + CRDDescription: &crdDescription, + CR: cr, + }, + }, + } fakeDynClient := f.FakeDynClient() @@ -171,7 +199,7 @@ func TestRetrieverWithConfigMap(t *testing.T) { t.Run("read", func(t *testing.T) { // reading from configMap, from status attribute - err = retriever.read("spec", "dbConfigMap", []string{ + err = retriever.read(cr, "spec", "dbConfigMap", []string{ "binding:env:object:configmap:user", "binding:env:object:configmap:password", }) @@ -190,7 +218,7 @@ func TestRetrieverWithConfigMap(t *testing.T) { t.Run("readConfigMap", func(t *testing.T) { retriever.data = make(map[string][]byte) - err := retriever.readConfigMap(crName, []string{"user", "password"}, "spec", "dbConfigMap") + err := retriever.readConfigMap(cr, crName, []string{"user", "password"}, "spec", "dbConfigMap") require.NoError(t, err) require.Contains(t, retriever.data, ("SERVICE_BINDING_DATABASE_CONFIGMAP_USER")) @@ -214,7 +242,16 @@ func TestCustomEnvParser(t *testing.T) { cr, err := mocks.UnstructuredDatabaseCRMock(ns, crName) require.NoError(t, err) - plan := &Plan{Ns: ns, Name: "retriever", CRDDescription: &crdDescription, CR: cr} + plan := &Plan{ + Ns: ns, + Name: "retriever", + RelatedResources: []*RelatedResource{ + { + CRDDescription: &crdDescription, + CR: cr, + }, + }, + } fakeDynClient := f.FakeDynClient() @@ -222,6 +259,7 @@ func TestCustomEnvParser(t *testing.T) { require.NotNil(t, retriever) t.Run("Should detect custom env values", func(t *testing.T) { + _ = retriever.ReadCRDDescriptionData(cr, &crdDescription) _, err = retriever.Retrieve() require.NoError(t, err) diff --git a/pkg/controller/servicebindingrequest/sbrcontroller_test.go b/pkg/controller/servicebindingrequest/sbrcontroller_test.go index 99ec7b6c6b..2e54d69033 100644 --- a/pkg/controller/servicebindingrequest/sbrcontroller_test.go +++ b/pkg/controller/servicebindingrequest/sbrcontroller_test.go @@ -7,6 +7,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/event" "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" @@ -29,9 +30,11 @@ func TestSBRControllerBuildSBRPredicate(t *testing.T) { sbrA := &v1alpha1.ServiceBindingRequest{ Spec: v1alpha1.ServiceBindingRequestSpec{ BackingServiceSelector: v1alpha1.BackingServiceSelector{ - Group: "test", - Version: "v1alpha1", - Kind: "TestHost", + GroupVersionKind: schema.GroupVersionKind{ + Group: "test", + Version: "v1alpha1", + Kind: "TestHost", + }, ResourceRef: "", }, }, @@ -39,9 +42,11 @@ func TestSBRControllerBuildSBRPredicate(t *testing.T) { sbrB := &v1alpha1.ServiceBindingRequest{ Spec: v1alpha1.ServiceBindingRequestSpec{ BackingServiceSelector: v1alpha1.BackingServiceSelector{ - Group: "test", - Version: "v1", - Kind: "TestHost", + GroupVersionKind: schema.GroupVersionKind{ + Group: "test", + Version: "v1", + Kind: "TestHost", + }, ResourceRef: "", }, }, diff --git a/pkg/controller/servicebindingrequest/secret.go b/pkg/controller/servicebindingrequest/secret.go index 280330f87a..3b08af83c6 100644 --- a/pkg/controller/servicebindingrequest/secret.go +++ b/pkg/controller/servicebindingrequest/secret.go @@ -94,9 +94,13 @@ func (s *Secret) Commit(data map[string][]byte) (*unstructured.Unstructured, err // Get an unstructured object from the secret handled by this component. It can return errors in case // the API server does. -func (s *Secret) Get() (*unstructured.Unstructured, error) { +func (s *Secret) Get() (*unstructured.Unstructured, bool, error) { resourceClient := s.buildResourceClient() - return resourceClient.Get(s.plan.Name, metav1.GetOptions{}) + u, err := resourceClient.Get(s.plan.Name, metav1.GetOptions{}) + if err != nil && !errors.IsNotFound(err) { + return nil, false, err + } + return u, u != nil, nil } // Delete the secret represented by this component. It can return error when the API server does. diff --git a/pkg/controller/servicebindingrequest/secret_test.go b/pkg/controller/servicebindingrequest/secret_test.go index f3f8988032..6d6db8adc5 100644 --- a/pkg/controller/servicebindingrequest/secret_test.go +++ b/pkg/controller/servicebindingrequest/secret_test.go @@ -23,7 +23,11 @@ func TestSecretNew(t *testing.T) { matchLabels := map[string]string{} sbr := mocks.ServiceBindingRequestMock(ns, name, "", "", deploymentsGVR, matchLabels, true) - plan := &Plan{Ns: ns, Name: name, CRDDescription: nil, SBR: *sbr} + plan := &Plan{ + Ns: ns, + Name: name, + SBR: *sbr, + } data := map[string][]byte{"key": []byte("value")} s := NewSecret(f.FakeDynClient(), plan) @@ -52,8 +56,9 @@ func TestSecretNew(t *testing.T) { }) t.Run("Get", func(t *testing.T) { - u, err := s.Get() + u, found, err := s.Get() assert.NoError(t, err) + assert.True(t, found) assertSecretNamespacedName(t, u, ns, name) }) } diff --git a/test/e2e/servicebindingrequest_test.go b/test/e2e/servicebindingrequest_test.go index 0fc45bd1d9..4bc023f1d7 100644 --- a/test/e2e/servicebindingrequest_test.go +++ b/test/e2e/servicebindingrequest_test.go @@ -24,6 +24,7 @@ import ( "github.com/redhat-developer/service-binding-operator/pkg/apis" "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" sbrcontroller "github.com/redhat-developer/service-binding-operator/pkg/controller/servicebindingrequest" + "github.com/redhat-developer/service-binding-operator/pkg/converter" "github.com/redhat-developer/service-binding-operator/test/mocks" ) @@ -339,7 +340,9 @@ func CreateSBR( t.Logf("Creating ServiceBindingRequest mock object '%#v'...", namespacedName) sbr := mocks.ServiceBindingRequestMock( namespacedName.Namespace, namespacedName.Name, resourceRef, "", applicationGVR, matchLabels, false) - require.NoError(t, f.Client.Create(ctx, sbr, cleanupOpts)) + u, err := converter.ToUnstructured(sbr) + require.NoError(t, err) + require.NoError(t, f.Client.Create(ctx, u, cleanupOpts)) return sbr } diff --git a/test/mocks/fake.go b/test/mocks/fake.go index 9754bf92d2..daf3aae679 100644 --- a/test/mocks/fake.go +++ b/test/mocks/fake.go @@ -12,13 +12,13 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" fakedynamic "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ocav1 "github.com/openshift/api/apps/v1" + v1alpha1 "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" ) @@ -106,10 +106,12 @@ func (f *Fake) AddMockedUnstructuredCSVWithVolumeMount(name string) { } // AddMockedDatabaseCR add mocked object from DatabaseCRMock. -func (f *Fake) AddMockedDatabaseCR(ref string) { +func (f *Fake) AddMockedDatabaseCR(ref string) runtime.Object { require.NoError(f.t, pgapis.AddToScheme(f.S)) f.S.AddKnownTypes(pgv1alpha1.SchemeGroupVersion, &pgv1alpha1.Database{}) - f.objs = append(f.objs, DatabaseCRMock(f.ns, ref)) + mock := DatabaseCRMock(f.ns, ref) + f.objs = append(f.objs, mock) + return mock } func (f *Fake) AddMockedUnstructuredDatabaseCR(ref string) { @@ -129,26 +131,29 @@ func (f *Fake) AddMockedUnstructuredDeploymentConfig(name string, matchLabels ma } // AddMockedUnstructuredDeployment add mocked object from UnstructuredDeploymentMock. -func (f *Fake) AddMockedUnstructuredDeployment(name string, matchLabels map[string]string) { +func (f *Fake) AddMockedUnstructuredDeployment(name string, matchLabels map[string]string) *unstructured.Unstructured { require.NoError(f.t, appsv1.AddToScheme(f.S)) d, err := UnstructuredDeploymentMock(f.ns, name, matchLabels) require.NoError(f.t, err) f.S.AddKnownTypes(appsv1.SchemeGroupVersion, &appsv1.Deployment{}) f.objs = append(f.objs, d) + return d } -func (f *Fake) AddMockedUnstructuredDatabaseCRD() { +func (f *Fake) AddMockedUnstructuredDatabaseCRD() *unstructured.Unstructured { require.NoError(f.t, apiextensionv1beta1.AddToScheme(f.S)) c, err := UnstructuredDatabaseCRDMock(f.ns) require.NoError(f.t, err) f.S.AddKnownTypes(apiextensionv1beta1.SchemeGroupVersion, &apiextensionv1beta1.CustomResourceDefinition{}) f.objs = append(f.objs, c) + return c } -func (f *Fake) AddMockedUnstructuredPostgresDatabaseCR(ref string) { +func (f *Fake) AddMockedUnstructuredPostgresDatabaseCR(ref string) *unstructured.Unstructured { d, err := UnstructuredPostgresDatabaseCRMock(f.ns, ref) require.NoError(f.t, err) f.objs = append(f.objs, d) + return d } // AddMockedSecret add mocked object from SecretMock. @@ -156,9 +161,12 @@ func (f *Fake) AddMockedSecret(name string) { f.objs = append(f.objs, SecretMock(f.ns, name)) } -// AddMockedConfigMap add mocked object from ConfigMapMock. -func (f *Fake) AddMockedConfigMap(name string) { - f.objs = append(f.objs, ConfigMapMock(f.ns, name)) +// AddMockedUnstructuredConfigMap add mocked object from ConfigMapMock. +func (f *Fake) AddMockedUnstructuredConfigMap(name string) { + mock := ConfigMapMock(f.ns, name) + uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(mock) + require.NoError(f.t, err) + f.objs = append(f.objs, &unstructured.Unstructured{Object: uObj}) } func (f *Fake) AddMockResource(resource runtime.Object) { @@ -171,7 +179,7 @@ func (f *Fake) FakeClient() client.Client { } // FakeDynClient returns fake dynamic api client. -func (f *Fake) FakeDynClient() dynamic.Interface { +func (f *Fake) FakeDynClient() *fakedynamic.FakeDynamicClient { return fakedynamic.NewSimpleDynamicClient(f.S, f.objs...) } diff --git a/test/mocks/mocks.go b/test/mocks/mocks.go index c9b4b61624..41034da255 100644 --- a/test/mocks/mocks.go +++ b/test/mocks/mocks.go @@ -10,8 +10,6 @@ import ( pgv1alpha1 "github.com/operator-backing-service-samples/postgresql-operator/pkg/apis/postgresql/v1alpha1" olmv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" olminstall "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" - v1alpha1 "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" - "github.com/redhat-developer/service-binding-operator/pkg/converter" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apiextensionv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" @@ -20,6 +18,9 @@ import ( ustrv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + + v1alpha1 "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1" + "github.com/redhat-developer/service-binding-operator/pkg/converter" ) // resource details employed in mocks @@ -379,6 +380,10 @@ func ServiceBindingRequestMock( bindUnannotated bool, ) *v1alpha1.ServiceBindingRequest { return &v1alpha1.ServiceBindingRequest{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceBindingRequest", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, ObjectMeta: metav1.ObjectMeta{ Namespace: ns, Name: name, @@ -392,9 +397,11 @@ func ServiceBindingRequestMock( }, }, BackingServiceSelector: v1alpha1.BackingServiceSelector{ - Group: CRDName, - Version: CRDVersion, - Kind: CRDKind, + GroupVersionKind: schema.GroupVersionKind{ + Group: CRDName, + Version: CRDVersion, + Kind: CRDKind, + }, ResourceRef: backingServiceResourceRef, }, ApplicationSelector: v1alpha1.ApplicationSelector{ @@ -420,7 +427,7 @@ func UnstructuredServiceBindingRequestMock( ) (*unstructured.Unstructured, error) { sbr := ServiceBindingRequestMock( ns, name, backingServiceResourceRef, applicationResourceRef, applicationGVR, matchLabels, false) - return converter.ToUnstructuredAsGVK(&sbr, v1alpha1.SchemeGroupVersion.WithKind(OperatorKind)) + return converter.ToUnstructured(sbr) } // DeploymentConfigListMock returns a list of DeploymentMock. @@ -532,7 +539,7 @@ func DeploymentMock(ns, name string, matchLabels map[string]string) appsv1.Deplo } } -//ThirdLevel ... +// ThirdLevel ... type ThirdLevel struct { Something string `json:"something"` } diff --git a/vendor/modules.txt b/vendor/modules.txt index c085540dc6..77052dc4b3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -129,7 +129,7 @@ github.com/mitchellh/go-homedir github.com/modern-go/concurrent # github.com/modern-go/reflect2 v1.0.1 github.com/modern-go/reflect2 -# github.com/openshift/api v3.9.1-0.20190424152011-77b8897ec79a+incompatible +# github.com/openshift/api v3.9.1-0.20190424152011-77b8897ec79a+incompatible => github.com/openshift/api v0.0.0-20190424152011-77b8897ec79a github.com/openshift/api/apps/v1 github.com/openshift/api/route/v1 # github.com/operator-backing-service-samples/postgresql-operator v0.0.0-20191023140509-5c3697ed3069