cmd/worker: support command-line mode; add list-updates

If the worker command is given arguments, it acts like a
command-line tool instead of starting a server.

Define a list-updates command to list the commit update records
in the DB.

Change-Id: I9827cf3f8949409dae9e77ce429bafafc6e43129
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/368494
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
index 2e88dbf..0019fd7 100644
--- a/cmd/worker/main.go
+++ b/cmd/worker/main.go
@@ -3,14 +3,18 @@
 // license that can be found in the LICENSE file.
 
 // Command worker runs the vuln worker server.
+// It can also be used to perform actions from the command line
+// by providing a sub-command.
 package main
 
 import (
 	"context"
+	"errors"
 	"flag"
 	"fmt"
 	"net/http"
 	"os"
+	"text/tabwriter"
 
 	"cloud.google.com/go/errorreporting"
 	"golang.org/x/vuln/internal/derrors"
@@ -35,16 +39,26 @@
 	if *namespace == "" {
 		die("need -namespace or VULN_WORKER_NAMESPACE")
 	}
-	if os.Getenv("PORT") == "" && flag.NArg() == 0 {
-		die("need PORT or command-line args")
-	}
-
 	ctx := log.WithLineLogger(context.Background())
 
 	fstore, err := store.NewFireStore(ctx, *project, *namespace)
 	if err != nil {
 		die("firestore: %v", err)
 	}
+	if flag.NArg() > 0 {
+		err = runCommandLine(ctx, fstore)
+	} else {
+		err = runServer(ctx, fstore)
+	}
+	if err != nil {
+		die("%v", err)
+	}
+}
+
+func runServer(ctx context.Context, st store.Store) error {
+	if os.Getenv("PORT") == "" {
+		return errors.New("need PORT")
+	}
 
 	if *errorReporting {
 		reportingClient, err := errorreporting.NewClient(ctx, *project, errorreporting.Config{
@@ -54,18 +68,50 @@
 			},
 		})
 		if err != nil {
-			die("errorreporting: %v", err)
+			return err
 		}
 		derrors.SetReportingClient(reportingClient)
 	}
 
-	_, err = worker.NewServer(ctx, *namespace, fstore)
+	_, err := worker.NewServer(ctx, *namespace, st)
 	if err != nil {
-		die("NewServer: %v", err)
+		return err
 	}
 	addr := ":" + os.Getenv("PORT")
 	log.Infof(ctx, "Listening on addr %s", addr)
-	die("listening: %v", http.ListenAndServe(addr, nil))
+	return fmt.Errorf("listening: %v", http.ListenAndServe(addr, nil))
+}
+
+const timeFormat = "2006/01/02 15:04:05"
+
+func runCommandLine(ctx context.Context, st store.Store) error {
+	switch flag.Arg(0) {
+	case "list-updates":
+		return listUpdatesCommand(ctx, st)
+	default:
+		return fmt.Errorf("unknown command: %q", flag.Arg(1))
+	}
+}
+
+func listUpdatesCommand(ctx context.Context, st store.Store) error {
+	recs, err := st.ListCommitUpdateRecords(ctx)
+	if err != nil {
+		return err
+	}
+	tw := tabwriter.NewWriter(os.Stdout, 1, 8, 2, ' ', 0)
+	fmt.Fprintf(tw, "Start\tEnd\tCommit\tCVEs Processed\n")
+	for _, r := range recs {
+		endTime := "unfinished"
+		if !r.EndedAt.IsZero() {
+			endTime = r.EndedAt.Format(timeFormat)
+		}
+		fmt.Fprintf(tw, "%s\t%s\t%s\t%d/%d (added %d, modified %d)\n",
+			r.StartedAt.Format(timeFormat),
+			endTime,
+			r.CommitHash,
+			r.NumProcessed, r.NumTotal, r.NumAdded, r.NumModified)
+	}
+	return tw.Flush()
 }
 
 func die(format string, args ...interface{}) {