[infra][ftx] Actually launch swarming task on the launch task step.

Bug: b/258456267
Change-Id: If23f200ca71ac03c7b25f065a5af797689fd1feb
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/864317
Commit-Queue: Vinicius Felizardo <felizardo@google.com>
Reviewed-by: Rahul Bangar <rahulbn@google.com>
diff --git a/cmd/ftxtest/common.go b/cmd/ftxtest/common.go
new file mode 100644
index 0000000..77c3e59
--- /dev/null
+++ b/cmd/ftxtest/common.go
@@ -0,0 +1,34 @@
+// Copyright 2023 The Fuchsia 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 (
+	"github.com/maruel/subcommands"
+
+	"go.chromium.org/luci/auth"
+	"go.chromium.org/luci/auth/client/authcli"
+)
+
+type commonFlags struct {
+	subcommands.CommandRunBase
+	authFlags authcli.Flags
+	project   string
+
+	parsedAuthOpts auth.Options
+}
+
+func (c *commonFlags) Init(authOpts auth.Options) {
+	c.authFlags = authcli.Flags{}
+	c.authFlags.Register(&c.Flags, authOpts)
+}
+
+func (c *commonFlags) Parse() error {
+	var err error
+	c.parsedAuthOpts, err = c.authFlags.Options()
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/cmd/ftxtest/main.go b/cmd/ftxtest/main.go
index 15e5688..bd19bd0 100644
--- a/cmd/ftxtest/main.go
+++ b/cmd/ftxtest/main.go
@@ -1,43 +1,44 @@
-// Copyright 2023 The Fuchsia Authors. All rights reserved
-// Use of this source code is governed by a BSD-style license that can
+// Copyright 2023 The Fuchsia 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 (
-	"context"
-	"fmt"
+	"log"
+	"os"
 
-	"github.com/golang/protobuf/ptypes/any"
-	"go.chromium.org/luci/luciexe/build"
-	ftxproto "go.fuchsia.dev/infra/cmd/ftxtest/proto"
+	"github.com/maruel/subcommands"
+	"go.chromium.org/luci/auth"
+	"go.chromium.org/luci/auth/client/authcli"
+	"go.chromium.org/luci/client/versioncli"
+	"go.chromium.org/luci/hardcoded/chromeinfra"
 )
 
