LUCI configuration definition language

Overview

Working with lucicfg

TODO: To be written.

Concepts

TODO: To be written.

Resolving naming ambiguities

TODO: To be written.

Defining durations {#durations_doc}

TODO: To be written.

Defining cron schedules {#schedules_doc}

TODO: To be written.

Core rules

core.generator

core.generator(impl = None)

Registers a callback that is called at the end of the config generation stage to modify/append/delete generated configs in an arbitrary way.

The callback accepts single argument ‘ctx’ which is a struct with the following fields: ‘config_set’: a dict {config file name -> (str | proto)}.

The callback is free to modify ctx.config_set in whatever way it wants, e.g. by adding new values there or mutating/deleting existing ones.

Arguments

  • impl: a callback func(ctx) -> None.

core.bucket

core.bucket(name, acls = None)

Defines a bucket: a container for LUCI resources that share the same ACL.

Arguments

  • name: name of the bucket, e.g. ‘ci’ or ‘try’. Required.
  • acls: list of acl.entry objects.

core.builder

core.builder(
    # Required arguments.
    name,
    bucket,
    recipe,

    # Optional arguments.
    properties = None,
    service_account = None,
    caches = None,
    execution_timeout = None,
    dimensions = None,
    priority = None,
    swarming_tags = None,
    expiration_timeout = None,
    build_numbers = None,
    experimental = None,
    task_template_canary_percentage = None,
    luci_migration_host = None,
    triggers = None,
    triggered_by = None,
)

Defines a generic builder.

It runs some recipe in some requested environment, passing it a struct with given properties. It is launched whenever something triggers it (a poller or some other builder, or maybe some external actor via Buildbucket or LUCI Scheduler APIs).

The full unique builder name (as expected by Buildbucket RPC interface) is a pair (“”, “/”), but within a single project config this builder can be referred to either via its bucket-scoped name (i.e. “/”) or just via it‘s name alone (i.e. “”), if this doesn’t introduce ambiguities.

The definition of what can potentially trigger what is defined through ‘triggers’ and ‘triggered_by’ fields. They specify how to prepare ACLs and other configuration of services that execute builds.

If builder A is defined as “triggers builder B”, it means all services should expect A builds to trigger B builds via LUCI Scheduler‘s EmitTriggers RPC or via Buildbucket’s ScheduleBuild RPC, but the actual triggering is still the responsibility of A's recipe.

There‘s a caveat though: only Scheduler ACLs are auto-generated by the config generator when one builder triggers another, because each Scheduler job has its own ACL and we can precisely configure who’s allowed to trigger this job.

Buildbucket ACLs are left unchanged though, since they apply to an entire bucket, and making a large scale change like that (without really knowing whether Buildbucket API will be used) is dangerous.

So if the recipe triggers other builds directly through Buildbucket, it is the responsibility of the config author (you) to correctly specify Buildbucket ACLs, e.g. by adding the corresponding service account to the bucket ACLs:

core.bucket(
    ...
    acls = [
        ...
        acl.entry(acl.BUILDBUCKET_TRIGGERER, <builder service account>),
        ...
    ],
)

This is not necessary if the recipe uses Scheduler API instead of Buildbucket.

Arguments

  • name: name of the builder, will show up in UIs and logs. Required.
  • bucket: name of the bucket the builder belongs to. Required.
  • recipe: name of a recipe to run, see core.recipe(...) rule. Required.
  • properties: a dict with string keys and JSON-serializable values, defining properties to pass to the recipe.
  • service_account: an email of a service account to run the recipe under: the recipe (and various tools it calls, e.g. gsutil) will be able to make outbound HTTP calls that have an OAuth access token belonging to this service account (provided it is registered with LUCI).
  • caches: a list of swarming.cache(...) objects describing Swarming named caches that should be present on the bot. See swarming.cache(...) doc for more details.
  • execution_timeout: how long to wait for a running build to finish before forcefully aborting it and marking the build as timed out. If None, defer the decision to Buildbucket service.
  • dimensions: a dict with swarming dimensions, indicating requirements for a bot to execute the build. Keys are strings (e.g. ‘os’), and values are either strings (e.g. ‘Linux’), swarming.dimension(...) objects (for defining expiring dimensions) or lists of thereof.
  • priority: int [1-255] or None, indicating swarming task priority, lower is more important. If None, defer the decision to BuildBucket service.
  • swarming_tags: a list of tags (“k:v” strings) to assign to the Swarming task that runs the builder. Each tag will also end up in “swarming_tag” Buildbucket tag, for example “swarming_tag:builder:release”.
  • expiration_timeout: how long to wait for a build to be picked up by a matching (based on ‘dimensions’) bot before canceling it and marking as expired. If None, defer the decision to Buildbucket service.
  • build_numbers: if True, generate monotonically increasing contiguous numbers for each build, unique within the builder. If None, defer the decision to Buildbucket service.
  • experimental: if True, by default a new build in this builder will be marked as experimental. This is seen from recipes and they may behave differently (e.g. avoiding any side-effects). If None, defer the decision to Buildbucket service.
  • task_template_canary_percentage: int [0-100] or None, indicating percentage of builds that should use a canary swarming task template. If None, defer the decision to Buildbucket service.
  • luci_migration_host: deprecated setting that was important during the migration from Buildbot to LUCI. Refer to Buildbucket docs for the meaning.
  • triggers: names of builders this builder triggers.
  • triggered_by: names of builders or pollers this builder is triggered by.

core.gitiles_poller {#core.gitiles_poller}

core.gitiles_poller(
    # Required arguments.
    name,
    bucket,
    repo,

    # Optional arguments.
    refs = None,
    refs_regexps = None,
    schedule = None,
    triggers = None,
)

Defines a gitiles poller which can trigger builders on git commits.

It watches a set of git refs and triggers builders if either: * A watched ref's tip has changed (e.g. a new commit landed on a ref). * A ref belonging to the watched set has just been created.

The watched ref set is defined via ‘refs’ and ‘refs_regexps’ fields. One is just a simple enumeration of refs, and another allows to use regular expressions to define what refs belong to the watched set. Both fields can be used at the same time. If neither is set, the gitiles_poller defaults to watching “refs/heads/master”.

Arguments {#core.gitiles_poller-args}

  • name: name of the poller, to refer to it from other rules. Required.
  • bucket: name of the bucket the poller belongs to. Required.
  • repo: URL of a git repository to poll, starting with https://. Required.
  • refs: a list of fully qualified refs to watch, e.g. “refs/heads/master” or “refs/tags/v1.2.3”.
  • refs_regexps: a list of regular expressions that define the watched set of refs, e.g. “refs/heads/[^/]+” or “refs/branch-heads/\d+.\d+”. The regular expression should have a literal prefix with at least two slashes present, e.g. “refs/release-\d+/foobar” is not allowed, because the literal prefix “refs/release-” contains only one slash. The regexp should not start with ^ or end with $ as they will be added automatically.
  • schedule: string with a schedule that describes when to run one iteration of the poller. It is rare to use custom schedules for pollers. By default, the poller will run each 30 sec.
  • triggers: names of builders to trigger whenever the poller detects a new git commit on any ref in the watched ref set.

core.logdog

core.logdog(gs_bucket = None)

Configuration for the LogDog service.

Arguments

  • gs_bucket: base Google Storage archival path, archive logs will be written to this bucket/path.

core.project

core.project(
    # Required arguments.
    name,

    # Optional arguments.
    buildbucket = None,
    logdog = None,
    scheduler = None,
    swarming = None,
    acls = None,
)

Defines a LUCI project.

There should be exactly one such definition in a single top-level config file.

Arguments

  • name: full name of the project. Required.
  • buildbucket: hostname of a Buildbucket service to use (if any).
  • logdog: hostname of a LogDog service to use (if any).
  • scheduler: hostname of a LUCI Scheduler service to use (if any).
  • swarming: hostname of a Swarming service to use (if any).
  • acls: list of acl.entry objects, will be inherited by all buckets.

core.recipe

core.recipe(
    # Required arguments.
    name,
    cipd_package,

    # Optional arguments.
    cipd_version = None,
    recipe = None,
)

Defines where to locate a particular recipe.

Builders refer to recipes in their ‘recipe’ field. Multiple builders can execute the same recipe (perhaps passing different properties to it).

Recipes are located inside cipd packages called “recipe bundles”. Typically the cipd package name with the recipe bundle will look like:

infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build

Recipes bundled from internal repositories are typically under

infra_internal/recipe_bundles/...

But if you're building your own recipe bundles, they could be located elsewhere.

The cipd version to fetch is usually a lower-cased git ref (like ‘refs/heads/master’), or it can be a cipd tag (like ‘git_revision:abc...’).

Arguments

  • name: name of this recipe entity, to refer to it from builders. If ‘recipe’ is None, also specifies the recipe name within the bundle. Required.
  • cipd_package: a cipd package name with the recipe bundle. Required.
  • cipd_version: a version of the recipe bundle package to fetch, default is ‘refs/heads/master’.
  • recipe: name of a recipe inside the recipe bundle if it differs from ‘name’. Useful if recipe names clash between different recipe bundles. When this happens, ‘name’ can be used as a non-ambiguous alias, and ‘recipe’ can provide the actual recipe name. Defaults to ‘name’.

ACLs

Roles {#roles_doc}

Below is the table with role constants that can be passed as roles in acl.entry(...).

Due to some inconsistencies in how LUCI service are currently implemented, some roles can be assigned only in core.project(...) rule, but some also in individual core.bucket(...) rules.

Similarly some roles can be assigned to individual users, other only to groups.

RoleScopePrincipalsAllows
acl.PROJECT_CONFIGS_READERproject onlygroups, usersReading contents of project configs through LUCI Config API/UI.
acl.LOGDOG_READERproject onlygroupsReading logs under project's logdog prefix.
acl.LOGDOG_WRITERproject onlygroupsWriting logs under project's logdog prefix.
acl.BUILDBUCKET_READERproject, bucketgroups, usersFetching info about a build, searching for builds in a bucket.
acl.BUILDBUCKET_TRIGGERERproject, bucketgroups, usersSame as BUILDBUCKET_READER + scheduling and canceling builds.
acl.BUILDBUCKET_OWNERproject, bucketgroups, usersFull access to the bucket (should be used rarely).
acl.SCHEDULER_READERproject, bucketgroups, usersViewing Scheduler jobs, invocations and their debug logs.
acl.SCHEDULER_TRIGGERERproject, bucketgroups, usersSame as SCHEDULER_READER + ability to trigger jobs.
acl.SCHEDULER_OWNERproject, bucketgroups, usersFull access to Scheduler jobs, including ability to abort them.

acl.entry

acl.entry(roles = None, groups = None, users = None)

An ACL entry: assigns given role (or roles) to given individuals or groups.

Specifying an empty ACL entry is allowed. It is ignored everywhere. Useful for things like:

core.project(
    acl = [
        acl.entry(acl.PROJECT_CONFIGS_READER, groups = [
            # TODO: fill me in
        ])
    ]
)

Arguments

  • roles: a single role (as acl.role) or a list of roles to assign.
  • groups: a single group name or a list of groups to assign the role to.
  • users: a single user email or a list of emails to assign the role to.

Returns

acl.entry struct, consider it opaque.

Swarming

swarming.cache

swarming.cache(path = None, name = None, wait_for_warm_cache = None)

A request for the bot to mount a named cache to a path.

Each bot has a LRU of named caches: think of them as local named directories in some protected place that survive between builds.

A build can request one or more such caches to be mounted (in read/write mode) at the requested path relative to some known root. In recipes-based builds, the path is relative to api.paths[‘cache’] dir.

If it's the first time a cache is mounted on this particular bot, it will appear as an empty directory. Otherwise it will contain whatever was left there by the previous build that mounted exact same named cache on this bot, even if that build is completely irrelevant to the current build and just happened to use the same named cache (sometimes this is useful to share state between different builders).

At the end of the build the cache directory is unmounted. If at that time the bot is running out of space, caches (in their entirety, the named cache directory and all files inside) are evicted in LRU manner until there's enough free disk space left. Renaming a cache is equivalent to clearing it from the builder perspective. The files will still be there, but eventually will be purged by GC.

Additionally, Buildbucket always implicitly requests to mount a special builder cache to ‘builder’ path:

swarming.cache('builder', name=some_hash('<project>/<bucket>/<builder>'))

This means that any LUCI builder has a “personal disk space” on the bot. Builder cache is often a good start before customizing caching. In recipes, it is available at api.path[‘cache’].join(‘builder’).

In order to share the builder cache directory among multiple builders, some explicitly named cache can be mounted to ‘builder’ path on these builders. Buildbucket will not try to override it with its auto-generated builder cache.

For example, if builders ‘a’ and ‘b’ both declare they use named cache swarming.cache(‘builder’, name=‘my_shared_cache’), and an ‘a’ build ran on a bot and left some files in the builder cache, then when a ‘b’ build runs on the same bot, the same files will be available in its builder cache.

If the pool of swarming bots is shared among multiple LUCI projects and projects mount same named cache, the cache will be shared across projects. To avoid affecting and being affected by other projects, prefix the cache name with something project-specific, e.g. “v8-”.

Arguments

  • path: path where the cache should be mounted to, relative to some known root (in recipes this root is api.path[‘cache’]). Must use POSIX format (forward slashes). In most cases, it does not need slashes at all. Must be unique in the given builder definition (cannot mount multiple caches to the same path).
  • name: identifier of the cache to mount to the path. Default is same value as ‘path’ itself. Must be unique in the given builder definition (cannot mount the same cache to multiple paths).
  • wait_for_warm_cache: how long to wait (with minutes precision) for a bot that has this named cache already to become available and pick up the build, before giving up and starting looking for any matching bot (regardless whether it has the cache or not). If there are no bots with this cache at all, the build will skip waiting and will immediately fallback to any matching bot. By default (if unset or zero), there‘ll be no attempt to find a bot with this cache already warm: the build may or may not end up on a warm bot, there’s no guarantee one way or another.

Returns

swarming.cache struct.

swarming.dimension

swarming.dimension(value = None, expiration = None)

A value of some Swarming dimension, annotated with its expiration time.

Intended to be used as a value in ‘dimensions’ dict when using dimensions that expire:

dimensions = {
    ...
    'device': swarming.dimension('preferred', expiration=5*time.minute),
    ...
}

Arguments

  • value: string value of the dimension.
  • expiration: how long to wait (with minutes precision) for a bot with this dimension to become available and pick up the build, or None to wait until the overall build expiration timeout.

Returns

swarming.dimension struct.

swarming.validate_caches {#swarming.validate_caches}

swarming.validate_caches(attr = None, caches = None)

Validates a list of caches.

Ensures each entry is swarming.cache struct, and no two entries use same name or path.

Arguments {#swarming.validate_caches-args}

  • attr: field name with caches, for error messages.
  • caches: a list of swarming.cache(...) entries to validate.

Returns {#swarming.validate_caches-returns}

Validate list of caches (may be an empty list, never None).

swarming.validate_dimensions {#swarming.validate_dimensions}

swarming.validate_dimensions(attr = None, dimensions = None)

Validates and normalizes a dict with dimensions.

The dict should have string keys and values are swarming.dimension, a string or a list of thereof (for repeated dimensions).

Arguments {#swarming.validate_dimensions-args}

  • attr: field name with dimensions, for error messages.
  • dimensions: a dict {string: string|swarming.dimension}.

Returns {#swarming.validate_dimensions-returns}

Validated and normalized dict in form {string: [swarming.dimension]}.

swarming.validate_tags {#swarming.validate_tags}

swarming.validate_tags(attr = None, tags = None)

Validates a list of “k:v” pairs with Swarming tags.

Arguments {#swarming.validate_tags-args}

  • attr: field name with tags, for error messages.
  • tags: a list of tags to validate.

Returns {#swarming.validate_tags-returns}

Validated list of tags in same order, with duplicates removed.

Other builtin functions

These functions are available in the global namespace.

TODO: To be written.