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  }