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  }