Author SHA1 Message Date
ic-builder de9b9c4a35 UP events solve open tickets even after bridge restarts
ci / vet (pull_request) Successful in 47s
In-memory dedupe resets on pod restart; recovery then no-ops and leaves
stale open tickets. UP now falls back to a GLPI search for an unsolved
ticket for that monitor.
Ticket: https://projects.knownelement.com/issues/824
2026-09-06 13:09:34 -05:00
ic-builder bfd05f0064 CI: install nodejs before checkout; recovery followup via ITILFollowup
ci / vet (pull_request) Successful in 1m42s
golang:alpine lacks node (actions/checkout 127). GLPI 11 rejects the
legacy /followup input shape -> item-endpoint first, legacy fallback;
solve no longer skipped when followup fails.
Ticket: https://projects.knownelement.com/issues/824
2026-09-06 13:00:55 -05:00
4 changed files with 27 additions and 10 deletions
+1
View File
@@ -11,6 +11,7 @@ jobs:
container: container:
image: golang:1.23-alpine image: golang:1.23-alpine
steps: steps:
- run: apk add --no-cache nodejs
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: gofmt -l . | tee /tmp/fmt.out && test ! -s /tmp/fmt.out - run: gofmt -l . | tee /tmp/fmt.out && test ! -s /tmp/fmt.out
- run: go vet ./... - run: go vet ./...
+2 -1
View File
@@ -3,7 +3,8 @@ WORKDIR /src
COPY go.mod ./ COPY go.mod ./
COPY cmd/ ./cmd/ COPY cmd/ ./cmd/
RUN go build -ldflags="-s -w" -o /out/bridge ./cmd/bridge RUN go build -ldflags="-s -w" -o /out/bridge ./cmd/bridge
FROM scratch FROM alpine:3.20
RUN apk add --no-cache ca-certificates && adduser -D -u 65532 app
COPY --from=build /out/bridge /bridge COPY --from=build /out/bridge /bridge
USER 65532:65532 USER 65532:65532
EXPOSE 8080 EXPOSE 8080
BIN
View File
Binary file not shown.
+21 -6
View File
@@ -131,14 +131,18 @@ func (g *glpiClient) openTicketFor(monitor string) (int, error) {
q.Set("criteria[1][field]", "12") // status q.Set("criteria[1][field]", "12") // status
q.Set("criteria[1][searchtype]", "equals") q.Set("criteria[1][searchtype]", "equals")
q.Set("criteria[1][value]", "notold") q.Set("criteria[1][value]", "notold")
var out []struct { var out struct {
ID int `json:"2"` Data []map[string]any `json:"data"`
} }
if err := g.do("GET", "/search/Ticket?"+q.Encode(), nil, &out); err != nil { if err := g.do("GET", "/search/Ticket?"+q.Encode(), nil, &out); err != nil {
return 0, err return 0, err
} }
if len(out) > 0 { for _, row := range out.Data {
return out[0].ID, nil if v, ok := row["2"]; ok {
var tid int
fmt.Sscanf(fmt.Sprint(v), "%d", &tid)
return tid, nil
}
} }
return 0, nil return 0, nil
} }
@@ -154,9 +158,17 @@ func (g *glpiClient) createTicket(name, content string) (int, error) {
} }
func (g *glpiClient) followup(ticket int, content string) error { func (g *glpiClient) followup(ticket int, content string) error {
// GLPI 11: followups live under the item endpoint; fall back to the
// legacy path when the item endpoint rejects the payload.
err := g.do("POST", fmt.Sprintf("/Ticket/%d/ITILFollowup", ticket), map[string]any{
"input": map[string]any{"content": content},
}, nil)
if err != nil {
return g.do("POST", "/followup", map[string]any{ return g.do("POST", "/followup", map[string]any{
"input": map[string]any{"itemtype": "Ticket", "items_id": ticket, "content": content}, "input": map[string]any{"itemtype": "Ticket", "items_id": ticket, "content": content},
}, nil) }, nil)
}
return nil
} }
type bridge struct { type bridge struct {
@@ -204,14 +216,17 @@ func (b *bridge) handle(w http.ResponseWriter, r *http.Request) {
case 1: // UP case 1: // UP
tid, exists := b.open[k.Monitor.Name] tid, exists := b.open[k.Monitor.Name]
if !exists { if !exists {
// restarts wipe the in-memory map; search GLPI for an open ticket
tid, _ = b.glpi.openTicketFor(k.Monitor.Name)
if tid == 0 {
return return
} }
}
delete(b.open, k.Monitor.Name) delete(b.open, k.Monitor.Name)
go func() { go func() {
fu := fmt.Sprintf("RECOVERY: monitor %s back UP at %s. Ticket auto-closing as solved with recovery evidence.", k.Monitor.Name, k.Heartbeat.Time) fu := fmt.Sprintf("RECOVERY: monitor %s back UP at %s. Ticket auto-closing as solved with recovery evidence.", k.Monitor.Name, k.Heartbeat.Time)
if err := b.glpi.followup(tid, fu); err != nil { if err := b.glpi.followup(tid, fu); err != nil {
log.Printf("followup failed: %v", err) log.Printf("followup failed (continuing to solve): %v", err)
return
} }
if err := b.glpi.do("POST", fmt.Sprintf("/Ticket/%d", tid), map[string]any{"input": map[string]any{"status": 11}}, nil); err != nil { // 11 = solved if err := b.glpi.do("POST", fmt.Sprintf("/Ticket/%d", tid), map[string]any{"input": map[string]any{"status": 11}}, nil); err != nil { // 11 = solved
log.Printf("solve failed: %v", err) log.Printf("solve failed: %v", err)