istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/helmreconciler/apply.go (about) 1 // Copyright Istio 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 helmreconciler 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 "sigs.k8s.io/controller-runtime/pkg/client" 24 25 "istio.io/istio/operator/pkg/cache" 26 "istio.io/istio/operator/pkg/metrics" 27 "istio.io/istio/operator/pkg/name" 28 "istio.io/istio/operator/pkg/object" 29 "istio.io/istio/operator/pkg/util" 30 "istio.io/istio/operator/pkg/util/progress" 31 ) 32 33 const fieldOwnerOperator = "istio-operator" 34 35 // AppliedResult is the result of applying a Manifest. 36 type AppliedResult struct { 37 // processedObjects is the list of objects that were processed in this apply operation. 38 processedObjects object.K8sObjects 39 // deployed is the number of objects have been deployed which means 40 // it's in the cache and it's not changed from the cache. 41 deployed int 42 } 43 44 // Succeed returns true if the apply operation succeeded. 45 func (r AppliedResult) Succeed() bool { 46 return len(r.processedObjects) > 0 || r.deployed > 0 47 } 48 49 // ApplyManifest applies the manifest to create or update resources. It returns the processed (created or updated) 50 // objects and the number of objects in the manifests. 51 func (h *HelmReconciler) ApplyManifest(manifest name.Manifest) (result AppliedResult, _ error) { 52 cname := string(manifest.Name) 53 crHash, err := h.getCRHash(cname) 54 if err != nil { 55 return result, err 56 } 57 58 scope.Infof("Processing resources from manifest: %s for CR %s", cname, crHash) 59 allObjects, err := object.ParseK8sObjectsFromYAMLManifest(manifest.Content) 60 if err != nil { 61 return result, err 62 } 63 64 objectCache := cache.GetCache(crHash) 65 66 // Ensure that for a given CR crHash only one control loop uses the per-crHash cache at any time. 67 objectCache.Mu.Lock() 68 defer objectCache.Mu.Unlock() 69 70 // No further locking required beyond this point, since we have a ptr to a cache corresponding to a CR crHash and no 71 // other controller is allowed to work on at the same time. 72 var changedObjects object.K8sObjects 73 var changedObjectKeys []string 74 allObjectsMap := make(map[string]bool) 75 76 // Check which objects in the manifest have changed from those in the cache. 77 for _, obj := range allObjects { 78 oh := obj.Hash() 79 allObjectsMap[oh] = true 80 if co, ok := objectCache.Cache[oh]; ok && obj.Equal(co) { 81 // Object is in the cache and unchanged. 82 metrics.AddResource(obj.FullName(), obj.GroupVersionKind().GroupKind()) 83 result.deployed++ 84 continue 85 } 86 changedObjects = append(changedObjects, obj) 87 changedObjectKeys = append(changedObjectKeys, oh) 88 } 89 90 var plog *progress.ManifestLog 91 if len(changedObjectKeys) > 0 { 92 plog = h.opts.ProgressLog.NewComponent(cname) 93 scope.Infof("The following objects differ between generated manifest and cache: \n - %s", strings.Join(changedObjectKeys, "\n - ")) 94 } else { 95 scope.Infof("Generated manifest objects are the same as cached for component %s.", cname) 96 } 97 98 // Objects are applied in groups: namespaces, CRDs, everything else, with wait for ready in between. 99 nsObjs := object.KindObjects(changedObjects, name.NamespaceStr) 100 crdObjs := object.KindObjects(changedObjects, name.CRDStr) 101 otherObjs := object.ObjectsNotInLists(changedObjects, nsObjs, crdObjs) 102 for _, objList := range []object.K8sObjects{nsObjs, crdObjs, otherObjs} { 103 // For a given group of objects, apply in sorted order of priority with no wait in between. 104 objList.Sort(object.DefaultObjectOrder()) 105 for _, obj := range objList { 106 obju := obj.UnstructuredObject() 107 if err := h.applyLabelsAndAnnotations(obju, cname); err != nil { 108 return result, err 109 } 110 if err := h.ApplyObject(obj.UnstructuredObject()); err != nil { 111 plog.ReportError(err.Error()) 112 return result, err 113 } 114 plog.ReportProgress() 115 metrics.AddResource(obj.FullName(), obj.GroupVersionKind().GroupKind()) 116 result.processedObjects = append(result.processedObjects, obj) 117 // Update the cache with the latest object. 118 objectCache.Cache[obj.Hash()] = obj 119 } 120 } 121 122 // Prune anything not in the manifest out of the cache. 123 var removeKeys []string 124 for k := range objectCache.Cache { 125 if !allObjectsMap[k] { 126 removeKeys = append(removeKeys, k) 127 } 128 } 129 for _, k := range removeKeys { 130 scope.Infof("Pruning object %s from cache.", k) 131 delete(objectCache.Cache, k) 132 } 133 134 if len(changedObjectKeys) > 0 { 135 err := WaitForResources(result.processedObjects, h.kubeClient, 136 h.opts.WaitTimeout, h.opts.DryRun, plog) 137 if err != nil { 138 werr := fmt.Errorf("failed to wait for resource: %v", err) 139 plog.ReportError(werr.Error()) 140 return result, werr 141 } 142 plog.ReportFinished() 143 144 } 145 return result, nil 146 } 147 148 // ApplyObject creates or updates an object in the API server depending on whether it already exists. 149 // It mutates obj. 150 func (h *HelmReconciler) ApplyObject(obj *unstructured.Unstructured) error { 151 if obj.GetKind() == "List" { 152 var errs util.Errors 153 list, err := obj.ToList() 154 if err != nil { 155 scope.Errorf("error converting List object: %s", err) 156 return err 157 } 158 for _, item := range list.Items { 159 err = h.ApplyObject(&item) 160 if err != nil { 161 errs = util.AppendErr(errs, err) 162 } 163 } 164 return errs.ToError() 165 } 166 167 objectStr := fmt.Sprintf("%s/%s/%s", obj.GetKind(), obj.GetNamespace(), obj.GetName()) 168 169 if scope.DebugEnabled() { 170 scope.Debugf("Processing object:\n%s\n\n", util.ToYAML(obj)) 171 } 172 173 if h.opts.DryRun { 174 scope.Infof("Not applying object %s because of dry run.", objectStr) 175 return nil 176 } 177 178 return h.serverSideApply(obj) 179 } 180 181 // use server-side apply, require kubernetes 1.16+ 182 func (h *HelmReconciler) serverSideApply(obj *unstructured.Unstructured) error { 183 objectStr := fmt.Sprintf("%s/%s/%s", obj.GetKind(), obj.GetNamespace(), obj.GetName()) 184 scope.Infof("using server side apply to update obj: %v", objectStr) 185 opts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(fieldOwnerOperator)} 186 if err := h.client.Patch(context.TODO(), obj, client.Apply, opts...); err != nil { 187 return fmt.Errorf("failed to update resource with server-side apply for obj %v: %v", objectStr, err) 188 } 189 return nil 190 }