...

Source file src/sigs.k8s.io/kustomize/api/filters/nameref/nameref.go

Documentation: sigs.k8s.io/kustomize/api/filters/nameref

     1  // Copyright 2022 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package nameref
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"sigs.k8s.io/kustomize/api/filters/fieldspec"
    11  	"sigs.k8s.io/kustomize/api/resmap"
    12  	"sigs.k8s.io/kustomize/api/resource"
    13  	"sigs.k8s.io/kustomize/api/types"
    14  	"sigs.k8s.io/kustomize/kyaml/errors"
    15  	"sigs.k8s.io/kustomize/kyaml/kio"
    16  	"sigs.k8s.io/kustomize/kyaml/resid"
    17  	"sigs.k8s.io/kustomize/kyaml/yaml"
    18  )
    19  
    20  // Filter updates a name references.
    21  type Filter struct {
    22  	// Referrer refers to another resource X by X's name.
    23  	// E.g. A Deployment can refer to a ConfigMap.
    24  	// The Deployment is the Referrer,
    25  	// the ConfigMap is the ReferralTarget.
    26  	// This filter seeks to repair the reference in Deployment, given
    27  	// that the ConfigMap's name may have changed.
    28  	Referrer *resource.Resource
    29  
    30  	// NameFieldToUpdate is the field in the Referrer
    31  	// that holds the name requiring an update.
    32  	// This is the field to write.
    33  	NameFieldToUpdate types.FieldSpec
    34  
    35  	// ReferralTarget is the source of the new value for
    36  	// the name, always in the 'metadata/name' field.
    37  	// This is the field to read.
    38  	ReferralTarget resid.Gvk
    39  
    40  	// Set of resources to scan to find the ReferralTarget.
    41  	ReferralCandidates resmap.ResMap
    42  }
    43  
    44  // At time of writing, in practice this is called with a slice with only
    45  // one entry, the node also referred to be the resource in the Referrer field.
    46  func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
    47  	return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
    48  }
    49  
    50  // The node passed in here is the same node as held in Referrer;
    51  // that's how the referrer's name field is updated.
    52  // Currently, however, this filter still needs the extra methods on Referrer
    53  // to consult things like the resource Id, its namespace, etc.
    54  // TODO(3455): No filter should use the Resource api; all information
    55  // about names should come from annotations, with helper methods
    56  // on the RNode object.  Resource should get stupider, RNode smarter.
    57  func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
    58  	if err := f.confirmNodeMatchesReferrer(node); err != nil {
    59  		// sanity check.
    60  		return nil, err
    61  	}
    62  	f.NameFieldToUpdate.Gvk = f.Referrer.GetGvk()
    63  	if err := node.PipeE(fieldspec.Filter{
    64  		FieldSpec: f.NameFieldToUpdate,
    65  		SetValue:  f.set,
    66  	}); err != nil {
    67  		return nil, errors.WrapPrefixf(
    68  			err, "updating name reference in '%s' field of '%s'",
    69  			f.NameFieldToUpdate.Path, f.Referrer.CurId().String())
    70  	}
    71  	return node, nil
    72  }
    73  
    74  // This function is called on the node found at FieldSpec.Path.
    75  // It's some node in the Referrer.
    76  func (f Filter) set(node *yaml.RNode) error {
    77  	if yaml.IsMissingOrNull(node) {
    78  		return nil
    79  	}
    80  	switch node.YNode().Kind {
    81  	case yaml.ScalarNode:
    82  		return f.setScalar(node)
    83  	case yaml.MappingNode:
    84  		return f.setMapping(node)
    85  	case yaml.SequenceNode:
    86  		return applyFilterToSeq(seqFilter{
    87  			setScalarFn:  f.setScalar,
    88  			setMappingFn: f.setMapping,
    89  		}, node)
    90  	default:
    91  		return fmt.Errorf("node must be a scalar, sequence or map")
    92  	}
    93  }
    94  
    95  // This method used when NameFieldToUpdate doesn't lead to
    96  // one scalar field (typically called 'name'), but rather
    97  // leads to a map field (called anything). In this case we
    98  // must complete the field path, looking for both  a 'name'
    99  // and a 'namespace' field to help select the proper
   100  // ReferralTarget to read the name and namespace from.
   101  func (f Filter) setMapping(node *yaml.RNode) error {
   102  	if node.YNode().Kind != yaml.MappingNode {
   103  		return fmt.Errorf("expect a mapping node")
   104  	}
   105  	nameNode, err := node.Pipe(yaml.FieldMatcher{Name: "name"})
   106  	if err != nil {
   107  		return errors.WrapPrefixf(err, "trying to match 'name' field")
   108  	}
   109  	if nameNode == nil {
   110  		// This is a _configuration_ error; the field path
   111  		// specified in NameFieldToUpdate.Path doesn't resolve
   112  		// to a map with a 'name' field, so we have no idea what
   113  		// field to update with a new name.
   114  		return fmt.Errorf("path config error; no 'name' field in node")
   115  	}
   116  	candidates, err := f.filterMapCandidatesByNamespace(node)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	oldName := nameNode.YNode().Value
   121  	// use allNamesAndNamespacesAreTheSame to compare referral candidates for functional identity,
   122  	// because we source both name and namespace values from the referral in this case.
   123  	referral, err := f.selectReferral(oldName, candidates, allNamesAndNamespacesAreTheSame)
   124  	if err != nil || referral == nil {
   125  		// Nil referral means nothing to do.
   126  		return err
   127  	}
   128  	f.recordTheReferral(referral)
   129  	if referral.GetName() == oldName && referral.GetNamespace() == "" {
   130  		// The name has not changed, nothing to do.
   131  		return nil
   132  	}
   133  	if err = node.PipeE(yaml.FieldSetter{
   134  		Name:        "name",
   135  		StringValue: referral.GetName(),
   136  	}); err != nil {
   137  		return err
   138  	}
   139  	if referral.GetNamespace() == "" {
   140  		// Don't write an empty string into the namespace field, as
   141  		// it should not replace the value "default".  The empty
   142  		// string is handled as a wild card here, not as an implicit
   143  		// specification of the "default" k8s namespace.
   144  		return nil
   145  	}
   146  	return node.PipeE(yaml.FieldSetter{
   147  		Name:        "namespace",
   148  		StringValue: referral.GetNamespace(),
   149  	})
   150  }
   151  
   152  func (f Filter) filterMapCandidatesByNamespace(
   153  	node *yaml.RNode) ([]*resource.Resource, error) {
   154  	namespaceNode, err := node.Pipe(yaml.FieldMatcher{Name: "namespace"})
   155  	if err != nil {
   156  		return nil, errors.WrapPrefixf(err, "trying to match 'namespace' field")
   157  	}
   158  	if namespaceNode == nil {
   159  		return f.ReferralCandidates.Resources(), nil
   160  	}
   161  	namespace := namespaceNode.YNode().Value
   162  	nsMap := f.ReferralCandidates.GroupedByOriginalNamespace()
   163  	if candidates, ok := nsMap[namespace]; ok {
   164  		return candidates, nil
   165  	}
   166  	nsMap = f.ReferralCandidates.GroupedByCurrentNamespace()
   167  	// This could be nil, or an empty list.
   168  	return nsMap[namespace], nil
   169  }
   170  
   171  func (f Filter) setScalar(node *yaml.RNode) error {
   172  	// use allNamesAreTheSame to compare referral candidates for functional identity,
   173  	// because we only source the name from the referral in this case.
   174  	referral, err := f.selectReferral(
   175  		node.YNode().Value, f.ReferralCandidates.Resources(), allNamesAreTheSame)
   176  	if err != nil || referral == nil {
   177  		// Nil referral means nothing to do.
   178  		return err
   179  	}
   180  	f.recordTheReferral(referral)
   181  	if referral.GetName() == node.YNode().Value {
   182  		// The name has not changed, nothing to do.
   183  		return nil
   184  	}
   185  	return node.PipeE(yaml.FieldSetter{StringValue: referral.GetName()})
   186  }
   187  
   188  // In the resource, make a note that it is referred to by the Referrer.
   189  func (f Filter) recordTheReferral(referral *resource.Resource) {
   190  	referral.AppendRefBy(f.Referrer.CurId())
   191  }
   192  
   193  // getRoleRefGvk returns a Gvk in the roleRef field. Return error
   194  // if the roleRef, roleRef/apiGroup or roleRef/kind is missing.
   195  func getRoleRefGvk(n *resource.Resource) (*resid.Gvk, error) {
   196  	roleRef, err := n.Pipe(yaml.Lookup("roleRef"))
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	if roleRef.IsNil() {
   201  		return nil, fmt.Errorf("roleRef cannot be found in %s", n.MustString())
   202  	}
   203  	apiGroup, err := roleRef.Pipe(yaml.Lookup("apiGroup"))
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	if apiGroup.IsNil() {
   208  		return nil, fmt.Errorf(
   209  			"apiGroup cannot be found in roleRef %s", roleRef.MustString())
   210  	}
   211  	kind, err := roleRef.Pipe(yaml.Lookup("kind"))
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	if kind.IsNil() {
   216  		return nil, fmt.Errorf(
   217  			"kind cannot be found in roleRef %s", roleRef.MustString())
   218  	}
   219  	return &resid.Gvk{
   220  		Group: apiGroup.YNode().Value,
   221  		Kind:  kind.YNode().Value,
   222  	}, nil
   223  }
   224  
   225  // sieveFunc returns true if the resource argument satisfies some criteria.
   226  type sieveFunc func(*resource.Resource) bool
   227  
   228  // doSieve uses a function to accept or ignore resources from a list.
   229  // If list is nil, returns immediately.
   230  // It's a filter obviously, but that term is overloaded here.
   231  func doSieve(list []*resource.Resource, fn sieveFunc) (s []*resource.Resource) {
   232  	for _, r := range list {
   233  		if fn(r) {
   234  			s = append(s, r)
   235  		}
   236  	}
   237  	return
   238  }
   239  
   240  func acceptAll(r *resource.Resource) bool {
   241  	return true
   242  }
   243  
   244  func previousNameMatches(name string) sieveFunc {
   245  	return func(r *resource.Resource) bool {
   246  		for _, id := range r.PrevIds() {
   247  			if id.Name == name {
   248  				return true
   249  			}
   250  		}
   251  		return false
   252  	}
   253  }
   254  
   255  func previousIdSelectedByGvk(gvk *resid.Gvk) sieveFunc {
   256  	return func(r *resource.Resource) bool {
   257  		for _, id := range r.PrevIds() {
   258  			if id.IsSelected(gvk) {
   259  				return true
   260  			}
   261  		}
   262  		return false
   263  	}
   264  }
   265  
   266  // If the we are updating a 'roleRef/name' field, the 'apiGroup' and 'kind'
   267  // fields in the same 'roleRef' map must be considered.
   268  // If either object is cluster-scoped, there can be a referral.
   269  // E.g. a RoleBinding (which exists in a namespace) can refer
   270  // to a ClusterRole (cluster-scoped) object.
   271  // https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole
   272  // Likewise, a ClusterRole can refer to a Secret (in a namespace).
   273  // Objects in different namespaces generally cannot refer to other
   274  // with some exceptions (e.g. RoleBinding and ServiceAccount are both
   275  // namespaceable, but the former can refer to accounts in other namespaces).
   276  func (f Filter) roleRefFilter() sieveFunc {
   277  	if !strings.HasSuffix(f.NameFieldToUpdate.Path, "roleRef/name") {
   278  		return acceptAll
   279  	}
   280  	roleRefGvk, err := getRoleRefGvk(f.Referrer)
   281  	if err != nil {
   282  		return acceptAll
   283  	}
   284  	return previousIdSelectedByGvk(roleRefGvk)
   285  }
   286  
   287  func prefixSuffixEquals(other resource.ResCtx, allowEmpty bool) sieveFunc {
   288  	return func(r *resource.Resource) bool {
   289  		return r.PrefixesSuffixesEquals(other, allowEmpty)
   290  	}
   291  }
   292  
   293  func (f Filter) sameCurrentNamespaceAsReferrer() sieveFunc {
   294  	referrerCurId := f.Referrer.CurId()
   295  	if referrerCurId.IsClusterScoped() {
   296  		// If the referrer is cluster-scoped, let anything through.
   297  		return acceptAll
   298  	}
   299  	return func(r *resource.Resource) bool {
   300  		if r.CurId().IsClusterScoped() {
   301  			// Allow cluster-scoped through.
   302  			return true
   303  		}
   304  		if r.GetKind() == "ServiceAccount" {
   305  			// Allow service accounts through, even though they
   306  			// are in a namespace.  A RoleBinding in another namespace
   307  			// can reference them.
   308  			return true
   309  		}
   310  		return referrerCurId.IsNsEquals(r.CurId())
   311  	}
   312  }
   313  
   314  // selectReferral picks the best referral from a list of candidates.
   315  func (f Filter) selectReferral(
   316  	// The name referral that may need to be updated.
   317  	oldName string,
   318  	candidates []*resource.Resource,
   319  	// function that returns whether two referrals are identical for the purposes of the transformation
   320  	candidatesIdentical func(resources []*resource.Resource) bool) (*resource.Resource, error) {
   321  	candidates = doSieve(candidates, previousNameMatches(oldName))
   322  	candidates = doSieve(candidates, previousIdSelectedByGvk(&f.ReferralTarget))
   323  	candidates = doSieve(candidates, f.roleRefFilter())
   324  	candidates = doSieve(candidates, f.sameCurrentNamespaceAsReferrer())
   325  	if len(candidates) == 1 {
   326  		return candidates[0], nil
   327  	}
   328  	candidates = doSieve(candidates, prefixSuffixEquals(f.Referrer, true))
   329  	if len(candidates) > 1 {
   330  		candidates = doSieve(candidates, prefixSuffixEquals(f.Referrer, false))
   331  	}
   332  	if len(candidates) == 1 {
   333  		return candidates[0], nil
   334  	}
   335  	if len(candidates) == 0 {
   336  		return nil, nil
   337  	}
   338  	if candidatesIdentical(candidates) {
   339  		// Just take the first one.
   340  		return candidates[0], nil
   341  	}
   342  	ids := getIds(candidates)
   343  	return nil, fmt.Errorf("found multiple possible referrals: %s\n%s", ids, f.failureDetails(candidates))
   344  }
   345  
   346  func (f Filter) failureDetails(resources []*resource.Resource) string {
   347  	msg := strings.Builder{}
   348  	msg.WriteString(fmt.Sprintf("\n**** Too many possible referral targets to referrer:\n%s\n", f.Referrer.MustYaml()))
   349  	for i, r := range resources {
   350  		msg.WriteString(fmt.Sprintf("--- possible referral %d:\n%s\n", i, r.MustYaml()))
   351  	}
   352  	return msg.String()
   353  }
   354  
   355  func allNamesAreTheSame(resources []*resource.Resource) bool {
   356  	name := resources[0].GetName()
   357  	for i := 1; i < len(resources); i++ {
   358  		if name != resources[i].GetName() {
   359  			return false
   360  		}
   361  	}
   362  	return true
   363  }
   364  
   365  func allNamesAndNamespacesAreTheSame(resources []*resource.Resource) bool {
   366  	name := resources[0].GetName()
   367  	namespace := resources[0].GetNamespace()
   368  	for i := 1; i < len(resources); i++ {
   369  		if name != resources[i].GetName() || namespace != resources[i].GetNamespace() {
   370  			return false
   371  		}
   372  	}
   373  	return true
   374  }
   375  
   376  func getIds(rs []*resource.Resource) string {
   377  	var result []string
   378  	for _, r := range rs {
   379  		result = append(result, r.CurId().String())
   380  	}
   381  	return strings.Join(result, ", ")
   382  }
   383  
   384  func checkEqual(k, a, b string) error {
   385  	if a != b {
   386  		return fmt.Errorf(
   387  			"node-referrerOriginal '%s' mismatch '%s' != '%s'",
   388  			k, a, b)
   389  	}
   390  	return nil
   391  }
   392  
   393  func (f Filter) confirmNodeMatchesReferrer(node *yaml.RNode) error {
   394  	meta, err := node.GetMeta()
   395  	if err != nil {
   396  		return err
   397  	}
   398  	gvk := f.Referrer.GetGvk()
   399  	if err = checkEqual(
   400  		"APIVersion", meta.APIVersion, gvk.ApiVersion()); err != nil {
   401  		return err
   402  	}
   403  	if err = checkEqual(
   404  		"Kind", meta.Kind, gvk.Kind); err != nil {
   405  		return err
   406  	}
   407  	if err = checkEqual(
   408  		"Name", meta.Name, f.Referrer.GetName()); err != nil {
   409  		return err
   410  	}
   411  	if err = checkEqual(
   412  		"Namespace", meta.Namespace, f.Referrer.GetNamespace()); err != nil {
   413  		return err
   414  	}
   415  	return nil
   416  }
   417  

View as plain text