github.com/kiali/kiali@v1.84.0/business/checkers/destinationrules/multi_match_checker.go (about)

     1  package destinationrules
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
     8  
     9  	"github.com/kiali/kiali/kubernetes"
    10  	"github.com/kiali/kiali/models"
    11  )
    12  
    13  const DestinationRulesCheckerType = "destinationrule"
    14  
    15  type MultiMatchChecker struct {
    16  	Cluster          string
    17  	DestinationRules []*networking_v1beta1.DestinationRule
    18  	ServiceEntries   map[string][]string
    19  	Namespaces       models.Namespaces
    20  }
    21  
    22  type subset struct {
    23  	Name      string
    24  	Namespace string
    25  	RuleName  string
    26  }
    27  
    28  type rule struct {
    29  	Name      string
    30  	Namespace string
    31  }
    32  
    33  // Check validates that no two destinationRules target the same host+subset combination
    34  func (m MultiMatchChecker) Check() models.IstioValidations {
    35  	validations := models.IstioValidations{}
    36  
    37  	// Equality search is: [fqdn.String()][subset] except for ServiceEntry targets which use [host][subset]
    38  	seenHostSubsets := make(map[string]map[string][]rule)
    39  
    40  	for _, dr := range m.DestinationRules {
    41  		destinationRulesName := dr.Name
    42  		destinationRulesNamespace := dr.Namespace
    43  		fqdn := kubernetes.GetHost(dr.Spec.Host, dr.Namespace, m.Namespaces.GetNames())
    44  
    45  		// Skip DR validation if it enables mTLS either namespace or mesh-wide
    46  		if isNonLocalmTLSForServiceEnabled(dr, fqdn.String()) {
    47  			continue
    48  		}
    49  
    50  		foundSubsets := extractSubsets(dr, destinationRulesName, destinationRulesNamespace)
    51  
    52  		if fqdn.IsWildcard() {
    53  			// We need to check the matching subsets from all hosts now
    54  			for _, h := range seenHostSubsets {
    55  				checkCollisions(validations, destinationRulesNamespace, destinationRulesName, foundSubsets, h, m.Cluster)
    56  			}
    57  			// We add * later
    58  		}
    59  		// Search "*" first and then exact name
    60  		if previous, found := seenHostSubsets[fmt.Sprintf("*.%s.%s", fqdn.Namespace, fqdn.Cluster)]; found {
    61  			// Need to check subsets of "*"
    62  			checkCollisions(validations, destinationRulesNamespace, destinationRulesName, foundSubsets, previous, m.Cluster)
    63  		}
    64  
    65  		if previous, found := seenHostSubsets[fqdn.String()]; found {
    66  			// Host found, need to check underlying subsets
    67  			checkCollisions(validations, destinationRulesNamespace, destinationRulesName, foundSubsets, previous, m.Cluster)
    68  		}
    69  		// Nothing threw an error, so add these
    70  		if _, found := seenHostSubsets[fqdn.String()]; !found {
    71  			seenHostSubsets[fqdn.String()] = make(map[string][]rule)
    72  		}
    73  		for _, s := range foundSubsets {
    74  			seenHostSubsets[fqdn.String()][s.Name] = append(seenHostSubsets[fqdn.String()][s.Name], rule{destinationRulesName, destinationRulesNamespace})
    75  		}
    76  
    77  	}
    78  
    79  	return validations
    80  }
    81  
    82  func isNonLocalmTLSForServiceEnabled(dr *networking_v1beta1.DestinationRule, service string) bool {
    83  	return strings.HasPrefix(service, "*") && ismTLSEnabled(dr)
    84  }
    85  
    86  func ismTLSEnabled(dr *networking_v1beta1.DestinationRule) bool {
    87  	if dr.Spec.TrafficPolicy != nil && dr.Spec.TrafficPolicy.Tls != nil {
    88  		mode := dr.Spec.TrafficPolicy.Tls.Mode.String()
    89  		return mode == "ISTIO_MUTUAL"
    90  	}
    91  	return false
    92  }
    93  
    94  func extractSubsets(dr *networking_v1beta1.DestinationRule, destinationRulesName string, destinationRulesNamespace string) []subset {
    95  	if len(dr.Spec.Subsets) > 0 {
    96  		foundSubsets := []subset{}
    97  		for _, ss := range dr.Spec.Subsets {
    98  			foundSubsets = append(foundSubsets, subset{
    99  				Name:      ss.Name,
   100  				Namespace: destinationRulesNamespace,
   101  				RuleName:  destinationRulesName,
   102  			})
   103  		}
   104  		return foundSubsets
   105  	}
   106  	// Matches all the subsets:~
   107  	return []subset{{"~", destinationRulesNamespace, destinationRulesName}}
   108  }
   109  
   110  func checkCollisions(validations models.IstioValidations, namespace, destinationRulesName string, foundSubsets []subset, existing map[string][]rule, cluster string) {
   111  	// If current subset is ~
   112  	if len(foundSubsets) == 1 && foundSubsets[0].Name == "~" {
   113  		// This should match any subset in the same hostname
   114  		for _, v := range existing {
   115  			for _, e := range v {
   116  				addError(validations, []string{namespace, e.Namespace}, []string{destinationRulesName, e.Name}, cluster)
   117  			}
   118  		}
   119  	}
   120  
   121  	// If we have existing subset with ~
   122  	if rules, found := existing["~"]; found {
   123  		for _, rule := range rules {
   124  			addError(validations, []string{namespace, rule.Namespace}, []string{destinationRulesName, rule.Name}, cluster)
   125  		}
   126  	}
   127  
   128  	for _, s := range foundSubsets {
   129  		if rules, found := existing[s.Name]; found {
   130  			for _, rule := range rules {
   131  				addError(validations, []string{namespace, rule.Namespace}, []string{destinationRulesName, rule.Name}, cluster)
   132  			}
   133  		}
   134  	}
   135  }
   136  
   137  // addError links new validation errors to the validations. namespaces nad destinationRuleNames must always be a pair
   138  func addError(validations models.IstioValidations, namespaces []string, destinationRuleNames []string, cluster string) models.IstioValidations {
   139  	key0, rrValidation0 := createError("destinationrules.multimatch", namespaces[0], destinationRuleNames[0], cluster, true)
   140  	key1, rrValidation1 := createError("destinationrules.multimatch", namespaces[1], destinationRuleNames[1], cluster, true)
   141  
   142  	rrValidation0.References = append(rrValidation0.References, key1)
   143  	rrValidation1.References = append(rrValidation1.References, key0)
   144  
   145  	validations.MergeValidations(models.IstioValidations{key0: rrValidation0})
   146  	validations.MergeValidations(models.IstioValidations{key1: rrValidation1})
   147  
   148  	return validations
   149  }
   150  
   151  func createError(errorText, namespace, destinationRuleName, cluster string, valid bool) (models.IstioValidationKey, *models.IstioValidation) {
   152  	key := models.IstioValidationKey{Name: destinationRuleName, Namespace: namespace, ObjectType: DestinationRulesCheckerType, Cluster: cluster}
   153  	checks := models.Build(errorText, "spec/host")
   154  	rrValidation := &models.IstioValidation{
   155  		Name:       destinationRuleName,
   156  		ObjectType: DestinationRulesCheckerType,
   157  		Valid:      valid,
   158  		Checks: []*models.IstioCheck{
   159  			&checks,
   160  		},
   161  		References: make([]models.IstioValidationKey, 0),
   162  	}
   163  
   164  	return key, rrValidation
   165  }