diff --git a/apis/duck/groupversion_info.go b/apis/duck/groupversion_info.go new file mode 100644 index 00000000..a5f483bd --- /dev/null +++ b/apis/duck/groupversion_info.go @@ -0,0 +1,18 @@ +/* +Copyright 2023 the original author or authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +kubebuilder:object:generate=true +package duck diff --git a/apis/duck/podspecable_types.go b/apis/duck/podspecable_types.go new file mode 100644 index 00000000..9ad90d20 --- /dev/null +++ b/apis/duck/podspecable_types.go @@ -0,0 +1,42 @@ +/* +Copyright 2023 the original author or authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package duck + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +type PodSpecable struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PodSpecableSpec `json:"spec,omitempty"` +} + +type PodSpecableSpec struct { + Template corev1.PodTemplateSpec `json:"template,omitempty"` +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PodSpecable) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/apis/duck/zz_generated.deepcopy.go b/apis/duck/zz_generated.deepcopy.go new file mode 100644 index 00000000..029a224f --- /dev/null +++ b/apis/duck/zz_generated.deepcopy.go @@ -0,0 +1,55 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2022 the original author or authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package duck + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodSpecable) DeepCopyInto(out *PodSpecable) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodSpecable. +func (in *PodSpecable) DeepCopy() *PodSpecable { + if in == nil { + return nil + } + out := new(PodSpecable) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodSpecableSpec) DeepCopyInto(out *PodSpecableSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodSpecableSpec. +func (in *PodSpecableSpec) DeepCopy() *PodSpecableSpec { + if in == nil { + return nil + } + out := new(PodSpecableSpec) + in.DeepCopyInto(out) + return out +} diff --git a/controllers/servicebinding_controller.go b/controllers/servicebinding_controller.go index 1cb11b04..35c85189 100644 --- a/controllers/servicebinding_controller.go +++ b/controllers/servicebinding_controller.go @@ -32,8 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" - "github.com/servicebinding/runtime/projector" - "github.com/servicebinding/runtime/resolver" + "github.com/servicebinding/runtime/lifecycle" ) //+kubebuilder:rbac:groups=servicebinding.io,resources=servicebindings,verbs=get;list;watch;create;update;patch;delete @@ -42,14 +41,14 @@ import ( //+kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete // ServiceBindingReconciler reconciles a ServiceBinding object -func ServiceBindingReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler[*servicebindingv1beta1.ServiceBinding] { +func ServiceBindingReconciler(c reconcilers.Config, hooks lifecycle.ServiceBindingHooks) *reconcilers.ResourceReconciler[*servicebindingv1beta1.ServiceBinding] { return &reconcilers.ResourceReconciler[*servicebindingv1beta1.ServiceBinding]{ Reconciler: &reconcilers.WithFinalizer[*servicebindingv1beta1.ServiceBinding]{ Finalizer: servicebindingv1beta1.GroupVersion.Group + "/finalizer", Reconciler: reconcilers.Sequence[*servicebindingv1beta1.ServiceBinding]{ - ResolveBindingSecret(), - ResolveWorkloads(), - ProjectBinding(), + ResolveBindingSecret(hooks), + ResolveWorkloads(hooks), + ProjectBinding(hooks), PatchWorkloads(), }, }, @@ -58,7 +57,7 @@ func ServiceBindingReconciler(c reconcilers.Config) *reconcilers.ResourceReconci } } -func ResolveBindingSecret() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { +func ResolveBindingSecret(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { return &reconcilers.SyncReconciler[*servicebindingv1beta1.ServiceBinding]{ Name: "ResolveBindingSecret", Sync: func(ctx context.Context, resource *servicebindingv1beta1.ServiceBinding) error { @@ -70,7 +69,7 @@ func ResolveBindingSecret() reconcilers.SubReconciler[*servicebindingv1beta1.Ser Namespace: resource.Namespace, Name: resource.Spec.Service.Name, } - secretName, err := resolver.New(TrackingClient(c)).LookupBindingSecret(ctx, ref) + secretName, err := hooks.GetResolver(TrackingClient(c)).LookupBindingSecret(ctx, ref) if err != nil { if apierrs.IsNotFound(err) { // leave Unknown, the provisioned service may be created shortly @@ -116,7 +115,7 @@ func ResolveBindingSecret() reconcilers.SubReconciler[*servicebindingv1beta1.Ser } } -func ResolveWorkloads() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { +func ResolveWorkloads(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { return &reconcilers.SyncReconciler[*servicebindingv1beta1.ServiceBinding]{ Name: "ResolveWorkloads", SyncDuringFinalization: true, @@ -129,7 +128,7 @@ func ResolveWorkloads() reconcilers.SubReconciler[*servicebindingv1beta1.Service Namespace: resource.Namespace, Name: resource.Spec.Workload.Name, } - workloads, err := resolver.New(TrackingClient(c)).LookupWorkloads(ctx, ref, resource.Spec.Workload.Selector) + workloads, err := hooks.GetResolver(TrackingClient(c)).LookupWorkloads(ctx, ref, resource.Spec.Workload.Selector) if err != nil { if apierrs.IsNotFound(err) { // leave Unknown, the workload may be created shortly @@ -161,19 +160,30 @@ func ResolveWorkloads() reconcilers.SubReconciler[*servicebindingv1beta1.Service //+kubebuilder:rbac:groups=servicebinding.io,resources=clusterworkloadresourcemappings,verbs=get;list;watch -func ProjectBinding() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { +func ProjectBinding(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { return &reconcilers.SyncReconciler[*servicebindingv1beta1.ServiceBinding]{ Name: "ProjectBinding", SyncDuringFinalization: true, Sync: func(ctx context.Context, resource *servicebindingv1beta1.ServiceBinding) error { c := reconcilers.RetrieveConfigOrDie(ctx) - projector := projector.New(resolver.New(TrackingClient(c))) + projector := hooks.GetProjector(hooks.GetResolver(TrackingClient(c))) workloads := RetrieveWorkloads(ctx) projectedWorkloads := make([]runtime.Object, len(workloads)) + if f := hooks.ServiceBindingPreProjection; f != nil { + if err := f(ctx, resource); err != nil { + return err + } + } for i := range workloads { workload := workloads[i].DeepCopyObject() + + if f := hooks.WorkloadPreProjection; f != nil { + if err := f(ctx, workload); err != nil { + return err + } + } if !resource.DeletionTimestamp.IsZero() { if err := projector.Unproject(ctx, resource, workload); err != nil { return err @@ -183,8 +193,19 @@ func ProjectBinding() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBi return err } } + if f := hooks.WorkloadPostProjection; f != nil { + if err := f(ctx, workload); err != nil { + return err + } + } + projectedWorkloads[i] = workload } + if f := hooks.ServiceBindingPostProjection; f != nil { + if err := f(ctx, resource); err != nil { + return err + } + } StashProjectedWorkloads(ctx, projectedWorkloads) diff --git a/controllers/servicebinding_controller_test.go b/controllers/servicebinding_controller_test.go index e67566d8..288f664e 100644 --- a/controllers/servicebinding_controller_test.go +++ b/controllers/servicebinding_controller_test.go @@ -44,6 +44,7 @@ import ( servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" "github.com/servicebinding/runtime/controllers" dieservicebindingv1beta1 "github.com/servicebinding/runtime/dies/v1beta1" + "github.com/servicebinding/runtime/lifecycle" ) func TestServiceBindingReconciler(t *testing.T) { @@ -302,7 +303,7 @@ func TestServiceBindingReconciler(t *testing.T) { rts.Run(t, scheme, func(t *testing.T, tc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { restMapper := c.RESTMapper().(*meta.DefaultRESTMapper) restMapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace) - return controllers.ServiceBindingReconciler(c) + return controllers.ServiceBindingReconciler(c, lifecycle.ServiceBindingHooks{}) }) } @@ -526,7 +527,7 @@ func TestResolveBindingSecret(t *testing.T) { } rts.Run(t, scheme, func(t *testing.T, tc *rtesting.SubReconcilerTestCase[*servicebindingv1beta1.ServiceBinding], c reconcilers.Config) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { - return controllers.ResolveBindingSecret() + return controllers.ResolveBindingSecret(lifecycle.ServiceBindingHooks{}) }) } @@ -760,7 +761,7 @@ func TestResolveWorkload(t *testing.T) { } rts.Run(t, scheme, func(t *testing.T, tc *rtesting.SubReconcilerTestCase[*servicebindingv1beta1.ServiceBinding], c reconcilers.Config) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { - return controllers.ResolveWorkloads() + return controllers.ResolveWorkloads(lifecycle.ServiceBindingHooks{}) }) } @@ -931,7 +932,7 @@ func TestProjectBinding(t *testing.T) { rts.Run(t, scheme, func(t *testing.T, tc *rtesting.SubReconcilerTestCase[*servicebindingv1beta1.ServiceBinding], c reconcilers.Config) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] { restMapper := c.RESTMapper().(*meta.DefaultRESTMapper) restMapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace) - return controllers.ProjectBinding() + return controllers.ProjectBinding(lifecycle.ServiceBindingHooks{}) }) } diff --git a/controllers/webhook_controller.go b/controllers/webhook_controller.go index 68ef52d0..84e72e3d 100644 --- a/controllers/webhook_controller.go +++ b/controllers/webhook_controller.go @@ -38,9 +38,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" - "github.com/servicebinding/runtime/projector" + "github.com/servicebinding/runtime/lifecycle" "github.com/servicebinding/runtime/rbac" - "github.com/servicebinding/runtime/resolver" ) //+kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=mutatingwebhookconfigurations,verbs=get;list;watch;create;update;patch @@ -97,7 +96,7 @@ func AdmissionProjectorReconciler(c reconcilers.Config, name string, accessCheck } } -func AdmissionProjectorWebhook(c reconcilers.Config) *reconcilers.AdmissionWebhookAdapter[*unstructured.Unstructured] { +func AdmissionProjectorWebhook(c reconcilers.Config, hooks lifecycle.ServiceBindingHooks) *reconcilers.AdmissionWebhookAdapter[*unstructured.Unstructured] { return &reconcilers.AdmissionWebhookAdapter[*unstructured.Unstructured]{ Name: "AdmissionProjectorWebhook", Reconciler: &reconcilers.SyncReconciler[*unstructured.Unstructured]{ @@ -135,13 +134,33 @@ func AdmissionProjectorWebhook(c reconcilers.Config) *reconcilers.AdmissionWebho } // project active bindings into workload - projector := projector.New(resolver.New(c)) + if f := hooks.WorkloadPreProjection; f != nil { + if err := f(ctx, workload); err != nil { + return err + } + } + projector := hooks.GetProjector(hooks.GetResolver(c)) for i := range activeServiceBindings { sb := activeServiceBindings[i].DeepCopy() sb.Default() + if f := hooks.ServiceBindingPreProjection; f != nil { + if err := f(ctx, sb); err != nil { + return err + } + } if err := projector.Project(ctx, sb, workload); err != nil { return err } + if f := hooks.ServiceBindingPostProjection; f != nil { + if err := f(ctx, sb); err != nil { + return err + } + } + } + if f := hooks.WorkloadPostProjection; f != nil { + if err := f(ctx, workload); err != nil { + return err + } } return nil diff --git a/controllers/webhook_controller_test.go b/controllers/webhook_controller_test.go index 82ff200c..cd071033 100644 --- a/controllers/webhook_controller_test.go +++ b/controllers/webhook_controller_test.go @@ -55,6 +55,7 @@ import ( servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" "github.com/servicebinding/runtime/controllers" dieservicebindingv1beta1 "github.com/servicebinding/runtime/dies/v1beta1" + "github.com/servicebinding/runtime/lifecycle" "github.com/servicebinding/runtime/rbac" ) @@ -519,7 +520,7 @@ func TestAdmissionProjectorWebhook(t *testing.T) { wts.Run(t, scheme, func(t *testing.T, tc *rtesting.AdmissionWebhookTestCase, c reconcilers.Config) *admission.Webhook { restMapper := c.RESTMapper().(*meta.DefaultRESTMapper) restMapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace) - return controllers.AdmissionProjectorWebhook(c).Build() + return controllers.AdmissionProjectorWebhook(c, lifecycle.ServiceBindingHooks{}).Build() }) } diff --git a/lifecycle/hooks.go b/lifecycle/hooks.go new file mode 100644 index 00000000..17d966fa --- /dev/null +++ b/lifecycle/hooks.go @@ -0,0 +1,80 @@ +/* +Copyright 2023 the original author or authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lifecycle + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" + "github.com/servicebinding/runtime/projector" + "github.com/servicebinding/runtime/resolver" +) + +type ServiceBindingHooks struct { + // ResolverFactory returns a resolver which is used to lookup binding + // related values. + // + // +optional + ResolverFactory func(client.Client) resolver.Resolver + + // ProjectorFactory returns a projector which is used to bind/unbind the + // service to/from the workload. + // + // +optional + ProjectorFactory func(projector.MappingSource) projector.ServiceBindingProjector + + // ServiceBindingPreProjection can be used to alter the resolved + // ServiceBinding before the projection. + // + // +optional + ServiceBindingPreProjection func(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding) error + + // ServiceBindingPostProjection can be used to alter the projected + // ServiceBinding before mutations are persisted. + // + // +optional + ServiceBindingPostProjection func(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding) error + + // WorkloadPreProjection can be used to alter the resolved workload before + // the projection. + // + // +optional + WorkloadPreProjection func(ctx context.Context, workload runtime.Object) error + + // WorkloadPostProjection can be used to alter the projected workload + // before mutations are persisted. + // + // +optional + WorkloadPostProjection func(ctx context.Context, workload runtime.Object) error +} + +func (h *ServiceBindingHooks) GetResolver(c client.Client) resolver.Resolver { + if h.ResolverFactory == nil { + return resolver.New(c) + } + return h.ResolverFactory(c) +} + +func (h *ServiceBindingHooks) GetProjector(r projector.MappingSource) projector.ServiceBindingProjector { + if h.ProjectorFactory == nil { + return projector.New(r) + } + return h.ProjectorFactory(r) +} diff --git a/lifecycle/vmware/migration.go b/lifecycle/vmware/migration.go new file mode 100644 index 00000000..becc48e3 --- /dev/null +++ b/lifecycle/vmware/migration.go @@ -0,0 +1,205 @@ +/* +Copyright 2023 the original author or authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vmware + +import ( + "context" + "errors" + "fmt" + "regexp" + + "github.com/go-logr/logr" + "github.com/vmware-labs/reconciler-runtime/reconcilers" + 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" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/servicebinding/runtime/apis/duck" + servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" + "github.com/servicebinding/runtime/lifecycle" +) + +func InstallMigrationHook(hooks lifecycle.ServiceBindingHooks) lifecycle.ServiceBindingHooks { + serviceBindingPostProjection := hooks.ServiceBindingPostProjection + hooks.ServiceBindingPostProjection = func(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding) error { + if err := CleanupServiceBinding(ctx, binding); err != nil { + return err + } + if serviceBindingPostProjection != nil { + return serviceBindingPostProjection(ctx, binding) + } + return nil + } + + workloadPreProjection := hooks.WorkloadPreProjection + hooks.WorkloadPreProjection = func(ctx context.Context, workload runtime.Object) error { + if err := CleanupWorkload(ctx, workload); err != nil { + return err + } + if workloadPreProjection != nil { + return workloadPreProjection(ctx, workload) + } + return nil + } + + return hooks +} + +func CleanupServiceBinding(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding) error { + if reconcilers.RetrieveRequest(ctx).Name == "" { + // we're not in a reconciler + return nil + } + c, err := reconcilers.RetrieveConfig(ctx) + if err != nil { + // we're not in a reconciler + return nil + } + + log := logr.FromContextOrDiscard(ctx). + WithName("VMware"). + WithName("CleanupServiceBinding") + ctx = logr.NewContext(ctx, log) + + // drop Available and Projected conditions + conds := []metav1.Condition{} + for _, c := range binding.Status.Conditions { + if c.Type == "Available" || c.Type == "Projected" { + continue + } + conds = append(conds, c) + } + binding.Status.Conditions = conds + + // drop internal resource + mapping, err := c.RESTMapper().RESTMapping(schema.GroupKind{Group: "internal.bindings.labs.vmware.com", Kind: "ServiceBindingProjection"}, "v1alpha1") + if err != nil || mapping == nil { + if !errors.Is(err, &meta.NoKindMatchError{}) { + return err + } else { + return nil + } + } + + sbp := &unstructured.Unstructured{} + sbp.SetAPIVersion("internal.bindings.labs.vmware.com/v1alpha1") + sbp.SetKind("ServiceBindingProjection") + if err := c.Get(ctx, client.ObjectKey{Namespace: binding.Namespace, Name: binding.Name}, sbp); err != nil { + if !apierrs.IsNotFound(err) { + log.Error(err, "unable to load ServiceBindingProjection") + } + return nil + } + if sbp.GetDeletionTimestamp() == nil { + if err := c.Delete(ctx, sbp); err != nil { + log.Error(err, "unable to delete ServiceBindingProjection") + return nil + } + } + for _, finalizer := range sbp.GetFinalizers() { + if err := reconcilers.ClearFinalizer(ctx, sbp, finalizer); err != nil { + log.Error(err, "unable to clear finalizer for ServiceBindingProjection", "finalizer", finalizer) + return nil + } + } + + return nil +} + +var bindingAnnotationRE = regexp.MustCompile(`^internal\.bindings\.labs\.vmware\.com/projection-[0-9a-f]{40}$`) + +func CleanupWorkload(ctx context.Context, workload runtime.Object) error { + log := logr.FromContextOrDiscard(ctx). + WithName("VMware"). + WithName("CleanupWorkload") + ctx = logr.NewContext(ctx, log) + + cast := &reconcilers.CastResource[client.Object, *duck.PodSpecable]{ + Reconciler: &reconcilers.SyncReconciler[*duck.PodSpecable]{ + Sync: func(ctx context.Context, workload *duck.PodSpecable) error { + // the VMware implementation only bound into PodSpecable resources + for k, v := range workload.Annotations { + if !bindingAnnotationRE.MatchString(k) { + continue + } + + bindingDigest := k[40:] + secretName := v + cleanupBinding(workload, bindingDigest, secretName) + } + + return nil + }, + }, + } + + if _, err := cast.Reconcile(ctx, workload.(client.Object)); err != nil { + // bail out, but don't fail + log.Error(err, "unexpected error during cleanup") + return nil + } + + return nil +} + +func cleanupBinding(workload *duck.PodSpecable, bindingDigest, secretName string) { + volumeName := fmt.Sprintf("binding-%s", bindingDigest) + + volumes := []corev1.Volume{} + for _, volume := range workload.Spec.Template.Spec.Volumes { + if volume.Name != volumeName { + volumes = append(volumes, volume) + } + } + workload.Spec.Template.Spec.Volumes = volumes + + for i := range workload.Spec.Template.Spec.Containers { + cleanupBindingContainer(&workload.Spec.Template.Spec.Containers[i], bindingDigest, secretName) + } + for i := range workload.Spec.Template.Spec.InitContainers { + cleanupBindingContainer(&workload.Spec.Template.Spec.InitContainers[i], bindingDigest, secretName) + } + + delete(workload.Annotations, fmt.Sprintf("internal.bindings.labs.vmware.com/projection-%s", bindingDigest)) + delete(workload.Annotations, fmt.Sprintf("internal.bindings.labs.vmware.com/projection-%s-type", bindingDigest)) + delete(workload.Annotations, fmt.Sprintf("internal.bindings.labs.vmware.com/projection-%s-provider", bindingDigest)) +} + +func cleanupBindingContainer(container *corev1.Container, bindingDigest, secretName string) { + volumeName := fmt.Sprintf("binding-%s", bindingDigest) + + volumeMounts := []corev1.VolumeMount{} + for _, volumeMount := range container.VolumeMounts { + if volumeMount.Name != volumeName { + volumeMounts = append(volumeMounts, volumeMount) + } + } + container.VolumeMounts = volumeMounts + + env := []corev1.EnvVar{} + for _, envvar := range container.Env { + if envvar.ValueFrom != nil || envvar.ValueFrom.SecretKeyRef != nil || envvar.ValueFrom.SecretKeyRef.Name != secretName { + env = append(env, envvar) + } + } + container.Env = env +} diff --git a/main.go b/main.go index 5b7d9ac9..c7ce810e 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,8 @@ import ( servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" "github.com/servicebinding/runtime/controllers" + "github.com/servicebinding/runtime/lifecycle" + "github.com/servicebinding/runtime/lifecycle/vmware" "github.com/servicebinding/runtime/rbac" //+kubebuilder:scaffold:imports ) @@ -59,11 +61,14 @@ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string + var migrateFromVMware bool flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&migrateFromVMware, "migrate-from-vmware", false, + "Enable migration from the VMware implementation.") opts := zap.Options{ Development: true, } @@ -98,8 +103,17 @@ func main() { config := reconcilers.NewConfig(mgr, &servicebindingv1beta1.ServiceBinding{}, syncPeriod) accessChecker := rbac.NewAccessChecker(config, 5*time.Minute) + hooks := lifecycle.ServiceBindingHooks{} + if migrateFromVMware { + setupLog.Info("Enabling VMware migration hooks.") + setupLog.Info("Use migration hooks only while migrating implementations. Leaving migration hooks on permanently incurs a performance penalty.") + hooks = vmware.InstallMigrationHook(hooks) + } + // add additional migration hooks here + serviceBindingController, err := controllers.ServiceBindingReconciler( config, + hooks, ).SetupWithManagerYieldingController(ctx, mgr) if err != nil { setupLog.Error(err, "unable to create controller", "controller", "ServiceBinding") @@ -123,7 +137,7 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "AdmissionProjector") os.Exit(1) } - mgr.GetWebhookServer().Register("/interceptor", controllers.AdmissionProjectorWebhook(config).Build()) + mgr.GetWebhookServer().Register("/interceptor", controllers.AdmissionProjectorWebhook(config, hooks).Build()) if err = controllers.TriggerReconciler( config,