github.com/cilium/cilium@v1.16.2/test/controlplane/services/helpers/lbmap_validation.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package helpers
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"slices"
    12  	"sort"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/pmezard/go-difflib/difflib"
    17  	"golang.org/x/exp/constraints"
    18  	"golang.org/x/exp/maps"
    19  
    20  	fakeTypes "github.com/cilium/cilium/pkg/datapath/fake/types"
    21  	lb "github.com/cilium/cilium/pkg/loadbalancer"
    22  	"github.com/cilium/cilium/pkg/testutils/mockmaps"
    23  	"github.com/cilium/cilium/test/controlplane/suite"
    24  )
    25  
    26  func ValidateLBMapGoldenFile(file string, datapath *fakeTypes.FakeDatapath) error {
    27  	lbmap := datapath.LBMockMap()
    28  	writeLBMap := func() error {
    29  		f, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
    30  		if err != nil {
    31  			return err
    32  		}
    33  		writeLBMapAsTable(f, lbmap)
    34  		f.Close()
    35  		return nil
    36  	}
    37  
    38  	if _, err := os.Stat(file); err == nil {
    39  		bs, err := os.ReadFile(file)
    40  		if err != nil {
    41  			return err
    42  		}
    43  		var buf bytes.Buffer
    44  		writeLBMapAsTable(&buf, lbmap)
    45  		if diff, ok := diffStrings(file, string(bs), buf.String()); !ok {
    46  			if *suite.FlagUpdate {
    47  				return writeLBMap()
    48  			} else {
    49  				return fmt.Errorf("lbmap mismatch:\n%s", diff)
    50  			}
    51  		}
    52  		return nil
    53  	} else {
    54  		// Mark failed as the expected output was missing, but
    55  		// continue with the rest of the steps.
    56  		return writeLBMap()
    57  	}
    58  }
    59  
    60  func diffStrings(file string, expected, actual string) (string, bool) {
    61  	diff := difflib.UnifiedDiff{
    62  		A:        difflib.SplitLines(expected),
    63  		B:        difflib.SplitLines(actual),
    64  		FromFile: file,
    65  		ToFile:   "<actual>",
    66  		Context:  10,
    67  	}
    68  	out, err := difflib.GetUnifiedDiffString(diff)
    69  	if err != nil {
    70  		return err.Error(), false
    71  	}
    72  	if out != "" {
    73  		return out, false
    74  	}
    75  	return "", true
    76  }
    77  
    78  func writeLBMapAsTable(w io.Writer, lbmap *mockmaps.LBMockMap) {
    79  	lbmap.Lock()
    80  	defer lbmap.Unlock()
    81  
    82  	// Since the order in which backends and services (and their ids)
    83  	// are allocated is non-deterministic, we sort the backends and services
    84  	// by address, and use the new ordering to allocate deterministic ids.
    85  	backends := make([]*lb.Backend, 0, len(lbmap.BackendByID))
    86  	for _, be := range lbmap.BackendByID {
    87  		backends = append(backends, be)
    88  	}
    89  	sort.Slice(backends, func(i, j int) bool {
    90  		return backends[i].L3n4Addr.StringWithProtocol() < backends[j].StringWithProtocol()
    91  	})
    92  	newBackendIds := map[lb.BackendID]int{}
    93  	for i, be := range backends {
    94  		newBackendIds[be.ID] = i
    95  	}
    96  
    97  	services := make([]*lb.SVC, 0, len(lbmap.ServiceByID))
    98  	for _, svc := range lbmap.ServiceByID {
    99  		services = append(services, svc)
   100  	}
   101  	// Sort services by type, then namespace/name and finally by frontend address.
   102  	sort.Slice(services, func(i, j int) bool {
   103  		if services[i].Type < services[j].Type {
   104  			return true
   105  		} else if services[i].Type > services[j].Type {
   106  			return false
   107  		}
   108  		if services[i].Name.Namespace < services[j].Name.Namespace {
   109  			return true
   110  		} else if services[i].Name.Namespace > services[j].Name.Namespace {
   111  			return false
   112  		}
   113  		if services[i].Name.Name < services[j].Name.Name {
   114  			return true
   115  		} else if services[i].Name.Name > services[j].Name.Name {
   116  			return false
   117  		}
   118  		return services[i].Frontend.L3n4Addr.StringWithProtocol() <
   119  			services[j].Frontend.L3n4Addr.StringWithProtocol()
   120  	})
   121  
   122  	// Map for linking backend to services that refer to it.
   123  	backendToServiceId := make(map[int][]string)
   124  
   125  	tw := suite.NewEmptyTable("Services", "ID", "Name", "Type", "Frontend", "Backend IDs")
   126  	for i, svc := range services {
   127  		for _, be := range svc.Backends {
   128  			id := newBackendIds[be.ID]
   129  			backendToServiceId[id] = append(backendToServiceId[id], strconv.FormatInt(int64(i), 10))
   130  		}
   131  		tw.AddRow(
   132  			strconv.FormatInt(int64(i), 10),
   133  			svc.Name.String(),
   134  			string(svc.Type),
   135  			svc.Frontend.StringWithProtocol(),
   136  			showBackendIDs(newBackendIds, svc.Backends),
   137  		)
   138  	}
   139  	tw.Write(w)
   140  
   141  	tw = suite.NewEmptyTable("Backends", "ID", "L3n4Addr", "State", "Linked Services")
   142  	for i, be := range backends {
   143  		stateStr, err := be.State.String()
   144  		if err != nil {
   145  			stateStr = err.Error()
   146  		}
   147  		tw.AddRow(
   148  			strconv.FormatInt(int64(i), 10),
   149  			be.L3n4Addr.StringWithProtocol(),
   150  			stateStr,
   151  			strings.Join(backendToServiceId[i], ", "),
   152  		)
   153  	}
   154  	tw.Write(w)
   155  
   156  }
   157  
   158  func showBackendIDs(idMap map[lb.BackendID]int, bes []*lb.Backend) string {
   159  	var ids []int
   160  	for _, be := range bes {
   161  		ids = append(ids, idMap[be.ID])
   162  	}
   163  	sort.Ints(ids)
   164  	var strs []string
   165  	for _, id := range ids {
   166  		strs = append(strs, strconv.FormatInt(int64(id), 10))
   167  	}
   168  	return strings.Join(strs, ", ")
   169  }
   170  
   171  // MapInOrder does an in-order traversal of a map.
   172  func MapInOrder[M ~map[K]V, K constraints.Ordered, V any](m M, fn func(K, V)) {
   173  	keys := maps.Keys(m)
   174  	slices.Sort(keys)
   175  	for _, k := range keys {
   176  		fn(k, m[k])
   177  	}
   178  }