blob: 2e76d2b3e9de97f60b47170e29c628271a3fdb7e [file] [log] [blame]
# Copyright 2018 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Recipe for testing LUCI configs."""
import re
from recipe_engine import post_process
from recipe_engine.recipe_api import Property
from PB.go.chromium.org.luci.buildbucket.proto import builder as builder_pb2
from PB.go.chromium.org.luci.buildbucket.proto import project_config as bb_pb2
from PB.go.chromium.org.luci.cq.api.config.v2 import cq as cq_pb2
DEPS = [
"fuchsia/buildbucket_util",
"fuchsia/checkout",
"fuchsia/luci_config",
"fuchsia/status_check",
"recipe_engine/buildbucket",
"recipe_engine/cipd",
"recipe_engine/context",
"recipe_engine/file",
"recipe_engine/path",
"recipe_engine/properties",
"recipe_engine/step",
]
PROPERTIES = {
"config_project": Property(
kind=str, help="Jiri remote manifest project containing the luci configs"
),
"manifest": Property(kind=str, help="Jiri manifest to use"),
"remote": Property(kind=str, help="Remote manifest repository"),
"starlark_paths": Property(kind=list, help="Starlark file paths to validate"),
"lucicfg_ensure_file": Property(
kind=str,
help=(
"CIPD ensure file containing the pinned lucicfg tool version, relative to "
"the integration repo"
),
),
"generated_dir": Property(
kind=str,
help=(
"Relative directory within the checkout containing generated files for all "
"projects"
),
),
}
NEW_BUILDERS_IN_CQ_MSG = """\
The following builder(s) are referenced in commit-queue.cfg but aren't yet
known to Buildbucket. Create the builder(s) in a separate change before adding
them to commit-queue.cfg.
"""
CQ_BUILDERS_DELETED_MSG = """\
The following builder(s) are running in CQ but missing from
cr-buildbucket.cfg. Remove the builder(s) from CQ in a separate change before
deleting them.
"""
def RunSteps(
api,
config_project,
manifest,
remote,
starlark_paths,
lucicfg_ensure_file,
generated_dir,
):
assert starlark_paths, "starlark_paths must not be empty"
checkout_root = api.path["start_dir"]
with api.context(infra_steps=True):
api.checkout.with_options(
path=checkout_root,
manifest=manifest,
remote=remote,
project=config_project,
)
integration_dir = checkout_root.join(
api.checkout.project(config_project, checkout_root)["path"]
)
lucicfg_dir = api.path.mkdtemp("lucicfg")
api.cipd.ensure(
lucicfg_dir, integration_dir.join(lucicfg_ensure_file), "install lucicfg"
)
lucicfg_path = lucicfg_dir.join("lucicfg")
with api.step.nest("validate"), api.step.defer_results():
for relative_path in starlark_paths:
abs_path = integration_dir.join(relative_path)
api.step(
relative_path,
[lucicfg_path, "validate", "-strict", "-fail-on-warnings", abs_path],
)
with api.step.nest("check for config deployment race conditions"):
check_for_deployment_race_conditions(api, integration_dir.join(generated_dir))
def check_for_deployment_race_conditions(api, generated_dir):
"""
There are some types of LUCI config changes that introduce race
conditions in the process of deploying the updated config files to their
corresponding services.
For example, it's not safe to simultaneously create a new builder and add
it to CQ, because CQ might ingest the updated configs before Buildbucket
and try to trigger the builder before Buildbucket even knows it exists.
This causes CQ to error out and add confusing comments to CLs.
To avoid this specific race condition, we compare the modified version of
commit-queue.cfg with the live version of cr-buildbucket.cfg to make sure
that even if CQ ingests the updated configs before Buildbucket does, CQ
won't try to trigger any builders that don't yet exist.
We also check that the live version of commit-queue.cfg doesn't reference
any builders that will soon be deleted, which would cause the same issue
if Buildbucket ingests the config changes before CQ does.
There are other race conditions that we don't bother checking here; for
example, it's technically not safe to immediately start running a new
builder in CI by adding it to luci-scheduler.cfg. However, LUCI scheduler
trigger failures don't produce user-visible error messages like CQ
trigger failures, so it's not as big a deal. Also, adding new builders to
CI is a very common workflow (adding builders to CQ is somewhat more
rare) and it would be annoying if that always required two separate
changes.
"""
# Our luci configs are set up such that each LUCI project's config files
# are in a subdirectory of the "generated" directory whose name is the same
# as the project name.
project_dirs = api.file.listdir(
"find projects", generated_dir, test_data=["fuchsia", "fuchsia_internal"]
)
all_live_builders = set()
all_local_builders = set()
for project_dir in project_dirs:
project = api.path.basename(project_dir)
live_bb_config = api.luci_config.buildbucket(project=project)
all_live_builders.update(all_builder_names(api, project, live_bb_config))
local_bb_config = api.luci_config.buildbucket(
project=project, local_dir=project_dir.join("luci")
)
all_local_builders.update(all_builder_names(api, project, local_bb_config))
for project_dir in project_dirs:
project = api.path.basename(project_dir)
cq_config_path = project_dir.join("luci", "commit-queue.cfg")
api.path.mock_add_paths(cq_config_path)
if not api.path.exists(cq_config_path): # pragma: no cover
# Skip projects that don't have CQ set up.
continue
local_cq_config = api.luci_config.commit_queue(
project=project, local_dir=project_dir.join("luci")
)
# Make sure that the new CQ config won't try to trigger non-existent builders.
nonexistent_builders = nonexistent_cq_builders(
local_cq_config, all_live_builders
)
if nonexistent_builders:
msg = NEW_BUILDERS_IN_CQ_MSG + "".join(
["\n- %s" % b for b in nonexistent_builders]
)
raise api.step.StepFailure(msg)
live_cq_config = api.luci_config.commit_queue(project=project)
# Make sure that the existing CQ config won't try to trigger builders
# that we're deleting.
nonexistent_builders = nonexistent_cq_builders(
live_cq_config, all_local_builders
)
if nonexistent_builders:
msg = CQ_BUILDERS_DELETED_MSG + "".join(
["\n- %s" % b for b in nonexistent_builders]
)
raise api.step.StepFailure(msg)
def all_builder_names(api, project_name, bb_config):
for bucket in bb_config.buckets:
for builder in bucket.swarming.builders:
yield api.buildbucket_util.full_builder_name(
builder_pb2.BuilderID(
project=project_name, bucket=bucket.name, builder=builder.name,
)
)
def nonexistent_cq_builders(cq_config, all_known_builders):
"""Checks that all builders in cq_config are known to Buildbucket.
Returns the names of any CQ builders that aren't in the list of builders
known to Buildbucket.
"""
nonexistent_builders = set()
for group in cq_config.config_groups:
for builder in group.verifiers.tryjob.builders:
if builder.name not in all_known_builders:
nonexistent_builders.add(builder.name)
return sorted(nonexistent_builders)
def GenTests(api):
properties = api.properties(
config_project="integration",
manifest="manifest/infra",
remote="https://fuchsia.googlesource.com/manifest",
starlark_paths=["main.star", "dev.star"],
lucicfg_ensure_file="cipd.ensure",
generated_dir="generated",
)
nesting = "check for config deployment race conditions"
def cq_config(builders):
return cq_pb2.Config(
config_groups=[
dict(
verifiers=dict(
tryjob=dict(
builders=[
dict(name=builder_name) for builder_name in builders
]
)
)
)
]
)
yield (
api.status_check.test("starlark")
+ api.buildbucket.try_build(project="fuchsia")
+ properties
# Cover the logic that reads builder names from cr-buildbucket.cfg.
+ api.luci_config.mock_local_config(
"fuchsia",
"cr-buildbucket.cfg",
bb_pb2.BuildbucketCfg(
buckets=[
dict(
name="try",
swarming=dict(builders=[dict(name="core.x64-asan")]),
)
]
),
nesting=nesting,
)
)
# The second file should be validated even if the first one fails.
yield (
api.status_check.test("first_file_invalid", status="failure")
+ api.buildbucket.try_build(project="fuchsia")
+ properties
+ api.step_data("validate.main.star", retcode=1)
+ api.post_process(post_process.MustRun, "validate.dev.star")
)
yield (
api.status_check.test("cq_new_builder", status="failure")
+ api.buildbucket.try_build(project="fuchsia")
+ properties
+ api.luci_config.mock_local_config(
"fuchsia",
"commit-queue.cfg",
cq_config(["fuchsia/try/core.x64-asan"]),
nesting=nesting,
)
+ api.post_process(
post_process.ResultReasonRE, re.escape(NEW_BUILDERS_IN_CQ_MSG)
)
)
yield (
api.status_check.test("deleting_cq_builder", status="failure")
+ api.buildbucket.try_build(project="fuchsia")
+ properties
+ api.luci_config.mock_config(
"fuchsia",
"commit-queue.cfg",
cq_config(["fuchsia/try/core.x64-asan"]),
nesting=nesting,
)
+ api.post_process(
post_process.ResultReasonRE, re.escape(CQ_BUILDERS_DELETED_MSG)
)
)