057302cb39
The incident view gets a "Seen before" section from the server's new /similar endpoint; an older server without it just shows nothing. C adds a note as the resolution note, alongside c for a plain note. Needs the server release that adds /similar. Claude-Session: https://claude.ai/code/session_01MMados3BD1oSjevHxbmVqU
621 lines
19 KiB
Go
621 lines
19 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SessionCookie is the cookie terdut-server's web UI signs in with.
|
|
const SessionCookie = "terdut_session"
|
|
|
|
// Client talks to terdut-server as the user who signed in. Login trades a
|
|
// username and password for a session, the same one the web UI holds, and every
|
|
// request after it carries that session's cookie.
|
|
//
|
|
// The cookie is attached by hand rather than through a cookie jar: the server
|
|
// marks it Secure behind https, and a jar drops a Secure cookie it is handed
|
|
// over plain http, which would make a local server unusable for no reason. There
|
|
// is nothing else a jar would do here — the token is opaque and does not change
|
|
// while the session lives.
|
|
type Client struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
session string
|
|
}
|
|
|
|
func NewClient(baseURL string) *Client {
|
|
return &Client{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
httpClient: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// SetSession resumes a session from a token saved earlier.
|
|
func (c *Client) SetSession(token string) { c.session = token }
|
|
|
|
// HasSession reports whether there is a session to try. It says nothing about
|
|
// whether the server still honours it.
|
|
func (c *Client) HasSession() bool { return c.session != "" }
|
|
|
|
// Login signs in and returns the session token, which the client also keeps and
|
|
// sends from then on. The server answers a wrong password, an unknown user and
|
|
// an account with no password all with the same 401, so the caller cannot tell
|
|
// them apart. Too many failures come back as 429.
|
|
func (c *Client) Login(username, password string) (string, error) {
|
|
body := struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}{Username: username, Password: password}
|
|
req, err := c.newRequestWithBody(http.MethodPost, "/api/login", body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// A stale session must not ride along on the request that replaces it.
|
|
req.Header.Del("Cookie")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 400 {
|
|
return "", statusError(resp)
|
|
}
|
|
for _, ck := range resp.Cookies() {
|
|
if ck.Name == SessionCookie && ck.Value != "" {
|
|
c.session = ck.Value
|
|
return ck.Value, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("server signed us in but sent no %s cookie", SessionCookie)
|
|
}
|
|
|
|
// Logout ends the session on the server and forgets it here.
|
|
func (c *Client) Logout() error {
|
|
req, err := c.newRequest(http.MethodPost, "/api/logout")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = c.do(req, nil)
|
|
c.session = ""
|
|
return err
|
|
}
|
|
|
|
// authorize puts the session on a request.
|
|
func (c *Client) authorize(req *http.Request) {
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.session != "" {
|
|
req.AddCookie(&http.Cookie{Name: SessionCookie, Value: c.session})
|
|
}
|
|
}
|
|
|
|
func (c *Client) newRequest(method, path string) (*http.Request, error) {
|
|
req, err := http.NewRequest(method, c.baseURL+path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.authorize(req)
|
|
return req, nil
|
|
}
|
|
|
|
// StatusError is a response the server answered with a 4xx or 5xx. Message is
|
|
// the server's own {"error": ...} text, empty when the body carried none.
|
|
type StatusError struct {
|
|
Code int
|
|
Message string
|
|
}
|
|
|
|
func (e *StatusError) Error() string {
|
|
if e.Message != "" {
|
|
return fmt.Sprintf("server returned %d: %s", e.Code, e.Message)
|
|
}
|
|
return fmt.Sprintf("server returned %d", e.Code)
|
|
}
|
|
|
|
// IsUnauthorized reports whether err is the server refusing the session: it
|
|
// expired, was ended elsewhere, or belongs to an account since disabled.
|
|
func IsUnauthorized(err error) bool {
|
|
var se *StatusError
|
|
return errors.As(err, &se) && se.Code == http.StatusUnauthorized
|
|
}
|
|
|
|
func statusError(resp *http.Response) error {
|
|
var e struct {
|
|
Error string `json:"error"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&e)
|
|
return &StatusError{Code: resp.StatusCode, Message: e.Error}
|
|
}
|
|
|
|
func (c *Client) do(req *http.Request, out any) error {
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return statusError(resp)
|
|
}
|
|
|
|
if out != nil {
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListAlerts fetches alerts. teamID limits them to one team; 0 means every team
|
|
// the caller belongs to. status may be "firing", "resolved", or "" for all.
|
|
// Set archived=true to fetch only archived alerts; false returns only non-archived.
|
|
//
|
|
// Alerts are read-only on the server — there is nothing to acknowledge or
|
|
// archive here. This is the raw feed, useful for checking what Alertmanager is
|
|
// actually sending; the work queue is ListIncidents.
|
|
func (c *Client) ListAlerts(teamID int64, status string, archived bool, limit int) ([]Alert, error) {
|
|
q := url.Values{}
|
|
if teamID > 0 {
|
|
q.Set("team_id", strconv.FormatInt(teamID, 10))
|
|
}
|
|
if status != "" {
|
|
q.Set("status", status)
|
|
}
|
|
if archived {
|
|
q.Set("archived", "true")
|
|
}
|
|
if limit > 0 {
|
|
q.Set("limit", strconv.Itoa(limit))
|
|
}
|
|
path := "/api/alerts"
|
|
if len(q) > 0 {
|
|
path += "?" + q.Encode()
|
|
}
|
|
|
|
req, err := c.newRequest(http.MethodGet, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var alerts []Alert
|
|
return alerts, c.do(req, &alerts)
|
|
}
|
|
|
|
// GetAlertStats fetches aggregate alert counts.
|
|
func (c *Client) GetAlertStats() (*AlertStats, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/stats/alerts")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var stats AlertStats
|
|
return &stats, c.do(req, &stats)
|
|
}
|
|
|
|
func (c *Client) newRequestWithBody(method, path string, body any) (*http.Request, error) {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest(method, c.baseURL+path, bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.authorize(req)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
return req, nil
|
|
}
|
|
|
|
func (c *Client) GetAlert(id int64) (*Alert, error) {
|
|
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/alerts/%d", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var alert Alert
|
|
return &alert, c.do(req, &alert)
|
|
}
|
|
|
|
// ── Incidents ──────────────────────────────────────────────────────────────
|
|
|
|
// ListIncidents fetches the work queue. teamID limits it to one team; 0 means
|
|
// every team the caller belongs to. status may be "triggered",
|
|
// "acknowledged", "resolved", or "" for the server default of open incidents
|
|
// only. archived and snoozed each switch the list to that set rather than
|
|
// adding to it, matching the server's filters.
|
|
func (c *Client) ListIncidents(teamID int64, status string, archived, snoozed bool, limit int) ([]Incident, error) {
|
|
q := url.Values{}
|
|
if teamID > 0 {
|
|
q.Set("team_id", strconv.FormatInt(teamID, 10))
|
|
}
|
|
if status != "" {
|
|
q.Set("status", status)
|
|
}
|
|
if archived {
|
|
q.Set("archived", "true")
|
|
}
|
|
if snoozed {
|
|
q.Set("snoozed", "true")
|
|
}
|
|
if limit > 0 {
|
|
q.Set("limit", strconv.Itoa(limit))
|
|
}
|
|
path := "/api/incidents"
|
|
if len(q) > 0 {
|
|
path += "?" + q.Encode()
|
|
}
|
|
|
|
req, err := c.newRequest(http.MethodGet, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incidents []Incident
|
|
return incidents, c.do(req, &incidents)
|
|
}
|
|
|
|
// GetIncident returns one incident with its member alerts inline.
|
|
func (c *Client) GetIncident(id int64) (*Incident, error) {
|
|
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/incidents/%d", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incident Incident
|
|
return &incident, c.do(req, &incident)
|
|
}
|
|
|
|
func (c *Client) GetIncidentTimeline(id int64) ([]IncidentEvent, error) {
|
|
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/incidents/%d/timeline", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var events []IncidentEvent
|
|
return events, c.do(req, &events)
|
|
}
|
|
|
|
// GetSimilarIncidents lists earlier resolved incidents that look like this one
|
|
// and have notes. Servers before the similar-incidents endpoint answer 404; the
|
|
// caller treats any error as "nothing to show".
|
|
func (c *Client) GetSimilarIncidents(id int64) ([]SimilarIncident, error) {
|
|
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/incidents/%d/similar", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var similar []SimilarIncident
|
|
return similar, c.do(req, &similar)
|
|
}
|
|
|
|
func (c *Client) AcknowledgeIncident(id int64) (*Incident, error) {
|
|
req, err := c.newRequest(http.MethodPost, fmt.Sprintf("/api/incidents/%d/acknowledge", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incident Incident
|
|
return &incident, c.do(req, &incident)
|
|
}
|
|
|
|
func (c *Client) UnacknowledgeIncident(id int64) error {
|
|
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/incidents/%d/acknowledge", id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
// ResolveIncident closes an incident by hand. This is terminal on the server: a
|
|
// later occurrence in the same group opens a new incident rather than reopening
|
|
// this one, and if the alert underneath never stops firing the incident stays
|
|
// closed. Use SnoozeIncident for "not now".
|
|
func (c *Client) ResolveIncident(id int64) (*Incident, error) {
|
|
req, err := c.newRequest(http.MethodPost, fmt.Sprintf("/api/incidents/%d/resolve", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incident Incident
|
|
return &incident, c.do(req, &incident)
|
|
}
|
|
|
|
func (c *Client) AssignIncident(id, userID int64) (*Incident, error) {
|
|
body := struct {
|
|
UserID int64 `json:"user_id"`
|
|
}{UserID: userID}
|
|
req, err := c.newRequestWithBody(http.MethodPost, fmt.Sprintf("/api/incidents/%d/assign", id), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incident Incident
|
|
return &incident, c.do(req, &incident)
|
|
}
|
|
|
|
// SnoozeIncident hides an incident from the default queue for a duration,
|
|
// without closing it.
|
|
func (c *Client) SnoozeIncident(id int64, duration string) (*Incident, error) {
|
|
body := struct {
|
|
Duration string `json:"duration"`
|
|
}{Duration: duration}
|
|
req, err := c.newRequestWithBody(http.MethodPost, fmt.Sprintf("/api/incidents/%d/snooze", id), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incident Incident
|
|
return &incident, c.do(req, &incident)
|
|
}
|
|
|
|
func (c *Client) UnsnoozeIncident(id int64) error {
|
|
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/incidents/%d/snooze", id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
func (c *Client) ArchiveIncident(id int64) (*Incident, error) {
|
|
req, err := c.newRequest(http.MethodPost, fmt.Sprintf("/api/incidents/%d/archive", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var incident Incident
|
|
return &incident, c.do(req, &incident)
|
|
}
|
|
|
|
func (c *Client) UnarchiveIncident(id int64) error {
|
|
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/incidents/%d/archive", id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
// AddNote appends a note to the incident's timeline. pinned files it as the
|
|
// resolution note: what fixed the incident, shown on similar ones later.
|
|
func (c *Client) AddNote(incidentID int64, content string, pinned bool) (*IncidentEvent, error) {
|
|
req, err := c.newRequestWithBody(http.MethodPost,
|
|
fmt.Sprintf("/api/incidents/%d/notes", incidentID), map[string]any{"content": content, "pinned": pinned})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var event IncidentEvent
|
|
return &event, c.do(req, &event)
|
|
}
|
|
|
|
// DeleteNote removes one of your own notes. Only notes are deletable — the rest
|
|
// of the timeline is a record of what happened.
|
|
func (c *Client) DeleteNote(incidentID, eventID int64) error {
|
|
req, err := c.newRequest(http.MethodDelete,
|
|
fmt.Sprintf("/api/incidents/%d/notes/%d", incidentID, eventID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
func (c *Client) GetIncidentStats() (*IncidentStats, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/stats/incidents")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var stats IncidentStats
|
|
return &stats, c.do(req, &stats)
|
|
}
|
|
|
|
// ── Statistics ─────────────────────────────────────────────────────────────
|
|
|
|
func (c *Client) GetTopAlerts(limit int) ([]TopAlert, error) {
|
|
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/stats/alerts/top?limit=%d", limit))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []TopAlert
|
|
return result, c.do(req, &result)
|
|
}
|
|
|
|
func (c *Client) GetStatsByHour() ([]HourStat, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/stats/alerts/by-hour")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []HourStat
|
|
return result, c.do(req, &result)
|
|
}
|
|
|
|
func (c *Client) GetStatsByDay() ([]DayStat, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/stats/alerts/by-day")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []DayStat
|
|
return result, c.do(req, &result)
|
|
}
|
|
|
|
// ── Teams ──────────────────────────────────────────────────────────────────
|
|
|
|
// ListTeams returns the teams the caller belongs to, with the caller's role in
|
|
// each. Everything else the server returns is scoped to these. A server that
|
|
// predates teams (v0.12) answers 404, which is how the TUI spots one.
|
|
func (c *Client) ListTeams() ([]Team, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/teams")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var teams []Team
|
|
return teams, c.do(req, &teams)
|
|
}
|
|
|
|
// ListTeamMembers returns who belongs to a team. A schedule can only be given to
|
|
// its own members, so this is the assignee list for one.
|
|
func (c *Client) ListTeamMembers(teamID int64) ([]TeamMember, error) {
|
|
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/teams/%d/members", teamID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var members []TeamMember
|
|
return members, c.do(req, &members)
|
|
}
|
|
|
|
// ── Schedule ───────────────────────────────────────────────────────────────
|
|
|
|
// GetSchedule returns a team's on-call entries between two YYYY-MM-DD dates.
|
|
func (c *Client) GetSchedule(teamID int64, from, to string) ([]ScheduleEntry, error) {
|
|
q := url.Values{"from": {from}, "to": {to}}
|
|
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/teams/%d/schedule?%s", teamID, q.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var entries []ScheduleEntry
|
|
return entries, c.do(req, &entries)
|
|
}
|
|
|
|
// GetCurrentOnCall returns today's on-call entries, one per team that has
|
|
// somebody scheduled. It is empty, not an error, when nobody is.
|
|
func (c *Client) GetCurrentOnCall() ([]ScheduleEntry, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/schedule/current")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var entries []ScheduleEntry
|
|
return entries, c.do(req, &entries)
|
|
}
|
|
|
|
// AssignSchedule puts one team member on call for the given dates. Only a team
|
|
// owner or an administrator may.
|
|
//
|
|
// The server holds one person per day and refuses a date somebody already has,
|
|
// so replace is what takes a shift off its current holder. It is all-or-nothing
|
|
// either way: a week of free and taken days moves as a unit, or not at all.
|
|
func (c *Client) AssignSchedule(teamID, userID int64, dates []string, replace bool) ([]ScheduleEntry, error) {
|
|
body := struct {
|
|
UserID int64 `json:"user_id"`
|
|
Dates []string `json:"dates"`
|
|
Replace bool `json:"replace,omitempty"`
|
|
}{UserID: userID, Dates: dates, Replace: replace}
|
|
req, err := c.newRequestWithBody(http.MethodPost, fmt.Sprintf("/api/teams/%d/schedule", teamID), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var entries []ScheduleEntry
|
|
return entries, c.do(req, &entries)
|
|
}
|
|
|
|
func (c *Client) DeleteScheduleEntry(teamID, id int64) error {
|
|
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/teams/%d/schedule/%d", teamID, id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
// ── Users ──────────────────────────────────────────────────────────────────
|
|
|
|
func (c *Client) ListUsers() ([]User, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/users")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var users []User
|
|
return users, c.do(req, &users)
|
|
}
|
|
|
|
func (c *Client) CreateUser(username, email string) (*User, error) {
|
|
body := struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
}{Username: username, Email: email}
|
|
req, err := c.newRequestWithBody(http.MethodPost, "/api/users", body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var user User
|
|
return &user, c.do(req, &user)
|
|
}
|
|
|
|
// SetUserNotifyTarget points a user's push notifications at an ntfy topic.
|
|
//
|
|
// An empty topic clears it: the server stores NULL, and that user's incidents
|
|
// page the shared fallback topic instead — which carries no Acknowledge button,
|
|
// because anyone subscribed to it could otherwise acknowledge as somebody else.
|
|
func (c *Client) SetUserNotifyTarget(userID int64, topic string) (*User, error) {
|
|
body := struct {
|
|
NtfyTopic string `json:"ntfy_topic"`
|
|
}{NtfyTopic: topic}
|
|
req, err := c.newRequestWithBody(http.MethodPut, fmt.Sprintf("/api/users/%d/notify", userID), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var user User
|
|
return &user, c.do(req, &user)
|
|
}
|
|
|
|
func (c *Client) DeleteUser(id int64) error {
|
|
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/users/%d", id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
func (c *Client) CreateAPIKey(userID int64, name string) (*APIKey, error) {
|
|
body := struct {
|
|
Name string `json:"name"`
|
|
}{Name: name}
|
|
req, err := c.newRequestWithBody(http.MethodPost, fmt.Sprintf("/api/users/%d/api-keys", userID), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var key APIKey
|
|
return &key, c.do(req, &key)
|
|
}
|
|
|
|
func (c *Client) DeleteAPIKey(userID, keyID int64) error {
|
|
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/users/%d/api-keys/%d", userID, keyID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
// Me returns the user the API key belongs to, and whether they have a web UI
|
|
// password.
|
|
func (c *Client) Me() (*Me, error) {
|
|
req, err := c.newRequest(http.MethodGet, "/api/me")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var me Me
|
|
return &me, c.do(req, &me)
|
|
}
|
|
|
|
// SetPassword sets a user's web UI password. current is only checked by the
|
|
// server when a user changes their own existing password; pass "" otherwise.
|
|
func (c *Client) SetPassword(userID int64, password, current string) error {
|
|
body := struct {
|
|
Password string `json:"password"`
|
|
CurrentPassword string `json:"current_password,omitempty"`
|
|
}{Password: password, CurrentPassword: current}
|
|
req, err := c.newRequestWithBody(http.MethodPut, fmt.Sprintf("/api/users/%d/password", userID), body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.do(req, nil)
|
|
}
|
|
|
|
// HealthCheck calls GET /healthz, which is unauthenticated and does no database
|
|
// check, so it says the process is up, not that the API key works.
|
|
func (c *Client) HealthCheck() error {
|
|
req, err := http.NewRequest(http.MethodGet, c.baseURL+"/healthz", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("health check failed: %s", resp.Status)
|
|
}
|
|
return nil
|
|
}
|