go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/ftxtest/swarming.go (about)

     1  // Copyright 2023 The Fuchsia Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style license that can
     3  // found in the LICENSE file.
     4  package main
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"go.chromium.org/luci/common/api/swarming/swarming/v1"
    19  	ftxproto "go.fuchsia.dev/infra/cmd/ftxtest/proto"
    20  )
    21  
    22  type Swarming struct {
    23  	instance string
    24  	service  *swarming.Service
    25  	cas      *CAS
    26  }
    27  
    28  const (
    29  	taskPriority         = 200
    30  	taskExpiration       = 5 * time.Hour
    31  	taskExecutionTimeout = 2 * time.Hour
    32  	poolTaskInterval     = 10 * time.Second
    33  	defaultSummaryPath   = "out/summary.json"
    34  )
    35  
    36  func NewSwarming(httpClient *http.Client, instance string, cas *CAS) (*Swarming, error) {
    37  	swarmingService, err := swarming.New(httpClient)
    38  	swarmingService.BasePath = fmt.Sprintf("https://%s.appspot.com/_ah/api/swarming/v1/", instance)
    39  	if err != nil {
    40  		return nil, fmt.Errorf("swarming.New: %v", err)
    41  	}
    42  	swarming := &Swarming{
    43  		service:  swarmingService,
    44  		instance: instance,
    45  		cas:      cas,
    46  	}
    47  	return swarming, nil
    48  }
    49  
    50  func (s *Swarming) LaunchTask(buildInput *ftxproto.InputProperties, parentTaskId string) (*swarming.SwarmingRpcsTaskRequestMetadata, error) {
    51  	casInput, err := s.casInput(buildInput)
    52  	if err != nil {
    53  		return nil, fmt.Errorf("casInput: %v", err)
    54  	}
    55  	hostTest := buildInput.Test.GetHost()
    56  	properties := &swarming.SwarmingRpcsTaskProperties{
    57  		ExecutionTimeoutSecs: int64(taskExpiration.Seconds()),
    58  		Command:              hostTest.Command,
    59  		CasInputRoot:         casInput,
    60  		Outputs:              []string{"out"},
    61  		Dimensions:           swarmmingStringPair(buildInput.TargetDimensions),
    62  		Env:                  swarmmingStringPair(hostTest.Env),
    63  	}
    64  	if len(buildInput.CipdPackages) > 0 {
    65  		properties.CipdInput = cipdInput(buildInput.CipdPackages)
    66  	}
    67  	return s.service.Tasks.New(&swarming.SwarmingRpcsNewTaskRequest{
    68  		Name:           buildInput.Name,
    69  		ParentTaskId:   parentTaskId,
    70  		ServiceAccount: "internal-artifact-readers@fuchsia-infra.iam.gserviceaccount.com",
    71  		ExpirationSecs: int64(taskExpiration.Seconds()),
    72  		Priority:       taskPriority,
    73  		Realm:          realm(buildInput),
    74  		Properties:     properties,
    75  	}).Do()
    76  }
    77  
    78  func (s *Swarming) WaitTask(taskId string) error {
    79  	for {
    80  		result, err := s.service.Task.Result(taskId).Fields("state", "failure", "internal_failure").Do()
    81  		if err != nil {
    82  			err = fmt.Errorf("swarming.Task.Result RPC failed: %v", err)
    83  			return err
    84  		}
    85  		if result.State == "PENDING" || result.State == "RUNNING" {
    86  			time.Sleep(poolTaskInterval)
    87  			continue
    88  		}
    89  		if result.State != "COMPLETED" || result.Failure || result.InternalFailure {
    90  			return fmt.Errorf("Task got %s want COMPLETED", result.State)
    91  		}
    92  		return nil
    93  	}
    94  }
    95  
    96  func (s *Swarming) casInput(buildInput *ftxproto.InputProperties) (*swarming.SwarmingRpcsCASReference, error) {
    97  	digestSplit := strings.Split(buildInput.InputArtifactsDigest, "/")
    98  	sizeBytes, err := strconv.ParseInt(digestSplit[1], 10, 64)
    99  	if err != nil {
   100  		return nil, fmt.Errorf("sizeBytes: %v", err)
   101  	}
   102  	return &swarming.SwarmingRpcsCASReference{
   103  		CasInstance: fmt.Sprintf("projects/%s/instances/default_instance", s.instance),
   104  		Digest: &swarming.SwarmingRpcsDigest{
   105  			Hash:      digestSplit[0],
   106  			SizeBytes: sizeBytes,
   107  		},
   108  	}, nil
   109  }
   110  
   111  func cipdInput(cipdPackages []*ftxproto.CipdPackage) *swarming.SwarmingRpcsCipdInput {
   112  	packages := []*swarming.SwarmingRpcsCipdPackage{}
   113  	for _, inputPkg := range cipdPackages {
   114  		packages = append(packages, &swarming.SwarmingRpcsCipdPackage{
   115  			Path:        inputPkg.Path,
   116  			Version:     inputPkg.Version,
   117  			PackageName: inputPkg.Name,
   118  		})
   119  	}
   120  	return &swarming.SwarmingRpcsCipdInput{
   121  		Packages: packages,
   122  	}
   123  }
   124  
   125  func swarmmingStringPair(m map[string]string) []*swarming.SwarmingRpcsStringPair {
   126  	result := []*swarming.SwarmingRpcsStringPair{}
   127  	for key, value := range m {
   128  		result = append(result, &swarming.SwarmingRpcsStringPair{
   129  			Key:   key,
   130  			Value: value,
   131  		})
   132  	}
   133  	return result
   134  }
   135  
   136  func realm(buildInput *ftxproto.InputProperties) string {
   137  	if buildInput.External {
   138  		return "fuchsia:try"
   139  	} else {
   140  		return "turquoise:global.try"
   141  	}
   142  }
   143  
   144  // testSummary determines the data for out/summary.json
   145  type testSummary struct {
   146  	Success bool `json:"success"`
   147  }
   148  
   149  func (s *Swarming) CheckTestFailure(ctx context.Context, taskId string, buildInput *ftxproto.InputProperties, buildOutput *ftxproto.OutputProperties) error {
   150  	task, err := s.service.Task.Result(taskId).Do()
   151  	if err != nil {
   152  		return fmt.Errorf("task.request: %v", err)
   153  	}
   154  	if task.CasOutputRoot == nil || task.CasOutputRoot.Digest == nil {
   155  		return errors.New("Swarming task did not produce CAS output")
   156  	}
   157  	d := task.CasOutputRoot.Digest
   158  	buildOutput.OutputArtifactsDigest = fmt.Sprintf("%s/%d", d.Hash, d.SizeBytes)
   159  	dir, err := s.cas.Download(ctx, d.Hash, d.SizeBytes)
   160  	if err != nil {
   161  		return fmt.Errorf("cas.Download: %v", err)
   162  	}
   163  	summaryPath := defaultSummaryPath
   164  	if buildInput.SummaryPath != "" {
   165  		summaryPath = buildInput.SummaryPath
   166  	}
   167  	summary := testSummary{}
   168  	err = readJSON(filepath.Join(dir, summaryPath), &summary)
   169  	if err != nil {
   170  
   171  		return fmt.Errorf("readJSON: %v", err)
   172  	}
   173  	if !summary.Success {
   174  		return errors.New("Test failure")
   175  	}
   176  	return nil
   177  }
   178  
   179  func readJSON(filename string, out any) error {
   180  	rawData, err := os.ReadFile(filename)
   181  	if err != nil {
   182  		return fmt.Errorf("os.ReadFile: %v", err)
   183  	}
   184  	err = json.Unmarshal(rawData, out)
   185  	if err != nil {
   186  		return fmt.Errorf("json.Unmarshal: %v", err)
   187  	}
   188  	return nil
   189  }