Skip to content

Commit e7b15b3

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. * --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)
1 parent 0d3255c commit e7b15b3

File tree

25 files changed

+3578
-1
lines changed

25 files changed

+3578
-1
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: 3 additions & 1 deletion
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
],
@@ -67,7 +69,7 @@ pkg_rpm(
6769
architecture = "x86_64",
6870
description = "A command line interface to interact with the Peridot build system",
6971
license = "MIT",
70-
release = "1",
72+
release = "2",
7173
source_date_epoch = 0,
7274
summary = "Peridot Command Line Interface",
7375
version = "0.2.3",

peridot/cmd/v1/peridot/main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ func init() {
5454
root.PersistentFlags().String("client-secret", "", "Client secret for authentication")
5555
root.PersistentFlags().String("project-id", "", "Peridot project ID")
5656
root.PersistentFlags().Bool("debug", false, "Debug mode")
57+
5758
root.PersistentFlags().StringP("output", "o", "table", "Output format (table|json)")
59+
root.PersistentFlags().Bool("no-color", false, "don't colorize output")
60+
root.PersistentFlags().Bool("no-wait", false, "don't wait for completion")
5861

5962
root.AddCommand(lookaside)
6063
lookaside.AddCommand(lookasideUpload)
@@ -65,6 +68,7 @@ func init() {
6568

6669
root.AddCommand(task)
6770
task.AddCommand(taskLogs)
71+
task.AddCommand(taskInfo)
6872

6973
root.AddCommand(project)
7074
project.AddCommand(projectInfo)
@@ -125,3 +129,11 @@ func debug() bool {
125129
func output() string {
126130
return viper.GetString("output")
127131
}
132+
133+
func color() bool {
134+
return !viper.GetBool("no-color")
135+
}
136+
137+
func wait() bool {
138+
return !viper.GetBool("no-wait")
139+
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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+
"time"
40+
41+
"github.com/google/uuid"
42+
"github.com/olekukonko/tablewriter"
43+
"github.com/spf13/cobra"
44+
"openapi.peridot.resf.org/peridotopenapi"
45+
)
46+
47+
var taskInfo = &cobra.Command{
48+
Use: "info [name-or-buildId]",
49+
Args: cobra.ExactArgs(1),
50+
Run: taskInfoMn,
51+
}
52+
53+
var (
54+
showLogLink bool
55+
showSubmitterInfo bool
56+
showDuration bool
57+
)
58+
59+
func init() {
60+
taskInfo.Flags().BoolVar(&succeeded, "succeeded", true, "only query successful tasks")
61+
taskInfo.Flags().BoolVar(&cancelled, "cancelled", false, "only query cancelled tasks")
62+
taskInfo.Flags().BoolVar(&failed, "failed", false, "only query failed tasks")
63+
taskInfo.MarkFlagsMutuallyExclusive("cancelled", "failed", "succeeded")
64+
65+
taskInfo.Flags().BoolVarP(&showLogLink, "logs", "L", false, "include log link in output (table format only)")
66+
taskInfo.Flags().BoolVar(&showSubmitterInfo, "submitter", false, "include submitter details (table format only)")
67+
taskInfo.Flags().BoolVar(&showDuration, "duration", true, "include duration from start to stop (table format only)")
68+
}
69+
70+
func getNextColor(colors tablewriter.Colors) tablewriter.Colors {
71+
bgColor := getNextBackgroundColor(colors[0])
72+
fgColor := colors[1]
73+
if bgColor == -1 {
74+
fgColor = getNextForegroundColor(0)
75+
bgColor = getNextBackgroundColor(0)
76+
}
77+
return tablewriter.Colors{bgColor, fgColor}
78+
}
79+
80+
func getNextForegroundColor(color int) int {
81+
switch color {
82+
case 0:
83+
return tablewriter.FgGreenColor
84+
case tablewriter.FgCyanColor:
85+
return tablewriter.FgHiGreenColor
86+
case tablewriter.FgHiCyanColor:
87+
return tablewriter.FgGreenColor
88+
default:
89+
color++
90+
return color
91+
}
92+
}
93+
94+
func getNextBackgroundColor(color int) int {
95+
switch color {
96+
case 0:
97+
return tablewriter.BgRedColor
98+
case tablewriter.BgCyanColor:
99+
return tablewriter.BgHiRedColor
100+
case tablewriter.BgHiCyanColor:
101+
return -1
102+
default:
103+
color++
104+
return color
105+
}
106+
}
107+
108+
func buildHeaderAndAutoMergeCells() ([]string, []int) {
109+
header := []string{"ptid", "tid", "status", "type", "arch", "created", "finished"}
110+
mergableNames := []string{"ptid", "type", "arch"}
111+
var autoMergeCells []int
112+
113+
// Conditional appending to header
114+
if showDuration {
115+
header = append(header, "duration")
116+
mergableNames = append(mergableNames, "duration")
117+
}
118+
if showSubmitterInfo {
119+
header = append(header, "submitter")
120+
mergableNames = append(mergableNames, "submitter")
121+
}
122+
if showLogLink {
123+
header = append(header, "logs")
124+
}
125+
126+
// Determine dynamic indices for auto-merge cells
127+
for _, itemName := range mergableNames {
128+
index := slices.Index(header, itemName)
129+
if index != -1 {
130+
autoMergeCells = append(autoMergeCells, index)
131+
}
132+
}
133+
134+
return header, autoMergeCells
135+
}
136+
137+
func convertSubTaskSliceToCSV(task peridotopenapi.V1AsyncTask) {
138+
subtasks, ok := task.GetSubtasksOk()
139+
if !ok {
140+
errFatal(fmt.Errorf("error getting subtasks: %v", ok))
141+
}
142+
143+
var parentTask = (*subtasks)[0]
144+
145+
var table = tablewriter.NewWriter(os.Stdout)
146+
// var data [][]string
147+
148+
var header, autoMergeCells = buildHeaderAndAutoMergeCells()
149+
150+
var lastColor = tablewriter.Colors{0, tablewriter.FgWhiteColor}
151+
var seenTasksColors = make(map[string]tablewriter.Colors)
152+
153+
var parentTaskIds []string // cache parentTaskIds for colorizing
154+
155+
// precache all the subtask's parent tasks so we know if we should color them
156+
for _, subtask := range *subtasks {
157+
parentTaskIds = append(parentTaskIds, subtask.GetParentTaskId())
158+
}
159+
160+
for _, subtask := range *subtasks {
161+
json, err := subtask.MarshalJSON()
162+
if err != nil {
163+
errFatal(err)
164+
}
165+
166+
if debug() {
167+
err = PrettyPrintJSON(json)
168+
if err != nil {
169+
errFatal(err)
170+
}
171+
// taskResponse, _ := subtask.GetResponse().MarshalJSON()
172+
// taskMetadata, _ := subtask.GetMetadata().MarshalJSON()
173+
}
174+
175+
subtaskId := subtask.GetId()
176+
subtaskParentTaskId := subtask.GetParentTaskId()
177+
createdAt := subtask.GetCreatedAt()
178+
finishedAt := subtask.GetFinishedAt()
179+
180+
row := []string{
181+
subtaskParentTaskId,
182+
subtaskId,
183+
string(subtask.GetStatus()),
184+
string(subtask.GetType()),
185+
subtask.GetArch(),
186+
formatTime(createdAt),
187+
formatTime(finishedAt),
188+
}
189+
190+
if showDuration {
191+
row = append(row, formatDuration(createdAt, finishedAt))
192+
}
193+
194+
if showSubmitterInfo {
195+
effectiveSubmitter := fmt.Sprintf("%s <%s>", parentTask.GetSubmitterId(), parentTask.GetSubmitterEmail())
196+
row = append(row, effectiveSubmitter)
197+
}
198+
199+
if showLogLink {
200+
row = append(row, getLogLink(subtaskId))
201+
}
202+
203+
if !color() {
204+
table.Append(row)
205+
continue
206+
}
207+
208+
nextColor := tablewriter.Colors{tablewriter.BgBlackColor, tablewriter.FgWhiteColor}
209+
needsColor := hasAny(parentTaskIds, subtaskId)
210+
if _, seen := seenTasksColors[subtaskId]; !seen && needsColor {
211+
debugP("before: lastcolor: %v next: %v", lastColor, nextColor)
212+
nextColor = getNextColor(lastColor)
213+
debugP("after: lastcolor: %v next: %v", lastColor, nextColor)
214+
lastColor = nextColor
215+
seenTasksColors[subtaskId] = nextColor
216+
}
217+
218+
tidColors := nextColor
219+
220+
ptidColors := tablewriter.Colors{tablewriter.FgWhiteColor, tablewriter.BgBlackColor}
221+
if seenColor, seen := seenTasksColors[subtaskParentTaskId]; seen {
222+
ptidColors = seenColor
223+
}
224+
225+
var colors = make([]tablewriter.Colors, len(row))
226+
227+
debugP("tidcolor: %v ptidcolor %d", tidColors, ptidColors)
228+
for i, v := range header {
229+
switch v {
230+
case "ptid":
231+
colors[i] = ptidColors
232+
case "tid":
233+
colors[i] = tidColors
234+
default:
235+
colors[i] = tablewriter.Colors{}
236+
}
237+
}
238+
239+
table.Rich(row, colors)
240+
}
241+
242+
table.SetHeader(header)
243+
table.SetAutoMergeCellsByColumnIndex(autoMergeCells)
244+
table.SetRowLine(true)
245+
table.Render()
246+
247+
}
248+
249+
func debugP(s string, args ...any) {
250+
if debug() {
251+
log.Printf(s, args...)
252+
}
253+
}
254+
255+
func hasAny(slice []string, target string) bool {
256+
if idx := slices.Index(slice, target); idx >= 0 {
257+
return true
258+
}
259+
return false
260+
}
261+
262+
func taskInfoMn(_ *cobra.Command, args []string) {
263+
// Ensure project id exists
264+
projectId := mustGetProjectID()
265+
266+
taskId := args[0]
267+
268+
err := uuid.Validate(taskId)
269+
if err != nil {
270+
errFatal(errors.New("invalid task id"))
271+
}
272+
273+
taskCl := getClient(serviceTask).(peridotopenapi.TaskServiceApi)
274+
log.Printf("Searching for task %s in project %s\n", taskId, projectId)
275+
276+
var waiting = false
277+
for {
278+
res, _, err := taskCl.GetTask(getContext(), projectId, taskId).Execute()
279+
if err != nil {
280+
errFatal(fmt.Errorf("error getting task: %s", err.Error()))
281+
}
282+
283+
task := res.GetTask()
284+
285+
switch output() {
286+
case "table":
287+
if !waiting || task.GetDone() {
288+
convertSubTaskSliceToCSV(task)
289+
}
290+
if wait() && !task.GetDone() {
291+
waiting = true
292+
log.Printf("Waiting for task %s to complete", task.GetTaskId())
293+
time.Sleep(5 * time.Second)
294+
continue
295+
}
296+
297+
case "json":
298+
taskJSON, err := res.MarshalJSON()
299+
if err != nil {
300+
errFatal(err)
301+
}
302+
303+
err = PrettyPrintJSON(taskJSON)
304+
if err != nil {
305+
errFatal(err)
306+
}
307+
}
308+
break
309+
}
310+
}

0 commit comments

Comments
 (0)