Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ on:

env:
GO_VERSION: '1.24'
GO_LINT: 'v2.1.6'

permissions:
contents: read

jobs:
check_linter_version:
name: check golangci-lint version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check golangci-lint version
run: make lint-check-version
check:
runs-on: ubuntu-latest
needs: check_linter_version
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
Expand All @@ -23,7 +32,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.1.5
version: ${{ env.GO_LINT }}

test:
name: go-test
Expand Down
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ default: test
SHELL=/bin/bash

SD_DB?="postgresql://pg:pass@localhost:5432/status_dashboard?sslmode=disable"
GOLANGCI_LINT_VERSION?="2.1.6"

test:
@echo running unit tests
Expand All @@ -17,9 +18,15 @@ build:
go build -o app cmd/main.go

lint:
@echo check linter version
if [[ $$(golangci-lint --version |awk '{print $$4}') == $(GOLANGCI_LINT_VERSION) ]]; then echo "current installed version is actual to $(GOLANGCI_LINT_VERSION)"; else echo "current version is not actual, please use $(GOLANGCI_LINT_VERSION)"; exit 1; fi
@echo running linter
golangci-lint run -v

lint-check-version:
@echo check linter version
@if [[ $(GO_LINT) == v$(GOLANGCI_LINT_VERSION) ]]; then echo "current installed version is actual to $(GOLANGCI_LINT_VERSION)"; else echo "current version $(GO_LINT) is not actual, please use $(GOLANGCI_LINT_VERSION)"; exit 1; fi

