Refactor the structure of the publish (upload) service

Refactor the structure to align with the existing provision service (https://source.corp.google.com/chromeos_public/src/platform/dev/src/chromiumos/test/provision/cmd/).

BUG=b:204473963
TEST=Manually run unit tests

Change-Id: I86851f7cdd889a1a8f10337221055d6f0ae86b46
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/3274095
Commit-Queue: Zhihui Xie <zhihuixie@chromium.org>
Tested-by: Zhihui Xie <zhihuixie@chromium.org>
Auto-Submit: Zhihui Xie <zhihuixie@chromium.org>
Reviewed-by: Otabek Kasimov <otabek@google.com>
Reviewed-by: Jaques Clapauch <jaquesc@google.com>
diff --git a/src/chromiumos/test/publish/cmd/cros-publish/main.go b/src/chromiumos/test/publish/cmd/cros-publish/main.go
new file mode 100644
index 0000000..e5b96f7
--- /dev/null
+++ b/src/chromiumos/test/publish/cmd/cros-publish/main.go
@@ -0,0 +1,179 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package main implements the cros-publish used to upload artifacts to GCS
+// bucket.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"chromiumos/test/publish/cmd/publishserver"
+	"go.chromium.org/luci/common/errors"
+)
+
+const (
+	// version is the version info of this command. It is filled in during emerge.
+	version         = "<unknown>"
+	helpDescription = `cros-publish tool
+
+The tool allows to upload test result artifacts to GCS buckets for testing
+needs. Please read go/cros-upload-to-gs-tko-design-proposal for mode details.
+
+Commands:
+  cli       Running publish server in CLI mode result will be printed to output
+						file.
+            usage: cros-publish cli -service_account_creds service_account_file
+
+  server    Starting server and allow work with server by RPC calls. Mostly
+						used for tests.
+            usage: cros-publish server -service_account_creds service_account_file [-port 80]
+
+  -version  Print version of lib.
+  -help     Print this help.`
+	defaultLogDirectory = "/tmp/publish/"
+	defaultPort         = 80
+)
+
+// createLogFile creates a file and its parent directory for logging purpose.
+func createLogFile() (*os.File, error) {
+	t := time.Now()
+	fullPath := filepath.Join(defaultLogDirectory, t.Format("20060102-150405"))
+	if err := os.MkdirAll(fullPath, 0755); err != nil {
+		return nil, fmt.Errorf("failed to create directory %v: %v", fullPath, err)
+	}
+
+	logFullPathName := filepath.Join(fullPath, "log.txt")
+
+	// Log the full output of the command to disk.
+	logFile, err := os.Create(logFullPathName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create file %v: %v", fullPath, err)
+	}
+	return logFile, nil
+}
+
+// newLogger creates a logger. Using go default logger for now.
+func newLogger(logFile *os.File) *log.Logger {
+	mw := io.MultiWriter(logFile, os.Stderr)
+	return log.New(mw, "", log.LstdFlags|log.LUTC)
+}
+
+type args struct {
+	// Common input params.
+	servviceAccountCreds string
+
+	// Server mode params
+	port int
+}
+
+func validate(a args) error {
+	if a.servviceAccountCreds == "" {
+		return errors.Reason("Service account file not specified").Err()
+	}
+
+	_, err := os.Open(a.servviceAccountCreds)
+	if err != nil {
+		return errors.Reason("Failed to read service account file").Err()
+	}
+	return nil
+}
+
+func startServer(d []string) int {
+	a := args{}
+	fs := flag.NewFlagSet("Start publish publishService", flag.ExitOnError)
+	fs.StringVar(&a.servviceAccountCreds, "service_account_creds", "", "path to service account file containing gcp credentials")
+	fs.IntVar(&a.port, "port", defaultPort, fmt.Sprintf("Specify the port for the publishService. Default value %d.", defaultPort))
+	fs.Parse(d)
+
+	logFile, err := createLogFile()
+	if err != nil {
+		log.Fatalln("Failed to create log file", err)
+		return 2
+	}
+	defer logFile.Close()
+
+	logger := newLogger(logFile)
+	err = validate(a)
+	if err != nil {
+		log.Fatalf("Validate arguments fail: %s", err)
+		return 2
+	}
+
+	publishService, destructor, err := publishserver.NewPublishService(logger, "")
+	defer destructor()
+	if err != nil {
+		logger.Fatalln("Failed to create publish: ", err)
+		return 2
+	}
+
+	err = publishService.StartServer(a.port)
+	if err != nil {
+		logger.Fatalln("Failed to perform publish: ", err)
+		return 1
+	}
+	return 0
+}
+
+// Specify run mode for CLI.
+type runMode string
+
+const (
+	runCli     runMode = "cli"
+	runServer  runMode = "server"
+	runVersion runMode = "version"
+	runHelp    runMode = "help"
+)
+
+func getRunMode() (runMode, error) {
+	if len(os.Args) > 1 {
+		for _, a := range os.Args {
+			if a == "-version" {
+				return runVersion, nil
+			}
+		}
+		switch strings.ToLower(os.Args[1]) {
+		case "cli":
+			return runCli, nil
+		case "server":
+			return runServer, nil
+		}
+	}
+	// If we did not find special run mode then just print help for user.
+	return runHelp, nil
+}
+
+func mainInternal() int {
+	rm, err := getRunMode()
+	if err != nil {
+		log.Fatalln(err)
+		return 2
+	}
+
+	switch rm {
+	case runCli:
+		log.Fatalln("CLI mode is not implemented yet!")
+		return 2
+	case runServer:
+		log.Printf("Running server mode!")
+		return startServer(os.Args[2:])
+	case runVersion:
+		log.Printf("cros-publish version: %s", version)
+		return 0
+	}
+
+	log.Printf(helpDescription)
+	return 0
+}
+
+func main() {
+	os.Exit(mainInternal())
+}
diff --git a/src/chromiumos/test/publish/cmd/publishserver/main.go b/src/chromiumos/test/publish/cmd/publishserver/main.go
deleted file mode 100644
index 2e2a1b5..0000000
--- a/src/chromiumos/test/publish/cmd/publishserver/main.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2021 The Chromium OS Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"io"
-	"log"
-	"net"
-	"os"
-	"path/filepath"
-	"time"
-)
-
-// Version is the version info of this command. It is filled in during emerge.
-var Version = "<unknown>"
-
-// createLogFile creates a file and its parent directory for logging purpose.
-func createLogFile() (*os.File, error) {
-	t := time.Now()
-	fullPath := filepath.Join("/tmp/publishserver/", t.Format("20060102-150405"))
-	if err := os.MkdirAll(fullPath, 0755); err != nil {
-		return nil, fmt.Errorf("failed to create directory %v: %v", fullPath, err)
-	}
-
-	logFullPathName := filepath.Join(fullPath, "log.txt")
-
-	// Log the full output of the command to disk.
-	logFile, err := os.Create(logFullPathName)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create file %v: %v", fullPath, err)
-	}
-	return logFile, nil
-}
-
-// newLogger creates a logger. Using go default logger for now.
-func newLogger(logFile *os.File) *log.Logger {
-	mw := io.MultiWriter(logFile, os.Stderr)
-	return log.New(mw, "", log.LstdFlags|log.LUTC)
-}
-
-func main() {
-	os.Exit(func() int {
-		flag.NewFlagSet("version", flag.ExitOnError)
-		gcpCredentials := flag.String("gcp-creds", "", "path to file containing gcp credentials")
-		flag.Parse()
-
-		if os.Args[1] == "version" {
-			fmt.Println("publishservice version ", Version)
-			return 0
-		}
-
-		if *gcpCredentials == "" {
-			fmt.Println("gcp-creds must be defined.")
-			return 1
-		}
-
-		logFile, err := createLogFile()
-		if err != nil {
-			log.Fatalln("Failed to create log file: ", err)
-		}
-		defer logFile.Close()
-
-		logger := newLogger(logFile)
-		logger.Println("Starting publishservice version ", Version)
-		l, err := net.Listen("tcp", ":0")
-		if err != nil {
-			logger.Fatalln("Failed to create a net listener: ", err)
-			return 2
-		}
-
-		server, destructor, err := newPublishServiceServer(l, logger, *gcpCredentials)
-		if err != nil {
-			logger.Fatalln("Failed to create server: ", err)
-			return 2
-		}
-		defer destructor()
-
-		err = server.Serve(l)
-		if err != nil {
-			logger.Fatalln("Failed to initialize server: ", err)
-		}
-		return 0
-	}())
-}
diff --git a/src/chromiumos/test/publish/cmd/publishserver/mock_storage/storagemock.go b/src/chromiumos/test/publish/cmd/publishserver/mock_storage/storagemock.go
index e443a04..d0d50e8 100644
--- a/src/chromiumos/test/publish/cmd/publishserver/mock_storage/storagemock.go
+++ b/src/chromiumos/test/publish/cmd/publishserver/mock_storage/storagemock.go
@@ -8,10 +8,11 @@
 package mock_storage
 
 import (
-	storage "chromiumos/test/publish/cmd/publishserver/storage"
 	context "context"
 	reflect "reflect"
 
+	storage "chromiumos/test/publish/cmd/publishserver/storage"
+
 	gomock "github.com/golang/mock/gomock"
 )
 
