diff --git a/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml index cd794389bf..1ed768d7f5 100644 --- a/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml +++ b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml @@ -148,4 +148,30 @@ properties: type: integer minimum: 1 maximum: 2147483648 + alpha: + type: object + title: Alpha properties for the GameServer + properties: + players: + type: object + title: Configuration of player capacity + properties: + initialCapacity: + type: integer + title: The intial player capacity that this Game Server has + minimum: 0 + webhook: + type: object + title: webhook to call when a player connects or disconnects + properties: + service: + properties: + name: + type: string + namespace: + type: string + path: + type: string + url: + type: string {{- end }} \ No newline at end of file diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index 4d14eb8e23..9177a4f9de 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -394,6 +394,32 @@ spec: type: integer minimum: 1 maximum: 2147483648 + alpha: + type: object + title: Alpha properties for the GameServer + properties: + players: + type: object + title: Configuration of player capacity + properties: + initialCapacity: + type: integer + title: The intial player capacity that this Game Server has + minimum: 0 + webhook: + type: object + title: webhook to call when a player connects or disconnects + properties: + service: + properties: + name: + type: string + namespace: + type: string + path: + type: string + url: + type: string subresources: # status enables the status subresource. status: {} @@ -678,6 +704,32 @@ spec: type: integer minimum: 1 maximum: 2147483648 + alpha: + type: object + title: Alpha properties for the GameServer + properties: + players: + type: object + title: Configuration of player capacity + properties: + initialCapacity: + type: integer + title: The intial player capacity that this Game Server has + minimum: 0 + webhook: + type: object + title: webhook to call when a player connects or disconnects + properties: + service: + properties: + name: + type: string + namespace: + type: string + path: + type: string + url: + type: string --- # Source: agones/templates/crds/gameserverallocationpolicy.yaml @@ -973,6 +1025,32 @@ spec: type: integer minimum: 1 maximum: 2147483648 + alpha: + type: object + title: Alpha properties for the GameServer + properties: + players: + type: object + title: Configuration of player capacity + properties: + initialCapacity: + type: integer + title: The intial player capacity that this Game Server has + minimum: 0 + webhook: + type: object + title: webhook to call when a player connects or disconnects + properties: + service: + properties: + name: + type: string + namespace: + type: string + path: + type: string + url: + type: string subresources: # status enables the status subresource. status: {} diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go index bdc38d9a0a..e5012d9464 100644 --- a/pkg/apis/agones/v1/gameserver.go +++ b/pkg/apis/agones/v1/gameserver.go @@ -19,12 +19,14 @@ import ( "fmt" "net" - "github.com/mattbaird/jsonpatch" + "agones.dev/agones/pkg/util/runtime" "agones.dev/agones/pkg" "agones.dev/agones/pkg/apis" "agones.dev/agones/pkg/apis/agones" + "github.com/mattbaird/jsonpatch" "github.com/pkg/errors" + admregv1b "k8s.io/api/admissionregistration/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -150,6 +152,20 @@ type GameServerSpec struct { SdkServer SdkServer `json:"sdkServer,omitempty"` // Template describes the Pod that will be created for the GameServer Template corev1.PodTemplateSpec `json:"template"` + // AlphaSpec describes the alpha properties for the GameServer + Alpha AlphaSpec `json:"alpha,omitempty"` +} + +// AlphaSpec is the alpha properties of the GameServer +type AlphaSpec struct { + Players PlayersSpec `json:"players"` +} + +// PlayersSpec tracks the initial player capacity, and what webhooks to send events to when there are +// connection/disconnection events. +type PlayersSpec struct { + InitialCapacity int64 `json:"initialCapacity,omitempty"` + Webhook *admregv1b.WebhookClientConfig `json:"webhook,omitempty"` } // GameServerState is the state for the GameServer @@ -210,6 +226,7 @@ type GameServerStatus struct { Address string `json:"address"` NodeName string `json:"nodeName"` ReservedUntil *metav1.Time `json:"reservedUntil"` + Alpha AlphaStatus `json:"alpha"` } // GameServerStatusPort shows the port that was allocated to a @@ -219,6 +236,17 @@ type GameServerStatusPort struct { Port int32 `json:"port"` } +// AlphaStatus is the alpha status values for a GameServer +type AlphaStatus struct { + Players PlayerStatus `json:"players"` +} + +// PlayerStatus stores the current player capacity values +type PlayerStatus struct { + Count int64 `json:"count"` + Capacity int64 `json:"capacity"` +} + // ApplyDefaults applies default values to the GameServer if they are not already populated func (gs *GameServer) ApplyDefaults() { // VersionAnnotation is the annotation that stores @@ -230,7 +258,7 @@ func (gs *GameServer) ApplyDefaults() { gs.ObjectMeta.Finalizers = append(gs.ObjectMeta.Finalizers, agones.GroupName) gs.Spec.ApplyDefaults() - gs.applyStateDefaults() + gs.applyStatusDefaults() } // ApplyDefaults applies default values to the GameServerSpec if they are not already populated @@ -277,15 +305,19 @@ func (gss *GameServerSpec) applyHealthDefaults() { } } -// applyStateDefaults applies state defaults -func (gs *GameServer) applyStateDefaults() { +// applyStatusDefaults applies Status defaults +func (gs *GameServer) applyStatusDefaults() { if gs.Status.State == "" { gs.Status.State = GameServerStateCreating - // applyStateDefaults() should be called after applyPortDefaults() + // applyStatusDefaults() should be called after applyPortDefaults() if gs.HasPortPolicy(Dynamic) || gs.HasPortPolicy(Passthrough) { gs.Status.State = GameServerStatePortAllocation } } + + if runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + gs.Status.Alpha.Players.Capacity = gs.Spec.Alpha.Players.InitialCapacity + } } // applyPortDefaults applies default values for all ports diff --git a/pkg/apis/agones/v1/gameserver_test.go b/pkg/apis/agones/v1/gameserver_test.go index ffe6bc13ee..20ffeed382 100644 --- a/pkg/apis/agones/v1/gameserver_test.go +++ b/pkg/apis/agones/v1/gameserver_test.go @@ -17,8 +17,11 @@ package v1 import ( "fmt" "strings" + "sync" "testing" + "agones.dev/agones/pkg/util/runtime" + "agones.dev/agones/pkg" "agones.dev/agones/pkg/apis" "agones.dev/agones/pkg/apis/agones" @@ -62,21 +65,25 @@ func TestGameServerApplyDefaults(t *testing.T) { t.Parallel() type expected struct { - protocol corev1.Protocol - state GameServerState - policy PortPolicy - health Health - scheduling apis.SchedulingStrategy - sdkServer SdkServer + protocol corev1.Protocol + state GameServerState + policy PortPolicy + health Health + scheduling apis.SchedulingStrategy + sdkServer SdkServer + alphaPlayerCapacity int64 } data := map[string]struct { - gameServer GameServer - container string - expected expected + gameServer GameServer + container string + featureFlags string + expected expected }{ "set basic defaults on a very simple gameserver": { + featureFlags: runtime.FeaturePlayerTracking + "=true", gameServer: GameServer{ Spec: GameServerSpec{ + Alpha: AlphaSpec{Players: PlayersSpec{InitialCapacity: 10}}, Ports: []GameServerPort{{ContainerPort: 999}}, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{Containers: []corev1.Container{ @@ -100,6 +107,7 @@ func TestGameServerApplyDefaults(t *testing.T) { GRPCPort: 9357, HTTPPort: 9358, }, + alphaPlayerCapacity: 10, }, }, "defaults on passthrough": { @@ -180,6 +188,7 @@ func TestGameServerApplyDefaults(t *testing.T) { gameServer: GameServer{ Spec: GameServerSpec{ Ports: []GameServerPort{{PortPolicy: Static}}, + Alpha: AlphaSpec{Players: PlayersSpec{InitialCapacity: 10}}, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}}}, }, @@ -319,8 +328,15 @@ func TestGameServerApplyDefaults(t *testing.T) { }, } + // otherwise the race condition detector is not happy. + mtx := sync.Mutex{} for name, test := range data { t.Run(name, func(t *testing.T) { + mtx.Lock() + err := runtime.ParseFeatures(test.featureFlags) + mtx.Unlock() + assert.NoError(t, err) + test.gameServer.ApplyDefaults() assert.Equal(t, pkg.Version, test.gameServer.Annotations[VersionAnnotation]) @@ -333,6 +349,7 @@ func TestGameServerApplyDefaults(t *testing.T) { assert.Equal(t, test.expected.scheduling, test.gameServer.Spec.Scheduling) assert.Equal(t, test.expected.health, test.gameServer.Spec.Health) assert.Equal(t, test.expected.sdkServer, test.gameServer.Spec.SdkServer) + assert.Equal(t, test.expected.alphaPlayerCapacity, test.gameServer.Status.Alpha.Players.Capacity) }) } } diff --git a/pkg/apis/agones/v1/zz_generated.deepcopy.go b/pkg/apis/agones/v1/zz_generated.deepcopy.go index 840f0f1482..bb4f9ebeea 100644 --- a/pkg/apis/agones/v1/zz_generated.deepcopy.go +++ b/pkg/apis/agones/v1/zz_generated.deepcopy.go @@ -21,9 +21,44 @@ package v1 import ( + v1beta1 "k8s.io/api/admissionregistration/v1beta1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlphaSpec) DeepCopyInto(out *AlphaSpec) { + *out = *in + in.Players.DeepCopyInto(&out.Players) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlphaSpec. +func (in *AlphaSpec) DeepCopy() *AlphaSpec { + if in == nil { + return nil + } + out := new(AlphaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlphaStatus) DeepCopyInto(out *AlphaStatus) { + *out = *in + out.Players = in.Players + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlphaStatus. +func (in *AlphaStatus) DeepCopy() *AlphaStatus { + if in == nil { + return nil + } + out := new(AlphaStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Fleet) DeepCopyInto(out *Fleet) { *out = *in @@ -301,6 +336,7 @@ func (in *GameServerSpec) DeepCopyInto(out *GameServerSpec) { out.Health = in.Health out.SdkServer = in.SdkServer in.Template.DeepCopyInto(&out.Template) + in.Alpha.DeepCopyInto(&out.Alpha) return } @@ -326,6 +362,7 @@ func (in *GameServerStatus) DeepCopyInto(out *GameServerStatus) { in, out := &in.ReservedUntil, &out.ReservedUntil *out = (*in).DeepCopy() } + out.Alpha = in.Alpha return } @@ -389,6 +426,43 @@ func (in *Health) DeepCopy() *Health { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlayerStatus) DeepCopyInto(out *PlayerStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlayerStatus. +func (in *PlayerStatus) DeepCopy() *PlayerStatus { + if in == nil { + return nil + } + out := new(PlayerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlayersSpec) DeepCopyInto(out *PlayersSpec) { + *out = *in + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(v1beta1.WebhookClientConfig) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlayersSpec. +func (in *PlayersSpec) DeepCopy() *PlayersSpec { + if in == nil { + return nil + } + out := new(PlayersSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SdkServer) DeepCopyInto(out *SdkServer) { *out = *in diff --git a/pkg/util/runtime/features.go b/pkg/util/runtime/features.go index 3aaaa79875..08e8bbf7b5 100644 --- a/pkg/util/runtime/features.go +++ b/pkg/util/runtime/features.go @@ -28,13 +28,17 @@ const ( // FeatureExample is an example feature gate flag, used for testing and demonstrative purposes FeatureExample Feature = "Example" + + // FeaturePlayerTracking is a feature flag to enable/disable player tracking features. + FeaturePlayerTracking = "PlayerTracking" ) var ( // featureDefaults is a map of all Feature Gates that are // operational in Agones, and what their default configuration is. featureDefaults = map[Feature]bool{ - FeatureExample: true, + FeatureExample: true, + FeaturePlayerTracking: false, } // featureGates is the storage of what features are enabled diff --git a/pkg/util/runtime/features_test.go b/pkg/util/runtime/features_test.go index 70d5fe39e6..70c44c1dd4 100644 --- a/pkg/util/runtime/features_test.go +++ b/pkg/util/runtime/features_test.go @@ -26,7 +26,11 @@ import ( func TestFeatures(t *testing.T) { t.Parallel() - featureDefaults[Feature("Test")] = false + // stable feature flag state + featureDefaults = map[Feature]bool{ + FeatureExample: true, + Feature("Test"): false, + } t.Run("invalid Feature gate", func(t *testing.T) { err := ParseFeatures("Foo") diff --git a/site/content/en/docs/Reference/agones_crd_api_reference.html b/site/content/en/docs/Reference/agones_crd_api_reference.html index f2099a04bc..a10ac5334d 100644 --- a/site/content/en/docs/Reference/agones_crd_api_reference.html +++ b/site/content/en/docs/Reference/agones_crd_api_reference.html @@ -2534,6 +2534,19 @@

