Compare commits

..

1 Commits

Author SHA1 Message Date
Niklas Ye 8b9adb4910 Let the instance service account list a team's deadman switches
CI / chart (push) Successful in 1s
CI / security (push) Successful in 34s
CI / test (push) Successful in 6m26s
Release / chart (push) Successful in 2s
Release / binaries (push) Successful in 15s
Release / scan-image (push) Successful in 6s
Release / test (push) Successful in 12s
Release / image (push) Successful in 1m8s
GET /api/teams/{id}/deadman/switches used requireTeamMember, which 404s
for the instance-scoped service account because it belongs to no team.
terdut-operator lists switches by name on every reconcile, so TerdutTeams
never became ready. Instance service accounts now pass, as they already do
for the write endpoints.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-20 15:34:31 +02:00
3 changed files with 34 additions and 3 deletions
+2 -2
View File
@@ -15,5 +15,5 @@ type: application
# appVersion and image.tag in values.yaml no longer agree, and that is not an oversight:
# image.tag stays "latest", which is what a local install actually pulls. appVersion is
# metadata and drives nothing.
version: 0.47.1
appVersion: "v0.47.1"
version: 0.47.2
appVersion: "v0.47.2"
+28
View File
@@ -490,3 +490,31 @@ func TestDeadman_UpdateInPlacePreservesID(t *testing.T) {
t.Errorf("expected the update to replace in place, not add a row, got %d switches", len(list))
}
}
// The operator reconciles a team's dead man's switches by listing them, and its
// instance-scoped account is not a member of any team it manages.
func TestServiceAccount_InstanceScopeListsATeamsDeadmanSwitches(t *testing.T) {
s := newTS(t)
instanceKey := createServiceAccount(t, s, s.key, "terdut-operator", models.ServiceAccountScopeInstance, 0)
team := createTeamAs(t, s, instanceKey, "team-a")
path := "/api/teams/" + id64(team) + "/deadman/switches"
resp := s.reqAs(t, instanceKey, http.MethodPost, path,
map[string]any{"name": "watchdog", "matcher": "alertname=Watchdog", "timeout_seconds": 900, "severity": "critical"})
if resp.StatusCode != http.StatusCreated {
t.Fatalf("create switch: %d", resp.StatusCode)
}
resp.Body.Close()
resp = s.reqAs(t, instanceKey, http.MethodGet, path, nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("list switches as the instance account: %d", resp.StatusCode)
}
var list []struct {
Name string `json:"name"`
}
decode(t, resp, &list)
if len(list) != 1 || list[0].Name != "watchdog" {
t.Errorf("expected the one switch just created, got %+v", list)
}
}
+4 -1
View File
@@ -826,7 +826,10 @@ func handleListTeamDeadman(db *sql.DB) http.HandlerFunc {
if !ok {
return
}
if !requireTeamMember(w, r, teamID) {
// The operator's instance-scoped account reconciles the switches by name,
// so it has to read them without being a member: that is owner reach,
// the same as the writes below, and still reads no incidents.
if !isInstanceServiceAccount(r.Context()) && !requireTeamMember(w, r, teamID) {
return
}