github.com/webmeshproj/webmesh-cni@v0.0.27/internal/types/client_test.go (about) 1 /* 2 Copyright 2023 Avi Zimmerman <avi.zimmerman@gmail.com>. 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 types 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "errors" 24 "os" 25 "path/filepath" 26 "testing" 27 "time" 28 29 "github.com/containernetworking/cni/pkg/skel" 30 storagev1 "github.com/webmeshproj/storage-provider-k8s/api/storage/v1" 31 "github.com/webmeshproj/webmesh/pkg/storage/testutil" 32 "k8s.io/client-go/rest" 33 "k8s.io/client-go/tools/clientcmd" 34 ctrl "sigs.k8s.io/controller-runtime" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 "sigs.k8s.io/controller-runtime/pkg/envtest" 37 "sigs.k8s.io/controller-runtime/pkg/log/zap" 38 39 meshcniv1 "github.com/webmeshproj/webmesh-cni/api/v1" 40 ) 41 42 func TestClient(t *testing.T) { 43 t.Parallel() 44 cfg := setupClientTest(t) 45 46 t.Run("NewClientForConfig", func(t *testing.T) { 47 t.Parallel() 48 49 t.Run("NilConf", func(t *testing.T) { 50 // Invalid configs should fail. 51 _, err := NewClientForConfig(ClientConfig{ 52 NetConf: &NetConf{}, 53 RestConfig: nil, 54 }) 55 if err == nil { 56 t.Fatal("Expected error for invalid config") 57 } 58 }) 59 60 t.Run("ValidConf", func(t *testing.T) { 61 // NewClient should never fail with a valid config. 62 client, err := NewClientForConfig(ClientConfig{ 63 NetConf: &NetConf{}, 64 RestConfig: cfg, 65 }) 66 if err != nil { 67 t.Fatal("Failed to create client", err) 68 } 69 // The client should be able to "Ping" the API server. 70 err = client.Ping(time.Second) 71 if err != nil { 72 t.Fatal("Failed to ping API server", err) 73 } 74 }) 75 }) 76 77 t.Run("NewClientFromNetConf", func(t *testing.T) { 78 t.Parallel() 79 80 kubeconfig, err := KubeconfigFromRestConfig(cfg, "default") 81 if err != nil { 82 t.Fatal("Failed to get kubeconfig", err) 83 } 84 85 t.Run("NilConf", func(t *testing.T) { 86 var netconf *NetConf 87 _, err := netconf.NewClient(time.Second) 88 if err == nil { 89 t.Fatal("Expected error for nil config") 90 } 91 }) 92 93 t.Run("InvalidKubeconfig", func(t *testing.T) { 94 netconf := &NetConf{ 95 Kubernetes: Kubernetes{ 96 Kubeconfig: "invalid", 97 }, 98 } 99 _, err := netconf.NewClient(time.Second) 100 if err == nil { 101 t.Fatal("Expected error for invalid kubeconfig") 102 } 103 }) 104 105 t.Run("ValidKubeconfig", func(t *testing.T) { 106 dirTmp, err := os.MkdirTemp("", "") 107 if err != nil { 108 t.Fatal("Failed to create temp dir", err) 109 } 110 defer os.RemoveAll(dirTmp) 111 kpath := filepath.Join(dirTmp, "kubeconfig") 112 err = clientcmd.WriteToFile(kubeconfig, kpath) 113 if err != nil { 114 t.Fatal("Failed to write kubeconfig", err) 115 } 116 netconf := &NetConf{ 117 Kubernetes: Kubernetes{ 118 Kubeconfig: kpath, 119 }, 120 } 121 client, err := netconf.NewClient(time.Second) 122 if err != nil { 123 t.Fatal("Failed to create client", err) 124 } 125 err = client.Ping(time.Second) 126 if err != nil { 127 t.Errorf("Failed to ping API server: %v", err) 128 } 129 }) 130 }) 131 132 t.Run("PeerContainers", func(t *testing.T) { 133 t.Parallel() 134 netConf := &NetConf{ 135 Interface: Interface{ 136 MTU: 1234, 137 DisableIPv4: false, 138 DisableIPv6: true, 139 }, 140 Kubernetes: Kubernetes{ 141 NodeName: "node-a", 142 Namespace: "default", 143 }, 144 } 145 cniclient, err := NewClientForConfig(ClientConfig{ 146 NetConf: netConf, 147 RestConfig: cfg, 148 }) 149 if err != nil { 150 t.Fatal("Failed to create client", err) 151 } 152 if err := cniclient.Ping(time.Second); err != nil { 153 t.Fatal("Failed to ping API server", err) 154 } 155 156 t.Run("CreatePeerContainer", func(t *testing.T) { 157 args := &skel.CmdArgs{ 158 ContainerID: "create-container-a", 159 Netns: "/proc/1/ns/net", 160 } 161 expectedContainer := netConf.ContainerFromArgs(args) 162 err := cniclient.CreatePeerContainer(context.Background(), args) 163 if err != nil { 164 t.Fatal("Failed to create peer container", err) 165 } 166 // We should eventually be able to get the container back and it should 167 // match the expected container. 168 var container *meshcniv1.PeerContainer 169 ok := testutil.Eventually[error](func() error { 170 container, err = cniclient.GetPeerContainer(context.Background(), args) 171 return err 172 }).ShouldNotError(time.Second*10, time.Second) 173 if !ok { 174 t.Fatal("Failed to get peer container", err) 175 } 176 expectedData, err := json.Marshal(expectedContainer.Spec) 177 if err != nil { 178 t.Fatal("Failed to marshal expected container", err) 179 } 180 actualData, err := json.Marshal(container.Spec) 181 if err != nil { 182 t.Fatal("Failed to marshal actual container", err) 183 } 184 if !bytes.Equal(expectedData, actualData) { 185 t.Fatalf("Expected container %s, got %s", string(expectedData), string(actualData)) 186 } 187 // Make the container ID invalid and try to get it again. 188 args.ContainerID = "invalid/container/id" 189 err = cniclient.CreatePeerContainer(context.Background(), args) 190 if err == nil { 191 t.Fatal("Expected error for invalid container ID") 192 } 193 }) 194 195 t.Run("GetPeerContainer", func(t *testing.T) { 196 args := &skel.CmdArgs{ 197 ContainerID: "get-container-a", 198 Netns: "/proc/1/ns/net", 199 } 200 err := cniclient.CreatePeerContainer(context.Background(), args) 201 if err != nil { 202 t.Fatal("Failed to create peer container", err) 203 } 204 // We should eventually be able to get the container back. 205 ok := testutil.Eventually[error](func() error { 206 _, err = cniclient.GetPeerContainer(context.Background(), args) 207 return err 208 }).ShouldNotError(time.Second*10, time.Second) 209 if !ok { 210 t.Fatal("Failed to get peer container", err) 211 } 212 // Try to get a non-existent container. 213 args.ContainerID = "non-existent-container" 214 _, err = cniclient.GetPeerContainer(context.Background(), args) 215 if err == nil { 216 t.Fatal("Expected error for non-existent container") 217 } 218 // The error should be a ErrPeerContainerNotFound. 219 if !IsPeerContainerNotFound(err) { 220 t.Fatal("Expected ErrPeerContainerNotFound") 221 } 222 }) 223 224 t.Run("DeletePeerContainer", func(t *testing.T) { 225 args := &skel.CmdArgs{ 226 ContainerID: "delete-container-a", 227 Netns: "/proc/1/ns/net", 228 } 229 err := cniclient.CreatePeerContainer(context.Background(), args) 230 if err != nil { 231 t.Fatal("Failed to create peer container", err) 232 } 233 // We should eventually be able to get the container back. 234 ok := testutil.Eventually[error](func() error { 235 _, err = cniclient.GetPeerContainer(context.Background(), args) 236 return err 237 }).ShouldNotError(time.Second*10, time.Second) 238 if !ok { 239 t.Fatal("Failed to get peer container", err) 240 } 241 // Delete the container. 242 err = cniclient.DeletePeerContainer(context.Background(), args) 243 if err != nil { 244 t.Fatal("Failed to delete peer container", err) 245 } 246 // The container should eventually be gone 247 ok = testutil.Eventually[error](func() error { 248 _, err = cniclient.GetPeerContainer(context.Background(), args) 249 return err 250 }).ShouldError(time.Second*10, time.Second) 251 if !ok { 252 t.Fatal("Expected error for non-existent container") 253 } 254 // The error should be a ErrPeerContainerNotFound. 255 if !IsPeerContainerNotFound(err) { 256 t.Fatal("Expected ErrPeerContainerNotFound, got:", err) 257 } 258 // Deleting non-existent containers should not fail. 259 args.ContainerID = "non-existent-container" 260 err = cniclient.DeletePeerContainer(context.Background(), args) 261 if err != nil { 262 t.Fatal("Failed to delete peer container", err) 263 } 264 }) 265 266 t.Run("EnsurePeerContainer", func(t *testing.T) { 267 // This test behaves more or less like the CreatePeerContainer test, but 268 // should only create the container once. 269 args := &skel.CmdArgs{ 270 ContainerID: "create-not-exists-container-a", 271 Netns: "/proc/1/ns/net", 272 } 273 err := cniclient.EnsureContainer(context.Background(), args) 274 if err != nil { 275 t.Fatal("Failed to create peer container", err) 276 } 277 var container1 *meshcniv1.PeerContainer 278 // We should eventually be able to get the container back. 279 ok := testutil.Eventually[error](func() error { 280 container1, err = cniclient.GetPeerContainer(context.Background(), args) 281 return err 282 }).ShouldNotError(time.Second*10, time.Second) 283 if !ok { 284 t.Fatal("Failed to get peer container", err) 285 } 286 // Furhter calls should not mutate the container. 287 err = cniclient.EnsureContainer(context.Background(), args) 288 if err != nil { 289 t.Fatal("Failed to create peer container", err) 290 } 291 var container2 *meshcniv1.PeerContainer 292 ok = testutil.Eventually[error](func() error { 293 container2, err = cniclient.GetPeerContainer(context.Background(), args) 294 return err 295 }).ShouldNotError(time.Second*10, time.Second) 296 if !ok { 297 t.Fatal("Failed to get peer container", err) 298 } 299 if container1.GetResourceVersion() != container2.GetResourceVersion() { 300 t.Fatal("Expected container to not be mutated") 301 } 302 }) 303 304 t.Run("WaitForStatus", func(t *testing.T) { 305 t.Run("Timeout", func(t *testing.T) { 306 args := &skel.CmdArgs{ 307 ContainerID: "timeout-container", 308 Netns: "/proc/1/ns/net", 309 } 310 err = cniclient.CreatePeerContainer(context.Background(), args) 311 if err != nil { 312 t.Fatal("Failed to create peer container", err) 313 } 314 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 315 defer cancel() 316 // Wait for Running should time out as the status is not set. 317 _, err = cniclient.WaitForRunning(ctx, args) 318 if err == nil { 319 t.Fatal("Expected error for timeout") 320 } else if !errors.Is(err, context.DeadlineExceeded) { 321 t.Fatal("Expected context error, got:", err) 322 } 323 // Try again with a higher timeout in a best-effort to get full coverage on the select. 324 ctx, cancel = context.WithTimeout(context.Background(), time.Second*2) 325 defer cancel() 326 // Wait for Running should time out as the status is not set. 327 _, err = cniclient.WaitForRunning(ctx, args) 328 if err == nil { 329 t.Fatal("Expected error for timeout") 330 } else if !errors.Is(err, context.DeadlineExceeded) { 331 t.Fatal("Expected context error, got:", err) 332 } 333 }) 334 335 t.Run("StatusNotReached", func(t *testing.T) { 336 args := &skel.CmdArgs{ 337 ContainerID: "unreached-status-container", 338 Netns: "/proc/1/ns/net", 339 } 340 raw, err := NewRawClientForConfig(cfg) 341 if err != nil { 342 t.Fatal("Failed to create raw client", err) 343 } 344 err = cniclient.CreatePeerContainer(context.Background(), args) 345 if err != nil { 346 t.Fatal("Failed to create peer container", err) 347 } 348 // Wait for the container to exist and then patch its status 349 var container *meshcniv1.PeerContainer 350 ok := testutil.Eventually[error](func() error { 351 container, err = cniclient.GetPeerContainer(context.Background(), args) 352 return err 353 }).ShouldNotError(time.Second*10, time.Second) 354 if !ok { 355 t.Fatal("Failed to get peer container", err) 356 } 357 container.Status.InterfaceStatus = meshcniv1.InterfaceStatusFailed 358 container.SetManagedFields(nil) 359 err = raw.Status().Patch(context.Background(), container, client.Apply, client.ForceOwnership, client.FieldOwner(meshcniv1.FieldOwner)) 360 if err != nil { 361 t.Fatal("Failed to patch peer container", err) 362 } 363 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 364 defer cancel() 365 // Wait for Running should time out as the status is not set. 366 _, err = cniclient.WaitForRunning(ctx, args) 367 if err == nil { 368 t.Fatal("Expected error for timeout") 369 } else if !errors.Is(err, context.DeadlineExceeded) { 370 t.Fatal("Expected context error, got:", err) 371 } 372 // Try again with a higher timeout in a best-effort to get full coverage on the select. 373 ctx, cancel = context.WithTimeout(context.Background(), time.Second*2) 374 defer cancel() 375 // Wait for Running should time out as the status is not set. 376 _, err = cniclient.WaitForRunning(ctx, args) 377 if err == nil { 378 t.Fatal("Expected error for timeout") 379 } else if !errors.Is(err, context.DeadlineExceeded) { 380 t.Fatal("Expected context error, got:", err) 381 } 382 }) 383 384 t.Run("StatusReached", func(t *testing.T) { 385 args := &skel.CmdArgs{ 386 ContainerID: "reached-status-container", 387 Netns: "/proc/1/ns/net", 388 } 389 raw, err := NewRawClientForConfig(cfg) 390 if err != nil { 391 t.Fatal("Failed to create raw client", err) 392 } 393 err = cniclient.CreatePeerContainer(context.Background(), args) 394 if err != nil { 395 t.Fatal("Failed to create peer container", err) 396 } 397 // Wait for the container to exist and then patch its status 398 var container *meshcniv1.PeerContainer 399 ok := testutil.Eventually[error](func() error { 400 container, err = cniclient.GetPeerContainer(context.Background(), args) 401 return err 402 }).ShouldNotError(time.Second*10, time.Second) 403 if !ok { 404 t.Fatal("Failed to get peer container", err) 405 } 406 container.Status.InterfaceStatus = meshcniv1.InterfaceStatusRunning 407 container.Status.IPv4Address = "test-ipv4" 408 container.Status.IPv6Address = "test-ipv6" 409 container.SetManagedFields(nil) 410 err = raw.Status().Patch(context.Background(), container, client.Apply, client.ForceOwnership, client.FieldOwner(meshcniv1.FieldOwner)) 411 if err != nil { 412 t.Fatal("Failed to patch peer container", err) 413 } 414 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 415 defer cancel() 416 // Wait for Running should time out as the status is not set. 417 container, err = cniclient.WaitForRunning(ctx, args) 418 if err != nil { 419 t.Fatal("Failed to wait for running", err) 420 } 421 if container.Status.IPv4Address != "test-ipv4" { 422 t.Fatal("Expected IPv4 address to be set") 423 } 424 if container.Status.IPv6Address != "test-ipv6" { 425 t.Fatal("Expected IPv6 address to be set") 426 } 427 // Do a raw test of the equivalent to better test real eventuality. 428 t.Run("Raw", func(t *testing.T) { 429 args := &skel.CmdArgs{ 430 ContainerID: "reached-status-container-raw", 431 Netns: "/proc/1/ns/net", 432 } 433 raw, err := NewRawClientForConfig(cfg) 434 if err != nil { 435 t.Fatal("Failed to create raw client", err) 436 } 437 container := cniclient.conf.ContainerFromArgs(args) 438 err = raw.Patch(context.Background(), &container, client.Apply, client.ForceOwnership, client.FieldOwner(meshcniv1.FieldOwner)) 439 if err != nil { 440 t.Fatal("Failed to patch peer container", err) 441 } 442 container.Status.InterfaceStatus = meshcniv1.InterfaceStatusRunning 443 container.Status.IPv4Address = "test-ipv4" 444 container.Status.IPv6Address = "test-ipv6" 445 container.SetManagedFields(nil) 446 err = raw.Status().Patch(context.Background(), &container, client.Apply, client.ForceOwnership, client.FieldOwner(meshcniv1.FieldOwner)) 447 if err != nil { 448 t.Fatal("Failed to patch peer container status", err) 449 } 450 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 451 defer cancel() 452 // Wait for Running should time out as the status is not set. 453 got, err := cniclient.WaitForRunning(ctx, args) 454 if err != nil { 455 t.Fatal("Failed to wait for running", err) 456 } 457 if got.Status.IPv4Address != "test-ipv4" { 458 t.Fatal("Expected IPv4 address to be set") 459 } 460 if got.Status.IPv6Address != "test-ipv6" { 461 t.Fatal("Expected IPv6 address to be set") 462 } 463 }) 464 }) 465 }) 466 }) 467 } 468 469 func setupClientTest(t *testing.T) *rest.Config { 470 t.Helper() 471 t.Log("Starting test environment") 472 ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true}))) 473 testenv := envtest.Environment{ 474 CRDs: storagev1.GetCustomResourceDefintions(), 475 CRDDirectoryPaths: []string{os.Getenv("CRD_PATHS")}, 476 ErrorIfCRDPathMissing: true, 477 ControlPlaneStartTimeout: time.Second * 20, 478 ControlPlaneStopTimeout: time.Second * 10, 479 } 480 cfg, err := testenv.Start() 481 if err != nil { 482 t.Fatal("Failed to start test environment", err) 483 } 484 t.Cleanup(func() { 485 t.Log("Stopping test environment") 486 err := testenv.Stop() 487 if err != nil { 488 t.Log("Failed to stop test environment", err) 489 } 490 }) 491 return cfg 492 }