Skip to content

Commit

Permalink
Implementation for V2 incident patching (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
sgmv authored Jan 15, 2025
1 parent 123ebdb commit 4ba39fc
Show file tree
Hide file tree
Showing 11 changed files with 478 additions and 114 deletions.
19 changes: 19 additions & 0 deletions internal/api/errors/component.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package errors

import (
"errors"
"fmt"
)

var ErrComponentDSNotExist = errors.New("component does not exist")

func NewErrComponentDSNotExist(componentID int) error {
return fmt.Errorf("%w, component_id: %d", ErrComponentDSNotExist, componentID)
}

var ErrComponentExist = errors.New("component already exists")
var ErrComponentInvalidFormat = errors.New("component invalid format")
var ErrComponentAttrInvalidFormat = errors.New("component attribute has invalid format")
var ErrComponentRegionAttrMissing = errors.New("component attribute region is missing or invalid")
var ErrComponentTypeAttrMissing = errors.New("component attribute type is missing or invalid")
var ErrComponentCategoryAttrMissing = errors.New("component attribute category is missing or invalid")
22 changes: 0 additions & 22 deletions internal/api/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,6 @@ func (e *MsgError) Error() string {
var ErrPageNotFound = errors.New("page not found")
var ErrInternalError = errors.New("internal server error")

var ErrIncidentDSNotExist = errors.New("incident does not exist")
var ErrIncidentEndDateShouldBeEmpty = errors.New("incident end_date should be empty")
var ErrIncidentUpdatesShouldBeEmpty = errors.New("incident updates should be empty")

var ErrIncidentCreationMaintenanceExists = errors.New("incident creation failed, component in maintenance incident")
var ErrIncidentCreationLowImpact = errors.New(
"incident creation failed, exists the incident with higher impact for component",
)

var ErrComponentDSNotExist = errors.New("component does not exist")

func NewErrComponentDSNotExist(componentID int) error {
return fmt.Errorf("%w, component_id: %d", ErrComponentDSNotExist, componentID)
}

var ErrComponentExist = errors.New("component already exists")
var ErrComponentInvalidFormat = errors.New("component invalid format")
var ErrComponentAttrInvalidFormat = errors.New("component attribute has invalid format")
var ErrComponentRegionAttrMissing = errors.New("component attribute region is missing or invalid")
var ErrComponentTypeAttrMissing = errors.New("component attribute type is missing or invalid")
var ErrComponentCategoryAttrMissing = errors.New("component attribute category is missing or invalid")

func Return404(c *gin.Context) {
c.JSON(http.StatusNotFound, ReturnError(ErrPageNotFound))
}
Expand Down
23 changes: 23 additions & 0 deletions internal/api/errors/incident.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package errors

import "errors"

var ErrIncidentDSNotExist = errors.New("incident does not exist")
var ErrIncidentEndDateShouldBeEmpty = errors.New("incident end_date should be empty")
var ErrIncidentUpdatesShouldBeEmpty = errors.New("incident updates should be empty")

var ErrIncidentCreationMaintenanceExists = errors.New("incident creation failed, component in maintenance")
var ErrIncidentCreationLowImpact = errors.New(
"incident creation failed, exists the incident with higher impact for component",
)

// Errors for patching incident

var ErrIncidentPatchMaintenanceImpactForbidden = errors.New("can not change impact for maintenance")
var ErrIncidentPatchMaintenanceStatus = errors.New("wrong status for maintenance")
var ErrIncidentPatchStatus = errors.New("wrong status for incident")
var ErrIncidentPatchClosedStatus = errors.New("wrong status for closed incident")
var ErrIncidentPatchOpenedStartDate = errors.New("can not change start date for open incident")
var ErrIncidentPatchOpenedEndDateMissing = errors.New("wrong end date with resolved status")
var ErrIncidentPatchImpactStatusWrong = errors.New("wrong status for changing impact")
var ErrIncidentPatchImpactToMaintenanceForbidden = errors.New("can not change impact to maintenance")
3 changes: 1 addition & 2 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ func (a *API) InitRoutes() {
v2Api.GET("incidents", v2.GetIncidentsHandler(a.db, a.log))
v2Api.POST("incidents", ValidateComponentsMW(a.db, a.log), v2.PostIncidentHandler(a.db, a.log))
v2Api.GET("incidents/:id", v2.GetIncidentHandler(a.db, a.log))
v2Api.PATCH("incidents/:id", ValidateComponentsMW(a.db, a.log), v2.PatchIncidentHandler(a.db, a.log))
v2Api.PATCH("incidents/:id", v2.PatchIncidentHandler(a.db, a.log))

v2Api.PATCH("incidents/:id", a.ValidateComponentsMW(), v2.PatchIncidentHandler(a.db, a.log))
v2Api.GET("availability", v2.GetComponentsAvailabilityHandler(a.db, a.log))
//nolint:gocritic
//v2Api.GET("rss")
Expand Down
25 changes: 0 additions & 25 deletions internal/api/v1/statuses.go

This file was deleted.

46 changes: 46 additions & 0 deletions internal/api/v2/statuses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package v2

const (
MaintenanceInProgress = "in progress"
// MaintenanceModified is placed if the time window was changed.
MaintenanceModified = "modified"
MaintenanceCompleted = "completed"
)

//nolint:gochecknoglobals
var maintenanceStatuses = map[string]struct{}{
MaintenanceInProgress: {},
MaintenanceModified: {},
MaintenanceCompleted: {},
}

// Incident actions for opened incidents.
const (
IncidentAnalysing = "analysing"
IncidentFixing = "fixing"
IncidentImpactChanged = "impact changed"
IncidentObserving = "observing"
IncidentResolved = "resolved"
)

//nolint:gochecknoglobals
var incidentOpenStatuses = map[string]struct{}{
IncidentAnalysing: {},
IncidentFixing: {},
IncidentImpactChanged: {},
IncidentObserving: {},
IncidentResolved: {},
}

// These statuses are using only for closed incidents.
const (
IncidentReopened = "reopened"
// IncidentChanged indicates if the end date was changed for closed incident.
IncidentChanged = "changed"
)

//nolint:gochecknoglobals
var incidentClosedStatuses = map[string]struct{}{
IncidentReopened: {},
IncidentChanged: {},
}
176 changes: 128 additions & 48 deletions internal/api/v2/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,27 @@ func GetIncidentHandler(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc {
return
}

components := make([]int, len(r.Components))
for i, comp := range r.Components {
components[i] = int(comp.ID)
}
c.JSON(http.StatusOK, toAPIIncident(r))
}
}

incData := IncidentData{
Title: *r.Text,
Impact: r.Impact,
Components: components,
StartDate: *r.StartDate,
EndDate: r.EndDate,
System: &r.System,
Updates: r.Statuses,
}
func toAPIIncident(inc *db.Incident) *Incident {
components := make([]int, len(inc.Components))
for i, comp := range inc.Components {
components[i] = int(comp.ID)
}

c.JSON(http.StatusOK, &Incident{incID, incData})
incData := IncidentData{
Title: *inc.Text,
Impact: inc.Impact,
Components: components,
StartDate: *inc.StartDate,
EndDate: inc.EndDate,
System: &inc.System,
Updates: inc.Statuses,
}

return &Incident{IncidentID{ID: int(inc.ID)}, incData}
}

// PostIncidentHandler creates an incident.
Expand Down Expand Up @@ -290,20 +294,13 @@ func createIncident(dbInst *db.DB, log *zap.Logger, inc *db.Incident, descriptio
}

type PatchIncidentData struct {
Title *string `json:"title,omitempty"`
// INCIDENT_IMPACTS = {
// 0: Impact(0, "maintenance", "Scheduled maintenance"),
// 1: Impact(1, "minor", "Minor incident (i.e. performance impact)"),
// 2: Impact(2, "major", "Major incident"),
// 3: Impact(3, "outage", "Service outage"),
// }
Impact *int `json:"impact,omitempty"`
Components []int `json:"components,omitempty"`
// Datetime format is standard: "2006-01-01T12:00:00Z"
StartDate *time.Time `json:"start_date,omitempty"`
EndDate *time.Time `json:"end_date,omitempty"`
System *bool `json:"system,omitempty"`
Update *db.IncidentStatus `json:"update,omitempty"`
Title *string `json:"title,omitempty"`
Impact *int `json:"impact,omitempty"`
Message string `json:"message" binding:"required"`
Status string `json:"status" binding:"required"`
UpdateDate time.Time `json:"update_date" binding:"required"`
StartDate *time.Time `json:"start_date,omitempty"`
EndDate *time.Time `json:"end_date,omitempty"`
}

func PatchIncidentHandler(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc {
Expand All @@ -322,37 +319,120 @@ func PatchIncidentHandler(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc {
return
}

var components []db.Component
if len(incData.Components) != 0 {
components = make([]db.Component, len(incData.Components))
for i, comp := range incData.Components {
components[i] = db.Component{ID: uint(comp)}
}
storedIncident, err := dbInst.GetIncident(incID.ID)
if err != nil {
apiErrors.RaiseInternalErr(c, err)
return
}

var statuses []db.IncidentStatus
if incData.Update != nil {
statuses = append(statuses, *incData.Update)
if err = checkPatchData(&incData, storedIncident); err != nil {
apiErrors.RaiseBadRequestErr(c, err)
return
}

dbInc := db.Incident{
ID: uint(incID.ID),
Text: incData.Title,
StartDate: incData.StartDate,
EndDate: incData.EndDate,
Impact: incData.Impact,
System: *incData.System,
Components: components,
Statuses: statuses,
updateFields(&incData, storedIncident)

status := db.IncidentStatus{
IncidentID: storedIncident.ID,
Status: incData.Status,
Text: incData.Message,
Timestamp: incData.UpdateDate,
}
storedIncident.Statuses = append(storedIncident.Statuses, status)

err := dbInst.ModifyIncident(&dbInc)
err = dbInst.ModifyIncident(storedIncident)
if err != nil {
apiErrors.RaiseInternalErr(c, err)
return
}

c.JSON(http.StatusOK, gin.H{"msg": "incident updated"})
if incData.Status == IncidentReopened {
err = dbInst.ReOpenIncident(storedIncident)
if err != nil {
apiErrors.RaiseInternalErr(c, err)
return
}
}

inc, errDB := dbInst.GetIncident(int(storedIncident.ID))
if errDB != nil {
apiErrors.RaiseInternalErr(c, errDB)
return
}

c.JSON(http.StatusOK, toAPIIncident(inc))
}
}

func checkPatchData(incoming *PatchIncidentData, stored *db.Incident) error {
if *stored.Impact == 0 {
if incoming.Impact != nil && *incoming.Impact != 0 {
return apiErrors.ErrIncidentPatchMaintenanceImpactForbidden
}

if _, ok := maintenanceStatuses[incoming.Status]; !ok {
return apiErrors.ErrIncidentPatchMaintenanceStatus
}

return nil
}

if stored.EndDate != nil {
if _, ok := incidentClosedStatuses[incoming.Status]; !ok {
return apiErrors.ErrIncidentPatchClosedStatus
}

if (incoming.StartDate != nil || incoming.EndDate != nil) && incoming.Status != IncidentChanged {
return apiErrors.ErrIncidentPatchClosedStatus
}

return nil
}

if (incoming.Impact != nil && *incoming.Impact != *stored.Impact) && incoming.Status != IncidentImpactChanged {
return apiErrors.ErrIncidentPatchImpactStatusWrong
}

if incoming.Impact != nil && *incoming.Impact != *stored.Impact && *incoming.Impact == 0 {
return apiErrors.ErrIncidentPatchImpactToMaintenanceForbidden
}

if _, ok := incidentOpenStatuses[incoming.Status]; !ok {
return apiErrors.ErrIncidentPatchStatus
}

if incoming.StartDate != nil {
return apiErrors.ErrIncidentPatchOpenedStartDate
}

return nil
}

func updateFields(income *PatchIncidentData, stored *db.Incident) {
if *stored.Impact == 0 || stored.EndDate != nil {
if income.StartDate != nil {
stored.StartDate = income.StartDate
}

if income.EndDate != nil {
stored.EndDate = income.EndDate
}
}

if income.Title != nil {
stored.Text = income.Title
}

if income.Impact != nil {
stored.Impact = income.Impact
}

if income.Status == IncidentReopened {
stored.EndDate = nil
}

if income.Status == IncidentResolved {
stored.EndDate = &income.UpdateDate
}
}

Expand Down
13 changes: 12 additions & 1 deletion internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ func (db *DB) SaveIncident(inc *Incident) (uint, error) {
return inc.ID, nil
}

// TODO: check this function for patching incident
func (db *DB) ModifyIncident(inc *Incident) error {
r := db.g.Updates(inc)

Expand All @@ -110,6 +109,18 @@ func (db *DB) ModifyIncident(inc *Incident) error {
return nil
}

// ReOpenIncident the special function if you need to NULL your end_date.
func (db *DB) ReOpenIncident(inc *Incident) error {
r := db.g.Model(&Incident{}).Where("id = ?", inc.ID).Updates(map[string]interface{}{
"end_date": nil,
})
if r.Error != nil {
return r.Error
}

return nil
}

func (db *DB) GetOpenedIncidentsWithComponent(name string, attrs []ComponentAttr) (*Incident, error) {
comp := &Component{Name: name, Attrs: attrs}
r := db.g.Model(&Component{}).Preload("Attrs").Find(comp)
Expand Down
Loading

0 comments on commit 4ba39fc

Please sign in to comment.