package controller import ( "context" "errors" "fmt" "net/http" "strings" "time" terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1" "git.ryuvia.com/niklas/terdut-operator/internal/tdclient" ) // reconcileEscalation applies spec.escalation as the team's whole ladder, or // clears it when the field is absent (an empty ladder is the server's "none"). // The second return is an expected, reportable condition (a bad duration, an // unknown user), the third a failure to retry. func (r *TerdutTeamReconciler) reconcileEscalation( ctx context.Context, tc *tdclient.Client, team *terdutv1alpha1.TerdutTeam, ) (*teamError, error) { body, err := buildEscalationRequest(team.Spec.Escalation) if err != nil { return &teamError{reason: terdutv1alpha1.ReasonInvalidSpec, message: err.Error()}, nil } if err := tc.SetEscalation(ctx, team.Status.TeamID, body); err != nil { // The server resolves usernames; one it does not know is a state to // wait out (the person may be created later), not a failure. if se, ok := errors.AsType[*tdclient.StatusError](err); ok && se.Code == http.StatusBadRequest && strings.HasPrefix(se.Message, "unknown user") { return &teamError{reason: terdutv1alpha1.ReasonUnknownUser, message: se.Message}, nil } return nil, fmt.Errorf("PUT /api/teams/%d/escalation: %w", team.Status.TeamID, err) } return nil, nil } func buildEscalationRequest(spec *terdutv1alpha1.EscalationSpec) (tdclient.SetEscalationRequest, error) { if spec == nil { return tdclient.SetEscalationRequest{Levels: []tdclient.EscalationLevelRequest{}}, nil } levels := make([]tdclient.EscalationLevelRequest, len(spec.Levels)) for i, lvl := range spec.Levels { timeout, err := time.ParseDuration(lvl.Timeout) if err != nil || timeout <= 0 { return tdclient.SetEscalationRequest{}, fmt.Errorf("spec.escalation.levels[%d].timeout %q is not a positive duration", i, lvl.Timeout) } targets := make([]tdclient.EscalationTargetRequest, len(lvl.Targets)) for j, t := range lvl.Targets { targets[j] = tdclient.EscalationTargetRequest{Kind: string(t.Kind), Username: t.Username} } levels[i] = tdclient.EscalationLevelRequest{ Position: int64(i + 1), TimeoutSeconds: int64(timeout.Seconds()), Targets: targets, } } return tdclient.SetEscalationRequest{ RepeatCount: spec.RepeatCount, FallbackTopic: spec.FallbackTopic, Levels: levels, }, nil } // reconcileDeadmanSwitches makes the team's switches on the server exactly // spec.deadmanSwitches, matched by name (unique per team server-side): create // what is missing, update what differs, delete what is not listed. In operator // mode nobody else can add one, so anything extra is leftover to remove. func (r *TerdutTeamReconciler) reconcileDeadmanSwitches( ctx context.Context, tc *tdclient.Client, team *terdutv1alpha1.TerdutTeam, ) (*teamError, error) { type want struct { spec terdutv1alpha1.DeadmanSwitchSpec seconds int64 } desired := make(map[string]want, len(team.Spec.DeadmanSwitches)) for _, sw := range team.Spec.DeadmanSwitches { timeout, err := time.ParseDuration(sw.Timeout) if err != nil || timeout <= 0 { return &teamError{ reason: terdutv1alpha1.ReasonInvalidSpec, message: fmt.Sprintf("spec.deadmanSwitches[%q].timeout %q is not a positive duration", sw.Name, sw.Timeout), }, nil } desired[sw.Name] = want{spec: sw, seconds: int64(timeout.Seconds())} } existing, err := tc.ListDeadmanSwitches(ctx, team.Status.TeamID) if err != nil { return nil, fmt.Errorf("GET /api/teams/%d/deadman/switches: %w", team.Status.TeamID, err) } seen := make(map[string]bool, len(existing)) for _, have := range existing { w, keep := desired[have.Name] if !keep { if err := tc.DeleteDeadmanSwitch(ctx, team.Status.TeamID, have.ID); err != nil { return nil, fmt.Errorf("DELETE deadman switch %q: %w", have.Name, err) } continue } seen[have.Name] = true severity := severityOrDefault(w.spec.Severity) if have.Matcher == w.spec.Matcher && have.TimeoutSeconds == w.seconds && have.Severity == severity { continue } if err := tc.UpdateDeadmanSwitch(ctx, team.Status.TeamID, have.ID, w.spec.Name, w.spec.Matcher, w.seconds, severity); err != nil { return nil, fmt.Errorf("PUT deadman switch %q: %w", have.Name, err) } } for _, sw := range team.Spec.DeadmanSwitches { if seen[sw.Name] { continue } w := desired[sw.Name] if _, err := tc.CreateDeadmanSwitch(ctx, team.Status.TeamID, sw.Name, sw.Matcher, w.seconds, severityOrDefault(sw.Severity)); err != nil { return nil, fmt.Errorf("POST deadman switch %q: %w", sw.Name, err) } } return nil, nil } func severityOrDefault(s string) string { if s == "" { return "critical" } return s }