-func main() {
-	input := &ftxproto.InputProperties{}
-	var writeOutputProps func(*any.Any)
-	build.Main(input, &writeOutputProps, nil, func(ctx context.Context, extraArgs []string, state *build.State) error {
-		_, _, err := LaunchTaskStep(ctx)
-		if err != nil {
-			return fmt.Errorf("LaunchTaskStep: %v", err)
-		}
-		return nil
-	})
+const (
+	// Version must be updated on functional change (behavior, arguments, supported commands).
+	version = "0.0.1"
+)
+
+func getApplication(defaultAuthOpts auth.Options) *subcommands.DefaultApplication {
+	defaultAuthOpts.Scopes = []string{
+		"https://www.googleapis.com/auth/userinfo.email",
+	}
+	return &subcommands.DefaultApplication{
+		Name:  "ftx-test",
+		Title: "Generic test execution for buildbucket.",
+		Commands: []*subcommands.Command{
+			cmdRun(defaultAuthOpts),
+			authcli.SubcommandInfo(defaultAuthOpts, "whoami", false),
+			authcli.SubcommandLogin(defaultAuthOpts, "login", false),
+			authcli.SubcommandLogout(defaultAuthOpts, "logout", false),
+			versioncli.CmdVersion(version),
+			subcommands.CmdHelp,
+		},
+	}
 }
 
-func LaunchTaskStep(ctx context.Context) (*Swarming, string, error) {
-	step, ctx := build.StartStep(ctx, "Launch Swarming Task")
-	swarming, err := NewSwarming(ctx)
-	if err != nil {
-		step.End(err)
-		return nil, "", fmt.Errorf("NewSwarming: %v", err)
-	}
-	taskId, err := swarming.LaunchTask()
-	if err != nil {
-		step.End(err)
-		return nil, "", fmt.Errorf("LaunchTask: %v", err)
-	}
-	md := fmt.Sprintf("* [swarming task](https://chrome-swarming.appspot.com/task?id=%s)", taskId)
-	step.SetSummaryMarkdown(md)
-	step.End(nil)
-	return swarming, taskId, nil
+func main() {
+	log.SetFlags(log.Lmicroseconds)
+	app := getApplication(chromeinfra.DefaultAuthOptions())
+	os.Exit(subcommands.Run(app, nil))
 }
diff --git a/cmd/ftxtest/proto/input.pb.go b/cmd/ftxtest/proto/input.pb.go
index b0017d2..816d45e 100644
--- a/cmd/ftxtest/proto/input.pb.go
+++ b/cmd/ftxtest/proto/input.pb.go
@@ -25,6 +25,10 @@
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
+	// Name of the test being run.
+	Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
+	// Whether this test should run in the Google internal or external infrastructure.
+	External bool `protobuf:"varint,7,opt,name=external,proto3" json:"external,omitempty"`
 	// CAS digest containing all of the inputs needed to both prepare targets
 	// and run tests.
 	InputArtifactsDigest string `protobuf:"bytes,1,opt,name=input_artifacts_digest,json=inputArtifactsDigest,proto3" json:"input_artifacts_digest,omitempty"`
@@ -71,6 +75,20 @@
 	return file_proto_input_proto_rawDescGZIP(), []int{0}
 }
 
+func (x *InputProperties) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *InputProperties) GetExternal() bool {
+	if x != nil {
+		return x.External
+	}
+	return false
+}
+
 func (x *InputProperties) GetInputArtifactsDigest() string {
 	if x != nil {
 		return x.InputArtifactsDigest
@@ -244,8 +262,11 @@
 
 var file_proto_input_proto_rawDesc = []byte{
 	0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x22, 0xf8, 0x02, 0x0a, 0x0f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x50, 0x72, 0x6f,
-	0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x69, 0x6e, 0x70, 0x75, 0x74,
+	0x6f, 0x74, 0x6f, 0x22, 0xa8, 0x03, 0x0a, 0x0f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x50, 0x72, 0x6f,
+	0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
+	0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x65,
+	0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65,
+	0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x16, 0x69, 0x6e, 0x70, 0x75, 0x74,
 	0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73,
 	0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x72,
 	0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a,
diff --git a/cmd/ftxtest/proto/input.proto b/cmd/ftxtest/proto/input.proto
index 9116bfd..076ccf8 100644
--- a/cmd/ftxtest/proto/input.proto
+++ b/cmd/ftxtest/proto/input.proto
@@ -3,6 +3,10 @@
 option go_package = "go.fuchsia.dev/infra/cmd/ftxtest/proto";
 
 message InputProperties {
+        // Name of the test being run.
+        string name = 6;
+        // Whether this test should run in the Google internal or external infrastructure.
+        bool external = 7;
         // CAS digest containing all of the inputs needed to both prepare targets
         // and run tests.
         string input_artifacts_digest = 1;
diff --git a/cmd/ftxtest/run.go b/cmd/ftxtest/run.go
new file mode 100644
index 0000000..c0a8589
--- /dev/null
+++ b/cmd/ftxtest/run.go
@@ -0,0 +1,101 @@
+// Copyright 2023 The Fuchsia 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 (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/golang/protobuf/ptypes/any"
+	"github.com/maruel/subcommands"
+	"go.chromium.org/luci/auth"
+	"go.chromium.org/luci/luciexe/build"
+	ftxproto "go.fuchsia.dev/infra/cmd/ftxtest/proto"
+)
+
+func cmdRun(authOpts auth.Options) *subcommands.Command {
+	return &subcommands.Command{
+		UsageLine: "run",
+		ShortDesc: "runs test",
+		LongDesc:  "Runs test based on luciexe protocol.",
+		CommandRun: func() subcommands.CommandRun {
+			r := &runImpl{}
+			r.Init(authOpts)
+			return r
+		},
+	}
+}
+
+type runImpl struct {
+	commonFlags
+
+	subcommands.CommandRunBase
+}
+
+func (r *runImpl) Init(defaultAuthOpts auth.Options) {
+	r.commonFlags.Init(defaultAuthOpts)
+}
+
+func (r *runImpl) Run(a subcommands.Application, args []string, env subcommands.Env) int {
+	if err := r.commonFlags.Parse(); err != nil {
+		fmt.Fprintf(os.Stderr, "Error parsing common flags: %v\n", err)
+		return 1
+	}
+	r.luciexeInit()
+	// luciexe uses os.exit to exit.
+	return 0
+}
+
+func (r *runImpl) luciexeInit() {
+	buildInput := &ftxproto.InputProperties{}
+	var writeOutputProps func(*any.Any)
+	build.Main(buildInput, &writeOutputProps, nil, func(ctx context.Context, extraArgs []string, state *build.State) error {
+		if len(buildInput.Name) == 0 {
+			return errors.New("Name is required.")
+		}
+		authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, r.parsedAuthOpts)
+		httpClient, err := authenticator.Client()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "You need to login first by running:\n")
+			fmt.Fprintf(os.Stderr, "  luci-auth login -scopes %q\n", strings.Join(r.parsedAuthOpts.Scopes, " "))
+			return errors.New("Not logged in.")
+		}
+		_, _, err = LaunchTaskStep(ctx, httpClient, buildInput)
+		if err != nil {
+			return fmt.Errorf("LaunchTaskStep: %v", err)
+		}
+		return nil
+	})
+}
+
+func LaunchTaskStep(ctx context.Context, httpClient *http.Client, buildInput *ftxproto.InputProperties) (*Swarming, string, error) {
+	step, ctx := build.StartStep(ctx, "Launch Swarming Task")
+	instance := instance(buildInput)
+	swarming, err := NewSwarming(ctx, httpClient, instance)
+	if err != nil {
+		step.End(err)
+		return nil, "", fmt.Errorf("NewSwarming: %v", err)
+	}
+	task, err := swarming.LaunchTask(buildInput)
+	if err != nil {
+		step.End(err)
+		return nil, "", fmt.Errorf("LaunchTask: %v", err)
+	}
+	md := fmt.Sprintf("* [swarming task](https://%s.appspot.com/task?id=%s)", instance, task.TaskId)
+	step.SetSummaryMarkdown(md)
+	step.End(nil)
+	return swarming, task.TaskId, nil
+}
+
+func instance(buildInput *ftxproto.InputProperties) string {
+	if buildInput.External {
+		return "chromium-swarm"
+	} else {
+		return "chrome-swarming"
+	}
+}
diff --git a/cmd/ftxtest/run.sh b/cmd/ftxtest/run.sh
index 82befe3..484743f 100755
--- a/cmd/ftxtest/run.sh
+++ b/cmd/ftxtest/run.sh
@@ -3,4 +3,4 @@
 set -x
 
 ./gen.sh
