github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/command/command.go (about) 1 // Package command defines common types to be used with command execution. 2 package command 3 4 import ( 5 "crypto/sha256" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "os" 10 "sort" 11 "strings" 12 "time" 13 14 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 15 log "github.com/golang/glog" 16 "github.com/google/uuid" 17 18 cpb "github.com/bazelbuild/remote-apis-sdks/go/api/command" 19 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 20 anypb "google.golang.org/protobuf/types/known/anypb" 21 tspb "google.golang.org/protobuf/types/known/timestamppb" 22 ) 23 24 // InputType can be specified to narrow down the matching for a given input path. 25 type InputType int 26 27 const ( 28 // UnspecifiedInputType means any input type will match. 29 UnspecifiedInputType InputType = iota 30 31 // DirectoryInputType means only directories match. 32 DirectoryInputType 33 34 // FileInputType means only files match. 35 FileInputType 36 37 // SymlinkInputType means only symlink match. 38 SymlinkInputType 39 ) 40 41 var inputTypes = [...]string{"UnspecifiedInputType", "DirectoryInputType", "FileInputType"} 42 43 func (s InputType) String() string { 44 if UnspecifiedInputType <= s && s <= FileInputType { 45 return inputTypes[s] 46 } 47 return fmt.Sprintf("InvalidInputType(%d)", s) 48 } 49 50 // SymlinkBehaviorType represents how symlinks are handled. 51 type SymlinkBehaviorType int 52 53 const ( 54 // UnspecifiedSymlinkBehavior means following clients.TreeSymlinkOpts 55 // or DefaultTreeSymlinkOpts if clients.TreeSymlinkOpts is null. 56 UnspecifiedSymlinkBehavior SymlinkBehaviorType = iota 57 58 // ResolveSymlink means symlinks are resolved. 59 ResolveSymlink 60 61 // PreserveSymlink means symlinks are kept as-is. 62 PreserveSymlink 63 ) 64 65 var symlinkBehaviorType = [...]string{"UnspecifiedSymlinkBehavior", "ResolveSymlink", "PreserveSymlink"} 66 67 func (s SymlinkBehaviorType) String() string { 68 if UnspecifiedSymlinkBehavior <= s && s <= PreserveSymlink { 69 return symlinkBehaviorType[s-UnspecifiedSymlinkBehavior] 70 } 71 return fmt.Sprintf("InvalidSymlinkBehaviorType(%d)", s) 72 } 73 74 // InputExclusion represents inputs to be excluded from being considered for command execution. 75 type InputExclusion struct { 76 // Required: the path regular expression to match for exclusion. 77 Regex string 78 79 // The input type to match for exclusion. 80 Type InputType 81 } 82 83 // VirtualInput represents an input that may exist on disk but shouldn't be accessed. 84 // We want to stage it on disk for the command execution. 85 type VirtualInput struct { 86 // The path for the input to be staged at, relative to the ExecRoot. 87 Path string 88 89 // The byte contents of the file to be staged. 90 Contents []byte 91 92 // The digest of the virtual input that is expected to exist in the CAS. 93 // Should not be used together with Contents. 94 Digest string 95 96 // Whether the file should be staged as executable. 97 IsExecutable bool 98 99 // Whether the file is actually an empty directory. This is used to provide 100 // empty directory inputs. When this is set, Contents and IsExecutable are 101 // ignored. 102 IsEmptyDirectory bool 103 104 // Mtime of the virtual input. 105 Mtime time.Time 106 107 // The virtual inputs' mode and permissions bits. 108 FileMode os.FileMode 109 } 110 111 // InputSpec represents all the required inputs to a remote command. 112 type InputSpec struct { 113 // Input paths (files or directories) that need to be present for the command execution. 114 Inputs []string 115 116 // Inputs not present on the local file system, but should be staged for command execution. 117 VirtualInputs []*VirtualInput 118 119 // Inputs matching these patterns will be excluded. 120 InputExclusions []*InputExclusion 121 122 // Environment variables the command relies on. 123 EnvironmentVariables map[string]string 124 125 // SymlinkBehavior represents the way symlinks will be handled. 126 SymlinkBehavior SymlinkBehaviorType 127 128 // Node properties of inputs. 129 InputNodeProperties map[string]*cpb.NodeProperties 130 } 131 132 // String returns the string representation of the VirtualInput. 133 func (s *VirtualInput) String() string { 134 return fmt.Sprintf("%+v", *s) 135 } 136 137 // String returns the string representation of the InputExclusion. 138 func (s *InputExclusion) String() string { 139 return fmt.Sprintf("%+v", *s) 140 } 141 142 // Identifiers is a group of identifiers of a command. 143 type Identifiers struct { 144 // CommandID is an optional id to use to identify a command. 145 CommandID string 146 147 // InvocationID is an optional id to use to identify an invocation spanning multiple commands. 148 InvocationID string 149 150 // CorrelatedInvocationID is an optional id to use to identify a build spanning multiple invocations. 151 CorrelatedInvocationID string 152 153 // ToolName is an optional tool name to pass to the remote server for logging. 154 ToolName string 155 156 // ToolVersion is an optional tool version to pass to the remote server for logging. 157 ToolVersion string 158 159 // ExecutionID is a UUID generated for a particular execution of this command. 160 ExecutionID string 161 } 162 163 // Command encompasses the complete information required to execute a command remotely. 164 // To make sure to initialize a valid Command object, call FillDefaultFieldValues on the created 165 // struct. 166 type Command struct { 167 // Identifiers used to identify this command to be passed to RE. 168 Identifiers *Identifiers 169 170 // Args (required): command line elements to execute. 171 Args []string 172 173 // ExecRoot is an absolute path to the execution root of the command. All the other paths are 174 // specified relatively to this path. 175 ExecRoot string 176 177 // WorkingDir is the working directory, relative to the exec root, for the command to run 178 // in. It must be a directory which exists in the input tree. If it is left empty, then the 179 // action is run from the exec root. 180 WorkingDir string 181 182 // RemoteWorkingDir is the working directory when executing the command on RE server. 183 // It's relative to exec root and, if provided, needs to have the same number of levels 184 // as WorkingDir. If not provided, the remote command is run from the WorkingDir 185 RemoteWorkingDir string 186 187 // InputSpec: the command inputs. 188 InputSpec *InputSpec 189 190 // OutputFiles are the command output files. 191 OutputFiles []string 192 193 // OutputDirs are the command output directories. 194 // The files and directories will likely be merged into a single Outputs field in the future. 195 OutputDirs []string 196 197 // Timeout is an optional duration to wait for command execution before timing out. 198 Timeout time.Duration 199 200 // Platform is the platform to use for the execution. 201 Platform map[string]string 202 } 203 204 func marshallMap(m map[string]string, buf *[]byte) { 205 var pkeys []string 206 for k := range m { 207 pkeys = append(pkeys, k) 208 } 209 sort.Strings(pkeys) 210 for _, k := range pkeys { 211 *buf = append(*buf, []byte(k)...) 212 *buf = append(*buf, []byte(m[k])...) 213 } 214 } 215 216 func marshallSlice(s []string, buf *[]byte) { 217 for _, i := range s { 218 *buf = append(*buf, []byte(i)...) 219 } 220 } 221 222 func marshallSortedSlice(s []string, buf *[]byte) { 223 ss := make([]string, len(s)) 224 copy(ss, s) 225 sort.Strings(ss) 226 marshallSlice(ss, buf) 227 } 228 229 // Validate checks whether all required command fields have been specified. 230 func (c *Command) Validate() error { 231 if c == nil { 232 return nil 233 } 234 if len(c.Args) == 0 { 235 return errors.New("missing command arguments") 236 } 237 if c.ExecRoot == "" { 238 return errors.New("missing command exec root") 239 } 240 if c.InputSpec == nil { 241 return errors.New("missing command input spec") 242 } 243 if c.Identifiers == nil { 244 return errors.New("missing command identifiers") 245 } 246 if c.RemoteWorkingDir != "" && levels(c.RemoteWorkingDir) != levels(c.WorkingDir) { 247 return fmt.Errorf("invalid RemoteWorkingDir=%q[%v level(s)], it's expected to have the same depth as WorkingDir=%q[%v level(s)]", 248 c.RemoteWorkingDir, levels(c.RemoteWorkingDir), c.WorkingDir, levels(c.WorkingDir)) 249 } 250 // TODO(olaola): make Platform required? 251 return nil 252 } 253 254 // Generates a stable id for the command. 255 func (c *Command) stableID() string { 256 var buf []byte 257 marshallSlice(c.Args, &buf) 258 buf = append(buf, []byte(c.ExecRoot)...) 259 buf = append(buf, []byte(c.WorkingDir)...) 260 marshallSortedSlice(c.OutputFiles, &buf) 261 marshallSortedSlice(c.OutputDirs, &buf) 262 buf = append(buf, []byte(c.Timeout.String())...) 263 marshallMap(c.Platform, &buf) 264 if c.InputSpec != nil { 265 marshallMap(c.InputSpec.EnvironmentVariables, &buf) 266 marshallSortedSlice(c.InputSpec.Inputs, &buf) 267 inputExclusions := make([]*InputExclusion, len(c.InputSpec.InputExclusions)) 268 copy(inputExclusions, c.InputSpec.InputExclusions) 269 sort.Slice(inputExclusions, func(i, j int) bool { 270 e1 := inputExclusions[i] 271 e2 := inputExclusions[j] 272 return e1.Regex > e2.Regex || e1.Regex == e2.Regex && e1.Type > e2.Type 273 }) 274 for _, e := range inputExclusions { 275 buf = append(buf, []byte(e.Regex)...) 276 buf = append(buf, []byte(e.Type.String())...) 277 } 278 } 279 sha256Arr := sha256.Sum256(buf) 280 return hex.EncodeToString(sha256Arr[:])[:8] 281 } 282 283 // FillDefaultFieldValues initializes valid default values to inner Command fields. 284 // This function should be called on every new Command object before use. 285 func (c *Command) FillDefaultFieldValues() { 286 if c == nil { 287 return 288 } 289 if c.Identifiers == nil { 290 c.Identifiers = &Identifiers{} 291 } 292 if c.Identifiers.CommandID == "" { 293 c.Identifiers.CommandID = c.stableID() 294 } 295 if c.Identifiers.ToolName == "" { 296 c.Identifiers.ToolName = "remote-client" 297 } 298 if c.Identifiers.InvocationID == "" { 299 if id, err := uuid.NewRandom(); err == nil { 300 c.Identifiers.InvocationID = id.String() 301 } else { 302 log.Warningf("Failed to generate InvocationID: %s", err) 303 } 304 } 305 if c.Identifiers.ExecutionID == "" { 306 if id, err := uuid.NewRandom(); err == nil { 307 c.Identifiers.ExecutionID = id.String() 308 } else { 309 log.Warningf("Failed to generate ExecutionID: %s", err) 310 } 311 } 312 if c.InputSpec == nil { 313 c.InputSpec = &InputSpec{} 314 } 315 } 316 317 func levels(path string) int { 318 return len(strings.Split(path, string(os.PathSeparator))) 319 } 320 321 // ExecutionOptions specify how to execute a given Command. 322 type ExecutionOptions struct { 323 // Whether to accept cached action results. Defaults to true. 324 AcceptCached bool 325 326 // When set, this execution results will not be cached. 327 DoNotCache bool 328 329 // Download command outputs after execution. Defaults to true. 330 DownloadOutputs bool 331 332 // Preserve mtimes for unchanged outputs when downloading. Defaults to false. 333 PreserveUnchangedOutputMtime bool 334 335 // Download command stdout and stderr. Defaults to true. If StreamOutErr is also set, this value 336 // is ignored for uncached actions if the server provides log streams for both stdout and stderr. 337 // For cached action results, or if the server does not provide log streams for stdout or stderr, 338 // this value will determine whether stdout and stderr is downloaded. 339 DownloadOutErr bool 340 341 // Request that stdout and stderr be streamed back to the client while the action is running. 342 // Defaults to false. If either stream is not provided by the server, the client will fall back to 343 // downloading the corresponding streams after the action has completed, provided DownloadOutErr 344 // is also set. The client may expect a delay in this scenario as the streams are downloaded after 345 // the fact. 346 StreamOutErr bool 347 } 348 349 // DefaultExecutionOptions returns the recommended ExecutionOptions. 350 func DefaultExecutionOptions() *ExecutionOptions { 351 return &ExecutionOptions{ 352 AcceptCached: true, 353 DoNotCache: false, 354 DownloadOutputs: true, 355 PreserveUnchangedOutputMtime: false, 356 DownloadOutErr: true, 357 StreamOutErr: false, 358 } 359 } 360 361 // ResultStatus represents the options for a finished command execution. 362 type ResultStatus int 363 364 const ( 365 // UnspecifiedResultStatus is an invalid value, should not be used. 366 UnspecifiedResultStatus ResultStatus = iota 367 368 // SuccessResultStatus indicates that the command executed successfully. 369 SuccessResultStatus 370 371 // CacheHitResultStatus indicates that the command was a cache hit. 372 CacheHitResultStatus 373 374 // NonZeroExitResultStatus indicates that the command executed with a non zero exit code. 375 NonZeroExitResultStatus 376 377 // TimeoutResultStatus indicates that the command exceeded its specified deadline. 378 TimeoutResultStatus 379 380 // InterruptedResultStatus indicates that the command execution was interrupted. 381 InterruptedResultStatus 382 383 // RemoteErrorResultStatus indicates that an error occurred on the remote server. 384 RemoteErrorResultStatus 385 386 // LocalErrorResultStatus indicates that an error occurred locally. 387 LocalErrorResultStatus 388 ) 389 390 var resultStatuses = [...]string{ 391 "UnspecifiedResultStatus", 392 "SuccessResultStatus", 393 "CacheHitResultStatus", 394 "NonZeroExitResultStatus", 395 "TimeoutResultStatus", 396 "InterruptedResultStatus", 397 "RemoteErrorResultStatus", 398 "LocalErrorResultStatus", 399 } 400 401 // IsOk returns whether the status indicates a successful action. 402 func (s ResultStatus) IsOk() bool { 403 return s == SuccessResultStatus || s == CacheHitResultStatus 404 } 405 406 func (s ResultStatus) String() string { 407 if UnspecifiedResultStatus <= s && s <= LocalErrorResultStatus { 408 return resultStatuses[s] 409 } 410 return fmt.Sprintf("InvalidResultStatus(%d)", s) 411 } 412 413 // Result is the result of a finished command execution. 414 type Result struct { 415 // Command exit code. 416 ExitCode int 417 // Status of the finished run. 418 Status ResultStatus 419 // Any error encountered. 420 Err error 421 } 422 423 // IsOk returns whether the result was successful. 424 func (r *Result) IsOk() bool { 425 return r.Status.IsOk() 426 } 427 428 // LocalErrorExitCode is an exit code corresponding to a local error. 429 const LocalErrorExitCode = 35 430 431 // TimeoutExitCode is an exit code corresponding to the command timing out remotely. 432 const TimeoutExitCode = /*SIGNAL_BASE=*/ 128 + /*SIGALRM=*/ 14 433 434 // RemoteErrorExitCode is an exit code corresponding to a remote server error. 435 const RemoteErrorExitCode = 45 436 437 // InterruptedExitCode is an exit code corresponding to an execution interruption by the user. 438 const InterruptedExitCode = 8 439 440 // NewLocalErrorResult constructs a Result from a local error. 441 func NewLocalErrorResult(err error) *Result { 442 return &Result{ 443 ExitCode: LocalErrorExitCode, 444 Status: LocalErrorResultStatus, 445 Err: err, 446 } 447 } 448 449 // NewRemoteErrorResult constructs a Result from a remote error. 450 func NewRemoteErrorResult(err error) *Result { 451 return &Result{ 452 ExitCode: RemoteErrorExitCode, 453 Status: RemoteErrorResultStatus, 454 Err: err, 455 } 456 } 457 458 // NewResultFromExitCode constructs a Result from a given command exit code. 459 func NewResultFromExitCode(exitCode int) *Result { 460 st := SuccessResultStatus 461 if exitCode != 0 { 462 st = NonZeroExitResultStatus 463 } 464 return &Result{ 465 ExitCode: exitCode, 466 Status: st, 467 } 468 } 469 470 // NewTimeoutResult constructs a new result for a timeout-exceeded command. 471 func NewTimeoutResult() *Result { 472 return &Result{ 473 ExitCode: TimeoutExitCode, 474 Status: TimeoutResultStatus, 475 } 476 } 477 478 // TimeInterval is a time window for an event. 479 type TimeInterval struct { 480 From, To time.Time 481 } 482 483 // These are the events that we export time metrics on: 484 const ( 485 // EventServerQueued: Queued time on the remote server. 486 EventServerQueued = "ServerQueued" 487 488 // EventServerWorker: The total remote worker (bot) time. 489 EventServerWorker = "ServerWorker" 490 491 // EventServerWorkerInputFetch: Time to fetch inputs to the remote bot. 492 EventServerWorkerInputFetch = "ServerWorkerInputFetch" 493 494 // EventServerWorkerExecution: The actual execution on the remote bot. 495 EventServerWorkerExecution = "ServerWorkerExecution" 496 497 // EventServerWorkerOutputUpload: Uploading outputs to the CAS on the bot. 498 EventServerWorkerOutputUpload = "ServerWorkerOutputUpload" 499 500 // EventDownloadResults: Downloading action results from CAS. 501 EventDownloadResults = "DownloadResults" 502 503 // EventComputeMerkleTree: Computing the input Merkle tree. 504 EventComputeMerkleTree = "ComputeMerkleTree" 505 506 // EventCheckActionCache: Checking the action cache. 507 EventCheckActionCache = "CheckActionCache" 508 509 // EventUpdateCachedResult: Uploading local outputs to CAS and updating cached 510 // action result. 511 EventUpdateCachedResult = "UpdateCachedResult" 512 513 // EventUploadInputs: Uploading action inputs to CAS for remote execution. 514 EventUploadInputs = "UploadInputs" 515 516 // EventExecuteRemotely: Total time to execute remotely. 517 EventExecuteRemotely = "ExecuteRemotely" 518 ) 519 520 // Metadata is general information associated with a Command execution. 521 type Metadata struct { 522 // CommandDigest is a digest of the command being executed. It can be used 523 // to detect changes in the command between builds. 524 CommandDigest digest.Digest 525 // ActionDigest is a digest of the action being executed. It can be used 526 // to detect changes in the action between builds. 527 ActionDigest digest.Digest 528 // The total number of input files. 529 InputFiles int 530 // The total number of input directories. 531 InputDirectories int 532 // The overall number of bytes from all the inputs. 533 TotalInputBytes int64 534 // Event times for remote events, by event name. 535 EventTimes map[string]*TimeInterval 536 537 AuxiliaryMetadata []*anypb.Any 538 // The total number of output files (incl symlinks). 539 OutputFiles int 540 // The total number of output directories (incl symlinks, but not recursive). 541 OutputDirectories int 542 // The overall number of bytes from all the output files (incl. stdout/stderr, but not symlinks). 543 TotalOutputBytes int64 544 // Output File digests. 545 OutputFileDigests map[string]digest.Digest 546 // Output Directory digests. 547 OutputDirectoryDigests map[string]digest.Digest 548 // Output Symlinks. 549 OutputSymlinks map[string]string 550 // Missing digests that are uploaded to CAS. 551 MissingDigests []digest.Digest 552 // LogicalBytesUploaded is the sum of sizes in bytes of the blobs that were uploaded. It should be 553 // the same value as the sum of digest sizes in MissingDigests. 554 LogicalBytesUploaded int64 555 // RealBytesUploaded is the number of bytes that were put on the wire for upload (exclusing metadata). 556 // It may differ from LogicalBytesUploaded due to compression. 557 RealBytesUploaded int64 558 // LogicalBytesDownloaded is the sum of sizes in bytes of the blobs that were downloaded. It should be 559 // the same value as the sum of digest sizes in OutputDigests. 560 LogicalBytesDownloaded int64 561 // RealBytesDownloaded is the number of bytes that were put on the wire for download (exclusing metadata). 562 // It may differ from LogicalBytesDownloaded due to compression. 563 RealBytesDownloaded int64 564 // StderrDigest is a digest of the standard error after being executed. 565 StderrDigest digest.Digest 566 // StdoutDigest is a digest of the standard output after being executed. 567 StdoutDigest digest.Digest 568 // TODO(olaola): Add a lot of other fields. 569 } 570 571 // ToREProto converts the Command to an RE API Command proto. 572 // `useOutputPathsField` selects what field/s to fill with the paths of outputs, 573 // which will depend on the RE API version. 574 func (c *Command) ToREProto(useOutputPathsField bool) *repb.Command { 575 workingDir := c.RemoteWorkingDir 576 if workingDir == "" { 577 workingDir = c.WorkingDir 578 } 579 cmdPb := &repb.Command{ 580 Arguments: c.Args, 581 WorkingDirectory: workingDir, 582 } 583 584 // In v2.1 of the RE API the `output_{files, directories}` fields were 585 // replaced by a single field: `output_paths`. 586 if useOutputPathsField { 587 cmdPb.OutputPaths = append(c.OutputFiles, c.OutputDirs...) 588 sort.Strings(cmdPb.OutputPaths) 589 } else { 590 cmdPb.OutputFiles = make([]string, len(c.OutputFiles)) 591 copy(cmdPb.OutputFiles, c.OutputFiles) 592 sort.Strings(cmdPb.OutputFiles) 593 594 cmdPb.OutputDirectories = make([]string, len(c.OutputDirs)) 595 copy(cmdPb.OutputDirectories, c.OutputDirs) 596 sort.Strings(cmdPb.OutputDirectories) 597 } 598 599 for name, val := range c.InputSpec.EnvironmentVariables { 600 cmdPb.EnvironmentVariables = append(cmdPb.EnvironmentVariables, &repb.Command_EnvironmentVariable{Name: name, Value: val}) 601 } 602 sort.Slice(cmdPb.EnvironmentVariables, func(i, j int) bool { return cmdPb.EnvironmentVariables[i].Name < cmdPb.EnvironmentVariables[j].Name }) 603 if len(c.Platform) > 0 { 604 cmdPb.Platform = &repb.Platform{} 605 for name, val := range c.Platform { 606 cmdPb.Platform.Properties = append(cmdPb.Platform.Properties, &repb.Platform_Property{Name: name, Value: val}) 607 } 608 sort.Slice(cmdPb.Platform.Properties, func(i, j int) bool { return cmdPb.Platform.Properties[i].Name < cmdPb.Platform.Properties[j].Name }) 609 } 610 return cmdPb 611 } 612 613 func FromREProto(cmdPb *repb.Command) *Command { 614 cmd := &Command{ 615 InputSpec: &InputSpec{ 616 EnvironmentVariables: make(map[string]string), 617 InputNodeProperties: make(map[string]*cpb.NodeProperties), 618 }, 619 Identifiers: &Identifiers{}, 620 WorkingDir: cmdPb.WorkingDirectory, 621 OutputFiles: cmdPb.OutputFiles, 622 OutputDirs: cmdPb.OutputDirectories, 623 Platform: make(map[string]string), 624 Args: cmdPb.Arguments, 625 } 626 627 // In v2.1 of the RE API the `output_{files, directories}` fields were 628 // replaced by a single field: `output_paths`. 629 if len(cmdPb.OutputPaths) > 0 { 630 cmd.OutputFiles = cmdPb.OutputPaths 631 cmd.OutputDirs = nil 632 } 633 for _, ev := range cmdPb.EnvironmentVariables { 634 cmd.InputSpec.EnvironmentVariables[ev.Name] = ev.Value 635 } 636 for _, pt := range cmdPb.GetPlatform().GetProperties() { 637 cmd.Platform[pt.Name] = pt.Value 638 } 639 return cmd 640 } 641 642 // FromProto parses a Command struct from a proto message. 643 func FromProto(p *cpb.Command) *Command { 644 ids := &Identifiers{ 645 CommandID: p.GetIdentifiers().GetCommandId(), 646 InvocationID: p.GetIdentifiers().GetInvocationId(), 647 CorrelatedInvocationID: p.GetIdentifiers().GetCorrelatedInvocationsId(), 648 ToolName: p.GetIdentifiers().GetToolName(), 649 ToolVersion: p.GetIdentifiers().GetToolVersion(), 650 ExecutionID: p.GetIdentifiers().GetExecutionId(), 651 } 652 is := inputSpecFromProto(p.GetInput()) 653 return &Command{ 654 Identifiers: ids, 655 ExecRoot: p.ExecRoot, 656 Args: p.Args, 657 WorkingDir: p.WorkingDirectory, 658 RemoteWorkingDir: p.RemoteWorkingDirectory, 659 InputSpec: is, 660 OutputFiles: p.GetOutput().GetOutputFiles(), 661 OutputDirs: p.GetOutput().GetOutputDirectories(), 662 Timeout: time.Duration(p.ExecutionTimeout) * time.Second, 663 Platform: p.Platform, 664 } 665 } 666 667 func inputSpecFromProto(is *cpb.InputSpec) *InputSpec { 668 var excl []*InputExclusion 669 for _, ex := range is.GetExcludeInputs() { 670 excl = append(excl, &InputExclusion{ 671 Regex: ex.Regex, 672 Type: inputTypeFromProto(ex.Type), 673 }) 674 } 675 var vis []*VirtualInput 676 for _, vi := range is.GetVirtualInputs() { 677 contents := make([]byte, len(vi.Contents)) 678 copy(contents, vi.Contents) 679 vis = append(vis, &VirtualInput{ 680 Path: vi.Path, 681 Contents: contents, 682 IsExecutable: vi.IsExecutable, 683 IsEmptyDirectory: vi.IsEmptyDirectory, 684 Digest: vi.Digest, 685 Mtime: vi.Mtime.AsTime(), 686 FileMode: os.FileMode(vi.Filemode), 687 }) 688 } 689 return &InputSpec{ 690 Inputs: is.GetInputs(), 691 VirtualInputs: vis, 692 InputExclusions: excl, 693 EnvironmentVariables: is.GetEnvironmentVariables(), 694 SymlinkBehavior: symlinkBehaviorFromProto(is.GetSymlinkBehavior()), 695 InputNodeProperties: is.GetInputNodeProperties(), 696 } 697 } 698 699 func NodePropertiesToAPI(np *cpb.NodeProperties) *repb.NodeProperties { 700 if np == nil { 701 return nil 702 } 703 res := &repb.NodeProperties{ 704 Mtime: np.GetMtime(), 705 UnixMode: np.GetUnixMode(), 706 } 707 if np.Properties != nil { 708 res.Properties = make([]*repb.NodeProperty, 0, len(np.Properties)) 709 } 710 for _, p := range np.GetProperties() { 711 res.Properties = append(res.Properties, &repb.NodeProperty{ 712 Name: p.Name, 713 Value: p.Value, 714 }) 715 } 716 return res 717 } 718 719 func NodePropertiesFromAPI(np *repb.NodeProperties) *cpb.NodeProperties { 720 if np == nil { 721 return nil 722 } 723 res := &cpb.NodeProperties{ 724 Mtime: np.GetMtime(), 725 UnixMode: np.GetUnixMode(), 726 } 727 if np.Properties != nil { 728 res.Properties = make([]*cpb.NodeProperty, 0, len(np.Properties)) 729 } 730 for _, p := range np.GetProperties() { 731 res.Properties = append(res.Properties, &cpb.NodeProperty{ 732 Name: p.Name, 733 Value: p.Value, 734 }) 735 } 736 return res 737 } 738 739 func inputSpecToProto(is *InputSpec) *cpb.InputSpec { 740 var excl []*cpb.ExcludeInput 741 for _, ex := range is.InputExclusions { 742 excl = append(excl, &cpb.ExcludeInput{ 743 Regex: ex.Regex, 744 Type: inputTypeToProto(ex.Type), 745 }) 746 } 747 var vis []*cpb.VirtualInput 748 for _, vi := range is.VirtualInputs { 749 contents := make([]byte, len(vi.Contents)) 750 copy(contents, vi.Contents) 751 vis = append(vis, &cpb.VirtualInput{ 752 Path: vi.Path, 753 Contents: contents, 754 IsExecutable: vi.IsExecutable, 755 IsEmptyDirectory: vi.IsEmptyDirectory, 756 Digest: vi.Digest, 757 Mtime: tspb.New(vi.Mtime), 758 Filemode: uint32(vi.FileMode), 759 }) 760 } 761 return &cpb.InputSpec{ 762 Inputs: is.Inputs, 763 VirtualInputs: vis, 764 ExcludeInputs: excl, 765 EnvironmentVariables: is.EnvironmentVariables, 766 SymlinkBehavior: symlinkBehaviorToProto(is.SymlinkBehavior), 767 InputNodeProperties: is.InputNodeProperties, 768 } 769 } 770 771 func inputTypeFromProto(t cpb.InputType_Value) InputType { 772 switch t { 773 case cpb.InputType_DIRECTORY: 774 return DirectoryInputType 775 case cpb.InputType_FILE: 776 return FileInputType 777 default: 778 return UnspecifiedInputType 779 } 780 } 781 782 func inputTypeToProto(t InputType) cpb.InputType_Value { 783 switch t { 784 case DirectoryInputType: 785 return cpb.InputType_DIRECTORY 786 case FileInputType: 787 return cpb.InputType_FILE 788 default: 789 return cpb.InputType_UNSPECIFIED 790 } 791 } 792 793 func symlinkBehaviorFromProto(t cpb.SymlinkBehaviorType_Value) SymlinkBehaviorType { 794 switch t { 795 case cpb.SymlinkBehaviorType_RESOLVE: 796 return ResolveSymlink 797 case cpb.SymlinkBehaviorType_PRESERVE: 798 return PreserveSymlink 799 default: 800 return UnspecifiedSymlinkBehavior 801 } 802 } 803 804 func symlinkBehaviorToProto(t SymlinkBehaviorType) cpb.SymlinkBehaviorType_Value { 805 switch t { 806 case ResolveSymlink: 807 return cpb.SymlinkBehaviorType_RESOLVE 808 case PreserveSymlink: 809 return cpb.SymlinkBehaviorType_PRESERVE 810 default: 811 return cpb.SymlinkBehaviorType_UNSPECIFIED 812 } 813 } 814 815 func protoStatusFromResultStatus(s ResultStatus) cpb.CommandResultStatus_Value { 816 switch s { 817 case SuccessResultStatus: 818 return cpb.CommandResultStatus_SUCCESS 819 case CacheHitResultStatus: 820 return cpb.CommandResultStatus_CACHE_HIT 821 case NonZeroExitResultStatus: 822 return cpb.CommandResultStatus_NON_ZERO_EXIT 823 case TimeoutResultStatus: 824 return cpb.CommandResultStatus_TIMEOUT 825 case InterruptedResultStatus: 826 return cpb.CommandResultStatus_INTERRUPTED 827 case RemoteErrorResultStatus: 828 return cpb.CommandResultStatus_REMOTE_ERROR 829 case LocalErrorResultStatus: 830 return cpb.CommandResultStatus_LOCAL_ERROR 831 default: 832 return cpb.CommandResultStatus_UNKNOWN 833 } 834 } 835 836 func protoStatusToResultStatus(s cpb.CommandResultStatus_Value) ResultStatus { 837 switch s { 838 case cpb.CommandResultStatus_SUCCESS: 839 return SuccessResultStatus 840 case cpb.CommandResultStatus_CACHE_HIT: 841 return CacheHitResultStatus 842 case cpb.CommandResultStatus_NON_ZERO_EXIT: 843 return NonZeroExitResultStatus 844 case cpb.CommandResultStatus_TIMEOUT: 845 return TimeoutResultStatus 846 case cpb.CommandResultStatus_INTERRUPTED: 847 return InterruptedResultStatus 848 case cpb.CommandResultStatus_REMOTE_ERROR: 849 return RemoteErrorResultStatus 850 case cpb.CommandResultStatus_LOCAL_ERROR: 851 return LocalErrorResultStatus 852 default: 853 return UnspecifiedResultStatus 854 } 855 } 856 857 // ToProto serializes a Command struct into a proto message. 858 func ToProto(cmd *Command) *cpb.Command { 859 if cmd == nil { 860 return nil 861 } 862 cPb := &cpb.Command{ 863 ExecRoot: cmd.ExecRoot, 864 Input: inputSpecToProto(cmd.InputSpec), 865 Output: &cpb.OutputSpec{OutputFiles: cmd.OutputFiles, OutputDirectories: cmd.OutputDirs}, 866 Args: cmd.Args, 867 ExecutionTimeout: int32(cmd.Timeout.Seconds()), 868 WorkingDirectory: cmd.WorkingDir, 869 RemoteWorkingDirectory: cmd.RemoteWorkingDir, 870 Platform: cmd.Platform, 871 } 872 if cmd.Identifiers != nil { 873 cPb.Identifiers = &cpb.Identifiers{ 874 CommandId: cmd.Identifiers.CommandID, 875 InvocationId: cmd.Identifiers.InvocationID, 876 ToolName: cmd.Identifiers.ToolName, 877 ExecutionId: cmd.Identifiers.ExecutionID, 878 } 879 } 880 return cPb 881 } 882 883 // ResultToProto serializes a command.Result struct into a proto message. 884 func ResultToProto(res *Result) *cpb.CommandResult { 885 if res == nil { 886 return nil 887 } 888 resPb := &cpb.CommandResult{ 889 Status: protoStatusFromResultStatus(res.Status), 890 ExitCode: int32(res.ExitCode), 891 } 892 if res.Err != nil { 893 resPb.Msg = res.Err.Error() 894 } 895 return resPb 896 } 897 898 // ResultFromProto parses a command.Result struct from a proto message. 899 func ResultFromProto(res *cpb.CommandResult) *Result { 900 if res == nil { 901 return nil 902 } 903 var err error 904 if res.Msg != "" { 905 err = errors.New(res.Msg) 906 } 907 return &Result{ 908 Status: protoStatusToResultStatus(res.Status), 909 ExitCode: int(res.ExitCode), 910 Err: err, 911 } 912 } 913 914 // TimeToProto converts a valid time.Time into a proto Timestamp. 915 func TimeToProto(t time.Time) *tspb.Timestamp { 916 if t.IsZero() { 917 return nil 918 } 919 return tspb.New(t) 920 } 921 922 // TimeFromProto converts a valid Timestamp proto into a time.Time. 923 func TimeFromProto(tPb *tspb.Timestamp) time.Time { 924 if tPb == nil { 925 return time.Time{} 926 } 927 return tPb.AsTime() 928 } 929 930 // TimeIntervalToProto serializes the SDK TimeInterval into a proto. 931 func TimeIntervalToProto(t *TimeInterval) *cpb.TimeInterval { 932 if t == nil { 933 return nil 934 } 935 return &cpb.TimeInterval{ 936 From: TimeToProto(t.From), 937 To: TimeToProto(t.To), 938 } 939 } 940 941 // TimeIntervalFromProto parses the SDK TimeInterval from a proto. 942 func TimeIntervalFromProto(t *cpb.TimeInterval) *TimeInterval { 943 if t == nil { 944 return nil 945 } 946 return &TimeInterval{ 947 From: TimeFromProto(t.From), 948 To: TimeFromProto(t.To), 949 } 950 }