github.com/cilium/cilium@v1.16.2/pkg/k8s/client/client_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package client
     5  
     6  import (
     7  	"context"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/cilium/hive/hivetest"
    14  	"github.com/spf13/pflag"
    15  	"github.com/stretchr/testify/require"
    16  	"k8s.io/apimachinery/pkg/api/errors"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  
    19  	"github.com/cilium/hive/cell"
    20  
    21  	"github.com/cilium/cilium/pkg/hive"
    22  	k8smetrics "github.com/cilium/cilium/pkg/k8s/metrics"
    23  	k8sversion "github.com/cilium/cilium/pkg/k8s/version"
    24  	"github.com/cilium/cilium/pkg/lock"
    25  	"github.com/cilium/cilium/pkg/logging"
    26  	"github.com/cilium/cilium/pkg/option"
    27  	"github.com/cilium/cilium/pkg/testutils"
    28  )
    29  
    30  func Test_runHeartbeat(t *testing.T) {
    31  	// k8s api server never replied back in the expected time. We should close all connections
    32  	k8smetrics.LastSuccessInteraction.Reset()
    33  	time.Sleep(2 * time.Millisecond)
    34  
    35  	testCtx, testCtxCancel := context.WithCancel(context.Background())
    36  
    37  	called := make(chan struct{})
    38  	runHeartbeat(
    39  		logging.DefaultLogger,
    40  		func(ctx context.Context) error {
    41  			// Block any attempt to connect return from a heartbeat until the
    42  			// test is complete.
    43  			<-testCtx.Done()
    44  			return nil
    45  		},
    46  		time.Millisecond,
    47  		func() {
    48  			close(called)
    49  		},
    50  	)
    51  
    52  	// We need to polling for the condition instead of using a time.After to
    53  	// give the opportunity for scheduler to run the goroutine inside runHeartbeat
    54  	err := testutils.WaitUntil(func() bool {
    55  		select {
    56  		case <-called:
    57  			return true
    58  		default:
    59  			return false
    60  		}
    61  	},
    62  		5*time.Second)
    63  	require.NoError(t, err, "Heartbeat should have closed all connections")
    64  	testCtxCancel()
    65  
    66  	// There are some connectivity issues, cilium is trying to reach kube-apiserver
    67  	// but it's only receiving errors for other requests. We should close all
    68  	// connections!
    69  
    70  	// Wait the double amount of time than the timeout to make sure
    71  	// LastSuccessInteraction is not taken into account and we will see that we
    72  	// will close all connections.
    73  	testCtx, testCtxCancel = context.WithCancel(context.Background())
    74  	time.Sleep(20 * time.Millisecond)
    75  
    76  	called = make(chan struct{})
    77  	runHeartbeat(
    78  		logging.DefaultLogger,
    79  		func(ctx context.Context) error {
    80  			// Block any attempt to connect return from a heartbeat until the
    81  			// test is complete.
    82  			<-testCtx.Done()
    83  			return nil
    84  		},
    85  		10*time.Millisecond,
    86  		func() {
    87  			close(called)
    88  		},
    89  	)
    90  
    91  	// We need to polling for the condition instead of using a time.After to
    92  	// give the opportunity for scheduler to run the goroutine inside runHeartbeat
    93  	err = testutils.WaitUntil(func() bool {
    94  		select {
    95  		case <-called:
    96  			return true
    97  		default:
    98  			return false
    99  		}
   100  	},
   101  		5*time.Second)
   102  	require.NoError(t, err, "Heartbeat should have closed all connections")
   103  	testCtxCancel()
   104  
   105  	// Cilium is successfully talking with kube-apiserver, we should not do
   106  	// anything.
   107  	k8smetrics.LastSuccessInteraction.Reset()
   108  
   109  	called = make(chan struct{})
   110  	runHeartbeat(
   111  		logging.DefaultLogger,
   112  		func(ctx context.Context) error {
   113  			close(called)
   114  			return nil
   115  		},
   116  		10*time.Millisecond,
   117  		func() {
   118  			t.Error("This should not have been called!")
   119  		},
   120  	)
   121  
   122  	select {
   123  	case <-time.After(20 * time.Millisecond):
   124  	case <-called:
   125  		t.Error("Heartbeat should have closed all connections")
   126  	}
   127  
   128  	// Cilium had the last interaction with kube-apiserver a long time ago.
   129  	// We should perform a heartbeat
   130  	k8smetrics.LastInteraction.Reset()
   131  	time.Sleep(50 * time.Millisecond)
   132  
   133  	called = make(chan struct{})
   134  	runHeartbeat(
   135  		logging.DefaultLogger,
   136  		func(ctx context.Context) error {
   137  			close(called)
   138  			return nil
   139  		},
   140  		10*time.Millisecond,
   141  		func() {
   142  			t.Error("This should not have been called!")
   143  		},
   144  	)
   145  
   146  	// We need to polling for the condition instead of using a time.After to
   147  	// give the opportunity for scheduler to run the goroutine inside runHeartbeat
   148  	err = testutils.WaitUntil(func() bool {
   149  		select {
   150  		case <-called:
   151  			return true
   152  		default:
   153  			return false
   154  		}
   155  	},
   156  		5*time.Second)
   157  	require.NoError(t, err, "Heartbeat should have closed all connections")
   158  
   159  	// Cilium had the last interaction with kube-apiserver a long time ago.
   160  	// We should perform a heartbeat but the heart beat will return
   161  	// an error so we should close all connections
   162  	k8smetrics.LastInteraction.Reset()
   163  	time.Sleep(50 * time.Millisecond)
   164  
   165  	called = make(chan struct{})
   166  	runHeartbeat(
   167  		logging.DefaultLogger,
   168  		func(ctx context.Context) error {
   169  			return &errors.StatusError{
   170  				ErrStatus: metav1.Status{
   171  					Code: http.StatusRequestTimeout,
   172  				},
   173  			}
   174  		},
   175  		10*time.Millisecond,
   176  		func() {
   177  			close(called)
   178  		},
   179  	)
   180  
   181  	// We need to polling for the condition instead of using a time.After to
   182  	// give the opportunity for scheduler to run the goroutine inside runHeartbeat
   183  	err = testutils.WaitUntil(func() bool {
   184  		select {
   185  		case <-called:
   186  			return true
   187  		default:
   188  			return false
   189  		}
   190  	},
   191  		5*time.Second)
   192  	require.NoError(t, err, "Heartbeat should have closed all connections")
   193  }
   194  
   195  func Test_client(t *testing.T) {
   196  	var requests lock.Map[string, *http.Request]
   197  	getRequest := func(k string) *http.Request {
   198  		v, _ := requests.Load(k)
   199  		return v
   200  	}
   201  
   202  	srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   203  		requests.Store(r.URL.Path, r)
   204  
   205  		w.Header().Set("Content-Type", "application/json")
   206  		switch r.URL.Path {
   207  		case "/version":
   208  			w.Write([]byte(`{
   209  			       "major": "1",
   210  			       "minor": "99"
   211  			}`))
   212  		default:
   213  			w.Write([]byte("{}"))
   214  		}
   215  	}))
   216  	srv.Start()
   217  	defer srv.Close()
   218  
   219  	var clientset Clientset
   220  	hive := hive.New(
   221  		Cell,
   222  		cell.Invoke(func(c Clientset) { clientset = c }),
   223  	)
   224  
   225  	// Set the server URL and use a low heartbeat timeout for quick test completion.
   226  	flags := pflag.NewFlagSet("", pflag.ContinueOnError)
   227  	hive.RegisterFlags(flags)
   228  	flags.Set(option.K8sAPIServer, srv.URL)
   229  	flags.Set(option.K8sHeartbeatTimeout, "5ms")
   230  
   231  	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
   232  	defer cancel()
   233  
   234  	tlog := hivetest.Logger(t)
   235  	require.NoError(t, hive.Start(tlog, ctx))
   236  
   237  	// Check that we see the connection probe and version check
   238  	require.NotNil(t, getRequest("/api/v1/namespaces/kube-system"))
   239  	require.NotNil(t, getRequest("/version"))
   240  	semVer := k8sversion.Version()
   241  	require.Equal(t, uint64(99), semVer.Minor)
   242  
   243  	// Wait until heartbeat has been seen to check that heartbeats are
   244  	// running.
   245  	err := testutils.WaitUntil(
   246  		func() bool { return getRequest("/healthz") != nil },
   247  		time.Second)
   248  	require.NoError(t, err)
   249  
   250  	// Test that all different clientsets are wired correctly.
   251  	_, err = clientset.CoreV1().Pods("test").Get(context.TODO(), "pod", metav1.GetOptions{})
   252  	require.NoError(t, err)
   253  	require.NotNil(t, getRequest("/api/v1/namespaces/test/pods/pod"))
   254  
   255  	_, err = clientset.Slim().CoreV1().Pods("test").Get(context.TODO(), "slim-pod", metav1.GetOptions{})
   256  	require.NoError(t, err)
   257  	require.NotNil(t, getRequest("/api/v1/namespaces/test/pods/slim-pod"))
   258  
   259  	_, err = clientset.ExtensionsV1beta1().DaemonSets("test").Get(context.TODO(), "ds", metav1.GetOptions{})
   260  	require.NoError(t, err)
   261  	require.NotNil(t, getRequest("/apis/extensions/v1beta1/namespaces/test/daemonsets/ds"))
   262  
   263  	_, err = clientset.CiliumV2().CiliumEndpoints("test").Get(context.TODO(), "ces", metav1.GetOptions{})
   264  	require.NoError(t, err)
   265  	require.NotNil(t, getRequest("/apis/cilium.io/v2/namespaces/test/ciliumendpoints/ces"))
   266  
   267  	require.NoError(t, hive.Stop(tlog, ctx))
   268  }