agones.dev/agones@v1.54.0/pkg/sdkserver/localsdk.go (about)

     1  // Copyright 2018 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 sdkserver
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"math/rand"
    22  	"os"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"github.com/fsnotify/fsnotify"
    29  	"github.com/mennanov/fmutils"
    30  	"github.com/pkg/errors"
    31  	"github.com/sirupsen/logrus"
    32  	"google.golang.org/protobuf/proto"
    33  	"k8s.io/apimachinery/pkg/util/yaml"
    34  
    35  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    36  	"agones.dev/agones/pkg/sdk"
    37  	"agones.dev/agones/pkg/sdk/alpha"
    38  	"agones.dev/agones/pkg/sdk/beta"
    39  	"agones.dev/agones/pkg/util/apiserver"
    40  	"agones.dev/agones/pkg/util/runtime"
    41  )
    42  
    43  var (
    44  	_ sdk.SDKServer   = &LocalSDKServer{}
    45  	_ alpha.SDKServer = &LocalSDKServer{}
    46  	_ beta.SDKServer  = &LocalSDKServer{}
    47  )
    48  
    49  func defaultGs() *sdk.GameServer {
    50  	gs := &sdk.GameServer{
    51  		ObjectMeta: &sdk.GameServer_ObjectMeta{
    52  			Name:              "local",
    53  			Namespace:         "default",
    54  			Uid:               "1234",
    55  			Generation:        1,
    56  			ResourceVersion:   "v1",
    57  			CreationTimestamp: time.Now().Unix(),
    58  			Labels:            map[string]string{"islocal": "true"},
    59  			Annotations:       map[string]string{"annotation": "true"},
    60  		},
    61  		Spec: &sdk.GameServer_Spec{
    62  			Health: &sdk.GameServer_Spec_Health{
    63  				Disabled:            false,
    64  				PeriodSeconds:       3,
    65  				FailureThreshold:    5,
    66  				InitialDelaySeconds: 10,
    67  			},
    68  		},
    69  		Status: &sdk.GameServer_Status{
    70  			State:   "Ready",
    71  			Address: "127.0.0.1",
    72  			Ports:   []*sdk.GameServer_Status_Port{{Name: "default", Port: 7777}},
    73  		},
    74  	}
    75  
    76  	if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
    77  		gs.Status.Counters = map[string]*sdk.GameServer_Status_CounterStatus{
    78  			"rooms": {Count: 1, Capacity: 10},
    79  		}
    80  		gs.Status.Lists = map[string]*sdk.GameServer_Status_ListStatus{
    81  			"players": {Values: []string{"test0", "test1", "test2"}, Capacity: 100},
    82  		}
    83  	}
    84  
    85  	return gs
    86  }
    87  
    88  // LocalSDKServer type is the SDKServer implementation for when the sidecar
    89  // is being run for local development, and doesn't connect to the
    90  // Kubernetes cluster
    91  //
    92  //nolint:govet // ignore fieldalignment, singleton
    93  type LocalSDKServer struct {
    94  	gsMutex           sync.RWMutex
    95  	gs                *sdk.GameServer
    96  	logger            *logrus.Entry
    97  	update            chan struct{}
    98  	updateObservers   sync.Map
    99  	testMutex         sync.Mutex
   100  	requestSequence   []string
   101  	expectedSequence  []string
   102  	gsState           agonesv1.GameServerState
   103  	gsReserveDuration *time.Duration
   104  	reserveTimer      *time.Timer
   105  	testMode          bool
   106  	testSdkName       string
   107  }
   108  
   109  // NewLocalSDKServer returns the default LocalSDKServer
   110  func NewLocalSDKServer(filePath string, testSdkName string) (*LocalSDKServer, error) {
   111  	l := &LocalSDKServer{
   112  		gsMutex:         sync.RWMutex{},
   113  		gs:              defaultGs(),
   114  		update:          make(chan struct{}),
   115  		updateObservers: sync.Map{},
   116  		testMutex:       sync.Mutex{},
   117  		requestSequence: make([]string, 0),
   118  		testMode:        false,
   119  		testSdkName:     testSdkName,
   120  		gsState:         agonesv1.GameServerStateScheduled,
   121  	}
   122  	l.logger = runtime.NewLoggerWithType(l)
   123  
   124  	if filePath != "" {
   125  		err := l.setGameServerFromFilePath(filePath)
   126  		if err != nil {
   127  			return l, err
   128  		}
   129  
   130  		watcher, err := fsnotify.NewWatcher()
   131  		if err != nil {
   132  			return l, err
   133  		}
   134  
   135  		go func() {
   136  			for event := range watcher.Events {
   137  				if event.Op != fsnotify.Write {
   138  					continue
   139  				}
   140  				l.logger.WithField("event", event).Info("File has been changed!")
   141  				err := l.setGameServerFromFilePath(filePath)
   142  				if err != nil {
   143  					l.logger.WithError(err).Error("error setting GameServer from file")
   144  					continue
   145  				}
   146  				l.logger.Info("Sending watched GameServer!")
   147  				l.update <- struct{}{}
   148  			}
   149  		}()
   150  
   151  		err = watcher.Add(filePath)
   152  		if err != nil {
   153  			l.logger.WithError(err).WithField("filePath", filePath).Error("error adding watcher")
   154  		}
   155  	}
   156  	if runtime.FeatureEnabled(runtime.FeaturePlayerTracking) && l.gs.Status.Players == nil {
   157  		l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{}
   158  	}
   159  
   160  	if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   161  		if l.gs.Status.Counters == nil {
   162  			l.gs.Status.Counters = make(map[string]*sdk.GameServer_Status_CounterStatus)
   163  		}
   164  		if l.gs.Status.Lists == nil {
   165  			l.gs.Status.Lists = make(map[string]*sdk.GameServer_Status_ListStatus)
   166  		}
   167  	}
   168  
   169  	go func() {
   170  		for value := range l.update {
   171  			l.logger.Info("Gameserver update received")
   172  			l.updateObservers.Range(func(observer, _ interface{}) bool {
   173  				observer.(chan struct{}) <- value
   174  				return true
   175  			})
   176  		}
   177  	}()
   178  
   179  	return l, nil
   180  }
   181  
   182  // GenerateUID - generate gameserver UID at random for testing
   183  func (l *LocalSDKServer) GenerateUID() {
   184  	// Generating Random UID
   185  	seededRand := rand.New(
   186  		rand.NewSource(time.Now().UnixNano()))
   187  	UID := fmt.Sprintf("%d", seededRand.Int())
   188  	l.gs.ObjectMeta.Uid = UID
   189  }
   190  
   191  // SetTestMode set test mode to collect the sequence of performed requests
   192  func (l *LocalSDKServer) SetTestMode(testMode bool) {
   193  	l.testMode = testMode
   194  }
   195  
   196  // SetExpectedSequence set expected request sequence which would be
   197  // verified against after run was completed
   198  func (l *LocalSDKServer) SetExpectedSequence(sequence []string) {
   199  	l.expectedSequence = sequence
   200  }
   201  
   202  // SetSdkName set SDK name to be added to the logs
   203  func (l *LocalSDKServer) SetSdkName(sdkName string) {
   204  	l.testSdkName = sdkName
   205  	l.logger = l.logger.WithField("sdkName", l.testSdkName)
   206  }
   207  
   208  // recordRequest append request name to slice
   209  func (l *LocalSDKServer) recordRequest(request string) {
   210  	if l.testMode {
   211  		l.testMutex.Lock()
   212  		defer l.testMutex.Unlock()
   213  		l.requestSequence = append(l.requestSequence, request)
   214  	}
   215  	if l.testSdkName != "" {
   216  		l.logger.Debugf("Received %s request", request)
   217  	}
   218  }
   219  
   220  // recordRequestWithValue append request name to slice only if
   221  // value equals to objMetaField: creationTimestamp or UID
   222  func (l *LocalSDKServer) recordRequestWithValue(request string, value string, objMetaField string) {
   223  	if l.testMode {
   224  		fieldVal := ""
   225  		switch objMetaField {
   226  		case "CreationTimestamp":
   227  			fieldVal = strconv.FormatInt(l.gs.ObjectMeta.CreationTimestamp, 10)
   228  		case "UID":
   229  			fieldVal = l.gs.ObjectMeta.Uid
   230  		case "PlayerCapacity":
   231  			if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   232  				return
   233  			}
   234  			fieldVal = strconv.FormatInt(l.gs.Status.Players.Capacity, 10)
   235  		case "PlayerIDs":
   236  			if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   237  				return
   238  			}
   239  			fieldVal = strings.Join(l.gs.Status.Players.Ids, ",")
   240  		default:
   241  			l.logger.Error("unexpected Field to compare")
   242  		}
   243  
   244  		if value == fieldVal {
   245  			l.testMutex.Lock()
   246  			defer l.testMutex.Unlock()
   247  			l.requestSequence = append(l.requestSequence, request)
   248  		} else {
   249  			l.logger.Errorf("expected to receive '%s' as value for '%s' request but received '%s'", fieldVal, request, value)
   250  		}
   251  	}
   252  }
   253  
   254  func (l *LocalSDKServer) updateState(newState agonesv1.GameServerState) {
   255  	l.gsState = newState
   256  	l.gs.Status.State = string(l.gsState)
   257  }
   258  
   259  // Ready logs that the Ready request has been received
   260  func (l *LocalSDKServer) Ready(context.Context, *sdk.Empty) (*sdk.Empty, error) {
   261  	l.logger.Info("Ready request has been received!")
   262  	l.recordRequest("ready")
   263  	l.gsMutex.Lock()
   264  	defer l.gsMutex.Unlock()
   265  
   266  	// Follow the GameServer state diagram
   267  	l.updateState(agonesv1.GameServerStateReady)
   268  	l.stopReserveTimer()
   269  	l.update <- struct{}{}
   270  	return &sdk.Empty{}, nil
   271  }
   272  
   273  // Allocate logs that an allocate request has been received
   274  func (l *LocalSDKServer) Allocate(context.Context, *sdk.Empty) (*sdk.Empty, error) {
   275  	l.logger.Info("Allocate request has been received!")
   276  	l.recordRequest("allocate")
   277  	l.gsMutex.Lock()
   278  	defer l.gsMutex.Unlock()
   279  	l.updateState(agonesv1.GameServerStateAllocated)
   280  	l.stopReserveTimer()
   281  	l.update <- struct{}{}
   282  
   283  	return &sdk.Empty{}, nil
   284  }
   285  
   286  // Shutdown logs that the shutdown request has been received
   287  func (l *LocalSDKServer) Shutdown(context.Context, *sdk.Empty) (*sdk.Empty, error) {
   288  	l.logger.Info("Shutdown request has been received!")
   289  	l.recordRequest("shutdown")
   290  	l.gsMutex.Lock()
   291  	defer l.gsMutex.Unlock()
   292  	l.updateState(agonesv1.GameServerStateShutdown)
   293  	l.stopReserveTimer()
   294  	l.update <- struct{}{}
   295  	return &sdk.Empty{}, nil
   296  }
   297  
   298  // Health logs each health ping that comes down the stream
   299  func (l *LocalSDKServer) Health(stream sdk.SDK_HealthServer) error {
   300  	for {
   301  		_, err := stream.Recv()
   302  		if err == io.EOF {
   303  			l.logger.Info("Health stream closed.")
   304  			return stream.SendAndClose(&sdk.Empty{})
   305  		}
   306  		if err != nil {
   307  			return errors.Wrap(err, "Error with Health check")
   308  		}
   309  		l.recordRequest("health")
   310  		l.logger.Info("Health Ping Received!")
   311  	}
   312  }
   313  
   314  // SetLabel applies a Label to the backing GameServer metadata
   315  func (l *LocalSDKServer) SetLabel(_ context.Context, kv *sdk.KeyValue) (*sdk.Empty, error) {
   316  	l.logger.WithField("values", kv).Info("Setting label")
   317  	l.gsMutex.Lock()
   318  	defer l.gsMutex.Unlock()
   319  
   320  	if l.gs.ObjectMeta == nil {
   321  		l.gs.ObjectMeta = &sdk.GameServer_ObjectMeta{}
   322  	}
   323  	if l.gs.ObjectMeta.Labels == nil {
   324  		l.gs.ObjectMeta.Labels = map[string]string{}
   325  	}
   326  
   327  	l.gs.ObjectMeta.Labels[metadataPrefix+kv.Key] = kv.Value
   328  	l.update <- struct{}{}
   329  	l.recordRequestWithValue("setlabel", kv.Value, "CreationTimestamp")
   330  	return &sdk.Empty{}, nil
   331  }
   332  
   333  // SetAnnotation applies a Annotation to the backing GameServer metadata
   334  func (l *LocalSDKServer) SetAnnotation(_ context.Context, kv *sdk.KeyValue) (*sdk.Empty, error) {
   335  	l.logger.WithField("values", kv).Info("Setting annotation")
   336  	l.gsMutex.Lock()
   337  	defer l.gsMutex.Unlock()
   338  
   339  	if l.gs.ObjectMeta == nil {
   340  		l.gs.ObjectMeta = &sdk.GameServer_ObjectMeta{}
   341  	}
   342  	if l.gs.ObjectMeta.Annotations == nil {
   343  		l.gs.ObjectMeta.Annotations = map[string]string{}
   344  	}
   345  
   346  	l.gs.ObjectMeta.Annotations[metadataPrefix+kv.Key] = kv.Value
   347  	l.update <- struct{}{}
   348  	l.recordRequestWithValue("setannotation", kv.Value, "UID")
   349  	return &sdk.Empty{}, nil
   350  }
   351  
   352  // GetGameServer returns current GameServer configuration.
   353  func (l *LocalSDKServer) GetGameServer(context.Context, *sdk.Empty) (*sdk.GameServer, error) {
   354  	l.logger.Info("Getting GameServer details")
   355  	l.recordRequest("gameserver")
   356  	l.gsMutex.RLock()
   357  	defer l.gsMutex.RUnlock()
   358  	return l.gs, nil
   359  }
   360  
   361  // WatchGameServer will return current GameServer configuration, 3 times, every 5 seconds
   362  func (l *LocalSDKServer) WatchGameServer(_ *sdk.Empty, stream sdk.SDK_WatchGameServerServer) error {
   363  	l.logger.Info("Connected to watch GameServer...")
   364  	observer := make(chan struct{}, 1)
   365  
   366  	defer func() {
   367  		l.updateObservers.Delete(observer)
   368  	}()
   369  
   370  	l.updateObservers.Store(observer, true)
   371  
   372  	l.recordRequest("watch")
   373  
   374  	// send initial game server state
   375  	observer <- struct{}{}
   376  
   377  	for range observer {
   378  		l.gsMutex.RLock()
   379  		err := stream.Send(l.gs)
   380  		l.gsMutex.RUnlock()
   381  		if err != nil {
   382  			l.logger.WithError(err).Error("error sending gameserver")
   383  			return err
   384  		}
   385  	}
   386  
   387  	return nil
   388  }
   389  
   390  // Reserve moves this GameServer to the Reserved state for the Duration specified
   391  func (l *LocalSDKServer) Reserve(ctx context.Context, d *sdk.Duration) (*sdk.Empty, error) {
   392  	l.logger.WithField("duration", d).Info("Reserve request has been received!")
   393  	l.recordRequest("reserve")
   394  	l.gsMutex.Lock()
   395  	defer l.gsMutex.Unlock()
   396  	if d.Seconds > 0 {
   397  		duration := time.Duration(d.Seconds) * time.Second
   398  		l.gsReserveDuration = &duration
   399  		l.resetReserveAfter(ctx, *l.gsReserveDuration)
   400  	}
   401  
   402  	l.updateState(agonesv1.GameServerStateReserved)
   403  	l.update <- struct{}{}
   404  
   405  	return &sdk.Empty{}, nil
   406  }
   407  
   408  func (l *LocalSDKServer) resetReserveAfter(ctx context.Context, duration time.Duration) {
   409  	if l.reserveTimer != nil {
   410  		l.reserveTimer.Stop()
   411  	}
   412  
   413  	l.reserveTimer = time.AfterFunc(duration, func() {
   414  		if _, err := l.Ready(ctx, &sdk.Empty{}); err != nil {
   415  			l.logger.WithError(err).Error("error returning to Ready after reserved ")
   416  		}
   417  	})
   418  }
   419  
   420  func (l *LocalSDKServer) stopReserveTimer() {
   421  	if l.reserveTimer != nil {
   422  		l.reserveTimer.Stop()
   423  	}
   424  	l.gsReserveDuration = nil
   425  }
   426  
   427  // PlayerConnect should be called when a player connects.
   428  // [Stage:Alpha]
   429  // [FeatureFlag:PlayerTracking]
   430  func (l *LocalSDKServer) PlayerConnect(_ context.Context, id *alpha.PlayerID) (*alpha.Bool, error) {
   431  	if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   432  		return &alpha.Bool{Bool: false}, errors.Errorf("%s not enabled", runtime.FeaturePlayerTracking)
   433  	}
   434  	l.logger.WithField("playerID", id.PlayerID).Info("Player Connected")
   435  	l.gsMutex.Lock()
   436  	defer l.gsMutex.Unlock()
   437  
   438  	if l.gs.Status.Players == nil {
   439  		l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{}
   440  	}
   441  
   442  	// the player is already connected, return false.
   443  	for _, playerID := range l.gs.Status.Players.Ids {
   444  		if playerID == id.PlayerID {
   445  			return &alpha.Bool{Bool: false}, nil
   446  		}
   447  	}
   448  
   449  	if l.gs.Status.Players.Count >= l.gs.Status.Players.Capacity {
   450  		return &alpha.Bool{Bool: false}, errors.New("Players are already at capacity")
   451  	}
   452  
   453  	l.gs.Status.Players.Ids = append(l.gs.Status.Players.Ids, id.PlayerID)
   454  	l.gs.Status.Players.Count = int64(len(l.gs.Status.Players.Ids))
   455  
   456  	l.update <- struct{}{}
   457  	l.recordRequestWithValue("playerconnect", "1234", "PlayerIDs")
   458  	return &alpha.Bool{Bool: true}, nil
   459  }
   460  
   461  // PlayerDisconnect should be called when a player disconnects.
   462  // [Stage:Alpha]
   463  // [FeatureFlag:PlayerTracking]
   464  func (l *LocalSDKServer) PlayerDisconnect(_ context.Context, id *alpha.PlayerID) (*alpha.Bool, error) {
   465  	if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   466  		return &alpha.Bool{Bool: false}, errors.Errorf("%s not enabled", runtime.FeaturePlayerTracking)
   467  	}
   468  	l.logger.WithField("playerID", id.PlayerID).Info("Player Disconnected")
   469  	l.gsMutex.Lock()
   470  	defer l.gsMutex.Unlock()
   471  
   472  	if l.gs.Status.Players == nil {
   473  		l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{}
   474  	}
   475  
   476  	found := -1
   477  	for i, playerID := range l.gs.Status.Players.Ids {
   478  		if playerID == id.PlayerID {
   479  			found = i
   480  			break
   481  		}
   482  	}
   483  	if found == -1 {
   484  		return &alpha.Bool{Bool: false}, nil
   485  	}
   486  
   487  	l.gs.Status.Players.Ids = append(l.gs.Status.Players.Ids[:found], l.gs.Status.Players.Ids[found+1:]...)
   488  	l.gs.Status.Players.Count = int64(len(l.gs.Status.Players.Ids))
   489  
   490  	l.update <- struct{}{}
   491  	l.recordRequestWithValue("playerdisconnect", "", "PlayerIDs")
   492  	return &alpha.Bool{Bool: true}, nil
   493  }
   494  
   495  // IsPlayerConnected returns if the playerID is currently connected to the GameServer.
   496  // [Stage:Alpha]
   497  // [FeatureFlag:PlayerTracking]
   498  func (l *LocalSDKServer) IsPlayerConnected(_ context.Context, id *alpha.PlayerID) (*alpha.Bool, error) {
   499  	if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   500  		return &alpha.Bool{Bool: false}, errors.Errorf("%s not enabled", runtime.FeaturePlayerTracking)
   501  	}
   502  
   503  	result := &alpha.Bool{Bool: false}
   504  	l.logger.WithField("playerID", id.PlayerID).Info("Is a Player Connected?")
   505  	l.gsMutex.Lock()
   506  	defer l.gsMutex.Unlock()
   507  
   508  	l.recordRequestWithValue("isplayerconnected", id.PlayerID, "PlayerIDs")
   509  
   510  	if l.gs.Status.Players == nil {
   511  		return result, nil
   512  	}
   513  
   514  	for _, playerID := range l.gs.Status.Players.Ids {
   515  		if id.PlayerID == playerID {
   516  			result.Bool = true
   517  			break
   518  		}
   519  	}
   520  
   521  	return result, nil
   522  }
   523  
   524  // GetConnectedPlayers returns the list of the currently connected player ids.
   525  // [Stage:Alpha]
   526  // [FeatureFlag:PlayerTracking]
   527  func (l *LocalSDKServer) GetConnectedPlayers(_ context.Context, _ *alpha.Empty) (*alpha.PlayerIDList, error) {
   528  	if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   529  		return nil, errors.Errorf("%s not enabled", runtime.FeaturePlayerTracking)
   530  	}
   531  	l.logger.Info("Getting Connected Players")
   532  
   533  	result := &alpha.PlayerIDList{List: []string{}}
   534  
   535  	l.gsMutex.Lock()
   536  	defer l.gsMutex.Unlock()
   537  	l.recordRequest("getconnectedplayers")
   538  
   539  	if l.gs.Status.Players == nil {
   540  		return result, nil
   541  	}
   542  	result.List = l.gs.Status.Players.Ids
   543  	return result, nil
   544  }
   545  
   546  // GetPlayerCount returns the current player count.
   547  // [Stage:Alpha]
   548  // [FeatureFlag:PlayerTracking]
   549  func (l *LocalSDKServer) GetPlayerCount(_ context.Context, _ *alpha.Empty) (*alpha.Count, error) {
   550  	if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   551  		return nil, errors.Errorf("%s not enabled", runtime.FeaturePlayerTracking)
   552  	}
   553  	l.logger.Info("Getting Player Count")
   554  	l.recordRequest("getplayercount")
   555  	l.gsMutex.RLock()
   556  	defer l.gsMutex.RUnlock()
   557  
   558  	result := &alpha.Count{}
   559  	if l.gs.Status.Players != nil {
   560  		result.Count = l.gs.Status.Players.Count
   561  	}
   562  
   563  	return result, nil
   564  }
   565  
   566  // SetPlayerCapacity to change the game server's player capacity.
   567  // [Stage:Alpha]
   568  // [FeatureFlag:PlayerTracking]
   569  func (l *LocalSDKServer) SetPlayerCapacity(_ context.Context, count *alpha.Count) (*alpha.Empty, error) {
   570  	if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   571  		return nil, errors.Errorf("%s not enabled", runtime.FeaturePlayerTracking)
   572  	}
   573  
   574  	l.logger.WithField("capacity", count.Count).Info("Setting Player Capacity")
   575  	l.gsMutex.Lock()
   576  	defer l.gsMutex.Unlock()
   577  
   578  	if l.gs.Status.Players == nil {
   579  		l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{}
   580  	}
   581  
   582  	l.gs.Status.Players.Capacity = count.Count
   583  
   584  	l.update <- struct{}{}
   585  	l.recordRequestWithValue("setplayercapacity", strconv.FormatInt(count.Count, 10), "PlayerCapacity")
   586  	return &alpha.Empty{}, nil
   587  }
   588  
   589  // GetPlayerCapacity returns the current player capacity.
   590  // [Stage:Alpha]
   591  // [FeatureFlag:PlayerTracking]
   592  func (l *LocalSDKServer) GetPlayerCapacity(_ context.Context, _ *alpha.Empty) (*alpha.Count, error) {
   593  	if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
   594  		return nil, errors.Errorf("%s not enabled", runtime.FeaturePlayerTracking)
   595  	}
   596  	l.logger.Info("Getting Player Capacity")
   597  	l.recordRequest("getplayercapacity")
   598  	l.gsMutex.RLock()
   599  	defer l.gsMutex.RUnlock()
   600  
   601  	// SDK.GetPlayerCapacity() has a contract of always return a number,
   602  	// so if we're nil, then let's always return a value, and
   603  	// remove lots of special cases upstream.
   604  	result := &alpha.Count{}
   605  	if l.gs.Status.Players != nil {
   606  		result.Count = l.gs.Status.Players.Capacity
   607  	}
   608  
   609  	return result, nil
   610  }
   611  
   612  // GetCounter returns a Counter. Returns not found if the counter does not exist.
   613  // [Stage:Beta]
   614  // [FeatureFlag:CountsAndLists]
   615  func (l *LocalSDKServer) GetCounter(_ context.Context, in *beta.GetCounterRequest) (*beta.Counter, error) {
   616  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   617  		return nil, errors.Errorf("%s not enabled", runtime.FeatureCountsAndLists)
   618  	}
   619  
   620  	if in == nil {
   621  		return nil, errors.Errorf("invalid argument. GetCounterRequest cannot be nil")
   622  	}
   623  
   624  	l.logger.WithField("name", in.Name).Info("Getting Counter")
   625  	l.recordRequest("getcounter")
   626  	l.gsMutex.RLock()
   627  	defer l.gsMutex.RUnlock()
   628  
   629  	if counter, ok := l.gs.Status.Counters[in.Name]; ok {
   630  		return &beta.Counter{Name: in.Name, Count: counter.Count, Capacity: counter.Capacity}, nil
   631  	}
   632  	return nil, errors.Errorf("not found. %s Counter not found", in.Name)
   633  }
   634  
   635  // UpdateCounter updates the given Counter. Unlike the SDKServer, this LocalSDKServer UpdateCounter
   636  // does not batch requests, and directly updates the localsdk gameserver.
   637  // Returns error if the Counter does not exist (name cannot be updated).
   638  // Returns error if the Count is out of range [0,Capacity].
   639  // [Stage:Beta]
   640  // [FeatureFlag:CountsAndLists]
   641  func (l *LocalSDKServer) UpdateCounter(_ context.Context, in *beta.UpdateCounterRequest) (*beta.Counter, error) {
   642  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   643  		return nil, errors.Errorf("%s not enabled", runtime.FeatureCountsAndLists)
   644  	}
   645  
   646  	if in.CounterUpdateRequest == nil {
   647  		return nil, errors.Errorf("invalid argument. CounterUpdateRequest cannot be nil")
   648  	}
   649  
   650  	name := in.CounterUpdateRequest.Name
   651  
   652  	l.logger.WithField("name", name).Info("Updating Counter")
   653  	l.gsMutex.Lock()
   654  	defer l.gsMutex.Unlock()
   655  
   656  	counter, ok := l.gs.Status.Counters[name]
   657  	if !ok {
   658  		return nil, errors.Errorf("not found. %s Counter not found", name)
   659  	}
   660  
   661  	tmpCounter := beta.Counter{Name: name, Count: counter.Count, Capacity: counter.Capacity}
   662  	// Set Capacity
   663  	if in.CounterUpdateRequest.Capacity != nil {
   664  		l.recordRequest("setcapacitycounter")
   665  		tmpCounter.Capacity = in.CounterUpdateRequest.Capacity.GetValue()
   666  		if tmpCounter.Capacity < 0 {
   667  			return nil, errors.Errorf("out of range. Capacity must be greater than or equal to 0. Found Capacity: %d",
   668  				tmpCounter.Capacity)
   669  		}
   670  	}
   671  	// Set Count
   672  	if in.CounterUpdateRequest.Count != nil {
   673  		l.recordRequest("setcountcounter")
   674  		tmpCounter.Count = in.CounterUpdateRequest.Count.GetValue()
   675  		if tmpCounter.Count < 0 || tmpCounter.Count > tmpCounter.Capacity {
   676  			return nil, errors.Errorf("out of range. Count must be within range [0,Capacity]. Found Count: %d, Capacity: %d",
   677  				tmpCounter.Count, tmpCounter.Capacity)
   678  		}
   679  	}
   680  	// Increment or Decrement Count
   681  	if in.CounterUpdateRequest.CountDiff != 0 {
   682  		l.recordRequest("updatecounter")
   683  		tmpCounter.Count += in.CounterUpdateRequest.CountDiff
   684  		if tmpCounter.Count < 0 || tmpCounter.Count > tmpCounter.Capacity {
   685  			return nil, errors.Errorf("out of range. Count must be within range [0,Capacity]. Found Count: %d, Capacity: %d",
   686  				tmpCounter.Count, tmpCounter.Capacity)
   687  		}
   688  	}
   689  
   690  	// Write newly updated List to gameserverstatus.
   691  	counter.Capacity = tmpCounter.Capacity
   692  	counter.Count = tmpCounter.Count
   693  	l.gs.Status.Counters[name] = counter
   694  	return &tmpCounter, nil
   695  }
   696  
   697  // GetList returns a List. Returns not found if the List does not exist.
   698  // [Stage:Beta]
   699  // [FeatureFlag:CountsAndLists]
   700  func (l *LocalSDKServer) GetList(_ context.Context, in *beta.GetListRequest) (*beta.List, error) {
   701  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   702  		return nil, errors.Errorf("%s not enabled", runtime.FeatureCountsAndLists)
   703  	}
   704  
   705  	l.logger.WithField("name", in.Name).Info("Getting List")
   706  	l.recordRequest("getlist")
   707  	l.gsMutex.RLock()
   708  	defer l.gsMutex.RUnlock()
   709  
   710  	if list, ok := l.gs.Status.Lists[in.Name]; ok {
   711  		return &beta.List{Name: in.Name, Capacity: list.Capacity, Values: list.Values}, nil
   712  	}
   713  	return nil, errors.Errorf("not found. %s List not found", in.Name)
   714  }
   715  
   716  // UpdateList returns the updated List. Returns not found if the List does not exist (name cannot be updated).
   717  // **THIS WILL OVERWRITE ALL EXISTING LIST.VALUES WITH ANY REQUEST LIST.VALUES**
   718  // Use AddListValue() or RemoveListValue() for modifying the List.Values field.
   719  // Returns invalid argument if the field mask path(s) are not field(s) of the List.
   720  // If a field mask path(s) is specified, but the value is not set in the request List object,
   721  // then the default value for the variable will be set (i.e. 0 for "capacity", empty list for "values").
   722  // [Stage:Beta]
   723  // [FeatureFlag:CountsAndLists]
   724  func (l *LocalSDKServer) UpdateList(_ context.Context, in *beta.UpdateListRequest) (*beta.List, error) {
   725  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   726  		return nil, errors.Errorf("%s not enabled", runtime.FeatureCountsAndLists)
   727  	}
   728  
   729  	if in.List == nil || in.UpdateMask == nil {
   730  		return nil, errors.Errorf("invalid argument. List: %v and UpdateMask %v cannot be nil", in.List, in.UpdateMask)
   731  	}
   732  
   733  	l.logger.WithField("name", in.List.Name).Info("Updating List")
   734  	l.recordRequest("updatelist")
   735  	l.gsMutex.Lock()
   736  	defer l.gsMutex.Unlock()
   737  
   738  	// TODO: https://google.aip.dev/134, "Update masks must support a special value *, meaning full replacement."
   739  	// Check if the UpdateMask paths are valid, return invalid argument if not.
   740  	if !in.UpdateMask.IsValid(in.List.ProtoReflect().Interface()) {
   741  		return nil, errors.Errorf("invalid argument. Field Mask Path(s): %v are invalid for List. Use valid field name(s): %v", in.UpdateMask.GetPaths(), in.List.ProtoReflect().Descriptor().Fields())
   742  	}
   743  
   744  	if in.List.Capacity < 0 || in.List.Capacity > apiserver.ListMaxCapacity {
   745  		return nil, errors.Errorf("out of range. Capacity must be within range [0,1000]. Found Capacity: %d", in.List.Capacity)
   746  	}
   747  
   748  	name := in.List.Name
   749  	if list, ok := l.gs.Status.Lists[name]; ok {
   750  		// Create *beta.List from *sdk.GameServer_Status_ListStatus for merging.
   751  		tmpList := &beta.List{Name: name, Capacity: list.Capacity, Values: list.Values}
   752  		// Removes any fields from the request object that are not included in the FieldMask Paths.
   753  		fmutils.Filter(in.List, in.UpdateMask.Paths)
   754  		// Removes any fields from the existing gameserver object that are included in the FieldMask Paths.
   755  		fmutils.Prune(tmpList, in.UpdateMask.Paths)
   756  		// Due due filtering and pruning all gameserver object field(s) contained in the FieldMask are overwritten by the request object field(s).
   757  		proto.Merge(tmpList, in.List)
   758  		// Silently truncate list values if Capacity < len(Values)
   759  		if tmpList.Capacity < int64(len(tmpList.Values)) {
   760  			tmpList.Values = append([]string{}, tmpList.Values[:tmpList.Capacity]...)
   761  		}
   762  		// Write newly updated List to gameserverstatus.
   763  		l.gs.Status.Lists[name].Capacity = tmpList.Capacity
   764  		l.gs.Status.Lists[name].Values = tmpList.Values
   765  		return &beta.List{Name: name, Capacity: l.gs.Status.Lists[name].Capacity, Values: l.gs.Status.Lists[name].Values}, nil
   766  	}
   767  	return nil, errors.Errorf("not found. %s List not found", name)
   768  }
   769  
   770  // AddListValue appends a value to the end of a List and returns updated List.
   771  // Returns not found if the List does not exist.
   772  // Returns already exists if the value is already in the List.
   773  // Returns out of range if the List is already at Capacity.
   774  // [Stage:Beta]
   775  // [FeatureFlag:CountsAndLists]
   776  func (l *LocalSDKServer) AddListValue(_ context.Context, in *beta.AddListValueRequest) (*beta.List, error) {
   777  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   778  		return nil, errors.Errorf("%s not enabled", runtime.FeatureCountsAndLists)
   779  	}
   780  
   781  	l.logger.WithField("name", in.Name).Info("Adding Value to List")
   782  	l.recordRequest("addlistvalue")
   783  	l.gsMutex.Lock()
   784  	defer l.gsMutex.Unlock()
   785  
   786  	if list, ok := l.gs.Status.Lists[in.Name]; ok {
   787  		// Verify room to add another value
   788  		if list.Capacity <= int64(len(list.Values)) {
   789  			return nil, errors.Errorf("out of range. No available capacity. Current Capacity: %d, List Size: %d", list.Capacity, len(list.Values))
   790  		}
   791  		// Verify value does not already exist in the list
   792  		for _, val := range l.gs.Status.Lists[in.Name].Values {
   793  			if in.Value == val {
   794  				return nil, errors.Errorf("already exists. Value: %s already in List: %s", in.Value, in.Name)
   795  			}
   796  		}
   797  		// Add new value to gameserverstatus.
   798  		l.gs.Status.Lists[in.Name].Values = append(l.gs.Status.Lists[in.Name].Values, in.Value)
   799  		return &beta.List{Name: in.Name, Capacity: l.gs.Status.Lists[in.Name].Capacity, Values: l.gs.Status.Lists[in.Name].Values}, nil
   800  	}
   801  	return nil, errors.Errorf("not found. %s List not found", in.Name)
   802  }
   803  
   804  // RemoveListValue removes a value from a List and returns updated List.
   805  // Returns not found if the List does not exist.
   806  // Returns not found if the value is not in the List.
   807  // [Stage:Beta]
   808  // [FeatureFlag:CountsAndLists]
   809  func (l *LocalSDKServer) RemoveListValue(_ context.Context, in *beta.RemoveListValueRequest) (*beta.List, error) {
   810  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   811  		return nil, errors.Errorf("%s not enabled", runtime.FeatureCountsAndLists)
   812  	}
   813  
   814  	l.logger.WithField("name", in.Name).Info("Removing Value from List")
   815  	l.recordRequest("removelistvalue")
   816  	l.gsMutex.Lock()
   817  	defer l.gsMutex.Unlock()
   818  
   819  	if list, ok := l.gs.Status.Lists[in.Name]; ok {
   820  		// Verify value exists in the list
   821  		for i, val := range l.gs.Status.Lists[in.Name].Values {
   822  			if in.Value == val {
   823  				// Remove value (maintains list ordering and modifies underlying gameserverstatus List.Values array).
   824  				list.Values = append(list.Values[:i], list.Values[i+1:]...)
   825  				return &beta.List{Name: in.Name, Capacity: l.gs.Status.Lists[in.Name].Capacity, Values: l.gs.Status.Lists[in.Name].Values}, nil
   826  			}
   827  		}
   828  		return nil, errors.Errorf("not found. Value: %s not found in List: %s", in.Value, in.Name)
   829  	}
   830  	return nil, errors.Errorf("not found. %s List not found", in.Name)
   831  }
   832  
   833  // Close tears down all the things
   834  func (l *LocalSDKServer) Close() {
   835  	l.updateObservers.Range(func(observer, _ interface{}) bool {
   836  		close(observer.(chan struct{}))
   837  		return true
   838  	})
   839  	l.compare()
   840  }
   841  
   842  // EqualSets tells whether expected and received slices contain the same elements.
   843  // A nil argument is equivalent to an empty slice.
   844  func (l *LocalSDKServer) EqualSets(expected, received []string) bool {
   845  	aSet := make(map[string]bool)
   846  	bSet := make(map[string]bool)
   847  	for _, v := range expected {
   848  		aSet[v] = true
   849  	}
   850  	for _, v := range received {
   851  		if _, ok := aSet[v]; !ok {
   852  			l.logger.WithField("request", v).Error("Found a request which was not expected")
   853  			return false
   854  		}
   855  		bSet[v] = true
   856  	}
   857  	for _, v := range expected {
   858  		if _, ok := bSet[v]; !ok {
   859  			l.logger.WithField("request", v).Error("Could not find a request which was expected")
   860  			return false
   861  		}
   862  	}
   863  	return true
   864  }
   865  
   866  // compare the results of a test run
   867  func (l *LocalSDKServer) compare() {
   868  	if l.testMode {
   869  		l.testMutex.Lock()
   870  		defer l.testMutex.Unlock()
   871  		if !l.EqualSets(l.expectedSequence, l.requestSequence) {
   872  			l.logger.WithField("expected", l.expectedSequence).WithField("received", l.requestSequence).Info("Testing Failed")
   873  			// we don't care if the mutex gets unlocked on exit, so ignore the warning.
   874  			// nolint: gocritic
   875  			os.Exit(1)
   876  		}
   877  		l.logger.Info("Received requests match expected list. Test run was successful")
   878  	}
   879  }
   880  
   881  func (l *LocalSDKServer) setGameServerFromFilePath(filePath string) error {
   882  	l.logger.WithField("filePath", filePath).Info("Reading GameServer configuration")
   883  
   884  	reader, err := os.Open(filePath) // nolint: gosec
   885  	defer reader.Close()             // nolint: staticcheck,errcheck
   886  
   887  	if err != nil {
   888  		return err
   889  	}
   890  
   891  	var gs agonesv1.GameServer
   892  	// 4096 is the number of bytes the YAMLOrJSONDecoder goes looking
   893  	// into the file to determine if it's JSON or YAML
   894  	// (JSON == has whitespace followed by an open brace).
   895  	// The Kubernetes uses 4096 bytes as its default, so that's what we'll
   896  	// use as well.
   897  	// https://github.com/kubernetes/kubernetes/blob/master/plugin/pkg/admission/podnodeselector/admission.go#L86
   898  	decoder := yaml.NewYAMLOrJSONDecoder(reader, 4096)
   899  	err = decoder.Decode(&gs)
   900  	if err != nil {
   901  		return err
   902  	}
   903  
   904  	l.gsMutex.Lock()
   905  	defer l.gsMutex.Unlock()
   906  	l.gs = convert(&gs)
   907  
   908  	// Set LogLevel if specified
   909  	logLevel := agonesv1.SdkServerLogLevelInfo
   910  	if gs.Spec.SdkServer.LogLevel != "" {
   911  		logLevel = gs.Spec.SdkServer.LogLevel
   912  	}
   913  	l.logger.WithField("logLevel", logLevel).Debug("Setting LogLevel configuration")
   914  	level, err := logrus.ParseLevel(strings.ToLower(string(logLevel)))
   915  	if err == nil {
   916  		l.logger.Logger.SetLevel(level)
   917  	} else {
   918  		l.logger.WithError(err).Warn("Specified wrong Logging.SdkServer. Setting default loglevel - Info")
   919  		l.logger.Logger.SetLevel(logrus.InfoLevel)
   920  	}
   921  	return nil
   922  }