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  }