diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 390021f..afd71eb 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: container: image: golang:1.23-alpine steps: + - run: apk add --no-cache nodejs - uses: actions/checkout@v4 - run: gofmt -l . | tee /tmp/fmt.out && test ! -s /tmp/fmt.out - run: go vet ./... diff --git a/Dockerfile b/Dockerfile index 95a1ab4..ee82207 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ WORKDIR /src COPY go.mod ./ COPY cmd/ ./cmd/ 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 USER 65532:65532 EXPOSE 8080 diff --git a/bridge b/bridge index b5622ec..71b2bba 100755 Binary files a/bridge and b/bridge differ diff --git a/cmd/bridge/main.go b/cmd/bridge/main.go index ba768a6..2906077 100644 --- a/cmd/bridge/main.go +++ b/cmd/bridge/main.go @@ -131,14 +131,18 @@ func (g *glpiClient) openTicketFor(monitor string) (int, error) { q.Set("criteria[1][field]", "12") // status q.Set("criteria[1][searchtype]", "equals") q.Set("criteria[1][value]", "notold") - var out []struct { - ID int `json:"2"` + var out struct { + Data []map[string]any `json:"data"` } if err := g.do("GET", "/search/Ticket?"+q.Encode(), nil, &out); err != nil { return 0, err } - if len(out) > 0 { - return out[0].ID, nil + for _, row := range out.Data { + if v, ok := row["2"]; ok { + var tid int + fmt.Sscanf(fmt.Sprint(v), "%d", &tid) + return tid, 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 { - return g.do("POST", "/followup", map[string]any{ - "input": map[string]any{"itemtype": "Ticket", "items_id": ticket, "content": content}, + // 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{ + "input": map[string]any{"itemtype": "Ticket", "items_id": ticket, "content": content}, + }, nil) + } + return nil } type bridge struct { @@ -210,8 +222,7 @@ func (b *bridge) handle(w http.ResponseWriter, r *http.Request) { 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) if err := b.glpi.followup(tid, fu); err != nil { - log.Printf("followup failed: %v", err) - return + log.Printf("followup failed (continuing to solve): %v", err) } 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)