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  }