diff --git a/addon/task.go b/addon/task.go index 011fbd2d..e0c9915b 100644 --- a/addon/task.go +++ b/addon/task.go @@ -93,13 +93,34 @@ func (h *Task) Succeeded() { // Failed report addon failed. // The reason can be a printf style format. func (h *Task) Failed(reason string, x ...interface{}) { + reason = fmt.Sprintf(reason, x...) h.report.Status = task.Failed - h.report.Error = fmt.Sprintf(reason, x...) + h.report.Errors = append( + h.report.Errors, + api.TaskError{ + Severity: "Error", + Description: reason, + }) h.pushReport() Log.Info( "Addon reported: failed.", - "error", - h.report.Error) + "reason", + reason) + return +} + +// +// Error report addon error. +// The description can be a printf style format. +func (h *Task) Error(severity, description string, x ...interface{}) { + description = fmt.Sprintf(description, x...) + h.report.Errors = append( + h.report.Errors, + api.TaskError{ + Severity: severity, + Description: description, + }) + h.pushReport() return } diff --git a/api/task.go b/api/task.go index b198d8da..5552fe61 100644 --- a/api/task.go +++ b/api/task.go @@ -496,6 +496,13 @@ type TTL struct { Failed int `json:"failed,omitempty"` } +// +// TaskError used in Task.Errors. +type TaskError struct { + Severity string `json:"severity"` + Description string `json:"description"` +} + // // Task REST resource. type Task struct { @@ -515,7 +522,7 @@ type Task struct { Purged bool `json:"purged,omitempty" yaml:",omitempty"` Started *time.Time `json:"started,omitempty" yaml:",omitempty"` Terminated *time.Time `json:"terminated,omitempty" yaml:",omitempty"` - Error string `json:"error,omitempty" yaml:",omitempty"` + Errors []TaskError `json:"errors,omitempty" yaml:",omitempty"` Pod string `json:"pod,omitempty" yaml:",omitempty"` Retries int `json:"retries,omitempty" yaml:",omitempty"` Canceled bool `json:"canceled,omitempty" yaml:",omitempty"` @@ -538,7 +545,6 @@ func (r *Task) With(m *model.Task) { r.State = m.State r.Started = m.Started r.Terminated = m.Terminated - r.Error = m.Error r.Pod = m.Pod r.Retries = m.Retries r.Canceled = m.Canceled @@ -551,6 +557,9 @@ func (r *Task) With(m *model.Task) { if m.TTL != nil { _ = json.Unmarshal(m.TTL, &r.TTL) } + if m.Errors != nil { + _ = json.Unmarshal(m.Errors, &r.Errors) + } } // @@ -579,7 +588,7 @@ func (r *Task) Model() (m *model.Task) { type TaskReport struct { Resource `yaml:",inline"` Status string `json:"status"` - Error string `json:"error,omitempty" yaml:",omitempty"` + Errors []TaskError `json:"errors,omitempty" yaml:",omitempty"` Total int `json:"total,omitempty" yaml:",omitempty"` Completed int `json:"completed,omitempty" yaml:",omitempty"` Activity []string `json:"activity,omitempty" yaml:",omitempty"` @@ -592,13 +601,15 @@ type TaskReport struct { func (r *TaskReport) With(m *model.TaskReport) { r.Resource.With(&m.Model) r.Status = m.Status - r.Error = m.Error r.Total = m.Total r.Completed = m.Completed r.TaskID = m.TaskID if m.Activity != nil { _ = json.Unmarshal(m.Activity, &r.Activity) } + if m.Errors != nil { + _ = json.Unmarshal(m.Errors, &r.Errors) + } if m.Result != nil { _ = json.Unmarshal(m.Result, &r.Result) } @@ -612,7 +623,6 @@ func (r *TaskReport) Model() (m *model.TaskReport) { } m = &model.TaskReport{ Status: r.Status, - Error: r.Error, Total: r.Total, Completed: r.Completed, TaskID: r.TaskID, @@ -623,6 +633,9 @@ func (r *TaskReport) Model() (m *model.TaskReport) { if r.Result != nil { m.Result, _ = json.Marshal(r.Result) } + if r.Errors != nil { + m.Errors, _ = json.Marshal(r.Errors) + } m.ID = r.ID return diff --git a/hack/add/analysis.sh b/hack/add/analysis.sh index 3ab7a019..fe9996b6 100755 --- a/hack/add/analysis.sh +++ b/hack/add/analysis.sh @@ -149,6 +149,11 @@ indirect: "true" version: 4.6 " >> ${file} echo -n "--- +name: github.com/hybernate +indirect: "true" +version: 5.0 +" >> ${file} +echo -n "--- name: github.com/ejb indirect: "true" version: 4.3 diff --git a/hack/cmd/addon/main.go b/hack/cmd/addon/main.go index b991eb04..f05f12ce 100644 --- a/hack/cmd/addon/main.go +++ b/hack/cmd/addon/main.go @@ -97,6 +97,8 @@ func main() { } return }) + + addon.Error("Warning", "Test warning.") } // diff --git a/migration/v6/migrate.go b/migration/v6/migrate.go index d29ac92b..de8f9e4b 100644 --- a/migration/v6/migrate.go +++ b/migration/v6/migrate.go @@ -1,6 +1,7 @@ package v6 import ( + "encoding/json" "github.com/jortel/go-utils/logr" "github.com/konveyor/tackle2-hub/migration/v6/model" "gorm.io/gorm" @@ -21,9 +22,76 @@ func (r Migration) Apply(db *gorm.DB) (err error) { return } err = db.AutoMigrate(r.Models()...) + if err != nil { + return + } + err = r.taskReportError(db) + if err != nil { + return + } + err = r.taskError(db) + if err != nil { + return + } return } func (r Migration) Models() []interface{} { return model.All() } + +func (r Migration) taskError(db *gorm.DB) (err error) { + type Task struct { + model.Task + Error string + } + var list []Task + err = db.Find(&Task{}, &list).Error + if err != nil { + return + } + for i := range list { + m := &list[i] + if m.Error == "" { + continue + } + m.Errors, _ = json.Marshal( + []model.TaskError{ + { + Severity: "Error", + Description: m.Error, + }, + }) + } + m := db.Migrator() + err = m.DropColumn(&model.Task{}, "Error") + return +} + +func (r Migration) taskReportError(db *gorm.DB) (err error) { + type TaskReport struct { + model.TaskReport + Error string + } + var list []TaskReport + err = db.Find(&TaskReport{}, &list).Error + if err != nil { + return + } + for i := range list { + m := &list[i] + if m.Error == "" { + continue + } + m.Errors, _ = json.Marshal( + []model.TaskError{ + { + Severity: "Error", + Description: m.Error, + }, + }) + } + m := db.Migrator() + err = m.DropColumn(&model.TaskReport{}, "Error") + return +} diff --git a/migration/v6/model/pkg.go b/migration/v6/model/pkg.go index 2b8dce2c..1958d277 100644 --- a/migration/v6/model/pkg.go +++ b/migration/v6/model/pkg.go @@ -29,12 +29,8 @@ 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 diff --git a/migration/v6/model/task.go b/migration/v6/model/task.go new file mode 100644 index 00000000..c74f45db --- /dev/null +++ b/migration/v6/model/task.go @@ -0,0 +1,92 @@ +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 new file mode 100644 index 00000000..cc61ce22 --- /dev/null +++ b/migration/v6/model/taskgroup.go @@ -0,0 +1,91 @@ +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/task/manager.go b/task/manager.go index e12aa3bc..fd753be9 100644 --- a/task/manager.go +++ b/task/manager.go @@ -132,7 +132,7 @@ func (m *Manager) startReady() { mark := time.Now() task.State = Failed task.Terminated = &mark - task.Error = "Hub is disconnected." + task.Error("Error", "Hub is disconnected.") sErr := m.DB.Save(task).Error Log.Error(sErr, "") continue @@ -157,7 +157,7 @@ func (m *Manager) startReady() { err := rt.Run(m.Client) if err != nil { if errors.Is(err, &AddonNotFound{}) { - ready.Error = err.Error() + ready.Error("Error", err.Error()) ready.State = Failed sErr := m.DB.Save(ready).Error Log.Error(sErr, "") @@ -269,7 +269,7 @@ func (r *Task) Run(client k8s.Client) (err error) { mark := time.Now() defer func() { if err != nil { - r.Error = err.Error() + r.Error("Error", err.Error()) r.Terminated = &mark r.State = Failed } @@ -354,16 +354,15 @@ func (r *Task) Reflect(client k8s.Client) (err error) { r.State = Succeeded r.Terminated = &mark case core.PodFailed: + r.Error("Error", "Pod failed: %s", pod.Status.Message) if r.Retries < Settings.Hub.Task.Retries { _ = client.Delete(context.TODO(), pod) r.Pod = "" - r.Error = "" r.State = Ready r.Retries++ } else { r.State = Failed r.Terminated = &mark - r.Error = "pod failed." } }