feat: status page authoring API
Adds owner|admin routes under /api/status-pages for authoring status pages and their incidents/maintenance windows, gated by the status_pages licence feature. Adds the "status" token scope resource and the ten route-scope entries, and regenerates the committed OpenAPI document. Also types ErrPageInvalid as a sentinel for status page/incident validation failures (previously bare errors), so statusPageError maps them to 400 instead of 500, and createStatusIncident/updateStatusIncident route through the shared error mapper rather than hand-rolling a 400 for any service error.
This commit is contained in:
@@ -26,6 +26,7 @@ var ScopeResources = []string{
|
||||
"vulns",
|
||||
"workloads",
|
||||
"settings",
|
||||
"status",
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -35,34 +35,34 @@ var impacts = map[string]bool{
|
||||
|
||||
func validateIncident(inc *models.StatusIncident) error {
|
||||
if inc.Title == "" {
|
||||
return errors.New("title is required")
|
||||
return fmt.Errorf("%w: title is required", ErrPageInvalid)
|
||||
}
|
||||
if len(inc.PageIDs) == 0 {
|
||||
return errors.New("at least one page is required")
|
||||
return fmt.Errorf("%w: at least one page is required", ErrPageInvalid)
|
||||
}
|
||||
switch inc.Kind {
|
||||
case models.StatusKindIncident:
|
||||
if !incidentStatuses[inc.Status] {
|
||||
return fmt.Errorf("invalid incident status %q", inc.Status)
|
||||
return fmt.Errorf("%w: invalid incident status %q", ErrPageInvalid, inc.Status)
|
||||
}
|
||||
case models.StatusKindMaintenance:
|
||||
if !maintenanceStatuses[inc.Status] {
|
||||
return fmt.Errorf("invalid maintenance status %q", inc.Status)
|
||||
return fmt.Errorf("%w: invalid maintenance status %q", ErrPageInvalid, inc.Status)
|
||||
}
|
||||
if inc.ScheduledStart == nil || inc.ScheduledEnd == nil {
|
||||
return errors.New("maintenance needs a scheduled start and end")
|
||||
return fmt.Errorf("%w: maintenance needs a scheduled start and end", ErrPageInvalid)
|
||||
}
|
||||
if !inc.ScheduledEnd.After(*inc.ScheduledStart) {
|
||||
return errors.New("maintenance must end after it starts")
|
||||
return fmt.Errorf("%w: maintenance must end after it starts", ErrPageInvalid)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid kind %q", inc.Kind)
|
||||
return fmt.Errorf("%w: invalid kind %q", ErrPageInvalid, inc.Kind)
|
||||
}
|
||||
if inc.Impact == "" {
|
||||
inc.Impact = models.ImpactNone
|
||||
}
|
||||
if !impacts[inc.Impact] {
|
||||
return fmt.Errorf("invalid impact %q", inc.Impact)
|
||||
return fmt.Errorf("%w: invalid impact %q", ErrPageInvalid, inc.Impact)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -214,14 +214,14 @@ func AppendStatusIncidentUpdate(instanceID, incidentID, status, body, author str
|
||||
return nil, err
|
||||
}
|
||||
if body == "" {
|
||||
return nil, errors.New("update body is required")
|
||||
return nil, fmt.Errorf("%w: update body is required", ErrPageInvalid)
|
||||
}
|
||||
valid := incidentStatuses
|
||||
if existing.Kind == models.StatusKindMaintenance {
|
||||
valid = maintenanceStatuses
|
||||
}
|
||||
if !valid[status] {
|
||||
return nil, fmt.Errorf("invalid status %q for a %s", status, existing.Kind)
|
||||
return nil, fmt.Errorf("%w: invalid status %q for a %s", ErrPageInvalid, status, existing.Kind)
|
||||
}
|
||||
|
||||
upd := models.StatusIncidentUpdate{At: time.Now(), Status: status, Body: body, Author: author}
|
||||
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
@@ -74,6 +75,12 @@ func EnsureStatusPageIndexes() error {
|
||||
var (
|
||||
ErrPageNotFound = errors.New("status page not found")
|
||||
ErrPageIDTaken = errors.New("that page id is already in use")
|
||||
|
||||
// ErrPageInvalid is the sentinel for validation failures on a page or
|
||||
// incident body — anything the caller can fix by sending a different
|
||||
// request. statusPageError maps it to 400; wrap it rather than returning a
|
||||
// bare error, or a bad request answers 500.
|
||||
ErrPageInvalid = errors.New("status page request invalid")
|
||||
)
|
||||
|
||||
// statusRedis is set by main.go at boot. It is nil in any process that has not
|
||||
@@ -134,7 +141,7 @@ func CreateStatusPage(instanceID string, p *models.StatusPage) (*models.StatusPa
|
||||
return nil, err
|
||||
}
|
||||
if p.Title == "" {
|
||||
return nil, errors.New("title is required")
|
||||
return nil, fmt.Errorf("%w: title is required", ErrPageInvalid)
|
||||
}
|
||||
p.ID = bson.ObjectID{}
|
||||
p.InstanceID = instanceID
|
||||
@@ -169,7 +176,7 @@ func CreateStatusPage(instanceID string, p *models.StatusPage) (*models.StatusPa
|
||||
// The page id itself is immutable: it is a URL that has been handed out.
|
||||
func UpdateStatusPage(instanceID, pageID string, p *models.StatusPage) (*models.StatusPage, error) {
|
||||
if p.Title == "" {
|
||||
return nil, errors.New("title is required")
|
||||
return nil, fmt.Errorf("%w: title is required", ErrPageInvalid)
|
||||
}
|
||||
if p.Sections == nil {
|
||||
p.Sections = []models.StatusPageSection{}
|
||||
|
||||
Reference in New Issue
Block a user