fix(operator): idle auto-stop couldn't write — patch the spec, and grant the patch
Two stacked blockers behind the frozen auto-stop, both found live after the first two fixes let the timer finally tick: - The stop used a whole-object Update while the same reconcile loop writes status; that risks clobbering a concurrent status write. Switch to the reaper's merge-patch pattern (spec.desiredState only; EmptySince is left for markStopped to clear). - The operator Role never carried minecraftservers:patch, so the call failed closed with 403 (visible in the operator log as 'cannot update resource "minecraftservers"'). Grant patch and pin it in the RBAC scope test. With all three layers fixed, the auto-stop path is: timer persists (schema), wake-up fires (requeue), spec write allowed (RBAC).
This commit is contained in:
3 files changed
+18
-5
No files matched your search
@@ -136,12 +136,15 @@ func APIBuildRole(p Params) *rbacv1.Role {
|
||||
// the manager's cache is namespace-scoped (see cmd/felis/operator.go), so a
|
||||
// namespaced Role is sufficient. The operator owns StatefulSets and Services
|
||||
// (Get/Create/Update — never patch or delete), writes only minecraftservers
|
||||
// status (Status().Update — `update` only), and reads RCON Secrets. It never
|
||||
// touches pods, PVCs, Events, or finalizers, so none appear here.
|
||||
// status (Status().Update — `update` only) and patches spec.desiredState to
|
||||
// Stopped for idle auto-stop (spec §8 — the one spec field it may write, using
|
||||
// the same merge patch as the reaper's Stop: without the grant the auto-stop
|
||||
// call fails closed with a 403), and reads RCON Secrets. It never touches pods,
|
||||
// PVCs, Events, or finalizers, so none appear here.
|
||||
func OperatorRole(p Params) *rbacv1.Role {
|
||||
p = p.withDefaults()
|
||||
return role(p.MinecraftNamespace, "felis-operator", ComponentOperator, []rbacv1.PolicyRule{
|
||||
rule([]string{groupFelis}, []string{"minecraftservers"}, []string{"get", "list", "watch"}),
|
||||
rule([]string{groupFelis}, []string{"minecraftservers"}, []string{"get", "list", "watch", "patch"}),
|
||||
rule([]string{groupFelis}, []string{"minecraftservers/status"}, []string{"update"}),
|
||||
rule([]string{groupApps}, []string{"statefulsets"}, []string{"get", "list", "watch", "create", "update"}),
|
||||
rule([]string{groupCore}, []string{"services"}, []string{"get", "list", "watch", "create", "update"}),
|
||||
|
||||
@@ -149,6 +149,11 @@ func TestOperatorRole_ScopeExact(t *testing.T) {
|
||||
t.Errorf("operator must have minecraftservers:%s (cached client)", v)
|
||||
}
|
||||
}
|
||||
// Idle auto-stop patches spec.desiredState=Stopped (spec §8). Found live:
|
||||
// without this grant the auto-stop fails closed with a 403.
|
||||
if !hasRule(op, groupFelis, "minecraftservers", "patch") {
|
||||
t.Error("operator must have minecraftservers:patch (idle auto-stop writes spec.desiredState)")
|
||||
}
|
||||
for _, v := range []string{"get", "list", "watch", "create", "update"} {
|
||||
if !hasRule(op, "apps", "statefulsets", v) {
|
||||
t.Errorf("operator must have statefulsets:%s", v)
|
||||
|
||||
Reference in new issue
Block a user