go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/model/taskrequest.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package model 16 17 import ( 18 "context" 19 "reflect" 20 "time" 21 22 "go.chromium.org/luci/auth/identity" 23 "go.chromium.org/luci/gae/service/datastore" 24 25 apipb "go.chromium.org/luci/swarming/proto/api_v2" 26 configpb "go.chromium.org/luci/swarming/proto/config" 27 "go.chromium.org/luci/swarming/server/acls" 28 ) 29 30 // TaskRequest contains a user request to execute a task. 31 // 32 // Key ID is a decreasing integer based on time plus some randomness on lower 33 // order bits. See NewTaskRequestID for the complete gory details. 34 // 35 // This entity is immutable. 36 type TaskRequest struct { 37 // Extra are entity properties that didn't match any declared ones below. 38 // 39 // Should normally be empty. 40 Extra datastore.PropertyMap `gae:"-,extra"` 41 42 // Key is derived based on time and randomness. 43 // 44 // It is normally serialized into a hex string. See TaskRequestKey. 45 Key *datastore.Key `gae:"$key"` 46 47 // TxnUUID is used internally to make the transaction that creates TaskRequest 48 // idempotent. 49 // 50 // Just a randomly generated string. Should not be used for anything else. 51 // Should not show up anywhere. 52 TxnUUID string `gae:"txn_uuid,noindex"` 53 54 // TaskSlices defines what to run. 55 // 56 // Each slice defines what to run and where. Slices are attempted one after 57 // another, until some ends up running on a bot. If an attempt to schedule 58 // a particular slice fails (i.e. there are no bots matching requested 59 // dimensions or the slice sits queued for too long and expires), the next 60 // slice is scheduled in its place. 61 // 62 // This is primarily used to requests bots with "hot" caches before falling 63 // back on more generic bots. 64 TaskSlices []TaskSlice `gae:"task_slices,lsp,noindex"` 65 66 // Created is a timestamp when this request was registered. 67 // 68 // The index is used in BQ exports and when cleaning up old tasks. 69 Created time.Time `gae:"created_ts"` 70 71 // Expiration is when to give up trying to run the task. 72 // 73 // If the task request is not scheduled by this moment, it will be aborted 74 // with EXPIRED status. This value always matches Expiration of the last 75 // TaskSlice. 76 // 77 // TODO(vadimsh): Why is it stored separately at all? 78 Expiration time.Time `gae:"expiration_ts,noindex"` 79 80 // Name of this task request as provided by the caller. Only for description. 81 Name string `gae:"name,noindex"` 82 83 // ParentTaskID is set when this task was created from another task. 84 // 85 // This is packed TaskToRun ID of an attempt that launched this task or null 86 // if this task doesn't have a parent. 87 // 88 // The index is used to find children of a particular parent task to cancel 89 // them when the parent task dies. 90 ParentTaskID datastore.Nullable[string, datastore.Indexed] `gae:"parent_task_id"` 91 92 // Authenticated is an identity that triggered this task. 93 // 94 // Derived from the caller credentials. 95 Authenticated identity.Identity `gae:"authenticated,noindex"` 96 97 // What user to "blame" for this task. 98 // 99 // Can be arbitrary, not asserted by any credentials. 100 User string `gae:"user,noindex"` 101 102 // Tags classify this task in some way. 103 // 104 // This is a generated property. This property contains both the tags 105 // specified by the user and the tags from every TaskSlice. 106 Tags []string `gae:"tags,noindex"` 107 108 // ManualTags are tags that are provided by the user. 109 // 110 // This is used to regenerate the list of tags for TaskResultSummary based on 111 // the actual TaskSlice used. 112 ManualTags []string `gae:"manual_tags,noindex"` 113 114 // ServiceAccount indicates what credentials the task uses when calling other 115 // services. 116 // 117 // Possible values are: `none`, `bot` or `<email>`. 118 ServiceAccount string `gae:"service_account,noindex"` 119 120 // Realm is task's realm controlling who can see and cancel this task. 121 // 122 // Missing for internally generated tasks such as termination tasks. 123 Realm string `gae:"realm,noindex"` 124 125 // RealmsEnabled is a legacy flag that should always be True. 126 // 127 // TODO(vadimsh): Get rid of it when Python code is no more. 128 RealmsEnabled bool `gae:"realms_enabled,noindex"` 129 130 // SchedulingAlgorithm is a scheduling algorithm set in pools.cfg at the time 131 // the request was created. 132 // 133 // TODO(vadimsh): This does nothing for RBE pools. 134 SchedulingAlgorithm configpb.Pool_SchedulingAlgorithm `gae:"scheduling_algorithm,noindex"` 135 136 // Priority of the this task. 137 // 138 // A lower number means higher priority. 139 Priority int64 `gae:"priority,noindex"` 140 141 // BotPingToleranceSecs is a maximum delay between bot pings before the bot is 142 // considered dead while running a task. 143 // 144 // TODO(vadimsh): Why do this per-task instead of per-pool or even hardcoded? 145 BotPingToleranceSecs int64 `gae:"bot_ping_tolerance_secs,noindex"` 146 147 // RBEInstance is an RBE instance to send the task to or "" to use Swarming 148 // native scheduler. 149 // 150 // Initialized when creating a task based on pools.cfg config. 151 RBEInstance string `gae:"rbe_instance,noindex"` 152 153 // PubSubTopic is a topic to send a task completion notification to. 154 PubSubTopic string `gae:"pubsub_topic,noindex"` 155 156 // PubSubAuthToken is a secret token to send as `auth_token` PubSub message 157 // attribute. 158 PubSubAuthToken string `gae:"pubsub_auth_token,noindex"` 159 160 // PubSubUserData is data to send in `userdata` field of PubSub messages. 161 PubSubUserData string `gae:"pubsub_userdata,noindex"` 162 163 // ResultDBUpdateToken is the ResultDB invocation's update token for the task 164 // run that was created for this request. 165 // 166 // This is empty if the task was deduplicated or if ResultDB integration was 167 // not enabled for this task. 168 ResultDBUpdateToken string `gae:"resultdb_update_token,noindex"` 169 170 // ResultDB is ResultDB integration configuration for this task. 171 ResultDB ResultDBConfig `gae:"resultdb,lsp,noindex"` 172 173 // HasBuildTask is true if the TaskRequest has an associated BuildTask. 174 HasBuildTask bool `gae:"has_build_task,noindex"` 175 176 // LegacyProperties is no longer used. 177 LegacyProperties LegacyProperty `gae:"properties"` 178 179 // LegacyHasBuildToken is no longer used. 180 LegacyHasBuildToken LegacyProperty `gae:"has_build_token"` 181 } 182 183 // Pool is the pool the task wants to run in. 184 func (p *TaskRequest) Pool() string { 185 pool, ok := p.TaskSlices[0].Properties.Dimensions["pool"] 186 if !ok || len(pool) == 0 { 187 return "" 188 } 189 return pool[0] 190 } 191 192 // BotID is a specific bot the task wants to run on, if any. 193 func (p *TaskRequest) BotID() string { 194 botID, ok := p.TaskSlices[0].Properties.Dimensions["id"] 195 if !ok || len(botID) == 0 { 196 return "" 197 } 198 return botID[0] 199 } 200 201 // TaskAuthInfo is information about the task for ACL checks. 202 func (p *TaskRequest) TaskAuthInfo() acls.TaskAuthInfo { 203 return acls.TaskAuthInfo{ 204 TaskID: RequestKeyToTaskID(p.Key, AsRequest), 205 Realm: p.Realm, 206 Pool: p.Pool(), 207 BotID: p.BotID(), 208 Submitter: p.Authenticated, 209 } 210 } 211 212 // TaskSlice defines where and how to run the task and when to give up. 213 // 214 // The task will fallback from one slice to the next until it finds a matching 215 // bot. 216 // 217 // This entity is not saved in the DB as a standalone entity, instead it is 218 // embedded in a TaskRequest, unindexed. 219 // 220 // This entity is immutable. 221 type TaskSlice struct { 222 // Extra are entity properties that didn't match any declared ones below. 223 // 224 // Should normally be empty. 225 Extra datastore.PropertyMap `gae:"-,extra"` 226 227 // Properties defines where and how to run the task. 228 // 229 // If a task is marked as an idempotent (see TaskProperties), property values 230 // are hashed in a reproducible way and the final hash is used to find a 231 // previously succeeded task that has the same properties. 232 Properties TaskProperties `gae:"properties,lsp"` 233 234 // ExpirationSecs defines how long the slice can sit in a pending queue. 235 // 236 // If this task slice is not scheduled by this moment, the next one will be 237 // enqueued instead. 238 ExpirationSecs int64 `gae:"expiration_secs"` 239 240 // WaitForCapacity is a legacy flag that does nothing, always false now. 241 // 242 // TODO(vadimsh): Remove it when no longer referenced 243 WaitForCapacity bool `gae:"wait_for_capacity"` 244 } 245 246 // ToProto returns an apipb.TaskSlice version of the TaskSlice. 247 func (p *TaskSlice) ToProto() *apipb.TaskSlice { 248 ts := &apipb.TaskSlice{ 249 ExpirationSecs: int32(p.ExpirationSecs), 250 WaitForCapacity: p.WaitForCapacity, 251 } 252 if properties := p.Properties.ToProto(); properties != nil { 253 ts.Properties = properties 254 } 255 return ts 256 } 257 258 // TaskProperties defines where and how to run the task. 259 // 260 // This entity is not saved in the DB as a standalone entity, instead it is 261 // embedded in a TaskSlice, unindexed. 262 // 263 // This entity is immutable. 264 type TaskProperties struct { 265 // Extra are entity properties that didn't match any declared ones below. 266 // 267 // Should normally be empty. 268 Extra datastore.PropertyMap `gae:"-,extra"` 269 270 // Idempotent, if true, means it is OK to skip running this task if there's 271 // already a successful task with the same properties hash. 272 // 273 // The results of such previous task will be reused as results of this task. 274 Idempotent bool `gae:"idempotent"` 275 276 // Dimensions are used to match this task to a bot. 277 // 278 // This is conceptually a set of `(key, value1 | value2 | ...)` pairs, each 279 // defining some constraint on a matching bot. For a bot to match the task, 280 // it should satisfy all constraints. 281 // 282 // For a bot to match a single `(key, value1 | value2| ...)` constraint, bot's 283 // value for dimension `key` should be equal to `value1` or `value2` and so 284 // on. 285 Dimensions TaskDimensions `gae:"dimensions"` 286 287 // ExecutionTimeoutSecs is the maximum duration the bot can take to run this 288 // task. 289 // 290 // It's also known as `hard_timeout` in the bot code. 291 ExecutionTimeoutSecs int64 `gae:"execution_timeout_secs"` 292 293 // GracePeriodSecs is the time between sending SIGTERM and SIGKILL when the 294 // task times out. 295 // 296 // As soon as the ask reaches its execution timeout, the task process is sent 297 // SIGTERM. The process should clean up and terminate. If it is still running 298 // after GracePeriodSecs, it gets killed via SIGKILL. 299 GracePeriodSecs int64 `gae:"grace_period_secs"` 300 301 // IOTimeoutSecs controls how soon to consider a "silent" process to be stuck. 302 // 303 // If a subprocess doesn't output new data to stdout for IOTimeoutSecs, 304 // consider the task timed out. Optional. 305 IOTimeoutSecs int64 `gae:"io_timeout_secs"` 306 307 // Command is a command line to run. 308 Command []string `gae:"command"` 309 310 // RelativeCwd is a working directory relative to the task root to run 311 // the command in. 312 RelativeCwd string `gae:"relative_cwd"` 313 314 // Env is environment variables to set when running the task process. 315 Env Env `gae:"env"` 316 317 // EnvPrefixes is environment path prefix variables. 318 // 319 // E.g. if a `PATH` key has values `[a, b]`, then the final `PATH` env var 320 // will be `a;b;$PATH` (where `;` is a platforms' env path separator). 321 EnvPrefixes EnvPrefixes `gae:"env_prefixes"` 322 323 // Caches defines what named caches to mount. 324 Caches []CacheEntry `gae:"caches,lsp"` 325 326 // CASInputRoot is a digest of the input root uploaded to RBE-CAS. 327 // 328 // This MUST be digest of `build.bazel.remote.execution.v2.Directory`. 329 CASInputRoot CASReference `gae:"cas_input_root,lsp"` 330 331 // CIPDInput defines what CIPD packages to install. 332 CIPDInput CIPDInput `gae:"cipd_input,lsp"` 333 334 // Outputs is a list of extra outputs to upload to RBE-CAS as task results. 335 // 336 // If empty, only files written to `${ISOLATED_OUTDIR}` will be returned. 337 // Otherwise, the files in this list will be added to those in that directory. 338 Outputs []string `gae:"outputs"` 339 340 // HasSecretBytes, if true, means there's a SecretBytes entity associated with 341 // the parent TaskRequest. 342 HasSecretBytes bool `gae:"has_secret_bytes"` 343 344 // Containment defines what task process containment mechanism to use. 345 // 346 // Not really implemented currently. 347 Containment Containment `gae:"containment,lsp"` 348 349 // LegacyInputsRef is no longer used. 350 LegacyInputsRef LegacyProperty `gae:"inputs_ref"` 351 } 352 353 // ToProto converts TaskProperties to apipb.TaskProperties. 354 func (p *TaskProperties) ToProto() *apipb.TaskProperties { 355 if reflect.DeepEqual(*p, TaskProperties{}) { 356 return nil 357 } 358 caches := make([]*apipb.CacheEntry, len(p.Caches)) 359 for i, cache := range p.Caches { 360 caches[i] = cache.ToProto() 361 } 362 taskProperties := &apipb.TaskProperties{ 363 Caches: caches, 364 CipdInput: p.CIPDInput.ToProto(), 365 Command: p.Command, 366 RelativeCwd: p.RelativeCwd, 367 Dimensions: p.Dimensions.ToProto(), 368 Env: p.Env.ToProto(), 369 EnvPrefixes: p.EnvPrefixes.ToProto(), 370 ExecutionTimeoutSecs: int32(p.ExecutionTimeoutSecs), 371 GracePeriodSecs: int32(p.GracePeriodSecs), 372 Idempotent: p.Idempotent, 373 CasInputRoot: p.CASInputRoot.ToProto(), 374 IoTimeoutSecs: int32(p.IOTimeoutSecs), 375 Outputs: p.Outputs, 376 Containment: p.Containment.ToProto(), 377 } 378 if p.HasSecretBytes { 379 taskProperties.SecretBytes = []byte("<REDACTED>") 380 } 381 return taskProperties 382 } 383 384 // CacheEntry describes a named cache that should be present on the bot. 385 type CacheEntry struct { 386 // Name is a logical cache name. 387 Name string `gae:"name"` 388 // Path is where to mount it relative to the task root directory. 389 Path string `gae:"path"` 390 } 391 392 // ToProto converts CacheEntry to apipb.CacheEntry. 393 func (p *CacheEntry) ToProto() *apipb.CacheEntry { 394 if p.Name == "" && p.Path == "" { 395 return nil 396 } 397 return &apipb.CacheEntry{ 398 Name: p.Name, 399 Path: p.Path, 400 } 401 } 402 403 // CASReference described where to fetch input files from. 404 type CASReference struct { 405 // CASInstance is a full name of RBE-CAS instance. 406 CASInstance string `gae:"cas_instance"` 407 // Digest identifies the root tree to fetch. 408 Digest CASDigest `gae:"digest,lsp"` 409 } 410 411 // ToProto converts CASReference to apipb.CASReference. 412 func (p *CASReference) ToProto() *apipb.CASReference { 413 if p.CASInstance == "" && p.Digest.Hash == "" && p.Digest.SizeBytes == 0 { 414 return nil 415 } 416 return &apipb.CASReference{ 417 CasInstance: p.CASInstance, 418 Digest: p.Digest.ToProto(), 419 } 420 } 421 422 // CASDigest represents an RBE-CAS blob's digest. 423 // 424 // Is is a representation of build.bazel.remote.execution.v2.Digest. See 425 // https://github.com/bazelbuild/remote-apis/blob/77cfb44a88577a7ade5dd2400425f6d50469ec6d/build/bazel/remote/execution/v2/remote_execution.proto#L753-L791 426 type CASDigest struct { 427 // Hash is blob's hash digest as a hex string. 428 Hash string `gae:"hash"` 429 // SizeBytes is the blob size. 430 SizeBytes int64 `gae:"size_bytes"` 431 } 432 433 // ToProto converts CASDigest to apipb.CASDigest. 434 func (p *CASDigest) ToProto() *apipb.Digest { 435 if p.Hash == "" { 436 return nil 437 } 438 return &apipb.Digest{ 439 Hash: p.Hash, 440 SizeBytes: p.SizeBytes, 441 } 442 } 443 444 // CIPDInput specifies which CIPD client and packages to install. 445 type CIPDInput struct { 446 // Server is URL of the CIPD server (including "https://" schema). 447 Server string `gae:"server"` 448 // ClientPackage defines a version of the CIPD client to use. 449 ClientPackage CIPDPackage `gae:"client_package,lsp"` 450 // Packages is a list of packages to install. 451 Packages []CIPDPackage `gae:"packages,lsp"` 452 } 453 454 // ToProto converts CIPDInput to apipb.CIPDInput. 455 func (p *CIPDInput) ToProto() *apipb.CipdInput { 456 if len(p.Packages) == 0 && p.ClientPackage.PackageName == "" { 457 return nil 458 } 459 packages := make([]*apipb.CipdPackage, len(p.Packages)) 460 for i, pkg := range p.Packages { 461 packages[i] = pkg.ToProto() 462 } 463 return &apipb.CipdInput{ 464 Server: p.Server, 465 ClientPackage: p.ClientPackage.ToProto(), 466 Packages: packages, 467 } 468 } 469 470 // CIPDPackage defines a CIPD package to install into the task directory. 471 type CIPDPackage struct { 472 // PackageName is a package name template (e.g. may include `${platform}`). 473 PackageName string `gae:"package_name"` 474 // Version is a package version to install. 475 Version string `gae:"version"` 476 // Path is a path relative to the task directory where to install the package. 477 Path string `gae:"path"` 478 } 479 480 // ToProto converts CIPDPackage to apipb.CipdPackage. 481 func (p *CIPDPackage) ToProto() *apipb.CipdPackage { 482 if p.PackageName == "" { 483 return nil 484 } 485 return &apipb.CipdPackage{ 486 PackageName: p.PackageName, 487 Version: p.Version, 488 Path: p.Path, 489 } 490 } 491 492 // Containment describes the task process containment. 493 type Containment struct { 494 LowerPriority bool `gae:"lower_priority"` 495 ContainmentType apipb.ContainmentType `gae:"containment_type"` 496 LimitProcesses int64 `gae:"limit_processes"` 497 LimitTotalCommittedMemory int64 `gae:"limit_total_committed_memory"` 498 } 499 500 // ToProto converts Containment struct to apipb.Containment 501 func (p *Containment) ToProto() *apipb.Containment { 502 if p.ContainmentType == 0 { 503 return nil 504 } 505 return &apipb.Containment{ 506 ContainmentType: p.ContainmentType, 507 } 508 } 509 510 // ResultDBConfig is ResultDB integration configuration for a task. 511 type ResultDBConfig struct { 512 // Enable indicates if the task should have ResultDB invocation. 513 // 514 // If True and this task is not deduplicated, create 515 // "task-{swarming_hostname}-{run_id}" invocation for this task, provide its 516 // update token to the task subprocess via LUCI_CONTEXT and finalize the 517 // invocation when the task is done. 518 // 519 // If the task is deduplicated, then TaskResult.InvocationName will be the 520 // invocation name of the original task. 521 Enable bool `gae:"enable"` 522 } 523 524 // ToProto converts ResultDBConfig struct to apipb.ResultDBCfg. 525 func (p *ResultDBConfig) ToProto() *apipb.ResultDBCfg { 526 return &apipb.ResultDBCfg{ 527 Enable: p.Enable, 528 } 529 } 530 531 // SecretBytes defines an optional secret byte string logically defined within 532 // TaskRequest. 533 // 534 // Stored separately for size and data-leakage reasons. All task slices reuse 535 // the same secret bytes (which is an implementation artifact, not a desired 536 // property). If a task slice uses secret bytes, it has HasSecretBytes == true. 537 type SecretBytes struct { 538 // Extra are entity properties that didn't match any declared ones below. 539 // 540 // Should normally be empty. 541 Extra datastore.PropertyMap `gae:"-,extra"` 542 543 // Key identifies the task and its concrete slice. 544 // 545 // See SecretBytesKey. 546 Key *datastore.Key `gae:"$key"` 547 548 // SecretBytes is the actual secret bytes blob. 549 SecretBytes []byte `gae:"secret_bytes,noindex"` 550 } 551 552 // SecretBytesKey constructs SecretBytes key given a TaskRequest key. 553 func SecretBytesKey(ctx context.Context, taskReq *datastore.Key) *datastore.Key { 554 return datastore.NewKey(ctx, "SecretBytes", "", 1, taskReq) 555 } 556 557 // TaskRequestID defines a mapping between request's idempotency ID and task ID. 558 // 559 // It is a root-level entity. Used to make sure at most one TaskRequest entity 560 // is created per the given request ID. 561 type TaskRequestID struct { 562 // Extra are entity properties that didn't match any declared ones below. 563 // 564 // Should normally be empty. 565 Extra datastore.PropertyMap `gae:"-,extra"` 566 567 // Key is derived from the request ID. 568 // 569 // See TaskRequestIDKey. 570 Key *datastore.Key `gae:"$key"` 571 572 // TaskID is a packed TaskResultSummary key identifying TaskRequest matching 573 // this request ID. 574 // 575 // Use TaskRequestKey(...) to get the actual datastore key from it. 576 TaskID string `gae:"task_id,noindex"` 577 578 // ExpireAt is when this entity should be removed from the datastore. 579 // 580 // This is used by a TTL policy: https://cloud.google.com/datastore/docs/ttl 581 ExpireAt time.Time `gae:"expire_at,noindex"` 582 } 583 584 // TaskRequestIDKey constructs a top-level TaskRequestID key. 585 func TaskRequestIDKey(ctx context.Context, requestID string) *datastore.Key { 586 return datastore.NewKey(ctx, "TaskRequestID", requestID, 0, nil) 587 } 588 589 // TaskDimensions defines requirements for a bot to match a task. 590 // 591 // Stored in JSON form in the datastore. 592 type TaskDimensions map[string][]string 593 594 // ToProperty stores the value as a JSON-blob property. 595 func (p *TaskDimensions) ToProperty() (datastore.Property, error) { 596 return ToJSONProperty(p) 597 } 598 599 // FromProperty loads a JSON-blob property. 600 func (p *TaskDimensions) FromProperty(prop datastore.Property) error { 601 return FromJSONProperty(prop, p) 602 } 603 604 // ToProto converts TaskDimensions to []*apipb.StringPair 605 func (p *TaskDimensions) ToProto() []*apipb.StringPair { 606 if len(*p) == 0 { 607 return nil 608 } 609 td := []*apipb.StringPair{} 610 for k, v := range *p { 611 for _, val := range v { 612 td = append(td, &apipb.StringPair{ 613 Key: k, 614 Value: val, 615 }) 616 } 617 } 618 SortStringPairs(td) 619 return td 620 } 621 622 // Env is a list of `(key, value)` pairs with environment variables to set. 623 // 624 // Stored in JSON form in the datastore. 625 type Env map[string]string 626 627 // ToProperty stores the value as a JSON-blob property. 628 func (p *Env) ToProperty() (datastore.Property, error) { 629 return ToJSONProperty(p) 630 } 631 632 // FromProperty loads a JSON-blob property. 633 func (p *Env) FromProperty(prop datastore.Property) error { 634 return FromJSONProperty(prop, p) 635 } 636 637 // ToProto converts Env to []*apipb.StringPair 638 func (p *Env) ToProto() []*apipb.StringPair { 639 if len(*p) == 0 { 640 return nil 641 } 642 sp := []*apipb.StringPair{} 643 for k, v := range *p { 644 sp = append(sp, &apipb.StringPair{ 645 Key: k, 646 Value: v, 647 }) 648 } 649 SortStringPairs(sp) 650 return sp 651 } 652 653 // EnvPrefixes is a list of `(key, []value)` pairs with env prefixes to add. 654 // 655 // Stored in JSON form in the datastore. 656 type EnvPrefixes map[string][]string 657 658 // ToProperty stores the value as a JSON-blob property. 659 func (p *EnvPrefixes) ToProperty() (datastore.Property, error) { 660 return ToJSONProperty(p) 661 } 662 663 // FromProperty loads a JSON-blob property. 664 func (p *EnvPrefixes) FromProperty(prop datastore.Property) error { 665 return FromJSONProperty(prop, p) 666 } 667 668 // ToProto converts EnvPrefixes to []*apipb.StringListPair 669 func (p EnvPrefixes) ToProto() []*apipb.StringListPair { 670 return MapToStringListPair((map[string][]string)(p), true) 671 } 672 673 // NewTaskRequestID generates an ID for a new task. 674 func NewTaskRequestID(ctx context.Context) int64 { 675 panic("not implemented") 676 }