fix(panel): streamline system settings and server creation
Use shared action buttons and default to the login space. Allow staff to save startup-only experience settings while running and request a durable restart through the existing operator flow. Grant the API PVC list permission needed to detect retained worlds before server creation, and show internal failures with a request ID. Cover permission, maintenance, restart and UI behavior with regression checks.
This commit is contained in:
34 files changed
+555
-103
No files matched your search
@@ -218,9 +218,11 @@ func (r *Reconciler) reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
||||
desired = v1alpha1.DesiredStopped
|
||||
}
|
||||
prevPhase, prevRestarts := server.Status.Phase, server.Status.AutoRestarts
|
||||
if _, ok := server.Annotations[v1alpha1.AnnotationStartRetry]; ok {
|
||||
if err := r.takeStartRetry(ctx, &server, desired); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
for _, annotation := range []string{v1alpha1.AnnotationStartRetry, v1alpha1.AnnotationRestart} {
|
||||
if _, ok := server.Annotations[annotation]; ok {
|
||||
if err := r.takeRestartRequest(ctx, &server, desired, annotation); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
var res ctrl.Result
|
||||
@@ -263,30 +265,34 @@ func (r *Reconciler) recordTransition(ctx context.Context, server *v1alpha1.Mine
|
||||
r.event(server, eventType, reason, fmt.Sprintf("%s → %s: %s", from, phase, msg))
|
||||
}
|
||||
|
||||
// takeStartRetry answers felis-api's AnnotationStartRetry: a server still Failed
|
||||
// and meant to run starts over as if freshly woken — pod recreated, restart
|
||||
// budget back to zero, a new start anchor — and in any other state the request
|
||||
// is stale and only removed. The fresh status is written before the request is
|
||||
// takeRestartRequest recreates a Failed pod for a start retry, or a Running pod
|
||||
// for an explicit restart. A request overtaken by a stop is only removed.
|
||||
// The fresh status is written before the request is
|
||||
// removed, so a pass that fails in between leaves the request to be taken again
|
||||
// (at the cost of one more pod recreate), never a removed request with the old
|
||||
// spent budget still in place.
|
||||
func (r *Reconciler) takeStartRetry(ctx context.Context, server *v1alpha1.MinecraftServer, desired v1alpha1.DesiredState) error {
|
||||
if desired == v1alpha1.DesiredRunning && server.Status.Phase == v1alpha1.PhaseFailed {
|
||||
func (r *Reconciler) takeRestartRequest(ctx context.Context, server *v1alpha1.MinecraftServer, desired v1alpha1.DesiredState, annotation string) error {
|
||||
explicit := annotation == v1alpha1.AnnotationRestart
|
||||
if desired == v1alpha1.DesiredRunning && (server.Status.Phase == v1alpha1.PhaseFailed || (explicit && server.Status.Phase == v1alpha1.PhaseRunning)) {
|
||||
pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: server.Name + "-0", Namespace: server.Namespace}}
|
||||
if err := r.Delete(ctx, pod); client.IgnoreNotFound(err) != nil {
|
||||
return err
|
||||
}
|
||||
server.Status.AutoRestarts = 0
|
||||
server.Status.StartRequestedAt = nil
|
||||
r.markStarting(server, "StartRetried", "start requested again after it failed; recreated the pod")
|
||||
reason, message := "StartRetried", "start requested again after it failed; recreated the pod"
|
||||
if explicit {
|
||||
reason, message = "RestartRequested", "restart requested; recreated the pod"
|
||||
}
|
||||
r.markStarting(server, reason, message)
|
||||
if err := r.patchStatus(ctx, server); err != nil {
|
||||
return err
|
||||
}
|
||||
log.FromContext(ctx).Info("start retried")
|
||||
r.event(server, corev1.EventTypeNormal, "StartRetried", "start requested again after it failed; recreated the pod")
|
||||
log.FromContext(ctx).Info(message)
|
||||
r.event(server, corev1.EventTypeNormal, reason, message)
|
||||
}
|
||||
patch := client.MergeFrom(server.DeepCopy())
|
||||
delete(server.Annotations, v1alpha1.AnnotationStartRetry)
|
||||
delete(server.Annotations, annotation)
|
||||
return r.Patch(ctx, server, patch)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,38 @@ func podPresent(t *testing.T, c client.Client) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func TestExplicitRestartIsConsumedOnce(t *testing.T) {
|
||||
srv := runningServer()
|
||||
srv.Status.Phase = v1alpha1.PhaseRunning
|
||||
srv.Annotations = map[string]string{v1alpha1.AnnotationRestart: "2026-07-01T12:00:00Z"}
|
||||
r, c := newReconciler(t, fakeProber{}, srv, rconSecret(), gamePod())
|
||||
reconcile(t, r, "survival")
|
||||
s := getServer(t, c, "survival")
|
||||
if s.Annotations[v1alpha1.AnnotationRestart] != "" || s.Spec.DesiredState != v1alpha1.DesiredRunning || s.Status.Phase != v1alpha1.PhaseStarting || podPresent(t, c) {
|
||||
t.Fatalf("restart: desired=%s phase=%s request=%s pod=%v", s.Spec.DesiredState, s.Status.Phase, s.Annotations[v1alpha1.AnnotationRestart], podPresent(t, c))
|
||||
}
|
||||
if err := c.Create(context.Background(), gamePod()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reconcile(t, r, "survival")
|
||||
if !podPresent(t, c) {
|
||||
t.Fatal("consumed request deleted the replacement pod")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopSupersedesExplicitRestart(t *testing.T) {
|
||||
srv := runningServer()
|
||||
srv.Spec.DesiredState = v1alpha1.DesiredStopped
|
||||
srv.Status.Phase = v1alpha1.PhaseRunning
|
||||
srv.Annotations = map[string]string{v1alpha1.AnnotationRestart: "2026-07-01T12:00:00Z"}
|
||||
r, c := newReconciler(t, fakeProber{}, srv, rconSecret(), gamePod())
|
||||
reconcile(t, r, "survival")
|
||||
s := getServer(t, c, "survival")
|
||||
if s.Annotations[v1alpha1.AnnotationRestart] != "" || s.Spec.DesiredState != v1alpha1.DesiredStopped || s.Status.Phase == v1alpha1.PhaseStarting {
|
||||
t.Fatalf("restart overrode stop: desired=%s phase=%s annotations=%v", s.Spec.DesiredState, s.Status.Phase, s.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
// Once the automatic restarts are spent, a retry request starts the server over
|
||||
// with the whole budget back: the pod is recreated, the start re-anchored, and
|
||||
// the next timeout is retried automatically again.
|
||||
|
||||
Reference in new issue
Block a user