From 0d0355b6445b3a5eaa7dec34502d5e5d8eb9c48e Mon Sep 17 00:00:00 2001 From: Samuel Lucidi Date: Wed, 2 Aug 2023 22:41:02 -0400 Subject: [PATCH] :warning: Separate Target and RuleSet models/apis (#464) Signed-off-by: Sam Lucidi --- api/analysis.go | 4 +- api/pkg.go | 1 + api/ruleset.go | 69 ++++- api/target.go | 306 ++++++++++++++++++++++ auth/roles.yaml | 20 +- docs/docs.go | 471 +++++++++++++++++++++++++++++----- docs/swagger.json | 471 +++++++++++++++++++++++++++++----- docs/swagger.yaml | 359 ++++++++++++++++++++++---- go.mod | 2 +- go.sum | 4 +- migration/pkg.go | 2 + migration/v8/migrate.go | 76 ++++++ migration/v8/model/pkg.go | 82 ++++++ migration/v8/model/ruleset.go | 69 +++++ migration/v8/model/target.go | 20 ++ model/pkg.go | 3 +- reaper/file.go | 1 + seed/ruleset.go | 14 +- seed/seed.go | 7 + seed/target.go | 147 +++++++++++ 20 files changed, 1913 insertions(+), 215 deletions(-) create mode 100644 api/target.go create mode 100644 migration/v8/migrate.go create mode 100644 migration/v8/model/pkg.go create mode 100644 migration/v8/model/ruleset.go create mode 100644 migration/v8/model/target.go create mode 100644 seed/target.go diff --git a/api/analysis.go b/api/analysis.go index 3c032bfdc..9bfc1fe75 100644 --- a/api/analysis.go +++ b/api/analysis.go @@ -995,7 +995,7 @@ func (h AnalysisHandler) AppIssueReports(ctx *gin.Context) { // @description - files // @tags issueappreports // @produce json -// @success 200 {object} []api.AppReport +// @success 200 {object} []api.IssueAppReport // @router /analyses/report/applications [get] func (h AnalysisHandler) IssueAppReports(ctx *gin.Context) { resources := []IssueAppReport{} @@ -1448,7 +1448,7 @@ func (h AnalysisHandler) DepReports(ctx *gin.Context) { // @description - indirect // @tags depappreports // @produce json -// @success 200 {object} []api.AppReport +// @success 200 {object} []api.DepAppReport // @router /analyses/report/applications [get] func (h AnalysisHandler) DepAppReports(ctx *gin.Context) { resources := []DepAppReport{} diff --git a/api/pkg.go b/api/pkg.go index a72b3cc0f..b5b3e7b0f 100644 --- a/api/pkg.go +++ b/api/pkg.go @@ -86,6 +86,7 @@ func All() []Handler { &FileHandler{}, &MigrationWaveHandler{}, &BatchHandler{}, + &TargetHandler{}, } } diff --git a/api/ruleset.go b/api/ruleset.go index 47ac0e7dd..1ca7832b2 100644 --- a/api/ruleset.go +++ b/api/ruleset.go @@ -3,7 +3,9 @@ package api import ( "encoding/json" "github.com/gin-gonic/gin" + qf "github.com/konveyor/tackle2-hub/api/filter" "github.com/konveyor/tackle2-hub/model" + "gorm.io/gorm" "gorm.io/gorm/clause" "net/http" ) @@ -61,6 +63,8 @@ func (h RuleSetHandler) Get(ctx *gin.Context) { // List godoc // @summary List all bindings. // @description List all bindings. +// @description filters: +// @description - labels // @tags rulesets // @produce json // @success 200 {object} []RuleSet @@ -71,7 +75,17 @@ func (h RuleSetHandler) List(ctx *gin.Context) { h.DB(ctx), clause.Associations, "Rules.File") - result := db.Find(&list) + + filter, err := qf.New(ctx, + []qf.Assert{ + {Field: "name", Kind: qf.STRING}, + {Field: "labels", Kind: qf.STRING}, + }) + if err != nil { + _ = ctx.Error(err) + return + } + result := db.Where("ID IN (?)", h.ruleSetIDs(ctx, filter)).Find(&list) if result.Error != nil { _ = ctx.Error(result.Error) return @@ -137,6 +151,10 @@ func (h RuleSetHandler) Delete(ctx *gin.Context) { _ = ctx.Error(result.Error) return } + if ruleset.Builtin() { + h.Status(ctx, http.StatusForbidden) + return + } result = h.DB(ctx).Delete(ruleset, id) if result.Error != nil { _ = ctx.Error(result.Error) @@ -163,8 +181,6 @@ func (h RuleSetHandler) Update(ctx *gin.Context) { _ = ctx.Error(err) return } - // - // Delete unwanted ruleSets. m := &model.RuleSet{} db := h.preLoad(h.DB(ctx), clause.Associations) result := db.First(m, id) @@ -172,6 +188,12 @@ func (h RuleSetHandler) Update(ctx *gin.Context) { _ = ctx.Error(result.Error) return } + if m.Builtin() { + h.Status(ctx, http.StatusForbidden) + return + } + // + // Delete unwanted rules. for _, ruleset := range m.Rules { if !r.HasRule(ruleset.ID) { err := h.DB(ctx).Delete(ruleset).Error @@ -218,6 +240,37 @@ func (h RuleSetHandler) Update(ctx *gin.Context) { h.Status(ctx, http.StatusNoContent) } +func (h *RuleSetHandler) ruleSetIDs(ctx *gin.Context, f qf.Filter) (q *gorm.DB) { + q = h.DB(ctx) + q = q.Model(&model.RuleSet{}) + q = q.Select("ID") + q = f.Where(q, "-Labels") + filter := f + if f, found := filter.Field("labels"); found { + if f.Value.Operator(qf.AND) { + var qs []*gorm.DB + for _, f = range f.Expand() { + f = f.As("json_each.value") + iq := h.DB(ctx) + iq = iq.Table("Rule") + iq = iq.Joins("m ,json_each(Labels)") + iq = iq.Select("m.RuleSetID") + qs = append(qs, iq) + } + q = q.Where("ID IN (?)", model.Intersect(qs...)) + } else { + f = f.As("json_each.value") + iq := h.DB(ctx) + iq = iq.Table("Rule") + iq = iq.Joins("m ,json_each(Labels)") + iq = iq.Select("m.RuleSetID") + iq = f.Where(iq) + q = q.Where("ID IN (?)", iq) + } + } + return +} + // // RuleSet REST resource. type RuleSet struct { @@ -225,9 +278,7 @@ type RuleSet struct { Kind string `json:"kind,omitempty"` Name string `json:"name"` Description string `json:"description"` - Image Ref `json:"image"` Rules []Rule `json:"rules"` - Custom bool `json:"custom,omitempty"` Repository *Repository `json:"repository,omitempty"` Identity *Ref `json:"identity,omitempty"` DependsOn []Ref `json:"dependsOn"` @@ -240,13 +291,7 @@ func (r *RuleSet) With(m *model.RuleSet) { r.Kind = m.Kind r.Name = m.Name r.Description = m.Description - r.Custom = m.Custom r.Identity = r.refPtr(m.IdentityID, m.Identity) - imgRef := Ref{ID: m.ImageID} - if m.Image != nil { - imgRef.Name = m.Image.Name - } - r.Image = imgRef _ = json.Unmarshal(m.Repository, &r.Repository) r.Rules = []Rule{} for i := range m.Rules { @@ -271,10 +316,8 @@ func (r *RuleSet) Model() (m *model.RuleSet) { Kind: r.Kind, Name: r.Name, Description: r.Description, - Custom: r.Custom, } m.ID = r.ID - m.ImageID = r.Image.ID m.IdentityID = r.idPtr(r.Identity) m.Rules = []model.Rule{} for _, rule := range r.Rules { diff --git a/api/target.go b/api/target.go new file mode 100644 index 000000000..c963091d8 --- /dev/null +++ b/api/target.go @@ -0,0 +1,306 @@ +package api + +import ( + "encoding/json" + "github.com/gin-gonic/gin" + "github.com/konveyor/tackle2-hub/model" + "gorm.io/gorm/clause" + "net/http" +) + +// +// Routes +const ( + TargetsRoot = "/targets" + TargetRoot = TargetsRoot + "/:" + ID +) + +// +// TargetHandler handles Target resource routes. +type TargetHandler struct { + BaseHandler +} + +func (h TargetHandler) AddRoutes(e *gin.Engine) { + routeGroup := e.Group("/") + routeGroup.Use(Required("targets"), Transaction) + routeGroup.GET(TargetsRoot, h.List) + routeGroup.GET(TargetsRoot+"/", h.List) + routeGroup.POST(TargetsRoot, h.Create) + routeGroup.GET(TargetRoot, h.Get) + routeGroup.PUT(TargetRoot, h.Update) + routeGroup.DELETE(TargetRoot, h.Delete) +} + +// Get godoc +// @summary Get a Target by ID. +// @description Get a Target by ID. +// @tags targets +// @produce json +// @success 200 {object} Target +// @router /targets/{id} [get] +// @param id path string true "Target ID" +func (h TargetHandler) Get(ctx *gin.Context) { + id := h.pk(ctx) + target := &model.Target{} + db := h.preLoad(h.DB(ctx), clause.Associations, "RuleSet.Rules", "RuleSet.Rules.File") + result := db.First(target, id) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + r := Target{} + r.With(target) + + h.Respond(ctx, http.StatusOK, r) +} + +// List godoc +// @summary List all targets. +// @description List all targets. +// @tags targets +// @produce json +// @success 200 {object} []Target +// @router /targets [get] +func (h TargetHandler) List(ctx *gin.Context) { + var list []model.Target + db := h.preLoad(h.DB(ctx), clause.Associations, "RuleSet.Rules", "RuleSet.Rules.File") + result := db.Find(&list) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + resources := []Target{} + for i := range list { + r := Target{} + r.With(&list[i]) + resources = append(resources, r) + } + + h.Respond(ctx, http.StatusOK, resources) +} + +// Create godoc +// @summary Create a target. +// @description Create a target. +// @tags targets +// @accept json +// @produce json +// @success 201 {object} Target +// @router /targets [post] +// @param target body Target true "Target data" +func (h TargetHandler) Create(ctx *gin.Context) { + target := &Target{} + err := h.Bind(ctx, target) + if err != nil { + return + } + + rs := target.RuleSet.Model() + rs.CreateUser = h.BaseHandler.CurrentUser(ctx) + result := h.DB(ctx).Create(rs) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + + m := target.Model() + m.CreateUser = h.BaseHandler.CurrentUser(ctx) + m.RuleSetID = &rs.ID + result = h.DB(ctx).Create(m) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + + db := h.preLoad(h.DB(ctx), clause.Associations, "RuleSet.Rules", "RuleSet.Rules.File") + result = db.First(m) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + target.With(m) + + h.Respond(ctx, http.StatusCreated, target) +} + +// Delete godoc +// @summary Delete a target. +// @description Delete a target. +// @tags targets +// @success 204 +// @router /targets/{id} [delete] +// @param id path string true "Target ID" +func (h TargetHandler) Delete(ctx *gin.Context) { + id := h.pk(ctx) + target := &model.Target{} + result := h.DB(ctx).First(target, id) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + if target.Builtin() { + h.Status(ctx, http.StatusForbidden) + return + } + result = h.DB(ctx).Delete(target, id) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + if target.RuleSetID != nil { + result = h.DB(ctx).Delete(&model.RuleSet{}, target.RuleSetID) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + } + + h.Status(ctx, http.StatusNoContent) +} + +// Update godoc +// @summary Update a target. +// @description Update a target. +// @tags targets +// @accept json +// @success 204 +// @router /targets/{id} [put] +// @param id path string true "Target ID" +// @param target body Target true "Target data" +func (h TargetHandler) Update(ctx *gin.Context) { + id := h.pk(ctx) + r := &Target{} + err := h.Bind(ctx, r) + if err != nil { + _ = ctx.Error(err) + return + } + + m := &model.Target{} + db := h.preLoad(h.DB(ctx), clause.Associations, "RuleSet.Rules", "RuleSet.Rules.File") + result := db.First(m, id) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + if m.Builtin() { + h.Status(ctx, http.StatusForbidden) + return + } + + rsid := m.RuleSetID + if rsid != nil { + rs := r.RuleSet.Model() + rs.ID = *m.RuleSetID + // + // Delete unwanted rules. + for _, ruleset := range rs.Rules { + if !r.RuleSet.HasRule(ruleset.ID) { + err := h.DB(ctx).Delete(ruleset).Error + if err != nil { + _ = ctx.Error(err) + return + } + } + } + rs.UpdateUser = h.BaseHandler.CurrentUser(ctx) + db = h.DB(ctx).Model(rs) + db = db.Omit(clause.Associations) + result = db.Updates(h.fields(rs)) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + err = h.DB(ctx).Model(rs).Association("DependsOn").Replace(rs.DependsOn) + if err != nil { + _ = ctx.Error(err) + return + } + err = h.DB(ctx).Model(rs).Association("Rules").Replace(rs.Rules) + if err != nil { + _ = ctx.Error(err) + return + } + // + // Update ruleSets. + for i := range rs.Rules { + rule := &rs.Rules[i] + db = h.DB(ctx).Model(rule) + err = db.Updates(h.fields(rule)).Error + if err != nil { + _ = ctx.Error(err) + return + } + } + } + + // + // Update target. + m = r.Model() + m.ID = id + m.RuleSetID = rsid + m.UpdateUser = h.BaseHandler.CurrentUser(ctx) + db = h.DB(ctx).Model(m) + db = db.Omit(clause.Associations) + result = db.Updates(h.fields(m)) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + + h.Status(ctx, http.StatusNoContent) +} + +// +// Target REST resource. +type Target struct { + Resource + Name string `json:"name"` + Description string `json:"description"` + Choice bool `json:"choice,omitempty"` + Custom bool `json:"custom,omitempty"` + Labels []Label `json:"labels"` + Image Ref `json:"image"` + RuleSet *RuleSet `json:"ruleset,omitempty"` +} + +type Label struct { + Name string `json:"name"` + Label string `json:"label"` +} + +// +// With updates the resource with the model. +func (r *Target) With(m *model.Target) { + r.Resource.With(&m.Model) + r.Name = m.Name + r.Description = m.Description + r.Choice = m.Choice + r.Custom = !m.Builtin() + if m.RuleSet != nil { + r.RuleSet = &RuleSet{} + r.RuleSet.With(m.RuleSet) + } + imgRef := Ref{ID: m.ImageID} + if m.Image != nil { + imgRef.Name = m.Image.Name + } + r.Image = imgRef + _ = json.Unmarshal(m.Labels, &r.Labels) +} + +// +// Model builds a model. +func (r *Target) Model() (m *model.Target) { + m = &model.Target{ + Name: r.Name, + Description: r.Description, + Choice: r.Choice, + } + m.ID = r.ID + m.ImageID = r.Image.ID + m.Labels, _ = json.Marshal(r.Labels) + + return +} diff --git a/auth/roles.yaml b/auth/roles.yaml index 60a7f90c6..7b9bfd7e1 100644 --- a/auth/roles.yaml +++ b/auth/roles.yaml @@ -167,6 +167,12 @@ - get - post - put + - name: targets + verbs: + - delete + - get + - post + - put - role: tackle-architect resources: - name: addons @@ -323,6 +329,12 @@ - get - post - put + - name: targets + verbs: + - delete + - get + - post + - put - role: tackle-migrator resources: - name: addons @@ -421,6 +433,9 @@ - name: migrationwaves verbs: - get + - name: targets + verb: + - get - role: tackle-project-manager resources: - name: addons @@ -512,4 +527,7 @@ - delete - get - post - - put \ No newline at end of file + - put + - name: targets + verbs: + - get \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 8530a2f4b..d7fa02cf5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -122,7 +122,7 @@ const docTemplate = `{ }, "/analyses/dependencies": { "get": { - "description": "Each report collates dependencies by name and SHA.\nfilters:\n- name\n- version\n- sha\n- indirect\n- labels\n- application.id\n- application.name\n- tag.id\nsort:\n- name\n- version\n- sha", + "description": "Each report collates dependencies by name and SHA.\nfilters:\n- provider\n- name\n- version\n- sha\n- indirect\n- labels\n- application.id\n- application.name\n- businessService.id\n- businessService.name\n- tag.id\nsort:\n- provider\n- name\n- version\n- sha", "produces": [ "application/json" ], @@ -211,12 +211,12 @@ const docTemplate = `{ }, "/analyses/report/applications": { "get": { - "description": "List application reports.\nfilters:\n- id\n- name\n- description\n- businessService\n- effort\n- incidents\n- files\n- issue.id\n- issue.name\n- issue.ruleset\n- issue.rule\n- issue.category\n- issue.effort\n- issue.labels\n- application.id\n- application.name\n- businessService.name\nsort:\n- id\n- name\n- description\n- businessService\n- effort\n- incidents\n- files", + "description": "List application reports.\nfilters:\n- id\n- name\n- description\n- businessService\n- provider\n- name\n- version\n- sha\n- indirect\n- dep.provider\n- dep.name\n- dep.version\n- dep.sha\n- dep.indirect\n- dep.labels\n- application.id\n- application.name\n- businessService.id\n- businessService.name\nsort:\n- name\n- description\n- businessService\n- provider\n- name\n- version\n- sha\n- indirect", "produces": [ "application/json" ], "tags": [ - "appreports" + "depappreports" ], "summary": "List application reports.", "responses": { @@ -225,7 +225,39 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/api.AppReport" + "$ref": "#/definitions/api.DepAppReport" + } + } + } + } + } + }, + "/analyses/report/applications/{id}/issues": { + "get": { + "description": "Each report collates issues by ruleset/rule.\nfilters:\n- ruleset\n- rule\n- category\n- effort\n- labels\nsort:\n- ruleset\n- rule\n- category\n- effort\n- files", + "produces": [ + "application/json" + ], + "tags": [ + "issuereport" + ], + "summary": "List application issue reports.", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api.IssueReport" } } } @@ -257,7 +289,7 @@ const docTemplate = `{ }, "/analyses/report/rules": { "get": { - "description": "Each report collates issues by ruleset/rule.\nfilters:\n- ruleset\n- rule\n- category\n- effort\n- labels\n- applications\n- application.id\n- application.name\n- tag.id\nsort:\n- ruleset\n- rule\n- category\n- effort\n- applications", + "description": "Each report collates issues by ruleset/rule.\nfilters:\n- ruleset\n- rule\n- category\n- effort\n- labels\n- applications\n- application.id\n- application.name\n- businessService.id\n- businessService.name\n- tag.id\nsort:\n- ruleset\n- rule\n- category\n- effort\n- applications", "produces": [ "application/json" ], @@ -3608,6 +3640,144 @@ const docTemplate = `{ } } }, + "/targets": { + "get": { + "description": "List all targets.", + "produces": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "List all targets.", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Target" + } + } + } + } + }, + "post": { + "description": "Create a target.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "Create a target.", + "parameters": [ + { + "description": "Target data", + "name": "target", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.Target" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.Target" + } + } + } + } + }, + "/targets/{id}": { + "get": { + "description": "Get a Target by ID.", + "produces": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "Get a Target by ID.", + "parameters": [ + { + "type": "string", + "description": "Target ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.Target" + } + } + } + }, + "put": { + "description": "Update a target.", + "consumes": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "Update a target.", + "parameters": [ + { + "type": "string", + "description": "Target ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Target data", + "name": "target", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.Target" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "description": "Delete a target.", + "tags": [ + "targets" + ], + "summary": "Delete a target.", + "parameters": [ + { + "type": "string", + "description": "Target ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/taskgroups": { "get": { "description": "List all task groups.", @@ -4637,49 +4807,6 @@ const docTemplate = `{ } } }, - "api.AppReport": { - "type": "object", - "properties": { - "businessService": { - "type": "string" - }, - "description": { - "type": "string" - }, - "effort": { - "type": "integer" - }, - "files": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "incidents": { - "type": "integer" - }, - "issue": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "rule": { - "type": "string" - }, - "ruleset": { - "type": "string" - } - } - }, - "name": { - "type": "string" - } - } - }, "api.Application": { "type": "object", "required": [ @@ -4835,6 +4962,52 @@ const docTemplate = `{ } } }, + "api.DepAppReport": { + "type": "object", + "properties": { + "businessService": { + "type": "string" + }, + "dependency": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "indirect": { + "type": "boolean" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "api.Dependency": { "type": "object", "properties": { @@ -5099,6 +5272,93 @@ const docTemplate = `{ } } }, + "api.IssueAppReport": { + "type": "object", + "properties": { + "businessService": { + "type": "string" + }, + "description": { + "type": "string" + }, + "effort": { + "type": "integer" + }, + "files": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "incidents": { + "type": "integer" + }, + "issue": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "ruleset": { + "type": "string" + } + } + }, + "name": { + "type": "string" + } + } + }, + "api.IssueReport": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "effort": { + "type": "integer" + }, + "files": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Link" + } + }, + "name": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "ruleset": { + "type": "string" + } + } + }, "api.IssueType": { "type": "object", "properties": { @@ -5139,6 +5399,17 @@ const docTemplate = `{ } } }, + "api.Label": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "api.Link": { "type": "object", "properties": { @@ -5357,7 +5628,12 @@ const docTemplate = `{ "id": { "type": "integer" }, - "labels": {}, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, "name": { "type": "string" }, @@ -5387,6 +5663,12 @@ const docTemplate = `{ "type": "string" } }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Link" + } + }, "name": { "type": "string" }, @@ -5407,11 +5689,11 @@ const docTemplate = `{ "createUser": { "type": "string" }, - "custom": { - "type": "boolean" - }, - "description": { - "type": "string" + "dependsOn": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Ref" + } }, "id": { "type": "integer" @@ -5419,9 +5701,6 @@ const docTemplate = `{ "identity": { "$ref": "#/definitions/api.Ref" }, - "image": { - "$ref": "#/definitions/api.Ref" - }, "kind": { "type": "string" }, @@ -5682,6 +5961,47 @@ const docTemplate = `{ } } }, + "api.Target": { + "type": "object", + "properties": { + "choice": { + "type": "boolean" + }, + "createTime": { + "type": "string" + }, + "createUser": { + "type": "string" + }, + "custom": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "image": { + "$ref": "#/definitions/api.Ref" + }, + "labels": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Label" + } + }, + "name": { + "type": "string" + }, + "ruleset": { + "$ref": "#/definitions/api.RuleSet" + }, + "updateUser": { + "type": "string" + } + } + }, "api.Task": { "type": "object", "required": [ @@ -5689,6 +6009,12 @@ const docTemplate = `{ "data" ], "properties": { + "activity": { + "type": "array", + "items": { + "type": "string" + } + }, "addon": { "type": "string" }, @@ -5710,8 +6036,11 @@ const docTemplate = `{ "data": { "type": "object" }, - "error": { - "type": "string" + "errors": { + "type": "array", + "items": { + "$ref": "#/definitions/api.TaskError" + } }, "id": { "type": "integer" @@ -5737,9 +6066,6 @@ const docTemplate = `{ "purged": { "type": "boolean" }, - "report": { - "$ref": "#/definitions/api.TaskReport" - }, "retries": { "type": "integer" }, @@ -5763,6 +6089,17 @@ const docTemplate = `{ } } }, + "api.TaskError": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "severity": { + "type": "string" + } + } + }, "api.TaskGroup": { "type": "object", "required": [ @@ -5822,8 +6159,11 @@ const docTemplate = `{ "createUser": { "type": "string" }, - "error": { - "type": "string" + "errors": { + "type": "array", + "items": { + "$ref": "#/definitions/api.TaskError" + } }, "id": { "type": "integer" @@ -5872,6 +6212,9 @@ const docTemplate = `{ "name": { "type": "string" }, + "provider": { + "type": "string" + }, "sha": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 1a7a828b8..063b466aa 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -110,7 +110,7 @@ }, "/analyses/dependencies": { "get": { - "description": "Each report collates dependencies by name and SHA.\nfilters:\n- name\n- version\n- sha\n- indirect\n- labels\n- application.id\n- application.name\n- tag.id\nsort:\n- name\n- version\n- sha", + "description": "Each report collates dependencies by name and SHA.\nfilters:\n- provider\n- name\n- version\n- sha\n- indirect\n- labels\n- application.id\n- application.name\n- businessService.id\n- businessService.name\n- tag.id\nsort:\n- provider\n- name\n- version\n- sha", "produces": [ "application/json" ], @@ -199,12 +199,12 @@ }, "/analyses/report/applications": { "get": { - "description": "List application reports.\nfilters:\n- id\n- name\n- description\n- businessService\n- effort\n- incidents\n- files\n- issue.id\n- issue.name\n- issue.ruleset\n- issue.rule\n- issue.category\n- issue.effort\n- issue.labels\n- application.id\n- application.name\n- businessService.name\nsort:\n- id\n- name\n- description\n- businessService\n- effort\n- incidents\n- files", + "description": "List application reports.\nfilters:\n- id\n- name\n- description\n- businessService\n- provider\n- name\n- version\n- sha\n- indirect\n- dep.provider\n- dep.name\n- dep.version\n- dep.sha\n- dep.indirect\n- dep.labels\n- application.id\n- application.name\n- businessService.id\n- businessService.name\nsort:\n- name\n- description\n- businessService\n- provider\n- name\n- version\n- sha\n- indirect", "produces": [ "application/json" ], "tags": [ - "appreports" + "depappreports" ], "summary": "List application reports.", "responses": { @@ -213,7 +213,39 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/api.AppReport" + "$ref": "#/definitions/api.DepAppReport" + } + } + } + } + } + }, + "/analyses/report/applications/{id}/issues": { + "get": { + "description": "Each report collates issues by ruleset/rule.\nfilters:\n- ruleset\n- rule\n- category\n- effort\n- labels\nsort:\n- ruleset\n- rule\n- category\n- effort\n- files", + "produces": [ + "application/json" + ], + "tags": [ + "issuereport" + ], + "summary": "List application issue reports.", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api.IssueReport" } } } @@ -245,7 +277,7 @@ }, "/analyses/report/rules": { "get": { - "description": "Each report collates issues by ruleset/rule.\nfilters:\n- ruleset\n- rule\n- category\n- effort\n- labels\n- applications\n- application.id\n- application.name\n- tag.id\nsort:\n- ruleset\n- rule\n- category\n- effort\n- applications", + "description": "Each report collates issues by ruleset/rule.\nfilters:\n- ruleset\n- rule\n- category\n- effort\n- labels\n- applications\n- application.id\n- application.name\n- businessService.id\n- businessService.name\n- tag.id\nsort:\n- ruleset\n- rule\n- category\n- effort\n- applications", "produces": [ "application/json" ], @@ -3596,6 +3628,144 @@ } } }, + "/targets": { + "get": { + "description": "List all targets.", + "produces": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "List all targets.", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Target" + } + } + } + } + }, + "post": { + "description": "Create a target.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "Create a target.", + "parameters": [ + { + "description": "Target data", + "name": "target", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.Target" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.Target" + } + } + } + } + }, + "/targets/{id}": { + "get": { + "description": "Get a Target by ID.", + "produces": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "Get a Target by ID.", + "parameters": [ + { + "type": "string", + "description": "Target ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.Target" + } + } + } + }, + "put": { + "description": "Update a target.", + "consumes": [ + "application/json" + ], + "tags": [ + "targets" + ], + "summary": "Update a target.", + "parameters": [ + { + "type": "string", + "description": "Target ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Target data", + "name": "target", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.Target" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "description": "Delete a target.", + "tags": [ + "targets" + ], + "summary": "Delete a target.", + "parameters": [ + { + "type": "string", + "description": "Target ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/taskgroups": { "get": { "description": "List all task groups.", @@ -4625,49 +4795,6 @@ } } }, - "api.AppReport": { - "type": "object", - "properties": { - "businessService": { - "type": "string" - }, - "description": { - "type": "string" - }, - "effort": { - "type": "integer" - }, - "files": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "incidents": { - "type": "integer" - }, - "issue": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "rule": { - "type": "string" - }, - "ruleset": { - "type": "string" - } - } - }, - "name": { - "type": "string" - } - } - }, "api.Application": { "type": "object", "required": [ @@ -4823,6 +4950,52 @@ } } }, + "api.DepAppReport": { + "type": "object", + "properties": { + "businessService": { + "type": "string" + }, + "dependency": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "indirect": { + "type": "boolean" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "api.Dependency": { "type": "object", "properties": { @@ -5087,6 +5260,93 @@ } } }, + "api.IssueAppReport": { + "type": "object", + "properties": { + "businessService": { + "type": "string" + }, + "description": { + "type": "string" + }, + "effort": { + "type": "integer" + }, + "files": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "incidents": { + "type": "integer" + }, + "issue": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "ruleset": { + "type": "string" + } + } + }, + "name": { + "type": "string" + } + } + }, + "api.IssueReport": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "effort": { + "type": "integer" + }, + "files": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Link" + } + }, + "name": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "ruleset": { + "type": "string" + } + } + }, "api.IssueType": { "type": "object", "properties": { @@ -5127,6 +5387,17 @@ } } }, + "api.Label": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "api.Link": { "type": "object", "properties": { @@ -5345,7 +5616,12 @@ "id": { "type": "integer" }, - "labels": {}, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, "name": { "type": "string" }, @@ -5375,6 +5651,12 @@ "type": "string" } }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Link" + } + }, "name": { "type": "string" }, @@ -5395,11 +5677,11 @@ "createUser": { "type": "string" }, - "custom": { - "type": "boolean" - }, - "description": { - "type": "string" + "dependsOn": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Ref" + } }, "id": { "type": "integer" @@ -5407,9 +5689,6 @@ "identity": { "$ref": "#/definitions/api.Ref" }, - "image": { - "$ref": "#/definitions/api.Ref" - }, "kind": { "type": "string" }, @@ -5670,6 +5949,47 @@ } } }, + "api.Target": { + "type": "object", + "properties": { + "choice": { + "type": "boolean" + }, + "createTime": { + "type": "string" + }, + "createUser": { + "type": "string" + }, + "custom": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "image": { + "$ref": "#/definitions/api.Ref" + }, + "labels": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Label" + } + }, + "name": { + "type": "string" + }, + "ruleset": { + "$ref": "#/definitions/api.RuleSet" + }, + "updateUser": { + "type": "string" + } + } + }, "api.Task": { "type": "object", "required": [ @@ -5677,6 +5997,12 @@ "data" ], "properties": { + "activity": { + "type": "array", + "items": { + "type": "string" + } + }, "addon": { "type": "string" }, @@ -5698,8 +6024,11 @@ "data": { "type": "object" }, - "error": { - "type": "string" + "errors": { + "type": "array", + "items": { + "$ref": "#/definitions/api.TaskError" + } }, "id": { "type": "integer" @@ -5725,9 +6054,6 @@ "purged": { "type": "boolean" }, - "report": { - "$ref": "#/definitions/api.TaskReport" - }, "retries": { "type": "integer" }, @@ -5751,6 +6077,17 @@ } } }, + "api.TaskError": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "severity": { + "type": "string" + } + } + }, "api.TaskGroup": { "type": "object", "required": [ @@ -5810,8 +6147,11 @@ "createUser": { "type": "string" }, - "error": { - "type": "string" + "errors": { + "type": "array", + "items": { + "$ref": "#/definitions/api.TaskError" + } }, "id": { "type": "integer" @@ -5860,6 +6200,9 @@ "name": { "type": "string" }, + "provider": { + "type": "string" + }, "sha": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 13f9bd49b..252e07ad0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -27,34 +27,6 @@ definitions: updateUser: type: string type: object - api.AppReport: - properties: - businessService: - type: string - description: - type: string - effort: - type: integer - files: - type: integer - id: - type: integer - incidents: - type: integer - issue: - properties: - id: - type: integer - name: - type: string - rule: - type: string - ruleset: - type: string - type: object - name: - type: string - type: object api.Application: properties: binary: @@ -157,6 +129,36 @@ definitions: - sourceReview - targetApplications type: object + api.DepAppReport: + properties: + businessService: + type: string + dependency: + properties: + id: + type: integer + indirect: + type: boolean + labels: + items: + type: string + type: array + name: + type: string + provider: + type: string + rule: + type: string + version: + type: string + type: object + description: + type: string + id: + type: integer + name: + type: string + type: object api.Dependency: properties: createTime: @@ -333,6 +335,63 @@ definitions: - rule - ruleset type: object + api.IssueAppReport: + properties: + businessService: + type: string + description: + type: string + effort: + type: integer + files: + type: integer + id: + type: integer + incidents: + type: integer + issue: + properties: + description: + type: string + id: + type: integer + name: + type: string + rule: + type: string + ruleset: + type: string + type: object + name: + type: string + type: object + api.IssueReport: + properties: + category: + type: string + description: + type: string + effort: + type: integer + files: + type: integer + id: + type: integer + labels: + items: + type: string + type: array + links: + items: + $ref: '#/definitions/api.Link' + type: array + name: + type: string + rule: + type: string + ruleset: + type: string + type: object api.IssueType: properties: id: @@ -359,6 +418,13 @@ definitions: required: - name type: object + api.Label: + properties: + label: + type: string + name: + type: string + type: object api.Link: properties: title: @@ -502,7 +568,10 @@ definitions: $ref: '#/definitions/api.Ref' id: type: integer - labels: {} + labels: + items: + type: string + type: array name: type: string updateUser: @@ -522,6 +591,10 @@ definitions: items: type: string type: array + links: + items: + $ref: '#/definitions/api.Link' + type: array name: type: string rule: @@ -535,16 +608,14 @@ definitions: type: string createUser: type: string - custom: - type: boolean - description: - type: string + dependsOn: + items: + $ref: '#/definitions/api.Ref' + type: array id: type: integer identity: $ref: '#/definitions/api.Ref' - image: - $ref: '#/definitions/api.Ref' kind: type: string name: @@ -716,8 +787,39 @@ definitions: required: - id type: object + api.Target: + properties: + choice: + type: boolean + createTime: + type: string + createUser: + type: string + custom: + type: boolean + description: + type: string + id: + type: integer + image: + $ref: '#/definitions/api.Ref' + labels: + items: + $ref: '#/definitions/api.Label' + type: array + name: + type: string + ruleset: + $ref: '#/definitions/api.RuleSet' + updateUser: + type: string + type: object api.Task: properties: + activity: + items: + type: string + type: array addon: type: string application: @@ -732,8 +834,10 @@ definitions: type: string data: type: object - error: - type: string + errors: + items: + $ref: '#/definitions/api.TaskError' + type: array id: type: integer image: @@ -750,8 +854,6 @@ definitions: type: integer purged: type: boolean - report: - $ref: '#/definitions/api.TaskReport' retries: type: integer started: @@ -770,6 +872,13 @@ definitions: - addon - data type: object + api.TaskError: + properties: + description: + type: string + severity: + type: string + type: object api.TaskGroup: properties: addon: @@ -809,8 +918,10 @@ definitions: type: string createUser: type: string - error: - type: string + errors: + items: + $ref: '#/definitions/api.TaskError' + type: array id: type: integer result: @@ -840,6 +951,8 @@ definitions: type: array name: type: string + provider: + type: string sha: type: string updateUser: @@ -1034,6 +1147,7 @@ paths: description: |- Each report collates dependencies by name and SHA. filters: + - provider - name - version - sha @@ -1041,8 +1155,11 @@ paths: - labels - application.id - application.name + - businessService.id + - businessService.name - tag.id sort: + - provider - name - version - sha @@ -1124,27 +1241,64 @@ paths: - name - description - businessService - - effort - - incidents - - files - - issue.id - - issue.name - - issue.ruleset - - issue.rule - - issue.category - - issue.effort - - issue.labels + - provider + - name + - version + - sha + - indirect + - dep.provider + - dep.name + - dep.version + - dep.sha + - dep.indirect + - dep.labels - application.id - application.name + - businessService.id - businessService.name sort: - - id - name - description - businessService + - provider + - name + - version + - sha + - indirect + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/api.DepAppReport' + type: array + summary: List application reports. + tags: + - depappreports + /analyses/report/applications/{id}/issues: + get: + description: |- + Each report collates issues by ruleset/rule. + filters: + - ruleset + - rule + - category + - effort + - labels + sort: + - ruleset + - rule + - category - effort - - incidents - files + parameters: + - description: Application ID + in: path + name: id + required: true + type: string produces: - application/json responses: @@ -1152,11 +1306,11 @@ paths: description: OK schema: items: - $ref: '#/definitions/api.AppReport' + $ref: '#/definitions/api.IssueReport' type: array - summary: List application reports. + summary: List application issue reports. tags: - - appreports + - issuereport /analyses/report/issues/{id}/files: get: description: |- @@ -1194,6 +1348,8 @@ paths: - applications - application.id - application.name + - businessService.id + - businessService.name - tag.id sort: - ruleset @@ -3423,6 +3579,97 @@ paths: summary: Update a tag. tags: - tags + /targets: + get: + description: List all targets. + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/api.Target' + type: array + summary: List all targets. + tags: + - targets + post: + consumes: + - application/json + description: Create a target. + parameters: + - description: Target data + in: body + name: target + required: true + schema: + $ref: '#/definitions/api.Target' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/api.Target' + summary: Create a target. + tags: + - targets + /targets/{id}: + delete: + description: Delete a target. + parameters: + - description: Target ID + in: path + name: id + required: true + type: string + responses: + "204": + description: No Content + summary: Delete a target. + tags: + - targets + get: + description: Get a Target by ID. + parameters: + - description: Target ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.Target' + summary: Get a Target by ID. + tags: + - targets + put: + consumes: + - application/json + description: Update a target. + parameters: + - description: Target ID + in: path + name: id + required: true + type: string + - description: Target data + in: body + name: target + required: true + schema: + $ref: '#/definitions/api.Target' + responses: + "204": + description: No Content + summary: Update a target. + tags: + - targets /taskgroups: get: description: List all task groups. diff --git a/go.mod b/go.mod index e3443231b..6b0803e38 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.3.0 github.com/jortel/go-utils v0.1.1 - github.com/konveyor/tackle2-seed v0.0.0-20230714183056-d397517afd22 + github.com/konveyor/tackle2-seed v0.0.0-20230731150314-953199a73e93 github.com/mattn/go-sqlite3 v1.14.17 github.com/onsi/gomega v1.27.6 github.com/prometheus/client_golang v1.15.0 diff --git a/go.sum b/go.sum index f55520933..89150508f 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/konveyor/tackle2-seed v0.0.0-20230714183056-d397517afd22 h1:6W0xmtOp/Uh9vs8ItNdzyQzNaf5ZTS7UBjbLsHYd4F4= -github.com/konveyor/tackle2-seed v0.0.0-20230714183056-d397517afd22/go.mod h1:9SLWPe6DTNhZ41PaNcPYoY9/ToYdMvwZtj/XFr2L+tU= +github.com/konveyor/tackle2-seed v0.0.0-20230731150314-953199a73e93 h1:uMJnrcH2+luiLf1l7skj5kBJSEffbhyl41uvXA+JKUQ= +github.com/konveyor/tackle2-seed v0.0.0-20230731150314-953199a73e93/go.mod h1:4yas66RZJudLqkehmiFeQZ6NDgXk+XutksjoKFx8JzY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= diff --git a/migration/pkg.go b/migration/pkg.go index 7fce6a633..c8127d672 100644 --- a/migration/pkg.go +++ b/migration/pkg.go @@ -8,6 +8,7 @@ import ( v5 "github.com/konveyor/tackle2-hub/migration/v5" v6 "github.com/konveyor/tackle2-hub/migration/v6" v7 "github.com/konveyor/tackle2-hub/migration/v7" + v8 "github.com/konveyor/tackle2-hub/migration/v8" "github.com/konveyor/tackle2-hub/settings" "gorm.io/gorm" ) @@ -47,5 +48,6 @@ func All() []Migration { v5.Migration{}, v6.Migration{}, v7.Migration{}, + v8.Migration{}, } } diff --git a/migration/v8/migrate.go b/migration/v8/migrate.go new file mode 100644 index 000000000..e7fa1e5c6 --- /dev/null +++ b/migration/v8/migrate.go @@ -0,0 +1,76 @@ +package v8 + +import ( + "encoding/json" + liberr "github.com/jortel/go-utils/error" + "github.com/jortel/go-utils/logr" + v7 "github.com/konveyor/tackle2-hub/migration/v7/model" + "github.com/konveyor/tackle2-hub/migration/v8/model" + "gorm.io/gorm" +) + +var log = logr.WithName("migration|v8") + +type Migration struct{} + +func (r Migration) Apply(db *gorm.DB) (err error) { + result := db.Model(model.Setting{}).Where("key = ?", "ui.ruleset.order").Update("key", "ui.target.order") + if result.Error != nil { + err = liberr.Wrap(err) + return + } + + oldCustomRuleSets := []v7.RuleSet{} + result = db.Find(&oldCustomRuleSets, "uuid IS NULL") + if result.Error != nil { + err = liberr.Wrap(err) + return + } + + err = db.AutoMigrate(r.Models()...) + if err != nil { + err = liberr.Wrap(err) + return + } + + for _, rs := range oldCustomRuleSets { + target := model.Target{ + Name: rs.Name, + Description: rs.Description, + RuleSetID: &rs.ID, + ImageID: rs.ImageID, + } + target.CreateUser = rs.CreateUser + + type TargetLabel struct { + Name string `json:"name"` + Label string `json:"label"` + } + + uniqueLabels := make(map[string]bool) + for _, rule := range rs.Rules { + ruleLabels := []string{} + _ = json.Unmarshal(rule.Labels, &ruleLabels) + for _, label := range ruleLabels { + uniqueLabels[label] = true + } + } + + targetLabels := []TargetLabel{} + for k, _ := range uniqueLabels { + targetLabels = append(targetLabels, TargetLabel{Name: k, Label: k}) + } + target.Labels, _ = json.Marshal(targetLabels) + result = db.Save(target) + if result.Error != nil { + err = liberr.Wrap(result.Error) + return + } + } + + return +} + +func (r Migration) Models() []interface{} { + return model.All() +} diff --git a/migration/v8/model/pkg.go b/migration/v8/model/pkg.go new file mode 100644 index 000000000..269614bed --- /dev/null +++ b/migration/v8/model/pkg.go @@ -0,0 +1,82 @@ +package model + +import "github.com/konveyor/tackle2-hub/migration/v7/model" + +// +// JSON field (data) type. +type JSON = []byte + +type Model = model.Model +type Application = model.Application +type TechDependency = model.TechDependency +type Incident = model.Incident +type Analysis = model.Analysis +type Issue = model.Issue +type Bucket = model.Bucket +type BucketOwner = model.BucketOwner +type BusinessService = model.BusinessService +type Dependency = model.Dependency +type File = model.File +type Fact = model.Fact +type Identity = model.Identity +type Import = model.Import +type ImportSummary = model.ImportSummary +type ImportTag = model.ImportTag +type JobFunction = model.JobFunction +type MigrationWave = model.MigrationWave +type Proxy = model.Proxy +type Review = model.Review +type Setting = model.Setting +type Stakeholder = model.Stakeholder +type StakeholderGroup = model.StakeholderGroup +type Tag = model.Tag +type TagCategory = model.TagCategory +type Task = model.Task +type TaskGroup = model.TaskGroup +type TaskReport = model.TaskReport +type Ticket = model.Ticket +type Tracker = model.Tracker +type TTL = model.TTL +type ApplicationTag = model.ApplicationTag +type DependencyCyclicError = model.DependencyCyclicError + +// +// All builds all models. +// Models are enumerated such that each are listed after +// all the other models on which they may depend. +func All() []interface{} { + return []interface{}{ + Application{}, + TechDependency{}, + Incident{}, + Analysis{}, + Issue{}, + Bucket{}, + BusinessService{}, + Dependency{}, + File{}, + Fact{}, + Identity{}, + Import{}, + ImportSummary{}, + ImportTag{}, + JobFunction{}, + MigrationWave{}, + Proxy{}, + Review{}, + Setting{}, + RuleSet{}, + Rule{}, + Stakeholder{}, + StakeholderGroup{}, + Tag{}, + TagCategory{}, + Target{}, + Task{}, + TaskGroup{}, + TaskReport{}, + Ticket{}, + Tracker{}, + ApplicationTag{}, + } +} diff --git a/migration/v8/model/ruleset.go b/migration/v8/model/ruleset.go new file mode 100644 index 000000000..4edf0c021 --- /dev/null +++ b/migration/v8/model/ruleset.go @@ -0,0 +1,69 @@ +package model + +import "gorm.io/gorm" + +// +// RuleSet - Analysis ruleset. +type RuleSet struct { + Model + UUID *string `gorm:"uniqueIndex"` + Kind string + Name string `gorm:"uniqueIndex;not null"` + Description string + Repository JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity + Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` + DependsOn []RuleSet `gorm:"many2many:RuleSetDependencies;constraint:OnDelete:CASCADE"` +} + +func (r *RuleSet) Builtin() bool { + return r.UUID != nil +} + +// +// BeforeUpdate hook to avoid cyclic dependencies. +func (r *RuleSet) BeforeUpdate(db *gorm.DB) (err error) { + seen := make(map[uint]bool) + var nextDeps []RuleSet + var nextRuleSetIDs []uint + for _, dep := range r.DependsOn { + nextRuleSetIDs = append(nextRuleSetIDs, dep.ID) + } + for len(nextRuleSetIDs) != 0 { + result := db.Preload("DependsOn").Where("ID IN ?", nextRuleSetIDs).Find(&nextDeps) + if result.Error != nil { + err = result.Error + return + } + nextRuleSetIDs = nextRuleSetIDs[:0] + for _, nextDep := range nextDeps { + for _, dep := range nextDep.DependsOn { + if seen[dep.ID] { + continue + } + if dep.ID == r.ID { + err = DependencyCyclicError{} + return + } + seen[dep.ID] = true + nextRuleSetIDs = append(nextRuleSetIDs, dep.ID) + } + } + } + + return +} + +// +// Rule - Analysis rule. +type Rule struct { + Model + Name string + Description string + Labels JSON `gorm:"type:json"` + RuleSetID uint `gorm:"uniqueIndex:RuleA;not null"` + RuleSet *RuleSet + FileID *uint `gorm:"uniqueIndex:RuleA" ref:"file"` + File *File +} diff --git a/migration/v8/model/target.go b/migration/v8/model/target.go new file mode 100644 index 000000000..f6e0dfe54 --- /dev/null +++ b/migration/v8/model/target.go @@ -0,0 +1,20 @@ +package model + +// +// Target - analysis rule selector. +type Target struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex;not null"` + Description string + Choice bool + Labels JSON `gorm:"type:json"` + ImageID uint `gorm:"index" ref:"file"` + Image *File + RuleSetID *uint `gorm:"index"` + RuleSet *RuleSet +} + +func (r *Target) Builtin() bool { + return r.UUID != nil +} diff --git a/model/pkg.go b/model/pkg.go index 2cebf736e..6b534753b 100644 --- a/model/pkg.go +++ b/model/pkg.go @@ -1,7 +1,7 @@ package model import ( - "github.com/konveyor/tackle2-hub/migration/v7/model" + "github.com/konveyor/tackle2-hub/migration/v8/model" "gorm.io/datatypes" ) @@ -38,6 +38,7 @@ type Stakeholder = model.Stakeholder type StakeholderGroup = model.StakeholderGroup type Tag = model.Tag type TagCategory = model.TagCategory +type Target = model.Target type Task = model.Task type TaskGroup = model.TaskGroup type TaskReport = model.TaskReport diff --git a/reaper/file.go b/reaper/file.go index bfd78d2ae..350be2e98 100644 --- a/reaper/file.go +++ b/reaper/file.go @@ -68,6 +68,7 @@ func (r *FileReaper) busy(file *model.File) (busy bool, err error) { for _, m := range []interface{}{ &model.RuleSet{}, &model.Rule{}, + &model.Target{}, } { n, err = ref.Count(m, "file", file.ID) if err != nil { diff --git a/seed/ruleset.go b/seed/ruleset.go index 6b431d99d..4376960c2 100644 --- a/seed/ruleset.go +++ b/seed/ruleset.go @@ -79,16 +79,8 @@ func (r *RuleSet) Apply(db *gorm.DB) (err error) { ruleSet = &model.RuleSet{} } } - - file, fErr := r.file(db, rs.Image()) - if fErr != nil { - err = fErr - return - } ruleSet.Name = rs.Name - ruleSet.Description = rs.Description ruleSet.UUID = &rs.UUID - ruleSet.ImageID = file.ID result := db.Save(ruleSet) if result.Error != nil { err = liberr.Wrap(result.Error) @@ -140,7 +132,7 @@ func (r *RuleSet) applyRules(db *gorm.DB, ruleSet *model.RuleSet, rs libseed.Rul } for _, rl := range rs.Rules { labels, _ := json.Marshal(rl.Labels()) - file, fErr := r.file(db, rl.Path) + f, fErr := file(db, rl.Path) if fErr != nil { err = liberr.Wrap(fErr) return @@ -148,7 +140,7 @@ func (r *RuleSet) applyRules(db *gorm.DB, ruleSet *model.RuleSet, rs libseed.Rul rule := model.Rule{ Labels: labels, RuleSetID: ruleSet.ID, - FileID: &file.ID, + FileID: &f.ID, } result = db.Save(&rule) if result.Error != nil { @@ -161,7 +153,7 @@ func (r *RuleSet) applyRules(db *gorm.DB, ruleSet *model.RuleSet, rs libseed.Rul // // Create a File model and copy a real file to its path. -func (r *RuleSet) file(db *gorm.DB, filePath string) (file *model.File, err error) { +func file(db *gorm.DB, filePath string) (file *model.File, err error) { file = &model.File{ Name: path.Base(filePath), } diff --git a/seed/seed.go b/seed/seed.go index 83fde439d..b600b9477 100644 --- a/seed/seed.go +++ b/seed/seed.go @@ -17,6 +17,7 @@ type Hub struct { TagCategory JobFunction RuleSet + Target } // @@ -29,6 +30,8 @@ func (r *Hub) With(seed libseed.Seed) (err error) { err = r.JobFunction.With(seed) case libseed.KindRuleSet: err = r.RuleSet.With(seed) + case libseed.KindTarget: + err = r.Target.With(seed) default: err = liberr.New("unknown kind", "kind", seed.Kind, "file", seed.Filename()) } @@ -50,6 +53,10 @@ func (r *Hub) Apply(db *gorm.DB) (err error) { if err != nil { return } + err = r.Target.Apply(db) + if err != nil { + return + } return } diff --git a/seed/target.go b/seed/target.go new file mode 100644 index 000000000..5651a9f24 --- /dev/null +++ b/seed/target.go @@ -0,0 +1,147 @@ +package seed + +import ( + "encoding/json" + "errors" + "fmt" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/model" + libseed "github.com/konveyor/tackle2-seed/pkg" + "gorm.io/gorm" +) + +// +// Target applies Target seeds. +type Target struct { + targets []libseed.Target +} + +// +// With collects all the Target seeds. +func (r *Target) With(seed libseed.Seed) (err error) { + items, err := seed.DecodeItems() + if err != nil { + return + } + for _, item := range items { + r.targets = append(r.targets, item.(libseed.Target)) + } + return +} + +// +// Apply seeds the database with JobFunctions. +func (r *Target) Apply(db *gorm.DB) (err error) { + log.Info("Applying Targets", "count", len(r.targets)) + + ids := []uint{} + for i := range r.targets { + t := r.targets[i] + target, found, fErr := r.find(db, "uuid = ?", t.UUID) + if fErr != nil { + err = fErr + return + } + // model exists and is being renamed + if found && target.Name != t.Name { + // ensure that the target name is clear + collision, collides, fErr := r.find(db, "name = ? and id != ?", t.Name, target.ID) + if fErr != nil { + err = fErr + return + } + if collides { + err = r.rename(db, collision) + if err != nil { + return + } + } + } else { + target, found, fErr = r.find(db, "name = ?", t.Name) + if fErr != nil { + err = fErr + return + } + if found && target.CreateUser != "" { + err = r.rename(db, target) + if err != nil { + return + } + found = false + } + if !found { + target = &model.Target{} + } + } + + f, fErr := file(db, t.Image()) + if fErr != nil { + err = liberr.Wrap(fErr) + return + } + labels, _ := json.Marshal(t.Labels) + + target.UUID = &t.UUID + target.Name = t.Name + target.Description = t.Description + target.Choice = t.Choice + target.ImageID = f.ID + target.Labels = labels + result := db.Save(&target) + if result.Error != nil { + err = liberr.Wrap(result.Error) + return + } + ids = append(ids, target.ID) + } + + value, _ := json.Marshal(ids) + uiOrder := model.Setting{Key: "ui.target.order", Value: value} + result := db.Where("key", "ui.target.order").Updates(uiOrder) + if result.Error != nil { + err = liberr.Wrap(err) + return + } + + return +} + +// +// Convenience method to find a Target. +func (r *Target) find(db *gorm.DB, conditions ...interface{}) (t *model.Target, found bool, err error) { + t = &model.Target{} + result := db.First(t, conditions...) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return + } + err = liberr.Wrap(result.Error) + return + } + found = true + return +} + +// +// Rename a Target by adding a suffix. +func (r *Target) rename(db *gorm.DB, t *model.Target) (err error) { + suffix := 0 + for { + suffix++ + newName := fmt.Sprintf("%s (%d)", t.Name, suffix) + _, found, fErr := r.find(db, "name = ?", newName) + if fErr != nil { + err = fErr + return + } + if !found { + t.Name = newName + result := db.Save(t) + if result.Error != nil { + err = liberr.Wrap(result.Error) + return + } + return + } + } +}