feat(converge): 报告缺 RCON 的老用户服务器,-user-rcon 按新建时的配置补上
This commit is contained in:
3 files changed
+135
-1
No files matched your search
+53
-1
@@ -11,12 +11,14 @@ import (
|
|||||||
|
|
||||||
"felis.lolicon.best/internal/apis/felis/v1alpha1"
|
"felis.lolicon.best/internal/apis/felis/v1alpha1"
|
||||||
"felis.lolicon.best/internal/config"
|
"felis.lolicon.best/internal/config"
|
||||||
|
"felis.lolicon.best/internal/naming"
|
||||||
"felis.lolicon.best/internal/platform"
|
"felis.lolicon.best/internal/platform"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdConverge is the explicit convergence pass over already-installed system
|
// cmdConverge is the explicit convergence pass over already-installed system
|
||||||
// servers (#1), plus the idle-stop default for user servers that predate it. Provisioning is create-if-absent, so a field the desired spec
|
// servers (#1), plus the idle-stop default for user servers that predate it, and
|
||||||
|
// with -user-rcon their RCON block (#3). Provisioning is create-if-absent, so a field the desired spec
|
||||||
// gained after an install (spec.rcon, spec.startup.healthHTTPPort, a derived env
|
// gained after an install (spec.rcon, spec.startup.healthHTTPPort, a derived env
|
||||||
// key) never reaches the existing CR — and nothing says so. This command fills
|
// key) never reaches the existing CR — and nothing says so. This command fills
|
||||||
// exactly those zero-value fields; see convergeSystemServers for the full contract
|
// exactly those zero-value fields; see convergeSystemServers for the full contract
|
||||||
@@ -29,6 +31,7 @@ func cmdConverge(args []string, stdout, stderr io.Writer) int {
|
|||||||
fs := flag.NewFlagSet("converge", flag.ContinueOnError)
|
fs := flag.NewFlagSet("converge", flag.ContinueOnError)
|
||||||
fs.SetOutput(stderr)
|
fs.SetOutput(stderr)
|
||||||
cfgPath := fs.String("config", defaultSetupConfigPath, "path to felis.toml")
|
cfgPath := fs.String("config", defaultSetupConfigPath, "path to felis.toml")
|
||||||
|
userRcon := fs.Bool("user-rcon", false, "also turn RCON on for user servers created before it was the default")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
if errors.Is(err, flag.ErrHelp) {
|
if errors.Is(err, flag.ErrHelp) {
|
||||||
return 0
|
return 0
|
||||||
@@ -59,6 +62,7 @@ func cmdConverge(args []string, stdout, stderr io.Writer) int {
|
|||||||
defaultPanelHostname(cfg.Server.RootDomain, cfg.Auth.PanelHostname))
|
defaultPanelHostname(cfg.Server.RootDomain, cfg.Auth.PanelHostname))
|
||||||
|
|
||||||
outcomes = append(outcomes, convergeUserServerIdle(context.Background(), cl, cfg.K8s.Namespace)...)
|
outcomes = append(outcomes, convergeUserServerIdle(context.Background(), cl, cfg.K8s.Namespace)...)
|
||||||
|
outcomes = append(outcomes, convergeUserServerRcon(context.Background(), cl, cfg.K8s.Namespace, *userRcon)...)
|
||||||
|
|
||||||
fmt.Fprintln(stdout, "felis converge: filling fields an installed server predates (operator-set values are never overwritten):")
|
fmt.Fprintln(stdout, "felis converge: filling fields an installed server predates (operator-set values are never overwritten):")
|
||||||
exit := 0
|
exit := 0
|
||||||
@@ -113,3 +117,51 @@ func convergeUserServerIdle(ctx context.Context, cl client.Client, namespace str
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convergeUserServerRcon handles user servers created before RCON was part of every
|
||||||
|
// new server (694e3cb): spec.rcon entirely unset. Such a server has a dead console,
|
||||||
|
// reports nobody online, and never idles out, because all three ride RCON.
|
||||||
|
//
|
||||||
|
// Only with fill does it turn RCON on, with the same block CreateServer writes
|
||||||
|
// today; without it each such server gets a line saying so. The fill is opt-in
|
||||||
|
// because the operator gates readiness on the RCON probe: a server whose image does
|
||||||
|
// not open the listener RCON_PASSWORD asks for would sit in Starting until it is
|
||||||
|
// marked Failed. Felis's own paper and lobby images open it; an image a user brought
|
||||||
|
// may not, and only the operator running this can tell. A server that already
|
||||||
|
// carries any RCON setting (on or off) is left alone and produces no line.
|
||||||
|
func convergeUserServerRcon(ctx context.Context, cl client.Client, namespace string, fill bool) []systemServerOutcome {
|
||||||
|
var list v1alpha1.MinecraftServerList
|
||||||
|
if err := cl.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
||||||
|
return []systemServerOutcome{{name: "user servers", err: fmt.Errorf("list servers: %w", err)}}
|
||||||
|
}
|
||||||
|
var out []systemServerOutcome
|
||||||
|
for i := range list.Items {
|
||||||
|
ms := &list.Items[i]
|
||||||
|
if ms.Labels[v1alpha1.LabelSystemRole] != "" || ms.Spec.Rcon != (v1alpha1.RconSpec{}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !fill {
|
||||||
|
out = append(out, systemServerOutcome{name: ms.Name, available: true,
|
||||||
|
skipped: "no RCON (console, online count and idle stop are off); once its image serves RCON, sudo felis converge -user-rcon turns it on"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changed, err := patchOnConflictRetry(ctx, cl, ms, func() bool {
|
||||||
|
if ms.Spec.Rcon != (v1alpha1.RconSpec{}) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ms.Spec.Rcon = v1alpha1.RconSpec{
|
||||||
|
Enabled: true,
|
||||||
|
SecretRef: v1alpha1.SecretKeyRef{Name: naming.RconSecretName(ms.Name), Key: naming.RconSecretKey},
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
out = append(out, systemServerOutcome{name: ms.Name, err: fmt.Errorf("converge %s: %w", ms.Name, err)})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
out = append(out, systemServerOutcome{name: ms.Name, available: true, updated: true, changes: []string{"spec.rcon"}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -229,3 +229,67 @@ func TestConvergeUserServerIdle(t *testing.T) {
|
|||||||
t.Fatalf("second pass = %+v, want nothing to do", again)
|
t.Fatalf("second pass = %+v, want nothing to do", again)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConvergeUserServerRcon reports a user server with no RCON block at all and
|
||||||
|
// fills it only when asked, with the block CreateServer writes. RCON turned off on
|
||||||
|
// purpose, a server with its own secret, and a system server stay as they are and
|
||||||
|
// produce no line.
|
||||||
|
func TestConvergeUserServerRcon(t *testing.T) {
|
||||||
|
scheme := newSystemServerScheme(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
mk := func(name string, rcon v1alpha1.RconSpec, role string) *v1alpha1.MinecraftServer {
|
||||||
|
ms := &v1alpha1.MinecraftServer{}
|
||||||
|
ms.Name, ms.Namespace = name, "minecraft"
|
||||||
|
ms.Spec.Rcon = rcon
|
||||||
|
if role != "" {
|
||||||
|
ms.Labels = map[string]string{v1alpha1.LabelSystemRole: role}
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
own := v1alpha1.RconSpec{Enabled: true, Port: 25580, SecretRef: v1alpha1.SecretKeyRef{Name: "own", Key: "pw"}}
|
||||||
|
off := v1alpha1.RconSpec{SecretRef: v1alpha1.SecretKeyRef{Name: "rcon-off", Key: naming.RconSecretKey}}
|
||||||
|
cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
|
||||||
|
mk("demo", v1alpha1.RconSpec{}, ""),
|
||||||
|
mk("off", off, ""),
|
||||||
|
mk("own", own, ""),
|
||||||
|
mk(naming.SystemLobbyServer, v1alpha1.RconSpec{}, naming.SystemLobbyServer),
|
||||||
|
).Build()
|
||||||
|
get := func(name string) v1alpha1.RconSpec {
|
||||||
|
var ms v1alpha1.MinecraftServer
|
||||||
|
if err := cl.Get(ctx, client.ObjectKey{Namespace: "minecraft", Name: name}, &ms); err != nil {
|
||||||
|
t.Fatalf("get %s: %v", name, err)
|
||||||
|
}
|
||||||
|
return ms.Spec.Rcon
|
||||||
|
}
|
||||||
|
|
||||||
|
report := convergeUserServerRcon(ctx, cl, "minecraft", false)
|
||||||
|
if len(report) != 1 || report[0].name != "demo" || report[0].updated || report[0].err != nil ||
|
||||||
|
!strings.Contains(report[0].skipped, "-user-rcon") {
|
||||||
|
t.Fatalf("report = %+v, want one skipped line for demo naming -user-rcon", report)
|
||||||
|
}
|
||||||
|
if got := get("demo"); got != (v1alpha1.RconSpec{}) {
|
||||||
|
t.Fatalf("the report-only pass wrote demo's rcon: %+v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
filled := convergeUserServerRcon(ctx, cl, "minecraft", true)
|
||||||
|
if len(filled) != 1 || filled[0].name != "demo" || !filled[0].updated || filled[0].err != nil {
|
||||||
|
t.Fatalf("fill = %+v, want exactly one update for demo", filled)
|
||||||
|
}
|
||||||
|
want := map[string]v1alpha1.RconSpec{
|
||||||
|
"demo": {Enabled: true, SecretRef: v1alpha1.SecretKeyRef{
|
||||||
|
Name: naming.RconSecretName("demo"), Key: naming.RconSecretKey}},
|
||||||
|
"off": off,
|
||||||
|
"own": own,
|
||||||
|
naming.SystemLobbyServer: {},
|
||||||
|
}
|
||||||
|
for name, rcon := range want {
|
||||||
|
if got := get(name); got != rcon {
|
||||||
|
t.Errorf("%s rcon = %+v, want %+v", name, got, rcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fill := range []bool{false, true} {
|
||||||
|
if again := convergeUserServerRcon(ctx, cl, "minecraft", fill); len(again) != 0 {
|
||||||
|
t.Fatalf("second pass (fill=%v) = %+v, want nothing to do", fill, again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1225,6 +1225,24 @@ empty. A server whose idle stop an admin turned off keeps a duration on its
|
|||||||
spec (`autoStopEnabled: false`, `emptySecondsBeforeStop` set), so converge
|
spec (`autoStopEnabled: false`, `emptySecondsBeforeStop` set), so converge
|
||||||
leaves it off; only servers it actually filled get a line.
|
leaves it off; only servers it actually filled get a line.
|
||||||
|
|
||||||
|
A **user** server created before every new server got RCON (`spec.rcon` wholly
|
||||||
|
unset) has a dead console, always shows 0 online, and never idles out, since all
|
||||||
|
three ride RCON. Plain `felis converge` lists each such server and changes
|
||||||
|
nothing; `-user-rcon` turns RCON on for them with the block a server created
|
||||||
|
today gets (the `<name>-rcon` Secret the operator provisions):
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo felis converge -user-rcon
|
||||||
|
kubectl -n minecraft get minecraftserver -o custom-columns=NAME:.metadata.name,RCON:.spec.rcon.enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
It is opt-in for the reason above: the new start waits on the RCON probe, and a
|
||||||
|
server whose image does not open the listener that `RCON_PASSWORD` asks for
|
||||||
|
stays in `Starting` until it is marked `Failed`. Felis's own paper and lobby
|
||||||
|
images open it; check a server running an image a user brought before filling
|
||||||
|
it. A server whose RCON someone set, on or off, is never touched. The change
|
||||||
|
applies at the server's next start.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. World PVC survives after I deleted the MinecraftServer
|
## 13. World PVC survives after I deleted the MinecraftServer
|
||||||
|
|||||||
Reference in new issue
Block a user