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 }