go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/luci/lib/swarming.star (about)

     1  # Copyright 2019 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  """Swarming related supporting structs and functions."""
    16  
    17  load("@stdlib//internal/validate.star", "validate")
    18  load("@stdlib//internal/time.star", "time")
    19  
    20  # A struct returned by swarming.cache(...).
    21  #
    22  # See swarming.cache(...) function for all details.
    23  #
    24  # Fields:
    25  #   path: string, where to mount the cache.
    26  #   name: string, name of the cache to mount.
    27  #   wait_for_warm_cache: duration or None, how long to wait for a warm cache.
    28  _cache_ctor = __native__.genstruct("swarming.cache")
    29  
    30  # A struct returned by swarming.dimension(...).
    31  #
    32  # See swarming.dimension(...) function for all details.
    33  #
    34  # Fields:
    35  #   value: string, value of the dimension.
    36  #   expiration: duration or None, when the dimension expires.
    37  _dimension_ctor = __native__.genstruct("swarming.dimension")
    38  
    39  def _cache(path, *, name = None, wait_for_warm_cache = None):
    40      """Represents a request for the bot to mount a named cache to a path.
    41  
    42      Each bot has a LRU of named caches: think of them as local named directories
    43      in some protected place that survive between builds.
    44  
    45      A build can request one or more such caches to be mounted (in read/write
    46      mode) at the requested path relative to some known root. In recipes-based
    47      builds, the path is relative to `api.paths['cache']` dir.
    48  
    49      If it's the first time a cache is mounted on this particular bot, it will
    50      appear as an empty directory. Otherwise it will contain whatever was left
    51      there by the previous build that mounted exact same named cache on this bot,
    52      even if that build is completely irrelevant to the current build and just
    53      happened to use the same named cache (sometimes this is useful to share
    54      state between different builders).
    55  
    56      At the end of the build the cache directory is unmounted. If at that time
    57      the bot is running out of space, caches (in their entirety, the named cache
    58      directory and all files inside) are evicted in LRU manner until there's
    59      enough free disk space left. Renaming a cache is equivalent to clearing it
    60      from the builder perspective. The files will still be there, but eventually
    61      will be purged by GC.
    62  
    63      Additionally, Buildbucket always implicitly requests to mount a special
    64      builder cache to 'builder' path:
    65  
    66          swarming.cache('builder', name=some_hash('<project>/<bucket>/<builder>'))
    67  
    68      This means that any LUCI builder has a "personal disk space" on the bot.
    69      Builder cache is often a good start before customizing caching. In recipes,
    70      it is available at `api.path['cache'].join('builder')`.
    71  
    72      In order to share the builder cache directory among multiple builders, some
    73      explicitly named cache can be mounted to `builder` path on these builders.
    74      Buildbucket will not try to override it with its auto-generated builder
    75      cache.
    76  
    77      For example, if builders **A** and **B** both declare they use named cache
    78      `swarming.cache('builder', name='my_shared_cache')`, and an **A** build ran
    79      on a bot and left some files in the builder cache, then when a **B** build
    80      runs on the same bot, the same files will be available in its builder cache.
    81  
    82      If the pool of swarming bots is shared among multiple LUCI projects and
    83      projects mount same named cache, the cache will be shared across projects.
    84      To avoid affecting and being affected by other projects, prefix the cache
    85      name with something project-specific, e.g. `v8-`.
    86  
    87      Args:
    88        path: path where the cache should be mounted to, relative to some known
    89          root (in recipes this root is `api.path['cache']`). Must use POSIX
    90          format (forward slashes). In most cases, it does not need slashes at
    91          all. Must be unique in the given builder definition (cannot mount
    92          multiple caches to the same path). Required.
    93        name: identifier of the cache to mount to the path. Default is same value
    94          as `path` itself. Must be unique in the given builder definition (cannot
    95          mount the same cache to multiple paths).
    96        wait_for_warm_cache: how long to wait (with minutes precision) for a bot
    97          that has this named cache already to become available and pick up the
    98          build, before giving up and starting looking for any matching bot
    99          (regardless whether it has the cache or not). If there are no bots with
   100          this cache at all, the build will skip waiting and will immediately
   101          fallback to any matching bot. By default (if unset or zero), there'll be
   102          no attempt to find a bot with this cache already warm: the build may or
   103          may not end up on a warm bot, there's no guarantee one way or another.
   104  
   105      Returns:
   106        swarming.cache struct with fields `path`, `name` and `wait_for_warm_cache`.
   107      """
   108      path = validate.string("path", path)
   109      name = validate.string("name", name, default = path, required = False)
   110      return _cache_ctor(
   111          path = path,
   112          name = name,
   113          wait_for_warm_cache = validate.duration(
   114              "wait_for_warm_cache",
   115              wait_for_warm_cache,
   116              precision = time.minute,
   117              min = time.minute,
   118              required = False,
   119          ),
   120      )
   121  
   122  def _dimension(value, *, expiration = None):
   123      """A value of some Swarming dimension, annotated with its expiration time.
   124  
   125      Intended to be used as a value in `dimensions` dict of luci.builder(...)
   126      when using dimensions that expire:
   127  
   128      ```python
   129      luci.builder(
   130          ...
   131          dimensions = {
   132              ...
   133              'device': swarming.dimension('preferred', expiration=5*time.minute),
   134              ...
   135          },
   136          ...
   137      )
   138      ```
   139  
   140      Args:
   141        value: string value of the dimension. Required.
   142        expiration: how long to wait (with minutes precision) for a bot with this
   143          dimension to become available and pick up the build, or None to wait
   144          until the overall build expiration timeout.
   145  
   146      Returns:
   147        swarming.dimension struct with fields `value` and `expiration`.
   148      """
   149      return _dimension_ctor(
   150          value = validate.string("value", value),
   151          expiration = validate.duration(
   152              "expiration",
   153              expiration,
   154              precision = time.minute,
   155              min = time.minute,
   156              required = False,
   157          ),
   158      )
   159  
   160  def _validate_caches(attr, caches):
   161      """Validates a list of caches.
   162  
   163      Ensures each entry is swarming.cache struct, and no two entries use same
   164      name or path.
   165  
   166      DocTags:
   167        Advanced.
   168  
   169      Args:
   170        attr: field name with caches, for error messages. Required.
   171        caches: a list of swarming.cache(...) entries to validate. Required.
   172  
   173      Returns:
   174        Validates list of caches (may be an empty list, never None).
   175      """
   176      per_path = {}
   177      per_name = {}
   178      caches = validate.list(attr, caches)
   179      for c in caches:
   180          validate.struct(attr, c, _cache_ctor)
   181          if c.path in per_path:
   182              fail('bad "caches": caches %s and %s use same path' % (c, per_path[c.path]))
   183          if c.name in per_name:
   184              fail('bad "caches": caches %s and %s use same name' % (c, per_name[c.name]))
   185          per_path[c.path] = c
   186          per_name[c.name] = c
   187      return caches
   188  
   189  def _validate_dimensions(attr, dimensions, *, allow_none = False):
   190      """Validates and normalizes a dict with dimensions.
   191  
   192      The dict should have string keys and values are swarming.dimension, a string
   193      or a list of thereof (for repeated dimensions).
   194  
   195      DocTags:
   196        Advanced.
   197  
   198      Args:
   199        attr: field name with dimensions, for error messages. Required.
   200        dimensions: a dict `{string: string|swarming.dimension}`. Required.
   201        allow_none: if True, allow None values (indicates absence of the dimension).
   202  
   203      Returns:
   204        Validated and normalized dict in form `{string: [swarming.dimension]}`.
   205      """
   206      out = {}
   207      for k, v in validate.str_dict(attr, dimensions).items():
   208          validate.string(attr, k)
   209          if allow_none and v == None:
   210              out[k] = None
   211          elif type(v) == "list":
   212              out[k] = [_as_dim(k, x) for x in v]
   213          else:
   214              out[k] = [_as_dim(k, v)]
   215      return out
   216  
   217  def _as_dim(key, val):
   218      """string|swarming.dimension -> swarming.dimension."""
   219      if val == None:
   220          fail("bad dimension %r: None value is not allowed" % key)
   221      if type(val) == "string":
   222          return _dimension(val)
   223      return validate.struct(key, val, _dimension_ctor)
   224  
   225  def _validate_tags(attr, tags):
   226      """Validates a list of `k:v` pairs with Swarming tags.
   227  
   228      DocTags:
   229        Advanced.
   230  
   231      Args:
   232        attr: field name with tags, for error messages. Required.
   233        tags: a list of tags to validate. Required.
   234  
   235      Returns:
   236        Validated list of tags in same order, with duplicates removed.
   237      """
   238      out = set()  # note: in starlark sets/dicts remember the order
   239      for t in validate.list(attr, tags):
   240          validate.string(attr, t, regexp = r".+\:.+")
   241          out = out.union([t])
   242      return list(out)
   243  
   244  swarming = struct(
   245      cache = _cache,
   246      dimension = _dimension,
   247  
   248      # Validators are useful for macros that modify caches, dimensions, etc.
   249      validate_caches = _validate_caches,
   250      validate_dimensions = _validate_dimensions,
   251      validate_tags = _validate_tags,
   252  )