libs/skylab/request: add facility for specifing SchedulableLabels

BUG=974004
TEST=unit tests updated

Change-Id: I08727775982940a18f9ed6c7a9f9c4eb456e7bdc
Reviewed-on: https://chromium-review.googlesource.com/c/infra/infra/+/1659235
Reviewed-by: Prathmesh Prabhu <pprabhu@chromium.org>
Commit-Queue: Aviv Keshet <akeshet@chromium.org>
Cr-Commit-Position: refs/heads/master@{#23693}
diff --git a/go/src/infra/libs/skylab/request/request.go b/go/src/infra/libs/skylab/request/request.go
index 5f2037c..3f373b2 100644
--- a/go/src/infra/libs/skylab/request/request.go
+++ b/go/src/infra/libs/skylab/request/request.go
@@ -14,23 +14,41 @@
 	"go.chromium.org/luci/common/data/strpair"
 	"go.chromium.org/luci/common/errors"
 
+	"infra/libs/skylab/inventory"
+	swarming_inventory "infra/libs/skylab/inventory/swarming"
 	"infra/libs/skylab/worker"
 )
 
 // Args defines the set of arguments for creating a request.
 type Args struct {
-	Cmd                     worker.Command
-	Tags                    []string
+	// Cmd specifies the payload command to run for the request.
+	Cmd worker.Command
+	// Tags specifies swarming tags to apply to the request.
+	Tags []string
+	// ProvisionableDimensions specifies the provisionable dimensions in raw
+	// string form; e.g. {"provisionable-cros-version:foo-cq-R75-1.2.3.4"}
 	ProvisionableDimensions []string
-	Dimensions              []string
-	TimeoutMins             int
-	Priority                int64
-	ParentTaskID            string
+	// Dimensions specifies swarming dimensions in raw string form.
+	//
+	// It is preferable to specify dimensions via the SchedulableLabels
+	// argument. This argument should only be used for user-supplied freeform
+	// dimensions; e.g. {"label-power:battery"}
+	//
+	// TODO(akeshet): This feature is needed to support `skylab create-test`
+	// which allows arbitrary user-specified dimensions. If and when that
+	// feature is dropped, then this feature can be dropped as well.
+	Dimensions []string
+	// SchedulableLabels specifies schedulable label requirements that will
+	// be translated to dimensions.
+	SchedulableLabels inventory.SchedulableLabels
+	TimeoutMins       int
+	Priority          int64
+	ParentTaskID      string
 }
 
 // New creates a new swarming request for the given worker command and parameters.
 func New(args Args) (*swarming.SwarmingRpcsNewTaskRequest, error) {
-	slices, err := getSlices(args.Cmd, args.ProvisionableDimensions, args.Dimensions, args.TimeoutMins)
+	slices, err := getSlices(args.Cmd, args.ProvisionableDimensions, args.Dimensions, args.SchedulableLabels, args.TimeoutMins)
 	if err != nil {
 		return nil, errors.Annotate(err, "create request").Err()
 	}
@@ -46,14 +64,18 @@
 }
 
 // getSlices generates and returns the set of swarming task slices for the given test task.
