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  }