github.xiaoq7.com/operator-framework/operator-sdk@v0.8.2/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/controller" 33 "sigs.k8s.io/controller-runtime/pkg/event" 34 crthandler "sigs.k8s.io/controller-runtime/pkg/handler" 35 "sigs.k8s.io/controller-runtime/pkg/manager" 36 crtpredicate "sigs.k8s.io/controller-runtime/pkg/predicate" 37 logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 38 "sigs.k8s.io/controller-runtime/pkg/source" 39 40 "github.com/operator-framework/operator-sdk/pkg/helm/release" 41 "github.com/operator-framework/operator-sdk/pkg/predicate" 42 ) 43 44 var log = logf.Log.WithName("helm.controller") 45 46 // WatchOptions contains the necessary values to create a new controller that 47 // manages helm releases in a particular namespace based on a GVK watch. 48 type WatchOptions struct { 49 Namespace string 50 GVK schema.GroupVersionKind 51 ManagerFactory release.ManagerFactory 52 ReconcilePeriod time.Duration 53 WatchDependentResources bool 54 } 55 56 // Add creates a new helm operator controller and adds it to the manager 57 func Add(mgr manager.Manager, options WatchOptions) error { 58 r := &HelmOperatorReconciler{ 59 Client: mgr.GetClient(), 60 GVK: options.GVK, 61 ManagerFactory: options.ManagerFactory, 62 ReconcilePeriod: options.ReconcilePeriod, 63 } 64 65 // Register the GVK with the schema 66 mgr.GetScheme().AddKnownTypeWithName(options.GVK, &unstructured.Unstructured{}) 67 metav1.AddToGroupVersion(mgr.GetScheme(), options.GVK.GroupVersion()) 68 69 controllerName := fmt.Sprintf("%v-controller", strings.ToLower(options.GVK.Kind)) 70 c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: r}) 71 if err != nil { 72 return err 73 } 74 75 o := &unstructured.Unstructured{} 76 o.SetGroupVersionKind(options.GVK) 77 if err := c.Watch(&source.Kind{Type: o}, &crthandler.EnqueueRequestForObject{}, predicate.GenerationChangedPredicate{}); err != nil { 78 return err 79 } 80 81 if options.WatchDependentResources { 82 watchDependentResources(mgr, r, c) 83 } 84 85 log.Info("Watching resource", "apiVersion", options.GVK.GroupVersion(), "kind", options.GVK.Kind, "namespace", options.Namespace, "reconcilePeriod", options.ReconcilePeriod.String()) 86 return nil 87 } 88 89 // watchDependentResources adds a release hook function to the HelmOperatorReconciler 90 // that adds watches for resources in released Helm charts. 91 func watchDependentResources(mgr manager.Manager, r *HelmOperatorReconciler, c controller.Controller) { 92 owner := &unstructured.Unstructured{} 93 owner.SetGroupVersionKind(r.GVK) 94 95 dependentPredicate := crtpredicate.Funcs{ 96 // We don't need to reconcile dependent resource creation events 97 // because dependent resources are only ever created during 98 // reconciliation. Another reconcile would be redundant. 99 CreateFunc: func(e event.CreateEvent) bool { 100 o := e.Object.(*unstructured.Unstructured) 101 log.V(1).Info("Skipping reconciliation for dependent resource creation", "name", o.GetName(), "namespace", o.GetNamespace(), "apiVersion", o.GroupVersionKind().GroupVersion(), "kind", o.GroupVersionKind().Kind) 102 return false 103 }, 104 105 // Reconcile when a dependent resource is deleted so that it can be 106 // recreated. 107 DeleteFunc: func(e event.DeleteEvent) bool { 108 o := e.Object.(*unstructured.Unstructured) 109 log.V(1).Info("Reconciling due to dependent resource deletion", "name", o.GetName(), "namespace", o.GetNamespace(), "apiVersion", o.GroupVersionKind().GroupVersion(), "kind", o.GroupVersionKind().Kind) 110 return true 111 }, 112 113 // Reconcile when a dependent resource is updated, so that it can 114 // be patched back to the resource managed by the Helm release, if 115 // necessary. Ignore updates that only change the status and 116 // resourceVersion. 117 UpdateFunc: func(e event.UpdateEvent) bool { 118 old := e.ObjectOld.(*unstructured.Unstructured).DeepCopy() 119 new := e.ObjectNew.(*unstructured.Unstructured).DeepCopy() 120 121 delete(old.Object, "status") 122 delete(new.Object, "status") 123 old.SetResourceVersion("") 124 new.SetResourceVersion("") 125 126 if reflect.DeepEqual(old.Object, new.Object) { 127 return false 128 } 129 log.V(1).Info("Reconciling due to dependent resource update", "name", new.GetName(), "namespace", new.GetNamespace(), "apiVersion", new.GroupVersionKind().GroupVersion(), "kind", new.GroupVersionKind().Kind) 130 return true 131 }, 132 } 133 134 var m sync.RWMutex 135 watches := map[schema.GroupVersionKind]struct{}{} 136 releaseHook := func(release *rpb.Release) error { 137 dec := yaml.NewDecoder(bytes.NewBufferString(release.GetManifest())) 138 for { 139 var u unstructured.Unstructured 140 err := dec.Decode(&u.Object) 141 if err == io.EOF { 142 return nil 143 } 144 if err != nil { 145 return err 146 } 147 148 gvk := u.GroupVersionKind() 149 m.RLock() 150 _, ok := watches[gvk] 151 m.RUnlock() 152 if ok { 153 continue 154 } 155 156 restMapper := mgr.GetRESTMapper() 157 depMapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) 158 if err != nil { 159 return err 160 } 161 ownerMapping, err := restMapper.RESTMapping(owner.GroupVersionKind().GroupKind(), owner.GroupVersionKind().Version) 162 if err != nil { 163 return err 164 } 165 166 depClusterScoped := depMapping.Scope.Name() == meta.RESTScopeNameRoot 167 ownerClusterScoped := ownerMapping.Scope.Name() == meta.RESTScopeNameRoot 168 169 if !ownerClusterScoped && depClusterScoped { 170 m.Lock() 171 watches[gvk] = struct{}{} 172 m.Unlock() 173 log.Info("Cannot watch cluster-scoped dependent resource for namespace-scoped owner. Changes to this dependent resource type will not be reconciled", 174 "ownerApiVersion", r.GVK.GroupVersion(), "ownerKind", r.GVK.Kind, "apiVersion", gvk.GroupVersion(), "kind", gvk.Kind) 175 continue 176 } 177 178 err = c.Watch(&source.Kind{Type: &u}, &crthandler.EnqueueRequestForOwner{OwnerType: owner}, dependentPredicate) 179 if err != nil { 180 return err 181 } 182 183 m.Lock() 184 watches[gvk] = struct{}{} 185 m.Unlock() 186 log.Info("Watching dependent resource", "ownerApiVersion", r.GVK.GroupVersion(), "ownerKind", r.GVK.Kind, "apiVersion", gvk.GroupVersion(), "kind", gvk.Kind) 187 } 188 } 189 r.releaseHook = releaseHook 190 }