-func getSlices(cmd worker.Command, provisionableDimensions []string, dimensions []string, timeoutMins int) ([]*swarming.SwarmingRpcsTaskSlice, error) {
+func getSlices(cmd worker.Command, provisionableDimensions []string, dimensions []string, inv inventory.SchedulableLabels, timeoutMins int) ([]*swarming.SwarmingRpcsTaskSlice, error) {
 	slices := make([]*swarming.SwarmingRpcsTaskSlice, 1, 2)
 
-	basePairs, err := toPairs(dimensions)
+	rawPairs, err := stringToPairs(dimensions)
 	if err != nil {
 		return nil, errors.Annotate(err, "create slices").Err()
 	}
-	provisionablePairs, err := toPairs(provisionableDimensions)
+
+	inventoryPairs := schedulableLabelsToPairs(inv)
+	basePairs := append(inventoryPairs, rawPairs...)
+
+	provisionablePairs, err := stringToPairs(provisionableDimensions)
 	if err != nil {
 		return nil, errors.Annotate(err, "create slices").Err()
 	}
@@ -105,9 +127,9 @@
 	return labels
 }
 
-// toPairs converts a slice of strings in foo:bar form to a slice of swarming
+// stringToPairs converts a slice of strings in foo:bar form to a slice of swarming
 // rpc string pairs.
-func toPairs(dimensions []string) ([]*swarming.SwarmingRpcsStringPair, error) {
+func stringToPairs(dimensions []string) ([]*swarming.SwarmingRpcsStringPair, error) {
 	pairs := make([]*swarming.SwarmingRpcsStringPair, len(dimensions))
 	for i, d := range dimensions {
 		k, v := strpair.Parse(d)
@@ -118,3 +140,14 @@
 	}
 	return pairs, nil
 }
+
+func schedulableLabelsToPairs(inv inventory.SchedulableLabels) []*swarming.SwarmingRpcsStringPair {
+	dimensions := swarming_inventory.Convert(&inv)
+	pairs := make([]*swarming.SwarmingRpcsStringPair, 0, len(dimensions))
+	for key, values := range dimensions {
+		for _, value := range values {
+			pairs = append(pairs, &swarming.SwarmingRpcsStringPair{Key: key, Value: value})
+		}
+	}
+	return pairs
+}
diff --git a/go/src/infra/libs/skylab/request/request_test.go b/go/src/infra/libs/skylab/request/request_test.go
index 5d259ff..87f9d5c 100644
--- a/go/src/infra/libs/skylab/request/request_test.go
+++ b/go/src/infra/libs/skylab/request/request_test.go
@@ -10,14 +10,17 @@
 
 	. "github.com/smartystreets/goconvey/convey"
 
+	"infra/libs/skylab/inventory"
 	"infra/libs/skylab/request"
 )
 
 func TestProvisionableDimensions(t *testing.T) {
-	Convey("Given request arguments that specify provisionable and regular dimenisons", t, func() {
+	Convey("Given request arguments that specify provisionable and regular dimenisons and inventory labels", t, func() {
+		model := "foo-model"
 		args := request.Args{
 			Dimensions:              []string{"k1:v1"},
 			ProvisionableDimensions: []string{"k2:v2", "k3:v3"},
+			SchedulableLabels:       inventory.SchedulableLabels{Model: &model},
 		}
 		Convey("when a request is formed", func() {
 			req, err := request.New(args)
@@ -30,22 +33,28 @@
 				// Second slice (fallback) requires only non-provisionable dimensions.
 				s0 := req.TaskSlices[0]
 				s1 := req.TaskSlices[1]
-				So(s0.Properties.Dimensions, ShouldHaveLength, 3)
-				So(s1.Properties.Dimensions, ShouldHaveLength, 1)
+				So(s0.Properties.Dimensions, ShouldHaveLength, 4)
+				So(s1.Properties.Dimensions, ShouldHaveLength, 2)
 
 				d00 := s0.Properties.Dimensions[0]
 				d01 := s0.Properties.Dimensions[1]
 				d02 := s0.Properties.Dimensions[2]
-				So(d00.Key, ShouldEqual, "k1")
-				So(d00.Value, ShouldEqual, "v1")
-				So(d01.Key, ShouldEqual, "k2")
-				So(d01.Value, ShouldEqual, "v2")
-				So(d02.Key, ShouldEqual, "k3")
-				So(d02.Value, ShouldEqual, "v3")
+				d03 := s0.Properties.Dimensions[3]
+				So(d00.Key, ShouldEqual, "label-model")
+				So(d00.Value, ShouldEqual, "foo-model")
+				So(d01.Key, ShouldEqual, "k1")
+				So(d01.Value, ShouldEqual, "v1")
+				So(d02.Key, ShouldEqual, "k2")
+				So(d02.Value, ShouldEqual, "v2")
+				So(d03.Key, ShouldEqual, "k3")
+				So(d03.Value, ShouldEqual, "v3")
 
 				d10 := s1.Properties.Dimensions[0]
-				So(d10.Key, ShouldEqual, "k1")
-				So(d10.Value, ShouldEqual, "v1")
+				d11 := s1.Properties.Dimensions[1]
+				So(d10.Key, ShouldEqual, "label-model")
+				So(d10.Value, ShouldEqual, "foo-model")
+				So(d11.Key, ShouldEqual, "k1")
+				So(d11.Value, ShouldEqual, "v1")
 
 				// First slice command doesn't include provisioning.
 				// Second slice (fallback) does.