-LUCIEXE_FAKEBUILD=test/build.json go run -mod=vendor *.go -- -working-dir=$PWD
+LUCIEXE_FAKEBUILD=test/build.json go run -mod=vendor *.go run
diff --git a/cmd/ftxtest/swarming.go b/cmd/ftxtest/swarming.go
index bfcdf60..0f4f61c 100644
--- a/cmd/ftxtest/swarming.go
+++ b/cmd/ftxtest/swarming.go
@@ -6,32 +6,88 @@
 import (
 	"context"
 	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
 
-	"go.chromium.org/luci/auth"
 	"go.chromium.org/luci/common/api/swarming/swarming/v1"
+	ftxproto "go.fuchsia.dev/infra/cmd/ftxtest/proto"
 )
 
 type Swarming struct {
-	client *swarming.Service
+	instance string
+	service  *swarming.Service
 }
 
-func NewSwarming(ctx context.Context) (*Swarming, error) {
-	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, auth.Options{})
-	httpClient, err := authenticator.Client()
-	if err != nil {
-		return nil, fmt.Errorf("authenticator.Client: %v", err)
-	}
-	swarmingClient, err := swarming.New(httpClient)
+const (
+	taskPriority         = 200
+	taskExpiration       = 5 * time.Hour
+	taskExecutionTimeout = 2 * time.Hour
+)
+
+func NewSwarming(ctx context.Context, httpClient *http.Client, instance string) (*Swarming, error) {
+	swarmingService, err := swarming.New(httpClient)
+	swarmingService.BasePath = fmt.Sprintf("https://%s.appspot.com/_ah/api/swarming/v1/", instance)
 	if err != nil {
 		return nil, fmt.Errorf("swarming.New: %v", err)
 	}
 	swarming := &Swarming{
-		client: swarmingClient,
+		service:  swarmingService,
+		instance: instance,
 	}
 	return swarming, nil
 }
 
