From ec9f81ae186fcdf57ac95956086ab408ccc1b33e Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 19 Jan 2023 18:51:03 -0500 Subject: [PATCH] Unproject using the same mapping used to project the binding From the spec: > When a service binding projection is removed, the controller MUST use > the same mappings from the projection creation. After a > ClusterWorkloadResourceMapping resource is modified, each binding > targeting the mapped workload type MUST be removed, then reattempted > with the latest mapping. We now stash the mapping used to project the binding on the workload as an annotation. When unprojecting that same binding, we use the stashed mapping to unproject the binding. If updating an existing binding, the stashed mapping is used to cleanup existing state before the updated mapping is used to re-project the binding into the workload. Signed-off-by: Scott Andrews --- controllers/servicebinding_controller_test.go | 12 + controllers/webhook_controller_test.go | 19 + projector/binding.go | 131 ++- projector/binding_test.go | 873 ++++++++++++++++-- projector/interface.go | 13 +- projector/mapping.go | 56 +- projector/metapodtemplate.go | 24 +- projector/metapodtemplate_test.go | 91 +- resolver/cluster.go | 42 +- resolver/cluster_test.go | 287 +++--- resolver/interface.go | 13 +- 11 files changed, 1254 insertions(+), 307 deletions(-) diff --git a/controllers/servicebinding_controller_test.go b/controllers/servicebinding_controller_test.go index 0cba6b3b..81215cbd 100644 --- a/controllers/servicebinding_controller_test.go +++ b/controllers/servicebinding_controller_test.go @@ -53,6 +53,8 @@ func TestServiceBindingReconciler(t *testing.T) { secretName := "my-secret" key := types.NamespacedName{Namespace: namespace, Name: name} + podSpecableMapping := `{"versions":[{"version":"*","annotations":".spec.template.metadata.annotations","containers":[{"path":".spec.template.spec.initContainers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"},{"path":".spec.template.spec.containers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"}],"volumes":".spec.template.spec.volumes"}]}` + scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(servicebindingv1beta1.AddToScheme(scheme)) @@ -102,6 +104,9 @@ func TestServiceBindingReconciler(t *testing.T) { }) }) projectedWorkload := workload. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.AddAnnotation(fmt.Sprintf("projector.servicebinding.io/mapping-%s", uid), podSpecableMapping) + }). SpecDie(func(d *dieappsv1.DeploymentSpecDie) { d.TemplateDie(func(d *diecorev1.PodTemplateSpecDie) { d.MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -143,6 +148,7 @@ func TestServiceBindingReconciler(t *testing.T) { }) }) }).DieReleaseUnstructured() + unstructured.SetNestedMap(unprojectedWorkload.UnstructuredContent(), map[string]interface{}{}, "metadata", "annotations") unstructured.SetNestedMap(unprojectedWorkload.UnstructuredContent(), map[string]interface{}{}, "spec", "template", "metadata", "annotations") containers, _, _ := unstructured.NestedSlice(unprojectedWorkload.UnstructuredContent(), "spec", "template", "spec", "containers") unstructured.SetNestedSlice(containers[0].(map[string]interface{}), []interface{}{}, "volumeMounts") @@ -731,6 +737,8 @@ func TestProjectBinding(t *testing.T) { uid := types.UID("dde10100-d7b3-4cba-9430-51d60a8612a6") secretName := "my-secret" + podSpecableMapping := `{"versions":[{"version":"*","annotations":".spec.template.metadata.annotations","containers":[{"path":".spec.template.spec.initContainers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"},{"path":".spec.template.spec.containers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"}],"volumes":".spec.template.spec.volumes"}]}` + scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(servicebindingv1beta1.AddToScheme(scheme)) @@ -781,6 +789,9 @@ func TestProjectBinding(t *testing.T) { }) }) projectedWorkload := workload. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.AddAnnotation(fmt.Sprintf("projector.servicebinding.io/mapping-%s", uid), podSpecableMapping) + }). SpecDie(func(d *dieappsv1.DeploymentSpecDie) { d.TemplateDie(func(d *diecorev1.PodTemplateSpecDie) { d.MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -822,6 +833,7 @@ func TestProjectBinding(t *testing.T) { }) }) }).DieReleaseUnstructured() + unstructured.SetNestedMap(unprojectedWorkload.UnstructuredContent(), map[string]interface{}{}, "metadata", "annotations") unstructured.SetNestedMap(unprojectedWorkload.UnstructuredContent(), map[string]interface{}{}, "spec", "template", "metadata", "annotations") containers, _, _ := unstructured.NestedSlice(unprojectedWorkload.UnstructuredContent(), "spec", "template", "spec", "containers") unstructured.SetNestedSlice(containers[0].(map[string]interface{}), []interface{}{}, "volumeMounts") diff --git a/controllers/webhook_controller_test.go b/controllers/webhook_controller_test.go index c767784a..5818e61d 100644 --- a/controllers/webhook_controller_test.go +++ b/controllers/webhook_controller_test.go @@ -188,6 +188,8 @@ func TestAdmissionProjectorWebhook(t *testing.T) { requestUID := types.UID("9deefaa1-2c90-4f40-9c7b-3f5c1fd75dde") bindingUID := types.UID("89deaf20-7bab-4610-81db-6f8c3f7fa51d") + podSpecableMapping := `{"versions":[{"version":"*","annotations":".spec.template.metadata.annotations","containers":[{"path":".spec.template.spec.initContainers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"},{"path":".spec.template.spec.containers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"}],"volumes":".spec.template.spec.volumes"}]}` + workload := dieappsv1.DeploymentBlank. APIVersion("apps/v1"). Kind("Deployment"). @@ -275,6 +277,9 @@ func TestAdmissionProjectorWebhook(t *testing.T) { AdmissionRequest: request. Object( workload. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.AddAnnotation(fmt.Sprintf("projector.servicebinding.io/mapping-%s", bindingUID), podSpecableMapping) + }). SpecDie(func(d *dieappsv1.DeploymentSpecDie) { d.TemplateDie(func(d *diecorev1.PodTemplateSpecDie) { d.MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -331,6 +336,13 @@ func TestAdmissionProjectorWebhook(t *testing.T) { ExpectedResponse: admission.Response{ AdmissionResponse: response.DieRelease(), Patches: []jsonpatch.Operation{ + { + Operation: "add", + Path: "/metadata/annotations", + Value: map[string]interface{}{ + fmt.Sprintf("projector.servicebinding.io/mapping-%s", bindingUID): podSpecableMapping, + }, + }, { Operation: "add", Path: "/spec/template/metadata/annotations", @@ -399,6 +411,13 @@ func TestAdmissionProjectorWebhook(t *testing.T) { ExpectedResponse: admission.Response{ AdmissionResponse: response.DieRelease(), Patches: []jsonpatch.Operation{ + { + Operation: "add", + Path: "/metadata/annotations", + Value: map[string]interface{}{ + fmt.Sprintf("projector.servicebinding.io/mapping-%s", bindingUID): podSpecableMapping, + }, + }, { Operation: "add", Path: "/spec/template/metadata/annotations", diff --git a/projector/binding.go b/projector/binding.go index 036cfead..85b124c2 100644 --- a/projector/binding.go +++ b/projector/binding.go @@ -18,12 +18,15 @@ package projector import ( "context" + "encoding/json" "fmt" "path" "sort" "strings" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" @@ -37,6 +40,7 @@ const ( SecretAnnotationPrefix = Group + "/secret-" TypeAnnotationPrefix = Group + "/type-" ProviderAnnotationPrefix = Group + "/provider-" + MappingAnnotationPrefix = Group + "/mapping-" ) var _ ServiceBindingProjector = (*serviceBindingProjector)(nil) @@ -54,35 +58,95 @@ func New(mappingSource MappingSource) ServiceBindingProjector { } func (p *serviceBindingProjector) Project(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) error { - mapping, err := p.mappingSource.LookupMapping(ctx, workload) + ctx, resourceMapping, version, err := p.lookupClusterMapping(ctx, workload) if err != nil { return err } - mpt, err := NewMetaPodTemplate(ctx, workload, mapping) + + // rather than attempt to merge an existing binding, unproject it + if err := p.Unproject(ctx, binding, workload); err != nil { + return err + } + + versionMapping := MappingVersion(version, resourceMapping) + mpt, err := NewMetaPodTemplate(ctx, workload, versionMapping) if err != nil { return err } p.project(binding, mpt) - return mpt.WriteToWorkload(ctx) + + if p.secretName(binding) != "" { + if err := p.stashLocalMapping(binding, mpt, resourceMapping); err != nil { + return err + } + } + if err := mpt.WriteToWorkload(ctx); err != nil { + return err + } + + return nil } func (p *serviceBindingProjector) Unproject(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) error { - mapping, err := p.mappingSource.LookupMapping(ctx, workload) + resourceMapping, err := p.retrieveLocalMapping(binding, workload) + if err != nil { + return err + } + ctx, m, version, err := p.lookupClusterMapping(ctx, workload) if err != nil { return err } - mpt, err := NewMetaPodTemplate(ctx, workload, mapping) + if resourceMapping == nil { + // fall back to using the remote mappings, this isn't ideal as the mapping may have changed after the binding was originally projected + resourceMapping = m + } + versionMapping := MappingVersion(version, resourceMapping) + mpt, err := NewMetaPodTemplate(ctx, workload, versionMapping) if err != nil { return err } p.unproject(binding, mpt) - return mpt.WriteToWorkload(ctx) + + if err := p.stashLocalMapping(binding, mpt, nil); err != nil { + return err + } + if err := mpt.WriteToWorkload(ctx); err != nil { + return err + } + + return nil } -func (p *serviceBindingProjector) project(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate) { - // rather than attempt to merge an existing binding, unproject it - p.unproject(binding, mpt) +type mappingValue struct { + WorkloadMapping *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec + RESTMapping *meta.RESTMapping +} + +// lookupClusterMapping resolves the mapping from the context or from the cluster. This +// avoids redundant calls to the mappingSource for the same workload call when Unproject +// is called from Project. When the lookup is from the cluster, the value is stashed into +// the context for future lookups in this turn. +func (p *serviceBindingProjector) lookupClusterMapping(ctx context.Context, workload runtime.Object) (context.Context, *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec, string, error) { + raw := ctx.Value(mappingValue{}) + if value, ok := raw.(mappingValue); ok { + return ctx, value.WorkloadMapping, value.RESTMapping.Resource.Version, nil + } + rm, err := p.mappingSource.LookupRESTMapping(ctx, workload) + if err != nil { + return ctx, nil, "", err + } + wm, err := p.mappingSource.LookupWorkloadMapping(ctx, rm.Resource) + if err != nil { + return ctx, nil, "", err + } + ctx = context.WithValue(ctx, mappingValue{}, mappingValue{ + WorkloadMapping: wm, + RESTMapping: rm, + }) + return ctx, wm, rm.Resource.Version, nil +} +func (p *serviceBindingProjector) project(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate) { if p.secretName(binding) == "" { // no secret to bind return @@ -100,9 +164,9 @@ func (p *serviceBindingProjector) unproject(binding *servicebindingv1beta1.Servi } // cleanup annotations - delete(mpt.Annotations, p.secretAnnotationName(binding)) - delete(mpt.Annotations, p.typeAnnotationName(binding)) - delete(mpt.Annotations, p.providerAnnotationName(binding)) + delete(mpt.PodTemplateAnnotations, p.secretAnnotationName(binding)) + delete(mpt.PodTemplateAnnotations, p.typeAnnotationName(binding)) + delete(mpt.PodTemplateAnnotations, p.providerAnnotationName(binding)) } func (p *serviceBindingProjector) projectVolume(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate) { @@ -296,7 +360,7 @@ func (p *serviceBindingProjector) projectEnv(binding *servicebindingv1beta1.Serv func (p *serviceBindingProjector) unprojectEnv(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate, mc *metaContainer) { env := []corev1.EnvVar{} - secret := mpt.Annotations[p.secretAnnotationName(binding)] + secret := mpt.PodTemplateAnnotations[p.secretAnnotationName(binding)] typeFieldPath := fmt.Sprintf("metadata.annotations['%s']", p.typeAnnotationName(binding)) providerFieldPath := fmt.Sprintf("metadata.annotations['%s']", p.providerAnnotationName(binding)) for _, e := range mc.Env { @@ -364,7 +428,7 @@ func (p *serviceBindingProjector) isProjectedEnv(e corev1.EnvVar, secrets sets.S func (p *serviceBindingProjector) knownProjectedSecrets(mpt *metaPodTemplate) sets.String { secrets := sets.NewString() - for k, v := range mpt.Annotations { + for k, v := range mpt.PodTemplateAnnotations { if strings.HasPrefix(k, SecretAnnotationPrefix) { secrets.Insert(v) } @@ -385,7 +449,7 @@ func (p *serviceBindingProjector) secretAnnotation(binding *servicebindingv1beta if secret == "" { return "" } - mpt.Annotations[key] = secret + mpt.PodTemplateAnnotations[key] = secret return secret } @@ -399,7 +463,7 @@ func (p *serviceBindingProjector) volumeName(binding *servicebindingv1beta1.Serv func (p *serviceBindingProjector) typeAnnotation(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate) string { key := p.typeAnnotationName(binding) - mpt.Annotations[key] = binding.Spec.Type + mpt.PodTemplateAnnotations[key] = binding.Spec.Type return key } @@ -409,10 +473,43 @@ func (p *serviceBindingProjector) typeAnnotationName(binding *servicebindingv1be func (p *serviceBindingProjector) providerAnnotation(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate) string { key := p.providerAnnotationName(binding) - mpt.Annotations[key] = binding.Spec.Provider + mpt.PodTemplateAnnotations[key] = binding.Spec.Provider return key } func (p *serviceBindingProjector) providerAnnotationName(binding *servicebindingv1beta1.ServiceBinding) string { return fmt.Sprintf("%s%s", ProviderAnnotationPrefix, binding.UID) } + +func (p *serviceBindingProjector) retrieveLocalMapping(binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) (*servicebindingv1beta1.ClusterWorkloadResourceMappingSpec, error) { + annoations := workload.(metav1.Object).GetAnnotations() + if annoations == nil { + return nil, nil + } + data, ok := annoations[p.mappingAnnotationName(binding)] + if !ok { + return nil, nil + } + var mapping servicebindingv1beta1.ClusterWorkloadResourceMappingSpec + if err := json.Unmarshal([]byte(data), &mapping); err != nil { + return nil, err + } + return &mapping, nil +} + +func (p *serviceBindingProjector) stashLocalMapping(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate, mapping *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec) error { + if mapping == nil { + delete(mpt.WorkloadAnnotations, p.mappingAnnotationName(binding)) + return nil + } + data, err := json.Marshal(mapping) + if err != nil { + return err + } + mpt.WorkloadAnnotations[p.mappingAnnotationName(binding)] = string(data) + return nil +} + +func (p *serviceBindingProjector) mappingAnnotationName(binding *servicebindingv1beta1.ServiceBinding) string { + return fmt.Sprintf("%s%s", MappingAnnotationPrefix, binding.UID) +} diff --git a/projector/binding_test.go b/projector/binding_test.go index 4891ec81..4a71b51b 100644 --- a/projector/binding_test.go +++ b/projector/binding_test.go @@ -25,8 +25,10 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" @@ -37,6 +39,20 @@ func TestBinding(t *testing.T) { bindingName := "my-binding" secretName := "my-secret" + podSpecableMapping := `{"versions":[{"version":"*","annotations":".spec.template.metadata.annotations","containers":[{"path":".spec.template.spec.initContainers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"},{"path":".spec.template.spec.containers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"}],"volumes":".spec.template.spec.volumes"}]}` + cronJobMapping := `{"versions":[{"version":"*","annotations":".spec.jobTemplate.spec.template.metadata.annotations","containers":[{"path":".spec.jobTemplate.spec.template.spec.initContainers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"},{"path":".spec.jobTemplate.spec.template.spec.containers[*]","name":".name","env":".env","volumeMounts":".volumeMounts"}],"volumes":".spec.jobTemplate.spec.template.spec.volumes"}]}` + + deploymentRESTMapping := &meta.RESTMapping{ + GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, + Scope: meta.RESTScopeNamespace, + } + cronJobRESTMapping := &meta.RESTMapping{ + GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"}, + Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"}, + Scope: meta.RESTScopeNamespace, + } + tests := []struct { name string mapping MappingSource @@ -47,7 +63,7 @@ func TestBinding(t *testing.T) { }{ { name: "podspecable", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -92,6 +108,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -115,75 +136,632 @@ func TestBinding(t *testing.T) { }, }, }, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: "init-hello", - Env: []corev1.EnvVar{ - { - Name: "SERVICE_BINDING_ROOT", - Value: "/bindings", - }, - }, - VolumeMounts: []corev1.VolumeMount{ + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + { + Name: "init-hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/custom/path", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/custom/path/my-binding", + }, + }, + }, + { + Name: "hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "almost podspecable", + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ + { + Version: "*", + Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", + Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ + { + Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", + Name: ".name", + }, + { + Path: ".spec.jobTemplate.spec.template.spec.containers[*]", + Name: ".name", + }, + }, + Volumes: ".spec.jobTemplate.spec.template.spec.volumes", + }, + }, + }, cronJobRESTMapping), + binding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + UID: uid, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Name: bindingName, + }, + Status: servicebindingv1beta1.ServiceBindingStatus{ + Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ + Name: secretName, + }, + }, + }, + workload: &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "init-hello", + }, + { + Name: "init-hello-2", + }, + }, + Containers: []corev1.Container{ + { + Name: "hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/custom/path", + }, + }, + }, + { + Name: "hello-2", + }, + }, + }, + }, + }, + }, + }, + }, + expected: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": cronJobMapping, + }, + }, + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/secret-26894874-4719-4802-8f43-8ceed127b4c2": "my-secret", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-secret", + }, + }, + }, + }, + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + { + Name: "init-hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/custom/path", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/custom/path/my-binding", + }, + }, + }, + { + Name: "hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "almost podspecable, unbind with stashed mapping", + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, cronJobRESTMapping), + binding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + UID: uid, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Name: bindingName, + }, + Status: servicebindingv1beta1.ServiceBindingStatus{ + Binding: nil, + }, + }, + workload: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": cronJobMapping, + }, + }, + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/secret-26894874-4719-4802-8f43-8ceed127b4c2": "my-secret", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-secret", + }, + }, + }, + }, + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + { + Name: "init-hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/custom/path", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/custom/path/my-binding", + }, + }, + }, + { + Name: "hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "init-hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + }, + { + Name: "init-hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/custom/path", + }, + }, + }, + { + Name: "hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "almost podspecable, unbind with cluster mapping", + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ + { + Version: "*", + Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", + Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ + { + Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", + Name: ".name", + }, + { + Path: ".spec.jobTemplate.spec.template.spec.containers[*]", + Name: ".name", + }, + }, + Volumes: ".spec.jobTemplate.spec.template.spec.volumes", + }, + }, + }, cronJobRESTMapping), + binding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + UID: uid, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Name: bindingName, + }, + Status: servicebindingv1beta1.ServiceBindingStatus{ + Binding: nil, + }, + }, + workload: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/secret-26894874-4719-4802-8f43-8ceed127b4c2": "my-secret", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-secret", + }, + }, + }, + }, + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, + }, { - Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", - ReadOnly: true, - MountPath: "/bindings/my-binding", + Name: "init-hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, }, }, - }, - { - Name: "init-hello-2", - Env: []corev1.EnvVar{ + Containers: []corev1.Container{ { - Name: "SERVICE_BINDING_ROOT", - Value: "/bindings", + Name: "hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/custom/path", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/custom/path/my-binding", + }, + }, }, - }, - VolumeMounts: []corev1.VolumeMount{ { - Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", - ReadOnly: true, - MountPath: "/bindings/my-binding", + Name: "hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, }, }, }, }, - Containers: []corev1.Container{ - { - Name: "hello", - Env: []corev1.EnvVar{ + }, + }, + }, + }, + expected: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ { - Name: "SERVICE_BINDING_ROOT", - Value: "/custom/path", + Name: "init-hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{}, }, - }, - VolumeMounts: []corev1.VolumeMount{ { - Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", - ReadOnly: true, - MountPath: "/custom/path/my-binding", + Name: "init-hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{}, }, }, - }, - { - Name: "hello-2", - Env: []corev1.EnvVar{ + Containers: []corev1.Container{ { - Name: "SERVICE_BINDING_ROOT", - Value: "/bindings", + Name: "hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/custom/path", + }, + }, + VolumeMounts: []corev1.VolumeMount{}, }, - }, - VolumeMounts: []corev1.VolumeMount{ { - Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", - ReadOnly: true, - MountPath: "/bindings/my-binding", + Name: "hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{}, }, }, + Volumes: []corev1.Volume{}, }, }, }, @@ -192,21 +770,8 @@ func TestBinding(t *testing.T) { }, }, { - name: "almost podspecable", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ - Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", - Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ - { - Path: ".spec.jobTemplate.spec.template.spec.containers[*]", - Name: ".name", - }, - { - Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", - Name: ".name", - }, - }, - Volumes: ".spec.jobTemplate.spec.template.spec.volumes", - }), + name: "almost podspecable, unable to unbind without mapping", + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, cronJobRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -215,23 +780,73 @@ func TestBinding(t *testing.T) { Name: bindingName, }, Status: servicebindingv1beta1.ServiceBindingStatus{ - Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ - Name: secretName, - }, + Binding: nil, }, }, workload: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/secret-26894874-4719-4802-8f43-8ceed127b4c2": "my-secret", + }, + }, Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-secret", + }, + }, + }, + }, + }, + }, + }, + }, InitContainers: []corev1.Container{ { Name: "init-hello", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, }, { Name: "init-hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, }, }, Containers: []corev1.Container{ @@ -243,9 +858,29 @@ func TestBinding(t *testing.T) { Value: "/custom/path", }, }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/custom/path/my-binding", + }, + }, }, { Name: "hello-2", + Env: []corev1.EnvVar{ + { + Name: "SERVICE_BINDING_ROOT", + Value: "/bindings", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "servicebinding-26894874-4719-4802-8f43-8ceed127b4c2", + ReadOnly: true, + MountPath: "/bindings/my-binding", + }, + }, }, }, }, @@ -255,6 +890,9 @@ func TestBinding(t *testing.T) { }, }, expected: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ @@ -360,7 +998,7 @@ func TestBinding(t *testing.T) { }, { name: "no containers", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -376,6 +1014,11 @@ func TestBinding(t *testing.T) { }, workload: &appsv1.Deployment{}, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -409,7 +1052,7 @@ func TestBinding(t *testing.T) { }, { name: "rotate binding secret", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -472,6 +1115,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -522,7 +1170,7 @@ func TestBinding(t *testing.T) { }, { name: "project service binding env", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -558,6 +1206,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -630,7 +1283,7 @@ func TestBinding(t *testing.T) { }, { name: "remove service binding env", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -715,6 +1368,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -765,7 +1423,7 @@ func TestBinding(t *testing.T) { }, { name: "update service binding env", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -860,6 +1518,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -932,7 +1595,7 @@ func TestBinding(t *testing.T) { }, { name: "project service binding type and provider for env and volume", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -970,6 +1633,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1062,7 +1730,7 @@ func TestBinding(t *testing.T) { }, { name: "update service binding type and provider", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -1177,6 +1845,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1249,7 +1922,7 @@ func TestBinding(t *testing.T) { }, { name: "no binding if missing secret", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -1270,6 +1943,9 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1290,7 +1966,7 @@ func TestBinding(t *testing.T) { }, { name: "only bind to allowed containers", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -1327,6 +2003,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1388,7 +2069,7 @@ func TestBinding(t *testing.T) { }, { name: "preserve other bindings", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -1569,6 +2250,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1765,7 +2451,7 @@ func TestBinding(t *testing.T) { }, { name: "apply binding should be idempotent", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ UID: uid, @@ -1871,6 +2557,11 @@ func TestBinding(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, + }, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1967,20 +2658,32 @@ func TestBinding(t *testing.T) { }, { name: "invalid container jsonpath", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ - Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ + mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Path: "[", + Version: "*", + Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ + { + Path: "[", + }, + }, }, }, - }), + }, deploymentRESTMapping), binding: &servicebindingv1beta1.ServiceBinding{}, workload: &appsv1.Deployment{}, expectedErr: true, }, { - name: "conversion error", - mapping: NewStaticMapping(&servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}), + name: "conversion error", + mapping: NewStaticMapping( + &servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{}, + &meta.RESTMapping{ + GroupVersionKind: schema.GroupVersionKind{Group: "test", Version: "v1", Kind: "BadMarshalJSON"}, + Resource: schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "badmarshaljsons"}, + Scope: meta.RESTScopeNamespace, + }, + ), workload: &BadMarshalJSON{}, expectedErr: true, }, diff --git a/projector/interface.go b/projector/interface.go index 4cbbc7d6..9b50159b 100644 --- a/projector/interface.go +++ b/projector/interface.go @@ -19,7 +19,9 @@ package projector import ( "context" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" ) @@ -32,8 +34,11 @@ type ServiceBindingProjector interface { } type MappingSource interface { - // LookupMapping the mapping template for the workload. Typically a ClusterWorkloadResourceMapping is defined for the workload's - // fully qualified resource `{resource}.{group}`. The workload's version is either directly matched, or the wildcard version `*` - // mapping template is returned. If no explicit mapping is found, a mapping appropriate for a PodSpecable resource may be used. - LookupMapping(ctx context.Context, workload runtime.Object) (*servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate, error) + // LookupRESTMapping returns the RESTMapping for the workload type. The rest mapping contains a GroupVersionResource which can + // be used to fetch the workload mapping. + LookupRESTMapping(ctx context.Context, obj runtime.Object) (*meta.RESTMapping, error) + + // LookupWorkloadMapping the mapping template for the workload. Typically a ClusterWorkloadResourceMapping is defined for the + // workload's fully qualified resource `{resource}.{group}`. + LookupWorkloadMapping(ctx context.Context, gvr schema.GroupVersionResource) (*servicebindingv1beta1.ClusterWorkloadResourceMappingSpec, error) } diff --git a/projector/mapping.go b/projector/mapping.go index e387a438..af3695bc 100644 --- a/projector/mapping.go +++ b/projector/mapping.go @@ -19,28 +19,68 @@ package projector import ( "context" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" ) +// The workload's version is either directly matched, or the wildcard version `*` +// mapping template is returned. If no explicit mapping is found, a mapping appropriate for a PodSpecable resource may be used. +func MappingVersion(version string, mappings *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec) *servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate { + wildcardMapping := servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{Version: "*"} + var mapping *servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate + for _, v := range mappings.Versions { + switch v.Version { + case version: + mapping = &v + case "*": + wildcardMapping = v + } + } + if mapping == nil { + // use wildcard version by default + mapping = &wildcardMapping + } + + mapping = mapping.DeepCopy() + mapping.Default() + + return mapping +} + var _ MappingSource = (*staticMapping)(nil) type staticMapping struct { - mapping *servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate + workloadMapping *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec + restMapping *meta.RESTMapping } -// NewStaticMapping returns a single ClusterWorkloadResourceMappingTemplate for each lookup. It is useful for +// NewStaticMapping returns a single ClusterWorkloadResourceMappingSpec for each lookup. It is useful for // testing. -func NewStaticMapping(mapping *servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate) MappingSource { - mapping = mapping.DeepCopy() - mapping.Default() +func NewStaticMapping(wm *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec, rm *meta.RESTMapping) MappingSource { + if len(wm.Versions) == 0 { + wm.Versions = []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ + { + Version: "*", + }, + } + } + for i := range wm.Versions { + wm.Versions[i].Default() + } return &staticMapping{ - mapping: mapping, + workloadMapping: wm, + restMapping: rm, } } -func (m *staticMapping) LookupMapping(ctx context.Context, workload runtime.Object) (*servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate, error) { - return m.mapping, nil +func (m *staticMapping) LookupRESTMapping(ctx context.Context, obj runtime.Object) (*meta.RESTMapping, error) { + return m.restMapping, nil +} + +func (m *staticMapping) LookupWorkloadMapping(ctx context.Context, gvr schema.GroupVersionResource) (*servicebindingv1beta1.ClusterWorkloadResourceMappingSpec, error) { + return m.workloadMapping, nil } diff --git a/projector/metapodtemplate.go b/projector/metapodtemplate.go index 8a8c3b45..03250b72 100644 --- a/projector/metapodtemplate.go +++ b/projector/metapodtemplate.go @@ -35,9 +35,10 @@ type metaPodTemplate struct { workload runtime.Object mapping *servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate - Annotations map[string]string - Containers []metaContainer - Volumes []corev1.Volume + WorkloadAnnotations map[string]string + PodTemplateAnnotations map[string]string + Containers []metaContainer + Volumes []corev1.Volume } // metaContainer contains the aspects of a Container that are appropriate for service binding. @@ -55,9 +56,10 @@ func NewMetaPodTemplate(ctx context.Context, workload runtime.Object, mapping *s workload: workload, mapping: mapping, - Annotations: map[string]string{}, - Containers: []metaContainer{}, - Volumes: []corev1.Volume{}, + WorkloadAnnotations: map[string]string{}, + PodTemplateAnnotations: map[string]string{}, + Containers: []metaContainer{}, + Volumes: []corev1.Volume{}, } u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(workload) @@ -66,7 +68,10 @@ func NewMetaPodTemplate(ctx context.Context, workload runtime.Object, mapping *s } uv := reflect.ValueOf(u) - if err := mpt.getAt(mpt.mapping.Annotations, uv, &mpt.Annotations); err != nil { + if err := mpt.getAt(".metadata.annotations", uv, &mpt.WorkloadAnnotations); err != nil { + return nil, err + } + if err := mpt.getAt(mpt.mapping.Annotations, uv, &mpt.PodTemplateAnnotations); err != nil { return nil, err } for i := range mpt.mapping.Containers { @@ -120,7 +125,10 @@ func (mpt *metaPodTemplate) WriteToWorkload(ctx context.Context) error { } uv := reflect.ValueOf(u) - if err := mpt.setAt(mpt.mapping.Annotations, &mpt.Annotations, uv); err != nil { + if err := mpt.setAt(".metadata.annotations", &mpt.WorkloadAnnotations, uv); err != nil { + return err + } + if err := mpt.setAt(mpt.mapping.Annotations, &mpt.PodTemplateAnnotations, uv); err != nil { return err } ci := 0 diff --git a/projector/metapodtemplate_test.go b/projector/metapodtemplate_test.go index 81cc490c..a150f933 100644 --- a/projector/metapodtemplate_test.go +++ b/projector/metapodtemplate_test.go @@ -33,8 +33,11 @@ import ( ) func TestNewMetaPodTemplate(t *testing.T) { - testAnnotations := map[string]string{ - "key": "value", + testPodTemplateAnnotations := map[string]string{ + "hello": "podtemplate", + } + testWorkloadAnnotations := map[string]string{ + "hello": "workload", } testEnv := corev1.EnvVar{ Name: "NAME", @@ -64,10 +67,13 @@ func TestNewMetaPodTemplate(t *testing.T) { name: "podspecable", mapping: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: testWorkloadAnnotations, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Annotations: testAnnotations, + Annotations: testPodTemplateAnnotations, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ @@ -94,7 +100,8 @@ func TestNewMetaPodTemplate(t *testing.T) { }, }, expected: &metaPodTemplate{ - Annotations: testAnnotations, + WorkloadAnnotations: testWorkloadAnnotations, + PodTemplateAnnotations: testPodTemplateAnnotations, Containers: []metaContainer{ { Name: pointer.String("init-hello"), @@ -137,12 +144,15 @@ func TestNewMetaPodTemplate(t *testing.T) { Volumes: ".spec.jobTemplate.spec.template.spec.volumes", }, workload: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: testWorkloadAnnotations, + }, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Annotations: testAnnotations, + Annotations: testPodTemplateAnnotations, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ @@ -171,7 +181,8 @@ func TestNewMetaPodTemplate(t *testing.T) { }, }, expected: &metaPodTemplate{ - Annotations: testAnnotations, + WorkloadAnnotations: testWorkloadAnnotations, + PodTemplateAnnotations: testPodTemplateAnnotations, Containers: []metaContainer{ { Name: pointer.String("init-hello"), @@ -202,9 +213,10 @@ func TestNewMetaPodTemplate(t *testing.T) { mapping: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}, workload: &appsv1.Deployment{}, expected: &metaPodTemplate{ - Annotations: map[string]string{}, - Containers: []metaContainer{}, - Volumes: []corev1.Volume{}, + WorkloadAnnotations: map[string]string{}, + PodTemplateAnnotations: map[string]string{}, + Containers: []metaContainer{}, + Volumes: []corev1.Volume{}, }, }, { @@ -222,7 +234,8 @@ func TestNewMetaPodTemplate(t *testing.T) { }, }, expected: &metaPodTemplate{ - Annotations: map[string]string{}, + WorkloadAnnotations: map[string]string{}, + PodTemplateAnnotations: map[string]string{}, Containers: []metaContainer{ { Name: pointer.String(""), @@ -259,7 +272,8 @@ func TestNewMetaPodTemplate(t *testing.T) { }, }, expected: &metaPodTemplate{ - Annotations: map[string]string{}, + WorkloadAnnotations: map[string]string{}, + PodTemplateAnnotations: map[string]string{}, Containers: []metaContainer{ { Name: nil, @@ -280,10 +294,13 @@ func TestNewMetaPodTemplate(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: testWorkloadAnnotations, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Annotations: testAnnotations, + Annotations: testPodTemplateAnnotations, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -305,8 +322,9 @@ func TestNewMetaPodTemplate(t *testing.T) { }, }, expected: &metaPodTemplate{ - Annotations: testAnnotations, - Containers: []metaContainer{}, + WorkloadAnnotations: testWorkloadAnnotations, + PodTemplateAnnotations: testPodTemplateAnnotations, + Containers: []metaContainer{}, Volumes: []corev1.Volume{ testVolume, }, @@ -330,7 +348,7 @@ func TestNewMetaPodTemplate(t *testing.T) { Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Annotations: testAnnotations, + Annotations: testPodTemplateAnnotations, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -348,7 +366,8 @@ func TestNewMetaPodTemplate(t *testing.T) { }, }, expected: &metaPodTemplate{ - Annotations: map[string]string{}, + WorkloadAnnotations: map[string]string{}, + PodTemplateAnnotations: map[string]string{}, Containers: []metaContainer{ { Name: pointer.String(""), @@ -403,8 +422,11 @@ func TestNewMetaPodTemplate(t *testing.T) { } func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { - testAnnotations := map[string]string{ - "key": "value", + testPodTemplateAnnotations := map[string]string{ + "hello": "podtemplate", + } + testWorkloadAnnotations := map[string]string{ + "hello": "workload", } testEnv := corev1.EnvVar{ Name: "NAME", @@ -435,7 +457,8 @@ func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { name: "podspecable", mapping: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}, metadata: metaPodTemplate{ - Annotations: testAnnotations, + WorkloadAnnotations: testWorkloadAnnotations, + PodTemplateAnnotations: testPodTemplateAnnotations, Containers: []metaContainer{ { Name: pointer.String("init-hello"), @@ -477,10 +500,13 @@ func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: testWorkloadAnnotations, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Annotations: testAnnotations, + Annotations: testPodTemplateAnnotations, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ @@ -530,7 +556,8 @@ func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { Volumes: ".spec.jobTemplate.spec.template.spec.volumes", }, metadata: metaPodTemplate{ - Annotations: testAnnotations, + WorkloadAnnotations: testWorkloadAnnotations, + PodTemplateAnnotations: testPodTemplateAnnotations, Containers: []metaContainer{ { Name: pointer.String("init-hello"), @@ -576,13 +603,15 @@ func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { }, }, expected: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: testWorkloadAnnotations, + }, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Annotations: testAnnotations, + Annotations: testPodTemplateAnnotations, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ @@ -621,12 +650,16 @@ func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { name: "no containers", mapping: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}, metadata: metaPodTemplate{ - Annotations: map[string]string{}, - Containers: []metaContainer{}, - Volumes: []corev1.Volume{}, + WorkloadAnnotations: map[string]string{}, + PodTemplateAnnotations: map[string]string{}, + Containers: []metaContainer{}, + Volumes: []corev1.Volume{}, }, workload: &appsv1.Deployment{}, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -643,7 +676,8 @@ func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { name: "empty container", mapping: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{}, metadata: metaPodTemplate{ - Annotations: map[string]string{}, + WorkloadAnnotations: map[string]string{}, + PodTemplateAnnotations: map[string]string{}, Containers: []metaContainer{ { Name: pointer.String(""), @@ -665,6 +699,9 @@ func TestMetaPodTemplate_WriteToWorkload(t *testing.T) { }, }, expected: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ diff --git a/resolver/cluster.go b/resolver/cluster.go index fee27796..a554172b 100644 --- a/resolver/cluster.go +++ b/resolver/cluster.go @@ -22,9 +22,11 @@ import ( corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" 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/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" @@ -43,8 +45,8 @@ type clusterResolver struct { client client.Client } -func (m *clusterResolver) LookupMapping(ctx context.Context, workload runtime.Object) (*servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate, error) { - gvk, err := apiutil.GVKForObject(workload, m.client.Scheme()) +func (m *clusterResolver) LookupRESTMapping(ctx context.Context, obj runtime.Object) (*meta.RESTMapping, error) { + gvk, err := apiutil.GVKForObject(obj, m.client.Scheme()) if err != nil { return nil, err } @@ -52,34 +54,30 @@ func (m *clusterResolver) LookupMapping(ctx context.Context, workload runtime.Ob if err != nil { return nil, err } + return rm, nil +} + +func (m *clusterResolver) LookupWorkloadMapping(ctx context.Context, gvr schema.GroupVersionResource) (*servicebindingv1beta1.ClusterWorkloadResourceMappingSpec, error) { wrm := &servicebindingv1beta1.ClusterWorkloadResourceMapping{} - err = m.client.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s.%s", rm.Resource.Resource, rm.Resource.Group)}, wrm) - if err != nil { + + if err := m.client.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s.%s", gvr.Resource, gvr.Group)}, wrm); err != nil { if !apierrs.IsNotFound(err) { return nil, err } - } - - // find version mapping - wildcardMapping := servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{Version: "*"} - var mapping *servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate - for _, v := range wrm.Spec.Versions { - switch v.Version { - case gvk.Version: - mapping = &v - case "*": - wildcardMapping = v + wrm.Spec = servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ + { + Version: "*", + }, + }, } } - if mapping == nil { - // use wildcard version by default - mapping = &wildcardMapping - } - mapping = mapping.DeepCopy() - mapping.Default() + for i := range wrm.Spec.Versions { + wrm.Spec.Versions[i].Default() + } - return mapping, nil + return &wrm.Spec, nil } func (r *clusterResolver) LookupBindingSecret(ctx context.Context, serviceRef corev1.ObjectReference) (string, error) { diff --git a/resolver/cluster_test.go b/resolver/cluster_test.go index 8e532749..54582960 100644 --- a/resolver/cluster_test.go +++ b/resolver/cluster_test.go @@ -39,94 +39,158 @@ import ( "github.com/servicebinding/runtime/resolver" ) -func TestClusterResolver_LookupMapping(t *testing.T) { +func TestClusterResolver_LookupRESTMapping(t *testing.T) { scheme := runtime.NewScheme() utilruntime.Must(appsv1.AddToScheme(scheme)) utilruntime.Must(batchv1.AddToScheme(scheme)) utilruntime.Must(servicebindingv1beta1.AddToScheme(scheme)) + deploymentRESTMapping := &meta.RESTMapping{ + GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, + Scope: meta.RESTScopeNamespace, + } + cronJobRESTMapping := &meta.RESTMapping{ + GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"}, + Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"}, + Scope: meta.RESTScopeNamespace, + } + tests := []struct { name string givenObjects []client.Object workload client.Object - expected *servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate + expected *meta.RESTMapping expectedErr bool }{ { - name: "default mapping", + name: "deloyment mapping", givenObjects: []client.Object{}, workload: &appsv1.Deployment{}, - expected: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ - Version: "*", - Annotations: ".spec.template.metadata.annotations", - Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ - { - Path: ".spec.template.spec.initContainers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", + expected: deploymentRESTMapping, + }, + { + name: "cronjob mapping", + givenObjects: []client.Object{}, + workload: &batchv1.CronJob{}, + expected: cronJobRESTMapping, + }, + { + name: "error if workload type not found in scheme", + givenObjects: []client.Object{ + &servicebindingv1beta1.ClusterWorkloadResourceMapping{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myworkloads.workload.local", }, - { - Path: ".spec.template.spec.containers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", + Spec: servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ + { + Version: "*", + }, + }, }, }, - Volumes: ".spec.template.spec.volumes", }, + // this is a bogus workload type, but it's sufficient for the test (we need a type object that is not registered with the scheme) + workload: &networkingv1.Ingress{}, + expectedErr: true, }, { - name: "custom mapping", + name: "error if workload type not found in restmapper", givenObjects: []client.Object{ &servicebindingv1beta1.ClusterWorkloadResourceMapping{ ObjectMeta: metav1.ObjectMeta{ - Name: "cronjobs.batch", + Name: "myworkloads.workload.local", }, Spec: servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Version: "v1", - Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", - Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ - { - Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", - Name: ".name", - }, - { - Path: ".spec.jobTemplate.spec.template.spec.containers[*]", - Name: ".name", - }, - }, - Volumes: ".spec.jobTemplate.spec.template.spec.volumes", + Version: "*", }, }, }, }, }, - workload: &batchv1.CronJob{}, - expected: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ - Version: "v1", - Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", - Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ - { - Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", - }, + workload: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "workload.local/v1", + "kind": "MyWorkload", + }, + }, + expectedErr: true, + }, + } + + for _, c := range tests { + t.Run(c.name, func(t *testing.T) { + ctx := context.TODO() + + client := rtesting.NewFakeClient(scheme, c.givenObjects...) + restMapper := client.RESTMapper().(*meta.DefaultRESTMapper) + restMapper.Add(deploymentRESTMapping.GroupVersionKind, deploymentRESTMapping.Scope) + restMapper.Add(cronJobRESTMapping.GroupVersionKind, cronJobRESTMapping.Scope) + resolver := resolver.New(client) + + actual, err := resolver.LookupRESTMapping(ctx, c.workload) + + if (err != nil) != c.expectedErr { + t.Errorf("LookupRESTMapping() expected err: %v", err) + } + if c.expectedErr { + return + } + scopeComp := cmp.Comparer(func(a, b meta.RESTScope) bool { return a.Name() == b.Name() }) + if diff := cmp.Diff(c.expected, actual, scopeComp); diff != "" { + t.Errorf("LookupRESTMapping() gvr (-expected, +actual): %s", diff) + } + }) + } +} + +func TestClusterResolver_LookupWorkloadMapping(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(appsv1.AddToScheme(scheme)) + utilruntime.Must(batchv1.AddToScheme(scheme)) + utilruntime.Must(servicebindingv1beta1.AddToScheme(scheme)) + + tests := []struct { + name string + givenObjects []client.Object + gvr schema.GroupVersionResource + expected *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec + expectedRESTMapping *meta.RESTMapping + expectedErr bool + }{ + { + name: "default mapping", + givenObjects: []client.Object{}, + gvr: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, + expected: &servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Path: ".spec.jobTemplate.spec.template.spec.containers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", + Version: "*", + Annotations: ".spec.template.metadata.annotations", + Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ + { + Path: ".spec.template.spec.initContainers[*]", + Name: ".name", + Env: ".env", + VolumeMounts: ".volumeMounts", + }, + { + Path: ".spec.template.spec.containers[*]", + Name: ".name", + Env: ".env", + VolumeMounts: ".volumeMounts", + }, + }, + Volumes: ".spec.template.spec.volumes", }, }, - Volumes: ".spec.jobTemplate.spec.template.spec.volumes", }, }, { - name: "custom mapping with wildcard", + name: "custom mapping", givenObjects: []client.Object{ &servicebindingv1beta1.ClusterWorkloadResourceMapping{ ObjectMeta: metav1.ObjectMeta{ @@ -135,7 +199,7 @@ func TestClusterResolver_LookupMapping(t *testing.T) { Spec: servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Version: "*", + Version: "v1", Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ { @@ -153,29 +217,33 @@ func TestClusterResolver_LookupMapping(t *testing.T) { }, }, }, - workload: &batchv1.CronJob{}, - expected: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ - Version: "*", - Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", - Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ - { - Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", - }, + gvr: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"}, + expected: &servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Path: ".spec.jobTemplate.spec.template.spec.containers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", + Version: "v1", + Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", + Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ + { + Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", + Name: ".name", + Env: ".env", + VolumeMounts: ".volumeMounts", + }, + { + Path: ".spec.jobTemplate.spec.template.spec.containers[*]", + Name: ".name", + Env: ".env", + VolumeMounts: ".volumeMounts", + }, + }, + Volumes: ".spec.jobTemplate.spec.template.spec.volumes", }, }, - Volumes: ".spec.jobTemplate.spec.template.spec.volumes", }, }, { - name: "default mapping is used when resource version is not defined, and no wildcard is defined", + name: "custom mapping with wildcard", givenObjects: []client.Object{ &servicebindingv1beta1.ClusterWorkloadResourceMapping{ ObjectMeta: metav1.ObjectMeta{ @@ -184,7 +252,7 @@ func TestClusterResolver_LookupMapping(t *testing.T) { Spec: servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Version: "v1beta1", // the workload is version v1 + Version: "*", Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ { @@ -202,72 +270,30 @@ func TestClusterResolver_LookupMapping(t *testing.T) { }, }, }, - workload: &batchv1.CronJob{}, - expected: &servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ - Version: "*", - // default PodSpecable mapping, it won't actually work for a CronJob, - // but absent an explicit mapping, this is what's required. - Annotations: ".spec.template.metadata.annotations", - Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ - { - Path: ".spec.template.spec.initContainers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", - }, + gvr: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"}, + expected: &servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ + Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Path: ".spec.template.spec.containers[*]", - Name: ".name", - Env: ".env", - VolumeMounts: ".volumeMounts", - }, - }, - Volumes: ".spec.template.spec.volumes", - }, - }, - { - name: "error if workload type not found in scheme", - givenObjects: []client.Object{ - &servicebindingv1beta1.ClusterWorkloadResourceMapping{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myworkloads.workload.local", - }, - Spec: servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ - Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ + Version: "*", + Annotations: ".spec.jobTemplate.spec.template.metadata.annotations", + Containers: []servicebindingv1beta1.ClusterWorkloadResourceMappingContainer{ { - Version: "*", + Path: ".spec.jobTemplate.spec.template.spec.initContainers[*]", + Name: ".name", + Env: ".env", + VolumeMounts: ".volumeMounts", }, - }, - }, - }, - }, - // this is a bogus workload type, but it's sufficient for the test (we need a type object that is not registered with the scheme) - workload: &networkingv1.Ingress{}, - expectedErr: true, - }, - { - name: "error if workload type not found in restmapper", - givenObjects: []client.Object{ - &servicebindingv1beta1.ClusterWorkloadResourceMapping{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myworkloads.workload.local", - }, - Spec: servicebindingv1beta1.ClusterWorkloadResourceMappingSpec{ - Versions: []servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate{ { - Version: "*", + Path: ".spec.jobTemplate.spec.template.spec.containers[*]", + Name: ".name", + Env: ".env", + VolumeMounts: ".volumeMounts", }, }, + Volumes: ".spec.jobTemplate.spec.template.spec.volumes", }, }, }, - workload: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "workload.local/v1", - "kind": "MyWorkload", - }, - }, - expectedErr: true, }, } @@ -276,21 +302,18 @@ func TestClusterResolver_LookupMapping(t *testing.T) { ctx := context.TODO() client := rtesting.NewFakeClient(scheme, c.givenObjects...) - restMapper := client.RESTMapper().(*meta.DefaultRESTMapper) - restMapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace) - restMapper.Add(schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"}, meta.RESTScopeNamespace) resolver := resolver.New(client) - actual, err := resolver.LookupMapping(ctx, c.workload) + actual, err := resolver.LookupWorkloadMapping(ctx, c.gvr) if (err != nil) != c.expectedErr { - t.Errorf("LookupMapping() expected err: %v", err) + t.Errorf("LookupWorkloadMapping() expected err: %v", err) } if c.expectedErr { return } if diff := cmp.Diff(c.expected, actual); diff != "" { - t.Errorf("LookupMapping() (-expected, +actual): %s", diff) + t.Errorf("LookupWorkloadMapping() (-expected, +actual): %s", diff) } }) } diff --git a/resolver/interface.go b/resolver/interface.go index 528db7c1..f617f330 100644 --- a/resolver/interface.go +++ b/resolver/interface.go @@ -20,17 +20,22 @@ import ( "context" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" ) type Resolver interface { - // LookupMapping returns the mapping template for the workload. Typically a ClusterWorkloadResourceMapping is defined for the workload's - // fully qualified resource `{resource}.{group}`. The workload's version is either directly matched, or the wildcard version `*` - // mapping template is returned. If no explicit mapping is found, a mapping appropriate for a PodSpecable resource may be used. - LookupMapping(ctx context.Context, workload runtime.Object) (*servicebindingv1beta1.ClusterWorkloadResourceMappingTemplate, error) + // LookupRESTMapping returns the RESTMapping for the workload type. The rest mapping contains a GroupVersionResource which can + // be used to fetch the workload mapping. + LookupRESTMapping(ctx context.Context, obj runtime.Object) (*meta.RESTMapping, error) + + // LookupWorkloadMapping the mapping template for the workload. Typically a ClusterWorkloadResourceMapping is defined for the + // workload's fully qualified resource `{resource}.{group}`. + LookupWorkloadMapping(ctx context.Context, gvr schema.GroupVersionResource) (*servicebindingv1beta1.ClusterWorkloadResourceMappingSpec, error) // LookupBindingSecret returns the binding secret name exposed by the service following the Provisioned Service duck-type // (`.status.binding.name`). If a direction binding is used (where the referenced service is itself a Secret) the referenced Secret is