Skip to content

Commit c13e6fe

Browse files
authored
Component extraction (#69)
1 parent 050049d commit c13e6fe

File tree

11 files changed

+230
-28
lines changed

11 files changed

+230
-28
lines changed

.github/workflows/ci.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@ on:
88

99
env:
1010
GO_VERSION: '1.24'
11+
GO_LINT: 'v2.1.6'
1112

1213
permissions:
1314
contents: read
1415

1516
jobs:
17+
check_linter_version:
18+
name: check golangci-lint version
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
- name: Check golangci-lint version
23+
run: make lint-check-version
1624
check:
1725
runs-on: ubuntu-latest
26+
needs: check_linter_version
1827
steps:
1928
- uses: actions/checkout@v4
2029
- uses: actions/setup-go@v5
@@ -23,7 +32,7 @@ jobs:
2332
- name: golangci-lint
2433
uses: golangci/golangci-lint-action@v7
2534
with:
26-
version: v2.1.5
35+
version: ${{ env.GO_LINT }}
2736

2837
test:
2938
name: go-test

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ default: test
33
SHELL=/bin/bash
44

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

78
test:
89
@echo running unit tests
@@ -17,9 +18,15 @@ build:
1718
go build -o app cmd/main.go
1819

1920
lint:
21+
@echo check linter version
22+
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
2023
@echo running linter
2124
golangci-lint run -v
2225

26+
lint-check-version:
27+
@echo check linter version
28+
@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
29+
2330
migrate-up:
2431
@echo staring migrations
2532
migrate -database $(SD_DB) -path db/migrations up

internal/api/common/common.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ func MoveIncidentToHigherImpact(
1515
if incWithHighImpact == nil {
1616
if len(incident.Components) > 1 {
1717
log.Info("no active incidents with requested impact, opening the new one")
18-
return dbInst.ExtractComponentToNewIncident(storedComponent, incident, impact, text)
18+
components := []db.Component{*storedComponent}
19+
return dbInst.ExtractComponentsToNewIncident(components, incident, impact, text)
1920
}
2021
log.Info(
2122
"only one component in the incident, increase impact",

internal/api/middleware.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func ValidateComponentsMW(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc {
2020
return func(c *gin.Context) {
2121
logger.Info("start to validate given components")
2222
type Components struct {
23-
Components []int `json:"components"`
23+
Components []int `json:"components" binding:"required,min=1"`
2424
}
2525

2626
var components Components

internal/api/routes.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@ func (a *API) InitRoutes() {
4545
)
4646
v2API.GET("incidents/:id", v2.GetIncidentHandler(a.db, a.log))
4747
v2API.PATCH("incidents/:id", AuthenticationMW(a.oa2Prov, a.log), v2.PatchIncidentHandler(a.db, a.log))
48+
v2API.POST("incidents/:id/extract",
49+
AuthenticationMW(a.oa2Prov, a.log),
50+
ValidateComponentsMW(a.db, a.log),
51+
v2.PostIncidentExtractHandler(a.db, a.log))
4852

4953
v2API.GET("availability", v2.GetComponentsAvailabilityHandler(a.db, a.log))
50-
51-
//nolint:gocritic
52-
//v2API.GET("/separate/<incident_id>/<component_id>") - > investigate it!!!
5354
}
5455

5556
rssFEED := a.r.Group("rss")

internal/api/v2/statuses.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var maintenanceStatuses = map[string]struct{}{
1616

1717
// Incident actions for opened incidents.
1818
const (
19+
IncidentDetected = "detected" // not implemented yet
1920
IncidentAnalysing = "analysing"
2021
IncidentFixing = "fixing"
2122
IncidentImpactChanged = "impact changed"
@@ -25,6 +26,7 @@ const (
2526

2627
//nolint:gochecknoglobals
2728
var incidentOpenStatuses = map[string]struct{}{
29+
IncidentDetected: {},
2830
IncidentAnalysing: {},
2931
IncidentFixing: {},
3032
IncidentImpactChanged: {},

internal/api/v2/v2.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,71 @@ func updateFields(income *PatchIncidentData, stored *db.Incident) {
440440
}
441441
}
442442

443+
type PostIncidentSeparateData struct {
444+
Components []int `json:"components" binding:"required,min=1"`
445+
}
446+
447+
func PostIncidentExtractHandler(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc { //nolint:gocognit
448+
return func(c *gin.Context) {
449+
logger.Debug("start to extract components to the new incident")
450+
451+
var incID IncidentID
452+
if err := c.ShouldBindUri(&incID); err != nil {
453+
apiErrors.RaiseBadRequestErr(c, err)
454+
return
455+
}
456+
457+
var incData PostIncidentSeparateData
458+
if err := c.ShouldBindBodyWithJSON(&incData); err != nil {
459+
apiErrors.RaiseBadRequestErr(c, err)
460+
return
461+
}
462+
463+
logger.Debug(
464+
"extract components from the incident",
465+
zap.Any("components", incData.Components),
466+
zap.Int("incident_id", incID.ID),
467+
)
468+
469+
storedInc, err := dbInst.GetIncident(incID.ID)
470+
if err != nil {
471+
apiErrors.RaiseInternalErr(c, err)
472+
return
473+
}
474+
475+
var movedComponents []db.Component
476+
var movedCounter int
477+
for _, incCompID := range incData.Components {
478+
present := false
479+
for _, storedComp := range storedInc.Components {
480+
if incCompID == int(storedComp.ID) {
481+
present = true
482+
movedComponents = append(movedComponents, storedComp)
483+
movedCounter++
484+
break
485+
}
486+
}
487+
if !present {
488+
apiErrors.RaiseBadRequestErr(c, fmt.Errorf("component %d is not in the incident", incCompID))
489+
return
490+
}
491+
}
492+
493+
if movedCounter == len(storedInc.Components) {
494+
apiErrors.RaiseBadRequestErr(c, fmt.Errorf("can not move all components to the new incident, keep at least one"))
495+
return
496+
}
497+
498+
inc, err := dbInst.ExtractComponentsToNewIncident(movedComponents, storedInc, *storedInc.Impact, *storedInc.Text)
499+
if err != nil {
500+
apiErrors.RaiseInternalErr(c, err)
501+
return
502+
}
503+
504+
c.JSON(http.StatusOK, toAPIIncident(inc))
505+
}
506+
}
507+
443508
type Component struct {
444509
ComponentID
445510
Attributes []ComponentAttribute `json:"attributes"`

internal/db/db.go

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ func (db *DB) GetIncident(id int) (*Incident, error) {
7676
Where(inc).
7777
Preload("Statuses").
7878
Preload("Components", func(db *gorm.DB) *gorm.DB {
79-
return db.Select("ID")
79+
return db.Select("ID, Name")
8080
}).
81+
Preload("Components.Attrs").
8182
First(&inc)
8283

8384
if r.Error != nil {
@@ -378,9 +379,13 @@ func (db *DB) MoveComponentFromOldToAnotherIncident(
378379
return incNew, nil
379380
}
380381

381-
func (db *DB) ExtractComponentToNewIncident(
382-
comp *Component, incOld *Incident, impact int, text string,
382+
func (db *DB) ExtractComponentsToNewIncident(
383+
comp []Component, incOld *Incident, impact int, text string,
383384
) (*Incident, error) {
385+
if len(comp) == 0 {
386+
return nil, fmt.Errorf("no components to extract")
387+
}
388+
384389
timeNow := time.Now().UTC()
385390

386391
inc := &Incident{
@@ -390,39 +395,43 @@ func (db *DB) ExtractComponentToNewIncident(
390395
Impact: &impact,
391396
Statuses: []IncidentStatus{},
392397
System: false,
393-
Components: []Component{*comp},
398+
Components: comp,
394399
}
395400

396401
id, err := db.SaveIncident(inc)
397402
if err != nil {
398403
return nil, err
399404
}
400405

401-
incText := fmt.Sprintf("%s moved from %s", comp.PrintAttrs(), incOld.Link())
402-
inc.Statuses = append(inc.Statuses, IncidentStatus{
403-
IncidentID: id,
404-
Status: statusSYSTEM,
405-
Text: incText,
406-
Timestamp: timeNow,
407-
})
406+
for _, c := range comp {
407+
incText := fmt.Sprintf("%s moved from %s", c.PrintAttrs(), incOld.Link())
408+
inc.Statuses = append(inc.Statuses, IncidentStatus{
409+
IncidentID: id,
410+
Status: statusSYSTEM,
411+
Text: incText,
412+
Timestamp: timeNow,
413+
})
414+
}
408415

409416
err = db.ModifyIncident(inc)
410417
if err != nil {
411418
return nil, err
412419
}
413420

414-
err = db.g.Model(incOld).Association("Components").Delete(comp)
415-
if err != nil {
416-
return nil, err
417-
}
421+
for _, c := range comp {
422+
err = db.g.Model(incOld).Association("Components").Delete(c)
423+
if err != nil {
424+
return nil, err
425+
}
418426

419-
incText = fmt.Sprintf("%s moved to %s", comp.PrintAttrs(), inc.Link())
420-
incOld.Statuses = append(incOld.Statuses, IncidentStatus{
421-
IncidentID: inc.ID,
422-
Status: statusSYSTEM,
423-
Text: incText,
424-
Timestamp: timeNow,
425-
})
427+
incText := fmt.Sprintf("%s moved to %s", c.PrintAttrs(), inc.Link())
428+
incOld.Statuses = append(incOld.Statuses, IncidentStatus{
429+
IncidentID: inc.ID,
430+
Status: statusSYSTEM,
431+
Text: incText,
432+
Timestamp: timeNow,
433+
})
434+
}
426435

427436
err = db.ModifyIncident(incOld)
428437
if err != nil {

openapi.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,36 @@ paths:
313313
description: Invalid ID supplied
314314
'404':
315315
description: Incident not found.
316+
/v2/incidents/{incident_id}/extract:
317+
post:
318+
summary: extract components to the new incident
319+
tags:
320+
- incidents
321+
parameters:
322+
- name: incident_id
323+
in: path
324+
description: ID of incident to return
325+
required: true
326+
schema:
327+
type: integer
328+
format: int64
329+
requestBody:
330+
content:
331+
application/json:
332+
schema:
333+
$ref: '#/components/schemas/IncidentPostExtract'
334+
required: true
335+
responses:
336+
'200':
337+
description: successful operation, return the new incident id
338+
content:
339+
application/json:
340+
schema:
341+
$ref: '#/components/schemas/Incident'
342+
'400':
343+
description: Invalid ID supplied
344+
'404':
345+
description: Incident not found.
316346

317347
/v1/component_status:
318348
get:
@@ -597,6 +627,16 @@ components:
597627
end_date:
598628
type: string
599629
format: date-time
630+
IncidentPostExtract:
631+
type: object
632+
required:
633+
- components
634+
properties:
635+
components:
636+
type: array
637+
items:
638+
type: string
639+
example: [ 218, 254 ]
600640
IncidentStatus:
601641
type: object
602642
allOf:

tests/main_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,7 @@ func initRoutesV2(t *testing.T, c *gin.Engine, dbInst *db.DB, logger *zap.Logger
136136
v2Api.POST("incidents", api.ValidateComponentsMW(dbInst, logger), v2.PostIncidentHandler(dbInst, logger))
137137
v2Api.GET("incidents/:id", v2.GetIncidentHandler(dbInst, logger))
138138
v2Api.PATCH("incidents/:id", v2.PatchIncidentHandler(dbInst, logger))
139+
v2Api.POST("incidents/:id/extract", v2.PostIncidentExtractHandler(dbInst, logger))
140+
139141
v2Api.GET("availability", v2.GetComponentsAvailabilityHandler(dbInst, logger))
140142
}

0 commit comments

Comments
 (0)