k8s.io/apiserver@v0.31.1/pkg/server/egressselector/egress_selector_test.go (about) 1 /* 2 Copyright 2019 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 egressselector 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "net" 24 "strings" 25 "testing" 26 "time" 27 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 utilnet "k8s.io/apimachinery/pkg/util/net" 30 "k8s.io/apiserver/pkg/apis/apiserver" 31 "k8s.io/apiserver/pkg/server/egressselector/metrics" 32 "k8s.io/component-base/metrics/legacyregistry" 33 "k8s.io/component-base/metrics/testutil" 34 testingclock "k8s.io/utils/clock/testing" 35 clientmetrics "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/client/metrics" 36 ccmetrics "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/common/metrics" 37 "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client" 38 ) 39 40 type fakeEgressSelection struct { 41 directDialerCalled bool 42 } 43 44 func TestEgressSelector(t *testing.T) { 45 testcases := []struct { 46 name string 47 input *apiserver.EgressSelectorConfiguration 48 services []struct { 49 egressType EgressType 50 validateDialer func(dialer utilnet.DialFunc, s *fakeEgressSelection) (bool, error) 51 lookupError *string 52 dialerError *string 53 } 54 expectedError *string 55 }{ 56 { 57 name: "direct", 58 input: &apiserver.EgressSelectorConfiguration{ 59 TypeMeta: metav1.TypeMeta{ 60 Kind: "", 61 APIVersion: "", 62 }, 63 EgressSelections: []apiserver.EgressSelection{ 64 { 65 Name: "cluster", 66 Connection: apiserver.Connection{ 67 ProxyProtocol: apiserver.ProtocolDirect, 68 }, 69 }, 70 { 71 Name: "controlplane", 72 Connection: apiserver.Connection{ 73 ProxyProtocol: apiserver.ProtocolDirect, 74 }, 75 }, 76 { 77 Name: "etcd", 78 Connection: apiserver.Connection{ 79 ProxyProtocol: apiserver.ProtocolDirect, 80 }, 81 }, 82 }, 83 }, 84 services: []struct { 85 egressType EgressType 86 validateDialer func(dialer utilnet.DialFunc, s *fakeEgressSelection) (bool, error) 87 lookupError *string 88 dialerError *string 89 }{ 90 { 91 Cluster, 92 validateDirectDialer, 93 nil, 94 nil, 95 }, 96 { 97 ControlPlane, 98 validateDirectDialer, 99 nil, 100 nil, 101 }, 102 { 103 Etcd, 104 validateDirectDialer, 105 nil, 106 nil, 107 }, 108 }, 109 expectedError: nil, 110 }, 111 } 112 113 for _, tc := range testcases { 114 t.Run(tc.name, func(t *testing.T) { 115 // Setup the various pieces such as the fake dialer prior to initializing the egress selector. 116 // Go doesn't allow function pointer comparison, nor does its reflect package 117 // So overriding the default dialer to detect if it is returned. 118 fake := &fakeEgressSelection{} 119 directDialer = fake.fakeDirectDialer 120 cs, err := NewEgressSelector(tc.input) 121 if err == nil && tc.expectedError != nil { 122 t.Errorf("calling NewEgressSelector expected error: %s, did not get it", *tc.expectedError) 123 } 124 if err != nil && tc.expectedError == nil { 125 t.Errorf("unexpected error calling NewEgressSelector got: %#v", err) 126 } 127 if err != nil && tc.expectedError != nil && err.Error() != *tc.expectedError { 128 t.Errorf("calling NewEgressSelector expected error: %s, got %#v", *tc.expectedError, err) 129 } 130 131 for _, service := range tc.services { 132 networkContext := NetworkContext{EgressSelectionName: service.egressType} 133 dialer, lookupErr := cs.Lookup(networkContext) 134 if lookupErr == nil && service.lookupError != nil { 135 t.Errorf("calling Lookup expected error: %s, did not get it", *service.lookupError) 136 } 137 if lookupErr != nil && service.lookupError == nil { 138 t.Errorf("unexpected error calling Lookup got: %#v", lookupErr) 139 } 140 if lookupErr != nil && service.lookupError != nil && lookupErr.Error() != *service.lookupError { 141 t.Errorf("calling Lookup expected error: %s, got %#v", *service.lookupError, lookupErr) 142 } 143 fake.directDialerCalled = false 144 ok, dialerErr := service.validateDialer(dialer, fake) 145 if dialerErr == nil && service.dialerError != nil { 146 t.Errorf("calling Lookup expected error: %s, did not get it", *service.dialerError) 147 } 148 if dialerErr != nil && service.dialerError == nil { 149 t.Errorf("unexpected error calling Lookup got: %#v", dialerErr) 150 } 151 if dialerErr != nil && service.dialerError != nil && dialerErr.Error() != *service.dialerError { 152 t.Errorf("calling Lookup expected error: %s, got %#v", *service.dialerError, dialerErr) 153 } 154 if !ok { 155 t.Errorf("Could not validate dialer for service %q", service.egressType) 156 } 157 } 158 }) 159 } 160 } 161 162 func (s *fakeEgressSelection) fakeDirectDialer(ctx context.Context, network, address string) (net.Conn, error) { 163 s.directDialerCalled = true 164 return nil, nil 165 } 166 167 func validateDirectDialer(dialer utilnet.DialFunc, s *fakeEgressSelection) (bool, error) { 168 conn, err := dialer(context.Background(), "tcp", "127.0.0.1:8080") 169 if err != nil { 170 return false, err 171 } 172 if conn != nil { 173 return false, nil 174 } 175 return s.directDialerCalled, nil 176 } 177 178 type fakeProxyServerConnector struct { 179 connectorErr bool 180 proxierErr bool 181 } 182 183 func (f *fakeProxyServerConnector) connect(context.Context) (proxier, error) { 184 if f.connectorErr { 185 return nil, fmt.Errorf("fake error") 186 } 187 return &fakeProxier{err: f.proxierErr}, nil 188 } 189 190 type fakeProxier struct { 191 err bool 192 } 193 194 func (f *fakeProxier) proxy(_ context.Context, _ string) (net.Conn, error) { 195 if f.err { 196 return nil, fmt.Errorf("fake error") 197 } 198 return nil, nil 199 } 200 201 func TestMetrics(t *testing.T) { 202 testcases := map[string]struct { 203 connectorErr bool 204 proxierErr bool 205 metrics []string 206 want string 207 }{ 208 "connect to proxy server start": { 209 connectorErr: true, 210 proxierErr: true, 211 metrics: []string{"apiserver_egress_dialer_dial_start_total"}, 212 want: ` 213 # HELP apiserver_egress_dialer_dial_start_total [ALPHA] Dial starts, labeled by the protocol (http-connect or grpc) and transport (tcp or uds). 214 # TYPE apiserver_egress_dialer_dial_start_total counter 215 apiserver_egress_dialer_dial_start_total{protocol="fake_protocol",transport="fake_transport"} 1 216 `, 217 }, 218 "connect to proxy server error": { 219 connectorErr: true, 220 proxierErr: false, 221 metrics: []string{"apiserver_egress_dialer_dial_failure_count"}, 222 want: ` 223 # HELP apiserver_egress_dialer_dial_failure_count [ALPHA] Dial failure count, labeled by the protocol (http-connect or grpc), transport (tcp or uds), and stage (connect or proxy). The stage indicates at which stage the dial failed 224 # TYPE apiserver_egress_dialer_dial_failure_count counter 225 apiserver_egress_dialer_dial_failure_count{protocol="fake_protocol",stage="connect",transport="fake_transport"} 1 226 `, 227 }, 228 "connect succeeded, proxy failed": { 229 connectorErr: false, 230 proxierErr: true, 231 metrics: []string{"apiserver_egress_dialer_dial_failure_count"}, 232 want: ` 233 # HELP apiserver_egress_dialer_dial_failure_count [ALPHA] Dial failure count, labeled by the protocol (http-connect or grpc), transport (tcp or uds), and stage (connect or proxy). The stage indicates at which stage the dial failed 234 # TYPE apiserver_egress_dialer_dial_failure_count counter 235 apiserver_egress_dialer_dial_failure_count{protocol="fake_protocol",stage="proxy",transport="fake_transport"} 1 236 `, 237 }, 238 "successful": { 239 connectorErr: false, 240 proxierErr: false, 241 metrics: []string{"apiserver_egress_dialer_dial_duration_seconds"}, 242 want: ` 243 # HELP apiserver_egress_dialer_dial_duration_seconds [ALPHA] Dial latency histogram in seconds, labeled by the protocol (http-connect or grpc), transport (tcp or uds) 244 # TYPE apiserver_egress_dialer_dial_duration_seconds histogram 245 apiserver_egress_dialer_dial_duration_seconds_bucket{protocol="fake_protocol",transport="fake_transport",le="0.005"} 1 246 apiserver_egress_dialer_dial_duration_seconds_bucket{protocol="fake_protocol",transport="fake_transport",le="0.025"} 1 247 apiserver_egress_dialer_dial_duration_seconds_bucket{protocol="fake_protocol",transport="fake_transport",le="0.1"} 1 248 apiserver_egress_dialer_dial_duration_seconds_bucket{protocol="fake_protocol",transport="fake_transport",le="0.5"} 1 249 apiserver_egress_dialer_dial_duration_seconds_bucket{protocol="fake_protocol",transport="fake_transport",le="2.5"} 1 250 apiserver_egress_dialer_dial_duration_seconds_bucket{protocol="fake_protocol",transport="fake_transport",le="12.5"} 1 251 apiserver_egress_dialer_dial_duration_seconds_bucket{protocol="fake_protocol",transport="fake_transport",le="+Inf"} 1 252 apiserver_egress_dialer_dial_duration_seconds_sum{protocol="fake_protocol",transport="fake_transport"} 0 253 apiserver_egress_dialer_dial_duration_seconds_count{protocol="fake_protocol",transport="fake_transport"} 1 254 `, 255 }, 256 } 257 for tn, tc := range testcases { 258 259 t.Run(tn, func(t *testing.T) { 260 metrics.Metrics.Reset() 261 metrics.Metrics.SetClock(testingclock.NewFakeClock(time.Now())) 262 d := dialerCreator{ 263 connector: &fakeProxyServerConnector{ 264 connectorErr: tc.connectorErr, 265 proxierErr: tc.proxierErr, 266 }, 267 options: metricsOptions{ 268 transport: "fake_transport", 269 protocol: "fake_protocol", 270 }, 271 } 272 dialer := d.createDialer() 273 dialer(context.TODO(), "", "") 274 if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.want), tc.metrics...); err != nil { 275 t.Errorf("Err in comparing metrics %v", err) 276 } 277 }) 278 } 279 } 280 281 func TestKonnectivityClientMetrics(t *testing.T) { 282 testcases := []struct { 283 name string 284 metrics []string 285 trigger func() 286 want string 287 }{ 288 { 289 name: "stream packets", 290 metrics: []string{"konnectivity_network_proxy_client_stream_packets_total"}, 291 trigger: func() { 292 clientmetrics.Metrics.ObservePacket(ccmetrics.SegmentFromClient, client.PacketType_DIAL_REQ) 293 }, 294 want: ` 295 # HELP konnectivity_network_proxy_client_stream_packets_total Count of packets processed, by segment and packet type (example: from_client, DIAL_REQ) 296 # TYPE konnectivity_network_proxy_client_stream_packets_total counter 297 konnectivity_network_proxy_client_stream_packets_total{packet_type="DIAL_REQ",segment="from_client"} 1 298 `, 299 }, 300 { 301 name: "stream errors", 302 metrics: []string{"konnectivity_network_proxy_client_stream_errors_total"}, 303 trigger: func() { 304 clientmetrics.Metrics.ObserveStreamError(ccmetrics.SegmentToClient, errors.New("example"), client.PacketType_DIAL_RSP) 305 }, 306 want: ` 307 # HELP konnectivity_network_proxy_client_stream_errors_total Count of gRPC stream errors, by segment, grpc Code, packet type. (example: from_agent, Code.Unavailable, DIAL_RSP) 308 # TYPE konnectivity_network_proxy_client_stream_errors_total counter 309 konnectivity_network_proxy_client_stream_errors_total{code="Unknown",packet_type="DIAL_RSP",segment="to_client"} 1 310 `, 311 }, 312 { 313 name: "dial failure", 314 metrics: []string{"konnectivity_network_proxy_client_dial_failure_total"}, 315 trigger: func() { 316 clientmetrics.Metrics.ObserveDialFailure(clientmetrics.DialFailureTimeout) 317 }, 318 want: ` 319 # HELP konnectivity_network_proxy_client_dial_failure_total Number of dial failures observed, by reason (example: remote endpoint error) 320 # TYPE konnectivity_network_proxy_client_dial_failure_total counter 321 konnectivity_network_proxy_client_dial_failure_total{reason="timeout"} 1 322 `, 323 }, 324 { 325 name: "client connections", 326 metrics: []string{"konnectivity_network_proxy_client_client_connections"}, 327 trigger: func() { 328 clientmetrics.Metrics.GetClientConnectionsMetric().WithLabelValues("dialing").Inc() 329 }, 330 want: ` 331 # HELP konnectivity_network_proxy_client_client_connections Number of open client connections, by status (Example: dialing) 332 # TYPE konnectivity_network_proxy_client_client_connections gauge 333 konnectivity_network_proxy_client_client_connections{status="dialing"} 1 334 `, 335 }, 336 } 337 for _, tc := range testcases { 338 t.Run(tc.name, func(t *testing.T) { 339 tc.trigger() 340 if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.want), tc.metrics...); err != nil { 341 t.Errorf("GatherAndCompare error: %v", err) 342 } 343 }) 344 } 345 }