github.com/pachyderm/pachyderm@v1.13.4/src/server/pps/pretty/pretty.go (about)

     1  package pretty
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  	"text/template"
    10  
    11  	units "github.com/docker/go-units"
    12  	"github.com/fatih/color"
    13  	"github.com/gogo/protobuf/types"
    14  	"github.com/juju/ansiterm"
    15  	"github.com/pachyderm/pachyderm/src/client"
    16  	pfsclient "github.com/pachyderm/pachyderm/src/client/pfs"
    17  	"github.com/pachyderm/pachyderm/src/client/pkg/errors"
    18  	ppsclient "github.com/pachyderm/pachyderm/src/client/pps"
    19  	"github.com/pachyderm/pachyderm/src/server/pkg/pretty"
    20  )
    21  
    22  const (
    23  	// PipelineHeader is the header for pipelines.
    24  	PipelineHeader = "NAME\tVERSION\tINPUT\tCREATED\tSTATE / LAST JOB\tDESCRIPTION\t\n"
    25  	// JobHeader is the header for jobs
    26  	JobHeader = "ID\tPIPELINE\tSTARTED\tDURATION\tRESTART\tPROGRESS\tDL\tUL\tSTATE\t\n"
    27  	// DatumHeader is the header for datums
    28  	DatumHeader = "ID\tFILES\tSTATUS\tTIME\t\n"
    29  	// SecretHeader is the header for secrets
    30  	SecretHeader = "NAME\tTYPE\tCREATED\t\n"
    31  	// jobReasonLen is the amount of the job reason that we print
    32  	jobReasonLen = 25
    33  )
    34  
    35  func safeTrim(s string, l int) string {
    36  	if len(s) < l {
    37  		return s
    38  	}
    39  	return strings.TrimSpace(s[:l]) + "..."
    40  }
    41  
    42  // PrintJobInfo pretty-prints job info.
    43  func PrintJobInfo(w io.Writer, jobInfo *ppsclient.JobInfo, fullTimestamps bool) {
    44  	fmt.Fprintf(w, "%s\t", jobInfo.Job.ID)
    45  	fmt.Fprintf(w, "%s\t", jobInfo.Pipeline.Name)
    46  	if fullTimestamps {
    47  		fmt.Fprintf(w, "%s\t", jobInfo.Started.String())
    48  	} else {
    49  		fmt.Fprintf(w, "%s\t", pretty.Ago(jobInfo.Started))
    50  	}
    51  	if jobInfo.Finished != nil {
    52  		fmt.Fprintf(w, "%s\t", pretty.TimeDifference(jobInfo.Started, jobInfo.Finished))
    53  	} else {
    54  		fmt.Fprintf(w, "-\t")
    55  	}
    56  	fmt.Fprintf(w, "%d\t", jobInfo.Restart)
    57  	fmt.Fprintf(w, "%s\t", Progress(jobInfo))
    58  	fmt.Fprintf(w, "%s\t", pretty.Size(jobInfo.Stats.DownloadBytes))
    59  	fmt.Fprintf(w, "%s\t", pretty.Size(jobInfo.Stats.UploadBytes))
    60  	if jobInfo.State == ppsclient.JobState_JOB_FAILURE {
    61  		fmt.Fprintf(w, "%s: %s\t", JobState(jobInfo.State), safeTrim(jobInfo.Reason, jobReasonLen))
    62  	} else {
    63  		fmt.Fprintf(w, "%s\t", JobState(jobInfo.State))
    64  	}
    65  	fmt.Fprintln(w)
    66  }
    67  
    68  // PrintPipelineInfo pretty-prints pipeline info.
    69  func PrintPipelineInfo(w io.Writer, pipelineInfo *ppsclient.PipelineInfo, fullTimestamps bool) {
    70  	if pipelineInfo.Transform == nil {
    71  		fmt.Fprintf(w, "%s\t", pipelineInfo.Pipeline.Name)
    72  		fmt.Fprint(w, "-\t")
    73  		fmt.Fprint(w, "-\t")
    74  		fmt.Fprint(w, "-\t")
    75  		fmt.Fprintf(w, "%s / %s\t", pipelineState(pipelineInfo.State), JobState(pipelineInfo.LastJobState))
    76  		fmt.Fprint(w, "could not retrieve pipeline spec\t")
    77  	} else {
    78  		fmt.Fprintf(w, "%s\t", pipelineInfo.Pipeline.Name)
    79  		fmt.Fprintf(w, "%d\t", pipelineInfo.Version)
    80  		fmt.Fprintf(w, "%s\t", ShorthandInput(pipelineInfo.Input))
    81  		if fullTimestamps {
    82  			fmt.Fprintf(w, "%s\t", pipelineInfo.CreatedAt.String())
    83  		} else {
    84  			fmt.Fprintf(w, "%s\t", pretty.Ago(pipelineInfo.CreatedAt))
    85  		}
    86  		fmt.Fprintf(w, "%s / %s\t", pipelineState(pipelineInfo.State), JobState(pipelineInfo.LastJobState))
    87  		fmt.Fprintf(w, "%s\t", pipelineInfo.Description)
    88  	}
    89  	fmt.Fprintln(w)
    90  }
    91  
    92  // PrintWorkerStatusHeader pretty prints a worker status header.
    93  func PrintWorkerStatusHeader(w io.Writer) {
    94  	fmt.Fprint(w, "WORKER\tJOB\tDATUM\tSTARTED\tQUEUE\t\n")
    95  }
    96  
    97  // PrintWorkerStatus pretty prints a worker status.
    98  func PrintWorkerStatus(w io.Writer, workerStatus *ppsclient.WorkerStatus, fullTimestamps bool) {
    99  	fmt.Fprintf(w, "%s\t", workerStatus.WorkerID)
   100  	fmt.Fprintf(w, "%s\t", workerStatus.JobID)
   101  	for _, datum := range workerStatus.Data {
   102  		fmt.Fprintf(w, datum.Path)
   103  	}
   104  	fmt.Fprintf(w, "\t")
   105  	if fullTimestamps {
   106  		fmt.Fprintf(w, "%s\t", workerStatus.Started.String())
   107  	} else {
   108  		fmt.Fprintf(w, "%s\t", pretty.Ago(workerStatus.Started))
   109  	}
   110  	fmt.Fprintf(w, "%d\t", workerStatus.QueueSize)
   111  	fmt.Fprintln(w)
   112  }
   113  
   114  // PrintableJobInfo is a wrapper around JobInfo containing any formatting options
   115  // used within the template to conditionally print information.
   116  type PrintableJobInfo struct {
   117  	*ppsclient.JobInfo
   118  	FullTimestamps bool
   119  }
   120  
   121  // NewPrintableJobInfo constructs a PrintableJobInfo from just a JobInfo.
   122  func NewPrintableJobInfo(ji *ppsclient.JobInfo) *PrintableJobInfo {
   123  	return &PrintableJobInfo{
   124  		JobInfo: ji,
   125  	}
   126  }
   127  
   128  // PrintDetailedJobInfo pretty-prints detailed job info.
   129  func PrintDetailedJobInfo(w io.Writer, jobInfo *PrintableJobInfo) error {
   130  	template, err := template.New("JobInfo").Funcs(funcMap).Parse(
   131  		`ID: {{.Job.ID}} {{if .Pipeline}}
   132  Pipeline: {{.Pipeline.Name}} {{end}} {{if .ParentJob}}
   133  Parent: {{.ParentJob.ID}} {{end}}{{if .FullTimestamps}}
   134  Started: {{.Started}}{{else}}
   135  Started: {{prettyAgo .Started}} {{end}}{{if .Finished}}
   136  Duration: {{prettyTimeDifference .Started .Finished}} {{end}}
   137  State: {{jobState .State}}
   138  Reason: {{.Reason}}
   139  Processed: {{.DataProcessed}}
   140  Failed: {{.DataFailed}}
   141  Skipped: {{.DataSkipped}}
   142  Recovered: {{.DataRecovered}}
   143  Total: {{.DataTotal}}
   144  Data Downloaded: {{prettySize .Stats.DownloadBytes}}
   145  Data Uploaded: {{prettySize .Stats.UploadBytes}}
   146  Download Time: {{prettyDuration .Stats.DownloadTime}}
   147  Process Time: {{prettyDuration .Stats.ProcessTime}}
   148  Upload Time: {{prettyDuration .Stats.UploadTime}}
   149  Datum Timeout: {{.DatumTimeout}}
   150  Job Timeout: {{.JobTimeout}}
   151  Worker Status:
   152  {{workerStatus .}}Restarts: {{.Restart}}
   153  ParallelismSpec: {{.ParallelismSpec}}
   154  {{ if .ResourceRequests }}ResourceRequests:
   155    CPU: {{ .ResourceRequests.Cpu }}
   156    Memory: {{ .ResourceRequests.Memory }} {{end}}
   157  {{ if .ResourceLimits }}ResourceLimits:
   158    CPU: {{ .ResourceLimits.Cpu }}
   159    Memory: {{ .ResourceLimits.Memory }}
   160    {{ if .ResourceLimits.Gpu }}GPU:
   161      Type: {{ .ResourceLimits.Gpu.Type }}
   162      Number: {{ .ResourceLimits.Gpu.Number }} {{end}} {{end}}
   163  {{ if .SidecarResourceLimits }}SidecarResourceLimits:
   164    CPU: {{ .SidecarResourceLimits.Cpu }}
   165    Memory: {{ .SidecarResourceLimits.Memory }} {{end}}
   166  {{ if .Service }}Service:
   167  	{{ if .Service.InternalPort }}InternalPort: {{ .Service.InternalPort }} {{end}}
   168  	{{ if .Service.ExternalPort }}ExternalPort: {{ .Service.ExternalPort }} {{end}} {{end}}Input:
   169  {{jobInput .}}
   170  Transform:
   171  {{prettyTransform .Transform}} {{if .OutputCommit}}
   172  Output Commit: {{.OutputCommit.ID}} {{end}} {{ if .StatsCommit }}
   173  Stats Commit: {{.StatsCommit.ID}} {{end}} {{ if .Egress }}
   174  Egress: {{.Egress.URL}} {{end}}
   175  `)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	return template.Execute(w, jobInfo)
   180  }
   181  
   182  // PrintablePipelineInfo is a wrapper around PipelinInfo containing any formatting options
   183  // used within the template to conditionally print information.
   184  type PrintablePipelineInfo struct {
   185  	*ppsclient.PipelineInfo
   186  	FullTimestamps bool
   187  }
   188  
   189  // NewPrintablePipelineInfo constructs a PrintablePipelineInfo from just a PipelineInfo.
   190  func NewPrintablePipelineInfo(pi *ppsclient.PipelineInfo) *PrintablePipelineInfo {
   191  	return &PrintablePipelineInfo{
   192  		PipelineInfo: pi,
   193  	}
   194  }
   195  
   196  // PrintDetailedPipelineInfo pretty-prints detailed pipeline info.
   197  func PrintDetailedPipelineInfo(w io.Writer, pipelineInfo *PrintablePipelineInfo) error {
   198  	template, err := template.New("PipelineInfo").Funcs(funcMap).Parse(
   199  		`Name: {{.Pipeline.Name}}{{if .Description}}
   200  Description: {{.Description}}{{end}}{{if .FullTimestamps }}
   201  Created: {{.CreatedAt}}{{ else }}
   202  Created: {{prettyAgo .CreatedAt}} {{end}}
   203  State: {{pipelineState .State}}
   204  Reason: {{.Reason}}
   205  Workers Available: {{.WorkersAvailable}}/{{.WorkersRequested}}
   206  Stopped: {{ .Stopped }}
   207  Parallelism Spec: {{.ParallelismSpec}}
   208  {{ if .ResourceRequests }}ResourceRequests:
   209    CPU: {{ .ResourceRequests.Cpu }}
   210    Memory: {{ .ResourceRequests.Memory }} {{end}}
   211  {{ if .ResourceLimits }}ResourceLimits:
   212    CPU: {{ .ResourceLimits.Cpu }}
   213    Memory: {{ .ResourceLimits.Memory }}
   214    {{ if .ResourceLimits.Gpu }}GPU:
   215      Type: {{ .ResourceLimits.Gpu.Type }} 
   216      Number: {{ .ResourceLimits.Gpu.Number }} {{end}} {{end}}
   217  Datum Timeout: {{.DatumTimeout}}
   218  Job Timeout: {{.JobTimeout}}
   219  Input:
   220  {{pipelineInput .PipelineInfo}}
   221  {{ if .GithookURL }}Githook URL: {{.GithookURL}} {{end}}
   222  Output Branch: {{.OutputBranch}}
   223  Transform:
   224  {{prettyTransform .Transform}}
   225  {{ if .Egress }}Egress: {{.Egress.URL}} {{end}}
   226  {{if .RecentError}} Recent Error: {{.RecentError}} {{end}}
   227  Job Counts:
   228  {{jobCounts .JobCounts}}
   229  `)
   230  	if err != nil {
   231  		return err
   232  	}
   233  	err = template.Execute(w, pipelineInfo)
   234  	if err != nil {
   235  		return err
   236  	}
   237  	return nil
   238  }
   239  
   240  // PrintDatumInfo pretty-prints file info.
   241  // If recurse is false and directory size is 0, display "-" instead
   242  // If fast is true and file size is 0, display "-" instead
   243  func PrintDatumInfo(w io.Writer, datumInfo *ppsclient.DatumInfo) {
   244  	totalTime := "-"
   245  	if datumInfo.Stats != nil {
   246  		totalTime = units.HumanDuration(client.GetDatumTotalTime(datumInfo.Stats))
   247  	}
   248  	if datumInfo.Datum.ID == "" {
   249  		datumInfo.Datum.ID = "-"
   250  	}
   251  	fmt.Fprintf(w, "%s\t%s\t%s\t%s\t", datumInfo.Datum.ID, datumFiles(datumInfo), datumState(datumInfo.State), totalTime)
   252  	fmt.Fprintln(w)
   253  }
   254  
   255  func datumFiles(datumInfo *ppsclient.DatumInfo) string {
   256  	builder := &strings.Builder{}
   257  	for i, fi := range datumInfo.Data {
   258  		if i != 0 {
   259  			builder.WriteString(", ")
   260  		}
   261  		fmt.Fprintf(builder, "%s@%s:%s", fi.File.Commit.Repo.Name, fi.File.Commit.ID, fi.File.Path)
   262  	}
   263  	return builder.String()
   264  }
   265  
   266  // PrintDetailedDatumInfo pretty-prints detailed info about a datum
   267  func PrintDetailedDatumInfo(w io.Writer, datumInfo *ppsclient.DatumInfo) {
   268  	fmt.Fprintf(w, "ID\t%s\n", datumInfo.Datum.ID)
   269  	fmt.Fprintf(w, "Job ID\t%s\n", datumInfo.Datum.Job.ID)
   270  	fmt.Fprintf(w, "State\t%s\n", datumInfo.State)
   271  	fmt.Fprintf(w, "Data Downloaded\t%s\n", pretty.Size(datumInfo.Stats.DownloadBytes))
   272  	fmt.Fprintf(w, "Data Uploaded\t%s\n", pretty.Size(datumInfo.Stats.UploadBytes))
   273  
   274  	totalTime := client.GetDatumTotalTime(datumInfo.Stats).String()
   275  	fmt.Fprintf(w, "Total Time\t%s\n", totalTime)
   276  
   277  	var downloadTime string
   278  	dl, err := types.DurationFromProto(datumInfo.Stats.DownloadTime)
   279  	if err != nil {
   280  		downloadTime = err.Error()
   281  	} else {
   282  		downloadTime = dl.String()
   283  	}
   284  	fmt.Fprintf(w, "Download Time\t%s\n", downloadTime)
   285  
   286  	var procTime string
   287  	proc, err := types.DurationFromProto(datumInfo.Stats.ProcessTime)
   288  	if err != nil {
   289  		procTime = err.Error()
   290  	} else {
   291  		procTime = proc.String()
   292  	}
   293  	fmt.Fprintf(w, "Process Time\t%s\n", procTime)
   294  
   295  	var uploadTime string
   296  	ul, err := types.DurationFromProto(datumInfo.Stats.UploadTime)
   297  	if err != nil {
   298  		uploadTime = err.Error()
   299  	} else {
   300  		uploadTime = ul.String()
   301  	}
   302  	fmt.Fprintf(w, "Upload Time\t%s\n", uploadTime)
   303  
   304  	fmt.Fprintf(w, "PFS State:\n")
   305  	tw := ansiterm.NewTabWriter(w, 10, 1, 3, ' ', 0)
   306  	PrintFileHeader(tw)
   307  	PrintFile(tw, datumInfo.PfsState)
   308  	tw.Flush()
   309  	fmt.Fprintf(w, "Inputs:\n")
   310  	tw = ansiterm.NewTabWriter(w, 10, 1, 3, ' ', 0)
   311  	PrintFileHeader(tw)
   312  	for _, d := range datumInfo.Data {
   313  		PrintFile(tw, d.File)
   314  	}
   315  	tw.Flush()
   316  }
   317  
   318  // PrintSecretInfo pretty-prints secret info.
   319  func PrintSecretInfo(w io.Writer, secretInfo *ppsclient.SecretInfo) {
   320  	fmt.Fprintf(w, "%s\t%s\t%s\t\n", secretInfo.Secret.Name, secretInfo.Type, pretty.Ago(secretInfo.CreationTimestamp))
   321  }
   322  
   323  // PrintFileHeader prints the header for a pfs file.
   324  func PrintFileHeader(w io.Writer) {
   325  	fmt.Fprintf(w, "  REPO\tCOMMIT\tPATH\t\n")
   326  }
   327  
   328  // PrintFile values for a pfs file.
   329  func PrintFile(w io.Writer, file *pfsclient.File) {
   330  	fmt.Fprintf(w, "  %s\t%s\t%s\t\n", file.Commit.Repo.Name, file.Commit.ID, file.Path)
   331  }
   332  
   333  func datumState(datumState ppsclient.DatumState) string {
   334  	switch datumState {
   335  	case ppsclient.DatumState_SKIPPED:
   336  		return color.New(color.FgYellow).SprintFunc()("skipped")
   337  	case ppsclient.DatumState_FAILED:
   338  		return color.New(color.FgRed).SprintFunc()("failed")
   339  	case ppsclient.DatumState_RECOVERED:
   340  		return color.New(color.FgYellow).SprintFunc()("recovered")
   341  	case ppsclient.DatumState_SUCCESS:
   342  		return color.New(color.FgGreen).SprintFunc()("success")
   343  	}
   344  	return "-"
   345  }
   346  
   347  // JobState returns the state of a job as a pretty printed string.
   348  func JobState(jobState ppsclient.JobState) string {
   349  	switch jobState {
   350  	case ppsclient.JobState_JOB_STARTING:
   351  		return color.New(color.FgYellow).SprintFunc()("starting")
   352  	case ppsclient.JobState_JOB_RUNNING:
   353  		return color.New(color.FgYellow).SprintFunc()("running")
   354  	case ppsclient.JobState_JOB_MERGING:
   355  		return color.New(color.FgYellow).SprintFunc()("merging")
   356  	case ppsclient.JobState_JOB_FAILURE:
   357  		return color.New(color.FgRed).SprintFunc()("failure")
   358  	case ppsclient.JobState_JOB_SUCCESS:
   359  		return color.New(color.FgGreen).SprintFunc()("success")
   360  	case ppsclient.JobState_JOB_KILLED:
   361  		return color.New(color.FgRed).SprintFunc()("killed")
   362  	case ppsclient.JobState_JOB_EGRESSING:
   363  		return color.New(color.FgYellow).SprintFunc()("egressing")
   364  
   365  	}
   366  	return "-"
   367  }
   368  
   369  // Progress pretty prints the datum progress of a job.
   370  func Progress(ji *ppsclient.JobInfo) string {
   371  	if ji.DataRecovered != 0 {
   372  		return fmt.Sprintf("%d + %d + %d / %d", ji.DataProcessed, ji.DataSkipped, ji.DataRecovered, ji.DataTotal)
   373  	}
   374  	return fmt.Sprintf("%d + %d / %d", ji.DataProcessed, ji.DataSkipped, ji.DataTotal)
   375  }
   376  
   377  func pipelineState(pipelineState ppsclient.PipelineState) string {
   378  	switch pipelineState {
   379  	case ppsclient.PipelineState_PIPELINE_STARTING:
   380  		return color.New(color.FgYellow).SprintFunc()("starting")
   381  	case ppsclient.PipelineState_PIPELINE_RUNNING:
   382  		return color.New(color.FgGreen).SprintFunc()("running")
   383  	case ppsclient.PipelineState_PIPELINE_RESTARTING:
   384  		return color.New(color.FgYellow).SprintFunc()("restarting")
   385  	case ppsclient.PipelineState_PIPELINE_FAILURE:
   386  		return color.New(color.FgRed).SprintFunc()("failure")
   387  	case ppsclient.PipelineState_PIPELINE_PAUSED:
   388  		return color.New(color.FgYellow).SprintFunc()("paused")
   389  	case ppsclient.PipelineState_PIPELINE_STANDBY:
   390  		return color.New(color.FgYellow).SprintFunc()("standby")
   391  	case ppsclient.PipelineState_PIPELINE_CRASHING:
   392  		return color.New(color.FgRed).SprintFunc()("crashing")
   393  	}
   394  	return "-"
   395  }
   396  
   397  func jobInput(jobInfo PrintableJobInfo) string {
   398  	if jobInfo.Input == nil {
   399  		return ""
   400  	}
   401  	input, err := json.MarshalIndent(jobInfo.Input, "", "  ")
   402  	if err != nil {
   403  		panic(errors.Wrapf(err, "error marshalling input"))
   404  	}
   405  	return string(input) + "\n"
   406  }
   407  
   408  func workerStatus(jobInfo PrintableJobInfo) string {
   409  	var buffer bytes.Buffer
   410  	writer := ansiterm.NewTabWriter(&buffer, 20, 1, 3, ' ', 0)
   411  	PrintWorkerStatusHeader(writer)
   412  	for _, workerStatus := range jobInfo.WorkerStatus {
   413  		PrintWorkerStatus(writer, workerStatus, jobInfo.FullTimestamps)
   414  	}
   415  	// can't error because buffer can't error on Write
   416  	writer.Flush()
   417  	return buffer.String()
   418  }
   419  
   420  func pipelineInput(pipelineInfo *ppsclient.PipelineInfo) string {
   421  	if pipelineInfo.Input == nil {
   422  		return ""
   423  	}
   424  	input, err := json.MarshalIndent(pipelineInfo.Input, "", "  ")
   425  	if err != nil {
   426  		panic(errors.Wrapf(err, "error marshalling input"))
   427  	}
   428  	return string(input) + "\n"
   429  }
   430  
   431  func jobCounts(counts map[int32]int32) string {
   432  	var buffer bytes.Buffer
   433  	for i := int32(ppsclient.JobState_JOB_STARTING); i <= int32(ppsclient.JobState_JOB_SUCCESS); i++ {
   434  		fmt.Fprintf(&buffer, "%s: %d\t", JobState(ppsclient.JobState(i)), counts[i])
   435  	}
   436  	return buffer.String()
   437  }
   438  
   439  func prettyTransform(transform *ppsclient.Transform) (string, error) {
   440  	result, err := json.MarshalIndent(transform, "", "  ")
   441  	if err != nil {
   442  		return "", err
   443  	}
   444  	return pretty.UnescapeHTML(string(result)), nil
   445  }
   446  
   447  // ShorthandInput renders a pps.Input as a short, readable string
   448  func ShorthandInput(input *ppsclient.Input) string {
   449  	switch {
   450  	case input == nil:
   451  		return "none"
   452  	case input.Pfs != nil:
   453  		return fmt.Sprintf("%s:%s", input.Pfs.Repo, input.Pfs.Glob)
   454  	case input.Cross != nil:
   455  		var subInput []string
   456  		for _, input := range input.Cross {
   457  			subInput = append(subInput, ShorthandInput(input))
   458  		}
   459  		return "(" + strings.Join(subInput, " ⨯ ") + ")"
   460  	case input.Join != nil:
   461  		var subInput []string
   462  		for _, input := range input.Join {
   463  			subInput = append(subInput, ShorthandInput(input))
   464  		}
   465  		return "(" + strings.Join(subInput, " ⋈ ") + ")"
   466  	case input.Group != nil:
   467  		var subInput []string
   468  		for _, input := range input.Group {
   469  			subInput = append(subInput, ShorthandInput(input))
   470  		}
   471  		return "(Group: " + strings.Join(subInput, ", ") + ")"
   472  	case input.Union != nil:
   473  		var subInput []string
   474  		for _, input := range input.Union {
   475  			subInput = append(subInput, ShorthandInput(input))
   476  		}
   477  		return "(" + strings.Join(subInput, " ∪ ") + ")"
   478  	case input.Cron != nil:
   479  		return fmt.Sprintf("%s:%s", input.Cron.Name, input.Cron.Spec)
   480  	}
   481  	return ""
   482  }
   483  
   484  var funcMap = template.FuncMap{
   485  	"pipelineState":        pipelineState,
   486  	"jobState":             JobState,
   487  	"datumState":           datumState,
   488  	"workerStatus":         workerStatus,
   489  	"pipelineInput":        pipelineInput,
   490  	"jobInput":             jobInput,
   491  	"prettyAgo":            pretty.Ago,
   492  	"prettyTimeDifference": pretty.TimeDifference,
   493  	"prettyDuration":       pretty.Duration,
   494  	"prettySize":           pretty.Size,
   495  	"jobCounts":            jobCounts,
   496  	"prettyTransform":      prettyTransform,
   497  }