sigs.k8s.io/cluster-api@v1.7.1/internal/util/ssa/filterintent.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package ssa 18 19 import ( 20 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 22 "sigs.k8s.io/cluster-api/internal/contract" 23 ) 24 25 // FilterObjectInput holds info required while filtering the object. 26 type FilterObjectInput struct { 27 // AllowedPaths instruct FilterObject to ignore everything except given paths. 28 AllowedPaths []contract.Path 29 30 // IgnorePaths instruct FilterObject to ignore given paths. 31 // NOTE: IgnorePaths are used to filter out fields nested inside AllowedPaths, e.g. 32 // spec.ControlPlaneEndpoint. 33 // NOTE: ignore paths which point to an array are not supported by the current implementation. 34 IgnorePaths []contract.Path 35 } 36 37 // FilterObject filter out changes not relevant for the controller. 38 func FilterObject(obj *unstructured.Unstructured, input *FilterObjectInput) { 39 // filter out changes not in the allowed paths (fields to not consider, e.g. status); 40 if len(input.AllowedPaths) > 0 { 41 FilterIntent(&FilterIntentInput{ 42 Path: contract.Path{}, 43 Value: obj.Object, 44 ShouldFilter: IsPathNotAllowed(input.AllowedPaths), 45 }) 46 } 47 48 // filter out changes for ignore paths (well known fields owned by other controllers, e.g. 49 // spec.controlPlaneEndpoint in the InfrastructureCluster object); 50 if len(input.IgnorePaths) > 0 { 51 FilterIntent(&FilterIntentInput{ 52 Path: contract.Path{}, 53 Value: obj.Object, 54 ShouldFilter: IsPathIgnored(input.IgnorePaths), 55 }) 56 } 57 } 58 59 // FilterIntent ensures that object only includes the fields and values for which the controller has an opinion, 60 // and filter out everything else by removing it from the Value. 61 // NOTE: This func is called recursively only for fields of type Map, but this is ok given the current use cases 62 // this func has to address. More specifically, we are using this func for filtering out not allowed paths and for ignore paths; 63 // all of them are defined in reconcile_state.go and are targeting well-known fields inside nested maps. 64 // Allowed paths / ignore paths which point to an array are not supported by the current implementation. 65 func FilterIntent(ctx *FilterIntentInput) bool { 66 value, ok := ctx.Value.(map[string]interface{}) 67 if !ok { 68 return false 69 } 70 71 gotDeletions := false 72 for field := range value { 73 fieldCtx := &FilterIntentInput{ 74 // Compose the Path for the nested field. 75 Path: ctx.Path.Append(field), 76 // Gets the original and the modified Value for the field. 77 Value: value[field], 78 // Carry over global values from the context. 79 ShouldFilter: ctx.ShouldFilter, 80 } 81 82 // If the field should be filtered out, delete it from the modified object. 83 if fieldCtx.ShouldFilter(fieldCtx.Path) { 84 delete(value, field) 85 gotDeletions = true 86 continue 87 } 88 89 // Process nested fields and get in return if FilterIntent removed fields. 90 if FilterIntent(fieldCtx) { 91 // Ensure we are not leaving empty maps around. 92 if v, ok := fieldCtx.Value.(map[string]interface{}); ok && len(v) == 0 { 93 delete(value, field) 94 gotDeletions = true 95 } 96 } 97 } 98 return gotDeletions 99 } 100 101 // FilterIntentInput holds info required while filtering the intent for server side apply. 102 // NOTE: in server side apply an intent is a partial object that only includes the fields and values for which the user has an opinion. 103 type FilterIntentInput struct { 104 // the Path of the field being processed. 105 Path contract.Path 106 107 // the Value for the current Path. 108 Value interface{} 109 110 // ShouldFilter handle the func that determine if the current Path should be dropped or not. 111 ShouldFilter func(path contract.Path) bool 112 } 113 114 // IsPathAllowed returns true when the Path is one of the AllowedPaths. 115 func IsPathAllowed(allowedPaths []contract.Path) func(path contract.Path) bool { 116 return func(path contract.Path) bool { 117 for _, p := range allowedPaths { 118 // NOTE: we allow everything Equal or one IsParentOf one of the allowed paths. 119 // e.g. if allowed Path is metadata.labels, we allow both metadata and metadata.labels; 120 // this is required because allowed Path is called recursively. 121 if path.Overlaps(p) { 122 return true 123 } 124 } 125 return false 126 } 127 } 128 129 // IsPathNotAllowed returns true when the Path is NOT one of the AllowedPaths. 130 func IsPathNotAllowed(allowedPaths []contract.Path) func(path contract.Path) bool { 131 return func(path contract.Path) bool { 132 isAllowed := IsPathAllowed(allowedPaths) 133 return !isAllowed(path) 134 } 135 } 136 137 // IsPathIgnored returns true when the Path is one of the IgnorePaths. 138 func IsPathIgnored(ignorePaths []contract.Path) func(path contract.Path) bool { 139 return func(path contract.Path) bool { 140 for _, p := range ignorePaths { 141 if path.Equal(p) { 142 return true 143 } 144 } 145 return false 146 } 147 }