diff --git a/src/chromiumos/test/publish/cmd/publishserver/publishserver.go b/src/chromiumos/test/publish/cmd/publishserver/publishserver.go
index 181eead..36a7919 100644
--- a/src/chromiumos/test/publish/cmd/publishserver/publishserver.go
+++ b/src/chromiumos/test/publish/cmd/publishserver/publishserver.go
@@ -3,53 +3,49 @@
 // found in the LICENSE file.
 
 // Implements publish_service.proto (see proto for details)
-package main
+package publishserver
 
 import (
-	"chromiumos/lro"
-	"chromiumos/test/publish/cmd/publishserver/storage"
 	"context"
 	"log"
-	"net"
+
+	"chromiumos/lro"
+	"chromiumos/test/publish/cmd/publishserver/storage"
 
 	"go.chromium.org/chromiumos/config/go/longrunning"
 	"go.chromium.org/chromiumos/config/go/test/api"
-	"google.golang.org/grpc"
 )
 
-// PublishServiceServer implementation of publish_service.proto
-type PublishServiceServer struct {
+// PublishService implementation of publish_service.proto
+type PublishService struct {
 	manager  *lro.Manager
 	logger   *log.Logger
 	gsClient storage.GSClientInterface
 }
 
-// newPublishServiceServer creates a new publish service server to listen to rpc requests.
-func newPublishServiceServer(l net.Listener, logger *log.Logger, gcpCredentials string) (*grpc.Server, func(), error) {
+// NewPublishService creates a new publish service with the GCP storage client.
+func NewPublishService(logger *log.Logger, gcpCredentials string) (*PublishService, func(), error) {
 	gsClient, err := storage.NewGSClient(context.Background(), gcpCredentials)
 	if err != nil {
 		return nil, nil, err
 	}
-	s := &PublishServiceServer{
+	publishService := &PublishService{
 		manager:  lro.New(),
 		logger:   logger,
 		gsClient: gsClient,
 	}
 
-	server := grpc.NewServer()
 	destructor := func() {
-		s.manager.Close()
-		s.gsClient.Close()
+		publishService.manager.Close()
+		publishService.gsClient.Close()
 	}
 
-	api.RegisterPublishServiceServer(server, s)
-	logger.Println("publishservice listen to request at ", l.Addr().String())
-	return server, destructor, nil
+	return publishService, destructor, nil
 }
 
 // UploadToGS uploads the designated folder to the provided Google Cloud Storage
 // bucket/object
-func (s *PublishServiceServer) UploadToGS(ctx context.Context, req *api.UploadToGSRequest) (*longrunning.Operation, error) {
+func (s *PublishService) UploadToGS(ctx context.Context, req *api.UploadToGSRequest) (*longrunning.Operation, error) {
 	s.logger.Println("Received api.UploadToGSRequest: ", *req)
 	op := s.manager.NewOperation()
 	if err := s.gsClient.Upload(ctx, req.LocalDirectory, req.GsDirectory); err != nil {
diff --git a/src/chromiumos/test/publish/cmd/publishserver/publishserver_server.go b/src/chromiumos/test/publish/cmd/publishserver/publishserver_server.go
new file mode 100644
index 0000000..aed7b5d
--- /dev/null
+++ b/src/chromiumos/test/publish/cmd/publishserver/publishserver_server.go
@@ -0,0 +1,35 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package publishserver
+
+import (
+	"fmt"
+	"net"
+
+	"go.chromium.org/chromiumos/config/go/longrunning"
+	"go.chromium.org/chromiumos/config/go/test/api"
+	"go.chromium.org/luci/common/errors"
+	"google.golang.org/grpc"
+
+	"chromiumos/lro"
+)
+
+// StartServer starts publish server on requested port
+func (s *PublishService) StartServer(port int) error {
+	l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
+	if err != nil {
+		return errors.Annotate(err, "Start publish server: failed to create listener at %d", port).Err()
+	}
+
+	s.manager = lro.New()
+	defer s.manager.Close()
+	server := grpc.NewServer()
+
+	api.RegisterPublishServiceServer(server, s)
+	longrunning.RegisterOperationsServer(server, s.manager)
+
+	s.logger.Println("Publish server is listening to request at ", l.Addr().String())
+	return server.Serve(l)
+}
diff --git a/src/chromiumos/test/publish/cmd/publishserver/tests/publishserver_test.go b/src/chromiumos/test/publish/cmd/publishserver/tests/publishserver_test.go
index f29062c..ec4a41a 100644
--- a/src/chromiumos/test/publish/cmd/publishserver/tests/publishserver_test.go
+++ b/src/chromiumos/test/publish/cmd/publishserver/tests/publishserver_test.go
@@ -5,8 +5,6 @@
 package tests
 
 import (
-	"chromiumos/test/publish/cmd/publishserver/mock_storage"
-	"chromiumos/test/publish/cmd/publishserver/storage"
 	"context"
 	"fmt"
 	"io/ioutil"
@@ -14,6 +12,9 @@
 	"path"
 	"testing"
 
+	"chromiumos/test/publish/cmd/publishserver/mock_storage"
+	"chromiumos/test/publish/cmd/publishserver/storage"
+
 	"github.com/golang/mock/gomock"
 )