From 7af183a9c75382b662aaa6a3682c3cf8ef0ecb26 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 08:17:12 -0600 Subject: [PATCH 01/18] :sparkles: add analysis profiles CRUD. Signed-off-by: Jeff Ortel --- api/pkg.go | 1 + api/profile.go | 241 ++++++++++++++++++++ hack/add/analysis-profile.sh | 45 ++++ migration/pkg.go | 2 + migration/v21/migrate.go | 17 ++ migration/v21/model/analysis.go | 188 ++++++++++++++++ migration/v21/model/application.go | 339 +++++++++++++++++++++++++++++ migration/v21/model/assessment.go | 102 +++++++++ migration/v21/model/core.go | 250 +++++++++++++++++++++ migration/v21/model/mod.patch | 21 ++ migration/v21/model/pkg.go | 63 ++++++ migration/v21/model/platform.go | 38 ++++ model/pkg.go | 4 +- reaper/file.go | 1 + 14 files changed, 1311 insertions(+), 1 deletion(-) create mode 100644 api/profile.go create mode 100755 hack/add/analysis-profile.sh create mode 100644 migration/v21/migrate.go create mode 100644 migration/v21/model/analysis.go create mode 100644 migration/v21/model/application.go create mode 100644 migration/v21/model/assessment.go create mode 100644 migration/v21/model/core.go create mode 100644 migration/v21/model/mod.patch create mode 100644 migration/v21/model/pkg.go create mode 100644 migration/v21/model/platform.go diff --git a/api/pkg.go b/api/pkg.go index 439dcd364..f8574baf8 100644 --- a/api/pkg.go +++ b/api/pkg.go @@ -70,6 +70,7 @@ func All() []Handler { return []Handler{ &AddonHandler{}, &AdoptionPlanHandler{}, + &AnalysisProfileHandler{}, &AnalysisHandler{}, &ApplicationHandler{}, &AuthHandler{}, diff --git a/api/profile.go b/api/profile.go new file mode 100644 index 000000000..662f56cee --- /dev/null +++ b/api/profile.go @@ -0,0 +1,241 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/konveyor/tackle2-hub/model" + "gorm.io/gorm/clause" +) + +// Routes +const ( + AnalysisProfilesRoot = "/analysis/profiles" + AnalysisProfileRoot = AnalysisProfilesRoot + "/:id" + AnalysisProfileBundle = AnalysisProfileRoot + "/:bundle" +) + +// AnalysisProfileHandler handles application Profile resource routes. +type AnalysisProfileHandler struct { + BaseHandler +} + +func (h AnalysisProfileHandler) AddRoutes(e *gin.Engine) { + routeGroup := e.Group("/") + routeGroup.Use(Required("Profiles")) + routeGroup.GET(AnalysisProfileRoot, h.Get) + routeGroup.GET(AnalysisProfilesRoot, h.List) + routeGroup.GET(AnalysisProfilesRoot+"/", h.List) + routeGroup.POST(AnalysisProfilesRoot, h.Create) + routeGroup.PUT(AnalysisProfileRoot, h.Update) + routeGroup.DELETE(AnalysisProfileRoot, h.Delete) +} + +// Get godoc +// @summary Get a Profile by ID. +// @description Get a Profile by ID. +// @tags Profiles +// @produce json +// @success 200 {object} AnalysisProfile +// @router /Profiles/{id} [get] +// @param id path int true "Profile ID" +func (h AnalysisProfileHandler) Get(ctx *gin.Context) { + r := AnalysisProfile{} + id := h.pk(ctx) + m := &model.AnalysisProfile{} + db := h.DB(ctx) + db = db.Preload(clause.Associations) + err := db.First(m, id).Error + if err != nil { + _ = ctx.Error(err) + return + } + r.With(m) + + h.Respond(ctx, http.StatusOK, r) +} + +// List godoc +// @summary List all Profiles. +// @description List all Profiles. +// @tags Profiles +// @produce json +// @success 200 {object} []AnalysisProfile +// @router /Profiles [get] +func (h AnalysisProfileHandler) List(ctx *gin.Context) { + resources := []AnalysisProfile{} + var list []model.AnalysisProfile + db := h.DB(ctx) + db = db.Preload(clause.Associations) + err := db.Find(&list).Error + if err != nil { + _ = ctx.Error(err) + return + } + for i := range list { + m := &list[i] + r := AnalysisProfile{} + r.With(m) + resources = append(resources, r) + } + + h.Respond(ctx, http.StatusOK, resources) +} + +// Create godoc +// @summary Create a Profile. +// @description Create a Profile. +// @tags Profiles +// @accept json +// @produce json +// @success 201 {object} Profile +// @router /Profiles [post] +// @param Profile body AnalysisProfile true "Profile data" +func (h AnalysisProfileHandler) Create(ctx *gin.Context) { + r := &AnalysisProfile{} + err := h.Bind(ctx, r) + if err != nil { + _ = ctx.Error(err) + return + } + m := r.Model() + m.CreateUser = h.CurrentUser(ctx) + db := h.DB(ctx) + db = db.Omit(clause.Associations) + err = db.Create(m).Error + if err != nil { + _ = ctx.Error(err) + return + } + db = h.DB(ctx).Model(m) + err = db.Association("Targets").Replace(m.Targets) + if err != nil { + _ = ctx.Error(err) + return + } + r.With(m) + h.Respond(ctx, http.StatusCreated, r) +} + +// Delete godoc +// @summary Delete a Profile. +// @description Delete a Profile. +// @tags Profiles +// @success 204 +// @router /Profiles/{id} [delete] +// @param id path int true "Profile ID" +func (h AnalysisProfileHandler) Delete(ctx *gin.Context) { + id := h.pk(ctx) + m := &model.AnalysisProfile{} + db := h.DB(ctx) + err := db.First(m, id).Error + if err != nil { + _ = ctx.Error(err) + return + } + err = db.Delete(m).Error + if err != nil { + _ = ctx.Error(err) + return + } + + h.Status(ctx, http.StatusNoContent) +} + +// Update godoc +// @summary Update a Profile. +// @description Update a Profile. +// @tags Profiles +// @accept json +// @success 204 +// @router /Profiles/{id} [put] +// @param id path int true "Profile ID" +// @param Profile body AnalysisProfile true "Profile data" +func (h AnalysisProfileHandler) Update(ctx *gin.Context) { + id := h.pk(ctx) + r := &AnalysisProfile{} + err := h.Bind(ctx, r) + if err != nil { + _ = ctx.Error(err) + return + } + m := r.Model() + m.ID = id + m.UpdateUser = h.CurrentUser(ctx) + db := h.DB(ctx) + err = db.Save(m).Error + if err != nil { + _ = ctx.Error(err) + return + } + + h.Status(ctx, http.StatusNoContent) +} + +type InExList = model.InExList + +// AnalysisProfile REST resource. +type AnalysisProfile struct { + Resource `yaml:",inline"` + Name string `json:"name"` + Mode struct { + WithDeps bool `json:"withDeps" yaml:"withDeps"` + } `json:"mode"` + Scope struct { + WithKnownLibs bool `json:"withKnownLibs" yaml:"withKnownLibs"` + Packages InExList `json:"packages"` + } `json:"scope"` + Rules struct { + Targets []Ref `json:"targets"` + Labels InExList `json:"labels"` + Files []Ref `json:"files"` + Repository Repository `json:"repository"` + } +} + +// With updates the resource with the model. +func (r *AnalysisProfile) With(m *model.AnalysisProfile) { + r.Resource.With(&m.Model) + r.Mode.WithDeps = m.WithDeps + r.Scope.WithKnownLibs = m.WithKnownLibs + r.Scope.Packages = m.Packages + r.Rules.Labels = m.Labels + r.Rules.Repository = Repository(m.Repository) + r.Rules.Targets = make([]Ref, len(m.Targets)) + for i, t := range m.Targets { + r.Rules.Targets[i] = + Ref{ + ID: t.ID, + Name: t.Name, + } + } + r.Rules.Files = make([]Ref, len(m.Files)) + for i, f := range m.Files { + r.Rules.Files[i] = Ref(f) + } +} + +// Model builds a model. +func (r *AnalysisProfile) Model() (m *model.AnalysisProfile) { + m = &model.AnalysisProfile{} + m.WithDeps = r.Mode.WithDeps + m.WithKnownLibs = r.Scope.WithKnownLibs + m.Packages = r.Scope.Packages + m.Labels = r.Rules.Labels + m.Repository = model.Repository(r.Rules.Repository) + m.Targets = make([]model.Target, len(r.Rules.Targets)) + for i, t := range r.Rules.Targets { + m.Targets[i] = + model.Target{ + Model: model.Model{ + ID: t.ID, + }, + Name: t.Name, + } + } + m.Files = make([]model.Ref, len(r.Rules.Files)) + for i, f := range r.Rules.Files { + m.Files[i] = model.Ref(f) + } + return +} diff --git a/hack/add/analysis-profile.sh b/hack/add/analysis-profile.sh new file mode 100755 index 000000000..88c8e07e6 --- /dev/null +++ b/hack/add/analysis-profile.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +host="${HOST:-localhost:8080}" + +id="${1:-0}" # 0=system-assigned. +name="${1:-Test}-${id}" +repository="${2:-https://github.com/WASdev/sample.daytrader7.git}" + +# create application. +curl -X POST ${host}/analysis/profiles \ + -H 'Content-Type:application/x-yaml' \ + -H 'Accept:application/x-yaml' \ + -d \ +" +id: ${id} +name: ${name} +mode: + withDeps: true +scope: + withKnownLibs: true + packages: + included: + - one + - two + excluded: + - three + - four +rules: + labels: + included: + - A + - B + excluded: + - C + - D + targets: + - id: 1 + - id: 2 + - id: 3 + files: + - id: 400 + repository: + kind: git + url: ${repository} +" diff --git a/migration/pkg.go b/migration/pkg.go index fe9b82547..fa721f4d8 100644 --- a/migration/pkg.go +++ b/migration/pkg.go @@ -14,6 +14,7 @@ import ( v19 "github.com/konveyor/tackle2-hub/migration/v19" v2 "github.com/konveyor/tackle2-hub/migration/v2" v20 "github.com/konveyor/tackle2-hub/migration/v20" + v21 "github.com/konveyor/tackle2-hub/migration/v21" v3 "github.com/konveyor/tackle2-hub/migration/v3" v4 "github.com/konveyor/tackle2-hub/migration/v4" v5 "github.com/konveyor/tackle2-hub/migration/v5" @@ -68,5 +69,6 @@ func All() []Migration { v18.Migration{}, v19.Migration{}, v20.Migration{}, + v21.Migration{}, } } diff --git a/migration/v21/migrate.go b/migration/v21/migrate.go new file mode 100644 index 000000000..84d950b5b --- /dev/null +++ b/migration/v21/migrate.go @@ -0,0 +1,17 @@ +package v21 + +import ( + "github.com/konveyor/tackle2-hub/migration/v21/model" + "gorm.io/gorm" +) + +type Migration struct{} + +func (r Migration) Apply(db *gorm.DB) (err error) { + err = db.AutoMigrate(r.Models()...) + return +} + +func (r Migration) Models() []any { + return model.All() +} diff --git a/migration/v21/model/analysis.go b/migration/v21/model/analysis.go new file mode 100644 index 000000000..556044bd5 --- /dev/null +++ b/migration/v21/model/analysis.go @@ -0,0 +1,188 @@ +package model + +import ( + "github.com/konveyor/tackle2-hub/migration/json" + "gorm.io/gorm" +) + +// Analysis report. +type Analysis struct { + Model + Effort int + Commit string + Archived bool + Summary []ArchivedInsight `gorm:"type:json;serializer:json"` + Insights []Insight `gorm:"constraint:OnDelete:CASCADE"` + Dependencies []TechDependency `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID uint `gorm:"index;not null"` + Application *Application +} + +// TechDependency report dependency. +type TechDependency struct { + Model + Provider string `gorm:"uniqueIndex:depA"` + Name string `gorm:"uniqueIndex:depA"` + Version string `gorm:"uniqueIndex:depA"` + SHA string `gorm:"uniqueIndex:depA"` + Indirect bool + Labels []string `gorm:"type:json;serializer:json"` + AnalysisID uint `gorm:"index;uniqueIndex:depA;not null"` + Analysis *Analysis +} + +// Insight report insights. +type Insight struct { + Model + RuleSet string `gorm:"uniqueIndex:insightA;not null"` + Rule string `gorm:"uniqueIndex:insightA;not null"` + Name string `gorm:"index"` + Description string + Category string `gorm:"index;not null"` + Incidents []Incident `gorm:"foreignKey:InsightID;constraint:OnDelete:CASCADE"` + Links []Link `gorm:"type:json;serializer:json"` + Facts json.Map `gorm:"type:json;serializer:json"` + Labels []string `gorm:"type:json;serializer:json"` + Effort int `gorm:"index;not null"` + AnalysisID uint `gorm:"index;uniqueIndex:insightA;not null"` + Analysis *Analysis +} + +// Incident report an issue incident. +type Incident struct { + Model + File string `gorm:"index;not null"` + Line int + Message string + CodeSnip string + Facts json.Map `gorm:"type:json;serializer:json"` + InsightID uint `gorm:"index;not null"` + Insight *Insight +} + +// RuleSet - Analysis ruleset. +type RuleSet struct { + Model + UUID *string `gorm:"uniqueIndex"` + Kind string + Name string `gorm:"uniqueIndex;not null"` + Description string + Repository Repository `gorm:"type:json;serializer: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 []string `gorm:"type:json;serializer:json"` + RuleSetID uint `gorm:"uniqueIndex:RuleA;not null"` + RuleSet *RuleSet + FileID *uint `gorm:"uniqueIndex:RuleA" ref:"file"` + File *File +} + +// Target - analysis rule selector. +type Target struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex;not null"` + Description string + Provider string + Choice bool + Labels []TargetLabel `gorm:"type:json;serializer:json"` + ImageID uint `gorm:"index" ref:"file"` + Image *File + RuleSetID *uint `gorm:"index"` + RuleSet *RuleSet +} + +func (r *Target) Builtin() bool { + return r.UUID != nil +} + +type AnalysisProfile struct { + Model + Name string `gorm:"uniqueIndex"` + WithDeps bool + WithKnownLibs bool + Packages InExList `gorm:"type:json;serializer:json"` + Labels InExList `gorm:"type:json;serializer:json"` + Files []json.Ref `gorm:"type:json;serializer:json" ref:"[]file"` + Repository Repository `gorm:"type:json;serializer:json"` + Targets []Target `gorm:"many2many:analysisProfileTargets;constraint:OnDelete:CASCADE"` +} + +// +// JSON Fields. +// + +// ArchivedInsight resource created when issues are archived. +type ArchivedInsight struct { + RuleSet string `json:"ruleSet"` + Rule string `json:"rule"` + Name string `json:"name,omitempty" yaml:",omitempty"` + Description string `json:"description,omitempty" yaml:",omitempty"` + Category string `json:"category"` + Effort int `json:"effort"` + Incidents int `json:"incidents"` +} + +// Link URL link. +type Link struct { + URL string `json:"url"` + Title string `json:"title,omitempty"` +} + +// TargetLabel - label format specific to Targets +type TargetLabel struct { + Name string `json:"name"` + Label string `json:"label"` +} + +// InExList contains items included and excluded. +type InExList struct { + Included []string `json:"included"` + Excluded []string `json:"excluded"` +} diff --git a/migration/v21/model/application.go b/migration/v21/model/application.go new file mode 100644 index 000000000..59853f3a4 --- /dev/null +++ b/migration/v21/model/application.go @@ -0,0 +1,339 @@ +package model + +import ( + "fmt" + "sync" + "time" + + "github.com/konveyor/tackle2-hub/migration/json" + "gorm.io/gorm" +) + +type Application struct { + Model + BucketOwner + Name string `gorm:"index;unique;not null"` + Description string + Review *Review `gorm:"constraint:OnDelete:CASCADE"` + Repository Repository `gorm:"type:json;serializer:json"` + Assets Repository `gorm:"type:json;serializer:json"` + Coordinates *json.Document `gorm:"type:json;serializer:json"` + Binary string + Facts []Fact `gorm:"constraint:OnDelete:CASCADE"` + Comments string + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + Tags []Tag `gorm:"many2many:ApplicationTags"` + Identities []Identity `gorm:"many2many:ApplicationIdentity;constraint:OnDelete:CASCADE"` + BusinessServiceID *uint `gorm:"index"` + BusinessService *BusinessService + OwnerID *uint `gorm:"index"` + Owner *Stakeholder `gorm:"foreignKey:OwnerID"` + Contributors []Stakeholder `gorm:"many2many:ApplicationContributors;constraint:OnDelete:CASCADE"` + Analyses []Analysis `gorm:"constraint:OnDelete:CASCADE"` + MigrationWaveID *uint `gorm:"index"` + MigrationWave *MigrationWave + PlatformID *uint `gorm:"index"` + Platform *Platform + Ticket *Ticket `gorm:"constraint:OnDelete:CASCADE"` + Assessments []Assessment `gorm:"constraint:OnDelete:CASCADE"` + Manifest []Manifest `gorm:"constraint:OnDelete:CASCADE"` +} + +type Fact struct { + ApplicationID uint `gorm:"<-:create;primaryKey"` + Key string `gorm:"<-:create;primaryKey"` + Source string `gorm:"<-:create;primaryKey;not null"` + Value any `gorm:"type:json;not null;serializer:json"` + Application *Application +} + +// ApplicationTag represents a row in the join table for the +// many-to-many relationship between Applications and Tags. +type ApplicationTag struct { + ApplicationID uint `gorm:"primaryKey"` + TagID uint `gorm:"primaryKey"` + Source string `gorm:"primaryKey;not null"` + Application Application `gorm:"constraint:OnDelete:CASCADE"` + Tag Tag `gorm:"constraint:OnDelete:CASCADE"` +} + +// TableName must return "ApplicationTags" to ensure compatibility +// with the autogenerated join table name. +func (ApplicationTag) TableName() string { + return "ApplicationTags" +} + +type ApplicationIdentity struct { + ApplicationID uint `gorm:"primaryKey"` + IdentityID uint `gorm:"primaryKey;index"` + Role string `gorm:"primaryKey"` + Application Application `gorm:"constraint:OnDelete:CASCADE"` + Identity Identity `gorm:"constraint:OnDelete:CASCADE"` +} + +// depMutex ensures Dependency.Create() is not executed concurrently. +var depMutex sync.Mutex + +type Dependency struct { + Model + ToID uint `gorm:"index"` + To *Application `gorm:"foreignKey:ToID;constraint:OnDelete:CASCADE"` + FromID uint `gorm:"index"` + From *Application `gorm:"foreignKey:FromID;constraint:OnDelete:CASCADE"` +} + +// Create a dependency synchronized using a mutex. +func (r *Dependency) Create(db *gorm.DB) (err error) { + depMutex.Lock() + defer depMutex.Unlock() + err = db.Create(r).Error + return +} + +// BeforeCreate detects cyclic dependencies. +func (r *Dependency) BeforeCreate(db *gorm.DB) (err error) { + var nextDeps []*Dependency + var nextAppsIDs []uint + nextAppsIDs = append(nextAppsIDs, r.FromID) + for len(nextAppsIDs) != 0 { + db.Where("ToID IN ?", nextAppsIDs).Find(&nextDeps) + nextAppsIDs = nextAppsIDs[:0] // empty array, but keep capacity + for _, nextDep := range nextDeps { + if nextDep.FromID == r.ToID { + err = DependencyCyclicError{} + return + } + nextAppsIDs = append(nextAppsIDs, nextDep.FromID) + } + } + + return +} + +// DependencyCyclicError reports cyclic Dependency error. +type DependencyCyclicError struct{} + +func (e DependencyCyclicError) Error() string { + return "Cyclic dependencies are not permitted." +} + +type BusinessService struct { + Model + Name string `gorm:"index;unique;not null"` + Description string + Applications []Application `gorm:"constraint:OnDelete:SET NULL"` + StakeholderID *uint `gorm:"index"` + Stakeholder *Stakeholder +} + +type JobFunction struct { + Model + UUID *string `gorm:"uniqueIndex"` + Username string + Name string `gorm:"index;unique;not null"` + Stakeholders []Stakeholder `gorm:"constraint:OnDelete:SET NULL"` +} + +type Stakeholder struct { + Model + Name string `gorm:"not null;"` + Email string `gorm:"index;unique;not null"` + Groups []StakeholderGroup `gorm:"many2many:StakeholderGroupStakeholder;constraint:OnDelete:CASCADE"` + BusinessServices []BusinessService `gorm:"constraint:OnDelete:SET NULL"` + JobFunctionID *uint `gorm:"index"` + JobFunction *JobFunction + Owns []Application `gorm:"foreignKey:OwnerID;constraint:OnDelete:SET NULL"` + Contributes []Application `gorm:"many2many:ApplicationContributors;constraint:OnDelete:CASCADE"` + MigrationWaves []MigrationWave `gorm:"many2many:MigrationWaveStakeholders;constraint:OnDelete:CASCADE"` + Assessments []Assessment `gorm:"many2many:AssessmentStakeholders;constraint:OnDelete:CASCADE"` + Archetypes []Archetype `gorm:"many2many:ArchetypeStakeholders;constraint:OnDelete:CASCADE"` +} + +type StakeholderGroup struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Description string + Stakeholders []Stakeholder `gorm:"many2many:StakeholderGroupStakeholder;constraint:OnDelete:CASCADE"` + MigrationWaves []MigrationWave `gorm:"many2many:MigrationWaveStakeholderGroups;constraint:OnDelete:CASCADE"` + Assessments []Assessment `gorm:"many2many:AssessmentStakeholderGroups;constraint:OnDelete:CASCADE"` + Archetypes []Archetype `gorm:"many2many:ArchetypeStakeholderGroups;constraint:OnDelete:CASCADE"` +} + +type MigrationWave struct { + Model + Name string `gorm:"uniqueIndex:MigrationWaveA"` + StartDate time.Time `gorm:"uniqueIndex:MigrationWaveA"` + EndDate time.Time `gorm:"uniqueIndex:MigrationWaveA"` + Applications []Application `gorm:"constraint:OnDelete:SET NULL"` + Stakeholders []Stakeholder `gorm:"many2many:MigrationWaveStakeholders;constraint:OnDelete:CASCADE"` + StakeholderGroups []StakeholderGroup `gorm:"many2many:MigrationWaveStakeholderGroups;constraint:OnDelete:CASCADE"` +} + +type Archetype struct { + Model + Name string + Description string + Comments string + Review *Review `gorm:"constraint:OnDelete:CASCADE"` + Assessments []Assessment `gorm:"constraint:OnDelete:CASCADE"` + CriteriaTags []Tag `gorm:"many2many:ArchetypeCriteriaTags;constraint:OnDelete:CASCADE"` + Tags []Tag `gorm:"many2many:ArchetypeTags;constraint:OnDelete:CASCADE"` + Stakeholders []Stakeholder `gorm:"many2many:ArchetypeStakeholders;constraint:OnDelete:CASCADE"` + StakeholderGroups []StakeholderGroup `gorm:"many2many:ArchetypeStakeholderGroups;constraint:OnDelete:CASCADE"` + Profiles []TargetProfile `gorm:"constraint:OnDelete:CASCADE"` +} + +type TargetProfile struct { + Model + Name string `gorm:"uniqueIndex:targetProfileA;not null"` + ArchetypeID uint `gorm:"uniqueIndex:targetProfileA;not null"` + Archetype Archetype + Generators []ProfileGenerator `gorm:"order:Index;constraint:OnDelete:CASCADE"` +} + +type ProfileGenerator struct { + GeneratorID uint `gorm:"index"` + TargetProfileID uint `gorm:"index;uniqueIndex:profileGeneratorA"` + Index int `gorm:"index;uniqueIndex:profileGeneratorA"` + TargetProfile TargetProfile + Generator Generator +} + +type Tag struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex:tagA;not null"` + CategoryID uint `gorm:"uniqueIndex:tagA;index;not null"` + Category TagCategory +} + +type TagCategory struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"index;unique;not null"` + Color string + Tags []Tag `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE"` +} + +type Ticket struct { + Model + // Kind of ticket in the external tracker. + Kind string `gorm:"not null"` + // Parent resource that this ticket should belong to in the tracker. (e.g. Jira project) + Parent string `gorm:"not null"` + // Custom fields to send to the tracker when creating the ticket + Fields json.Map `gorm:"type:json;serializer:json"` + // Whether the last attempt to do something with the ticket reported an error + Error bool + // Error message, if any + Message string + // Whether the ticket was created in the external tracker + Created bool + // Reference id in external tracker + Reference string + // URL to ticket in external tracker + Link string + // Status of ticket in external tracker + Status string + LastUpdated time.Time + Application *Application + ApplicationID uint `gorm:"uniqueIndex:ticketA;not null"` + Tracker *Tracker + TrackerID uint `gorm:"uniqueIndex:ticketA;not null"` +} + +type Tracker struct { + Model + Name string `gorm:"index;unique;not null"` + URL string + Kind string + Identity *Identity + IdentityID uint + Connected bool + LastUpdated time.Time + Message string + Insecure bool + Tickets []Ticket +} + +type Import struct { + Model + Filename string + ApplicationName string + BusinessService string + Comments string + Dependency string + DependencyDirection string + Description string + ErrorMessage string + IsValid bool + RecordType1 string + ImportSummary ImportSummary + ImportSummaryID uint `gorm:"index"` + Processed bool + ImportTags []ImportTag `gorm:"constraint:OnDelete:CASCADE"` + BinaryGroup string + BinaryArtifact string + BinaryVersion string + BinaryPackaging string + RepositoryKind string + RepositoryURL string + RepositoryBranch string + RepositoryPath string + Owner string + Contributors string +} + +func (r *Import) AsMap() (m map[string]any) { + m = make(map[string]any) + m["filename"] = r.Filename + m["applicationName"] = r.ApplicationName + // "Application Name" is necessary in order for + // the UI to display the error report correctly. + m["Application Name"] = r.ApplicationName + m["businessService"] = r.BusinessService + m["comments"] = r.Comments + m["dependency"] = r.Dependency + m["dependencyDirection"] = r.DependencyDirection + m["description"] = r.Description + m["errorMessage"] = r.ErrorMessage + m["isValid"] = r.IsValid + m["processed"] = r.Processed + m["recordType1"] = r.RecordType1 + for i, tag := range r.ImportTags { + m[fmt.Sprintf("category%v", i+1)] = tag.Category + m[fmt.Sprintf("tag%v", i+1)] = tag.Name + } + return +} + +type ImportSummary struct { + Model + Content []byte + Filename string + ImportStatus string + Imports []Import `gorm:"constraint:OnDelete:CASCADE"` + CreateEntities bool +} + +type ImportTag struct { + Model + Name string + Category string + ImportID uint `gorm:"index"` + Import *Import +} + +// +// JSON Fields. +// + +// Repository represents an SCM repository. +type Repository struct { + Kind string `json:"kind"` + URL string `json:"url"` + Branch string `json:"branch"` + Tag string `json:"tag"` + Path string `json:"path"` +} diff --git a/migration/v21/model/assessment.go b/migration/v21/model/assessment.go new file mode 100644 index 000000000..0b51e714d --- /dev/null +++ b/migration/v21/model/assessment.go @@ -0,0 +1,102 @@ +package model + +type Questionnaire struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"unique"` + Description string + Required bool + Sections []Section `gorm:"type:json;serializer:json"` + Thresholds Thresholds `gorm:"type:json;serializer:json"` + RiskMessages RiskMessages `gorm:"type:json;serializer:json"` + Assessments []Assessment `gorm:"constraint:OnDelete:CASCADE"` +} + +// Builtin returns true if this is a Konveyor-provided questionnaire. +func (r *Questionnaire) Builtin() bool { + return r.UUID != nil +} + +type Assessment struct { + Model + ApplicationID *uint `gorm:"uniqueIndex:AssessmentA"` + Application *Application + ArchetypeID *uint `gorm:"uniqueIndex:AssessmentB"` + Archetype *Archetype + QuestionnaireID uint `gorm:"uniqueIndex:AssessmentA;uniqueIndex:AssessmentB"` + Questionnaire Questionnaire + Sections []Section `gorm:"type:json;serializer:json"` + Thresholds Thresholds `gorm:"type:json;serializer:json"` + RiskMessages RiskMessages `gorm:"type:json;serializer:json"` + Stakeholders []Stakeholder `gorm:"many2many:AssessmentStakeholders;constraint:OnDelete:CASCADE"` + StakeholderGroups []StakeholderGroup `gorm:"many2many:AssessmentStakeholderGroups;constraint:OnDelete:CASCADE"` +} + +type Review struct { + Model + BusinessCriticality uint `gorm:"not null"` + EffortEstimate string `gorm:"not null"` + ProposedAction string `gorm:"not null"` + WorkPriority uint `gorm:"not null"` + Comments string + ApplicationID *uint `gorm:"uniqueIndex"` + Application *Application + ArchetypeID *uint `gorm:"uniqueIndex"` + Archetype *Archetype +} + +// +// JSON Fields. +// + +// Section represents a group of questions in a questionnaire. +type Section struct { + Order uint `json:"order" yaml:"order"` + Name string `json:"name" yaml:"name"` + Questions []Question `json:"questions" yaml:"questions" binding:"min=1,dive"` + Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` +} + +// Question represents a question in a questionnaire. +type Question struct { + Order uint `json:"order" yaml:"order"` + Text string `json:"text" yaml:"text"` + Explanation string `json:"explanation" yaml:"explanation"` + IncludeFor []CategorizedTag `json:"includeFor,omitempty" yaml:"includeFor,omitempty"` + ExcludeFor []CategorizedTag `json:"excludeFor,omitempty" yaml:"excludeFor,omitempty"` + Answers []Answer `json:"answers" yaml:"answers" binding:"min=1,dive"` +} + +// Answer represents an answer to a question in a questionnaire. +type Answer struct { + Order uint `json:"order" yaml:"order"` + Text string `json:"text" yaml:"text"` + Risk string `json:"risk" yaml:"risk" binding:"oneof=red yellow green unknown"` + Rationale string `json:"rationale" yaml:"rationale"` + Mitigation string `json:"mitigation" yaml:"mitigation"` + ApplyTags []CategorizedTag `json:"applyTags,omitempty" yaml:"applyTags,omitempty"` + AutoAnswerFor []CategorizedTag `json:"autoAnswerFor,omitempty" yaml:"autoAnswerFor,omitempty"` + Selected bool `json:"selected,omitempty" yaml:"selected,omitempty"` + AutoAnswered bool `json:"autoAnswered,omitempty" yaml:"autoAnswered,omitempty"` +} + +// CategorizedTag represents a human-readable pair of category and tag. +type CategorizedTag struct { + Category string `json:"category" yaml:"category"` + Tag string `json:"tag" yaml:"tag"` +} + +// RiskMessages contains messages to display for each risk level. +type RiskMessages struct { + Red string `json:"red" yaml:"red"` + Yellow string `json:"yellow" yaml:"yellow"` + Green string `json:"green" yaml:"green"` + Unknown string `json:"unknown" yaml:"unknown"` +} + +// Thresholds contains the threshold values for determining risk for the questionnaire. +type Thresholds struct { + Red uint `json:"red" yaml:"red"` + Yellow uint `json:"yellow" yaml:"yellow"` + Unknown uint `json:"unknown" yaml:"unknown"` +} diff --git a/migration/v21/model/core.go b/migration/v21/model/core.go new file mode 100644 index 000000000..9ca7b556e --- /dev/null +++ b/migration/v21/model/core.go @@ -0,0 +1,250 @@ +package model + +import ( + "os" + "path" + "time" + + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/migration/json" + "gorm.io/gorm" +) + +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +// PK sequence. +type PK struct { + Kind string `gorm:"<-:create;primaryKey"` + LastID uint +} + +// Setting hub settings. +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value any `gorm:"type:json;serializer:json"` +} + +// As unmarshalls the value of the Setting into the `ptr` parameter. +func (r *Setting) As(ptr any) (err error) { + bytes, err := json.Marshal(r.Value) + if err != nil { + err = liberr.Wrap(err) + } + err = json.Unmarshal(bytes, ptr) + if err != nil { + err = liberr.Wrap(err) + } + return +} + +type Bucket struct { + Model + Path string `gorm:"<-:create;uniqueIndex"` + Expiration *time.Time +} + +func (m *Bucket) BeforeCreate(db *gorm.DB) (err error) { + if m.Path == "" { + uid := uuid.New() + m.Path = path.Join( + Settings.Hub.Bucket.Path, + uid.String()) + err = os.MkdirAll(m.Path, 0777) + if err != nil { + err = liberr.Wrap( + err, + "path", + m.Path) + } + } + return +} + +type BucketOwner struct { + BucketID *uint `gorm:"index" ref:"bucket"` + Bucket *Bucket +} + +func (m *BucketOwner) BeforeCreate(db *gorm.DB) (err error) { + if !m.HasBucket() { + b := &Bucket{} + err = db.Create(b).Error + m.SetBucket(&b.ID) + } + return +} + +func (m *BucketOwner) SetBucket(id *uint) { + m.BucketID = id + m.Bucket = nil +} + +func (m *BucketOwner) HasBucket() (b bool) { + return m.BucketID != nil +} + +type File struct { + Model + Name string + Encoding string + Path string `gorm:"<-:create;uniqueIndex"` + Expiration *time.Time +} + +func (m *File) BeforeCreate(db *gorm.DB) (err error) { + uid := uuid.New() + m.Path = path.Join( + Settings.Hub.Bucket.Path, + ".file", + uid.String()) + err = os.MkdirAll(path.Dir(m.Path), 0777) + if err != nil { + err = liberr.Wrap( + err, + "path", + m.Path) + } + return +} + +type Task struct { + Model + BucketOwner + Name string `gorm:"index"` + Kind string + Addon string `gorm:"index"` + Extensions []string `gorm:"type:json;serializer:json"` + State string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Policy TaskPolicy `gorm:"type:json;serializer:json"` + TTL TTL `gorm:"type:json;serializer:json"` + Data json.Data `gorm:"type:json;serializer:json"` + Started *time.Time + Terminated *time.Time + Retained bool `gorm:"index"` + Reaped bool `gorm:"index"` + Errors []TaskError `gorm:"type:json;serializer:json"` + Events []TaskEvent `gorm:"type:json;serializer:json"` + Pod string `gorm:"index"` + Retries int + Attached []Attachment `gorm:"type:json;serializer:json" ref:"[]file"` + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint `gorm:"index"` + Application *Application + PlatformID *uint `gorm:"index"` + Platform *Platform + TaskGroupID *uint `gorm:"<-:create;index"` + TaskGroup *TaskGroup +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + return +} + +type TaskReport struct { + Model + Status string + Total int + Completed int + Activity []string `gorm:"type:json;serializer:json"` + Errors []TaskError `gorm:"type:json;serializer:json"` + Attached []Attachment `gorm:"type:json;serializer:json" ref:"[]file"` + Result json.Data `gorm:"type:json;serializer:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Mode string + Kind string + Addon string + Extensions []string `gorm:"type:json;serializer:json"` + State string + Priority int + Policy TaskPolicy `gorm:"type:json;serializer:json"` + Data json.Data `gorm:"type:json;serializer:json"` + List []Task `gorm:"type:json;serializer:json"` + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` +} + +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded []string `gorm:"type:json;serializer:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"index;not null"` + Name string `gorm:"index;unique;not null"` + Default bool + Description string + User string + Password string `secret:""` + Key string `secret:""` + Settings string `secret:""` + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` + Applications []Application `gorm:"many2many:ApplicationIdentity;constraint:OnDelete:CASCADE"` +} + +// +// JSON Fields. +// + +// Attachment file attachment. +type Attachment struct { + ID uint `json:"id" binding:"required"` + Name string `json:"name,omitempty" yaml:",omitempty"` + Activity int `json:"activity,omitempty" yaml:",omitempty"` +} + +// TaskError used in Task.Errors. +type TaskError struct { + Severity string `json:"severity"` + Description string `json:"description"` +} + +// TaskEvent task event. +type TaskEvent struct { + Kind string `json:"kind"` + Count int `json:"count"` + Reason string `json:"reason,omitempty" yaml:",omitempty"` + Last time.Time `json:"last"` +} + +// TaskPolicy scheduling policy. +type TaskPolicy struct { + Isolated bool `json:"isolated,omitempty" yaml:",omitempty"` + PreemptEnabled bool `json:"preemptEnabled,omitempty" yaml:"preemptEnabled,omitempty"` + PreemptExempt bool `json:"preemptExempt,omitempty" yaml:"preemptExempt,omitempty"` +} + +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty" yaml:",omitempty"` + Pending int `json:"pending,omitempty" yaml:",omitempty"` + Running int `json:"running,omitempty" yaml:",omitempty"` + Succeeded int `json:"succeeded,omitempty" yaml:",omitempty"` + Failed int `json:"failed,omitempty" yaml:",omitempty"` +} diff --git a/migration/v21/model/mod.patch b/migration/v21/model/mod.patch new file mode 100644 index 000000000..abd7be0d7 --- /dev/null +++ b/migration/v21/model/mod.patch @@ -0,0 +1,21 @@ +diff -ruN '--exclude=mod.patch' migration/v19/model/core.go migration/v20/model/core.go +--- migration/v19/model/core.go 2025-10-27 16:17:15.057379687 -0500 ++++ migration/v20/model/core.go 2025-10-27 16:11:13.781809158 -0500 +@@ -130,6 +130,8 @@ + Data json.Data `gorm:"type:json;serializer:json"` + Started *time.Time + Terminated *time.Time ++ Retained bool `gorm:"index"` ++ Reaped bool `gorm:"index"` + Errors []TaskError `gorm:"type:json;serializer:json"` + Events []TaskEvent `gorm:"type:json;serializer:json"` + Pod string `gorm:"index"` +@@ -140,7 +142,7 @@ + Application *Application + PlatformID *uint `gorm:"index"` + Platform *Platform +- TaskGroupID *uint `gorm:"<-:create"` ++ TaskGroupID *uint `gorm:"<-:create;index"` + TaskGroup *TaskGroup + } + diff --git a/migration/v21/model/pkg.go b/migration/v21/model/pkg.go new file mode 100644 index 000000000..7c49e535d --- /dev/null +++ b/migration/v21/model/pkg.go @@ -0,0 +1,63 @@ +package model + +import ( + "github.com/konveyor/tackle2-hub/settings" +) + +var ( + Settings = &settings.Settings +) + +// JSON field (data) type. +type JSON = []byte + +// All builds all models. +// Models are enumerated such that each are listed after +// all the other models on which they may depend. +func All() []any { + return []any{ + Application{}, + TechDependency{}, + Incident{}, + Analysis{}, + AnalysisProfile{}, + Insight{}, + Bucket{}, + BusinessService{}, + Dependency{}, + File{}, + Fact{}, + Generator{}, + Identity{}, + Import{}, + ImportSummary{}, + ImportTag{}, + JobFunction{}, + Manifest{}, + MigrationWave{}, + PK{}, + Platform{}, + Proxy{}, + Review{}, + Setting{}, + RuleSet{}, + Rule{}, + Stakeholder{}, + StakeholderGroup{}, + Tag{}, + TagCategory{}, + Target{}, + TargetProfile{}, + Task{}, + TaskGroup{}, + TaskReport{}, + Ticket{}, + Tracker{}, + ApplicationTag{}, + ApplicationIdentity{}, + Questionnaire{}, + Assessment{}, + Archetype{}, + ProfileGenerator{}, + } +} diff --git a/migration/v21/model/platform.go b/migration/v21/model/platform.go new file mode 100644 index 000000000..13c5f0cae --- /dev/null +++ b/migration/v21/model/platform.go @@ -0,0 +1,38 @@ +package model + +import ( + "github.com/konveyor/tackle2-hub/migration/json" +) + +type Manifest struct { + Model + Content json.Map `gorm:"type:json;serializer:json"` + Secret json.Map `gorm:"type:json;serializer:json" secret:""` + ApplicationID uint + Application Application +} + +type Platform struct { + Model + Name string + Kind string + URL string + IdentityID *uint + Identity *Identity + Applications []Application `gorm:"constraint:OnDelete:SET NULL"` + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` +} + +type Generator struct { + Model + UUID *string `gorm:"uniqueIndex"` + Kind string + Name string + Description string + Repository Repository `gorm:"type:json;serializer:json"` + Params json.Map `gorm:"type:json;serializer:json"` + Values json.Map `gorm:"type:json;serializer:json"` + IdentityID *uint + Identity *Identity + Profiles []TargetProfile `gorm:"many2many:TargetGenerator;constraint:OnDelete:CASCADE"` +} diff --git a/model/pkg.go b/model/pkg.go index 986624f79..1aecb731f 100644 --- a/model/pkg.go +++ b/model/pkg.go @@ -2,7 +2,7 @@ package model import ( "github.com/konveyor/tackle2-hub/migration/json" - "github.com/konveyor/tackle2-hub/migration/v20/model" + "github.com/konveyor/tackle2-hub/migration/v21/model" ) // Field (data) types. @@ -18,6 +18,7 @@ type Assessment = model.Assessment type TechDependency = model.TechDependency type Incident = model.Incident type Analysis = model.Analysis +type AnalysisProfile = model.AnalysisProfile type Insight = model.Insight type Bucket = model.Bucket type BucketOwner = model.BucketOwner @@ -68,6 +69,7 @@ type TaskError = model.TaskError type TaskEvent = model.TaskEvent type TaskPolicy = model.TaskPolicy type TTL = model.TTL +type InExList = model.InExList // Assessment JSON fields type Section = model.Section diff --git a/reaper/file.go b/reaper/file.go index 0ba47f57f..eb2e3b73e 100644 --- a/reaper/file.go +++ b/reaper/file.go @@ -35,6 +35,7 @@ func (r *FileReaper) Run() { &model.TaskReport{}, &model.Rule{}, &model.Target{}, + &model.AnalysisProfile{}, } { err := finder.Find(m, "file", ids) if err != nil { From bbeb531d134fc97dbb69a458066ab2c7b03bd084 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 09:45:03 -0600 Subject: [PATCH 02/18] checkpoint Signed-off-by: Jeff Ortel --- api/archetype.go | 15 ++++-- api/profile.go | 30 ++++++++---- migration/v21/model/analysis.go | 1 + migration/v21/model/application.go | 10 ++-- test/api/analysisprofile/api_test.go | 69 ++++++++++++++++++++++++++++ test/api/analysisprofile/pkg.go | 19 ++++++++ test/api/analysisprofile/samples.go | 24 ++++++++++ test/api/questionnaire/samples.go | 6 +-- 8 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 test/api/analysisprofile/api_test.go create mode 100644 test/api/analysisprofile/pkg.go create mode 100644 test/api/analysisprofile/samples.go diff --git a/api/archetype.go b/api/archetype.go index 2cfb06224..84f3ea9a4 100644 --- a/api/archetype.go +++ b/api/archetype.go @@ -52,6 +52,7 @@ func (h ArchetypeHandler) Get(ctx *gin.Context) { id := h.pk(ctx) db := h.DB(ctx) db = db.Preload(clause.Associations) + db = db.Preload("Profiles.AnalysisProfile") db = db.Preload("Profiles.Generators.Generator") db = db.Preload("Profiles.Generators", func(db *gorm.DB) *gorm.DB { return db.Order("`Index`") @@ -97,6 +98,7 @@ func (h ArchetypeHandler) List(ctx *gin.Context) { var list []model.Archetype db := h.DB(ctx) db = db.Preload(clause.Associations) + db = db.Preload("Profiles.AnalysisProfile") db = db.Preload("Profiles.Generators.Generator") db = db.Preload("Profiles.Generators", func(db *gorm.DB) *gorm.DB { return db.Order("`Index`") @@ -472,9 +474,10 @@ func (h ArchetypeHandler) updateGenerators(ctx *gin.Context, m *model.Archetype) // TargetProfile REST resource. type TargetProfile struct { - Resource `yaml:",inline"` - Name string `json:"name" binding:"required"` - Generators []Ref `json:"generators"` + Resource `yaml:",inline"` + Name string `json:"name" binding:"required"` + Generators []Ref `json:"generators"` + AnalysisProfile *Ref `json:"analysisProfile,omitempty" yaml:"analysisProfile,omitempty"` } // With updates the resource with the model. @@ -487,6 +490,9 @@ func (r *TargetProfile) With(m *model.TargetProfile) { ref.With(g.Generator.ID, g.Generator.Name) r.Generators = append(r.Generators, ref) } + r.AnalysisProfile = r.refPtr( + m.AnalysisProfileID, + m.AnalysisProfile) } // Model builds a model from the resource. @@ -502,6 +508,9 @@ func (r *TargetProfile) Model() (m *model.TargetProfile) { m.Generators, g) } + if r.AnalysisProfile != nil { + m.AnalysisProfileID = &r.AnalysisProfile.ID + } return } diff --git a/api/profile.go b/api/profile.go index 662f56cee..fdf0c1f04 100644 --- a/api/profile.go +++ b/api/profile.go @@ -176,31 +176,37 @@ type InExList = model.InExList // AnalysisProfile REST resource. type AnalysisProfile struct { - Resource `yaml:",inline"` - Name string `json:"name"` - Mode struct { + Resource `yaml:",inline"` + Name string `json:"name"` + Description string `json:"description,omitempty" yaml:",omitempty"` + Mode struct { WithDeps bool `json:"withDeps" yaml:"withDeps"` } `json:"mode"` Scope struct { WithKnownLibs bool `json:"withKnownLibs" yaml:"withKnownLibs"` - Packages InExList `json:"packages"` + Packages InExList `json:"packages,omitempty" yaml:",omitempty"` } `json:"scope"` Rules struct { - Targets []Ref `json:"targets"` - Labels InExList `json:"labels"` - Files []Ref `json:"files"` - Repository Repository `json:"repository"` + Targets []Ref `json:"targets"` + Labels InExList `json:"labels,omitempty" yaml:",omitempty"` + Files []Ref `json:"files,omitempty" yaml:",omitempty"` + Repository *Repository `json:"repository,omitempty" yaml:",omitempty"` } } // With updates the resource with the model. func (r *AnalysisProfile) With(m *model.AnalysisProfile) { r.Resource.With(&m.Model) + r.Name = m.Name + r.Description = m.Description r.Mode.WithDeps = m.WithDeps r.Scope.WithKnownLibs = m.WithKnownLibs r.Scope.Packages = m.Packages r.Rules.Labels = m.Labels - r.Rules.Repository = Repository(m.Repository) + if m.Repository != (model.Repository{}) { + repository := Repository(m.Repository) + r.Rules.Repository = &repository + } r.Rules.Targets = make([]Ref, len(m.Targets)) for i, t := range m.Targets { r.Rules.Targets[i] = @@ -218,11 +224,15 @@ func (r *AnalysisProfile) With(m *model.AnalysisProfile) { // Model builds a model. func (r *AnalysisProfile) Model() (m *model.AnalysisProfile) { m = &model.AnalysisProfile{} + m.Name = r.Name + m.Description = r.Description m.WithDeps = r.Mode.WithDeps m.WithKnownLibs = r.Scope.WithKnownLibs m.Packages = r.Scope.Packages m.Labels = r.Rules.Labels - m.Repository = model.Repository(r.Rules.Repository) + if r.Rules.Repository != nil { + m.Repository = model.Repository(*r.Rules.Repository) + } m.Targets = make([]model.Target, len(r.Rules.Targets)) for i, t := range r.Rules.Targets { m.Targets[i] = diff --git a/migration/v21/model/analysis.go b/migration/v21/model/analysis.go index 556044bd5..21a91c1ed 100644 --- a/migration/v21/model/analysis.go +++ b/migration/v21/model/analysis.go @@ -145,6 +145,7 @@ func (r *Target) Builtin() bool { type AnalysisProfile struct { Model Name string `gorm:"uniqueIndex"` + Description string WithDeps bool WithKnownLibs bool Packages InExList `gorm:"type:json;serializer:json"` diff --git a/migration/v21/model/application.go b/migration/v21/model/application.go index 59853f3a4..401517af9 100644 --- a/migration/v21/model/application.go +++ b/migration/v21/model/application.go @@ -186,10 +186,12 @@ type Archetype struct { type TargetProfile struct { Model - Name string `gorm:"uniqueIndex:targetProfileA;not null"` - ArchetypeID uint `gorm:"uniqueIndex:targetProfileA;not null"` - Archetype Archetype - Generators []ProfileGenerator `gorm:"order:Index;constraint:OnDelete:CASCADE"` + Name string `gorm:"uniqueIndex:targetProfileA;not null"` + ArchetypeID uint `gorm:"uniqueIndex:targetProfileA;not null"` + Archetype Archetype + Generators []ProfileGenerator `gorm:"order:Index;constraint:OnDelete:CASCADE"` + AnalysisProfileID *uint + AnalysisProfile *AnalysisProfile `gorm:"constraint:OnDelete:SET NULL"` } type ProfileGenerator struct { diff --git a/test/api/analysisprofile/api_test.go b/test/api/analysisprofile/api_test.go new file mode 100644 index 000000000..da7558f5d --- /dev/null +++ b/test/api/analysisprofile/api_test.go @@ -0,0 +1,69 @@ +package manifest + +import ( + "encoding/json" + "testing" + + "github.com/konveyor/tackle2-hub/api" + "github.com/konveyor/tackle2-hub/test/assert" +) + +func TestGeneratorCRUD(t *testing.T) { + var r api.Generator + b, _ := json.Marshal(Base) + _ = json.Unmarshal(b, &r) + // identity + identity := &api.Identity{ + Name: t.Name(), + Kind: t.Name(), + } + err := RichClient.Identity.Create(identity) + assert.Must(t, err) + defer func() { + _ = RichClient.Identity.Delete(identity.ID) + }() + + // Create. + r.Identity = &api.Ref{ + ID: identity.ID, + Name: identity.Name, + } + err = Generator.Create(&r) + if err != nil { + t.Fatalf(err.Error()) + } + + // Get + got, err := Generator.Get(r.ID) + if err != nil { + t.Fatalf(err.Error()) + } + if !assert.Eq(got, r) { + t.Errorf("Different response error.\nGot:\n%+v\nExpected:\n%+v", got, &r) + } + + // Update. + r.Name = r.Name + "updated" + err = Generator.Update(&r) + if err != nil { + t.Errorf(err.Error()) + } + got, err = Generator.Get(r.ID) + if err != nil { + t.Fatalf(err.Error()) + } + r.UpdateUser = got.UpdateUser + if !assert.Eq(got, r) { + t.Errorf("Different response error.\nGot:\n%+v\nExpected:\n%+v", got, &r) + } + + // Delete. + err = Generator.Delete(r.ID) + if err != nil { + t.Errorf(err.Error()) + } + _, err = Generator.Get(r.ID) + if err == nil { + t.Errorf("Resource exits, but should be deleted: %v", r) + } +} diff --git a/test/api/analysisprofile/pkg.go b/test/api/analysisprofile/pkg.go new file mode 100644 index 000000000..2d4723556 --- /dev/null +++ b/test/api/analysisprofile/pkg.go @@ -0,0 +1,19 @@ +package manifest + +import ( + "github.com/konveyor/tackle2-hub/binding" + "github.com/konveyor/tackle2-hub/test/api/client" +) + +var ( + RichClient *binding.RichClient + Generator binding.Generator +) + +func init() { + // Prepare RichClient and login to Hub API (configured from env variables). + RichClient = client.PrepareRichClient() + + // Shortcut for RuleSet-related RichClient methods. + Generator = RichClient.Generator +} diff --git a/test/api/analysisprofile/samples.go b/test/api/analysisprofile/samples.go new file mode 100644 index 000000000..fe89e16aa --- /dev/null +++ b/test/api/analysisprofile/samples.go @@ -0,0 +1,24 @@ +package manifest + +import ( + "github.com/konveyor/tackle2-hub/api" +) + +// Set of valid resources for tests and reuse. +var ( + Base = api.AnalysisProfile{ + Name: "Test", + Description: "This is a test", + Repository: &api.Repository{ + URL: "https://github.com/konveyor/tackle2-hub", + }, + Params: api.Map{ + "p1": "v1", + "p2": "v2", + }, + Values: api.Map{ + "p1": "v1", + "p2": "v2", + }, + } +) diff --git a/test/api/questionnaire/samples.go b/test/api/questionnaire/samples.go index 00618b9bb..a1d1a512c 100644 --- a/test/api/questionnaire/samples.go +++ b/test/api/questionnaire/samples.go @@ -8,9 +8,9 @@ import ( // Set of valid resources for tests and reuse. var ( Questionnaire1 = api.Questionnaire{ - Name: "Questionnaire1", - Description: "Questionnaire minimal sample 1", - Required: true, + Name: "Questionnaire1", + Description: "Questionnaire minimal sample 1", + Required: true, Thresholds: api.Thresholds{ Red: 30, Yellow: 20, From 2d61c91d90a21cdaac1a72d945d73597109ad4d9 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 13:16:30 -0600 Subject: [PATCH 03/18] Update binding and add test. Signed-off-by: Jeff Ortel --- binding/analysisprofile.go | 44 ++++++++++++++++++++++++++++ binding/richclient.go | 4 +++ test/api/analysisprofile/api_test.go | 33 ++++++--------------- test/api/analysisprofile/pkg.go | 8 ++--- test/api/analysisprofile/samples.go | 39 +++++++++++++----------- 5 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 binding/analysisprofile.go diff --git a/binding/analysisprofile.go b/binding/analysisprofile.go new file mode 100644 index 000000000..c15404480 --- /dev/null +++ b/binding/analysisprofile.go @@ -0,0 +1,44 @@ +package binding + +import ( + "github.com/konveyor/tackle2-hub/api" +) + +// AnalysisProfile API. +type AnalysisProfile struct { + client *Client +} + +// Create a AnalysisProfile. +func (h *AnalysisProfile) Create(r *api.AnalysisProfile) (err error) { + err = h.client.Post(api.AnalysisProfilesRoot, &r) + return +} + +// Get a AnalysisProfile by ID. +func (h *AnalysisProfile) Get(id uint) (r *api.AnalysisProfile, err error) { + r = &api.AnalysisProfile{} + path := Path(api.AnalysisProfileRoot).Inject(Params{api.ID: id}) + err = h.client.Get(path, r) + return +} + +// List AnalysisProfiles. +func (h *AnalysisProfile) List() (list []api.AnalysisProfile, err error) { + list = []api.AnalysisProfile{} + err = h.client.Get(api.AnalysisProfilesRoot, &list) + return +} + +// Update a AnalysisProfile. +func (h *AnalysisProfile) Update(r *api.AnalysisProfile) (err error) { + path := Path(api.AnalysisProfileRoot).Inject(Params{api.ID: r.ID}) + err = h.client.Put(path, r) + return +} + +// Delete a AnalysisProfile. +func (h *AnalysisProfile) Delete(id uint) (err error) { + err = h.client.Delete(Path(api.AnalysisProfileRoot).Inject(Params{api.ID: id})) + return +} diff --git a/binding/richclient.go b/binding/richclient.go index 4e1dc909c..3a625ce51 100644 --- a/binding/richclient.go +++ b/binding/richclient.go @@ -12,6 +12,7 @@ var ( // The RichClient provides API integration. type RichClient struct { Addon Addon + AnalysisProfile AnalysisProfile Application Application Archetype Archetype Assessment Assessment @@ -55,6 +56,9 @@ func New(baseURL string) (r *RichClient) { Addon: Addon{ client: client, }, + AnalysisProfile: AnalysisProfile{ + client: client, + }, Application: Application{ client: client, }, diff --git a/test/api/analysisprofile/api_test.go b/test/api/analysisprofile/api_test.go index da7558f5d..879c176d6 100644 --- a/test/api/analysisprofile/api_test.go +++ b/test/api/analysisprofile/api_test.go @@ -1,4 +1,4 @@ -package manifest +package analysisprofile import ( "encoding/json" @@ -8,33 +8,18 @@ import ( "github.com/konveyor/tackle2-hub/test/assert" ) -func TestGeneratorCRUD(t *testing.T) { - var r api.Generator +func TestAnalysisProfileCRUD(t *testing.T) { + var r api.AnalysisProfile b, _ := json.Marshal(Base) _ = json.Unmarshal(b, &r) - // identity - identity := &api.Identity{ - Name: t.Name(), - Kind: t.Name(), - } - err := RichClient.Identity.Create(identity) - assert.Must(t, err) - defer func() { - _ = RichClient.Identity.Delete(identity.ID) - }() - // Create. - r.Identity = &api.Ref{ - ID: identity.ID, - Name: identity.Name, - } - err = Generator.Create(&r) + err := AnalysisProfile.Create(&r) if err != nil { t.Fatalf(err.Error()) } // Get - got, err := Generator.Get(r.ID) + got, err := AnalysisProfile.Get(r.ID) if err != nil { t.Fatalf(err.Error()) } @@ -44,11 +29,11 @@ func TestGeneratorCRUD(t *testing.T) { // Update. r.Name = r.Name + "updated" - err = Generator.Update(&r) + err = AnalysisProfile.Update(&r) if err != nil { t.Errorf(err.Error()) } - got, err = Generator.Get(r.ID) + got, err = AnalysisProfile.Get(r.ID) if err != nil { t.Fatalf(err.Error()) } @@ -58,11 +43,11 @@ func TestGeneratorCRUD(t *testing.T) { } // Delete. - err = Generator.Delete(r.ID) + err = AnalysisProfile.Delete(r.ID) if err != nil { t.Errorf(err.Error()) } - _, err = Generator.Get(r.ID) + _, err = AnalysisProfile.Get(r.ID) if err == nil { t.Errorf("Resource exits, but should be deleted: %v", r) } diff --git a/test/api/analysisprofile/pkg.go b/test/api/analysisprofile/pkg.go index 2d4723556..567b19fa2 100644 --- a/test/api/analysisprofile/pkg.go +++ b/test/api/analysisprofile/pkg.go @@ -1,4 +1,4 @@ -package manifest +package analysisprofile import ( "github.com/konveyor/tackle2-hub/binding" @@ -6,8 +6,8 @@ import ( ) var ( - RichClient *binding.RichClient - Generator binding.Generator + RichClient *binding.RichClient + AnalysisProfile binding.AnalysisProfile ) func init() { @@ -15,5 +15,5 @@ func init() { RichClient = client.PrepareRichClient() // Shortcut for RuleSet-related RichClient methods. - Generator = RichClient.Generator + AnalysisProfile = RichClient.AnalysisProfile } diff --git a/test/api/analysisprofile/samples.go b/test/api/analysisprofile/samples.go index fe89e16aa..cc8763e84 100644 --- a/test/api/analysisprofile/samples.go +++ b/test/api/analysisprofile/samples.go @@ -1,24 +1,29 @@ -package manifest +package analysisprofile import ( "github.com/konveyor/tackle2-hub/api" ) -// Set of valid resources for tests and reuse. var ( - Base = api.AnalysisProfile{ - Name: "Test", - Description: "This is a test", - Repository: &api.Repository{ - URL: "https://github.com/konveyor/tackle2-hub", - }, - Params: api.Map{ - "p1": "v1", - "p2": "v2", - }, - Values: api.Map{ - "p1": "v1", - "p2": "v2", - }, - } + Base = api.AnalysisProfile{} ) + +func init() { + Base.Name = "base" + Base.Description = "Base analysis profiling test." + Base.Mode.WithDeps = true + Base.Scope.WithKnownLibs = true + Base.Scope.Packages.Included = []string{"pA", "pB"} + Base.Scope.Packages.Excluded = []string{"pC", "pD"} + Base.Rules.Targets = []api.Ref{ + {ID: 2, Name: "Containerization"}, + } + Base.Rules.Labels.Included = []string{"A", "B"} + Base.Rules.Labels.Excluded = []string{"C", "D"} + Base.Rules.Files = []api.Ref{ + {ID: 400}, + } + Base.Rules.Repository = &api.Repository{ + URL: "https://github.com/konveyor/testapp", + } +} From 1addeee761bb22ffd0d76bdf0672b982750ebdcd Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 14:02:02 -0600 Subject: [PATCH 04/18] checkpoint Signed-off-by: Jeff Ortel --- migration/v21/model/application.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migration/v21/model/application.go b/migration/v21/model/application.go index 401517af9..624adc700 100644 --- a/migration/v21/model/application.go +++ b/migration/v21/model/application.go @@ -190,8 +190,8 @@ type TargetProfile struct { ArchetypeID uint `gorm:"uniqueIndex:targetProfileA;not null"` Archetype Archetype Generators []ProfileGenerator `gorm:"order:Index;constraint:OnDelete:CASCADE"` - AnalysisProfileID *uint - AnalysisProfile *AnalysisProfile `gorm:"constraint:OnDelete:SET NULL"` + AnalysisProfileID *uint `gorm:"index"` + AnalysisProfile *AnalysisProfile `gorm:"constraint:OnDelete:SET NULL"` } type ProfileGenerator struct { From 279cae3193bf48e08776960f6f6c9a63c6da2671 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 14:03:08 -0600 Subject: [PATCH 05/18] checkpoint Signed-off-by: Jeff Ortel --- migration/v21/model/mod.patch | 81 +++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/migration/v21/model/mod.patch b/migration/v21/model/mod.patch index abd7be0d7..7a166fb43 100644 --- a/migration/v21/model/mod.patch +++ b/migration/v21/model/mod.patch @@ -1,21 +1,64 @@ -diff -ruN '--exclude=mod.patch' migration/v19/model/core.go migration/v20/model/core.go ---- migration/v19/model/core.go 2025-10-27 16:17:15.057379687 -0500 -+++ migration/v20/model/core.go 2025-10-27 16:11:13.781809158 -0500 -@@ -130,6 +130,8 @@ - Data json.Data `gorm:"type:json;serializer:json"` - Started *time.Time - Terminated *time.Time -+ Retained bool `gorm:"index"` -+ Reaped bool `gorm:"index"` - Errors []TaskError `gorm:"type:json;serializer:json"` - Events []TaskEvent `gorm:"type:json;serializer:json"` - Pod string `gorm:"index"` -@@ -140,7 +142,7 @@ - Application *Application - PlatformID *uint `gorm:"index"` - Platform *Platform -- TaskGroupID *uint `gorm:"<-:create"` -+ TaskGroupID *uint `gorm:"<-:create;index"` - TaskGroup *TaskGroup +diff -ruN '--exclude=mod.patch' migration/v20/model/analysis.go migration/v21/model/analysis.go +--- migration/v20/model/analysis.go 2025-10-27 16:11:13.781809158 -0500 ++++ migration/v21/model/analysis.go 2025-11-07 12:08:40.454018870 -0600 +@@ -142,6 +142,19 @@ + return r.UUID != nil } ++type AnalysisProfile struct { ++ Model ++ Name string `gorm:"uniqueIndex"` ++ Description string ++ WithDeps bool ++ WithKnownLibs bool ++ Packages InExList `gorm:"type:json;serializer:json"` ++ Labels InExList `gorm:"type:json;serializer:json"` ++ Files []json.Ref `gorm:"type:json;serializer:json" ref:"[]file"` ++ Repository Repository `gorm:"type:json;serializer:json"` ++ Targets []Target `gorm:"many2many:analysisProfileTargets;constraint:OnDelete:CASCADE"` ++} ++ + // + // JSON Fields. + // +@@ -168,3 +181,9 @@ + Name string `json:"name"` + Label string `json:"label"` + } ++ ++// InExList contains items included and excluded. ++type InExList struct { ++ Included []string `json:"included"` ++ Excluded []string `json:"excluded"` ++} +diff -ruN '--exclude=mod.patch' migration/v20/model/application.go migration/v21/model/application.go +--- migration/v20/model/application.go 2025-10-27 16:11:13.781809158 -0500 ++++ migration/v21/model/application.go 2025-11-07 14:01:27.483825203 -0600 +@@ -186,10 +186,12 @@ + + type TargetProfile struct { + Model +- Name string `gorm:"uniqueIndex:targetProfileA;not null"` +- ArchetypeID uint `gorm:"uniqueIndex:targetProfileA;not null"` +- Archetype Archetype +- Generators []ProfileGenerator `gorm:"order:Index;constraint:OnDelete:CASCADE"` ++ Name string `gorm:"uniqueIndex:targetProfileA;not null"` ++ ArchetypeID uint `gorm:"uniqueIndex:targetProfileA;not null"` ++ Archetype Archetype ++ Generators []ProfileGenerator `gorm:"order:Index;constraint:OnDelete:CASCADE"` ++ AnalysisProfileID *uint `gorm:"index"` ++ AnalysisProfile *AnalysisProfile `gorm:"constraint:OnDelete:SET NULL"` + } + + type ProfileGenerator struct { +diff -ruN '--exclude=mod.patch' migration/v20/model/pkg.go migration/v21/model/pkg.go +--- migration/v20/model/pkg.go 2025-10-27 16:11:13.781809158 -0500 ++++ migration/v21/model/pkg.go 2025-11-07 12:08:40.454018870 -0600 +@@ -20,6 +20,7 @@ + TechDependency{}, + Incident{}, + Analysis{}, ++ AnalysisProfile{}, + Insight{}, + Bucket{}, + BusinessService{}, From 5dfd10bca19c39b5e00b53b46e5e5f11d237ba5d Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 14:54:26 -0600 Subject: [PATCH 06/18] checkpoint Signed-off-by: Jeff Ortel --- .coderabbit/config.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .coderabbit/config.yml diff --git a/.coderabbit/config.yml b/.coderabbit/config.yml new file mode 100644 index 000000000..92df5225c --- /dev/null +++ b/.coderabbit/config.yml @@ -0,0 +1,5 @@ +reviews: + path_instructions: + - path: migrations/*/model/* + instructions: | + Limit review to code referenced the diff described by the mod.path file. From eb0272ecd15bf192653dfbc856b8a465c386c54d Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 14:58:29 -0600 Subject: [PATCH 07/18] checkpoint Signed-off-by: Jeff Ortel --- .coderabbit/config.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.coderabbit/config.yml b/.coderabbit/config.yml index 92df5225c..e384e2a79 100644 --- a/.coderabbit/config.yml +++ b/.coderabbit/config.yml @@ -1,5 +1,7 @@ reviews: path_instructions: - - path: migrations/*/model/* - instructions: | - Limit review to code referenced the diff described by the mod.path file. + - paths: + - "migrations/*/model/*" + instructions: | + Limit review to code referenced in the diff described by the mod.path file. + From 8317052474d98fa4ec6e322ac3a1e44cef73bc73 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 15:00:48 -0600 Subject: [PATCH 08/18] checkpoint Signed-off-by: Jeff Ortel --- .coderabbit/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coderabbit/config.yml b/.coderabbit/config.yml index e384e2a79..9bb04e35a 100644 --- a/.coderabbit/config.yml +++ b/.coderabbit/config.yml @@ -1,7 +1,7 @@ reviews: path_instructions: - paths: - - "migrations/*/model/*" + - "migration/*/model/*" instructions: | Limit review to code referenced in the diff described by the mod.path file. From 80ba1b466b73a19a9eec849aa0cb045f148277d6 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Fri, 7 Nov 2025 15:28:51 -0600 Subject: [PATCH 09/18] checkpoint Signed-off-by: Jeff Ortel --- .coderabbit/config.yml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .coderabbit/config.yml diff --git a/.coderabbit/config.yml b/.coderabbit/config.yml deleted file mode 100644 index 9bb04e35a..000000000 --- a/.coderabbit/config.yml +++ /dev/null @@ -1,7 +0,0 @@ -reviews: - path_instructions: - - paths: - - "migration/*/model/*" - instructions: | - Limit review to code referenced in the diff described by the mod.path file. - From da2fd7fdcf96222e44e00a833a28b39c5081b5b7 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 09:26:29 -0600 Subject: [PATCH 10/18] checkpoint Signed-off-by: Jeff Ortel --- api/profile.go | 115 ++++++++++++++++++++++------ test/api/analysisprofile/samples.go | 47 +++++++----- 2 files changed, 118 insertions(+), 44 deletions(-) diff --git a/api/profile.go b/api/profile.go index fdf0c1f04..d7c15a5a3 100644 --- a/api/profile.go +++ b/api/profile.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/konveyor/tackle2-hub/assessment" "github.com/konveyor/tackle2-hub/model" "gorm.io/gorm/clause" ) @@ -13,6 +14,8 @@ const ( AnalysisProfilesRoot = "/analysis/profiles" AnalysisProfileRoot = AnalysisProfilesRoot + "/:id" AnalysisProfileBundle = AnalysisProfileRoot + "/:bundle" + // + AppAnalysisProfilesRoot = ApplicationRoot + "/analysis/profiles" ) // AnalysisProfileHandler handles application Profile resource routes. @@ -29,6 +32,8 @@ func (h AnalysisProfileHandler) AddRoutes(e *gin.Engine) { routeGroup.POST(AnalysisProfilesRoot, h.Create) routeGroup.PUT(AnalysisProfileRoot, h.Update) routeGroup.DELETE(AnalysisProfileRoot, h.Delete) + // + routeGroup.GET(AppAnalysisProfilesRoot, h.AppProfileList) } // Get godoc @@ -37,14 +42,13 @@ func (h AnalysisProfileHandler) AddRoutes(e *gin.Engine) { // @tags Profiles // @produce json // @success 200 {object} AnalysisProfile -// @router /Profiles/{id} [get] +// @router /analysis/profiles/{id} [get] // @param id path int true "Profile ID" func (h AnalysisProfileHandler) Get(ctx *gin.Context) { r := AnalysisProfile{} id := h.pk(ctx) m := &model.AnalysisProfile{} db := h.DB(ctx) - db = db.Preload(clause.Associations) err := db.First(m, id).Error if err != nil { _ = ctx.Error(err) @@ -61,12 +65,11 @@ func (h AnalysisProfileHandler) Get(ctx *gin.Context) { // @tags Profiles // @produce json // @success 200 {object} []AnalysisProfile -// @router /Profiles [get] +// @router /analysis/profiles [get] func (h AnalysisProfileHandler) List(ctx *gin.Context) { resources := []AnalysisProfile{} var list []model.AnalysisProfile db := h.DB(ctx) - db = db.Preload(clause.Associations) err := db.Find(&list).Error if err != nil { _ = ctx.Error(err) @@ -89,7 +92,7 @@ func (h AnalysisProfileHandler) List(ctx *gin.Context) { // @accept json // @produce json // @success 201 {object} Profile -// @router /Profiles [post] +// @router /analysis/profiles [post] // @param Profile body AnalysisProfile true "Profile data" func (h AnalysisProfileHandler) Create(ctx *gin.Context) { r := &AnalysisProfile{} @@ -122,7 +125,7 @@ func (h AnalysisProfileHandler) Create(ctx *gin.Context) { // @description Delete a Profile. // @tags Profiles // @success 204 -// @router /Profiles/{id} [delete] +// @router /analysis/profiles/{id} [delete] // @param id path int true "Profile ID" func (h AnalysisProfileHandler) Delete(ctx *gin.Context) { id := h.pk(ctx) @@ -145,10 +148,10 @@ func (h AnalysisProfileHandler) Delete(ctx *gin.Context) { // Update godoc // @summary Update a Profile. // @description Update a Profile. -// @tags Profiles +// @tags AnalysisProfiles // @accept json // @success 204 -// @router /Profiles/{id} [put] +// @router /analysis/profiles/{id} [put] // @param id path int true "Profile ID" // @param Profile body AnalysisProfile true "Profile data" func (h AnalysisProfileHandler) Update(ctx *gin.Context) { @@ -172,26 +175,92 @@ func (h AnalysisProfileHandler) Update(ctx *gin.Context) { h.Status(ctx, http.StatusNoContent) } +// AppProfileList godoc +// @summary List analysis profiles. +// @description List analysis profiles mapped to an application through archetypes. +// @tags AnalysisProfiles +// @produce json +// @success 200 {object} []AnalysisProfile +// @router /applications/{id}/analysis/profiles [get] +// @param id path int true "Application ID" +func (h AnalysisProfileHandler) AppProfileList(ctx *gin.Context) { + resources := []AnalysisProfile{} + // Fetch application. + application := &model.Application{} + id := h.pk(ctx) + db := h.DB(ctx) + db = db.Preload(clause.Associations) + result := db.First(application, id) + if result.Error != nil { + _ = ctx.Error(result.Error) + return + } + // Resolve archetypes and profiles. + memberResolver, err := assessment.NewMembershipResolver(h.DB(ctx)) + if err != nil { + _ = ctx.Error(err) + return + } + var ids []uint + app := assessment.Application{} + app.With(application) + archetypes, err := memberResolver.Archetypes(app) + for _, archetype := range archetypes { + for _, p := range archetype.Profiles { + if p.AnalysisProfileID != nil { + ids = append(ids, *p.AnalysisProfileID) + } + } + } + // Fetch profiles. + var list []model.AnalysisProfile + db = h.DB(ctx) + db = db.Preload(clause.Associations) + err = db.Find(&list, ids).Error + if err != nil { + _ = ctx.Error(err) + return + } + for i := range list { + m := &list[i] + r := AnalysisProfile{} + r.With(m) + resources = append(resources, r) + } + + h.Respond(ctx, http.StatusOK, resources) +} + +// InExList include/exclude list. type InExList = model.InExList +// ApMode analysis mode. +type ApMode struct { + WithDeps bool `json:"withDeps" yaml:"withDeps"` +} + +// ApScope analysis scope. +type ApScope struct { + WithKnownLibs bool `json:"withKnownLibs" yaml:"withKnownLibs"` + Packages InExList `json:"packages,omitempty" yaml:",omitempty"` +} + +// ApRules analysis rules. +type ApRules struct { + Targets []Ref `json:"targets"` + Labels InExList `json:"labels,omitempty" yaml:",omitempty"` + Files []Ref `json:"files,omitempty" yaml:",omitempty"` + Repository *Repository `json:"repository,omitempty" yaml:",omitempty"` +} + // AnalysisProfile REST resource. type AnalysisProfile struct { Resource `yaml:",inline"` - Name string `json:"name"` - Description string `json:"description,omitempty" yaml:",omitempty"` - Mode struct { - WithDeps bool `json:"withDeps" yaml:"withDeps"` - } `json:"mode"` - Scope struct { - WithKnownLibs bool `json:"withKnownLibs" yaml:"withKnownLibs"` - Packages InExList `json:"packages,omitempty" yaml:",omitempty"` - } `json:"scope"` - Rules struct { - Targets []Ref `json:"targets"` - Labels InExList `json:"labels,omitempty" yaml:",omitempty"` - Files []Ref `json:"files,omitempty" yaml:",omitempty"` - Repository *Repository `json:"repository,omitempty" yaml:",omitempty"` - } + Name string `json:"name"` + Description string `json:"description,omitempty" yaml:",omitempty"` + Mode ApMode `json:"mode"` + Scope ApScope `json:"scope"` + Rules ApRules `json:"rules"` } // With updates the resource with the model. diff --git a/test/api/analysisprofile/samples.go b/test/api/analysisprofile/samples.go index cc8763e84..73ff2852a 100644 --- a/test/api/analysisprofile/samples.go +++ b/test/api/analysisprofile/samples.go @@ -5,25 +5,30 @@ import ( ) var ( - Base = api.AnalysisProfile{} + Base = api.AnalysisProfile{ + Name: "Test", + Description: "This is a test analysis profile", + Mode: api.ApMode{WithDeps: true}, + Scope: api.ApScope{ + WithKnownLibs: true, + Packages: api.InExList{ + Included: []string{"pA", "pB"}, + Excluded: []string{"pC", "pD"}, + }, + }, + Rules: api.ApRules{ + Targets: []api.Ref{ + {ID: 2, Name: "Containerization"}, + }, + Labels: api.InExList{ + Included: []string{"rA", "rB"}, + Excluded: []string{"rC", "rD"}, + }, + Files: []api.Ref{ + {ID: 400}, + }, + Repository: &api.Repository{ + URL: "https://github.com/konveyor/testapp", + }, + }} ) - -func init() { - Base.Name = "base" - Base.Description = "Base analysis profiling test." - Base.Mode.WithDeps = true - Base.Scope.WithKnownLibs = true - Base.Scope.Packages.Included = []string{"pA", "pB"} - Base.Scope.Packages.Excluded = []string{"pC", "pD"} - Base.Rules.Targets = []api.Ref{ - {ID: 2, Name: "Containerization"}, - } - Base.Rules.Labels.Included = []string{"A", "B"} - Base.Rules.Labels.Excluded = []string{"C", "D"} - Base.Rules.Files = []api.Ref{ - {ID: 400}, - } - Base.Rules.Repository = &api.Repository{ - URL: "https://github.com/konveyor/testapp", - } -} From e6542b946f617de6d0b7a7f550bac2e2fd1b1d5b Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 09:55:36 -0600 Subject: [PATCH 11/18] checkpoint Signed-off-by: Jeff Ortel --- api/profile.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/profile.go b/api/profile.go index d7c15a5a3..dcae3ac07 100644 --- a/api/profile.go +++ b/api/profile.go @@ -49,6 +49,7 @@ func (h AnalysisProfileHandler) Get(ctx *gin.Context) { id := h.pk(ctx) m := &model.AnalysisProfile{} db := h.DB(ctx) + db = db.Preload(clause.Associations) err := db.First(m, id).Error if err != nil { _ = ctx.Error(err) @@ -70,6 +71,7 @@ func (h AnalysisProfileHandler) List(ctx *gin.Context) { resources := []AnalysisProfile{} var list []model.AnalysisProfile db := h.DB(ctx) + db = db.Preload(clause.Associations) err := db.Find(&list).Error if err != nil { _ = ctx.Error(err) From 0ad0843f28116dc9bd98f21aa9353f884e773c76 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 10:11:40 -0600 Subject: [PATCH 12/18] checkpoint Signed-off-by: Jeff Ortel --- test/api/archetype/api_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/api/archetype/api_test.go b/test/api/archetype/api_test.go index 86d0f357c..9558cbb32 100644 --- a/test/api/archetype/api_test.go +++ b/test/api/archetype/api_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/konveyor/tackle2-hub/api" + "github.com/konveyor/tackle2-hub/test/api/analysisprofile" "github.com/konveyor/tackle2-hub/test/assert" ) @@ -29,14 +30,25 @@ func TestArchetypeCRUD(t *testing.T) { _ = RichClient.Generator.Delete(genC.ID) _ = RichClient.Generator.Delete(genD.ID) }() + // analysis profile. + ap1 := &analysisprofile.Base + err := RichClient.AnalysisProfile.Create(ap1) + assert.Must(t, err) + defer func() { + _ = RichClient.AnalysisProfile.Delete(ap1.ID) + }() // Create. for i := range r.Profiles { p := &r.Profiles[i] + p.AnalysisProfile = &api.Ref{ + ID: ap1.ID, + Name: ap1.Name, + } p.Generators = append( p.Generators, api.Ref{ID: genA.ID, Name: genA.Name}) } - err := Archetype.Create(&r) + err = Archetype.Create(&r) if err != nil { t.Errorf(err.Error()) } From 308383cfeb0a86c93c09a836863029e950d0ebb9 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 10:29:47 -0600 Subject: [PATCH 13/18] checkpoint Signed-off-by: Jeff Ortel --- api/profile.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/profile.go b/api/profile.go index dcae3ac07..c081c5c2e 100644 --- a/api/profile.go +++ b/api/profile.go @@ -173,6 +173,12 @@ func (h AnalysisProfileHandler) Update(ctx *gin.Context) { _ = ctx.Error(err) return } + db = h.DB(ctx).Model(m) + err = db.Association("Targets").Replace(m.Targets) + if err != nil { + _ = ctx.Error(err) + return + } h.Status(ctx, http.StatusNoContent) } From 6e1c678c3e46add93a0b5e0b0bd40422f69ac7eb Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 10:31:31 -0600 Subject: [PATCH 14/18] checkpoint Signed-off-by: Jeff Ortel --- api/profile.go | 4 ++++ hack/add/analysis-profile.sh | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/profile.go b/api/profile.go index c081c5c2e..ac6cba3ed 100644 --- a/api/profile.go +++ b/api/profile.go @@ -213,6 +213,10 @@ func (h AnalysisProfileHandler) AppProfileList(ctx *gin.Context) { app := assessment.Application{} app.With(application) archetypes, err := memberResolver.Archetypes(app) + if err != nil { + _ = ctx.Error(err) + return + } for _, archetype := range archetypes { for _, p := range archetype.Profiles { if p.AnalysisProfileID != nil { diff --git a/hack/add/analysis-profile.sh b/hack/add/analysis-profile.sh index 88c8e07e6..4824992fd 100755 --- a/hack/add/analysis-profile.sh +++ b/hack/add/analysis-profile.sh @@ -3,8 +3,8 @@ host="${HOST:-localhost:8080}" id="${1:-0}" # 0=system-assigned. -name="${1:-Test}-${id}" -repository="${2:-https://github.com/WASdev/sample.daytrader7.git}" +name="${2:-Test}-${id}" +repository="${3:-https://github.com/WASdev/sample.daytrader7.git}" # create application. curl -X POST ${host}/analysis/profiles \ From ea701f7ae67058086b86430f087f4a3e86c5c201 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 10:32:48 -0600 Subject: [PATCH 15/18] checkpoint Signed-off-by: Jeff Ortel --- test/api/analysisprofile/pkg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api/analysisprofile/pkg.go b/test/api/analysisprofile/pkg.go index 567b19fa2..4b403b33c 100644 --- a/test/api/analysisprofile/pkg.go +++ b/test/api/analysisprofile/pkg.go @@ -14,6 +14,6 @@ func init() { // Prepare RichClient and login to Hub API (configured from env variables). RichClient = client.PrepareRichClient() - // Shortcut for RuleSet-related RichClient methods. + // Shortcut for Profile-related RichClient methods. AnalysisProfile = RichClient.AnalysisProfile } From 86109605d3ecc60d48a875782c5674c937c31e2e Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 11:20:02 -0600 Subject: [PATCH 16/18] checkpoint Signed-off-by: Jeff Ortel --- api/profile.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/api/profile.go b/api/profile.go index ac6cba3ed..94e7f2966 100644 --- a/api/profile.go +++ b/api/profile.go @@ -225,19 +225,21 @@ func (h AnalysisProfileHandler) AppProfileList(ctx *gin.Context) { } } // Fetch profiles. - var list []model.AnalysisProfile - db = h.DB(ctx) - db = db.Preload(clause.Associations) - err = db.Find(&list, ids).Error - if err != nil { - _ = ctx.Error(err) - return - } - for i := range list { - m := &list[i] - r := AnalysisProfile{} - r.With(m) - resources = append(resources, r) + if len(ids) > 0 { + db = h.DB(ctx) + db = db.Preload(clause.Associations) + var list []model.AnalysisProfile + err = db.Find(&list, ids).Error + if err != nil { + _ = ctx.Error(err) + return + } + for i := range list { + m := &list[i] + r := AnalysisProfile{} + r.With(m) + resources = append(resources, r) + } } h.Respond(ctx, http.StatusOK, resources) From 9e305d7cc70e99d87ad1451c04aab96e6457bf61 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 11:45:40 -0600 Subject: [PATCH 17/18] checkpoint Signed-off-by: Jeff Ortel --- api/profile.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/profile.go b/api/profile.go index 94e7f2966..9daaa78f3 100644 --- a/api/profile.go +++ b/api/profile.go @@ -168,6 +168,7 @@ func (h AnalysisProfileHandler) Update(ctx *gin.Context) { m.ID = id m.UpdateUser = h.CurrentUser(ctx) db := h.DB(ctx) + db = db.Omit(clause.Associations) err = db.Save(m).Error if err != nil { _ = ctx.Error(err) From dd9f34137f2a589f8f35fa574b26fd52bb76c159 Mon Sep 17 00:00:00 2001 From: Jeff Ortel Date: Mon, 10 Nov 2025 12:36:28 -0600 Subject: [PATCH 18/18] checkpoint Signed-off-by: Jeff Ortel --- api/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/profile.go b/api/profile.go index 9daaa78f3..c72e4025f 100644 --- a/api/profile.go +++ b/api/profile.go @@ -25,7 +25,7 @@ type AnalysisProfileHandler struct { func (h AnalysisProfileHandler) AddRoutes(e *gin.Engine) { routeGroup := e.Group("/") - routeGroup.Use(Required("Profiles")) + routeGroup.Use(Required("Profiles"), Transaction) routeGroup.GET(AnalysisProfileRoot, h.Get) routeGroup.GET(AnalysisProfilesRoot, h.List) routeGroup.GET(AnalysisProfilesRoot+"/", h.List)