github.com/cilium/cilium@v1.16.2/pkg/k8s/apis/cilium.io/v2/validator/unknown_fields.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package validator 5 6 import ( 7 "encoding/json" 8 "errors" 9 "fmt" 10 "regexp" 11 12 "github.com/google/go-cmp/cmp" 13 "github.com/google/go-cmp/cmp/cmpopts" 14 "github.com/jeremywohl/flatten" 15 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 "k8s.io/apimachinery/pkg/runtime" 17 18 cilium_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 19 "github.com/cilium/cilium/pkg/logging/logfields" 20 ) 21 22 // detectUnknownFields will check the given policy against the expected policy 23 // for any unknown fields that were found. The expected policy is retrieved by 24 // taking the given policy, marshalling it into its Golang type (CNP or CCNP), 25 // and unmarshalling it back into bytes. We can rely on this process to strip 26 // away "unknown" fields (specifically, fields that don't have a `json:...` 27 // annotation), hence it is expected. Once it's in bytes, we convert it to an 28 // unstructured.Unstructured type so that it matches with the given policy. A 29 // diff is performed between the two to uncover any differences. 30 // 31 // Special treatment is given if a top-level description field is found, and 32 // returns ErrTopLevelDescriptionFound if so. This is likely to be the most 33 // common path of this function as we can usually rely on the validation of the 34 // CRDs themselves, however the top-level description field has been widely 35 // used incorrectly (see https://github.com/cilium/cilium/issues/13155). 36 // 37 // This function returns the following possible errors: 38 // - ErrTopLevelDescriptionFound 39 // - ErrUnknownFields 40 // - ErrUnknownKind 41 // - Other marshalling / unmarshalling errors 42 func detectUnknownFields(policy *unstructured.Unstructured) error { 43 kind := policy.GetKind() 44 45 scopedLog := log 46 switch kind { 47 case cilium_v2.CNPKindDefinition: 48 scopedLog = scopedLog.WithField(logfields.CiliumNetworkPolicyName, 49 policy.GetName()) 50 case cilium_v2.CCNPKindDefinition: 51 scopedLog = scopedLog.WithField(logfields.CiliumClusterwideNetworkPolicyName, 52 policy.GetName()) 53 default: 54 return ErrUnknownKind{ 55 kind: kind, 56 } 57 } 58 59 if _, ok := policy.Object["description"]; ok { 60 scopedLog.Warn(warnTopLevelDescriptionField) 61 return ErrTopLevelDescriptionFound 62 } 63 64 policyBytes, err := policy.MarshalJSON() 65 if err != nil { 66 return err 67 } 68 69 var filtered map[string]interface{} 70 switch kind { 71 case cilium_v2.CNPKindDefinition: 72 cnp := new(cilium_v2.CiliumNetworkPolicy) 73 if err := json.Unmarshal(policyBytes, cnp); err != nil { 74 return err 75 } 76 filtered, err = runtime.DefaultUnstructuredConverter.ToUnstructured(cnp) 77 case cilium_v2.CCNPKindDefinition: 78 ccnp := new(cilium_v2.CiliumClusterwideNetworkPolicy) 79 if err := json.Unmarshal(policyBytes, ccnp); err != nil { 80 return err 81 } 82 filtered, err = runtime.DefaultUnstructuredConverter.ToUnstructured(ccnp) 83 default: 84 // We've already validated above that there can only be two kinds: CNP 85 // & CCNP. This is likely to be a developer error if hit, so fatal. 86 scopedLog.WithField("kind", kind).Fatal("Unexpected kind found when processing policy") 87 } 88 if err != nil { 89 return err 90 } 91 92 given, err := getFields(policy.Object) 93 if err != nil { 94 return err 95 } 96 97 expected, err := getFields(filtered) 98 if err != nil { 99 return err 100 } 101 102 // Compare the expected policy with the given policy to find all unknown 103 // fields. 104 var r reporter 105 if !cmp.Equal( 106 expected, 107 given, 108 cmp.Reporter(&r), 109 cmpopts.SortSlices(func(o, n string) bool { 110 return o < n 111 }), 112 ) { 113 scopedLog.Warn(warnUnknownFields) 114 return ErrUnknownFields{ 115 extras: r.extras, 116 } 117 } 118 119 return nil 120 } 121 122 const ( 123 warnTopLevelDescriptionField = "It seems you have a policy with a " + 124 "top-level description. This field is no longer supported. Please migrate " + 125 "your policy's description field under `spec` or `specs`." 126 127 warnUnknownFields = "It seems you have a policy with extra unknown fields. " + 128 "Consider removing these fields, as they have no effect. The presence " + 129 "of these fields may have introduced a false sense security, so please " + 130 "check whether your policy is actually behaving as you expect." 131 ) 132 133 // ErrTopLevelDescriptionFound is the error returned if a policy contains a 134 // top-level description field. Instead this field should be moved to under 135 // (Rule).Description. 136 var ErrTopLevelDescriptionFound = errors.New("top-level description field found") 137 138 // ErrUnknownFields is an error representing the condition where unknown fields 139 // were found within a policy during validation. Fields that are not expected 140 // to be in the policy will be put inside the "extras" slice. 141 type ErrUnknownFields struct { 142 extras []string 143 } 144 145 func (e ErrUnknownFields) Error() string { 146 return fmt.Sprintf("unknown fields found, extra:%v", e.extras) 147 } 148 149 // ErrUnknownKind is an error representing an unknown Kubernetes object kind 150 // that is passed to the validator. 151 type ErrUnknownKind struct { 152 kind string 153 } 154 155 func (e ErrUnknownKind) Error() string { 156 return fmt.Sprintf("unknown kind %q", e.kind) 157 } 158 159 func getFields(u map[string]interface{}) ([]string, error) { 160 flat, err := flattenObject(u) 161 if err != nil { 162 return nil, err 163 } 164 165 // set is used as a lookup for whether we've already seen the field path. 166 // This is useful to dedup entries that match the "matchLabels" or 167 // "matchExpressions" field path. Without this lookup, we will return a 168 // slice containing duplicate entries. See example below. 169 // { 170 // "spec": { 171 // "endpointSelector": { 172 // "matchLabels": { 173 // "app": "", 174 // "key": "", 175 // "operator": "" 176 // } 177 // } 178 // } 179 // } 180 // => []string{"spec.endpointSelector.matchLabels", 181 // "spec.endpointSelector.matchLabels", 182 // "spec.endpointSelector.matchLabels"} 183 // Here we get an entry for each label inside "matchLabels". What we want 184 // is []string{"spec.endpointSelector.matchLabels"}. See comment inside the 185 // for-loop below for why we have to truncate the labels. 186 set := make(map[string]struct{}) 187 188 fields := make([]string, 0, len(flat)) 189 for f := range flat { 190 // Due to converting to Unstructured (same issue as ignoring fields 191 // below), we need to truncate any label under "matchLabels" or 192 // "matchExpressions", to effectively ignore the labels. This is 193 // because they are arbitrary as the user can specify anything they 194 // want. We will strip off the label, and keep the entire field path up 195 // to and including "matchLabels" or "matchExpressions", which we 196 // insert to the "fields" slice. 197 if matches := arbitraryLabelRegex.FindStringSubmatch(f); len(matches) > 1 { 198 m := matches[1] // matches[0] contains the full match 199 200 if _, seen := set[m]; !seen { 201 set[m] = struct{}{} // Mark as seen 202 fields = append(fields, m) 203 } 204 } else if !isIgnoredField(f) { 205 fields = append(fields, f) 206 } 207 } 208 209 return fields, nil 210 } 211 212 // arbitraryLabelRegex matches any field path that includes "matchLabels" or 213 // "matchExpressions". For example, it matches the following: 214 // - spec.endpointSelector.matchLabels.* 215 // - specs.0.ingress.0.fromEndpoints.0.matchLabels.* 216 // - specs.0.ingress.0.fromEndpoints.0.matchExpressions.* 217 var arbitraryLabelRegex = regexp.MustCompile(`^(.+\.(matchLabels|matchExpressions))\..+$`) 218 219 func flattenObject(obj map[string]interface{}) (map[string]interface{}, error) { 220 return flatten.Flatten(obj, "", flatten.DotStyle) 221 } 222 223 func isIgnoredField(f string) bool { 224 // We ignore the creation timestamp and the name because when marshalling 225 // and unmarshalling happens when converting to Unstructured, these fields 226 // are added in the "expected" policy. These fields missing should not be 227 // warned about. Specifically for "metadata.name", a CRD cannot be created 228 // without it, so in reality, we can rely on the CRD validation, hence it 229 // is safe to ignore at this level of the code. 230 return f == "metadata.creationTimestamp" || f == "metadata.name" 231 } 232 233 // reporter is a custom reporter adhering to the cmp.Reporter interface. 234 type reporter struct { 235 path cmp.Path 236 extras []string 237 } 238 239 func (r *reporter) PushStep(ps cmp.PathStep) { 240 r.path = append(r.path, ps) 241 } 242 243 func (r *reporter) Report(rs cmp.Result) { 244 if !rs.Equal() { 245 // The below call returns two values of the diff, vx & xy. In our case, 246 // vx represents a "missing" value (-) in the diff, and vy represents 247 // an "extra" value (+) in the diff. We ignore vx because it is not 248 // possible to have "missing" values, because the validator will catch 249 // "missing" or "required" values earlier on. We only care about 250 // "extra" values here. 251 _, vy := r.path.Last().Values() 252 if vy.IsValid() { 253 r.extras = append(r.extras, vy.String()) 254 } 255 } 256 } 257 258 func (r *reporter) PopStep() { 259 r.path = r.path[:len(r.path)-1] 260 }