diff --git a/api/v1alpha1/operandbindinfo_types.go b/api/v1alpha1/operandbindinfo_types.go index d7516360..35cb293b 100644 --- a/api/v1alpha1/operandbindinfo_types.go +++ b/api/v1alpha1/operandbindinfo_types.go @@ -72,10 +72,14 @@ type Bindable struct { // The configmap identifies an existing configmap object. if it exists, the ODLM will share to the namespace of the OperandRequest. // +optional Configmap string `json:"configmap,omitempty"` - // Route data will shared by copying it into a configmap which is then + // Route data will be shared by copying it into a configmap which is then // created in the target namespace // +optional Route *Route `json:"route,omitempty"` + // Service data will be shared by copying it into a configmap which is then + // created in the target namespace + // +optional + Service *ServiceData `json:"service,omitempty"` } // Route represents the name and data inside an OpenShift route. @@ -89,6 +93,17 @@ type Route struct { Data map[string]string `json:"data"` } +// ServiceData represents the name and data inside an Kubernetes Service. +type ServiceData struct { + // Name is the name of the Kubernetes Service resource + // +optional + Name string `json:"name"` + // Data is a key-value pair where the value is a YAML path to a value in the + // Kubernetes Service, e.g. .spec.ports[0]port + // +optional + Data map[string]string `json:"data"` +} + // OperandBindInfoStatus defines the observed state of OperandBindInfo. type OperandBindInfoStatus struct { // Phase describes the overall phase of OperandBindInfo. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8cfe54b7..531bb62f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -34,6 +34,11 @@ func (in *Bindable) DeepCopyInto(out *Bindable) { *out = new(Route) (*in).DeepCopyInto(*out) } + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceData) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bindable. @@ -806,6 +811,28 @@ func (in *Route) DeepCopy() *Route { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceData) DeepCopyInto(out *ServiceData) { + *out = *in + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceData. +func (in *ServiceData) DeepCopy() *ServiceData { + if in == nil { + return nil + } + out := new(ServiceData) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { *out = *in diff --git a/bundle/manifests/operand-deployment-lifecycle-manager.clusterserviceversion.yaml b/bundle/manifests/operand-deployment-lifecycle-manager.clusterserviceversion.yaml index 709e8c9d..0b388dc0 100644 --- a/bundle/manifests/operand-deployment-lifecycle-manager.clusterserviceversion.yaml +++ b/bundle/manifests/operand-deployment-lifecycle-manager.clusterserviceversion.yaml @@ -129,7 +129,7 @@ metadata: categories: Developer Tools, Monitoring, Logging & Tracing, Security certified: "false" containerImage: icr.io/cpopen/odlm:latest - createdAt: "2023-11-02T22:53:11Z" + createdAt: "2023-11-03T17:30:05Z" description: The Operand Deployment Lifecycle Manager provides a Kubernetes CRD-based API to manage the lifecycle of operands. nss.operator.ibm.com/managed-operators: ibm-odlm olm.skipRange: '>=1.2.0 <4.2.1' diff --git a/bundle/manifests/operator.ibm.com_operandbindinfos.yaml b/bundle/manifests/operator.ibm.com_operandbindinfos.yaml index 822fc474..ede2685f 100644 --- a/bundle/manifests/operator.ibm.com_operandbindinfos.yaml +++ b/bundle/manifests/operator.ibm.com_operandbindinfos.yaml @@ -66,8 +66,8 @@ spec: of the OperandRequest. type: string route: - description: Route data will shared by copying it into a configmap - which is then created in the target namespace + description: Route data will be shared by copying it into a + configmap which is then created in the target namespace properties: data: additionalProperties: @@ -84,6 +84,22 @@ spec: description: The secret identifies an existing secret. if it exists, the ODLM will share to the namespace of the OperandRequest. type: string + service: + description: Service data will be shared by copying it into + a configmap which is then created in the target namespace + properties: + data: + additionalProperties: + type: string + description: Data is a key-value pair where the value is + a YAML path to a value in the Kubernetes Service, e.g. + .spec.ports[0]port + type: object + name: + description: Name is the name of the Kubernetes Service + resource + type: string + type: object type: object description: The bindings section is used to specify information about the access/configuration data that is to be shared. diff --git a/bundle/manifests/operator.ibm.com_operandrequests.yaml b/bundle/manifests/operator.ibm.com_operandrequests.yaml index 50fbab51..f6109d5b 100644 --- a/bundle/manifests/operator.ibm.com_operandrequests.yaml +++ b/bundle/manifests/operator.ibm.com_operandrequests.yaml @@ -89,8 +89,8 @@ spec: share to the namespace of the OperandRequest. type: string route: - description: Route data will shared by copying it - into a configmap which is then created in the + description: Route data will be shared by copying + it into a configmap which is then created in the target namespace properties: data: @@ -110,6 +110,23 @@ spec: if it exists, the ODLM will share to the namespace of the OperandRequest. type: string + service: + description: Service data will be shared by copying + it into a configmap which is then created in the + target namespace + properties: + data: + additionalProperties: + type: string + description: Data is a key-value pair where + the value is a YAML path to a value in the + Kubernetes Service, e.g. .spec.ports[0]port + type: object + name: + description: Name is the name of the Kubernetes + Service resource + type: string + type: object type: object description: The bindings section is used to specify names of secret and/or configmap. diff --git a/config/crd/bases/operator.ibm.com_operandbindinfos.yaml b/config/crd/bases/operator.ibm.com_operandbindinfos.yaml index dc3deb6a..c9b0f10e 100644 --- a/config/crd/bases/operator.ibm.com_operandbindinfos.yaml +++ b/config/crd/bases/operator.ibm.com_operandbindinfos.yaml @@ -64,8 +64,8 @@ spec: of the OperandRequest. type: string route: - description: Route data will shared by copying it into a configmap - which is then created in the target namespace + description: Route data will be shared by copying it into a + configmap which is then created in the target namespace properties: data: additionalProperties: @@ -82,6 +82,22 @@ spec: description: The secret identifies an existing secret. if it exists, the ODLM will share to the namespace of the OperandRequest. type: string + service: + description: Service data will be shared by copying it into + a configmap which is then created in the target namespace + properties: + data: + additionalProperties: + type: string + description: Data is a key-value pair where the value is + a YAML path to a value in the Kubernetes Service, e.g. + .spec.ports[0]port + type: object + name: + description: Name is the name of the Kubernetes Service + resource + type: string + type: object type: object description: The bindings section is used to specify information about the access/configuration data that is to be shared. diff --git a/config/crd/bases/operator.ibm.com_operandrequests.yaml b/config/crd/bases/operator.ibm.com_operandrequests.yaml index c9ba0a55..b6935961 100644 --- a/config/crd/bases/operator.ibm.com_operandrequests.yaml +++ b/config/crd/bases/operator.ibm.com_operandrequests.yaml @@ -87,8 +87,8 @@ spec: share to the namespace of the OperandRequest. type: string route: - description: Route data will shared by copying it - into a configmap which is then created in the + description: Route data will be shared by copying + it into a configmap which is then created in the target namespace properties: data: @@ -108,6 +108,23 @@ spec: if it exists, the ODLM will share to the namespace of the OperandRequest. type: string + service: + description: Service data will be shared by copying + it into a configmap which is then created in the + target namespace + properties: + data: + additionalProperties: + type: string + description: Data is a key-value pair where + the value is a YAML path to a value in the + Kubernetes Service, e.g. .spec.ports[0]port + type: object + name: + description: Name is the name of the Kubernetes + Service resource + type: string + type: object type: object description: The bindings section is used to specify names of secret and/or configmap. diff --git a/config/e2e/crd/bases/operator.ibm.com_operandbindinfos.yaml b/config/e2e/crd/bases/operator.ibm.com_operandbindinfos.yaml index 78790021..0b560cde 100644 --- a/config/e2e/crd/bases/operator.ibm.com_operandbindinfos.yaml +++ b/config/e2e/crd/bases/operator.ibm.com_operandbindinfos.yaml @@ -68,8 +68,8 @@ spec: of the OperandRequest. type: string route: - description: Route data will shared by copying it into a configmap - which is then created in the target namespace + description: Route data will be shared by copying it into a + configmap which is then created in the target namespace properties: data: additionalProperties: @@ -86,6 +86,22 @@ spec: description: The secret identifies an existing secret. if it exists, the ODLM will share to the namespace of the OperandRequest. type: string + service: + description: Service data will be shared by copying it into + a configmap which is then created in the target namespace + properties: + data: + additionalProperties: + type: string + description: Data is a key-value pair where the value is + a YAML path to a value in the Kubernetes Service, e.g. + .spec.ports[0]port + type: object + name: + description: Name is the name of the Kubernetes Service + resource + type: string + type: object type: object description: The bindings section is used to specify information about the access/configuration data that is to be shared. diff --git a/config/e2e/crd/bases/operator.ibm.com_operandrequests.yaml b/config/e2e/crd/bases/operator.ibm.com_operandrequests.yaml index bf63d956..770a90e7 100644 --- a/config/e2e/crd/bases/operator.ibm.com_operandrequests.yaml +++ b/config/e2e/crd/bases/operator.ibm.com_operandrequests.yaml @@ -91,8 +91,8 @@ spec: share to the namespace of the OperandRequest. type: string route: - description: Route data will shared by copying it - into a configmap which is then created in the + description: Route data will be shared by copying + it into a configmap which is then created in the target namespace properties: data: @@ -112,6 +112,23 @@ spec: if it exists, the ODLM will share to the namespace of the OperandRequest. type: string + service: + description: Service data will be shared by copying + it into a configmap which is then created in the + target namespace + properties: + data: + additionalProperties: + type: string + description: Data is a key-value pair where + the value is a YAML path to a value in the + Kubernetes Service, e.g. .spec.ports[0]port + type: object + name: + description: Name is the name of the Kubernetes + Service resource + type: string + type: object type: object description: The bindings section is used to specify names of secret and/or configmap. diff --git a/controllers/operandbindinfo/operandbindinfo_controller.go b/controllers/operandbindinfo/operandbindinfo_controller.go index c4fab85d..66067532 100644 --- a/controllers/operandbindinfo/operandbindinfo_controller.go +++ b/controllers/operandbindinfo/operandbindinfo_controller.go @@ -17,6 +17,7 @@ package operandbindinfo import ( + "bytes" "context" "fmt" "reflect" @@ -36,6 +37,7 @@ import ( utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" + "k8s.io/client-go/util/jsonpath" "k8s.io/klog" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -221,6 +223,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re requeue = requeue || requeueRoute } } + // Copy Service data into configmap and share configmap + if binding.Service != nil { + requeueService, err := r.copyService(ctx, *binding.Service, "", operandNamespace, bindRequest.Namespace, key, bindInfoInstance, requestInstance) + if err != nil { + merr.Add(err) + continue + } + requeue = requeue || requeueService + } } } if len(merr.Errors) != 0 { @@ -427,6 +438,8 @@ func (r *Reconciler) copyConfigmap(ctx context.Context, sourceName, targetName, return false, nil } +// copyRoute reads the data map and copies OCP Route data as specified by the +// field path in the data map values func (r *Reconciler) copyRoute(ctx context.Context, route operatorv1alpha1.Route, targetName, sourceNs, targetNs, key string, bindInfoInstance *operatorv1alpha1.OperandBindInfo, requestInstance *operatorv1alpha1.OperandRequest) (requeue bool, err error) { if route.Name == "" || sourceNs == "" || targetNs == "" { @@ -521,7 +534,103 @@ func (r *Reconciler) copyRoute(ctx context.Context, route operatorv1alpha1.Route return false, nil } -// sanitizedOdlmRouteData takes a map, i.e. ODLM's Route.Data, and an OCP Route.Spec, +// copyRoute reads the data map and copies K8s Service data as specified by the +// field path in the data map values +func (r *Reconciler) copyService(ctx context.Context, service operatorv1alpha1.ServiceData, targetName, sourceNs, targetNs, key string, + bindInfoInstance *operatorv1alpha1.OperandBindInfo, requestInstance *operatorv1alpha1.OperandRequest) (requeue bool, err error) { + if service.Name == "" || sourceNs == "" || targetNs == "" { + return false, nil + } + + if service.Name == targetName && sourceNs == targetNs { + return false, nil + } + + if targetName == "" { + if publicPrefix.MatchString(key) { + targetName = bindInfoInstance.Name + "-" + service.Name + } else { + return false, nil + } + } + + sourceService := &corev1.Service{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: service.Name, Namespace: sourceNs}, sourceService); err != nil { + if apierrors.IsNotFound(err) { + klog.V(3).Infof("Route %s/%s is not found", sourceNs, service.Name) + r.Recorder.Eventf(bindInfoInstance, corev1.EventTypeNormal, "NotFound", "No Service %s in the namespace %s", service.Name, sourceNs) + return true, nil + } + return false, errors.Wrapf(err, "failed to get Service %s/%s", sourceNs, service.Name) + } + // Create the ConfigMap to the OperandRequest namespace + labels := make(map[string]string) + // Copy from the original labels to the target labels + for k, v := range sourceService.Labels { + labels[k] = v + } + labels[bindInfoInstance.Namespace+"."+bindInfoInstance.Name+"/bindinfo"] = "true" + labels[constant.OpbiTypeLabel] = "copy" + + sanitizedData, err := sanitizeServiceData(service.Data, *sourceService) + if err != nil { + return false, err + } + cmCopy := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetName, + Namespace: targetNs, + Labels: labels, + }, + Data: sanitizedData, + } + // Set the OperandRequest as the controller of the configmap + if err := controllerutil.SetControllerReference(requestInstance, cmCopy, r.Scheme); err != nil { + return false, errors.Wrapf(err, "failed to set OperandRequest %s as the owner of ConfigMap %s", requestInstance.Name, service.Name) + } + + var podRefreshment bool + // Create the ConfigMap in the OperandRequest namespace + if err := r.Create(ctx, cmCopy); err != nil { + if apierrors.IsAlreadyExists(err) { + // If already exist, update the ConfigMap + existingCm := &corev1.ConfigMap{} + if err := r.Client.Get(ctx, types.NamespacedName{Namespace: targetNs, Name: targetName}, existingCm); err != nil { + return false, errors.Wrapf(err, "failed to get ConfigMap %s/%s", targetNs, targetName) + } + if needUpdate := util.CompareConfigMap(cmCopy, existingCm); needUpdate { + podRefreshment = true + if err := r.Update(ctx, cmCopy); err != nil { + return false, errors.Wrapf(err, "failed to update ConfigMap %s/%s", targetNs, service.Name) + } + } + } else { + return false, errors.Wrapf(err, "failed to create ConfigMap %s/%s", targetNs, service.Name) + } + } + + if podRefreshment { + if err := r.refreshPods(targetNs, targetName, "configmap"); err != nil { + return false, errors.Wrapf(err, "failed to refresh pods mounting ConfigMap %s/%s", targetNs, targetName) + } + } + + // Set the OperandBindInfo label for the ConfigMap + util.EnsureLabelsForService(sourceService, map[string]string{ + bindInfoInstance.Namespace + "." + bindInfoInstance.Name + "/bindinfo": "true", + constant.OpbiTypeLabel: "original", + }) + + // Update the operand Configmap + if err := r.Update(ctx, sourceService); err != nil { + return false, errors.Wrapf(err, "failed to update ConfigMap %s/%s", sourceService.Namespace, sourceService.Name) + } + klog.V(1).Infof("Service %s is copied from the namespace %s to the namespace %s", service.Name, sourceNs, targetNs) + + return false, nil +} + +// sanitizeOdlmRouteData takes a map, i.e. ODLM's Route.Data, and an OCP Route.Spec, // and returns a map ready to be included into a ConfigMap's data. The ODLM's // Route.Data is sanitized because the values are YAML path references // in map because they correspond to YAML fields in a OCP Route. Ensures that: @@ -559,6 +668,40 @@ func sanitizeOdlmRouteData(m map[string]string, route ocproute.RouteSpec) (map[s return sanitized, nil } +// sanitizeServiceData takes a map, i.e. ODLM's Service.Data, and a K8s Service object +// and returns a map ready to be included into a ConfigMap's data. The ODLM's +// Service.Data is sanitized because the values are YAML fields in a K8s Service. +// Ensures that: +// 1. the field actually exists, otherwise returns an error +// 2. extracts the value from the K8s Service's field, the value will be +// stringified +func sanitizeServiceData(m map[string]string, service corev1.Service) (map[string]string, error) { + sanitized := make(map[string]string, len(m)) + jpath := jsonpath.New("sanitizeServiceData") + for k, v := range m { + trueValue := "" + stringParts := strings.Split(v, "+") + for _, s := range stringParts { + actual := s + if strings.HasPrefix(s, ".") { + if len(s) > 1 { + if err := jpath.Parse("{" + s + "}"); err != nil { + return nil, err + } + buf := new(bytes.Buffer) + if err := jpath.Execute(buf, service); err != nil { + return nil, err + } + actual = buf.String() + } + } + trueValue += actual + } + sanitized[k] = trueValue + } + return sanitized, nil +} + // nestedBasicType is a wrapper around the various unstructured.Nested methods // used to extract values from nested fields. It takes the same arguments // as the Nested methods and returns a string if some basic type value is found. diff --git a/controllers/util/util.go b/controllers/util/util.go index 578b204a..a17bf799 100644 --- a/controllers/util/util.go +++ b/controllers/util/util.go @@ -270,6 +270,15 @@ func EnsureLabelsForRoute(r *ocproute.Route, labels map[string]string) { } } +func EnsureLabelsForService(s *corev1.Service, labels map[string]string) { + if s.Labels == nil { + s.Labels = make(map[string]string) + } + for k, v := range labels { + s.Labels[k] = v + } +} + func CompareSecret(secret *corev1.Secret, existingSecret *corev1.Secret) (needUpdate bool) { return !equality.Semantic.DeepEqual(secret.GetLabels(), existingSecret.GetLabels()) || !equality.Semantic.DeepEqual(secret.Type, existingSecret.Type) || !equality.Semantic.DeepEqual(secret.Data, existingSecret.Data) || !equality.Semantic.DeepEqual(secret.StringData, existingSecret.StringData) }