migrate-up:
@echo staring migrations
migrate -database $(SD_DB) -path db/migrations up
Expand Down
3 changes: 2 additions & 1 deletion internal/api/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ func MoveIncidentToHigherImpact(
if incWithHighImpact == nil {
if len(incident.Components) > 1 {
log.Info("no active incidents with requested impact, opening the new one")
return dbInst.ExtractComponentToNewIncident(storedComponent, incident, impact, text)
components := []db.Component{*storedComponent}
return dbInst.ExtractComponentsToNewIncident(components, incident, impact, text)
}
log.Info(
"only one component in the incident, increase impact",
Expand Down
2 changes: 1 addition & 1 deletion internal/api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func ValidateComponentsMW(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
logger.Info("start to validate given components")
type Components struct {
Components []int `json:"components"`
Components []int `json:"components" binding:"required,min=1"`
}

var components Components
Expand Down
7 changes: 4 additions & 3 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ func (a *API) InitRoutes() {
)
v2API.GET("incidents/:id", v2.GetIncidentHandler(a.db, a.log))
v2API.PATCH("incidents/:id", AuthenticationMW(a.oa2Prov, a.log), v2.PatchIncidentHandler(a.db, a.log))
v2API.POST("incidents/:id/extract",
AuthenticationMW(a.oa2Prov, a.log),
ValidateComponentsMW(a.db, a.log),
v2.PostIncidentExtractHandler(a.db, a.log))

v2API.GET("availability", v2.GetComponentsAvailabilityHandler(a.db, a.log))

//nolint:gocritic
//v2API.GET("/separate/<incident_id>/<component_id>") - > investigate it!!!
}

rssFEED := a.r.Group("rss")
Expand Down
2 changes: 2 additions & 0 deletions internal/api/v2/statuses.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var maintenanceStatuses = map[string]struct{}{

// Incident actions for opened incidents.
const (
IncidentDetected = "detected" // not implemented yet
IncidentAnalysing = "analysing"
IncidentFixing = "fixing"
IncidentImpactChanged = "impact changed"
Expand All @@ -25,6 +26,7 @@ const (

//nolint:gochecknoglobals
var incidentOpenStatuses = map[string]struct{}{
IncidentDetected: {},
IncidentAnalysing: {},
IncidentFixing: {},
IncidentImpactChanged: {},
Expand Down
65 changes: 65 additions & 0 deletions internal/api/v2/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,71 @@ func updateFields(income *PatchIncidentData, stored *db.Incident) {
}
}

type PostIncidentSeparateData struct {
Components []int `json:"components" binding:"required,min=1"`
}

func PostIncidentExtractHandler(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc { //nolint:gocognit
return func(c *gin.Context) {
logger.Debug("start to extract components to the new incident")

var incID IncidentID
if err := c.ShouldBindUri(&incID); err != nil {
apiErrors.RaiseBadRequestErr(c, err)
return
}

var incData PostIncidentSeparateData
if err := c.ShouldBindBodyWithJSON(&incData); err != nil {
apiErrors.RaiseBadRequestErr(c, err)
return
}

logger.Debug(
"extract components from the incident",
zap.Any("components", incData.Components),
zap.Int("incident_id", incID.ID),
)

storedInc, err := dbInst.GetIncident(incID.ID)
if err != nil {
apiErrors.RaiseInternalErr(c, err)
return
}

var movedComponents []db.Component
var movedCounter int
for _, incCompID := range incData.Components {
present := false
for _, storedComp := range storedInc.Components {
if incCompID == int(storedComp.ID) {
present = true
movedComponents = append(movedComponents, storedComp)
movedCounter++
break
}
}
if !present {
apiErrors.RaiseBadRequestErr(c, fmt.Errorf("component %d is not in the incident", incCompID))
return
}
}

if movedCounter == len(storedInc.Components) {
apiErrors.RaiseBadRequestErr(c, fmt.Errorf("can not move all components to the new incident, keep at least one"))
return
}

inc, err := dbInst.ExtractComponentsToNewIncident(movedComponents, storedInc, *storedInc.Impact, *storedInc.Text)
if err != nil {
apiErrors.RaiseInternalErr(c, err)
return
}

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

type Component struct {
ComponentID
Attributes []ComponentAttribute `json:"attributes"`
Expand Down
53 changes: 31 additions & 22 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ func (db *DB) GetIncident(id int) (*Incident, error) {
Where(inc).
Preload("Statuses").
Preload("Components", func(db *gorm.DB) *gorm.DB {
return db.Select("ID")
return db.Select("ID, Name")
}).
Preload("Components.Attrs").
First(&inc)

if r.Error != nil {
Expand Down Expand Up @@ -378,9 +379,13 @@ func (db *DB) MoveComponentFromOldToAnotherIncident(
return incNew, nil
}

func (db *DB) ExtractComponentToNewIncident(
comp *Component, incOld *Incident, impact int, text string,
func (db *DB) ExtractComponentsToNewIncident(
comp []Component, incOld *Incident, impact int, text string,
) (*Incident, error) {
if len(comp) == 0 {
return nil, fmt.Errorf("no components to extract")
}

timeNow := time.Now().UTC()

inc := &Incident{
Expand All @@ -390,39 +395,43 @@ func (db *DB) ExtractComponentToNewIncident(
Impact: &impact,
Statuses: []IncidentStatus{},
System: false,
Components: []Component{*comp},
Components: comp,
}

id, err := db.SaveIncident(inc)
if err != nil {
return nil, err
}

incText := fmt.Sprintf("%s moved from %s", comp.PrintAttrs(), incOld.Link())
inc.Statuses = append(inc.Statuses, IncidentStatus{
IncidentID: id,
Status: statusSYSTEM,
Text: incText,
Timestamp: timeNow,
})
for _, c := range comp {
incText := fmt.Sprintf("%s moved from %s", c.PrintAttrs(), incOld.Link())
inc.Statuses = append(inc.Statuses, IncidentStatus{
IncidentID: id,
Status: statusSYSTEM,
Text: incText,
Timestamp: timeNow,
})
}

err = db.ModifyIncident(inc)
if err != nil {
return nil, err
}

err = db.g.Model(incOld).Association("Components").Delete(comp)
if err != nil {
return nil, err
}
for _, c := range comp {
err = db.g.Model(incOld).Association("Components").Delete(c)
if err != nil {
return nil, err
}

incText = fmt.Sprintf("%s moved to %s", comp.PrintAttrs(), inc.Link())
incOld.Statuses = append(incOld.Statuses, IncidentStatus{
IncidentID: inc.ID,
Status: statusSYSTEM,
Text: incText,
Timestamp: timeNow,
})
incText := fmt.Sprintf("%s moved to %s", c.PrintAttrs(), inc.Link())
incOld.Statuses = append(incOld.Statuses, IncidentStatus{
IncidentID: inc.ID,
Status: statusSYSTEM,
Text: incText,
Timestamp: timeNow,
})
}

err = db.ModifyIncident(incOld)
if err != nil {
Expand Down
40 changes: 40 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,36 @@ paths:
description: Invalid ID supplied
'404':
description: Incident not found.
/v2/incidents/{incident_id}/extract:
post:
summary: extract components to the new incident
tags:
- incidents
parameters:
- name: incident_id
in: path
description: ID of incident to return
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/IncidentPostExtract'
required: true
responses:
'200':
description: successful operation, return the new incident id
content:
application/json:
schema:
$ref: '#/components/schemas/Incident'
'400':
description: Invalid ID supplied
'404':
description: Incident not found.

/v1/component_status:
get:
Expand Down Expand Up @@ -597,6 +627,16 @@ components:
end_date:
type: string
format: date-time
IncidentPostExtract:
type: object
required:
- components
properties:
components:
type: array
items:
type: string
example: [ 218, 254 ]
IncidentStatus:
type: object
allOf:
Expand Down
2 changes: 2 additions & 0 deletions tests/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,7 @@ func initRoutesV2(t *testing.T, c *gin.Engine, dbInst *db.DB, logger *zap.Logger
v2Api.POST("incidents", api.ValidateComponentsMW(dbInst, logger), v2.PostIncidentHandler(dbInst, logger))
v2Api.GET("incidents/:id", v2.GetIncidentHandler(dbInst, logger))
v2Api.PATCH("incidents/:id", v2.PatchIncidentHandler(dbInst, logger))
v2Api.POST("incidents/:id/extract", v2.PostIncidentExtractHandler(dbInst, logger))

v2Api.GET("availability", v2.GetComponentsAvailabilityHandler(dbInst, logger))
}
Loading