Loading docs/openapi.yaml +10 −0 Changes for docs/openapi.yaml: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -488,6 +488,16 @@ components: Present and true when the CR carries the label felis.lolicon.best/forwarding=legacy. The proxy then forwards this server's players BungeeCord-style in the handshake address instead of modern forwarding (Felis-Legacy Velocity fork only). autoRestarts: type: integer format: int32 description: Present when non-zero; how often the operator recreated the pod of this start after it timed out (at most 3). startGaveUp: type: boolean description: >- Present and true for a Failed server no automatic retry will bring up: its start timed out with the retries spent, or its spec is invalid. A Failed server without it is still in its restart backoff and may come up on its own. FleetServer: description: One row of the fleet-wide admin read (internal/api/handlers_user.go fleetServerView). Loading internal/api/cluster.go +6 −0 Changes for internal/api/cluster.go: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -37,6 +37,12 @@ type ServerInfo struct { // LegacyForwarding mirrors the CR's forwarding=legacy label: the proxy // forwards this server's players in the handshake address (#15). LegacyForwarding bool `json:"legacyForwarding,omitempty"` // AutoRestarts is how often the operator has recreated the pod of this start // after it timed out; StartGaveUp is true once no automatic retry is coming // (v1alpha1.StartGaveUp). A Failed server without StartGaveUp is still in its // restart backoff and may yet come up on its own. AutoRestarts int32 `json:"autoRestarts,omitempty"` StartGaveUp bool `json:"startGaveUp,omitempty"` } // CreateServerInput is the validated, structured create-server form (spec §15). Loading internal/api/k8scluster.go +2 −0 Changes for internal/api/k8scluster.go: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -447,6 +447,8 @@ func serverInfo(ms *v1alpha1.MinecraftServer) *ServerInfo { PlayerCountUnknown: ms.Status.Phase == v1alpha1.PhaseRunning && meta.IsStatusConditionFalse(ms.Status.Conditions, v1alpha1.ConditionPlayersCounted), LegacyForwarding: ms.Labels[v1alpha1.LabelForwarding] == v1alpha1.ForwardingLegacy, AutoRestarts: ms.Status.AutoRestarts, StartGaveUp: v1alpha1.StartGaveUp(&ms.Status), } } Loading internal/api/k8scluster_test.go +30 −0 Changes for internal/api/k8scluster_test.go: 30 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -290,3 +290,33 @@ func TestServerListCarriesLegacyForwarding(t *testing.T) { } } } // A Failed server reaches the proxy and the panel with whether the operator will // still retry it: the proxy keeps a waiting player through the restart backoff and // lets them go once the retries are spent. func TestServerInfoCarriesStartGaveUp(t *testing.T) { failed := func(name string, restarts int32) *v1alpha1.MinecraftServer { ms := testServer(name, name) now := metav1.Now() ms.Status = v1alpha1.MinecraftServerStatus{ Phase: v1alpha1.PhaseFailed, AutoRestarts: restarts, StartRequestedAt: &now, Conditions: []metav1.Condition{{Type: v1alpha1.ConditionReady, Status: metav1.ConditionFalse, Reason: v1alpha1.ReasonStartupTimeout}}, } return ms } retrying := serverInfo(failed("retrying", 1)) if retrying.StartGaveUp || retrying.AutoRestarts != 1 { t.Fatalf("retrying: startGaveUp=%v autoRestarts=%d, want false 1", retrying.StartGaveUp, retrying.AutoRestarts) } spent := serverInfo(failed("spent", v1alpha1.MaxAutoRestarts)) b, err := json.Marshal(spent) if err != nil { t.Fatal(err) } if !spent.StartGaveUp || !strings.Contains(string(b), `"startGaveUp":true`) { t.Fatalf("spent: startGaveUp=%v JSON %s, want true", spent.StartGaveUp, b) } } internal/apis/felis/v1alpha1/minecraftserver_types.go +27 −1 Changes for internal/apis/felis/v1alpha1/minecraftserver_types.go: 27 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) Loading Loading @@ -91,6 +92,31 @@ const ( ConditionPlayersCounted = "PlayersCounted" ) // Ready-condition reasons of a start that timed out: the pod never passed its TCP // readiness, or RCON never answered. Both are retried by recreating the pod, at // most MaxAutoRestarts times with a doubling backoff. const ( ReasonStartupTimeout = "StartupTimeout" ReasonReadinessTimeout = "ReadinessTimeout" ) // MaxAutoRestarts bounds how often the operator retries a timed-out start. const MaxAutoRestarts = 3 // StartGaveUp reports a Failed server that no automatic retry will bring up: its // start timed out with the retries spent, or it failed for a reason the operator // never retries (an invalid spec). Only a person moves it on. A Failed server still // inside its restart backoff has not given up: the operator recreates its pod when // the backoff runs out, and whoever waits on it should keep waiting. func StartGaveUp(s *MinecraftServerStatus) bool { if s.Phase != PhaseFailed { return false } c := meta.FindStatusCondition(s.Conditions, ConditionReady) timedOut := c != nil && (c.Reason == ReasonStartupTimeout || c.Reason == ReasonReadinessTimeout) return !timedOut || s.AutoRestarts >= MaxAutoRestarts || s.StartRequestedAt == nil } // +kubebuilder:object:root=true // +kubebuilder:subresource:status Loading Loading @@ -310,7 +336,7 @@ type MinecraftServerStatus struct { // the server becomes unoccupied. EmptySince *metav1.Time `json:"emptySince,omitempty"` // AutoRestarts counts how often the operator recreated the pod of a start // that timed out (at most 3, with a doubling backoff); reaching Ready or // that timed out (at most MaxAutoRestarts, with a doubling backoff); reaching Ready or // stopping resets it. AutoRestarts int32 `json:"autoRestarts,omitempty"` // LastAutoRestartAt is when the operator last recreated the pod. Loading Loading
docs/openapi.yaml +10 −0 Changes for docs/openapi.yaml: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -488,6 +488,16 @@ components: Present and true when the CR carries the label felis.lolicon.best/forwarding=legacy. The proxy then forwards this server's players BungeeCord-style in the handshake address instead of modern forwarding (Felis-Legacy Velocity fork only). autoRestarts: type: integer format: int32 description: Present when non-zero; how often the operator recreated the pod of this start after it timed out (at most 3). startGaveUp: type: boolean description: >- Present and true for a Failed server no automatic retry will bring up: its start timed out with the retries spent, or its spec is invalid. A Failed server without it is still in its restart backoff and may come up on its own. FleetServer: description: One row of the fleet-wide admin read (internal/api/handlers_user.go fleetServerView). Loading
internal/api/cluster.go +6 −0 Changes for internal/api/cluster.go: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -37,6 +37,12 @@ type ServerInfo struct { // LegacyForwarding mirrors the CR's forwarding=legacy label: the proxy // forwards this server's players in the handshake address (#15). LegacyForwarding bool `json:"legacyForwarding,omitempty"` // AutoRestarts is how often the operator has recreated the pod of this start // after it timed out; StartGaveUp is true once no automatic retry is coming // (v1alpha1.StartGaveUp). A Failed server without StartGaveUp is still in its // restart backoff and may yet come up on its own. AutoRestarts int32 `json:"autoRestarts,omitempty"` StartGaveUp bool `json:"startGaveUp,omitempty"` } // CreateServerInput is the validated, structured create-server form (spec §15). Loading
internal/api/k8scluster.go +2 −0 Changes for internal/api/k8scluster.go: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -447,6 +447,8 @@ func serverInfo(ms *v1alpha1.MinecraftServer) *ServerInfo { PlayerCountUnknown: ms.Status.Phase == v1alpha1.PhaseRunning && meta.IsStatusConditionFalse(ms.Status.Conditions, v1alpha1.ConditionPlayersCounted), LegacyForwarding: ms.Labels[v1alpha1.LabelForwarding] == v1alpha1.ForwardingLegacy, AutoRestarts: ms.Status.AutoRestarts, StartGaveUp: v1alpha1.StartGaveUp(&ms.Status), } } Loading
internal/api/k8scluster_test.go +30 −0 Changes for internal/api/k8scluster_test.go: 30 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -290,3 +290,33 @@ func TestServerListCarriesLegacyForwarding(t *testing.T) { } } } // A Failed server reaches the proxy and the panel with whether the operator will // still retry it: the proxy keeps a waiting player through the restart backoff and // lets them go once the retries are spent. func TestServerInfoCarriesStartGaveUp(t *testing.T) { failed := func(name string, restarts int32) *v1alpha1.MinecraftServer { ms := testServer(name, name) now := metav1.Now() ms.Status = v1alpha1.MinecraftServerStatus{ Phase: v1alpha1.PhaseFailed, AutoRestarts: restarts, StartRequestedAt: &now, Conditions: []metav1.Condition{{Type: v1alpha1.ConditionReady, Status: metav1.ConditionFalse, Reason: v1alpha1.ReasonStartupTimeout}}, } return ms } retrying := serverInfo(failed("retrying", 1)) if retrying.StartGaveUp || retrying.AutoRestarts != 1 { t.Fatalf("retrying: startGaveUp=%v autoRestarts=%d, want false 1", retrying.StartGaveUp, retrying.AutoRestarts) } spent := serverInfo(failed("spent", v1alpha1.MaxAutoRestarts)) b, err := json.Marshal(spent) if err != nil { t.Fatal(err) } if !spent.StartGaveUp || !strings.Contains(string(b), `"startGaveUp":true`) { t.Fatalf("spent: startGaveUp=%v JSON %s, want true", spent.StartGaveUp, b) } }
internal/apis/felis/v1alpha1/minecraftserver_types.go +27 −1 Changes for internal/apis/felis/v1alpha1/minecraftserver_types.go: 27 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) Loading Loading @@ -91,6 +92,31 @@ const ( ConditionPlayersCounted = "PlayersCounted" ) // Ready-condition reasons of a start that timed out: the pod never passed its TCP // readiness, or RCON never answered. Both are retried by recreating the pod, at // most MaxAutoRestarts times with a doubling backoff. const ( ReasonStartupTimeout = "StartupTimeout" ReasonReadinessTimeout = "ReadinessTimeout" ) // MaxAutoRestarts bounds how often the operator retries a timed-out start. const MaxAutoRestarts = 3 // StartGaveUp reports a Failed server that no automatic retry will bring up: its // start timed out with the retries spent, or it failed for a reason the operator // never retries (an invalid spec). Only a person moves it on. A Failed server still // inside its restart backoff has not given up: the operator recreates its pod when // the backoff runs out, and whoever waits on it should keep waiting. func StartGaveUp(s *MinecraftServerStatus) bool { if s.Phase != PhaseFailed { return false } c := meta.FindStatusCondition(s.Conditions, ConditionReady) timedOut := c != nil && (c.Reason == ReasonStartupTimeout || c.Reason == ReasonReadinessTimeout) return !timedOut || s.AutoRestarts >= MaxAutoRestarts || s.StartRequestedAt == nil } // +kubebuilder:object:root=true // +kubebuilder:subresource:status Loading Loading @@ -310,7 +336,7 @@ type MinecraftServerStatus struct { // the server becomes unoccupied. EmptySince *metav1.Time `json:"emptySince,omitempty"` // AutoRestarts counts how often the operator recreated the pod of a start // that timed out (at most 3, with a doubling backoff); reaching Ready or // that timed out (at most MaxAutoRestarts, with a doubling backoff); reaching Ready or // stopping resets it. AutoRestarts int32 `json:"autoRestarts,omitempty"` // LastAutoRestartAt is when the operator last recreated the pod. Loading