Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Task report files #577

Merged
merged 19 commits into from
Dec 19, 2023
38 changes: 38 additions & 0 deletions addon/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,44 @@ func (h *Task) Activity(entry string, v ...interface{}) {
return
}

//
// Attach ensures the file is attached to the report
// associated with the last entry in the activity.
func (h *Task) Attach(f *api.File) {
index := len(h.report.Activity)
h.AttachAt(f, index)
return
}

//
// AttachAt ensures the file is attached to
// the report indexed to the activity.
// The activity is a 1-based index. Zero(0) means NOT associated.
func (h *Task) AttachAt(f *api.File, activity int) {
for _, ref := range h.report.Attached {
if ref.ID == f.ID {
return
}
}
Log.Info(
"Addon attached: %s",
"path",
f.Path,
"activity",
activity)
h.report.Attached = append(
h.report.Attached,
api.Attachment{
Activity: activity,
Ref: api.Ref{
ID: f.ID,
Name: f.Name,
},
})
h.pushReport()
return
}

//
// Total report addon total items.
func (h *Task) Total(n int) {
Expand Down
60 changes: 60 additions & 0 deletions api/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (h FileHandler) AddRoutes(e *gin.Engine) {
routeGroup.GET(FilesRoot+"/", h.List)
routeGroup.POST(FileRoot, h.Create)
routeGroup.PUT(FileRoot, h.Create)
routeGroup.PATCH(FileRoot, h.Append)
routeGroup.GET(FileRoot, h.Get)
routeGroup.DELETE(FileRoot, h.Delete)
}
Expand Down Expand Up @@ -122,6 +123,65 @@ func (h FileHandler) Create(ctx *gin.Context) {
h.Respond(ctx, http.StatusCreated, r)
}

// Append godoc
// @summary Append a file.
// @description Append a file.
// @tags file
// @accept json
// @produce json
// @success 204
// @router /files/{id} [put]
// @param name id uint true "File ID"
func (h FileHandler) Append(ctx *gin.Context) {
var err error
input, err := ctx.FormFile(FileField)
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
m := &model.File{}
id := h.pk(ctx)
db := h.DB(ctx)
err = db.First(m, id).Error
if err != nil {
_ = ctx.Error(err)
return
}
reader, err := input.Open()
if err != nil {
err = &BadRequestError{err.Error()}
_ = ctx.Error(err)
return
}
defer func() {
_ = reader.Close()
}()
writer, err := os.OpenFile(m.Path, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
_ = ctx.Error(err)
return
}
defer func() {
_ = writer.Close()
}()
_, err = io.Copy(writer, reader)
if err != nil {
_ = ctx.Error(err)
return
}
db = h.DB(ctx)
db = db.Model(m)
user := h.BaseHandler.CurrentUser(ctx)
err = db.Update("UpdateUser", user).Error
if err != nil {
_ = ctx.Error(err)
return
}

h.Status(ctx, http.StatusNoContent)
}

// Get godoc
// @summary Get a file by ID.
// @description Get a file by ID. Returns api.File when Accept=application/json else the file content.
Expand Down
126 changes: 99 additions & 27 deletions api/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import (
tasking "github.com/konveyor/tackle2-hub/task"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"io/ioutil"
k8serr "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/utils/strings/slices"
"net/http"
"sort"
"strconv"
"strings"
"time"
)

Expand Down Expand Up @@ -83,6 +88,14 @@ func (h TaskHandler) Get(ctx *gin.Context) {
}
r := Task{}
r.With(task)
q := ctx.Query("merged")
if b, _ := strconv.ParseBool(q); b {
err := r.injectFiles(h.DB(ctx))
if err != nil {
_ = ctx.Error(result.Error)
return
}
}

h.Respond(ctx, http.StatusOK, r)
}
Expand Down Expand Up @@ -510,26 +523,27 @@ type TaskError struct {
// Task REST resource.
type Task struct {
Resource `yaml:",inline"`
Name string `json:"name"`
Locator string `json:"locator,omitempty" yaml:",omitempty"`
Priority int `json:"priority,omitempty" yaml:",omitempty"`
Variant string `json:"variant,omitempty" yaml:",omitempty"`
Policy string `json:"policy,omitempty" yaml:",omitempty"`
TTL *TTL `json:"ttl,omitempty" yaml:",omitempty"`
Addon string `json:"addon,omitempty" binding:"required" yaml:",omitempty"`
Data interface{} `json:"data" swaggertype:"object" binding:"required"`
Application *Ref `json:"application,omitempty" yaml:",omitempty"`
State string `json:"state"`
Image string `json:"image,omitempty" yaml:",omitempty"`
Pod string `json:"pod,omitempty" yaml:",omitempty"`
Retries int `json:"retries,omitempty" yaml:",omitempty"`
Started *time.Time `json:"started,omitempty" yaml:",omitempty"`
Terminated *time.Time `json:"terminated,omitempty" yaml:",omitempty"`
Canceled bool `json:"canceled,omitempty" yaml:",omitempty"`
Bucket *Ref `json:"bucket,omitempty" yaml:",omitempty"`
Purged bool `json:"purged,omitempty" yaml:",omitempty"`
Errors []TaskError `json:"errors,omitempty" yaml:",omitempty"`
Activity []string `json:"activity,omitempty" yaml:",omitempty"`
Name string `json:"name"`
Locator string `json:"locator,omitempty" yaml:",omitempty"`
Priority int `json:"priority,omitempty" yaml:",omitempty"`
Variant string `json:"variant,omitempty" yaml:",omitempty"`
Policy string `json:"policy,omitempty" yaml:",omitempty"`
TTL *TTL `json:"ttl,omitempty" yaml:",omitempty"`
Addon string `json:"addon,omitempty" binding:"required" yaml:",omitempty"`
Data interface{} `json:"data" swaggertype:"object" binding:"required"`
Application *Ref `json:"application,omitempty" yaml:",omitempty"`
State string `json:"state"`
Image string `json:"image,omitempty" yaml:",omitempty"`
Pod string `json:"pod,omitempty" yaml:",omitempty"`
Retries int `json:"retries,omitempty" yaml:",omitempty"`
Started *time.Time `json:"started,omitempty" yaml:",omitempty"`
Terminated *time.Time `json:"terminated,omitempty" yaml:",omitempty"`
Canceled bool `json:"canceled,omitempty" yaml:",omitempty"`
Bucket *Ref `json:"bucket,omitempty" yaml:",omitempty"`
Purged bool `json:"purged,omitempty" yaml:",omitempty"`
Errors []TaskError `json:"errors,omitempty" yaml:",omitempty"`
Activity []string `json:"activity,omitempty" yaml:",omitempty"`
Attached []Attachment `json:"attached" yaml:",omitempty"`
}

//
Expand Down Expand Up @@ -563,6 +577,7 @@ func (r *Task) With(m *model.Task) {
report.With(m.Report)
r.Activity = report.Activity
r.Errors = append(report.Errors, r.Errors...)
r.Attached = report.Attached
switch r.State {
case tasking.Succeeded:
switch report.Status {
Expand Down Expand Up @@ -594,17 +609,58 @@ func (r *Task) Model() (m *model.Task) {
return
}

//
// injectFiles inject attached files into the activity.
func (r *Task) injectFiles(db *gorm.DB) (err error) {
sort.Slice(
r.Attached,
func(i, j int) bool {
return r.Attached[i].Activity > r.Attached[j].Activity
})
for _, ref := range r.Attached {
if ref.Activity == 0 {
continue
}
if ref.Activity > len(r.Activity) {
continue
}
m := &model.File{}
err = db.First(m, ref.ID).Error
if err != nil {
return
}
b, nErr := ioutil.ReadFile(m.Path)
if nErr != nil {
err = nErr
return
}
var content []string
for _, s := range strings.Split(string(b), "\n") {
content = append(
content,
"> "+s)
}
snipA := slices.Clone(r.Activity[:ref.Activity])
snipB := slices.Clone(r.Activity[ref.Activity:])
r.Activity = append(
append(snipA, content...),
snipB...)
}
return
}

//
// TaskReport REST resource.
type TaskReport struct {
Resource `yaml:",inline"`
Status string `json:"status"`
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"`
Result interface{} `json:"result,omitempty" yaml:",omitempty" swaggertype:"object"`
TaskID uint `json:"task"`
Status string `json:"status"`
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"`
Attached []Attachment `json:"attached,omitempty" yaml:",omitempty"`
Result interface{} `json:"result,omitempty" yaml:",omitempty" swaggertype:"object"`
TaskID uint `json:"task"`
}

//
Expand All @@ -621,6 +677,9 @@ func (r *TaskReport) With(m *model.TaskReport) {
if m.Errors != nil {
_ = json.Unmarshal(m.Errors, &r.Errors)
}
if m.Attached != nil {
_ = json.Unmarshal(m.Attached, &r.Attached)
}
if m.Result != nil {
_ = json.Unmarshal(m.Result, &r.Result)
}
Expand All @@ -647,7 +706,20 @@ func (r *TaskReport) Model() (m *model.TaskReport) {
if r.Errors != nil {
m.Errors, _ = json.Marshal(r.Errors)
}
if r.Attached != nil {
m.Attached, _ = json.Marshal(r.Attached)
}
m.ID = r.ID

return
}

//
// Attachment associates Files with a TaskReport.
type Attachment struct {
// Ref references an attached File.
Ref `yaml:",inline"`
// Activity index (1-based) association with an
// activity entry. Zero(0) indicates not associated.
Activity int `json:"activity,omitempty" yaml:",omitempty"`
}
46 changes: 40 additions & 6 deletions binding/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,19 @@ func (r *Client) FileGet(path, destination string) (err error) {
}

//
// FilePut uploads a file.
// FilePost uploads a file.
// Returns the created File resource.
func (r *Client) FilePut(path, source string, object interface{}) (err error) {
func (r *Client) FilePost(path, source string, object interface{}) (err error) {
if source == "" {
fields := []Field{
{
Name: api.FileField,
Reader: bytes.NewReader([]byte{}),
},
}
err = r.FileSend(path, http.MethodPost, fields, object)
return
}
isDir, nErr := r.IsDir(source, true)
if nErr != nil {
err = nErr
Expand All @@ -440,14 +450,24 @@ func (r *Client) FilePut(path, source string, object interface{}) (err error) {
Path: source,
},
}
err = r.FileSend(path, http.MethodPut, fields, object)
err = r.FileSend(path, http.MethodPost, fields, object)
return
}

//
// FilePost uploads a file.
// FilePut uploads a file.
// Returns the created File resource.
func (r *Client) FilePost(path, source string, object interface{}) (err error) {
func (r *Client) FilePut(path, source string, object interface{}) (err error) {
if source == "" {
fields := []Field{
{
Name: api.FileField,
Reader: bytes.NewReader([]byte{}),
},
}
err = r.FileSend(path, http.MethodPut, fields, object)
return
}
isDir, nErr := r.IsDir(source, true)
if nErr != nil {
err = nErr
Expand All @@ -463,7 +483,21 @@ func (r *Client) FilePost(path, source string, object interface{}) (err error) {
Path: source,
},
}
err = r.FileSend(path, http.MethodPost, fields, object)
err = r.FileSend(path, http.MethodPut, fields, object)
return
}

//
// FilePatch appends file.
// Returns the created File resource.
func (r *Client) FilePatch(path string, buffer []byte) (err error) {
fields := []Field{
{
Name: api.FileField,
Reader: bytes.NewReader(buffer),
},
}
err = r.FileSend(path, http.MethodPatch, fields, nil)
return
}

Expand Down
Loading
Loading