summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/unit/test_web.py22
-rw-r--r--tests/unit/test_zk.py24
-rw-r--r--zuul/connection/__init__.py49
-rw-r--r--zuul/driver/github/githubconnection.py10
-rw-r--r--zuul/zk/branch_cache.py66
5 files changed, 119 insertions, 52 deletions
diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py
index 5d38c5b19..ba0c10488 100644
--- a/tests/unit/test_web.py
+++ b/tests/unit/test_web.py
@@ -3282,3 +3282,25 @@ class TestWebStartup(ZuulTestCase):
# If the config didn't load correctly, we won't have the jobs
jobs = self.get_url("api/tenant/tenant-one/jobs").json()
self.assertEqual(len(jobs), 10)
+
+
+class TestWebUnprotectedBranches(BaseWithWeb):
+ config_file = 'zuul-github-driver.conf'
+ tenant_config_file = 'config/unprotected-branches/main.yaml'
+
+ def test_no_protected_branches(self):
+ """Regression test to check that zuul-web doesn't display
+ config errors when no protected branch exists."""
+ self.startWebServer()
+ tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
+
+ project2 = tenant.untrusted_projects[1]
+ tpc2 = tenant.project_configs[project2.canonical_name]
+
+ # project2 should have no parsed branch
+ self.assertEqual(0, len(tpc2.parsed_branch_config.keys()))
+
+ # Zuul-web should not display any config errors
+ config_errors = self.get_url(
+ "api/tenant/tenant-one/config-errors").json()
+ self.assertEqual(len(config_errors), 0)
diff --git a/tests/unit/test_zk.py b/tests/unit/test_zk.py
index e2893e9df..e45629bef 100644
--- a/tests/unit/test_zk.py
+++ b/tests/unit/test_zk.py
@@ -1834,9 +1834,9 @@ class TestBranchCache(ZooKeeperBaseTestCase):
sorted(cache.getProjectBranches('project1', True)),
test_data['project1']['protected']
)
- self.assertEqual(
- cache.getProjectBranches('project1', False),
- None,
+ self.assertRaises(
+ LookupError,
+ lambda: cache.getProjectBranches('project1', False)
)
cache.setProjectBranches('project1', False,
@@ -1862,21 +1862,21 @@ class TestBranchCache(ZooKeeperBaseTestCase):
},
}
- self.assertEqual(
- cache.getProjectBranches('project1', True),
- None
+ self.assertRaises(
+ LookupError,
+ lambda: cache.getProjectBranches('project1', True)
)
- self.assertEqual(
- cache.getProjectBranches('project1', False),
- None
+ self.assertRaises(
+ LookupError,
+ lambda: cache.getProjectBranches('project1', False)
)
# Test the other order; all followed by protected-only
cache.setProjectBranches('project1', False,
test_data['project1']['all'])
- self.assertEqual(
- cache.getProjectBranches('project1', True),
- None
+ self.assertRaises(
+ LookupError,
+ lambda: cache.getProjectBranches('project1', True)
)
self.assertEqual(
sorted(cache.getProjectBranches('project1', False)),
diff --git a/zuul/connection/__init__.py b/zuul/connection/__init__.py
index fb43fad4d..53be27d38 100644
--- a/zuul/connection/__init__.py
+++ b/zuul/connection/__init__.py
@@ -208,9 +208,9 @@ class ZKBranchCacheMixin:
"""
# Figure out which queries we have a cache for
protected_branches = self._branch_cache.getProjectBranches(
- project.name, True)
+ project.name, True, default=None)
all_branches = self._branch_cache.getProjectBranches(
- project.name, False)
+ project.name, False, default=None)
# Update them if we have them
if protected_branches is not None:
@@ -238,24 +238,26 @@ class ZKBranchCacheMixin:
:returns: The list of branch names.
"""
exclude_unprotected = tenant.getExcludeUnprotectedBranches(project)
- if self._branch_cache:
- branches = self._branch_cache.getProjectBranches(
- project.name, exclude_unprotected, min_ltime)
- else:
- # Handle the case where tenant validation doesn't use the cache
- branches = None
+ branches = None
- if branches:
+ if self._branch_cache:
+ try:
+ branches = self._branch_cache.getProjectBranches(
+ project.name, exclude_unprotected, min_ltime)
+ except LookupError:
+ if self.read_only:
+ # A scheduler hasn't attempted to fetch them yet
+ raise ReadOnlyBranchCacheError(
+ "Will not fetch project branches as read-only is set")
+ else:
+ branches = None
+
+ if branches is not None:
return sorted(branches)
-
- if self.read_only:
- if branches is None:
- # A scheduler hasn't attempted to fetch them yet
- raise ReadOnlyBranchCacheError(
- "Will not fetch project branches as read-only is set")
+ elif self.read_only:
# A scheduler has previously attempted a fetch, but got
- # the empty list due to an error; we can't retry since
- # we're read-only
+ # the None due to an error; we can't retry since we're
+ # read-only.
raise RuntimeError(
"Will not fetch project branches as read-only is set")
@@ -265,14 +267,12 @@ class ZKBranchCacheMixin:
except Exception:
# We weren't able to get the branches. We need to tell
# future schedulers to try again but tell zuul-web that we
- # tried and failed. Set the branches to the empty list to
- # indicate that we have performed a fetch and retrieved no
- # data. Any time we encounter the empty list in the
- # cache, we will try again (since it is not reasonable to
- # have a project with no branches).
+ # tried and failed. Set the branches to None to indicate
+ # that we have performed a fetch and retrieved no data. Any
+ # time we encounter None in the cache, we will try again.
if self._branch_cache:
self._branch_cache.setProjectBranches(
- project.name, exclude_unprotected, [])
+ project.name, exclude_unprotected, None)
raise
self.log.info("Got branches for %s" % project.name)
@@ -313,7 +313,8 @@ class ZKBranchCacheMixin:
# cache, so we never clear it.
branches = self._branch_cache.getProjectBranches(
- project_name, True)
+ project_name, True, default=None)
+
if not branches:
branches = []
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index 99631dda2..04759c15f 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -613,9 +613,13 @@ class GithubEventProcessor(object):
project = self.connection.source.getProject(project_name)
# Save all protected branches
- old_protected_branches = set(
- self.connection._branch_cache.getProjectBranches(
- project_name, True))
+ cached_branches = self.connection._branch_cache.getProjectBranches(
+ project_name, True, default=None)
+
+ if cached_branches is None:
+ raise RuntimeError(f"No branches for project {project_name}")
+ old_protected_branches = set(cached_branches)
+
# Update the project banches
self.log.debug('Updating branches for %s after '
'branch protection rule "%s" was %s',
diff --git a/zuul/zk/branch_cache.py b/zuul/zk/branch_cache.py
index 08f0d6046..35d0c69cd 100644
--- a/zuul/zk/branch_cache.py
+++ b/zuul/zk/branch_cache.py
@@ -21,6 +21,9 @@ from zuul.zk.locks import SessionAwareReadLock, SessionAwareWriteLock, locked
from kazoo.exceptions import NoNodeError
+# Default marker to raise an exception on cache miss in getProjectBranches()
+RAISE_EXCEPTION = object()
+
class BranchCacheZKObject(ShardedZKObject):
"""Store the branch cache in ZK
@@ -42,6 +45,9 @@ class BranchCacheZKObject(ShardedZKObject):
If a project is absent from the dict, it needs to be queried from
the source.
+ If there was an error fetching the branches, None will be stored
+ as a sentinel value.
+
When performing an exclude_unprotected query, remove any duplicate
branches from remaider to save space. When determining the full
list of branches, combine both lists.
@@ -117,44 +123,78 @@ class BranchCache:
self.cache.remainder.pop(p, None)
def getProjectBranches(self, project_name, exclude_unprotected,
- min_ltime=-1):
+ min_ltime=-1, default=RAISE_EXCEPTION):
"""Get the branch names for the given project.
+ Checking the branch cache we need to distinguish three different
+ cases:
+
+ 1. cache miss (not queried yet)
+ 2. cache hit (including empty list of branches)
+ 3. error when fetching branches
+
+ If the cache doesn't contain any branches for the project and no
+ default value is provided a LookupError is raised.
+
+ If there was an error fetching the branches, the return value
+ will be None.
+
+ Otherwise the list of branches will be returned.
+
:param str project_name:
The project for which the branches are returned.
:param bool exclude_unprotected:
Whether to return all or only protected branches.
:param int min_ltime:
The minimum cache ltime to consider the cache valid.
+ :param any default:
+ Optional default value to return if no cache entry exits.
- :returns: The list of branch names, or None if the cache
- cannot satisfy the request.
+ :returns: The list of branch names, or None if there was
+ an error when fetching the branches.
"""
if self.ltime < min_ltime:
with locked(self.rlock):
self.cache.refresh(self.zk_context)
- protected_branches = self.cache.protected.get(project_name)
- remainder_branches = self.cache.remainder.get(project_name)
+ protected_branches = None
+ try:
+ protected_branches = self.cache.protected[project_name]
+ except KeyError:
+ if exclude_unprotected:
+ if default is RAISE_EXCEPTION:
+ raise LookupError(
+ f"No branches for project {project_name}")
+ else:
+ return default
+
+ if not exclude_unprotected:
+ try:
+ remainder_branches = self.cache.remainder[project_name]
+ except KeyError:
+ if default is RAISE_EXCEPTION:
+ raise LookupError(
+ f"No branches for project {project_name}")
+ else:
+ return default
- if exclude_unprotected:
- if protected_branches is not None:
- return protected_branches
- else:
if remainder_branches is not None:
return (protected_branches or []) + remainder_branches
- return None
+ return protected_branches
def setProjectBranches(self, project_name, exclude_unprotected, branches):
"""Set the branch names for the given project.
+ Use None as a sentinel value for the branches to indicate that
+ there was a fetch error.
+
:param str project_name:
The project for the branches.
:param bool exclude_unprotected:
Whether this is a list of all or only protected branches.
:param list[str] branches:
- The list of branches
+ The list of branches or None to indicate a fetch error.
"""
with locked(self.wlock):
@@ -162,13 +202,13 @@ class BranchCache:
if exclude_unprotected:
self.cache.protected[project_name] = branches
remainder_branches = self.cache.remainder.get(project_name)
- if remainder_branches:
+ if remainder_branches and branches:
remainder = list(set(remainder_branches) -
set(branches))
self.cache.remainder[project_name] = remainder
else:
protected_branches = self.cache.protected.get(project_name)
- if protected_branches:
+ if protected_branches and branches:
remainder = list(set(branches) -
set(protected_branches))
else: