Skip to content

Commit 1f14c9f

Browse files
committed
feat(peridot-cli/task-info): fetch and display task details
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.
1 parent 0d3255c commit 1f14c9f

File tree

25 files changed

+3524
-0
lines changed

25 files changed

+3524
-0
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ require (
117117
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
118118
github.com/modern-go/reflect2 v1.0.2 // indirect
119119
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
120+
github.com/olekukonko/tablewriter v0.0.5 // indirect
120121
github.com/pborman/uuid v1.2.1 // indirect
121122
github.com/pelletier/go-toml v1.8.1 // indirect
122123
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -184,4 +185,5 @@ replace (
184185
peridot.resf.org/peridot/pb => ./bazel-bin/peridot/proto/v1/peridotpb_go_proto_/peridot.resf.org/peridot/pb
185186
peridot.resf.org/peridot/yumrepofs/pb => ./bazel-bin/peridot/proto/v1/yumrepofs/yumrepofspb_go_proto_/peridot.resf.org/peridot/yumrepofs/pb
186187
)
188+
187189
// sync-replace-end

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
452452
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
453453
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
454454
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
455+
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
456+
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
455457
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
456458
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
457459
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=

peridot/cmd/v1/peridot/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ go_library(
2323
"project_list.go",
2424
"task.go",
2525
"task_logs.go",
26+
"task_info.go",
2627
"utils.go",
2728
],
2829
data = [
@@ -39,6 +40,7 @@ go_library(
3940
"//vendor/github.com/spf13/cobra",
4041
"//vendor/github.com/spf13/viper",
4142
"//vendor/openapi.peridot.resf.org/peridotopenapi",
43+
"//vendor/github.com/olekukonko/tablewriter",
4244
"@org_golang_x_oauth2//:oauth2",
4345
"@org_golang_x_oauth2//clientcredentials",
4446
],

peridot/cmd/v1/peridot/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func init() {
6565

6666
root.AddCommand(task)
6767
task.AddCommand(taskLogs)
68+
task.AddCommand(taskInfo)
6869

6970
root.AddCommand(project)
7071
project.AddCommand(projectInfo)
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
2+
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
3+
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
4+
//
5+
// Redistribution and use in source and binary forms, with or without
6+
// modification, are permitted provided that the following conditions are met:
7+
//
8+
// 1. Redistributions of source code must retain the above copyright notice,
9+
// this list of conditions and the following disclaimer.
10+
//
11+
// 2. Redistributions in binary form must reproduce the above copyright notice,
12+
// this list of conditions and the following disclaimer in the documentation
13+
// and/or other materials provided with the distribution.
14+
//
15+
// 3. Neither the name of the copyright holder nor the names of its contributors
16+
// may be used to endorse or promote products derived from this software without
17+
// specific prior written permission.
18+
//
19+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
// POSSIBILITY OF SUCH DAMAGE.
30+
31+
package main
32+
33+
import (
34+
"errors"
35+
"fmt"
36+
"log"
37+
"os"
38+
"slices"
39+
40+
"github.com/google/uuid"
41+
"github.com/olekukonko/tablewriter"
42+
"github.com/spf13/cobra"
43+
"openapi.peridot.resf.org/peridotopenapi"
44+
)
45+
46+
var taskInfo = &cobra.Command{
47+
Use: "info [name-or-buildId]",
48+
Args: cobra.ExactArgs(1),
49+
Run: taskInfoMn,
50+
}
51+
52+
var (
53+
showLogLink bool
54+
showSubmitterInfo bool
55+
showDuration bool
56+
)
57+
58+
func init() {
59+
taskInfo.Flags().BoolVar(&succeeded, "succeeded", true, "only query successful tasks")
60+
taskInfo.Flags().BoolVar(&cancelled, "cancelled", false, "only query cancelled tasks")
61+
taskInfo.Flags().BoolVar(&failed, "failed", false, "only query failed tasks")
62+
taskInfo.MarkFlagsMutuallyExclusive("cancelled", "failed", "succeeded")
63+
64+
taskInfo.Flags().BoolVarP(&showLogLink, "logs", "L", false, "include log link in output (table format only)")
65+
taskInfo.Flags().BoolVar(&showSubmitterInfo, "submitter", false, "include submitter details (table format only)")
66+
taskInfo.Flags().BoolVar(&showDuration, "duration", true, "include duration from start to stop (table format only)")
67+
}
68+
69+
func getNextColor(color int) int {
70+
switch color {
71+
case 0:
72+
return tablewriter.FgRedColor
73+
case tablewriter.FgCyanColor:
74+
return tablewriter.FgHiRedColor
75+
case tablewriter.FgHiWhiteColor:
76+
return tablewriter.FgRedColor
77+
default:
78+
color++
79+
return color
80+
}
81+
}
82+
83+
func buildHeaderAndAutoMergeCells() ([]string, []int) {
84+
header := []string{"ptid", "tid", "status", "type", "arch", "created", "finished"}
85+
mergableNames := []string{"ptid", "type", "arch"}
86+
var autoMergeCells []int
87+
88+
// Conditional appending to header
89+
if showDuration {
90+
header = append(header, "duration")
91+
mergableNames = append(mergableNames, "duration")
92+
}
93+
if showSubmitterInfo {
94+
header = append(header, "submitter")
95+
mergableNames = append(mergableNames, "submitter")
96+
}
97+
if showLogLink {
98+
header = append(header, "logs")
99+
}
100+
101+
// Determine dynamic indices for auto-merge cells
102+
for _, itemName := range mergableNames {
103+
index := slices.Index(header, itemName)
104+
if index != -1 {
105+
autoMergeCells = append(autoMergeCells, index)
106+
}
107+
}
108+
109+
return header, autoMergeCells
110+
}
111+
112+
func convertSubTaskSliceToCSV(task peridotopenapi.V1AsyncTask) {
113+
subtasks, ok := task.GetSubtasksOk()
114+
if !ok {
115+
errFatal(fmt.Errorf("error getting subtasks: %v", ok))
116+
}
117+
118+
var parentTask = (*subtasks)[0]
119+
120+
var table = tablewriter.NewWriter(os.Stdout)
121+
// var data [][]string
122+
123+
var header, autoMergeCells = buildHeaderAndAutoMergeCells()
124+
125+
var lastColor = 0 // initial color
126+
var seenTasksColors = make(map[string]int) // track seen task Ids
127+
128+
var parentTaskIds []string // cache parentTaskIds for colorizing
129+
130+
// precache all the subtask's parent tasks so we know if we should color them
131+
for _, subtask := range *subtasks {
132+
parentTaskIds = append(parentTaskIds, subtask.GetParentTaskId())
133+
}
134+
135+
for _, subtask := range *subtasks {
136+
json, err := subtask.MarshalJSON()
137+
if err != nil {
138+
errFatal(err)
139+
}
140+
141+
if debug() {
142+
err = PrettyPrintJSON(json)
143+
if err != nil {
144+
errFatal(err)
145+
}
146+
// taskResponse, _ := subtask.GetResponse().MarshalJSON()
147+
// taskMetadata, _ := subtask.GetMetadata().MarshalJSON()
148+
}
149+
150+
subtaskId := subtask.GetId()
151+
subtaskParentTaskId := subtask.GetParentTaskId()
152+
createdAt := subtask.GetCreatedAt()
153+
finishedAt := subtask.GetFinishedAt()
154+
155+
row := []string{
156+
subtaskParentTaskId,
157+
subtaskId,
158+
string(subtask.GetStatus()),
159+
string(subtask.GetType()),
160+
subtask.GetArch(),
161+
formatTime(createdAt),
162+
formatTime(finishedAt),
163+
}
164+
165+
if showDuration {
166+
row = append(row, formatDuration(createdAt, finishedAt))
167+
}
168+
169+
if showSubmitterInfo {
170+
effectiveSubmitter := fmt.Sprintf("%s <%s>", parentTask.GetSubmitterId(), parentTask.GetSubmitterEmail())
171+
row = append(row, effectiveSubmitter)
172+
}
173+
174+
if showLogLink {
175+
row = append(row, getLogLink(subtaskId))
176+
}
177+
178+
nextColor := tablewriter.FgWhiteColor
179+
needsColor := taskIdIsAnyParentTaskId(parentTaskIds, subtaskId)
180+
if _, seen := seenTasksColors[subtaskId]; !seen && needsColor {
181+
debugP("before: lastcolor: %d nextcolor %d", lastColor, nextColor)
182+
nextColor = getNextColor(lastColor)
183+
debugP("after: lastcolor: %d nextcolor %d", lastColor, nextColor)
184+
lastColor = nextColor
185+
seenTasksColors[subtaskId] = nextColor
186+
}
187+
188+
ptidColor := tablewriter.FgWhiteColor
189+
if seenColor, seen := seenTasksColors[subtaskParentTaskId]; seen {
190+
ptidColor = seenColor
191+
}
192+
193+
var colors = make([]tablewriter.Colors, len(row))
194+
195+
debugP("color: %d ptidcolor %d", nextColor, ptidColor)
196+
for i, v := range header {
197+
switch v {
198+
case "ptid":
199+
colors[i] = tablewriter.Colors{ptidColor}
200+
case "tid":
201+
if needsColor {
202+
colors[i] = tablewriter.Colors{nextColor} // if it is a parent
203+
} else {
204+
colors[i] = tablewriter.Colors{tablewriter.BgBlackColor, tablewriter.FgWhiteColor} // childless cat ladies
205+
}
206+
default:
207+
colors[i] = tablewriter.Colors{}
208+
}
209+
}
210+
211+
table.Rich(row, colors)
212+
}
213+
214+
table.SetHeader(header)
215+
table.SetAutoMergeCellsByColumnIndex(autoMergeCells)
216+
table.SetRowLine(true)
217+
table.Render()
218+
219+
}
220+
221+
func debugP(s string, args ...any) {
222+
if debug() {
223+
log.Printf(s, args...)
224+
}
225+
}
226+
227+
func taskIdIsAnyParentTaskId(parentTaskIds []string, subtaskId string) bool {
228+
if idx := slices.Index(parentTaskIds, subtaskId); idx > 0 {
229+
return true
230+
}
231+
return false
232+
}
233+
234+
func taskInfoMn(_ *cobra.Command, args []string) {
235+
// Ensure project id exists
236+
projectId := mustGetProjectID()
237+
238+
taskId := args[0]
239+
240+
err := uuid.Validate(taskId)
241+
if err != nil {
242+
errFatal(errors.New("invalid task id"))
243+
}
244+
245+
taskCl := getClient(serviceTask).(peridotopenapi.TaskServiceApi)
246+
log.Printf("Searching for task %s in project %s\n", taskId, projectId)
247+
248+
res, _, err := taskCl.GetTask(getContext(), projectId, taskId).Execute()
249+
if err != nil {
250+
errFatal(fmt.Errorf("error getting task: %s", err.Error()))
251+
}
252+
253+
switch output() {
254+
case "table":
255+
convertSubTaskSliceToCSV(res.GetTask())
256+
257+
case "json":
258+
taskJSON, err := res.MarshalJSON()
259+
if err != nil {
260+
errFatal(err)
261+
}
262+
263+
err = PrettyPrintJSON(taskJSON)
264+
if err != nil {
265+
errFatal(err)
266+
}
267+
}
268+
}

peridot/cmd/v1/peridot/utils.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import (
3939
"log"
4040
"net/http"
4141
"strconv"
42+
"strings"
43+
"time"
4244

4345
"golang.org/x/oauth2"
4446
"golang.org/x/oauth2/clientcredentials"
@@ -208,3 +210,18 @@ func PrettyPrintJSON(data []byte) error {
208210
fmt.Println(string(formattedJSON))
209211
return nil
210212
}
213+
214+
func formatTime(t time.Time) string {
215+
return t.Format("2006-01-02 15:04:05")
216+
}
217+
218+
func formatDuration(start, end time.Time) string {
219+
duration := end.Sub(start)
220+
return time.Time{}.Add(duration).Format("15:04:05")
221+
}
222+
223+
func getLogLink(subtaskId string) string {
224+
return fmt.Sprintf("https://%s/api/v1/projects/%s/tasks/%s/logs",
225+
strings.Replace(endpoint(), "-api", "", 1),
226+
mustGetProjectID(), subtaskId)
227+
}

repositories.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,12 @@ def go_repositories():
10681068
sum = "h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=",
10691069
version = "v1.3.1",
10701070
)
1071+
go_repository(
1072+
name = "com_github_olekukonko_tablewriter",
1073+
importpath = "github.com/olekukonko/tablewriter",
1074+
sum = "h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=",
1075+
version = "v0.0.5",
1076+
)
10711077
go_repository(
10721078
name = "com_github_oneofone_xxhash",
10731079
importpath = "github.com/OneOfOne/xxhash",

vendor/github.com/olekukonko/tablewriter/BUILD.bazel

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)