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