Files
zuul/zuul/zk/image_registry.py
James E. Blair 36af7c37e1 Fix image build event race and add more image api tests
This change does two things:

1) It adds tests that verify that in the case where images are
   managed in a central repo, admins may not use the web api of
   the wrong tenant (ie, a tenant that is not actually managing
   the builds of these images) to trigger or delete a build or
   upload.

In expanding the tests to include more than one tenant, a previously
existing race condition was more reliably observed, so it is fixed:

2) The following race sequence could happen:

[1] launcher triggers builds for 2 missing images
[2] scheduler begins reporting the image buildset (2 jobs)
[2] scheduler adds image build artifact #1 to registry
[1] launcher receives signal from zk watch that the image registry
    was updated and checks for missing images
[1] launcher observes that image #2 is still missing and submits
    an image build trigger
[2] scheduler adds image build artifact #2 to registry
[2] scheduler removes queue item
[2] scheduler processes trigger event, begins new build of
    image #2

To resolve this, we note that the addition of multiple image artifacts
is a critical section.  We adjust the creation order so that the
sequence is now:

* Create artifact #1 with state=None
* Create artifact #2 with state=None
* Create upload #1 and update artifact #1 to ready
* Create upload #2 and update artifact #2 to ready

The critical section is now bounded by the condition of having any
IBAs with state=None.  We now check for that within the launcher and wait
for it to clear before we decide if any images are missing.

Change-Id: Iad1b68cf8c9cf6bd5f13dab0471e7bf5fd290fbf
2025-05-22 15:00:00 -07:00

100 lines
3.7 KiB
Python

# Copyright 2024 Acme Gating, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
from zuul.zk.launcher import LockableZKObjectCache
from zuul.model import ImageBuildArtifact, ImageUpload
class ImageBuildRegistry(LockableZKObjectCache):
def __init__(self, zk_client, updated_event=None):
self.builds_by_image_name = collections.defaultdict(set)
super().__init__(
zk_client,
updated_event,
root=ImageBuildArtifact.ROOT,
items_path=ImageBuildArtifact.IMAGES_PATH,
locks_path=ImageBuildArtifact.LOCKS_PATH,
zkobject_class=ImageBuildArtifact,
)
def postCacheHook(self, event, data, stat, key, obj):
exists = key in self._cached_objects
if exists:
if obj:
builds = self.builds_by_image_name[obj.canonical_name]
builds.add(key)
else:
if obj:
builds = self.builds_by_image_name[obj.canonical_name]
builds.discard(key)
super().postCacheHook(event, data, stat, key, obj)
def getArtifactsForImage(self, image_canonical_name):
keys = list(self.builds_by_image_name[image_canonical_name])
arts = [self._cached_objects.get(key) for key in keys]
arts = [a for a in arts if a is not None]
# Sort in a stable order, primarily by timestamp, then format
# for identical timestamps.
arts = sorted(arts, key=lambda x: x.format)
arts = sorted(arts, key=lambda x: x.timestamp)
return arts
def getAllArtifacts(self):
keys = []
for image_canonical_name in self.builds_by_image_name.keys():
keys.extend(self.builds_by_image_name[image_canonical_name])
arts = [self._cached_objects.get(key) for key in keys]
arts = [a for a in arts if a is not None]
# Sort in a stable order, primarily by timestamp, then format
# for identical timestamps.
arts = sorted(arts, key=lambda x: x.format)
arts = sorted(arts, key=lambda x: x.timestamp)
return arts
class ImageUploadRegistry(LockableZKObjectCache):
def __init__(self, zk_client, updated_event=None):
self.uploads_by_image_name = collections.defaultdict(set)
super().__init__(
zk_client,
updated_event,
root=ImageUpload.ROOT,
items_path=ImageUpload.UPLOADS_PATH,
locks_path=ImageUpload.LOCKS_PATH,
zkobject_class=ImageUpload,
)
def postCacheHook(self, event, data, stat, key, obj):
exists = key in self._cached_objects
if exists:
if obj:
uploads = self.uploads_by_image_name[obj.canonical_name]
uploads.add(key)
else:
if obj:
uploads = self.uploads_by_image_name[obj.canonical_name]
uploads.discard(key)
super().postCacheHook(event, data, stat, key, obj)
def getUploadsForImage(self, image_canonical_name):
keys = list(self.uploads_by_image_name[image_canonical_name])
uploads = [self._cached_objects.get(key) for key in keys]
uploads = [u for u in uploads if u is not None]
uploads = sorted(uploads, key=lambda x: x.timestamp)
return uploads