github.com/chenbh/concourse/v6@v6.4.2/fly/commands/builds.go (about) 1 package commands 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "strconv" 8 "time" 9 10 "github.com/chenbh/concourse/v6/atc" 11 "github.com/chenbh/concourse/v6/fly/commands/internal/displayhelpers" 12 "github.com/chenbh/concourse/v6/fly/commands/internal/flaghelpers" 13 "github.com/chenbh/concourse/v6/fly/rc" 14 "github.com/chenbh/concourse/v6/fly/ui" 15 "github.com/chenbh/concourse/v6/go-concourse/concourse" 16 "github.com/fatih/color" 17 ) 18 19 const timeDateLayout = "2006-01-02@15:04:05-0700" 20 const inputTimeLayout = "2006-01-02 15:04:05" 21 22 type BuildsCommand struct { 23 AllTeams bool `short:"a" long:"all-teams" description:"Show builds for the all teams that user has access to"` 24 Count int `short:"c" long:"count" default:"50" description:"Number of builds you want to limit the return to"` 25 CurrentTeam bool `long:"current-team" description:"Show builds for the currently targeted team"` 26 Job flaghelpers.JobFlag `short:"j" long:"job" value-name:"PIPELINE/JOB" description:"Name of a job to get builds for"` 27 Json bool `long:"json" description:"Print command result as JSON"` 28 Pipeline flaghelpers.PipelineFlag `short:"p" long:"pipeline" description:"Name of a pipeline to get builds for"` 29 Teams []string `short:"n" long:"team" description:"Show builds for these teams"` 30 Since string `long:"since" description:"Start of the range to filter builds"` 31 Until string `long:"until" description:"End of the range to filter builds"` 32 } 33 34 func (command *BuildsCommand) Execute([]string) error { 35 var ( 36 builds = make([]atc.Build, 0) 37 teams = make([]concourse.Team, 0) 38 39 timeSince time.Time 40 timeUntil time.Time 41 page = concourse.Page{} 42 ) 43 44 target, err := rc.LoadTarget(Fly.Target, Fly.Verbose) 45 if err != nil { 46 return err 47 } 48 49 err = target.Validate() 50 if err != nil { 51 return err 52 } 53 54 page, err = command.validateBuildArguments(timeSince, page, timeUntil) 55 if err != nil { 56 return err 57 } 58 59 page.Limit = command.Count 60 page.Timestamps = command.Since != "" || command.Until != "" 61 62 currentTeam := target.Team() 63 client := target.Client() 64 65 builds, err = command.getBuilds(builds, currentTeam, page, client, teams) 66 if err != nil { 67 return err 68 } 69 70 return command.displayBuilds(builds) 71 } 72 73 func (command *BuildsCommand) getBuilds(builds []atc.Build, currentTeam concourse.Team, page concourse.Page, client concourse.Client, teams []concourse.Team) ([]atc.Build, error) { 74 var err error 75 if command.pipelineFlag() { 76 builds, err = command.validatePipelineBuilds(builds, currentTeam, page) 77 if err != nil { 78 return nil, err 79 } 80 } else if command.jobFlag() { 81 builds, err = command.validateJobBuilds(builds, currentTeam, page) 82 if err != nil { 83 return nil, err 84 } 85 } else if command.AllTeams { 86 teams, err = command.getAllTeams(client, teams) 87 if err != nil { 88 return nil, err 89 } 90 } else if len(command.Teams) > 0 || command.CurrentTeam { 91 teams = command.validateCurrentTeam(teams, currentTeam, client) 92 } else { 93 builds, _, err = client.Builds(page) 94 if err != nil { 95 return nil, err 96 } 97 } 98 99 for _, team := range teams { 100 teamBuilds, _, err := team.Builds(page) 101 if err != nil { 102 return nil, err 103 } 104 105 builds = append(builds, teamBuilds...) 106 } 107 108 return builds, err 109 } 110 111 func (command *BuildsCommand) getAllTeams(client concourse.Client, teams []concourse.Team) ([]concourse.Team, error) { 112 atcTeams, err := client.ListTeams() 113 if err != nil { 114 return nil, err 115 } 116 for _, atcTeam := range atcTeams { 117 teams = append(teams, client.Team(atcTeam.Name)) 118 } 119 return teams, nil 120 } 121 122 func (command *BuildsCommand) validateCurrentTeam(teams []concourse.Team, currentTeam concourse.Team, client concourse.Client) []concourse.Team { 123 if command.CurrentTeam { 124 teams = append(teams, currentTeam) 125 } 126 for _, teamName := range command.Teams { 127 teams = append(teams, client.Team(teamName)) 128 } 129 return teams 130 } 131 132 func (command *BuildsCommand) validateJobBuilds(builds []atc.Build, currentTeam concourse.Team, page concourse.Page) ([]atc.Build, error) { 133 var ( 134 err error 135 found bool 136 ) 137 138 builds, _, found, err = currentTeam.JobBuilds( 139 command.Job.PipelineName, 140 command.Job.JobName, 141 page, 142 ) 143 if err != nil { 144 return nil, err 145 } 146 if !found { 147 displayhelpers.Failf("pipeline/job not found") 148 } 149 return builds, err 150 } 151 152 func (command *BuildsCommand) validatePipelineBuilds(builds []atc.Build, currentTeam concourse.Team, page concourse.Page) ([]atc.Build, error) { 153 _, err := command.Pipeline.Validate() 154 if err != nil { 155 return nil, err 156 } 157 158 var found bool 159 builds, _, found, err = currentTeam.PipelineBuilds( 160 string(command.Pipeline), 161 page, 162 ) 163 164 if err != nil { 165 return nil, err 166 } 167 168 if !found { 169 displayhelpers.Failf("pipeline not found") 170 } 171 172 return builds, err 173 } 174 175 func (command *BuildsCommand) displayBuilds(builds []atc.Build) error { 176 var err error 177 if command.Json { 178 err = displayhelpers.JsonPrint(builds) 179 if err != nil { 180 return err 181 } 182 return nil 183 } 184 185 table := ui.Table{ 186 Headers: ui.TableRow{ 187 {Contents: "id", Color: color.New(color.Bold)}, 188 {Contents: "pipeline/job", Color: color.New(color.Bold)}, 189 {Contents: "build", Color: color.New(color.Bold)}, 190 {Contents: "status", Color: color.New(color.Bold)}, 191 {Contents: "start", Color: color.New(color.Bold)}, 192 {Contents: "end", Color: color.New(color.Bold)}, 193 {Contents: "duration", Color: color.New(color.Bold)}, 194 {Contents: "team", Color: color.New(color.Bold)}, 195 }, 196 } 197 198 buildCap := command.buildCap(builds) 199 for _, b := range builds[:buildCap] { 200 startTimeCell, endTimeCell, durationCell := populateTimeCells(time.Unix(b.StartTime, 0), time.Unix(b.EndTime, 0)) 201 202 var pipelineJobCell, buildCell ui.TableCell 203 if b.PipelineName == "" { 204 pipelineJobCell.Contents = "one-off" 205 buildCell.Contents = "n/a" 206 } else { 207 pipelineJobCell.Contents = fmt.Sprintf("%s/%s", b.PipelineName, b.JobName) 208 buildCell.Contents = b.Name 209 } 210 211 var statusCell ui.TableCell 212 statusCell.Contents = b.Status 213 214 switch b.Status { 215 case "pending": 216 statusCell.Color = ui.PendingColor 217 case "started": 218 statusCell.Color = ui.StartedColor 219 case "succeeded": 220 statusCell.Color = ui.SucceededColor 221 case "failed": 222 statusCell.Color = ui.FailedColor 223 case "errored": 224 statusCell.Color = ui.ErroredColor 225 case "aborted": 226 statusCell.Color = ui.AbortedColor 227 case "paused": 228 statusCell.Color = ui.PausedColor 229 } 230 231 table.Data = append(table.Data, []ui.TableCell{ 232 {Contents: strconv.Itoa(b.ID)}, 233 pipelineJobCell, 234 buildCell, 235 statusCell, 236 startTimeCell, 237 endTimeCell, 238 durationCell, 239 {Contents: b.TeamName}, 240 }) 241 } 242 243 return table.Render(os.Stdout, Fly.PrintTableHeaders) 244 } 245 246 func (command *BuildsCommand) validateBuildArguments(timeSince time.Time, page concourse.Page, timeUntil time.Time) (concourse.Page, error) { 247 var err error 248 if command.Since != "" { 249 timeSince, err = time.ParseInLocation(inputTimeLayout, command.Since, time.Now().Location()) 250 if err != nil { 251 return page, errors.New("Since time should be in the format: " + inputTimeLayout) 252 } 253 page.Since = int(timeSince.Unix()) 254 } 255 if command.Until != "" { 256 timeUntil, err = time.ParseInLocation(inputTimeLayout, command.Until, time.Now().Location()) 257 if err != nil { 258 return page, errors.New("Until time should be in the format: " + inputTimeLayout) 259 } 260 page.Until = int(timeUntil.Unix()) 261 } 262 if timeSince.After(timeUntil) && command.Since != "" && command.Until != "" { 263 return page, errors.New("Cannot have --since after --until") 264 } 265 if command.pipelineFlag() && command.jobFlag() { 266 return page, errors.New("Cannot specify both --pipeline and --job") 267 } 268 if command.CurrentTeam && command.AllTeams { 269 return page, errors.New("Cannot specify both --all-teams and --current-team") 270 } 271 if len(command.Teams) > 0 && command.AllTeams { 272 return page, errors.New("Cannot specify both --all-teams and --team") 273 } 274 return page, err 275 } 276 277 func populateTimeCells(startTime time.Time, endTime time.Time) (ui.TableCell, ui.TableCell, ui.TableCell) { 278 var startTimeCell ui.TableCell 279 var endTimeCell ui.TableCell 280 var durationCell ui.TableCell 281 282 startTime = startTime.Local() 283 endTime = endTime.Local() 284 zeroTime := time.Unix(0, 0) 285 286 if startTime == zeroTime { 287 startTimeCell.Contents = "n/a" 288 } else { 289 startTimeCell.Contents = startTime.Format(timeDateLayout) 290 } 291 292 if endTime == zeroTime { 293 endTimeCell.Contents = "n/a" 294 durationCell.Contents = fmt.Sprintf("%v+", roundSecondsOffDuration(time.Since(startTime))) 295 } else { 296 endTimeCell.Contents = endTime.Format(timeDateLayout) 297 durationCell.Contents = endTime.Sub(startTime).String() 298 } 299 300 if startTime == zeroTime { 301 durationCell.Contents = "n/a" 302 } 303 304 return startTimeCell, endTimeCell, durationCell 305 } 306 307 func roundSecondsOffDuration(d time.Duration) time.Duration { 308 return d - (d % time.Second) 309 } 310 311 func (command *BuildsCommand) jobFlag() bool { 312 return command.Job.PipelineName != "" && command.Job.JobName != "" 313 } 314 315 func (command *BuildsCommand) pipelineFlag() bool { 316 return command.Pipeline != "" 317 } 318 319 func (command *BuildsCommand) buildCap(builds []atc.Build) int { 320 if command.Count < len(builds) { 321 return command.Count 322 } 323 324 return len(builds) 325 }