sigs.k8s.io/cluster-api@v1.6.3/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  }