-func (s *Swarming) LaunchTask() (string, error) {
-	// TODO(b/258456267): Launch swarming task...
-	return "foo", nil
+func (s *Swarming) LaunchTask(buildInput *ftxproto.InputProperties) (*swarming.SwarmingRpcsTaskRequestMetadata, error) {
+	casInput, err := s.casInput(buildInput)
+	if err != nil {
+		return nil, fmt.Errorf("casInput: %v", err)
+	}
+	return s.service.Tasks.New(&swarming.SwarmingRpcsNewTaskRequest{
+		Name:           buildInput.Name,
+		ExpirationSecs: int64(taskExpiration.Seconds()),
+		Priority:       taskPriority,
+		Realm:          realm(buildInput),
+		Properties: &swarming.SwarmingRpcsTaskProperties{
+			ExecutionTimeoutSecs: int64(taskExpiration.Seconds()),
+			Command:              []string{buildInput.TestCommand},
+			CasInputRoot:         casInput,
+			Dimensions:           dimensions(buildInput),
+		},
+	}).Do()
+}
+
+func (s *Swarming) casInput(buildInput *ftxproto.InputProperties) (*swarming.SwarmingRpcsCASReference, error) {
+	digestSplit := strings.Split(buildInput.InputArtifactsDigest, "/")
+	sizeBytes, err := strconv.ParseInt(digestSplit[1], 10, 64)
+	if err != nil {
+		return nil, fmt.Errorf("sizeBytes: %v", err)
+	}
+	return &swarming.SwarmingRpcsCASReference{
+		CasInstance: fmt.Sprintf("projects/%s/instances/default_instance", s.instance),
+		Digest: &swarming.SwarmingRpcsDigest{
+			Hash:      digestSplit[0],
+			SizeBytes: sizeBytes,
+		},
+	}, nil
+}
+
+func dimensions(buildInput *ftxproto.InputProperties) []*swarming.SwarmingRpcsStringPair {
+	result := []*swarming.SwarmingRpcsStringPair{}
+	for key, value := range buildInput.TargetDimensions {
+		result = append(result, &swarming.SwarmingRpcsStringPair{
+			Key:   key,
+			Value: value,
+		})
+	}
+	return result
+}
+
+func realm(buildInput *ftxproto.InputProperties) string {
+	if buildInput.External {
+		return "fuchsia:try"
+	} else {
+		return "turquoise:global.try"
+	}
 }
diff --git a/cmd/ftxtest/test/build.json b/cmd/ftxtest/test/build.json
index 7eb02d4..a3dff20 100644
--- a/cmd/ftxtest/test/build.json
+++ b/cmd/ftxtest/test/build.json
@@ -1,11 +1,12 @@
 {
     "input": {
         "properties": {
-            "input_artifacts_digest": "0123456789abcdef",
-            "test_command": "some/command",
+            "name": "hello_swarming_test",
+            "external": false,
+            "input_artifacts_digest": "11ab3e1a0ba7bc9fbd6957c57222331f340dc981ec2bcb5edeffc05014e7a2ba/83",
+            "test_command": "turquoise/infra/foundation/go/ftxclient/examples/hello_swarming/hello_swarming.par",
             "target_dimensions": {
-                "d1": "v1",
-                "d2": "v2"
+                "pool": "fuchsia.dev.tests"
             }
         }
     }