github.com/fabianvf/ocp-release-operator-sdk@v0.0.0-20190426141702-57620ee2f090/pkg/helm/controller/controller.go (about) 1 // Copyright 2018 The Operator-SDK Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package controller 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "reflect" 22 "strings" 23 "sync" 24 "time" 25 26 yaml "gopkg.in/yaml.v2" 27 "k8s.io/apimachinery/pkg/api/meta" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 rpb "k8s.io/helm/pkg/proto/hapi/release" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 "sigs.k8s.io/controller-runtime/pkg/controller" 34 "sigs.k8s.io/controller-runtime/pkg/event" 35 crthandler "sigs.k8s.io/controller-runtime/pkg/handler" 36 "sigs.k8s.io/controller-runtime/pkg/manager" 37 crtpredicate "sigs.k8s.io/controller-runtime/pkg/predicate" 38 logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 39 "sigs.k8s.io/controller-runtime/pkg/source" 40 41 "github.com/operator-framework/operator-sdk/pkg/helm/release" 42 "github.com/operator-framework/operator-sdk/pkg/predicate" 43 ) 44 45 var log = logf.Log.WithName("helm.controller") 46 47 // WatchOptions contains the necessary values to create a new controller that 48 // manages helm releases in a particular namespace based on a GVK watch. 49 type WatchOptions struct { 50 Namespace string 51 GVK schema.GroupVersionKind 52 ManagerFactory release.ManagerFactory 53 ReconcilePeriod time.Duration 54 WatchDependentResources bool 55 } 56 57 // Add creates a new helm operator controller and adds it to the manager 58 func Add(mgr manager.Manager, options WatchOptions) error { 59 r := &HelmOperatorReconciler{ 60 // The default client will use the DelegatingReader for reads 61 // this forces it to use the cache for unstructured types. 62 Client: client.DelegatingClient{ 63 Reader: mgr.GetCache(), 64 Writer: mgr.GetClient(), 65 StatusClient: mgr.GetClient(), 66 }, 67 GVK: options.GVK, 68 ManagerFactory: options.ManagerFactory, 69 ReconcilePeriod: options.ReconcilePeriod, 70 } 71 72 // Register the GVK with the schema 73 mgr.GetScheme().AddKnownTypeWithName(options.GVK, &unstructured.Unstructured{}) 74 metav1.AddToGroupVersion(mgr.GetScheme(), options.GVK.GroupVersion()) 75 76 controllerName := fmt.Sprintf("%v-controller", strings.ToLower(options.GVK.Kind)) 77 c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: r}) 78 if err != nil { 79 return err 80 } 81 82 o := &unstructured.Unstructured{} 83 o.SetGroupVersionKind(options.GVK) 84 if err := c.Watch(&source.Kind{Type: o}, &crthandler.EnqueueRequestForObject{}, predicate.GenerationChangedPredicate{}); err != nil { 85 return err 86 } 87 88 if options.WatchDependentResources { 89 watchDependentResources(mgr, r, c) 90 } 91 92 log.Info("Watching resource", "apiVersion", options.GVK.GroupVersion(), "kind", options.GVK.Kind, "namespace", options.Namespace, "reconcilePeriod", options.ReconcilePeriod.String()) 93 return nil 94 } 95 96 // watchDependentResources adds a release hook function to the HelmOperatorReconciler 97 // that adds watches for resources in released Helm charts. 98 func watchDependentResources(mgr manager.Manager, r *HelmOperatorReconciler, c controller.Controller) { 99 owner := &unstructured.Unstructured{} 100 owner.SetGroupVersionKind(r.GVK) 101 102 dependentPredicate := crtpredicate.Funcs{ 103 // We don't need to reconcile dependent resource creation events 104 // because dependent resources are only ever created during 105 // reconciliation. Another reconcile would be redundant. 106 CreateFunc: func(e event.CreateEvent) bool { 107 o := e.Object.(*unstructured.Unstructured) 108 log.V(1).Info("Skipping reconciliation for dependent resource creation", "name", o.GetName(), "namespace", o.GetNamespace(), "apiVersion", o.GroupVersionKind().GroupVersion(), "kind", o.GroupVersionKind().Kind) 109 return false 110 }, 111 112 // Reconcile when a dependent resource is deleted so that it can be 113 // recreated. 114 DeleteFunc: func(e event.DeleteEvent) bool { 115 o := e.Object.(*unstructured.Unstructured) 116 log.V(1).Info("Reconciling due to dependent resource deletion", "name", o.GetName(), "namespace", o.GetNamespace(), "apiVersion", o.GroupVersionKind().GroupVersion(), "kind", o.GroupVersionKind().Kind) 117 return true 118 }, 119 120 // Reconcile when a dependent resource is updated, so that it can 121 // be patched back to the resource managed by the Helm release, if 122 // necessary. Ignore updates that only change the status and 123 // resourceVersion. 124 UpdateFunc: func(e event.UpdateEvent) bool { 125 old := e.ObjectOld.(*unstructured.Unstructured).DeepCopy() 126 new := e.ObjectNew.(*unstructured.Unstructured).DeepCopy() 127 128 delete(old.Object, "status") 129 delete(new.Object, "status") 130 old.SetResourceVersion("") 131 new.SetResourceVersion("") 132 133 if reflect.DeepEqual(old.Object, new.Object) { 134 return false 135 } 136 log.V(1).Info("Reconciling due to dependent resource update", "name", new.GetName(), "namespace", new.GetNamespace(), "apiVersion", new.GroupVersionKind().GroupVersion(), "kind", new.GroupVersionKind().Kind) 137 return true 138 }, 139 } 140 141 var m sync.RWMutex 142 watches := map[schema.GroupVersionKind]struct{}{} 143 releaseHook := func(release *rpb.Release) error { 144 dec := yaml.NewDecoder(bytes.NewBufferString(release.GetManifest())) 145 for { 146 var u unstructured.Unstructured 147 err := dec.Decode(&u.Object) 148 if err == io.EOF { 149 return nil 150 } 151 if err != nil { 152 return err 153 } 154 155 gvk := u.GroupVersionKind() 156 m.RLock() 157 _, ok := watches[gvk] 158 m.RUnlock() 159 if ok { 160 continue 161 } 162 163 restMapper := mgr.GetRESTMapper() 164 depMapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) 165 if err != nil { 166 return err 167 } 168 ownerMapping, err := restMapper.RESTMapping(owner.GroupVersionKind().GroupKind(), owner.GroupVersionKind().Version) 169 if err != nil { 170 return err 171 } 172 173 depClusterScoped := depMapping.Scope.Name() == meta.RESTScopeNameRoot 174 ownerClusterScoped := ownerMapping.Scope.Name() == meta.RESTScopeNameRoot 175 176 if !ownerClusterScoped && depClusterScoped { 177 m.Lock() 178 watches[gvk] = struct{}{} 179 m.Unlock() 180 log.Info("Cannot watch cluster-scoped dependent resource for namespace-scoped owner. Changes to this dependent resource type will not be reconciled", 181 "ownerApiVersion", r.GVK.GroupVersion(), "ownerKind", r.GVK.Kind, "apiVersion", gvk.GroupVersion(), "kind", gvk.Kind) 182 continue 183 } 184 185 err = c.Watch(&source.Kind{Type: &u}, &crthandler.EnqueueRequestForOwner{OwnerType: owner}, dependentPredicate) 186 if err != nil { 187 return err 188 } 189 190 m.Lock() 191 watches[gvk] = struct{}{} 192 m.Unlock() 193 log.Info("Watching dependent resource", "ownerApiVersion", r.GVK.GroupVersion(), "ownerKind", r.GVK.Kind, "apiVersion", gvk.GroupVersion(), "kind", gvk.Kind) 194 } 195 } 196 r.releaseHook = releaseHook 197 }