k8s.io/kubernetes@v1.29.3/test/e2e/network/netpol/reachability.go (about)

     1  /*
     2  Copyright 2020 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 netpol
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	v1 "k8s.io/api/core/v1"
    24  	"k8s.io/kubernetes/test/e2e/framework"
    25  )
    26  
    27  // TestCase describes the data for a netpol test
    28  type TestCase struct {
    29  	ToPort       int
    30  	Protocol     v1.Protocol
    31  	Reachability *Reachability
    32  }
    33  
    34  // PodString represents a namespace 'x' + pod 'a' as "x/a".
    35  type PodString string
    36  
    37  // NewPodString instantiates a PodString from the given namespace and name.
    38  func NewPodString(namespace string, podName string) PodString {
    39  	return PodString(fmt.Sprintf("%s/%s", namespace, podName))
    40  }
    41  
    42  // String converts back to a string
    43  func (pod PodString) String() string {
    44  	return string(pod)
    45  }
    46  
    47  func (pod PodString) split() (string, string) {
    48  	pieces := strings.Split(string(pod), "/")
    49  	if len(pieces) != 2 {
    50  		framework.Failf("expected ns/pod, found %+v", pieces)
    51  	}
    52  	return pieces[0], pieces[1]
    53  }
    54  
    55  // Namespace extracts the namespace
    56  func (pod PodString) Namespace() string {
    57  	ns, _ := pod.split()
    58  	return ns
    59  }
    60  
    61  // PodName extracts the pod name
    62  func (pod PodString) PodName() string {
    63  	_, podName := pod.split()
    64  	return podName
    65  }
    66  
    67  // Peer is used for matching pods by either or both of the pod's namespace and name.
    68  type Peer struct {
    69  	Namespace string
    70  	Pod       string
    71  }
    72  
    73  // Matches checks whether the Peer matches the PodString:
    74  // - an empty namespace means the namespace will always match
    75  // - otherwise, the namespace must match the PodString's namespace
    76  // - same goes for Pod: empty matches everything, otherwise must match exactly
    77  func (p *Peer) Matches(pod PodString) bool {
    78  	return (p.Namespace == "" || p.Namespace == pod.Namespace()) && (p.Pod == "" || p.Pod == pod.PodName())
    79  }
    80  
    81  // Reachability packages the data for a cluster-wide connectivity probe
    82  type Reachability struct {
    83  	Expected   *TruthTable
    84  	Observed   *TruthTable
    85  	PodStrings []PodString
    86  }
    87  
    88  // NewReachability instantiates a reachability
    89  func NewReachability(podStrings []PodString, defaultExpectation bool) *Reachability {
    90  	var podNames []string
    91  	for _, podString := range podStrings {
    92  		podNames = append(podNames, podString.String())
    93  	}
    94  	r := &Reachability{
    95  		Expected:   NewTruthTableFromItems(podNames, &defaultExpectation),
    96  		Observed:   NewTruthTableFromItems(podNames, nil),
    97  		PodStrings: podStrings,
    98  	}
    99  	return r
   100  }
   101  
   102  // AllowLoopback expects all communication from a pod to itself to be allowed.
   103  // In general, call it after setting up any other rules since loopback logic follows no policy.
   104  func (r *Reachability) AllowLoopback() {
   105  	for _, podString := range r.PodStrings {
   106  		podName := podString.String()
   107  		r.Expected.Set(podName, podName, true)
   108  	}
   109  }
   110  
   111  // Expect sets the expected value for a single observation
   112  func (r *Reachability) Expect(from PodString, to PodString, isConnected bool) {
   113  	r.Expected.Set(string(from), string(to), isConnected)
   114  }
   115  
   116  // ExpectAllIngress defines that any traffic going into the pod will be allowed/denied (true/false)
   117  func (r *Reachability) ExpectAllIngress(pod PodString, connected bool) {
   118  	r.Expected.SetAllTo(string(pod), connected)
   119  	if !connected {
   120  		framework.Logf("Denying all traffic *to* %s", pod)
   121  	}
   122  }
   123  
   124  // ExpectAllEgress defines that any traffic going out of the pod will be allowed/denied (true/false)
   125  func (r *Reachability) ExpectAllEgress(pod PodString, connected bool) {
   126  	r.Expected.SetAllFrom(string(pod), connected)
   127  	if !connected {
   128  		framework.Logf("Denying all traffic *from* %s", pod)
   129  	}
   130  }
   131  
   132  // ExpectPeer sets expected values using Peer matchers
   133  func (r *Reachability) ExpectPeer(from *Peer, to *Peer, connected bool) {
   134  	for _, fromPod := range r.PodStrings {
   135  		if from.Matches(fromPod) {
   136  			for _, toPod := range r.PodStrings {
   137  				if to.Matches(toPod) {
   138  					r.Expected.Set(fromPod.String(), toPod.String(), connected)
   139  				}
   140  			}
   141  		}
   142  	}
   143  }
   144  
   145  // Observe records a single connectivity observation
   146  func (r *Reachability) Observe(fromPod PodString, toPod PodString, isConnected bool) {
   147  	r.Observed.Set(fromPod.String(), toPod.String(), isConnected)
   148  }
   149  
   150  // Summary produces a useful summary of expected and observed data
   151  func (r *Reachability) Summary(ignoreLoopback bool) (trueObs int, falseObs int, ignoredObs int, comparison *TruthTable) {
   152  	comparison = r.Expected.Compare(r.Observed)
   153  	if !comparison.IsComplete() {
   154  		framework.Failf("observations not complete!")
   155  	}
   156  	falseObs, trueObs, ignoredObs = 0, 0, 0
   157  	for from, dict := range comparison.Values {
   158  		for to, val := range dict {
   159  			if ignoreLoopback && from == to {
   160  				// Never fail on loopback, because its not yet defined.
   161  				ignoredObs++
   162  			} else if val {
   163  				trueObs++
   164  			} else {
   165  				falseObs++
   166  			}
   167  		}
   168  	}
   169  	return
   170  }
   171  
   172  // PrintSummary prints the summary
   173  func (r *Reachability) PrintSummary(printExpected bool, printObserved bool, printComparison bool) {
   174  	right, wrong, ignored, comparison := r.Summary(ignoreLoopback)
   175  	if ignored > 0 {
   176  		framework.Logf("warning: this test doesn't take into consideration hairpin traffic, i.e. traffic whose source and destination is the same pod: %d cases ignored", ignored)
   177  	}
   178  	framework.Logf("reachability: correct:%v, incorrect:%v, result=%t\n\n", right, wrong, wrong == 0)
   179  	if printExpected {
   180  		framework.Logf("expected:\n\n%s\n\n\n", r.Expected.PrettyPrint(""))
   181  	}
   182  	if printObserved {
   183  		framework.Logf("observed:\n\n%s\n\n\n", r.Observed.PrettyPrint(""))
   184  	}
   185  	if printComparison {
   186  		framework.Logf("comparison:\n\n%s\n\n\n", comparison.PrettyPrint(""))
   187  	}
   188  }