diff --git a/hack/next-migration.sh b/hack/next-migration.sh index 87b026805..cec45a7b1 100755 --- a/hack/next-migration.sh +++ b/hack/next-migration.sh @@ -16,7 +16,7 @@ importRoot="github.com/konveyor/tackle2-hub/migration" # # Determine migration versions. # -migrations=($(find ${root} -maxdepth 1 -type d -name 'v*' -printf '%f\n' | sort)) +migrations=($(find ${root} -maxdepth 1 -type d -name 'v*' -printf '%f\n' | cut -c2-10 | sort -n)) current=${migrations[-1]} n=${current#"v"} @@ -31,9 +31,9 @@ echo "Current: ${currentDir}" echo "Next: ${nextDir}" # -# Create directores. +# New package. # -mkdir -p ${nextDir}/model +mkdir -p ${nextDir} # # Build migrate.go @@ -66,40 +66,9 @@ EOF echo "${migrate}" > ${file} # -# Build model/pkg.go +# Copy model # -file=${nextDir}/model/pkg.go -pkg=$(cat << EOF -package model - -import "${importRoot}/${current}/model" - -// -// JSON field (data) type. -type JSON = []byte -EOF -) - -echo "${pkg}" > ${file} - -echo "" >> ${file} -models=$(grep "type" model/pkg.go | grep "model") -echo "${models}" >> ${file} -echo -n " -// -// 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{}{ -" >> ${file} -models=$(grep "{}," ${currentDir}/model/pkg.go) -echo "${models}" | while read m -do - echo -e "\t\t${m}" >> ${file} -done -echo -e "\t}" >> ${file} -echo "}" >> ${file} +cp -r ${currentDir}/model ${nextDir} # # Register new migration. diff --git a/migration/migrate.go b/migration/migrate.go index dc2f53125..7f0e38258 100644 --- a/migration/migrate.go +++ b/migration/migrate.go @@ -6,7 +6,13 @@ import ( liberr "github.com/jortel/go-utils/error" "github.com/konveyor/tackle2-hub/database" "github.com/konveyor/tackle2-hub/model" + "github.com/konveyor/tackle2-hub/nas" "gorm.io/gorm" + "os" + "path" + "regexp" + "strconv" + "strings" ) // @@ -80,7 +86,11 @@ func Migrate(migrations []Migration) (err error) { err = liberr.Wrap(err, "version", ver) return } - + err = writeSchema(db, ver) + if err != nil { + err = liberr.Wrap(err, "version", ver) + return + } err = database.Close(db) if err != nil { err = liberr.Wrap(err, "version", ver) @@ -134,3 +144,62 @@ func autoMigrate(db *gorm.DB, models []interface{}) (err error) { } return } + +// +// writeSchema - writes the migrated schema to a file. +func writeSchema(db *gorm.DB, version int) (err error) { + var list []struct { + Type string `gorm:"column:type"` + Name string `gorm:"column:name"` + Table string `gorm:"column:tbl_name"` + RootPage int `gorm:"column:rootpage"` + SQL string `gorm:"column:sql"` + } + db = db.Table("sqlite_schema") + db = db.Order("1, 2") + err = db.Find(&list).Error + if err != nil { + return + } + dir := path.Join( + path.Dir(Settings.Hub.DB.Path), + "migration") + err = nas.MkDir(dir, 0755) + f, err := os.Create(path.Join(dir, strconv.Itoa(version))) + if err != nil { + return + } + defer func() { + _ = f.Close() + }() + pattern := regexp.MustCompile(`[,()]`) + SQL := func(in string) (out string) { + indent := "\n " + for { + m := pattern.FindStringIndex(in) + if m == nil { + out += in + break + } + out += indent + out += in[:m[0]] + out += indent + out += in[m[0]:m[1]] + in = in[m[1]:] + } + return + } + for _, m := range list { + s := strings.Join([]string{ + m.Type, + m.Name, + m.Table, + SQL(m.SQL), + }, "|") + _, err = f.WriteString(s + "\n") + if err != nil { + return + } + } + return +} diff --git a/migration/v10/model/analysis.go b/migration/v10/model/analysis.go index baa7781df..34efb6de2 100644 --- a/migration/v10/model/analysis.go +++ b/migration/v10/model/analysis.go @@ -1,5 +1,7 @@ package model +import "gorm.io/gorm" + // // Analysis report. type Analysis struct { @@ -76,3 +78,88 @@ type ArchivedIssue struct { Effort int `json:"effort"` Incidents int `json:"incidents"` } + +// +// 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 +} + +// +// 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/migration/v10/model/application.go b/migration/v10/model/application.go new file mode 100644 index 000000000..64e1e52a2 --- /dev/null +++ b/migration/v10/model/application.go @@ -0,0 +1,301 @@ +package model + +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + +type Application struct { + Model + BucketOwner + Name string `gorm:"index;unique;not null"` + Description string + Review *Review `gorm:"constraint:OnDelete:CASCADE"` + Repository JSON `gorm:"type: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 + Ticket *Ticket `gorm:"constraint:OnDelete:CASCADE"` + Assessments []Assessment `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 JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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"` +} + +type Tag struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + 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"` + Username string + Rank uint + 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 `gorm:"type: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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v10/model/assessment.go b/migration/v10/model/assessment.go new file mode 100644 index 000000000..f3eeddfa0 --- /dev/null +++ b/migration/v10/model/assessment.go @@ -0,0 +1,41 @@ +package model + +type Questionnaire struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"unique"` + Description string + Required bool + Sections JSON `gorm:"type:json"` + Thresholds JSON `gorm:"type:json"` + RiskMessages JSON `gorm:"type:json"` + Assessments []Assessment `gorm:"constraint:OnDelete:CASCADE"` +} + +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 JSON `gorm:"type:json"` + Thresholds JSON `gorm:"type:json"` + RiskMessages JSON `gorm:"type: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 +} diff --git a/migration/v10/model/core.go b/migration/v10/model/core.go new file mode 100644 index 000000000..c18e5663e --- /dev/null +++ b/migration/v10/model/core.go @@ -0,0 +1,357 @@ +package model + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/encryption" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Errors JSON + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil + m.Errors = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Error appends an error. +func (m *Task) Error(severity, description string, x ...interface{}) { + var list []TaskError + description = fmt.Sprintf(description, x...) + te := TaskError{Severity: severity, Description: description} + _ = json.Unmarshal(m.Errors, &list) + list = append(list, te) + m.Errors, _ = json.Marshal(list) +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +// +// TaskError used in Task.Errors. +type TaskError struct { + Severity string `json:"severity"` + Description string `json:"description"` +} + +type TaskReport struct { + Model + Status string + Errors JSON + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"index;unique;not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} + +// Encrypt sensitive fields. +// The ref identity is used to determine when sensitive fields +// have changed and need to be (re)encrypted. +func (r *Identity) Encrypt(ref *Identity) (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != ref.Password { + if r.Password != "" { + r.Password, err = aes.Encrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Key != ref.Key { + if r.Key != "" { + r.Key, err = aes.Encrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Settings != ref.Settings { + if r.Settings != "" { + r.Settings, err = aes.Encrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + return +} + +// Decrypt sensitive fields. +func (r *Identity) Decrypt() (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != "" { + r.Password, err = aes.Decrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Key != "" { + r.Key, err = aes.Decrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Settings != "" { + r.Settings, err = aes.Decrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + return +} diff --git a/migration/v10/model/pkg.go b/migration/v10/model/pkg.go index 4d66fc80b..46c071c59 100644 --- a/migration/v10/model/pkg.go +++ b/migration/v10/model/pkg.go @@ -1,47 +1,15 @@ package model -import "github.com/konveyor/tackle2-hub/migration/v9/model" +import "github.com/konveyor/tackle2-hub/settings" + +var ( + Settings = &settings.Settings +) // // JSON field (data) type. type JSON = []byte -type Model = model.Model -type Application = model.Application -type Archetype = model.Archetype -type Assessment = model.Assessment -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 Questionnaire = model.Questionnaire -type Review = model.Review -type Setting = model.Setting -type RuleSet = model.RuleSet -type Rule = model.Rule -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 -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 diff --git a/migration/v2/model/application.go b/migration/v2/model/application.go index 5b8c6af09..1c17a8c8a 100644 --- a/migration/v2/model/application.go +++ b/migration/v2/model/application.go @@ -1,5 +1,11 @@ package model +import ( + "fmt" + "gorm.io/gorm" + "sync" +) + type Application struct { Model BucketOwner @@ -16,3 +22,170 @@ type Application struct { BusinessServiceID *uint `gorm:"index"` BusinessService *BusinessService } + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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 + 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 +} + +type StakeholderGroup struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Description string + Stakeholders []Stakeholder `gorm:"many2many:StakeholderGroupStakeholder;constraint:OnDelete:CASCADE"` +} + +type Tag struct { + Model + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + TagTypeID uint `gorm:"uniqueIndex:tagA;index;not null"` + TagType TagType +} + +type TagType struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Rank uint + Color string + Tags []Tag `gorm:"constraint:OnDelete:CASCADE"` +} + +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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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("tagType%v", i+1)] = tag.TagType + 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 + TagType string + ImportID uint `gorm:"index"` + Import *Import +} diff --git a/migration/v2/model/review.go b/migration/v2/model/assessment.go similarity index 100% rename from migration/v2/model/review.go rename to migration/v2/model/assessment.go diff --git a/migration/v2/model/base.go b/migration/v2/model/base.go deleted file mode 100644 index 9f52d75d0..000000000 --- a/migration/v2/model/base.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -import ( - "time" -) - -// -// Model Base model. -type Model struct { - ID uint `gorm:"<-:create;primaryKey"` - CreateTime time.Time `gorm:"<-:create;autoCreateTime"` - CreateUser string `gorm:"<-:create"` - UpdateUser string -} diff --git a/migration/v2/model/bucket.go b/migration/v2/model/bucket.go deleted file mode 100644 index a2bb9fd09..000000000 --- a/migration/v2/model/bucket.go +++ /dev/null @@ -1,28 +0,0 @@ -package model - -import ( - "github.com/google/uuid" - liberr "github.com/jortel/go-utils/error" - "gorm.io/gorm" - "os" - "path" -) - -type BucketOwner struct { - Bucket string `gorm:"index"` -} - -func (m *BucketOwner) BeforeCreate(db *gorm.DB) (err error) { - uid := uuid.New() - m.Bucket = path.Join( - Settings.Hub.Bucket.Path, - uid.String()) - err = os.MkdirAll(m.Bucket, 0777) - if err != nil { - err = liberr.Wrap( - err, - "path", - m.Bucket) - } - return -} diff --git a/migration/v2/model/businessservice.go b/migration/v2/model/businessservice.go deleted file mode 100644 index 2eb888546..000000000 --- a/migration/v2/model/businessservice.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -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 -} diff --git a/migration/v2/model/core.go b/migration/v2/model/core.go new file mode 100644 index 000000000..32ab4fb82 --- /dev/null +++ b/migration/v2/model/core.go @@ -0,0 +1,225 @@ +package model + +import ( + "encoding/json" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +type BucketOwner struct { + Bucket string `gorm:"index"` +} + +func (m *BucketOwner) BeforeCreate(db *gorm.DB) (err error) { + uid := uuid.New() + m.Bucket = path.Join( + Settings.Hub.Bucket.Path, + uid.String()) + err = os.MkdirAll(m.Bucket, 0777) + if err != nil { + err = liberr.Wrap( + err, + "path", + m.Bucket) + } + return +} + +type Task struct { + Model + BucketOwner + Name string `gorm:"index"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON `gorm:"type:json"` + Data JSON `gorm:"type:json"` + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Error string + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + if m.TaskGroupID == nil { + err = m.BucketOwner.BeforeCreate(db) + } + m.Reset() + return +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON `gorm:"type:json"` + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON `gorm:"type:json"` + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.Bucket = m.Bucket + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +type TaskReport struct { + Model + Status string + Error string + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} diff --git a/migration/v2/model/dependency.go b/migration/v2/model/dependency.go deleted file mode 100644 index 6aa1974c9..000000000 --- a/migration/v2/model/dependency.go +++ /dev/null @@ -1,56 +0,0 @@ -package model - -import ( - "gorm.io/gorm" - "sync" -) - -// -// 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 -} - -// -// Validation Hook to avoid 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 -} - -// -// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. -type DependencyCyclicError struct{} - -func (err DependencyCyclicError) Error() string { - return "cyclic dependencies are not allowed" -} diff --git a/migration/v2/model/identity.go b/migration/v2/model/identity.go deleted file mode 100644 index 8625c0a7c..000000000 --- a/migration/v2/model/identity.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -// -// Identity represents and identity with a set of credentials. -type Identity struct { - Model - Kind string `gorm:"not null"` - Name string `gorm:"not null"` - Description string - User string - Password string - Key string - Settings string - Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` -} diff --git a/migration/v2/model/import.go b/migration/v2/model/import.go deleted file mode 100644 index 6ccdd9fac..000000000 --- a/migration/v2/model/import.go +++ /dev/null @@ -1,52 +0,0 @@ -package model - -import "fmt" - -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 -} - -func (r *Import) AsMap() (m map[string]interface{}) { - m = make(map[string]interface{}) - 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("tagType%v", i+1)] = tag.TagType - m[fmt.Sprintf("tag%v", i+1)] = tag.Name - } - return -} diff --git a/migration/v2/model/importsummary.go b/migration/v2/model/importsummary.go deleted file mode 100644 index f635f4de1..000000000 --- a/migration/v2/model/importsummary.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -type ImportSummary struct { - Model - Content []byte - Filename string - ImportStatus string - Imports []Import `gorm:"constraint:OnDelete:CASCADE"` - CreateEntities bool -} diff --git a/migration/v2/model/importtag.go b/migration/v2/model/importtag.go deleted file mode 100644 index 2c55cc67b..000000000 --- a/migration/v2/model/importtag.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type ImportTag struct { - Model - Name string - TagType string - ImportID uint `gorm:"index"` - Import *Import -} diff --git a/migration/v2/model/jobfunction.go b/migration/v2/model/jobfunction.go deleted file mode 100644 index fc14c08d7..000000000 --- a/migration/v2/model/jobfunction.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type JobFunction struct { - Model - Username string - Name string `gorm:"index;unique;not null"` - Stakeholders []Stakeholder `gorm:"constraint:OnDelete:SET NULL"` -} diff --git a/migration/v2/model/proxy.go b/migration/v2/model/proxy.go deleted file mode 100644 index 78794cc86..000000000 --- a/migration/v2/model/proxy.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -// -// Proxy configuration. -// kind = (http|https) -type Proxy struct { - Model - Enabled bool - Kind string `gorm:"uniqueIndex"` - Host string `gorm:"not null"` - Port int - Excluded JSON `gorm:"type:json"` - IdentityID *uint `gorm:"index"` - Identity *Identity -} diff --git a/migration/v2/model/setting.go b/migration/v2/model/setting.go deleted file mode 100644 index bd81324d1..000000000 --- a/migration/v2/model/setting.go +++ /dev/null @@ -1,7 +0,0 @@ -package model - -type Setting struct { - Model - Key string `gorm:"<-:create;uniqueIndex"` - Value JSON `gorm:"type:json"` -} diff --git a/migration/v2/model/stakeholder.go b/migration/v2/model/stakeholder.go deleted file mode 100644 index 99cb2973f..000000000 --- a/migration/v2/model/stakeholder.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -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 -} diff --git a/migration/v2/model/stakeholdergroup.go b/migration/v2/model/stakeholdergroup.go deleted file mode 100644 index 82ea86f3b..000000000 --- a/migration/v2/model/stakeholdergroup.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type StakeholderGroup struct { - Model - Name string `gorm:"index;unique;not null"` - Username string - Description string - Stakeholders []Stakeholder `gorm:"many2many:StakeholderGroupStakeholder;constraint:OnDelete:CASCADE"` -} diff --git a/migration/v2/model/tag.go b/migration/v2/model/tag.go deleted file mode 100644 index 0203ef863..000000000 --- a/migration/v2/model/tag.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type Tag struct { - Model - Name string `gorm:"uniqueIndex:tagA;not null"` - Username string - TagTypeID uint `gorm:"uniqueIndex:tagA;index;not null"` - TagType TagType -} diff --git a/migration/v2/model/tagtype.go b/migration/v2/model/tagtype.go deleted file mode 100644 index fd1cab43f..000000000 --- a/migration/v2/model/tagtype.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -type TagType struct { - Model - Name string `gorm:"index;unique;not null"` - Username string - Rank uint - Color string - Tags []Tag `gorm:"constraint:OnDelete:CASCADE"` -} diff --git a/migration/v2/model/task.go b/migration/v2/model/task.go deleted file mode 100644 index 235484ada..000000000 --- a/migration/v2/model/task.go +++ /dev/null @@ -1,61 +0,0 @@ -package model - -import ( - "gorm.io/gorm" - "time" -) - -type Task struct { - Model - BucketOwner - Name string `gorm:"index"` - Addon string `gorm:"index"` - Locator string `gorm:"index"` - Priority int - Image string - Variant string - Policy string - TTL JSON `gorm:"type:json"` - Data JSON `gorm:"type:json"` - Started *time.Time - Terminated *time.Time - State string `gorm:"index"` - Error string - Pod string `gorm:"index"` - Retries int - Canceled bool - Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` - ApplicationID *uint - Application *Application - TaskGroupID *uint `gorm:"<-:create"` - TaskGroup *TaskGroup -} - -func (m *Task) Reset() { - m.Started = nil - m.Terminated = nil - m.Report = nil -} - -func (m *Task) BeforeCreate(db *gorm.DB) (err error) { - if m.TaskGroupID == nil { - err = m.BucketOwner.BeforeCreate(db) - } - m.Reset() - return -} - -// -// Map alias. -type Map = map[string]interface{} - -// -// TTL time-to-live. -type TTL struct { - Created int `json:"created,omitempty"` - Pending int `json:"pending,omitempty"` - Postponed int `json:"postponed,omitempty"` - Running int `json:"running,omitempty"` - Succeeded int `json:"succeeded,omitempty"` - Failed int `json:"failed,omitempty"` -} diff --git a/migration/v2/model/taskgroup.go b/migration/v2/model/taskgroup.go deleted file mode 100644 index 2e58e74bc..000000000 --- a/migration/v2/model/taskgroup.go +++ /dev/null @@ -1,91 +0,0 @@ -package model - -import ( - "encoding/json" - liberr "github.com/jortel/go-utils/error" -) - -type TaskGroup struct { - Model - BucketOwner - Name string - Addon string - Data JSON `gorm:"type:json"` - Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` - List JSON `gorm:"type:json"` - State string -} - -// -// Propagate group data into the task. -func (m *TaskGroup) Propagate() (err error) { - for i := range m.Tasks { - task := &m.Tasks[i] - task.State = m.State - task.Bucket = m.Bucket - if task.Addon == "" { - task.Addon = m.Addon - } - if m.Data == nil { - continue - } - a := Map{} - err = json.Unmarshal(m.Data, &a) - if err != nil { - err = liberr.Wrap( - err, - "id", - m.ID) - return - } - b := Map{} - err = json.Unmarshal(task.Data, &b) - if err != nil { - err = liberr.Wrap( - err, - "id", - m.ID) - return - } - task.Data, _ = json.Marshal(m.merge(a, b)) - } - - return -} - -// -// merge maps B into A. -// The B map is the authority. -func (m *TaskGroup) merge(a, b Map) (out Map) { - if a == nil { - a = Map{} - } - if b == nil { - b = Map{} - } - out = Map{} - // - // Merge-in elements found in B and in A. - for k, v := range a { - out[k] = v - if bv, found := b[k]; found { - out[k] = bv - if av, cast := v.(Map); cast { - if bv, cast := bv.(Map); cast { - out[k] = m.merge(av, bv) - } else { - out[k] = bv - } - } - } - } - // - // Add elements found only in B. - for k, v := range b { - if _, found := a[k]; !found { - out[k] = v - } - } - - return -} diff --git a/migration/v2/model/taskreport.go b/migration/v2/model/taskreport.go deleted file mode 100644 index bd9393582..000000000 --- a/migration/v2/model/taskreport.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -type TaskReport struct { - Model - Status string - Error string - Total int - Completed int - Activity JSON `gorm:"type:json"` - Result JSON `gorm:"type:json"` - TaskID uint `gorm:"<-:create;uniqueIndex"` - Task *Task -} diff --git a/migration/v3/migrate.go b/migration/v3/migrate.go index 758be4e64..deed54cb4 100644 --- a/migration/v3/migrate.go +++ b/migration/v3/migrate.go @@ -17,7 +17,7 @@ type Migration struct{} func (r Migration) Apply(db *gorm.DB) (err error) { // // Tags/Categories. - err = db.Migrator().RenameTable(model.TagType{}, model.TagCategory{}) + err = db.Migrator().RenameTable(v2.TagType{}, model.TagCategory{}) if err != nil { err = liberr.Wrap(err) return diff --git a/migration/v3/model/rulebundle.go b/migration/v3/model/analysis.go similarity index 54% rename from migration/v3/model/rulebundle.go rename to migration/v3/model/analysis.go index ec4340df9..48e8dafdb 100644 --- a/migration/v3/model/rulebundle.go +++ b/migration/v3/model/analysis.go @@ -15,3 +15,16 @@ type RuleBundle struct { Identity *Identity RuleSets []RuleSet `gorm:"constraint:OnDelete:CASCADE"` } + +// +// RuleSet - Analysis ruleset. +type RuleSet struct { + Model + Name string `gorm:"uniqueIndex:RuleSetA;not null"` + Description string + Metadata JSON `gorm:"type:json"` + RuleBundleID uint `gorm:"uniqueIndex:RuleSetA;not null"` + RuleBundle *RuleBundle + FileID *uint `gorm:"index" ref:"file"` + File *File +} diff --git a/migration/v3/model/application.go b/migration/v3/model/application.go index e1aa101bf..9c5db0f16 100644 --- a/migration/v3/model/application.go +++ b/migration/v3/model/application.go @@ -1,5 +1,12 @@ package model +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + type Application struct { Model BucketOwner @@ -16,3 +23,252 @@ type Application struct { BusinessServiceID *uint `gorm:"index"` BusinessService *BusinessService } + +type Fact struct { + ApplicationID uint `gorm:"<-:create;primaryKey"` + Key string `gorm:"<-:create;primaryKey"` + Value JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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 + 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 +} + +type StakeholderGroup struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Description string + Stakeholders []Stakeholder `gorm:"many2many:StakeholderGroupStakeholder;constraint:OnDelete:CASCADE"` +} + +type Tag struct { + Model + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + CategoryID uint `gorm:"uniqueIndex:tagA;index;not null"` + Category TagCategory +} + +type TagCategory struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Rank uint + 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 `gorm:"type: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 Metadata struct { + Projects []Project `json:"projects"` +} + +type Project struct { + ID string `json:"id"` + Key string `json:"key"` + Name string `json:"name"` + IssueTypes []IssueType `json:"issueTypes"` +} + +type IssueType struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type Tracker struct { + Model + Name string `gorm:"index;unique;not null"` + URL string + Kind string + Identity *Identity + IdentityID uint + Metadata JSON `gorm:"type:json"` + Connected bool + LastUpdated time.Time + Message string + Tickets []Ticket `gorm:"constraint:OnDelete:CASCADE"` +} + +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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v3/model/applicationtag.go b/migration/v3/model/applicationtag.go deleted file mode 100644 index d6cb5974d..000000000 --- a/migration/v3/model/applicationtag.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -// -// 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" -} diff --git a/migration/v9/model/review.go b/migration/v3/model/assessment.go similarity index 69% rename from migration/v9/model/review.go rename to migration/v3/model/assessment.go index 6c113f579..e4936d2c6 100644 --- a/migration/v9/model/review.go +++ b/migration/v3/model/assessment.go @@ -7,8 +7,6 @@ type Review struct { ProposedAction string `gorm:"not null"` WorkPriority uint `gorm:"not null"` Comments string - ApplicationID *uint `gorm:"uniqueIndex"` + ApplicationID uint `gorm:"uniqueIndex"` Application *Application - ArchetypeID *uint `gorm:"uniqueIndex"` - Archetype *Archetype } diff --git a/migration/v3/model/bucket.go b/migration/v3/model/bucket.go deleted file mode 100644 index a856729da..000000000 --- a/migration/v3/model/bucket.go +++ /dev/null @@ -1,56 +0,0 @@ -package model - -import ( - "github.com/google/uuid" - liberr "github.com/jortel/go-utils/error" - "gorm.io/gorm" - "os" - "path" - "time" -) - -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 -} diff --git a/migration/v3/model/core.go b/migration/v3/model/core.go new file mode 100644 index 000000000..bd253bb69 --- /dev/null +++ b/migration/v3/model/core.go @@ -0,0 +1,273 @@ +package model + +import ( + "encoding/json" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Error string + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +type TaskReport struct { + Model + Status string + Error string + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} diff --git a/migration/v3/model/fact.go b/migration/v3/model/fact.go deleted file mode 100644 index 77b6cc384..000000000 --- a/migration/v3/model/fact.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type Fact struct { - ApplicationID uint `gorm:"<-:create;primaryKey"` - Key string `gorm:"<-:create;primaryKey"` - Value JSON `gorm:"type:json;not null"` - Application *Application -} diff --git a/migration/v3/model/file.go b/migration/v3/model/file.go deleted file mode 100644 index 97606a8b0..000000000 --- a/migration/v3/model/file.go +++ /dev/null @@ -1,33 +0,0 @@ -package model - -import ( - "github.com/google/uuid" - liberr "github.com/jortel/go-utils/error" - "gorm.io/gorm" - "os" - "path" - "time" -) - -type File struct { - Model - Name 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 -} diff --git a/migration/v3/model/import.go b/migration/v3/model/import.go deleted file mode 100644 index 680f8ad4b..000000000 --- a/migration/v3/model/import.go +++ /dev/null @@ -1,52 +0,0 @@ -package model - -import "fmt" - -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 -} - -func (r *Import) AsMap() (m map[string]interface{}) { - m = make(map[string]interface{}) - 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 -} diff --git a/migration/v3/model/importsummary.go b/migration/v3/model/importsummary.go deleted file mode 100644 index f635f4de1..000000000 --- a/migration/v3/model/importsummary.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -type ImportSummary struct { - Model - Content []byte - Filename string - ImportStatus string - Imports []Import `gorm:"constraint:OnDelete:CASCADE"` - CreateEntities bool -} diff --git a/migration/v3/model/importtag.go b/migration/v3/model/importtag.go deleted file mode 100644 index bf5166e12..000000000 --- a/migration/v3/model/importtag.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type ImportTag struct { - Model - Name string - Category string - ImportID uint `gorm:"index"` - Import *Import -} diff --git a/migration/v3/model/pkg.go b/migration/v3/model/pkg.go index f8bf03300..8a631093e 100644 --- a/migration/v3/model/pkg.go +++ b/migration/v3/model/pkg.go @@ -1,7 +1,6 @@ package model import ( - v2 "github.com/konveyor/tackle2-hub/migration/v2/model" "github.com/konveyor/tackle2-hub/settings" ) @@ -13,25 +12,6 @@ var ( // JSON field (data) type. type JSON = []byte -// -// Unchanged models imported from previous migration. -type Model = v2.Model -type BusinessService = v2.BusinessService -type Dependency = v2.Dependency -type Identity = v2.Identity -type JobFunction = v2.JobFunction -type Proxy = v2.Proxy -type Review = v2.Review -type Setting = v2.Setting -type Stakeholder = v2.Stakeholder -type StakeholderGroup = v2.StakeholderGroup -type TagType = v2.TagType -type TaskReport = v2.TaskReport - -// -// Errors -type DependencyCyclicError = v2.DependencyCyclicError - // // All builds all models. // Models are enumerated such that each are listed after diff --git a/migration/v3/model/ruleset.go b/migration/v3/model/ruleset.go deleted file mode 100644 index da558305d..000000000 --- a/migration/v3/model/ruleset.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -// -// RuleSet - Analysis ruleset. -type RuleSet struct { - Model - Name string `gorm:"uniqueIndex:RuleSetA;not null"` - Description string - Metadata JSON `gorm:"type:json"` - RuleBundleID uint `gorm:"uniqueIndex:RuleSetA;not null"` - RuleBundle *RuleBundle - FileID *uint `gorm:"index" ref:"file"` - File *File -} diff --git a/migration/v3/model/tag.go b/migration/v3/model/tag.go deleted file mode 100644 index 164dc63bb..000000000 --- a/migration/v3/model/tag.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type Tag struct { - Model - Name string `gorm:"uniqueIndex:tagA;not null"` - Username string - CategoryID uint `gorm:"uniqueIndex:tagA;index;not null"` - Category TagCategory -} diff --git a/migration/v3/model/tagcategory.go b/migration/v3/model/tagcategory.go deleted file mode 100644 index 10368768b..000000000 --- a/migration/v3/model/tagcategory.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -type TagCategory struct { - Model - Name string `gorm:"index;unique;not null"` - Username string - Rank uint - Color string - Tags []Tag `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE"` -} diff --git a/migration/v3/model/task.go b/migration/v3/model/task.go deleted file mode 100644 index 56c28c56c..000000000 --- a/migration/v3/model/task.go +++ /dev/null @@ -1,59 +0,0 @@ -package model - -import ( - "gorm.io/gorm" - "time" -) - -type Task struct { - Model - BucketOwner - Name string `gorm:"index"` - Addon string `gorm:"index"` - Locator string `gorm:"index"` - Priority int - Image string - Variant string - Policy string - TTL JSON - Data JSON - Started *time.Time - Terminated *time.Time - State string `gorm:"index"` - Error string - Pod string `gorm:"index"` - Retries int - Canceled bool - Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` - ApplicationID *uint - Application *Application - TaskGroupID *uint `gorm:"<-:create"` - TaskGroup *TaskGroup -} - -func (m *Task) Reset() { - m.Started = nil - m.Terminated = nil - m.Report = nil -} - -func (m *Task) BeforeCreate(db *gorm.DB) (err error) { - err = m.BucketOwner.BeforeCreate(db) - m.Reset() - return -} - -// -// Map alias. -type Map = map[string]interface{} - -// -// TTL time-to-live. -type TTL struct { - Created int `json:"created,omitempty"` - Pending int `json:"pending,omitempty"` - Postponed int `json:"postponed,omitempty"` - Running int `json:"running,omitempty"` - Succeeded int `json:"succeeded,omitempty"` - Failed int `json:"failed,omitempty"` -} diff --git a/migration/v3/model/taskgroup.go b/migration/v3/model/taskgroup.go deleted file mode 100644 index cc61ce221..000000000 --- a/migration/v3/model/taskgroup.go +++ /dev/null @@ -1,91 +0,0 @@ -package model - -import ( - "encoding/json" - liberr "github.com/jortel/go-utils/error" -) - -type TaskGroup struct { - Model - BucketOwner - Name string - Addon string - Data JSON - Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` - List JSON - State string -} - -// -// Propagate group data into the task. -func (m *TaskGroup) Propagate() (err error) { - for i := range m.Tasks { - task := &m.Tasks[i] - task.State = m.State - task.SetBucket(m.BucketID) - if task.Addon == "" { - task.Addon = m.Addon - } - if m.Data == nil { - continue - } - a := Map{} - err = json.Unmarshal(m.Data, &a) - if err != nil { - err = liberr.Wrap( - err, - "id", - m.ID) - return - } - b := Map{} - err = json.Unmarshal(task.Data, &b) - if err != nil { - err = liberr.Wrap( - err, - "id", - m.ID) - return - } - task.Data, _ = json.Marshal(m.merge(a, b)) - } - - return -} - -// -// merge maps B into A. -// The B map is the authority. -func (m *TaskGroup) merge(a, b Map) (out Map) { - if a == nil { - a = Map{} - } - if b == nil { - b = Map{} - } - out = Map{} - // - // Merge-in elements found in B and in A. - for k, v := range a { - out[k] = v - if bv, found := b[k]; found { - out[k] = bv - if av, cast := v.(Map); cast { - if bv, cast := bv.(Map); cast { - out[k] = m.merge(av, bv) - } else { - out[k] = bv - } - } - } - } - // - // Add elements found only in B. - for k, v := range b { - if _, found := a[k]; !found { - out[k] = v - } - } - - return -} diff --git a/migration/v3/model/ticket.go b/migration/v3/model/ticket.go deleted file mode 100644 index 4d83c0f63..000000000 --- a/migration/v3/model/ticket.go +++ /dev/null @@ -1,46 +0,0 @@ -package model - -import "time" - -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 `gorm:"type: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 Metadata struct { - Projects []Project `json:"projects"` -} - -type Project struct { - ID string `json:"id"` - Key string `json:"key"` - Name string `json:"name"` - IssueTypes []IssueType `json:"issueTypes"` -} - -type IssueType struct { - ID string `json:"id"` - Name string `json:"name"` -} diff --git a/migration/v3/model/tracker.go b/migration/v3/model/tracker.go deleted file mode 100644 index 5b3030e15..000000000 --- a/migration/v3/model/tracker.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -import "time" - -type Tracker struct { - Model - Name string `gorm:"index;unique;not null"` - URL string - Kind string - Identity *Identity - IdentityID uint - Metadata JSON `gorm:"type:json"` - Connected bool - LastUpdated time.Time - Message string - Tickets []Ticket `gorm:"constraint:OnDelete:CASCADE"` -} diff --git a/migration/v4/model/analysis.go b/migration/v4/model/analysis.go new file mode 100644 index 000000000..48e8dafdb --- /dev/null +++ b/migration/v4/model/analysis.go @@ -0,0 +1,30 @@ +package model + +// +// RuleBundle - Analysis rules. +type RuleBundle struct { + Model + Kind string + Name string `gorm:"uniqueIndex;not null"` + Description string + Custom bool + Repository JSON `gorm:"type:json"` + ImageID uint `gorm:"index" ref:"file"` + Image *File + IdentityID *uint `gorm:"index"` + Identity *Identity + RuleSets []RuleSet `gorm:"constraint:OnDelete:CASCADE"` +} + +// +// RuleSet - Analysis ruleset. +type RuleSet struct { + Model + Name string `gorm:"uniqueIndex:RuleSetA;not null"` + Description string + Metadata JSON `gorm:"type:json"` + RuleBundleID uint `gorm:"uniqueIndex:RuleSetA;not null"` + RuleBundle *RuleBundle + FileID *uint `gorm:"index" ref:"file"` + File *File +} diff --git a/migration/v4/model/application.go b/migration/v4/model/application.go index 44023729a..071a68440 100644 --- a/migration/v4/model/application.go +++ b/migration/v4/model/application.go @@ -1,5 +1,12 @@ package model +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + type Application struct { Model BucketOwner @@ -21,3 +28,266 @@ type Application struct { MigrationWaveID *uint `gorm:"index"` MigrationWave *MigrationWave } + +type Fact struct { + ApplicationID uint `gorm:"<-:create;primaryKey"` + Key string `gorm:"<-:create;primaryKey"` + Value JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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 + 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"` +} + +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"` +} + +type MigrationWave struct { + Model + Name string + StartDate time.Time + EndDate time.Time + Applications []Application `gorm:"constraint:OnDelete:SET NULL"` + Stakeholders []Stakeholder `gorm:"many2many:MigrationWaveStakeholders;constraint:OnDelete:CASCADE"` + StakeholderGroups []StakeholderGroup `gorm:"many2many:MigrationWaveStakeholderGroups;constraint:OnDelete:CASCADE"` +} + +type Tag struct { + Model + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + CategoryID uint `gorm:"uniqueIndex:tagA;index;not null"` + Category TagCategory +} + +type TagCategory struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Rank uint + 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 `gorm:"type: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 Metadata struct { + Projects []Project `json:"projects"` +} + +type Project struct { + ID string `json:"id"` + Key string `json:"key"` + Name string `json:"name"` + IssueTypes []IssueType `json:"issueTypes"` +} + +type IssueType struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type Tracker struct { + Model + Name string `gorm:"index;unique;not null"` + URL string + Kind string + Identity *Identity + IdentityID uint + Metadata JSON `gorm:"type:json"` + Connected bool + LastUpdated time.Time + Message string + Tickets []Ticket `gorm:"constraint:OnDelete:CASCADE"` +} + +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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v4/model/applicationtag.go b/migration/v4/model/applicationtag.go deleted file mode 100644 index d6cb5974d..000000000 --- a/migration/v4/model/applicationtag.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -// -// 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" -} diff --git a/migration/v4/model/assessment.go b/migration/v4/model/assessment.go new file mode 100644 index 000000000..e4936d2c6 --- /dev/null +++ b/migration/v4/model/assessment.go @@ -0,0 +1,12 @@ +package model + +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 +} diff --git a/migration/v4/model/businessservice.go b/migration/v4/model/businessservice.go deleted file mode 100644 index 2eb888546..000000000 --- a/migration/v4/model/businessservice.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -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 -} diff --git a/migration/v4/model/core.go b/migration/v4/model/core.go new file mode 100644 index 000000000..bd253bb69 --- /dev/null +++ b/migration/v4/model/core.go @@ -0,0 +1,273 @@ +package model + +import ( + "encoding/json" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Error string + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +type TaskReport struct { + Model + Status string + Error string + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} diff --git a/migration/v4/model/jobfunction.go b/migration/v4/model/jobfunction.go deleted file mode 100644 index fc14c08d7..000000000 --- a/migration/v4/model/jobfunction.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type JobFunction struct { - Model - Username string - Name string `gorm:"index;unique;not null"` - Stakeholders []Stakeholder `gorm:"constraint:OnDelete:SET NULL"` -} diff --git a/migration/v4/model/migrationwave.go b/migration/v4/model/migrationwave.go deleted file mode 100644 index 66052cc40..000000000 --- a/migration/v4/model/migrationwave.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -import "time" - -type MigrationWave struct { - Model - Name string - StartDate time.Time - EndDate time.Time - Applications []Application `gorm:"constraint:OnDelete:SET NULL"` - Stakeholders []Stakeholder `gorm:"many2many:MigrationWaveStakeholders;constraint:OnDelete:CASCADE"` - StakeholderGroups []StakeholderGroup `gorm:"many2many:MigrationWaveStakeholderGroups;constraint:OnDelete:CASCADE"` -} diff --git a/migration/v4/model/pkg.go b/migration/v4/model/pkg.go index d03a52fee..f333361bb 100644 --- a/migration/v4/model/pkg.go +++ b/migration/v4/model/pkg.go @@ -1,7 +1,6 @@ package model import ( - v3 "github.com/konveyor/tackle2-hub/migration/v3/model" "github.com/konveyor/tackle2-hub/settings" ) @@ -13,39 +12,6 @@ var ( // JSON field (data) type. type JSON = []byte -// -// Unchanged models imported from previous migration. -type Model = v3.Model -type Bucket = v3.Bucket -type BucketOwner = v3.BucketOwner -type Dependency = v3.Dependency -type Fact = v3.Fact -type File = v3.File -type Identity = v3.Identity -type Import = v3.Import -type ImportSummary = v3.ImportSummary -type ImportTag = v3.ImportTag -type Proxy = v3.Proxy -type Review = v3.Review -type RuleBundle = v3.RuleBundle -type RuleSet = v3.RuleSet -type Setting = v3.Setting -type Tag = v3.Tag -type TagCategory = v3.TagCategory -type Task = v3.Task -type TaskGroup = v3.TaskGroup -type TaskReport = v3.TaskReport -type Ticket = v3.Ticket -type Tracker = v3.Tracker -type TTL = v3.TTL -type Metadata = v3.Metadata -type Project = v3.Project -type IssueType = v3.IssueType - -// -// Errors -type DependencyCyclicError = v3.DependencyCyclicError - // // All builds all models. // Models are enumerated such that each are listed after diff --git a/migration/v4/model/stakeholder.go b/migration/v4/model/stakeholder.go deleted file mode 100644 index 66243f8f7..000000000 --- a/migration/v4/model/stakeholder.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -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"` -} diff --git a/migration/v4/model/stakeholdergroup.go b/migration/v4/model/stakeholdergroup.go deleted file mode 100644 index 384373cd0..000000000 --- a/migration/v4/model/stakeholdergroup.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -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"` -} diff --git a/migration/v5/model/analysis.go b/migration/v5/model/analysis.go index 12c72ab41..3624852ea 100644 --- a/migration/v5/model/analysis.go +++ b/migration/v5/model/analysis.go @@ -61,3 +61,32 @@ type Link struct { URL string `json:"url"` Title string `json:"title,omitempty"` } + +// +// RuleSet - Analysis ruleset. +type RuleSet struct { + Model + Kind string + Name string `gorm:"uniqueIndex;not null"` + Description string + Custom bool + Repository JSON `gorm:"type:json"` + ImageID uint `gorm:"index" ref:"file"` + Image *File + IdentityID *uint `gorm:"index"` + Identity *Identity + Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` +} + +// +// 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/v5/model/application.go b/migration/v5/model/application.go index fa1f5566b..aae434219 100644 --- a/migration/v5/model/application.go +++ b/migration/v5/model/application.go @@ -1,5 +1,12 @@ package model +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + type Application struct { Model BucketOwner @@ -23,3 +30,251 @@ type Application struct { MigrationWave *MigrationWave Ticket *Ticket `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 JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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 + 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"` +} + +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"` +} + +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 Tag struct { + Model + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + CategoryID uint `gorm:"uniqueIndex:tagA;index;not null"` + Category TagCategory +} + +type TagCategory struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Rank uint + 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 `gorm:"type: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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v5/model/applicationtag.go b/migration/v5/model/applicationtag.go deleted file mode 100644 index d6cb5974d..000000000 --- a/migration/v5/model/applicationtag.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -// -// 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" -} diff --git a/migration/v5/model/assessment.go b/migration/v5/model/assessment.go new file mode 100644 index 000000000..e4936d2c6 --- /dev/null +++ b/migration/v5/model/assessment.go @@ -0,0 +1,12 @@ +package model + +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 +} diff --git a/migration/v5/model/businessservice.go b/migration/v5/model/businessservice.go deleted file mode 100644 index 2eb888546..000000000 --- a/migration/v5/model/businessservice.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -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 -} diff --git a/migration/v5/model/core.go b/migration/v5/model/core.go new file mode 100644 index 000000000..a64b1e5b7 --- /dev/null +++ b/migration/v5/model/core.go @@ -0,0 +1,337 @@ +package model + +import ( + "encoding/json" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/encryption" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Error string + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +type TaskReport struct { + Model + Status string + Error string + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"index;unique;not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} + +// Encrypt sensitive fields. +// The ref identity is used to determine when sensitive fields +// have changed and need to be (re)encrypted. +func (r *Identity) Encrypt(ref *Identity) (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != ref.Password { + if r.Password != "" { + r.Password, err = aes.Encrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Key != ref.Key { + if r.Key != "" { + r.Key, err = aes.Encrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Settings != ref.Settings { + if r.Settings != "" { + r.Settings, err = aes.Encrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + return +} + +// Decrypt sensitive fields. +func (r *Identity) Decrypt() (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != "" { + r.Password, err = aes.Decrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Key != "" { + r.Key, err = aes.Decrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Settings != "" { + r.Settings, err = aes.Decrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + return +} diff --git a/migration/v5/model/fact.go b/migration/v5/model/fact.go deleted file mode 100644 index 71b3dbe9f..000000000 --- a/migration/v5/model/fact.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type Fact struct { - ApplicationID uint `gorm:"<-:create;primaryKey"` - Key string `gorm:"<-:create;primaryKey"` - Source string `gorm:"<-:create;primaryKey;not null"` - Value JSON `gorm:"type:json;not null"` - Application *Application -} diff --git a/migration/v5/model/identity.go b/migration/v5/model/identity.go deleted file mode 100644 index d0134ec86..000000000 --- a/migration/v5/model/identity.go +++ /dev/null @@ -1,83 +0,0 @@ -package model - -import ( - liberr "github.com/jortel/go-utils/error" - "github.com/konveyor/tackle2-hub/encryption" -) - -// Identity represents and identity with a set of credentials. -type Identity struct { - Model - Kind string `gorm:"not null"` - Name string `gorm:"index;unique;not null"` - Description string - User string - Password string - Key string - Settings string - Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` -} - -// Encrypt sensitive fields. -// The ref identity is used to determine when sensitive fields -// have changed and need to be (re)encrypted. -func (r *Identity) Encrypt(ref *Identity) (err error) { - passphrase := Settings.Encryption.Passphrase - aes := encryption.New(passphrase) - if r.Password != ref.Password { - if r.Password != "" { - r.Password, err = aes.Encrypt(r.Password) - if err != nil { - err = liberr.Wrap(err) - return - } - } - } - if r.Key != ref.Key { - if r.Key != "" { - r.Key, err = aes.Encrypt(r.Key) - if err != nil { - err = liberr.Wrap(err) - return - } - } - } - if r.Settings != ref.Settings { - if r.Settings != "" { - r.Settings, err = aes.Encrypt(r.Settings) - if err != nil { - err = liberr.Wrap(err) - return - } - } - } - return -} - -// Decrypt sensitive fields. -func (r *Identity) Decrypt() (err error) { - passphrase := Settings.Encryption.Passphrase - aes := encryption.New(passphrase) - if r.Password != "" { - r.Password, err = aes.Decrypt(r.Password) - if err != nil { - err = liberr.Wrap(err) - return - } - } - if r.Key != "" { - r.Key, err = aes.Decrypt(r.Key) - if err != nil { - err = liberr.Wrap(err) - return - } - } - if r.Settings != "" { - r.Settings, err = aes.Decrypt(r.Settings) - if err != nil { - err = liberr.Wrap(err) - return - } - } - return -} diff --git a/migration/v5/model/jobfunction.go b/migration/v5/model/jobfunction.go deleted file mode 100644 index fc14c08d7..000000000 --- a/migration/v5/model/jobfunction.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type JobFunction struct { - Model - Username string - Name string `gorm:"index;unique;not null"` - Stakeholders []Stakeholder `gorm:"constraint:OnDelete:SET NULL"` -} diff --git a/migration/v5/model/migrationwave.go b/migration/v5/model/migrationwave.go deleted file mode 100644 index 8cec17fee..000000000 --- a/migration/v5/model/migrationwave.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -import "time" - -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"` -} diff --git a/migration/v5/model/pkg.go b/migration/v5/model/pkg.go index 5294d6629..4ce2479de 100644 --- a/migration/v5/model/pkg.go +++ b/migration/v5/model/pkg.go @@ -1,7 +1,6 @@ package model import ( - v4 "github.com/konveyor/tackle2-hub/migration/v4/model" "github.com/konveyor/tackle2-hub/settings" ) @@ -13,30 +12,6 @@ var ( // JSON field (data) type. type JSON = []byte -// -// Unchanged models imported from previous migration. -type Model = v4.Model -type Bucket = v4.Bucket -type BucketOwner = v4.BucketOwner -type Dependency = v4.Dependency -type File = v4.File -type Import = v4.Import -type ImportSummary = v4.ImportSummary -type ImportTag = v4.ImportTag -type Proxy = v4.Proxy -type Review = v4.Review -type Setting = v4.Setting -type Tag = v4.Tag -type TagCategory = v4.TagCategory -type Task = v4.Task -type TaskGroup = v4.TaskGroup -type TaskReport = v4.TaskReport -type TTL = v4.TTL - -// -// Errors -type DependencyCyclicError = v4.DependencyCyclicError - // // All builds all models. // Models are enumerated such that each are listed after diff --git a/migration/v5/model/ruleset.go b/migration/v5/model/ruleset.go deleted file mode 100644 index 92551d428..000000000 --- a/migration/v5/model/ruleset.go +++ /dev/null @@ -1,30 +0,0 @@ -package model - -// -// RuleSet - Analysis ruleset. -type RuleSet struct { - Model - Kind string - Name string `gorm:"uniqueIndex;not null"` - Description string - Custom bool - Repository JSON `gorm:"type:json"` - ImageID uint `gorm:"index" ref:"file"` - Image *File - IdentityID *uint `gorm:"index"` - Identity *Identity - Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` -} - -// -// 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/v5/model/stakeholder.go b/migration/v5/model/stakeholder.go deleted file mode 100644 index 66243f8f7..000000000 --- a/migration/v5/model/stakeholder.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -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"` -} diff --git a/migration/v5/model/stakeholdergroup.go b/migration/v5/model/stakeholdergroup.go deleted file mode 100644 index 384373cd0..000000000 --- a/migration/v5/model/stakeholdergroup.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -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"` -} diff --git a/migration/v5/model/ticket.go b/migration/v5/model/ticket.go deleted file mode 100644 index e0c67a049..000000000 --- a/migration/v5/model/ticket.go +++ /dev/null @@ -1,30 +0,0 @@ -package model - -import "time" - -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 `gorm:"type: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"` -} diff --git a/migration/v5/model/tracker.go b/migration/v5/model/tracker.go deleted file mode 100644 index 8fe40c225..000000000 --- a/migration/v5/model/tracker.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -import "time" - -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 -} diff --git a/migration/v6/model/analysis.go b/migration/v6/model/analysis.go index 0f7d223de..43f3e8e1a 100644 --- a/migration/v6/model/analysis.go +++ b/migration/v6/model/analysis.go @@ -1,5 +1,7 @@ package model +import "gorm.io/gorm" + // // Analysis report. type Analysis struct { @@ -62,3 +64,67 @@ type Link struct { URL string `json:"url"` Title string `json:"title,omitempty"` } + +// +// RuleSet - Analysis ruleset. +type RuleSet struct { + Model + Kind string + Name string `gorm:"uniqueIndex;not null"` + Description string + Custom bool + Repository JSON `gorm:"type:json"` + ImageID uint `gorm:"index" ref:"file"` + Image *File + IdentityID *uint `gorm:"index"` + Identity *Identity + Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` + DependsOn []RuleSet `gorm:"many2many:RuleSetDependencies;constraint:OnDelete:CASCADE"` +} + +// +// 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/v6/model/application.go b/migration/v6/model/application.go new file mode 100644 index 000000000..aae434219 --- /dev/null +++ b/migration/v6/model/application.go @@ -0,0 +1,280 @@ +package model + +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + +type Application struct { + Model + BucketOwner + Name string `gorm:"index;unique;not null"` + Description string + Review *Review `gorm:"constraint:OnDelete:CASCADE"` + Repository JSON `gorm:"type: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 + Ticket *Ticket `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 JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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 + 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"` +} + +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"` +} + +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 Tag struct { + Model + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + CategoryID uint `gorm:"uniqueIndex:tagA;index;not null"` + Category TagCategory +} + +type TagCategory struct { + Model + Name string `gorm:"index;unique;not null"` + Username string + Rank uint + 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 `gorm:"type: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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v6/model/assessment.go b/migration/v6/model/assessment.go new file mode 100644 index 000000000..e4936d2c6 --- /dev/null +++ b/migration/v6/model/assessment.go @@ -0,0 +1,12 @@ +package model + +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 +} diff --git a/migration/v6/model/core.go b/migration/v6/model/core.go new file mode 100644 index 000000000..6d6557ae6 --- /dev/null +++ b/migration/v6/model/core.go @@ -0,0 +1,356 @@ +package model + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/encryption" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Errors JSON + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil + m.Errors = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Error appends an error. +func (m *Task) Error(severity, description string, x ...interface{}) { + var list []TaskError + description = fmt.Sprintf(description, x...) + te := TaskError{Severity: severity, Description: description} + _ = json.Unmarshal(m.Errors, &list) + list = append(list, te) + m.Errors, _ = json.Marshal(list) +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +// +// TaskError used in Task.Errors. +type TaskError struct { + Severity string `json:"severity"` + Description string `json:"description"` +} + +type TaskReport struct { + Model + Status string + Errors JSON + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"index;unique;not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} + +// Encrypt sensitive fields. +// The ref identity is used to determine when sensitive fields +// have changed and need to be (re)encrypted. +func (r *Identity) Encrypt(ref *Identity) (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != ref.Password { + if r.Password != "" { + r.Password, err = aes.Encrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Key != ref.Key { + if r.Key != "" { + r.Key, err = aes.Encrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Settings != ref.Settings { + if r.Settings != "" { + r.Settings, err = aes.Encrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + return +} + +// Decrypt sensitive fields. +func (r *Identity) Decrypt() (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != "" { + r.Password, err = aes.Decrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Key != "" { + r.Key, err = aes.Decrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Settings != "" { + r.Settings, err = aes.Decrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + return +} diff --git a/migration/v6/model/pkg.go b/migration/v6/model/pkg.go index e58d282a4..33c97725e 100644 --- a/migration/v6/model/pkg.go +++ b/migration/v6/model/pkg.go @@ -1,37 +1,15 @@ package model -import "github.com/konveyor/tackle2-hub/migration/v5/model" +import "github.com/konveyor/tackle2-hub/settings" + +var ( + Settings = &settings.Settings +) // // JSON field (data) type. type JSON = []byte -type Model = model.Model -type Application = model.Application -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 Ticket = model.Ticket -type Tracker = model.Tracker -type ApplicationTag = model.ApplicationTag -type DependencyCyclicError = model.DependencyCyclicError - // // All builds all models. // Models are enumerated such that each are listed after diff --git a/migration/v6/model/ruleset.go b/migration/v6/model/ruleset.go deleted file mode 100644 index 032818cb9..000000000 --- a/migration/v6/model/ruleset.go +++ /dev/null @@ -1,67 +0,0 @@ -package model - -import "gorm.io/gorm" - -// -// RuleSet - Analysis ruleset. -type RuleSet struct { - Model - Kind string - Name string `gorm:"uniqueIndex;not null"` - Description string - Custom bool - Repository JSON `gorm:"type:json"` - ImageID uint `gorm:"index" ref:"file"` - Image *File - IdentityID *uint `gorm:"index"` - Identity *Identity - Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` - DependsOn []RuleSet `gorm:"many2many:RuleSetDependencies;constraint:OnDelete:CASCADE"` -} - -// -// 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/v6/model/task.go b/migration/v6/model/task.go deleted file mode 100644 index c74f45db7..000000000 --- a/migration/v6/model/task.go +++ /dev/null @@ -1,92 +0,0 @@ -package model - -import ( - "encoding/json" - "fmt" - "gorm.io/gorm" - "time" -) - -type Task struct { - Model - BucketOwner - Name string `gorm:"index"` - Addon string `gorm:"index"` - Locator string `gorm:"index"` - Priority int - Image string - Variant string - Policy string - TTL JSON - Data JSON - Started *time.Time - Terminated *time.Time - State string `gorm:"index"` - Errors JSON - Pod string `gorm:"index"` - Retries int - Canceled bool - Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` - ApplicationID *uint - Application *Application - TaskGroupID *uint `gorm:"<-:create"` - TaskGroup *TaskGroup -} - -func (m *Task) Reset() { - m.Started = nil - m.Terminated = nil - m.Report = nil - m.Errors = nil -} - -func (m *Task) BeforeCreate(db *gorm.DB) (err error) { - err = m.BucketOwner.BeforeCreate(db) - m.Reset() - return -} - -// -// Error appends an error. -func (m *Task) Error(severity, description string, x ...interface{}) { - var list []TaskError - description = fmt.Sprintf(description, x...) - te := TaskError{Severity: severity, Description: description} - _ = json.Unmarshal(m.Errors, &list) - list = append(list, te) - m.Errors, _ = json.Marshal(list) -} - -// -// Map alias. -type Map = map[string]interface{} - -// -// TTL time-to-live. -type TTL struct { - Created int `json:"created,omitempty"` - Pending int `json:"pending,omitempty"` - Postponed int `json:"postponed,omitempty"` - Running int `json:"running,omitempty"` - Succeeded int `json:"succeeded,omitempty"` - Failed int `json:"failed,omitempty"` -} - -// -// TaskError used in Task.Errors. -type TaskError struct { - Severity string `json:"severity"` - Description string `json:"description"` -} - -type TaskReport struct { - Model - Status string - Errors JSON - Total int - Completed int - Activity JSON `gorm:"type:json"` - Result JSON `gorm:"type:json"` - TaskID uint `gorm:"<-:create;uniqueIndex"` - Task *Task -} diff --git a/migration/v6/model/taskgroup.go b/migration/v6/model/taskgroup.go deleted file mode 100644 index cc61ce221..000000000 --- a/migration/v6/model/taskgroup.go +++ /dev/null @@ -1,91 +0,0 @@ -package model - -import ( - "encoding/json" - liberr "github.com/jortel/go-utils/error" -) - -type TaskGroup struct { - Model - BucketOwner - Name string - Addon string - Data JSON - Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` - List JSON - State string -} - -// -// Propagate group data into the task. -func (m *TaskGroup) Propagate() (err error) { - for i := range m.Tasks { - task := &m.Tasks[i] - task.State = m.State - task.SetBucket(m.BucketID) - if task.Addon == "" { - task.Addon = m.Addon - } - if m.Data == nil { - continue - } - a := Map{} - err = json.Unmarshal(m.Data, &a) - if err != nil { - err = liberr.Wrap( - err, - "id", - m.ID) - return - } - b := Map{} - err = json.Unmarshal(task.Data, &b) - if err != nil { - err = liberr.Wrap( - err, - "id", - m.ID) - return - } - task.Data, _ = json.Marshal(m.merge(a, b)) - } - - return -} - -// -// merge maps B into A. -// The B map is the authority. -func (m *TaskGroup) merge(a, b Map) (out Map) { - if a == nil { - a = Map{} - } - if b == nil { - b = Map{} - } - out = Map{} - // - // Merge-in elements found in B and in A. - for k, v := range a { - out[k] = v - if bv, found := b[k]; found { - out[k] = bv - if av, cast := v.(Map); cast { - if bv, cast := bv.(Map); cast { - out[k] = m.merge(av, bv) - } else { - out[k] = bv - } - } - } - } - // - // Add elements found only in B. - for k, v := range b { - if _, found := a[k]; !found { - out[k] = v - } - } - - return -} diff --git a/migration/v7/model/analysis.go b/migration/v7/model/analysis.go new file mode 100644 index 000000000..a3f1ea1f2 --- /dev/null +++ b/migration/v7/model/analysis.go @@ -0,0 +1,131 @@ +package model + +import "gorm.io/gorm" + +// +// Analysis report. +type Analysis struct { + Model + Effort int + Issues []Issue `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 JSON `gorm:"type:json"` + AnalysisID uint `gorm:"index;uniqueIndex:depA;not null"` + Analysis *Analysis +} + +// +// Issue report issue (violation). +type Issue struct { + Model + RuleSet string `gorm:"uniqueIndex:issueA;not null"` + Rule string `gorm:"uniqueIndex:issueA;not null"` + Name string `gorm:"index"` + Description string + Category string `gorm:"index;not null"` + Incidents []Incident `gorm:"foreignKey:IssueID;constraint:OnDelete:CASCADE"` + Links JSON `gorm:"type:json"` + Facts JSON `gorm:"type:json"` + Labels JSON `gorm:"type:json"` + Effort int `gorm:"index;not null"` + AnalysisID uint `gorm:"index;uniqueIndex:issueA;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 `gorm:"type:json"` + IssueID uint `gorm:"index;not null"` + Issue *Issue +} + +// +// Link URL link. +type Link struct { + URL string `json:"url"` + Title string `json:"title,omitempty"` +} + +// +// RuleSet - Analysis ruleset. +type RuleSet struct { + Model + UUID *string `gorm:"uniqueIndex"` + Kind string + Name string `gorm:"uniqueIndex;not null"` + Description string + Custom bool + Repository JSON `gorm:"type:json"` + ImageID uint `gorm:"index" ref:"file"` + Image *File + IdentityID *uint `gorm:"index"` + Identity *Identity + Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` + DependsOn []RuleSet `gorm:"many2many:RuleSetDependencies;constraint:OnDelete:CASCADE"` +} + +// +// 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/v7/model/application.go b/migration/v7/model/application.go index fa1f5566b..f604ccfe0 100644 --- a/migration/v7/model/application.go +++ b/migration/v7/model/application.go @@ -1,5 +1,12 @@ package model +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + type Application struct { Model BucketOwner @@ -23,3 +30,254 @@ type Application struct { MigrationWave *MigrationWave Ticket *Ticket `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 JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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"` +} + +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"` +} + +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 Tag struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + 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"` + Username string + Rank uint + 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 `gorm:"type: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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v7/model/applicationtag.go b/migration/v7/model/applicationtag.go deleted file mode 100644 index d6cb5974d..000000000 --- a/migration/v7/model/applicationtag.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -// -// 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" -} diff --git a/migration/v7/model/assessment.go b/migration/v7/model/assessment.go new file mode 100644 index 000000000..e4936d2c6 --- /dev/null +++ b/migration/v7/model/assessment.go @@ -0,0 +1,12 @@ +package model + +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 +} diff --git a/migration/v7/model/businessservice.go b/migration/v7/model/businessservice.go deleted file mode 100644 index 2eb888546..000000000 --- a/migration/v7/model/businessservice.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -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 -} diff --git a/migration/v7/model/core.go b/migration/v7/model/core.go new file mode 100644 index 000000000..c18e5663e --- /dev/null +++ b/migration/v7/model/core.go @@ -0,0 +1,357 @@ +package model + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/encryption" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Errors JSON + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil + m.Errors = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Error appends an error. +func (m *Task) Error(severity, description string, x ...interface{}) { + var list []TaskError + description = fmt.Sprintf(description, x...) + te := TaskError{Severity: severity, Description: description} + _ = json.Unmarshal(m.Errors, &list) + list = append(list, te) + m.Errors, _ = json.Marshal(list) +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +// +// TaskError used in Task.Errors. +type TaskError struct { + Severity string `json:"severity"` + Description string `json:"description"` +} + +type TaskReport struct { + Model + Status string + Errors JSON + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"index;unique;not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} + +// Encrypt sensitive fields. +// The ref identity is used to determine when sensitive fields +// have changed and need to be (re)encrypted. +func (r *Identity) Encrypt(ref *Identity) (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != ref.Password { + if r.Password != "" { + r.Password, err = aes.Encrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Key != ref.Key { + if r.Key != "" { + r.Key, err = aes.Encrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Settings != ref.Settings { + if r.Settings != "" { + r.Settings, err = aes.Encrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + return +} + +// Decrypt sensitive fields. +func (r *Identity) Decrypt() (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != "" { + r.Password, err = aes.Decrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Key != "" { + r.Key, err = aes.Decrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Settings != "" { + r.Settings, err = aes.Decrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + return +} diff --git a/migration/v7/model/jobfunction.go b/migration/v7/model/jobfunction.go deleted file mode 100644 index d8fc8438c..000000000 --- a/migration/v7/model/jobfunction.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type JobFunction struct { - Model - UUID *string `gorm:"uniqueIndex"` - Username string - Name string `gorm:"index;unique;not null"` - Stakeholders []Stakeholder `gorm:"constraint:OnDelete:SET NULL"` -} diff --git a/migration/v7/model/migrationwave.go b/migration/v7/model/migrationwave.go deleted file mode 100644 index 8cec17fee..000000000 --- a/migration/v7/model/migrationwave.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -import "time" - -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"` -} diff --git a/migration/v7/model/pkg.go b/migration/v7/model/pkg.go index b179224ab..111324a64 100644 --- a/migration/v7/model/pkg.go +++ b/migration/v7/model/pkg.go @@ -1,35 +1,14 @@ package model -import "github.com/konveyor/tackle2-hub/migration/v6/model" +import "github.com/konveyor/tackle2-hub/settings" // // JSON field (data) type. type JSON = []byte -type Model = model.Model -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 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 Proxy = model.Proxy -type Review = model.Review -type Setting = model.Setting -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 DependencyCyclicError = model.DependencyCyclicError +var ( + Settings = &settings.Settings +) // // All builds all models. diff --git a/migration/v7/model/ruleset.go b/migration/v7/model/ruleset.go deleted file mode 100644 index b40d56993..000000000 --- a/migration/v7/model/ruleset.go +++ /dev/null @@ -1,68 +0,0 @@ -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 - Custom bool - Repository JSON `gorm:"type:json"` - ImageID uint `gorm:"index" ref:"file"` - Image *File - IdentityID *uint `gorm:"index"` - Identity *Identity - Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` - DependsOn []RuleSet `gorm:"many2many:RuleSetDependencies;constraint:OnDelete:CASCADE"` -} - -// -// 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/v7/model/stakeholder.go b/migration/v7/model/stakeholder.go deleted file mode 100644 index 66243f8f7..000000000 --- a/migration/v7/model/stakeholder.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -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"` -} diff --git a/migration/v7/model/stakeholdergroup.go b/migration/v7/model/stakeholdergroup.go deleted file mode 100644 index 384373cd0..000000000 --- a/migration/v7/model/stakeholdergroup.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -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"` -} diff --git a/migration/v7/model/tag.go b/migration/v7/model/tag.go deleted file mode 100644 index 80c8aa1ba..000000000 --- a/migration/v7/model/tag.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -type Tag struct { - Model - UUID *string `gorm:"uniqueIndex"` - Name string `gorm:"uniqueIndex:tagA;not null"` - Username string - CategoryID uint `gorm:"uniqueIndex:tagA;index;not null"` - Category TagCategory -} diff --git a/migration/v7/model/tagcategory.go b/migration/v7/model/tagcategory.go deleted file mode 100644 index cd9ac559b..000000000 --- a/migration/v7/model/tagcategory.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type TagCategory struct { - Model - UUID *string `gorm:"uniqueIndex"` - Name string `gorm:"index;unique;not null"` - Username string - Rank uint - Color string - Tags []Tag `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE"` -} diff --git a/migration/v8/model/analysis.go b/migration/v8/model/analysis.go new file mode 100644 index 000000000..3c9e2e52e --- /dev/null +++ b/migration/v8/model/analysis.go @@ -0,0 +1,151 @@ +package model + +import "gorm.io/gorm" + +// +// Analysis report. +type Analysis struct { + Model + Effort int + Issues []Issue `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 JSON `gorm:"type:json"` + AnalysisID uint `gorm:"index;uniqueIndex:depA;not null"` + Analysis *Analysis +} + +// +// Issue report issue (violation). +type Issue struct { + Model + RuleSet string `gorm:"uniqueIndex:issueA;not null"` + Rule string `gorm:"uniqueIndex:issueA;not null"` + Name string `gorm:"index"` + Description string + Category string `gorm:"index;not null"` + Incidents []Incident `gorm:"foreignKey:IssueID;constraint:OnDelete:CASCADE"` + Links JSON `gorm:"type:json"` + Facts JSON `gorm:"type:json"` + Labels JSON `gorm:"type:json"` + Effort int `gorm:"index;not null"` + AnalysisID uint `gorm:"index;uniqueIndex:issueA;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 `gorm:"type:json"` + IssueID uint `gorm:"index;not null"` + Issue *Issue +} + +// +// Link URL link. +type Link struct { + URL string `json:"url"` + Title string `json:"title,omitempty"` +} + +// +// 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 +} + +// +// 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/migration/v8/model/application.go b/migration/v8/model/application.go new file mode 100644 index 000000000..f604ccfe0 --- /dev/null +++ b/migration/v8/model/application.go @@ -0,0 +1,283 @@ +package model + +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + +type Application struct { + Model + BucketOwner + Name string `gorm:"index;unique;not null"` + Description string + Review *Review `gorm:"constraint:OnDelete:CASCADE"` + Repository JSON `gorm:"type: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 + Ticket *Ticket `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 JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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"` +} + +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"` +} + +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 Tag struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + 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"` + Username string + Rank uint + 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 `gorm:"type: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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v8/model/assessment.go b/migration/v8/model/assessment.go new file mode 100644 index 000000000..e4936d2c6 --- /dev/null +++ b/migration/v8/model/assessment.go @@ -0,0 +1,12 @@ +package model + +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 +} diff --git a/migration/v8/model/core.go b/migration/v8/model/core.go new file mode 100644 index 000000000..c18e5663e --- /dev/null +++ b/migration/v8/model/core.go @@ -0,0 +1,357 @@ +package model + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/encryption" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Errors JSON + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil + m.Errors = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Error appends an error. +func (m *Task) Error(severity, description string, x ...interface{}) { + var list []TaskError + description = fmt.Sprintf(description, x...) + te := TaskError{Severity: severity, Description: description} + _ = json.Unmarshal(m.Errors, &list) + list = append(list, te) + m.Errors, _ = json.Marshal(list) +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +// +// TaskError used in Task.Errors. +type TaskError struct { + Severity string `json:"severity"` + Description string `json:"description"` +} + +type TaskReport struct { + Model + Status string + Errors JSON + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"index;unique;not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} + +// Encrypt sensitive fields. +// The ref identity is used to determine when sensitive fields +// have changed and need to be (re)encrypted. +func (r *Identity) Encrypt(ref *Identity) (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != ref.Password { + if r.Password != "" { + r.Password, err = aes.Encrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Key != ref.Key { + if r.Key != "" { + r.Key, err = aes.Encrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Settings != ref.Settings { + if r.Settings != "" { + r.Settings, err = aes.Encrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + return +} + +// Decrypt sensitive fields. +func (r *Identity) Decrypt() (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != "" { + r.Password, err = aes.Decrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Key != "" { + r.Key, err = aes.Decrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Settings != "" { + r.Settings, err = aes.Decrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + return +} diff --git a/migration/v8/model/pkg.go b/migration/v8/model/pkg.go index 269614bed..615ee9e66 100644 --- a/migration/v8/model/pkg.go +++ b/migration/v8/model/pkg.go @@ -1,44 +1,14 @@ package model -import "github.com/konveyor/tackle2-hub/migration/v7/model" +import "github.com/konveyor/tackle2-hub/settings" // // 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 +var ( + Settings = &settings.Settings +) // // All builds all models. diff --git a/migration/v8/model/ruleset.go b/migration/v8/model/ruleset.go deleted file mode 100644 index 4edf0c021..000000000 --- a/migration/v8/model/ruleset.go +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index f6e0dfe54..000000000 --- a/migration/v8/model/target.go +++ /dev/null @@ -1,20 +0,0 @@ -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/migration/v9/model/analysis.go b/migration/v9/model/analysis.go new file mode 100644 index 000000000..3c9e2e52e --- /dev/null +++ b/migration/v9/model/analysis.go @@ -0,0 +1,151 @@ +package model + +import "gorm.io/gorm" + +// +// Analysis report. +type Analysis struct { + Model + Effort int + Issues []Issue `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 JSON `gorm:"type:json"` + AnalysisID uint `gorm:"index;uniqueIndex:depA;not null"` + Analysis *Analysis +} + +// +// Issue report issue (violation). +type Issue struct { + Model + RuleSet string `gorm:"uniqueIndex:issueA;not null"` + Rule string `gorm:"uniqueIndex:issueA;not null"` + Name string `gorm:"index"` + Description string + Category string `gorm:"index;not null"` + Incidents []Incident `gorm:"foreignKey:IssueID;constraint:OnDelete:CASCADE"` + Links JSON `gorm:"type:json"` + Facts JSON `gorm:"type:json"` + Labels JSON `gorm:"type:json"` + Effort int `gorm:"index;not null"` + AnalysisID uint `gorm:"index;uniqueIndex:issueA;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 `gorm:"type:json"` + IssueID uint `gorm:"index;not null"` + Issue *Issue +} + +// +// Link URL link. +type Link struct { + URL string `json:"url"` + Title string `json:"title,omitempty"` +} + +// +// 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 +} + +// +// 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/migration/v9/model/application.go b/migration/v9/model/application.go index b9673835f..64e1e52a2 100644 --- a/migration/v9/model/application.go +++ b/migration/v9/model/application.go @@ -1,5 +1,12 @@ package model +import ( + "fmt" + "gorm.io/gorm" + "sync" + "time" +) + type Application struct { Model BucketOwner @@ -24,3 +31,271 @@ type Application struct { Ticket *Ticket `gorm:"constraint:OnDelete:CASCADE"` Assessments []Assessment `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 JSON `gorm:"type:json;not null"` + 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" +} + +// +// 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 +} + +// +// Validation Hook to avoid 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 +} + +// +// Custom error type to allow API recognize Cyclic Dependency error and assign proper status code. +type DependencyCyclicError struct{} + +func (err DependencyCyclicError) Error() string { + return "cyclic dependencies are not allowed" +} + +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"` +} + +type Tag struct { + Model + UUID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex:tagA;not null"` + Username string + 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"` + Username string + Rank uint + 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 `gorm:"type: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 +} + +func (r *Import) AsMap() (m map[string]interface{}) { + m = make(map[string]interface{}) + 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 +} diff --git a/migration/v9/model/applicationtag.go b/migration/v9/model/applicationtag.go deleted file mode 100644 index d6cb5974d..000000000 --- a/migration/v9/model/applicationtag.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -// -// 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" -} diff --git a/migration/v9/model/archetype.go b/migration/v9/model/archetype.go deleted file mode 100644 index 795732479..000000000 --- a/migration/v9/model/archetype.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -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"` -} diff --git a/migration/v9/model/assessment.go b/migration/v9/model/assessment.go index 54bf99977..f3eeddfa0 100644 --- a/migration/v9/model/assessment.go +++ b/migration/v9/model/assessment.go @@ -26,3 +26,16 @@ type Assessment struct { 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 +} diff --git a/migration/v9/model/core.go b/migration/v9/model/core.go new file mode 100644 index 000000000..c18e5663e --- /dev/null +++ b/migration/v9/model/core.go @@ -0,0 +1,357 @@ +package model + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + liberr "github.com/jortel/go-utils/error" + "github.com/konveyor/tackle2-hub/encryption" + "gorm.io/gorm" + "os" + "path" + "time" +) + +// +// Model Base model. +type Model struct { + ID uint `gorm:"<-:create;primaryKey"` + CreateTime time.Time `gorm:"<-:create;autoCreateTime"` + CreateUser string `gorm:"<-:create"` + UpdateUser string +} + +type Setting struct { + Model + Key string `gorm:"<-:create;uniqueIndex"` + Value JSON `gorm:"type:json"` +} + +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 + 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"` + Addon string `gorm:"index"` + Locator string `gorm:"index"` + Priority int + Image string + Variant string + Policy string + TTL JSON + Data JSON + Started *time.Time + Terminated *time.Time + State string `gorm:"index"` + Errors JSON + Pod string `gorm:"index"` + Retries int + Canceled bool + Report *TaskReport `gorm:"constraint:OnDelete:CASCADE"` + ApplicationID *uint + Application *Application + TaskGroupID *uint `gorm:"<-:create"` + TaskGroup *TaskGroup +} + +func (m *Task) Reset() { + m.Started = nil + m.Terminated = nil + m.Report = nil + m.Errors = nil +} + +func (m *Task) BeforeCreate(db *gorm.DB) (err error) { + err = m.BucketOwner.BeforeCreate(db) + m.Reset() + return +} + +// +// Error appends an error. +func (m *Task) Error(severity, description string, x ...interface{}) { + var list []TaskError + description = fmt.Sprintf(description, x...) + te := TaskError{Severity: severity, Description: description} + _ = json.Unmarshal(m.Errors, &list) + list = append(list, te) + m.Errors, _ = json.Marshal(list) +} + +// +// Map alias. +type Map = map[string]interface{} + +// +// TTL time-to-live. +type TTL struct { + Created int `json:"created,omitempty"` + Pending int `json:"pending,omitempty"` + Postponed int `json:"postponed,omitempty"` + Running int `json:"running,omitempty"` + Succeeded int `json:"succeeded,omitempty"` + Failed int `json:"failed,omitempty"` +} + +// +// TaskError used in Task.Errors. +type TaskError struct { + Severity string `json:"severity"` + Description string `json:"description"` +} + +type TaskReport struct { + Model + Status string + Errors JSON + Total int + Completed int + Activity JSON `gorm:"type:json"` + Result JSON `gorm:"type:json"` + TaskID uint `gorm:"<-:create;uniqueIndex"` + Task *Task +} + +type TaskGroup struct { + Model + BucketOwner + Name string + Addon string + Data JSON + Tasks []Task `gorm:"constraint:OnDelete:CASCADE"` + List JSON + State string +} + +// +// Propagate group data into the task. +func (m *TaskGroup) Propagate() (err error) { + for i := range m.Tasks { + task := &m.Tasks[i] + task.State = m.State + task.SetBucket(m.BucketID) + if task.Addon == "" { + task.Addon = m.Addon + } + if m.Data == nil { + continue + } + a := Map{} + err = json.Unmarshal(m.Data, &a) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + b := Map{} + err = json.Unmarshal(task.Data, &b) + if err != nil { + err = liberr.Wrap( + err, + "id", + m.ID) + return + } + task.Data, _ = json.Marshal(m.merge(a, b)) + } + + return +} + +// +// merge maps B into A. +// The B map is the authority. +func (m *TaskGroup) merge(a, b Map) (out Map) { + if a == nil { + a = Map{} + } + if b == nil { + b = Map{} + } + out = Map{} + // + // Merge-in elements found in B and in A. + for k, v := range a { + out[k] = v + if bv, found := b[k]; found { + out[k] = bv + if av, cast := v.(Map); cast { + if bv, cast := bv.(Map); cast { + out[k] = m.merge(av, bv) + } else { + out[k] = bv + } + } + } + } + // + // Add elements found only in B. + for k, v := range b { + if _, found := a[k]; !found { + out[k] = v + } + } + + return +} + +// +// Proxy configuration. +// kind = (http|https) +type Proxy struct { + Model + Enabled bool + Kind string `gorm:"uniqueIndex"` + Host string `gorm:"not null"` + Port int + Excluded JSON `gorm:"type:json"` + IdentityID *uint `gorm:"index"` + Identity *Identity +} + +// Identity represents and identity with a set of credentials. +type Identity struct { + Model + Kind string `gorm:"not null"` + Name string `gorm:"index;unique;not null"` + Description string + User string + Password string + Key string + Settings string + Proxies []Proxy `gorm:"constraint:OnDelete:SET NULL"` +} + +// Encrypt sensitive fields. +// The ref identity is used to determine when sensitive fields +// have changed and need to be (re)encrypted. +func (r *Identity) Encrypt(ref *Identity) (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != ref.Password { + if r.Password != "" { + r.Password, err = aes.Encrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Key != ref.Key { + if r.Key != "" { + r.Key, err = aes.Encrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + if r.Settings != ref.Settings { + if r.Settings != "" { + r.Settings, err = aes.Encrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + } + return +} + +// Decrypt sensitive fields. +func (r *Identity) Decrypt() (err error) { + passphrase := Settings.Encryption.Passphrase + aes := encryption.New(passphrase) + if r.Password != "" { + r.Password, err = aes.Decrypt(r.Password) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Key != "" { + r.Key, err = aes.Decrypt(r.Key) + if err != nil { + err = liberr.Wrap(err) + return + } + } + if r.Settings != "" { + r.Settings, err = aes.Decrypt(r.Settings) + if err != nil { + err = liberr.Wrap(err) + return + } + } + return +} diff --git a/migration/v9/model/migrationwave.go b/migration/v9/model/migrationwave.go deleted file mode 100644 index 8cec17fee..000000000 --- a/migration/v9/model/migrationwave.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -import "time" - -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"` -} diff --git a/migration/v9/model/pkg.go b/migration/v9/model/pkg.go index fb1767a70..5bc918d1c 100644 --- a/migration/v9/model/pkg.go +++ b/migration/v9/model/pkg.go @@ -1,41 +1,14 @@ package model -import "github.com/konveyor/tackle2-hub/migration/v8/model" +import "github.com/konveyor/tackle2-hub/settings" // // JSON field (data) type. type JSON = []byte -type Model = model.Model -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 Proxy = model.Proxy -type Setting = model.Setting -type RuleSet = model.RuleSet -type Rule = model.Rule -type Tag = model.Tag -type TagCategory = model.TagCategory -type Target = model.Target -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 DependencyCyclicError = model.DependencyCyclicError +var ( + Settings = &settings.Settings +) // // All builds all models. diff --git a/migration/v9/model/stakeholder.go b/migration/v9/model/stakeholder.go deleted file mode 100644 index ad347ba4b..000000000 --- a/migration/v9/model/stakeholder.go +++ /dev/null @@ -1,16 +0,0 @@ -package model - -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"` -} diff --git a/migration/v9/model/stakeholdergroup.go b/migration/v9/model/stakeholdergroup.go deleted file mode 100644 index 085bf4aac..000000000 --- a/migration/v9/model/stakeholdergroup.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -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"` -}