GameServer

Template describes the Pod that will be created for the GameServer

+ + +alpha
+ + +AlphaSpec + + + + +

AlphaSpec describes the alpha properties for the GameServer

+ + @@ -2662,6 +2675,68 @@

GameServerSet +

AlphaSpec +

+

+(Appears on: +GameServerSpec) +

+

+

AlphaSpec is the alpha properties of the GameServer

+

+ + + + + + + + + + + + + +
FieldDescription
+players
+ + +PlayersSpec + + +
+
+

AlphaStatus +

+

+(Appears on: +GameServerStatus) +

+

+

AlphaStatus is the alpha status values for a GameServer

+

+ + + + + + + + + + + + + +
FieldDescription
+players
+ + +PlayerStatus + + +
+

FleetSpec

@@ -3098,6 +3173,19 @@

GameServerSpec

Template describes the Pod that will be created for the GameServer

+ + +alpha
+ + +AlphaSpec + + + + +

AlphaSpec describes the alpha properties for the GameServer

+ +

GameServerState @@ -3183,6 +3271,18 @@

GameServerStatus + + +alpha
+ + +AlphaStatus + + + + + +

GameServerStatusPort @@ -3346,6 +3446,19 @@

GameServerTemplateSpec

Template describes the Pod that will be created for the GameServer

+ + +alpha
+ + +AlphaSpec + + + + +

AlphaSpec describes the alpha properties for the GameServer

+ + @@ -3414,6 +3527,87 @@

Health +

PlayerStatus +

+

+(Appears on: +AlphaStatus) +

+

+

PlayerStatus stores the current player capacity values

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+count
+ +int64 + +
+
+capacity
+ +int64 + +
+
+

PlayersSpec +

+

+(Appears on: +AlphaSpec) +

+

+

PlayersSpec tracks the initial player capacity, and what webhooks to send events to when there are +connection/disconnection events.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+initialCapacity
+ +int64 + +
+
+webhook
+ + +Kubernetes admissionregistration/v1beta1.WebhookClientConfig + + +
+

PortPolicy (string alias)