Skip to content

Commit

Permalink
feat(peridot-cli/task-info): fetch and display task details
Browse files Browse the repository at this point in the history
given a task ID, fetch its details and display them to a table or to
json with `-o json`. Table view also adds a calculated task duration and
can optionally include the submitter information as well as a link to
logs for the task.

* --no-color - to skip colorizing output
* --L|--logs - include column with link to logs
* --submitter - show submitter
* --no-wait - control whether to wait until a task completes to output
  (table mode)
  • Loading branch information
NeilHanlon committed Jul 31, 2024
1 parent 0d3255c commit e7b15b3
Show file tree
Hide file tree
Showing 25 changed files with 3,578 additions and 1 deletion.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down Expand Up @@ -184,4 +185,5 @@ replace (
peridot.resf.org/peridot/pb => ./bazel-bin/peridot/proto/v1/peridotpb_go_proto_/peridot.resf.org/peridot/pb
peridot.resf.org/peridot/yumrepofs/pb => ./bazel-bin/peridot/proto/v1/yumrepofs/yumrepofspb_go_proto_/peridot.resf.org/peridot/yumrepofs/pb
)

// sync-replace-end
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
Expand Down
4 changes: 3 additions & 1 deletion peridot/cmd/v1/peridot/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ go_library(
"project_list.go",
"task.go",
"task_logs.go",
"task_info.go",
"utils.go",
],
data = [
Expand All @@ -39,6 +40,7 @@ go_library(
"//vendor/github.com/spf13/cobra",
"//vendor/github.com/spf13/viper",
"//vendor/openapi.peridot.resf.org/peridotopenapi",
"//vendor/github.com/olekukonko/tablewriter",
"@org_golang_x_oauth2//:oauth2",
"@org_golang_x_oauth2//clientcredentials",
],
Expand Down Expand Up @@ -67,7 +69,7 @@ pkg_rpm(
architecture = "x86_64",
description = "A command line interface to interact with the Peridot build system",
license = "MIT",
release = "1",
release = "2",
source_date_epoch = 0,
summary = "Peridot Command Line Interface",
version = "0.2.3",
Expand Down
12 changes: 12 additions & 0 deletions peridot/cmd/v1/peridot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ func init() {
root.PersistentFlags().String("client-secret", "", "Client secret for authentication")
root.PersistentFlags().String("project-id", "", "Peridot project ID")
root.PersistentFlags().Bool("debug", false, "Debug mode")

root.PersistentFlags().StringP("output", "o", "table", "Output format (table|json)")
root.PersistentFlags().Bool("no-color", false, "don't colorize output")
root.PersistentFlags().Bool("no-wait", false, "don't wait for completion")

root.AddCommand(lookaside)
lookaside.AddCommand(lookasideUpload)
Expand All @@ -65,6 +68,7 @@ func init() {

root.AddCommand(task)
task.AddCommand(taskLogs)
task.AddCommand(taskInfo)

root.AddCommand(project)
project.AddCommand(projectInfo)
Expand Down Expand Up @@ -125,3 +129,11 @@ func debug() bool {
func output() string {
return viper.GetString("output")
}

func color() bool {
return !viper.GetBool("no-color")
}

func wait() bool {
return !viper.GetBool("no-wait")
}
310 changes: 310 additions & 0 deletions peridot/cmd/v1/peridot/task_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package main

import (
"errors"
"fmt"
"log"
"os"
"slices"
"time"

"github.com/google/uuid"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"openapi.peridot.resf.org/peridotopenapi"
)

var taskInfo = &cobra.Command{
Use: "info [name-or-buildId]",
Args: cobra.ExactArgs(1),
Run: taskInfoMn,
}

var (
showLogLink bool
showSubmitterInfo bool
showDuration bool
)

func init() {
taskInfo.Flags().BoolVar(&succeeded, "succeeded", true, "only query successful tasks")
taskInfo.Flags().BoolVar(&cancelled, "cancelled", false, "only query cancelled tasks")
taskInfo.Flags().BoolVar(&failed, "failed", false, "only query failed tasks")
taskInfo.MarkFlagsMutuallyExclusive("cancelled", "failed", "succeeded")

taskInfo.Flags().BoolVarP(&showLogLink, "logs", "L", false, "include log link in output (table format only)")
taskInfo.Flags().BoolVar(&showSubmitterInfo, "submitter", false, "include submitter details (table format only)")
taskInfo.Flags().BoolVar(&showDuration, "duration", true, "include duration from start to stop (table format only)")
}

func getNextColor(colors tablewriter.Colors) tablewriter.Colors {
bgColor := getNextBackgroundColor(colors[0])
fgColor := colors[1]
if bgColor == -1 {
fgColor = getNextForegroundColor(0)
bgColor = getNextBackgroundColor(0)
}
return tablewriter.Colors{bgColor, fgColor}
}

func getNextForegroundColor(color int) int {
switch color {
case 0:
return tablewriter.FgGreenColor
case tablewriter.FgCyanColor:
return tablewriter.FgHiGreenColor
case tablewriter.FgHiCyanColor:
return tablewriter.FgGreenColor
default:
color++
return color
}
}

func getNextBackgroundColor(color int) int {
switch color {
case 0:
return tablewriter.BgRedColor
case tablewriter.BgCyanColor:
return tablewriter.BgHiRedColor
case tablewriter.BgHiCyanColor:
return -1
default:
color++
return color
}
}

func buildHeaderAndAutoMergeCells() ([]string, []int) {
header := []string{"ptid", "tid", "status", "type", "arch", "created", "finished"}
mergableNames := []string{"ptid", "type", "arch"}
var autoMergeCells []int

// Conditional appending to header
if showDuration {
header = append(header, "duration")
mergableNames = append(mergableNames, "duration")
}
if showSubmitterInfo {
header = append(header, "submitter")
mergableNames = append(mergableNames, "submitter")
}
if showLogLink {
header = append(header, "logs")
}

// Determine dynamic indices for auto-merge cells
for _, itemName := range mergableNames {
index := slices.Index(header, itemName)
if index != -1 {
autoMergeCells = append(autoMergeCells, index)
}
}

return header, autoMergeCells
}

func convertSubTaskSliceToCSV(task peridotopenapi.V1AsyncTask) {
subtasks, ok := task.GetSubtasksOk()
if !ok {
errFatal(fmt.Errorf("error getting subtasks: %v", ok))
}

var parentTask = (*subtasks)[0]

var table = tablewriter.NewWriter(os.Stdout)
// var data [][]string

var header, autoMergeCells = buildHeaderAndAutoMergeCells()

var lastColor = tablewriter.Colors{0, tablewriter.FgWhiteColor}
var seenTasksColors = make(map[string]tablewriter.Colors)

var parentTaskIds []string // cache parentTaskIds for colorizing

// precache all the subtask's parent tasks so we know if we should color them
for _, subtask := range *subtasks {
parentTaskIds = append(parentTaskIds, subtask.GetParentTaskId())
}

for _, subtask := range *subtasks {
json, err := subtask.MarshalJSON()
if err != nil {
errFatal(err)
}

if debug() {
err = PrettyPrintJSON(json)
if err != nil {
errFatal(err)
}
// taskResponse, _ := subtask.GetResponse().MarshalJSON()
// taskMetadata, _ := subtask.GetMetadata().MarshalJSON()
}

subtaskId := subtask.GetId()
subtaskParentTaskId := subtask.GetParentTaskId()
createdAt := subtask.GetCreatedAt()
finishedAt := subtask.GetFinishedAt()

row := []string{
subtaskParentTaskId,
subtaskId,
string(subtask.GetStatus()),
string(subtask.GetType()),
subtask.GetArch(),
formatTime(createdAt),
formatTime(finishedAt),
}

if showDuration {
row = append(row, formatDuration(createdAt, finishedAt))
}

if showSubmitterInfo {
effectiveSubmitter := fmt.Sprintf("%s <%s>", parentTask.GetSubmitterId(), parentTask.GetSubmitterEmail())
row = append(row, effectiveSubmitter)
}

if showLogLink {
row = append(row, getLogLink(subtaskId))
}

if !color() {
table.Append(row)
continue
}

nextColor := tablewriter.Colors{tablewriter.BgBlackColor, tablewriter.FgWhiteColor}
needsColor := hasAny(parentTaskIds, subtaskId)
if _, seen := seenTasksColors[subtaskId]; !seen && needsColor {
debugP("before: lastcolor: %v next: %v", lastColor, nextColor)
nextColor = getNextColor(lastColor)
debugP("after: lastcolor: %v next: %v", lastColor, nextColor)
lastColor = nextColor
seenTasksColors[subtaskId] = nextColor
}

tidColors := nextColor

ptidColors := tablewriter.Colors{tablewriter.FgWhiteColor, tablewriter.BgBlackColor}
if seenColor, seen := seenTasksColors[subtaskParentTaskId]; seen {
ptidColors = seenColor
}

var colors = make([]tablewriter.Colors, len(row))

debugP("tidcolor: %v ptidcolor %d", tidColors, ptidColors)
for i, v := range header {
switch v {
case "ptid":
colors[i] = ptidColors
case "tid":
colors[i] = tidColors
default:
colors[i] = tablewriter.Colors{}
}
}

table.Rich(row, colors)
}

table.SetHeader(header)
table.SetAutoMergeCellsByColumnIndex(autoMergeCells)
table.SetRowLine(true)
table.Render()

}

func debugP(s string, args ...any) {
if debug() {
log.Printf(s, args...)
}
}

func hasAny(slice []string, target string) bool {
if idx := slices.Index(slice, target); idx >= 0 {
return true
}
return false
}

func taskInfoMn(_ *cobra.Command, args []string) {
// Ensure project id exists
projectId := mustGetProjectID()

taskId := args[0]

err := uuid.Validate(taskId)
if err != nil {
errFatal(errors.New("invalid task id"))
}

taskCl := getClient(serviceTask).(peridotopenapi.TaskServiceApi)
log.Printf("Searching for task %s in project %s\n", taskId, projectId)

var waiting = false
for {
res, _, err := taskCl.GetTask(getContext(), projectId, taskId).Execute()
if err != nil {
errFatal(fmt.Errorf("error getting task: %s", err.Error()))
}

task := res.GetTask()

switch output() {
case "table":
if !waiting || task.GetDone() {
convertSubTaskSliceToCSV(task)
}
if wait() && !task.GetDone() {
waiting = true
log.Printf("Waiting for task %s to complete", task.GetTaskId())
time.Sleep(5 * time.Second)
continue
}

case "json":
taskJSON, err := res.MarshalJSON()
if err != nil {
errFatal(err)
}

err = PrettyPrintJSON(taskJSON)
if err != nil {
errFatal(err)
}
}
break
}
}
Loading

0 comments on commit e7b15b3

Please sign in to comment.