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 }