agones.dev/agones@v1.54.0/pkg/gameservers/migration_test.go (about)

     1  // Copyright 2020 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gameservers
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  	"time"
    21  
    22  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    23  	agtesting "agones.dev/agones/pkg/testing"
    24  	"github.com/heptiolabs/healthcheck"
    25  	"github.com/sirupsen/logrus"
    26  	"github.com/stretchr/testify/assert"
    27  	"github.com/stretchr/testify/require"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/watch"
    32  	k8stesting "k8s.io/client-go/testing"
    33  	"k8s.io/client-go/tools/cache"
    34  )
    35  
    36  func TestMigrationControllerSyncGameServer(t *testing.T) {
    37  	t.Parallel()
    38  
    39  	ipChangeFixture := "99.99.99.99"
    40  	nodeNameChangeFixture := "nodeChange"
    41  
    42  	type expected struct {
    43  		updated     bool
    44  		updateTests func(t *testing.T, gs *agonesv1.GameServer)
    45  		postTests   func(t *testing.T, mocks agtesting.Mocks)
    46  	}
    47  	fixtures := map[string]struct {
    48  		setup    func(*agonesv1.GameServer, *corev1.Pod, *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node)
    49  		expected expected
    50  	}{
    51  		"no change, gameserver nodeName not yet set": {
    52  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) {
    53  				return gs, pod, node
    54  			},
    55  			expected: expected{
    56  				updated:     false,
    57  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
    58  				postTests: func(t *testing.T, m agtesting.Mocks) {
    59  					agtesting.AssertNoEvent(t, m.FakeRecorder.Events)
    60  				},
    61  			},
    62  		},
    63  		"no change, with same address": {
    64  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) {
    65  				gs.Status.NodeName = nodeFixtureName
    66  				gs.Status.Address = ipFixture
    67  				return gs, pod, node
    68  			},
    69  			expected: expected{
    70  				updated:     false,
    71  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
    72  				postTests: func(t *testing.T, m agtesting.Mocks) {
    73  					agtesting.AssertNoEvent(t, m.FakeRecorder.Events)
    74  				},
    75  			},
    76  		},
    77  		"change before ready, ip only change": {
    78  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) {
    79  				gs.Status.NodeName = nodeFixtureName
    80  				gs.Status.Address = ipFixture
    81  				gs.Status.State = agonesv1.GameServerStateScheduled
    82  				node.Status.Addresses[0].Address = ipChangeFixture
    83  				return gs, pod, node
    84  			},
    85  			expected: expected{
    86  				updated: true,
    87  				updateTests: func(t *testing.T, gs *agonesv1.GameServer) {
    88  					assert.Equal(t, ipChangeFixture, gs.Status.Address)
    89  				},
    90  				postTests: func(t *testing.T, m agtesting.Mocks) {
    91  					agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Warning Scheduled Address updated due to Node migration")
    92  				},
    93  			},
    94  		},
    95  		"change before ready, full node change": {
    96  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) {
    97  				gs.Status.NodeName = nodeFixtureName
    98  				gs.Status.Address = ipFixture
    99  				gs.Status.State = agonesv1.GameServerStateScheduled
   100  
   101  				// full node name change
   102  				pod.Spec.NodeName = nodeNameChangeFixture
   103  				node.ObjectMeta.Name = nodeNameChangeFixture
   104  				node.Status.Addresses[0].Address = ipChangeFixture
   105  				return gs, pod, node
   106  			},
   107  			expected: expected{
   108  				updated: true,
   109  				updateTests: func(t *testing.T, gs *agonesv1.GameServer) {
   110  					assert.Equal(t, ipChangeFixture, gs.Status.Address)
   111  					assert.Equal(t, nodeNameChangeFixture, gs.Status.NodeName)
   112  				},
   113  				postTests: func(t *testing.T, m agtesting.Mocks) {
   114  					agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Warning Scheduled Address updated due to Node migration")
   115  				},
   116  			},
   117  		},
   118  		"change after ready": {
   119  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) {
   120  				gs.Status.NodeName = nodeFixtureName
   121  				gs.Status.Address = ipFixture
   122  				gs.Status.State = agonesv1.GameServerStateAllocated
   123  
   124  				// full node name change
   125  				pod.Spec.NodeName = nodeNameChangeFixture
   126  				node.ObjectMeta.Name = nodeNameChangeFixture
   127  				node.Status.Addresses[0].Address = ipChangeFixture
   128  
   129  				return gs, pod, node
   130  			},
   131  			expected: expected{
   132  				updated: true,
   133  				updateTests: func(t *testing.T, gs *agonesv1.GameServer) {
   134  					assert.Equal(t, agonesv1.GameServerStateUnhealthy, gs.Status.State)
   135  				},
   136  				postTests: func(t *testing.T, m agtesting.Mocks) {
   137  					agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Warning Unhealthy Node migration occurred")
   138  				},
   139  			},
   140  		},
   141  	}
   142  
   143  	for k, v := range fixtures {
   144  		t.Run(k, func(t *testing.T) {
   145  			m := agtesting.NewMocks()
   146  			c := NewMigrationController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, nilSyncPodPortsToGameServer)
   147  			c.recorder = m.FakeRecorder
   148  
   149  			gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
   150  				Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{}}
   151  			gs.ApplyDefaults()
   152  
   153  			pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   154  			require.NoError(t, err)
   155  			pod.Spec.NodeName = nodeFixtureName
   156  
   157  			node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName},
   158  				Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Address: ipFixture, Type: corev1.NodeExternalIP}}}}
   159  
   160  			gs, pod, node = v.setup(gs, pod, node)
   161  
   162  			// populate
   163  			m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   164  				return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{*gs}}, nil
   165  			})
   166  			m.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   167  				return true,
   168  					&corev1.NodeList{Items: []corev1.Node{*node}}, nil
   169  			})
   170  			m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   171  				return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil
   172  			})
   173  
   174  			// check values
   175  			updated := false
   176  			m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
   177  				updated = true
   178  				ua := action.(k8stesting.UpdateAction)
   179  				gs := ua.GetObject().(*agonesv1.GameServer)
   180  				v.expected.updateTests(t, gs)
   181  				return true, gs, nil
   182  			})
   183  
   184  			ctx, cancel := agtesting.StartInformers(m, c.nodeSynced, c.gameServerSynced, c.podSynced)
   185  			defer cancel()
   186  
   187  			err = c.syncGameServer(ctx, "default/test")
   188  			assert.NoError(t, err)
   189  			assert.Equal(t, v.expected.updated, updated)
   190  			v.expected.postTests(t, m)
   191  		})
   192  	}
   193  }
   194  
   195  func TestMigrationControllerRun(t *testing.T) {
   196  	logrus.SetLevel(logrus.DebugLevel)
   197  
   198  	m := agtesting.NewMocks()
   199  	c := NewMigrationController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, nilSyncPodPortsToGameServer)
   200  
   201  	node := corev1.Node{
   202  		ObjectMeta: metav1.ObjectMeta{
   203  			Name: nodeFixtureName,
   204  		},
   205  		Status: corev1.NodeStatus{
   206  			Addresses: []corev1.NodeAddress{
   207  				{
   208  					Type:    corev1.NodeExternalIP,
   209  					Address: ipFixture,
   210  				},
   211  			},
   212  		},
   213  	}
   214  	nodeChangedName := nodeFixtureName + "+changed"
   215  	nodeChanged := corev1.Node{
   216  		ObjectMeta: metav1.ObjectMeta{Name: nodeChangedName},
   217  		Status: corev1.NodeStatus{
   218  			Addresses: []corev1.NodeAddress{
   219  				{
   220  					Type:    corev1.NodeExternalIP,
   221  					Address: "no",
   222  				},
   223  			},
   224  		},
   225  	}
   226  
   227  	gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
   228  		Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{
   229  			NodeName: nodeFixtureName,
   230  			Address:  ipFixture,
   231  			State:    agonesv1.GameServerStateAllocated,
   232  		}}
   233  	gs.ApplyDefaults()
   234  
   235  	gsPod, err := gs.Pod(agtesting.FakeAPIHooks{})
   236  	require.NoError(t, err)
   237  	gsPod.Spec.NodeName = nodeFixtureName
   238  
   239  	received := make(chan string)
   240  	h := func(_ context.Context, name string) error {
   241  		assert.Equal(t, "default/test", name)
   242  		received <- name
   243  		return nil
   244  	}
   245  
   246  	podWatch := watch.NewFake()
   247  	m.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(podWatch, nil))
   248  
   249  	nodeWatch := watch.NewFake()
   250  	m.KubeClient.AddWatchReactor("nodes", k8stesting.DefaultWatchReactor(nodeWatch, nil))
   251  
   252  	gsWatch := watch.NewFake()
   253  	m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil))
   254  
   255  	c.workerqueue.SyncHandler = h
   256  
   257  	ctx, cancel := agtesting.StartInformers(m, c.nodeSynced, c.gameServerSynced, c.podSynced)
   258  	defer cancel()
   259  
   260  	go func() {
   261  		err := c.Run(ctx, 1)
   262  		assert.Nil(t, err, "Run should not error")
   263  	}()
   264  
   265  	noChange := func() {
   266  		require.True(t, cache.WaitForCacheSync(ctx.Done(), c.nodeSynced, c.gameServerSynced, c.podSynced))
   267  		select {
   268  		case <-received:
   269  			require.Fail(t, "should not be updated")
   270  		case <-time.After(1 * time.Second):
   271  		}
   272  	}
   273  
   274  	result := func() {
   275  		require.True(t, cache.WaitForCacheSync(ctx.Done(), c.nodeSynced, c.gameServerSynced, c.podSynced))
   276  		select {
   277  		case res := <-received:
   278  			require.Equal(t, "default/test", res)
   279  		case <-time.After(2 * time.Second):
   280  			require.Fail(t, "did not receive queue")
   281  		}
   282  	}
   283  
   284  	// initial pod, no gameserver, no nodes
   285  	logrus.Info("initial pod, no gameserver, no node")
   286  	gsPod.ObjectMeta.Labels["change"] = "no-pod-no-gameserver-no-node"
   287  	podWatch.Add(gsPod.DeepCopy())
   288  	noChange()
   289  
   290  	// pod with gameserver, no nodes
   291  	logrus.Info("pod with gameserver, no node")
   292  	gsWatch.Add(gs.DeepCopy())
   293  	require.True(t, cache.WaitForCacheSync(ctx.Done(), c.gameServerSynced))
   294  	gsPod.ObjectMeta.Labels["change"] = "pod-gameserver-no-node"
   295  	podWatch.Modify(gsPod.DeepCopy())
   296  	noChange()
   297  
   298  	// pod with gameserver, and nodes
   299  	logrus.Info("pod with gameserver, and node")
   300  	nodeWatch.Add(node.DeepCopy())
   301  	nodeWatch.Add(nodeChanged.DeepCopy())
   302  	require.True(t, cache.WaitForCacheSync(ctx.Done(), c.nodeSynced))
   303  	gsPod.ObjectMeta.Labels["change"] = "pod-gameserver-node"
   304  	podWatch.Modify(gsPod.DeepCopy())
   305  	noChange()
   306  
   307  	// pod with a different NodeName to the Node.
   308  	logrus.Info("pod with a different NodeName to the Node.")
   309  	gsPod.Spec.NodeName = nodeChangedName
   310  	gsPod.ObjectMeta.Labels["change"] = "pod-gameserver-node-changed"
   311  	podWatch.Modify(gsPod.DeepCopy())
   312  	result()
   313  
   314  	// deleted pod
   315  	now := metav1.Now()
   316  	logrus.Info("deleted pod")
   317  	gsPod.ObjectMeta.DeletionTimestamp = &now
   318  	podWatch.Modify(gsPod.DeepCopy())
   319  	noChange()
   320  
   321  	// non gameserver pod
   322  	pod := corev1.Pod{ObjectMeta: metav1.ObjectMeta{
   323  		Name:      "test2",
   324  		Namespace: "default",
   325  	}}
   326  	pod.Spec.NodeName = nodeFixtureName
   327  	podWatch.Add(pod.DeepCopy())
   328  	noChange()
   329  
   330  	pod.Spec.NodeName = nodeChangedName
   331  	podWatch.Modify(pod.DeepCopy())
   332  	noChange()
   333  }
   334  
   335  func TestMigrationControllerAnyAddressMatch(t *testing.T) {
   336  	fixtures := map[string]struct {
   337  		matches       bool
   338  		gsAddress     string
   339  		nodeAddresses []corev1.NodeAddress
   340  	}{
   341  		"NodeHostName matches": {
   342  			matches:       true,
   343  			gsAddress:     "NodeHostName",
   344  			nodeAddresses: []corev1.NodeAddress{{Address: "NodeHostName", Type: corev1.NodeHostName}},
   345  		},
   346  		"NodeExternalDNS matches": {
   347  			matches:       true,
   348  			gsAddress:     "NodeExternalDNS",
   349  			nodeAddresses: []corev1.NodeAddress{{Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS}},
   350  		},
   351  		"NodeExternalIP matches": {
   352  			matches:       true,
   353  			gsAddress:     "NodeExternalIP",
   354  			nodeAddresses: []corev1.NodeAddress{{Address: "NodeExternalIP", Type: corev1.NodeExternalIP}},
   355  		},
   356  		"NodeInternalDNS matches": {
   357  			matches:       true,
   358  			gsAddress:     "NodeInternalDNS",
   359  			nodeAddresses: []corev1.NodeAddress{{Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS}},
   360  		},
   361  		"NodeInternalIP matches": {
   362  			matches:       true,
   363  			gsAddress:     "NodeInternalIP",
   364  			nodeAddresses: []corev1.NodeAddress{{Address: "NodeInternalIP", Type: corev1.NodeInternalIP}},
   365  		},
   366  		"no matches": {
   367  			matches:   false,
   368  			gsAddress: "no-match",
   369  			nodeAddresses: []corev1.NodeAddress{
   370  				{Address: "NodeHostName", Type: corev1.NodeHostName},
   371  				{Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS},
   372  				{Address: "NodeExternalIP", Type: corev1.NodeExternalIP},
   373  				{Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS},
   374  				{Address: "NodeInternalIP", Type: corev1.NodeInternalIP},
   375  			},
   376  		},
   377  		"matches one of many 1": {
   378  			matches:   true,
   379  			gsAddress: "NodeInternalDNS",
   380  			nodeAddresses: []corev1.NodeAddress{
   381  				{Address: "NodeHostName", Type: corev1.NodeHostName},
   382  				{Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS},
   383  				{Address: "NodeExternalIP", Type: corev1.NodeExternalIP},
   384  				{Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS},
   385  				{Address: "NodeInternalIP", Type: corev1.NodeInternalIP},
   386  			},
   387  		},
   388  		"matches one of many 2": {
   389  			matches:   true,
   390  			gsAddress: "NodeInternalIP",
   391  			nodeAddresses: []corev1.NodeAddress{
   392  				{Address: "NodeHostName", Type: corev1.NodeHostName},
   393  				{Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS},
   394  				{Address: "NodeExternalIP", Type: corev1.NodeExternalIP},
   395  				{Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS},
   396  				{Address: "NodeInternalIP", Type: corev1.NodeInternalIP},
   397  			},
   398  		},
   399  	}
   400  	for k, v := range fixtures {
   401  		t.Run(k, func(t *testing.T) {
   402  			m := agtesting.NewMocks()
   403  			c := NewMigrationController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, nilSyncPodPortsToGameServer)
   404  			gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
   405  				Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{
   406  					Address: v.gsAddress,
   407  				}}
   408  			node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName},
   409  				Status: corev1.NodeStatus{Addresses: v.nodeAddresses}}
   410  
   411  			matches := c.anyAddressMatch(node, gs)
   412  			assert.Equal(t, v.matches, matches)
   413  		})
   414  	}
   415  }
   416  
   417  func nilSyncPodPortsToGameServer(*agonesv1.GameServer, *corev1.Pod) error { return nil }