github.com/zaquestion/lab@v0.25.1/cmd/ci_trace.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/MakeNowJust/heredoc/v2" 14 "github.com/pkg/errors" 15 "github.com/rsteube/carapace" 16 "github.com/spf13/cobra" 17 "github.com/zaquestion/lab/internal/action" 18 "github.com/zaquestion/lab/internal/git" 19 lab "github.com/zaquestion/lab/internal/gitlab" 20 ) 21 22 // ciLintCmd represents the lint command 23 var ciTraceCmd = &cobra.Command{ 24 Use: "trace [remote] [branch][:job]", 25 Aliases: []string{"logs"}, 26 Short: "Trace the output of a ci job", 27 Long: heredoc.Doc(` 28 Download the CI pipeline job artifacts for the given or current branch if 29 none provided. If a job is not specified the latest running job or last 30 job in the pipeline is used 31 32 The branch name, when using with the --merge-request option, can be the 33 merge request number, which matches the branch name internally. The "job" 34 portion is the given job name, which may contain whitespace characters 35 and which, for this specific case, must be quoted.`), 36 Example: heredoc.Doc(` 37 lab ci trace upstream feature_branch 38 lab ci trace upstream :'my custom stage' 39 lab ci trace upstream 18 --merge-request 40 lab ci trace upstream 18:'my custom stage' --merge-request 41 lab ci trace upstream 18:'my custom stage' --merge-request --bridge 'security-tests'`), 42 PersistentPreRun: labPersistentPreRun, 43 Run: func(cmd *cobra.Command, args []string) { 44 var ( 45 rn string 46 jobName string 47 err error 48 ) 49 jobName, branchArgs, err := filterJobArg(args) 50 if err != nil { 51 log.Fatal(err) 52 } 53 54 forMR, err := cmd.Flags().GetBool("merge-request") 55 if err != nil { 56 log.Fatal(err) 57 } 58 59 bridgeName, err = cmd.Flags().GetString("bridge") 60 if err != nil { 61 log.Fatal(err) 62 } else if bridgeName != "" { 63 followBridge = true 64 } else { 65 followBridge, err = cmd.Flags().GetBool("follow") 66 if err != nil { 67 log.Fatal(err) 68 } 69 } 70 71 rn, pipelineID, err := getPipelineFromArgs(branchArgs, forMR) 72 if err != nil { 73 log.Fatal(err) 74 } 75 76 pager := newPager(cmd.Flags()) 77 defer pager.Close() 78 79 err = doTrace(context.Background(), os.Stdout, rn, pipelineID, jobName) 80 if err != nil { 81 log.Fatal(err) 82 } 83 }, 84 } 85 86 func doTrace(ctx context.Context, w io.Writer, projID string, pipelineID int, name string) error { 87 var ( 88 once sync.Once 89 offset int64 90 ) 91 for range time.NewTicker(time.Second * 3).C { 92 if ctx.Err() == context.Canceled { 93 break 94 } 95 trace, job, err := lab.CITrace(projID, pipelineID, name, followBridge, bridgeName) 96 if err != nil || job == nil || trace == nil { 97 return errors.Wrap(err, "failed to find job") 98 } 99 switch job.Status { 100 case "pending": 101 fmt.Fprintf(w, "%s is pending... waiting for job to start\n", job.Name) 102 continue 103 case "manual": 104 fmt.Fprintf(w, "Manual job %s not started, waiting for job to start\n", job.Name) 105 continue 106 } 107 once.Do(func() { 108 if name == "" { 109 name = job.Name 110 } 111 fmt.Fprintf(w, "Showing logs for %s job #%d\n", job.Name, job.ID) 112 }) 113 114 _, err = io.CopyN(ioutil.Discard, trace, offset) 115 if err != nil { 116 return err 117 } 118 119 lenT, err := io.Copy(w, trace) 120 if err != nil { 121 return err 122 } 123 offset += int64(lenT) 124 125 if job.Status == "success" || 126 job.Status == "failed" || 127 job.Status == "cancelled" { 128 return nil 129 } 130 } 131 return nil 132 } 133 134 // filterJobArg might be a small function, but contain a lot of 135 // possibilities to be handled. It gets the remote, branch and jobname from 136 // the CLI args. These can be present in the following formats: 137 // 1. <remote> <branch>:<jobname> 138 // 2. <remote> :<jobname> 139 // 3. <remote> <branch> 140 // 4. <branch>:<jobname> 141 // 5. <remote> 142 // 6. :<jobname> 143 func filterJobArg(args []string) (string, []string, error) { 144 branchArgs := []string{} 145 jobName := "" 146 147 if len(args) == 1 { 148 // <remote> alone or :<jobname>? 149 ok, err := git.IsRemote(args[0]) 150 if err != nil { 151 return "", branchArgs, err 152 } 153 if ok { 154 branchArgs = append(branchArgs, args[0]) 155 } else { 156 jobName = args[0] 157 } 158 } else if len(args) > 1 { 159 // the first arg is always the remote, we just need to check 160 // later the jobName. 161 branchArgs = append(branchArgs, args[0]) 162 jobName = args[1] 163 } 164 165 // <branch>:<jobname>, <branch> or :<jobname>? 166 if strings.Contains(jobName, ":") { 167 // check for <branch>:<jobname> and :<jobname> 168 ps := strings.SplitN(jobName, ":", 2) 169 branchArgs = append(branchArgs, ps[0]) 170 jobName = ps[1] 171 } else { 172 // the jobName refers to a branch name 173 branchArgs = append(branchArgs, jobName) 174 jobName = "" 175 } 176 177 return jobName, branchArgs, nil 178 } 179 180 func init() { 181 ciTraceCmd.Flags().Bool("merge-request", false, "use merge request pipeline if enabled") 182 ciCmd.AddCommand(ciTraceCmd) 183 carapace.Gen(ciTraceCmd).PositionalCompletion( 184 action.Remotes(), 185 action.RemoteBranches(0), 186 ) 187 }