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 }