Build "master" images in "main" (#1090)

Signed-off-by: Mohammed Naser <mnaser@vexxhost.com>
diff --git a/images/Earthfile b/images/Earthfile
deleted file mode 100644
index 32cab4a..0000000
--- a/images/Earthfile
+++ /dev/null
@@ -1,52 +0,0 @@
-VERSION 0.8
-
-APT_INSTALL:
-  FUNCTION
-  ARG PACKAGES
-  RUN \
-    apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y ${PACKAGES} && \
-    apt-get clean && \
-    rm -rf /var/lib/apt/lists/*
-
-DNF_INSTALL:
-  FUNCTION
-  ARG PACKAGES
-  RUN \
-    dnf -y install \
-      ${PACKAGES} \
-      --setopt=install_weak_deps=False \
-      --setopt=tsflags=nodocs && \
-    dnf -y clean all && \
-    rm -rf /var/cache/dnf
-
-CREATE_PROJECT_USER:
-  FUNCTION
-  ARG PROJECT
-  ARG SHELL=/usr/sbin/nologin
-  RUN \
-    groupadd -g 42424 ${PROJECT} && \
-    useradd -u 42424 -g 42424 -M -d /var/lib/${PROJECT} -s ${SHELL} -c "${PROJECT} User" ${PROJECT} && \
-    mkdir -p /etc/${PROJECT} /var/log/${PROJECT} /var/lib/${PROJECT} /var/cache/${PROJECT} && \
-    chown -Rv ${PROJECT}:${PROJECT} /etc/${PROJECT} /var/log/${PROJECT} /var/lib/${PROJECT} /var/cache/${PROJECT}
-
-fetch-gerrit-patch:
-  FROM ./base+image
-  DO +APT_INSTALL --PACKAGES "ca-certificates curl git jq"
-  ARG --required IMAGE
-  ARG PROJECT=${IMAGE}
-  ARG --required CHANGE
-  ARG PROJECT_REF=master
-  DO ./openstack-service+GIT_CHECKOUT \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-  ARG REF=$(curl "https://review.opendev.org/changes/?q=${CHANGE}&o=CURRENT_REVISION" | tail -1 | jq -r '.[0].revisions[].ref')
-  COPY ${IMAGE}/patches/${PROJECT} /patches
-  RUN \
-    git fetch https://review.opendev.org/openstack/${PROJECT} ${REF} && \
-    git format-patch -1 --output-directory /gerrit FETCH_HEAD
-  ARG PATCH_ID=$(ls -1 /patches | wc -l | xargs printf "%04d")
-  RUN \
-    cp /gerrit/0001-* \
-    /patches/${PATCH_ID}-$(basename /gerrit/*.patch | sed 's/0001-//')
-  SAVE ARTIFACT /patches AS LOCAL ${IMAGE}/patches/${PROJECT}
diff --git a/images/barbican/Dockerfile b/images/barbican/Dockerfile
new file mode 100644
index 0000000..28de6ab
--- /dev/null
+++ b/images/barbican/Dockerfile
@@ -0,0 +1,27 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG BARBICAN_GIT_REF=ca57ef5436e20e90cf6cd6853efe3c89a9afd986
+ADD --keep-git-dir=true https://opendev.org/openstack/barbican.git#${BARBICAN_GIT_REF} /src/barbican
+RUN git -C /src/barbican fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/barbican \
+        pykmip
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/base/Earthfile b/images/base/Earthfile
deleted file mode 100644
index 8b7b2df..0000000
--- a/images/base/Earthfile
+++ /dev/null
@@ -1,7 +0,0 @@
-VERSION 0.7
-
-image:
-  FROM ubuntu:jammy-20240212
-  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
-  # CVE-2023-4641
-  DO ../+APT_INSTALL --PACKAGES "login=1:4.8.1-2ubuntu2.2 passwd=1:4.8.1-2ubuntu2.2"
diff --git a/images/build.sh b/images/build.sh
deleted file mode 100755
index d93f853..0000000
--- a/images/build.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash -xe
-
-TARGET=""
-PUSH=false
-
-while [[ $# -gt 0 ]]; do
-    key="$1"
-
-    case $key in
-        --push)
-        PUSH=true
-        shift
-        ;;
-        *)
-        if [ -z "$TARGET" ]; then
-            TARGET="$1"
-        else
-            echo "Invalid argument: $1"
-            exit 1
-        fi
-        shift
-        ;;
-    esac
-done
-
-if [ -z "$TARGET" ]; then
-    echo "Usage: $0 [--push] <target>"
-    exit 1
-fi
-
-docker buildx create --name=atmosphere --driver=docker-container || true
-
-if [ "$PUSH" = true ]; then
-    docker buildx bake --builder=atmosphere --provenance --sbom=true --push $TARGET
-
-    # Sign all images
-    export COSIGN_PASSWORD=""
-    for IMAGE in $(docker buildx bake --print ${TARGET} | jq -r '.target[].tags | select(. != null)[]'); do
-        cosign sign -y --recursive --key cosign.key ${IMAGE}
-    done
-else
-    docker buildx bake --builder=atmosphere --provenance --sbom=true $TARGET
-fi
diff --git a/images/builder/Earthfile b/images/builder/Earthfile
deleted file mode 100644
index f4e92d3..0000000
--- a/images/builder/Earthfile
+++ /dev/null
@@ -1,7 +0,0 @@
-VERSION 0.7
-
-image:
-  FROM ../base+image
-  DO ../+APT_INSTALL --PACKAGES "build-essential git python3-dev python3-pip python3-venv"
-  ARG POETRY_VERSION=1.4.2
-  RUN pip3 install --no-cache-dir poetry==${POETRY_VERSION}
diff --git a/images/cinder/Dockerfile b/images/cinder/Dockerfile
new file mode 100644
index 0000000..4f0fbb5
--- /dev/null
+++ b/images/cinder/Dockerfile
@@ -0,0 +1,37 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG CINDER_GIT_REF=b0f0b9015b9dfa228dff98eeee5116d8eca1c3cc
+ADD --keep-git-dir=true https://opendev.org/openstack/cinder.git#${CINDER_GIT_REF} /src/cinder
+RUN git -C /src/cinder fetch --unshallow
+COPY patches/cinder /patches/cinder
+RUN git -C /src/cinder apply --verbose /patches/cinder/*
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/cinder \
+        purestorage
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    ceph-common lsscsi nvme-cli python3-rados python3-rbd qemu-utils sysfsutils udev util-linux
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+ADD --chmod=755 https://dl.k8s.io/release/v1.29.3/bin/linux/amd64/kubectl /usr/local/bin/kubectl
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/cinder/Earthfile b/images/cinder/Earthfile
deleted file mode 100644
index 79561e7..0000000
--- a/images/cinder/Earthfile
+++ /dev/null
@@ -1,23 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=cinder
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=6abf43c220029bd47ec446971e14e293c9be2f78
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES="purestorage"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  COPY ../kubernetes+image/kubectl /usr/local/bin/kubectl
-  DO ../+APT_INSTALL \
-    --PACKAGES "ceph-common lsscsi nvme-cli python3-rados python3-rbd qemu-utils sysfsutils udev util-linux"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/cinder/patches/cinder/0001-Create-encrypted-volumes-directly-to-RBD.patch b/images/cinder/patches/cinder/0001-Create-encrypted-volumes-directly-to-RBD.patch
new file mode 100644
index 0000000..6993ab3
--- /dev/null
+++ b/images/cinder/patches/cinder/0001-Create-encrypted-volumes-directly-to-RBD.patch
@@ -0,0 +1,145 @@
+From 5ffb50d2873c48998491950138c848a9feef07fc Mon Sep 17 00:00:00 2001
+From: ricolin <ricolin@ricolky.com>
+Date: Fri, 1 Mar 2024 13:50:13 +0800
+Subject: [PATCH 1/3] Create encrypted volumes directly to RBD
+
+This fix slow on create encrypted volumes with temp file import.
+Encrypted volume create is now directly upload to RBD with qemu-img
+command without temprory image file generated.
+
+Closes-Bug: #2055517
+Change-Id: If7a72a4acd5600de1350289a9d9c38017d42659e
+---
+ cinder/tests/unit/volume/drivers/test_rbd.py  |  9 +--
+ cinder/volume/drivers/rbd.py                  | 62 +++++++++----------
+ ...ate-encrypted-volume-c1bb6b44b85c0242.yaml |  7 +++
+ 3 files changed, 40 insertions(+), 38 deletions(-)
+ create mode 100644 releasenotes/notes/improve-create-encrypted-volume-c1bb6b44b85c0242.yaml
+
+diff --git a/cinder/tests/unit/volume/drivers/test_rbd.py b/cinder/tests/unit/volume/drivers/test_rbd.py
+index f1ffeb89e..cf768df06 100644
+--- a/cinder/tests/unit/volume/drivers/test_rbd.py
++++ b/cinder/tests/unit/volume/drivers/test_rbd.py
+@@ -3247,7 +3247,6 @@ class RBDTestCase(test.TestCase):
+                 self.__dict__ = d
+ 
+         mock_temp_file.return_value.__enter__.side_effect = [
+-            DictObj({'name': '/imgfile'}),
+             DictObj({'name': '/passfile'})]
+ 
+         key_mgr = fake_keymgr.fake_api()
+@@ -3268,15 +3267,13 @@ class RBDTestCase(test.TestCase):
+                                                  self.context)
+             mock_open.assert_called_with('/passfile', 'w')
+ 
+-            mock_exec.assert_any_call(
++            mock_exec.assert_called_with(
+                 'qemu-img', 'create', '-f', 'luks', '-o',
+                 'cipher-alg=aes-256,cipher-mode=xts,ivgen-alg=essiv',
+                 '--object',
+                 'secret,id=luks_sec,format=raw,file=/passfile',
+-                '-o', 'key-secret=luks_sec', '/imgfile', '12288M')
+-            mock_exec.assert_any_call(
+-                'rbd', 'import', '--dest-pool', 'rbd', '--order', 22,
+-                '/imgfile', self.volume_c.name)
++                '-o', 'key-secret=luks_sec', 'rbd:rbd/%s' % self.volume_c.name,
++                '12288M')
+ 
+     @mock.patch('cinder.objects.Volume.get_by_id')
+     @mock.patch('cinder.db.volume_glance_metadata_get', return_value={})
+diff --git a/cinder/volume/drivers/rbd.py b/cinder/volume/drivers/rbd.py
+index 1f4dac8d9..aace801f3 100644
+--- a/cinder/volume/drivers/rbd.py
++++ b/cinder/volume/drivers/rbd.py
+@@ -1089,8 +1089,8 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
+                                  context: context.RequestContext) -> None:
+         """Create an encrypted volume.
+ 
+-        This works by creating an encrypted image locally,
+-        and then uploading it to the volume.
++        This works by creating an encrypted image and
++        then uploading it to the volume directly.
+         """
+         encryption = volume_utils.check_encryption_provider(volume, context)
+ 
+@@ -1102,37 +1102,35 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
+         # create a file
+         tmp_dir = volume_utils.image_conversion_dir()
+ 
+-        with tempfile.NamedTemporaryFile(dir=tmp_dir) as tmp_image:
+-            with tempfile.NamedTemporaryFile(dir=tmp_dir) as tmp_key:
+-                with open(tmp_key.name, 'w') as f:
+-                    f.write(passphrase)
+-
+-                cipher_spec = image_utils.decode_cipher(encryption['cipher'],
+-                                                        encryption['key_size'])
+-
+-                create_cmd = (
+-                    'qemu-img', 'create', '-f', 'luks',
+-                    '-o', 'cipher-alg=%(cipher_alg)s,'
+-                    'cipher-mode=%(cipher_mode)s,'
+-                    'ivgen-alg=%(ivgen_alg)s' % cipher_spec,
+-                    '--object', 'secret,id=luks_sec,'
+-                    'format=raw,file=%(passfile)s' % {'passfile':
+-                                                      tmp_key.name},
+-                    '-o', 'key-secret=luks_sec',
+-                    tmp_image.name,
+-                    '%sM' % (volume.size * 1024))
+-                self._execute(*create_cmd)
+-
+-            # Copy image into RBD
+-            chunk_size = self.configuration.rbd_store_chunk_size * units.Mi
+-            order = int(math.log(chunk_size, 2))
++        with tempfile.NamedTemporaryFile(dir=tmp_dir) as tmp_key:
++            with open(tmp_key.name, 'w') as f:
++                f.write(passphrase)
+ 
+-            cmd = ['rbd', 'import',
+-                   '--dest-pool', self.configuration.rbd_pool,
+-                   '--order', order,
+-                   tmp_image.name, volume.name]
+-            cmd.extend(self._ceph_args())
+-            self._execute(*cmd)
++            cipher_spec = image_utils.decode_cipher(encryption['cipher'],
++                                                    encryption['key_size'])
++
++            _, conf, user_id, _ = self._get_config_tuple()
++            rbd_options = ''
++            if user_id:
++                rbd_options += ':id=%(user_id)s' % {'user_id': user_id}
++            if conf:
++                rbd_options += ':conf=%(conf)s' % {'conf': conf}
++            create_cmd = (
++                'qemu-img', 'create', '-f', 'luks',
++                '-o', 'cipher-alg=%(cipher_alg)s,'
++                'cipher-mode=%(cipher_mode)s,'
++                'ivgen-alg=%(ivgen_alg)s' % cipher_spec,
++                '--object', 'secret,id=luks_sec,'
++                'format=raw,file=%(passfile)s' % {'passfile':
++                                                  tmp_key.name},
++                '-o', 'key-secret=luks_sec',
++                'rbd:%(pool_name)s/%(image_name)s%(rbd_options)s' % {
++                    'pool_name': self.configuration.rbd_pool,
++                    'image_name': volume.name,
++                    'rbd_options': rbd_options
++                },
++                '%sM' % (volume.size * 1024))
++            self._execute(*create_cmd)
+ 
+     def create_volume(self, volume: Volume) -> dict[str, Any]:
+         """Creates a logical volume."""
+diff --git a/releasenotes/notes/improve-create-encrypted-volume-c1bb6b44b85c0242.yaml b/releasenotes/notes/improve-create-encrypted-volume-c1bb6b44b85c0242.yaml
+new file mode 100644
+index 000000000..8bdff6746
+--- /dev/null
++++ b/releasenotes/notes/improve-create-encrypted-volume-c1bb6b44b85c0242.yaml
+@@ -0,0 +1,7 @@
++---
++fixes:
++  - |
++    [Bug 255517](https://bugs.launchpad.net/cinder/+bug/2055517): Fix slow
++    on create encrypted volumes with temp file import. Encrypted volume create
++    is now directly upload to rbd with qemu-img command without temprory image
++    file generated.
+-- 
+2.34.1
diff --git a/images/cinder/patches/cinder/0002-Allow-clone-encrypted-image-to-encrypted-volume.patch b/images/cinder/patches/cinder/0002-Allow-clone-encrypted-image-to-encrypted-volume.patch
new file mode 100644
index 0000000..db47071
--- /dev/null
+++ b/images/cinder/patches/cinder/0002-Allow-clone-encrypted-image-to-encrypted-volume.patch
@@ -0,0 +1,129 @@
+From c47fb9f0209076182787f06b306f30c3e1948592 Mon Sep 17 00:00:00 2001
+From: ricolin <rlin@vexxhost.com>
+Date: Sat, 16 Mar 2024 00:35:12 +0800
+Subject: [PATCH 2/3] Allow clone encrypted image to encrypted volume
+
+Exactly like what we did in copy-and-import image when create encrypted
+volume from encrypted image. If the image is encrypted, we will copy
+`cinder_encryption_key_id` from image metadata to volume. That means we
+should be safe to try directly clone from encrypted image.
+
+Related-Bug: #2055517
+Change-Id: Id6a1452c2c197a58677bf181470f54565fbd263b
+---
+ .../volume/flows/test_create_volume_flow.py   | 46 +++++++++++++++++++
+ cinder/volume/flows/manager/create_volume.py  |  9 +++-
+ ...clone-encryped-image-6961ca1439825dc4.yaml |  8 ++++
+ 3 files changed, 61 insertions(+), 2 deletions(-)
+ create mode 100644 releasenotes/notes/allow-clone-encryped-image-6961ca1439825dc4.yaml
+
+diff --git a/cinder/tests/unit/volume/flows/test_create_volume_flow.py b/cinder/tests/unit/volume/flows/test_create_volume_flow.py
+index ad5735596..6ff97aaa0 100644
+--- a/cinder/tests/unit/volume/flows/test_create_volume_flow.py
++++ b/cinder/tests/unit/volume/flows/test_create_volume_flow.py
+@@ -1203,6 +1203,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
+             encryption_key_id=fakes.ENCRYPTION_KEY_ID,
+             host='host@backend#pool')
+ 
++        fake_driver.clone_image.return_value = (None, False)
+         fake_image_service = fake_image.FakeImageService()
+         image_meta = {}
+         image_id = fakes.IMAGE_ID
+@@ -1219,6 +1220,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
+                                         image_meta, fake_image_service)
+ 
+         fake_driver.create_volume.assert_called_once_with(volume)
++        fake_driver.clone_image.assert_called_once()
+         fake_driver.copy_image_to_encrypted_volume.assert_not_called()
+         fake_driver.copy_image_to_volume.assert_called_once_with(
+             self.ctxt, volume, fake_image_service, image_id,
+@@ -1228,6 +1230,50 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
+                                                      image_meta=image_meta)
+         mock_cleanup_cg.assert_called_once_with(volume)
+ 
++    @mock.patch('cinder.volume.flows.manager.create_volume.'
++                'CreateVolumeFromSpecTask.'
++                '_handle_bootable_volume_glance_meta')
++    @mock.patch('cinder.image.image_utils.TemporaryImages.fetch')
++    @mock.patch('cinder.image.image_utils.qemu_img_info')
++    @mock.patch('cinder.image.image_utils.check_virtual_size')
++    def test_create_encrypted_volume_from_enc_image_clone(
++        self, mock_check_size, mock_qemu_img,
++        mock_fetch_img, mock_handle_bootable
++    ):
++        fake_db = mock.MagicMock()
++        fake_driver = mock.MagicMock()
++        fake_volume_manager = mock.MagicMock()
++        fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
++            fake_volume_manager, fake_db, fake_driver)
++        volume = fake_volume.fake_volume_obj(
++            self.ctxt,
++            encryption_key_id=fakes.ENCRYPTION_KEY_ID,
++            host='host@backend#pool')
++
++        fake_driver.clone_image.return_value = (None, True)
++        fake_image_service = fake_image.FakeImageService()
++        image_meta = {}
++        image_id = fakes.IMAGE_ID
++        image_meta['id'] = image_id
++        image_meta['status'] = 'active'
++        image_meta['size'] = 1
++        image_meta['cinder_encryption_key_id'] = \
++            '00000000-0000-0000-0000-000000000000'
++        image_location = 'abc'
++
++        fake_db.volume_update.return_value = volume
++        fake_manager._create_from_image(self.ctxt, volume,
++                                        image_location, image_id,
++                                        image_meta, fake_image_service)
++
++        fake_driver.create_volume.assert_not_called()
++        fake_driver.clone_image.assert_called_once()
++        fake_driver.copy_image_to_encrypted_volume.assert_not_called()
++        fake_driver.copy_image_to_volume.assert_not_called()
++        mock_handle_bootable.assert_called_once_with(self.ctxt, volume,
++                                                     image_id=image_id,
++                                                     image_meta=image_meta)
++
+     @ddt.data({'driver_error': True},
+               {'driver_error': False})
+     @mock.patch('cinder.backup.api.API.get_available_backup_service_host')
+diff --git a/cinder/volume/flows/manager/create_volume.py b/cinder/volume/flows/manager/create_volume.py
+index ac09ed898..549a49b00 100644
+--- a/cinder/volume/flows/manager/create_volume.py
++++ b/cinder/volume/flows/manager/create_volume.py
+@@ -1087,11 +1087,16 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
+         # dict containing provider_location for cloned volume
+         # and clone status.
+         # NOTE (lixiaoy1): Currently all images are raw data, we can't
+-        # use clone_image to copy data if new volume is encrypted.
++        # use clone_image to copy data if new volume is encrypted
++        # NOTE (ricolin):  If the image provided an encryption key, we have
++        # already cloned it to the volume's key in
++        # _get_encryption_key_id, so we can do a direct clone.
++        image_encryption_key = image_meta.get('cinder_encryption_key_id')
+         volume_is_encrypted = volume.encryption_key_id is not None
+         cloned = False
+         model_update = None
+-        if not volume_is_encrypted:
++        if not volume_is_encrypted or (
++                volume_is_encrypted and image_encryption_key):
+             model_update, cloned = self.driver.clone_image(context,
+                                                            volume,
+                                                            image_location,
+diff --git a/releasenotes/notes/allow-clone-encryped-image-6961ca1439825dc4.yaml b/releasenotes/notes/allow-clone-encryped-image-6961ca1439825dc4.yaml
+new file mode 100644
+index 000000000..d6c7e8eb8
+--- /dev/null
++++ b/releasenotes/notes/allow-clone-encryped-image-6961ca1439825dc4.yaml
+@@ -0,0 +1,8 @@
++---
++features:
++  - |
++    Allow clone encrypted image when create encrypted volume from image.
++    Exactly like what we did in copy-and-import image when create encrypted
++    volume from encrypted image. If the image is encrypted, we will copy
++    `cinder_encryption_key_id` from image metadata to volume. That means we
++    should be safe to try directly clone from encrypted image.
+-- 
+2.34.1
diff --git a/images/cinder/patches/cinder/0003-Allow-encrypted-volume-clone-from-Glance-image.patch b/images/cinder/patches/cinder/0003-Allow-encrypted-volume-clone-from-Glance-image.patch
new file mode 100644
index 0000000..f436449
--- /dev/null
+++ b/images/cinder/patches/cinder/0003-Allow-encrypted-volume-clone-from-Glance-image.patch
@@ -0,0 +1,319 @@
+From 97953c8bd8c7d61a3f68c3e829ff79290315ec5b Mon Sep 17 00:00:00 2001
+From: ricolin <rlin@vexxhost.com>
+Date: Fri, 15 Mar 2024 23:26:14 +0800
+Subject: [PATCH 3/3] Allow encrypted volume clone from Glance image
+
+Allow clone image when creating encrypted volume from Glance image if both
+stored in RBD.
+Previously, Glance image clone is not supported for encrypted volume
+creation. The old process is to download image to local disk, encrypt the
+local file, and import it back to RBD. This not just slow, but also
+protentially take large amount of local disk space from hosts that runs
+Cinder volume service.
+The new process is to try and clone from Glance image (if it's also stored
+in RBD), flatten it, and encrypting new image in RBD for volume. And If
+Glance image source is not clonable, will continue with copy-and-import
+method as previous flow.
+In above flow, If clone from Glance image is appliable. Even it still
+requires to clone and flatten RBD image might took some time, but should
+still be a lot faster than copy-and-import. And also no local disk will
+be used to store raw image in this case.
+This also introduced driver method `clone_image_and_encrypt` for drivers
+that seperate the clone process from non-encrypted volume so the create
+flow won't be affected.
+
+Related-Bug: #2055517
+Change-Id: Ia023646d8bc9468bf5cc8955f7013299b2a3a460
+---
+ .../volume/flows/test_create_volume_flow.py   | 49 ++++++++++
+ cinder/volume/driver.py                       | 11 +++
+ cinder/volume/drivers/rbd.py                  | 95 ++++++++++++++++---
+ cinder/volume/flows/manager/create_volume.py  |  8 +-
+ ...for-encrypted-volume-de477647e9016b8b.yaml | 21 ++++
+ 5 files changed, 167 insertions(+), 17 deletions(-)
+ create mode 100644 releasenotes/notes/allow-clone-image-for-encrypted-volume-de477647e9016b8b.yaml
+
+diff --git a/cinder/tests/unit/volume/flows/test_create_volume_flow.py b/cinder/tests/unit/volume/flows/test_create_volume_flow.py
+index 6ff97aaa0..a85bf7eec 100644
+--- a/cinder/tests/unit/volume/flows/test_create_volume_flow.py
++++ b/cinder/tests/unit/volume/flows/test_create_volume_flow.py
+@@ -1164,6 +1164,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
+         image_location = 'abc'
+ 
+         fake_db.volume_update.return_value = volume
++        fake_driver.clone_image_and_encrypt.return_value = (None, False)
+         fake_manager._create_from_image(self.ctxt, volume,
+                                         image_location, image_id,
+                                         image_meta, fake_image_service)
+@@ -1178,6 +1179,54 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
+                                                      image_meta=image_meta)
+         mock_cleanup_cg.assert_called_once_with(volume)
+ 
++    @mock.patch('cinder.volume.flows.manager.create_volume.'
++                'CreateVolumeFromSpecTask.'
++                '_prepare_image_cache_entry')
++    @mock.patch('cinder.volume.flows.manager.create_volume.'
++                'CreateVolumeFromSpecTask.'
++                '_handle_bootable_volume_glance_meta')
++    @mock.patch('cinder.image.image_utils.TemporaryImages.fetch')
++    @mock.patch('cinder.image.image_utils.qemu_img_info')
++    @mock.patch('cinder.image.image_utils.check_virtual_size')
++    def test_create_encrypted_volume_from_image_clone(
++        self, mock_check_size, mock_qemu_img, mock_fetch_img,
++        mock_handle_bootable, mock_prepare_image_cache
++    ):
++        fake_db = mock.MagicMock()
++        fake_driver = mock.MagicMock()
++        fake_volume_manager = mock.MagicMock()
++        fake_cache = mock.MagicMock()
++        fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
++            fake_volume_manager, fake_db, fake_driver, fake_cache)
++        volume = fake_volume.fake_volume_obj(
++            self.ctxt,
++            encryption_key_id=fakes.ENCRYPTION_KEY_ID,
++            host='host@backend#pool')
++
++        fake_image_service = fake_image.FakeImageService()
++        image_meta = {}
++        image_id = fakes.IMAGE_ID
++        image_meta['id'] = image_id
++        image_meta['status'] = 'active'
++        image_meta['size'] = 1
++        image_location = 'abc'
++
++        fake_db.volume_update.return_value = volume
++        fake_driver.clone_image_and_encrypt.return_value = (None, True)
++        fake_manager._create_from_image(self.ctxt, volume,
++                                        image_location, image_id,
++                                        image_meta, fake_image_service)
++
++        mock_prepare_image_cache.assert_not_called()
++        fake_driver.create_volume.assert_not_called()
++        fake_driver.clone_image.assert_not_called()
++        fake_driver.clone_image_and_encrypt.assert_called_once()
++        fake_driver.copy_image_to_encrypted_volume.assert_not_called()
++        fake_driver.copy_image_to_volume.assert_not_called()
++        mock_handle_bootable.assert_called_once_with(self.ctxt, volume,
++                                                     image_id=image_id,
++                                                     image_meta=image_meta)
++
+     @mock.patch('cinder.volume.flows.manager.create_volume.'
+                 'CreateVolumeFromSpecTask.'
+                 '_cleanup_cg_in_volume')
+diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py
+index 2ff27564b..030b4a8dd 100644
+--- a/cinder/volume/driver.py
++++ b/cinder/volume/driver.py
+@@ -1192,6 +1192,17 @@ class BaseVD(object, metaclass=abc.ABCMeta):
+         """
+         return None, False
+ 
++    def clone_image_and_encrypt(
++        self, context, volume, image_location, image_meta, image_service
++    ):
++        """Create and encrypt a volume efficiently from an existing image.
++
++        Refer to
++        :obj:`cinder.interface.volume_driver.VolumeDriverCore.clone_image`
++        for additional information.
++        """
++        return None, False
++
+     def backup_use_temp_snapshot(self):
+         """Get the configured setting for backup from snapshot.
+ 
+diff --git a/cinder/volume/drivers/rbd.py b/cinder/volume/drivers/rbd.py
+index aace801f3..ad0eea9d5 100644
+--- a/cinder/volume/drivers/rbd.py
++++ b/cinder/volume/drivers/rbd.py
+@@ -141,6 +141,13 @@ CONF.register_opts(RBD_OPTS, group=configuration.SHARED_CONF_GROUP)
+ EXTRA_SPECS_REPL_ENABLED = "replication_enabled"
+ EXTRA_SPECS_MULTIATTACH = "multiattach"
+ 
++# Note(ricolin): Reference ceph site for more information:
++# https://github.com/ceph/ceph/blob/main/src/include/rbd/librbd.h
++RBD_ENCRYPTION_ALG = {
++    'aes-128': 0,
++    'aes-256': 1
++}
++
+ QOS_KEY_MAP = {
+     'total_iops_sec': {
+         'ceph_key': 'rbd_qos_iops_limit',
+@@ -1190,6 +1197,20 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
+ 
+         return max(image_stripe_unit, default_stripe_unit)
+ 
++    def _encrypt_volume(self,
++                        context: context.RequestContext,
++                        volume: Volume,
++                        passphrase: str,
++                        cipher_spec: dict
++                        ) -> None:
++        LOG.debug("Encrypting volume $s", volume.name)
++        with RBDVolumeProxy(self, volume.name) as vol:
++            vol.encryption_format(
++                0,
++                passphrase,
++                RBD_ENCRYPTION_ALG[cipher_spec['cipher_alg']]
++            )
++
+     def _clone(self,
+                volume: Volume,
+                src_pool: str,
+@@ -1873,6 +1894,37 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
+                     image_location: Optional[list],
+                     image_meta: dict,
+                     image_service) -> tuple[dict, bool]:
++        return self._clone_image(context, volume, image_location,
++                                 image_meta, image_service)
++
++    def clone_image_and_encrypt(
++        self,
++        context: context.RequestContext,
++        volume: Volume,
++        image_location: Optional[list],
++        image_meta: dict,
++        image_service
++    ) -> tuple[dict, bool]:
++
++        # Note(ricolin): method `encryption_format` added after Ceph Pacific
++        # release (>=16.1.0).
++        if self.rbd and hasattr(
++            self.rbd.Image, 'encryption_format') and callable(
++                self.rbd.Image.encryption_format):
++            return self._clone_image(
++                context, volume, image_location,
++                image_meta, image_service, is_encrypt=True)
++        else:
++            return {}, False
++
++    def _clone_image(self,
++                     context: context.RequestContext,
++                     volume: Volume,
++                     image_location: Optional[list],
++                     image_meta: dict,
++                     image_service,
++                     is_encrypt: Optional[bool] = False
++                     ) -> tuple[dict, bool]:
+         if image_location:
+             # Note: image_location[0] is glance image direct_url.
+             # image_location[1] contains the list of all locations (including
+@@ -1890,12 +1942,41 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
+                         url_location, image_meta):
+                     _prefix, pool, image, snapshot = \
+                         self._parse_location(url_location)
++                    if is_encrypt:
++                        passphrase, cipher_spec = self._fetch_encryption_info(
++                            context, volume)
++                        if cipher_spec['cipher_alg'] not in RBD_ENCRYPTION_ALG:
++                            LOG.debug(
++                                "Skip clone. Cipher spec: %s not supported "
++                                "for encrypt volume directly from RBD.",
++                                cipher_spec)
++                            return ({}, False)
+                     volume_update = self._clone(volume, pool, image, snapshot)
++                    if is_encrypt:
++                        self._flatten(self.configuration.rbd_pool, volume.name)
++                        self._encrypt_volume(
++                            context, volume, passphrase, cipher_spec)
+                     volume_update['provider_location'] = None
+                     self._resize(volume)
+                     return volume_update, True
+         return ({}, False)
+ 
++    def _fetch_encryption_info(self,
++                               context: context.RequestContext,
++                               volume: Volume) -> tuple[str, dict]:
++        encryption = volume_utils.check_encryption_provider(
++            volume,
++            context)
++        # Fetch the key associated with the volume and decode the passphrase
++        keymgr = key_manager.API(CONF)
++        key = keymgr.get(context, encryption['encryption_key_id'])
++        passphrase = binascii.hexlify(key.get_encoded()).decode('utf-8')
++
++        # Decode the dm-crypt style cipher spec into something qemu-img can use
++        cipher_spec = image_utils.decode_cipher(encryption['cipher'],
++                                                encryption['key_size'])
++        return passphrase, cipher_spec
++
+     def copy_image_to_encrypted_volume(self,
+                                        context: context.RequestContext,
+                                        volume: Volume,
+@@ -1920,18 +2001,8 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
+                        volume: Volume,
+                        tmp_dir: str,
+                        src_image_path: Any) -> None:
+-        encryption = volume_utils.check_encryption_provider(
+-            volume,
+-            context)
+-
+-        # Fetch the key associated with the volume and decode the passphrase
+-        keymgr = key_manager.API(CONF)
+-        key = keymgr.get(context, encryption['encryption_key_id'])
+-        passphrase = binascii.hexlify(key.get_encoded()).decode('utf-8')
+-
+-        # Decode the dm-crypt style cipher spec into something qemu-img can use
+-        cipher_spec = image_utils.decode_cipher(encryption['cipher'],
+-                                                encryption['key_size'])
++        passphrase, cipher_spec = self._fetch_encryption_info(
++            context, volume)
+ 
+         tmp_dir = volume_utils.image_conversion_dir()
+ 
+diff --git a/cinder/volume/flows/manager/create_volume.py b/cinder/volume/flows/manager/create_volume.py
+index 549a49b00..8ea4c0fe1 100644
+--- a/cinder/volume/flows/manager/create_volume.py
++++ b/cinder/volume/flows/manager/create_volume.py
+@@ -1086,11 +1086,6 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
+         # NOTE (singn): two params need to be returned
+         # dict containing provider_location for cloned volume
+         # and clone status.
+-        # NOTE (lixiaoy1): Currently all images are raw data, we can't
+-        # use clone_image to copy data if new volume is encrypted
+-        # NOTE (ricolin):  If the image provided an encryption key, we have
+-        # already cloned it to the volume's key in
+-        # _get_encryption_key_id, so we can do a direct clone.
+         image_encryption_key = image_meta.get('cinder_encryption_key_id')
+         volume_is_encrypted = volume.encryption_key_id is not None
+         cloned = False
+@@ -1102,6 +1097,9 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
+                                                            image_location,
+                                                            image_meta,
+                                                            image_service)
++        else:
++            model_update, cloned = self.driver.clone_image_and_encrypt(
++                context, volume, image_location, image_meta, image_service)
+ 
+         # Try and clone the image if we have it set as a glance location.
+         if not cloned and 'cinder' in CONF.allowed_direct_url_schemes:
+diff --git a/releasenotes/notes/allow-clone-image-for-encrypted-volume-de477647e9016b8b.yaml b/releasenotes/notes/allow-clone-image-for-encrypted-volume-de477647e9016b8b.yaml
+new file mode 100644
+index 000000000..63d1f38cd
+--- /dev/null
++++ b/releasenotes/notes/allow-clone-image-for-encrypted-volume-de477647e9016b8b.yaml
+@@ -0,0 +1,21 @@
++---
++features:
++  - |
++    Allow clone image when creating encrypted volume from Glance image if both
++    stored in RBD.
++    Previously, Glance image clone is not supported for encrypted volume
++    creation. The old process is to download image to local disk, encrypt the
++    local file, and import it back to RBD. This not just slow, but also
++    protentially take large amount of local disk space from hosts that runs
++    Cinder volume service.
++    The new process is to try and clone from Glance image (if it's also stored
++    in RBD), flatten it, and encrypting new image in RBD for volume. And If
++    Glance image source is not clonable, will continue with copy-and-import
++    method as previous flow.
++    In above flow, If clone from Glance image is appliable. Even it still
++    requires to clone and flatten RBD image might took some time, but should
++    still be a lot faster than copy-and-import. And also no local disk will
++    be used to store raw image in this case.
++    This also introduced driver method `clone_image_and_encrypt` for drivers
++    that seperate the clone process from non-encrypted volume so the create
++    flow won't be affected.
+-- 
+2.34.1
diff --git a/images/cloud-archive-base/Earthfile b/images/cloud-archive-base/Earthfile
deleted file mode 100644
index 473d5d8..0000000
--- a/images/cloud-archive-base/Earthfile
+++ /dev/null
@@ -1,23 +0,0 @@
-VERSION 0.7
-
-image:
-  FROM ../base+image
-  DO ../+APT_INSTALL --PACKAGES "ca-certificates libpython3.10 lsb-release python3-distutils sudo ubuntu-cloud-keyring"
-  ARG RELEASE
-  IF [ "$(lsb_release -sc)" = "jammy" ]
-    IF [ "${RELEASE}" = "yoga" ]
-      # NOTE: Yoga shipped with 22.04, so no need to add an extra repository.
-      RUN echo "" > /etc/apt/sources.list.d/cloudarchive.list
-    ELSE IF [ "${RELEASE}" = "zed" ]
-      RUN echo "deb http://ubuntu-cloud.archive.canonical.com/ubuntu $(lsb_release -sc)-updates/${RELEASE} main" > /etc/apt/sources.list.d/cloudarchive.list
-    ELSE IF [ "${RELEASE}" = "2023.1" ]
-      RUN echo "deb http://ubuntu-cloud.archive.canonical.com/ubuntu $(lsb_release -sc)-updates/antelope main" > /etc/apt/sources.list.d/cloudarchive.list
-    ELSE IF [ "${RELEASE}" = "2023.2" ]
-      RUN echo "deb http://ubuntu-cloud.archive.canonical.com/ubuntu $(lsb_release -sc)-updates/bobcat main" > /etc/apt/sources.list.d/cloudarchive.list
-    ELSE IF [ "${RELEASE}" = "master" ]
-      RUN echo "deb http://ubuntu-cloud.archive.canonical.com/ubuntu $(lsb_release -sc)-updates/caracal main" > /etc/apt/sources.list.d/cloudarchive.list
-    ELSE
-      RUN echo "${RELEASE} is not supported on $(lsb_release -sc)"
-      RUN exit 1
-    END
-  END
diff --git a/images/cluster-api-provider-openstack/Dockerfile b/images/cluster-api-provider-openstack/Dockerfile
new file mode 100644
index 0000000..aa05489
--- /dev/null
+++ b/images/cluster-api-provider-openstack/Dockerfile
@@ -0,0 +1,32 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM alpine/git:2.43.0 AS src
+ARG CAPO_VERSION=v0.9.0
+ADD https://github.com/kubernetes-sigs/cluster-api-provider-openstack.git#${CAPO_VERSION} /src
+WORKDIR /src
+COPY /patches /patches
+RUN git apply /patches/*.patch
+
+FROM golang:1.20 AS builder
+COPY --from=src --link /src /src
+WORKDIR /src
+ARG ARCH
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} \
+    go build -ldflags "-extldflags '-static'" -o manager ${package}
+
+FROM gcr.io/distroless/static:nonroot
+COPY --from=builder /src/manager /manager
+USER 65532
+ENTRYPOINT ["/manager"]
diff --git a/images/cluster-api-provider-openstack/Earthfile b/images/cluster-api-provider-openstack/Earthfile
deleted file mode 100644
index 111f465..0000000
--- a/images/cluster-api-provider-openstack/Earthfile
+++ /dev/null
@@ -1,18 +0,0 @@
-VERSION 0.7
-
-ARG --global CAPO_VERSION=v0.8.0
-ARG --global EPOCH=2
-
-clone:
-  FROM ../builder+image
-  GIT CLONE --branch ${CAPO_VERSION} https://github.com/kubernetes-sigs/cluster-api-provider-openstack /workspace/src
-  WORKDIR /workspace/src
-  COPY patches /workspace/patches
-  RUN git apply --verbose /workspace/patches/*.patch
-  SAVE ARTIFACT /workspace/src
-
-image:
-  FROM DOCKERFILE -f +clone/src/Dockerfile +clone/src/*
-  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push ${REGISTRY}/capi-openstack-controller:${CAPO_VERSION}-${EPOCH}
diff --git a/images/cluster-api-provider-openstack/patches/0001-chore-bump-k8s-api-for-cve.patch b/images/cluster-api-provider-openstack/patches/0001-chore-bump-k8s-api-for-cve.patch
index 2812ac5..cd99927 100644
--- a/images/cluster-api-provider-openstack/patches/0001-chore-bump-k8s-api-for-cve.patch
+++ b/images/cluster-api-provider-openstack/patches/0001-chore-bump-k8s-api-for-cve.patch
@@ -1,158 +1,89 @@
-From 139a57e7b0d4c57033e281b061e459039a5e21d3 Mon Sep 17 00:00:00 2001
+From eed5b5cc2a6cf48c0c9e0245695d0ac143150186 Mon Sep 17 00:00:00 2001
 From: Mohammed Naser <mnaser@vexxhost.com>
-Date: Mon, 22 Jan 2024 16:22:52 -0500
-Subject: [PATCH 2/2] chore: bump k8s api for cve
+Date: Tue, 12 Mar 2024 18:18:25 -0400
+Subject: [PATCH] chore: bump k8s api for cve
 
 ---
- go.mod | 17 +++++++++--------
- go.sum | 36 +++++++++++++++++++-----------------
- 2 files changed, 28 insertions(+), 25 deletions(-)
+ go.mod |  8 ++++----
+ go.sum | 16 ++++++++--------
+ 2 files changed, 12 insertions(+), 12 deletions(-)
 
 diff --git a/go.mod b/go.mod
-index db4a954a..49d2f7cf 100644
+index 997f8354..d6c300cc 100644
 --- a/go.mod
 +++ b/go.mod
-@@ -15,8 +15,8 @@ require (
- 	github.com/onsi/gomega v1.27.8
- 	github.com/prometheus/client_golang v1.16.0
+@@ -15,7 +15,7 @@ require (
+ 	github.com/onsi/gomega v1.30.0
+ 	github.com/prometheus/client_golang v1.17.0
  	github.com/spf13/pflag v1.0.5
--	golang.org/x/crypto v0.11.0
--	golang.org/x/text v0.11.0
-+	golang.org/x/crypto v0.14.0
-+	golang.org/x/text v0.13.0
+-	golang.org/x/crypto v0.15.0
++	golang.org/x/crypto v0.17.0
+ 	golang.org/x/text v0.14.0
  	gopkg.in/ini.v1 v1.67.0
- 	k8s.io/api v0.27.2
- 	k8s.io/apiextensions-apiserver v0.27.2
+ 	k8s.io/api v0.28.4
 @@ -24,7 +24,7 @@ require (
- 	k8s.io/client-go v0.27.2
- 	k8s.io/component-base v0.27.2
- 	k8s.io/klog/v2 v2.90.1
--	k8s.io/kubernetes v1.27.2
-+	k8s.io/kubernetes v1.27.8
- 	k8s.io/utils v0.0.0-20230313181309-38a27ef9d749
- 	sigs.k8s.io/cluster-api v1.5.1
- 	sigs.k8s.io/cluster-api/test v1.5.1
-@@ -113,15 +113,16 @@ require (
- 	go.uber.org/multierr v1.11.0 // indirect
- 	go.uber.org/zap v1.24.0 // indirect
- 	golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
--	golang.org/x/net v0.13.0 // indirect
-+	golang.org/x/net v0.17.0 // indirect
- 	golang.org/x/oauth2 v0.10.0 // indirect
--	golang.org/x/sys v0.10.0 // indirect
--	golang.org/x/term v0.10.0 // indirect
-+	golang.org/x/sys v0.13.0 // indirect
-+	golang.org/x/term v0.13.0 // indirect
+ 	k8s.io/client-go v0.28.4
+ 	k8s.io/component-base v0.28.4
+ 	k8s.io/klog/v2 v2.100.1
+-	k8s.io/kubernetes v1.28.3
++	k8s.io/kubernetes v1.28.4
+ 	k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
+ 	sigs.k8s.io/cluster-api v1.6.0
+ 	sigs.k8s.io/cluster-api/test v1.6.0
+@@ -139,8 +139,8 @@ require (
+ 	golang.org/x/net v0.18.0 // indirect
+ 	golang.org/x/oauth2 v0.14.0 // indirect
+ 	golang.org/x/sync v0.4.0 // indirect
+-	golang.org/x/sys v0.14.0 // indirect
+-	golang.org/x/term v0.14.0 // indirect
++	golang.org/x/sys v0.15.0 // indirect
++	golang.org/x/term v0.15.0 // indirect
  	golang.org/x/time v0.3.0 // indirect
--	golang.org/x/tools v0.9.3 // indirect
-+	golang.org/x/tools v0.12.0 // indirect
- 	gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
- 	google.golang.org/appengine v1.6.7 // indirect
--	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
-+	google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a // indirect
-+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
- 	google.golang.org/protobuf v1.31.0 // indirect
- 	gopkg.in/inf.v0 v0.9.1 // indirect
- 	gopkg.in/yaml.v2 v2.4.0 // indirect
+ 	golang.org/x/tools v0.14.0 // indirect
+ 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 diff --git a/go.sum b/go.sum
-index 66bd8109..f18ece49 100644
+index e3d46fdc..f5767735 100644
 --- a/go.sum
 +++ b/go.sum
-@@ -516,8 +516,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
+@@ -460,8 +460,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
  golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
  golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
  golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
--golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
--golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
-+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+-golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
+-golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
++golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
++golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
  golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
  golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
  golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-@@ -555,7 +555,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
- golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
- golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
- golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
--golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
-+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
- golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
- golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
- golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-@@ -596,8 +596,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
- golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
- golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
- golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
--golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
--golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
-+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
- golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
- golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
- golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-@@ -673,13 +673,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
- golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+@@ -609,13 +609,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
  golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
  golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
--golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
--golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+-golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
+-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
++golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
++golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
  golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
  golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
  golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
--golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
--golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
-+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
-+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+-golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
+-golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
++golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
++golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
  golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
  golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
  golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-@@ -690,8 +690,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
- golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
- golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
- golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
--golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
--golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
-+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
- golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
- golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
- golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-@@ -752,8 +752,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
- golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
- golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
- golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
--golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
--golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
-+golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
-+golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
- golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
- golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
- golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-@@ -825,8 +825,10 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
- google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
- google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
- google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
--google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
--google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
-+google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a h1:HiYVD+FGJkTo+9zj1gqz0anapsa1JxjiSrN+BJKyUmE=
-+google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8=
-+google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
-+google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
- google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
- google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
- google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-@@ -914,8 +916,8 @@ k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
- k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
- k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
- k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
--k8s.io/kubernetes v1.27.2 h1:g4v9oY6u7vBUDEuq4FvC50Bbw2K7GZuvM00IIESWVf4=
--k8s.io/kubernetes v1.27.2/go.mod h1:U8ZXeKBAPxeb4J4/HOaxjw1A9K6WfSH+fY2SS7CR6IM=
-+k8s.io/kubernetes v1.27.8 h1:K848lTo/D0jvrxUlTvw4nNADixbhXLHgKNDP/KlFGy8=
-+k8s.io/kubernetes v1.27.8/go.mod h1:PUXXrx0IhAi+kI9BMDqNJHUnLndVv9W0DkriqyjuJOs=
- k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 h1:xMMXJlJbsU8w3V5N2FLDQ8YgU8s1EoULdbQBcAeNJkY=
- k8s.io/utils v0.0.0-20230313181309-38a27ef9d749/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+@@ -841,8 +841,8 @@ k8s.io/kms v0.28.4 h1:PMgY/3CQTWP9eIKmNQiTgjLIZ0ns6O+voagzD2/4mSg=
+ k8s.io/kms v0.28.4/go.mod h1:HL4/lR/bhjAJPbqycKtfhWiKh1Sp21cpHOL8P4oo87w=
+ k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
+ k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
+-k8s.io/kubernetes v1.28.3 h1:XTci6gzk+JR51UZuZQCFJ4CsyUkfivSjLI4O1P9z6LY=
+-k8s.io/kubernetes v1.28.3/go.mod h1:NhAysZWvHtNcJFFHic87ofxQN7loylCQwg3ZvXVDbag=
++k8s.io/kubernetes v1.28.4 h1:aRNxs5jb8FVTtlnxeA4FSDBVKuFwA8Gw40/U2zReBYA=
++k8s.io/kubernetes v1.28.4/go.mod h1:BTzDCKYAlu6LL9ITbfjwgwIrJ30hlTgbv0eXDoA/WoA=
+ k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
+ k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
  rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 -- 
 2.43.0
-
diff --git a/images/curl/Earthfile b/images/curl/Earthfile
deleted file mode 100644
index 06d3d8e..0000000
--- a/images/curl/Earthfile
+++ /dev/null
@@ -1,5 +0,0 @@
-VERSION 0.7
-
-image:
-  FROM curlimages/curl:7.78.0
-  WORKDIR /tmp
diff --git a/images/designate/Dockerfile b/images/designate/Dockerfile
new file mode 100644
index 0000000..c5430ad
--- /dev/null
+++ b/images/designate/Dockerfile
@@ -0,0 +1,33 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG DESIGNATE_GIT_REF=097ffc6df181290eba1bcd7c492b1b505bc15434
+ADD --keep-git-dir=true https://opendev.org/openstack/designate.git#${DESIGNATE_GIT_REF} /src/designate
+RUN git -C /src/designate fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/designate
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    bind9utils
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/designate/Earthfile b/images/designate/Earthfile
deleted file mode 100644
index 65e5af8..0000000
--- a/images/designate/Earthfile
+++ /dev/null
@@ -1,21 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=designate
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=2c817b3d7f01de44023f195c6e8de8853683a54a
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "bind9utils"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/glance/Dockerfile b/images/glance/Dockerfile
new file mode 100644
index 0000000..f408dfc
--- /dev/null
+++ b/images/glance/Dockerfile
@@ -0,0 +1,37 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG GLANCE_GIT_REF=0bcd6cd71c09917c6734421374fd598d73e8d0cc
+ADD --keep-git-dir=true https://opendev.org/openstack/glance.git#${GLANCE_GIT_REF} /src/glance
+RUN git -C /src/glance fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/glance_store.git#master /src/glance_store
+RUN git -C /src/glance_store fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/glance \
+        /src/glance_store[cinder]
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    ceph-common lsscsi nvme-cli python3-rados python3-rbd qemu-utils sysfsutils udev util-linux
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+ADD --chmod=755 https://dl.k8s.io/release/v1.29.3/bin/linux/amd64/kubectl /usr/local/bin/kubectl
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/glance/Earthfile b/images/glance/Earthfile
deleted file mode 100644
index 2646670..0000000
--- a/images/glance/Earthfile
+++ /dev/null
@@ -1,32 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=glance
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=9a4a3067b5c7c7f8ee9363bd939a3d86b260d660
-
-build.plugin:
-  ARG PLUGIN
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+GIT_CHECKOUT \
-    --PROJECT=${PLUGIN} \
-    --PROJECT_REF=stable/${RELEASE}
-  SAVE ARTIFACT /src
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  COPY (+build.plugin/src --PLUGIN=glance_store) /glance_store
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES="/glance_store[cinder]"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  COPY ../kubernetes+image/kubectl /usr/local/bin/kubectl
-  DO ../+APT_INSTALL \
-    --PACKAGES "ceph-common lsscsi nvme-cli python3-rados python3-rbd qemu-utils sysfsutils udev util-linux"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/glance/patches/glance_store/0000-rbd-compute-appropriate-resize-amount-before-resizin.patch b/images/glance/patches/glance_store/0000-rbd-compute-appropriate-resize-amount-before-resizin.patch
deleted file mode 100644
index 3f482cb..0000000
--- a/images/glance/patches/glance_store/0000-rbd-compute-appropriate-resize-amount-before-resizin.patch
+++ /dev/null
@@ -1,145 +0,0 @@
-From fa43561078bbee531ccc8acf2e185b3fc6d98b2a Mon Sep 17 00:00:00 2001
-From: Andrew Bogott <abogott@wikimedia.org>
-Date: Thu, 8 Jun 2023 07:54:16 -0500
-Subject: [PATCH] rbd: compute appropriate resize amount before resizing image
-
-Resolves a bug introduced in
-
-https://opendev.org/openstack/glance_store/commit/c43f19e8456b9e20f03709773fb2ffdb94807a0a
-
-This issue is only in evidence when glance is behind a proxy where the
-client buffer size can be lower (for haproxy: bufsize = 16384) which
-can cause unaligned reads
-
-(https://github.com/openstack/glance/blob/master/glance/common/wsgi.py#L1028).
-
-The response length can be bigger than the store_chunk_size for the
-first time, so at the end the RBD write will fail because it wants
-to write more data than the actual RBD image size after the first
-resize.
-
-Thanks to Robert Varjasi for investigating this issue!
-
-Fixes-Bug: 1916482
-Change-Id: Ie03693c2cb8b096978fb156231c3b1cab695470f
----
- glance_store/_drivers/rbd.py              |  6 ++--
- glance_store/tests/unit/test_rbd_store.py | 38 ++++++++++++++---------
- 2 files changed, 26 insertions(+), 18 deletions(-)
-
-diff --git a/glance_store/_drivers/rbd.py b/glance_store/_drivers/rbd.py
-index ba2defa..e53baef 100644
---- a/glance_store/_drivers/rbd.py
-+++ b/glance_store/_drivers/rbd.py
-@@ -535,12 +535,12 @@ class Store(driver.Store):
-         """Handle the rbd resize when needed."""
-         if image_size != 0 or self.size >= bytes_written + chunk_length:
-             return self.size
--        new_size = self.size + self.resize_amount
--        LOG.debug("resizing image to %s KiB" % (new_size / units.Ki))
--        image.resize(new_size)
-         # Note(jokke): We double how much we grow the image each time
-         # up to 8gigs to avoid resizing for each write on bigger images
-         self.resize_amount = min(self.resize_amount * 2, 8 * units.Gi)
-+        new_size = self.size + self.resize_amount
-+        LOG.debug("resizing image to %s KiB" % (new_size / units.Ki))
-+        image.resize(new_size)
-         return new_size
- 
-     @driver.back_compat_add
-diff --git a/glance_store/tests/unit/test_rbd_store.py b/glance_store/tests/unit/test_rbd_store.py
-index 4f24c26..fb6522a 100644
---- a/glance_store/tests/unit/test_rbd_store.py
-+++ b/glance_store/tests/unit/test_rbd_store.py
-@@ -213,10 +213,10 @@ class TestReSize(base.StoreBaseTest,
-                 data_len_temp = data_len
-                 resize_amount = self.store.WRITE_CHUNKSIZE
-                 while data_len_temp > 0:
-+                    resize_amount *= 2
-                     expected_calls.append(resize_amount + (data_len -
-                                                            data_len_temp))
-                     data_len_temp -= resize_amount
--                    resize_amount *= 2
-                     expected += 1
-                 self.assertEqual(expected, resize.call_count)
-                 resize.assert_has_calls([mock.call(call) for call in
-@@ -244,7 +244,7 @@ class TestReSize(base.StoreBaseTest,
-         # Current size is smaller than we need
-         self.store.size = 8
-         ret = self.store._resize_on_write(image, 0, 16, 16)
--        self.assertEqual(8 + self.store.WRITE_CHUNKSIZE, ret)
-+        self.assertEqual(8 + self.store.WRITE_CHUNKSIZE * 2, ret)
-         self.assertEqual(self.store.WRITE_CHUNKSIZE * 2,
-                          self.store.resize_amount)
-         image.resize.assert_called_once_with(ret)
-@@ -253,47 +253,55 @@ class TestReSize(base.StoreBaseTest,
-         image.resize.reset_mock()
-         self.store.size = ret
-         ret = self.store._resize_on_write(image, 0, 64, 16)
--        self.assertEqual(8 + self.store.WRITE_CHUNKSIZE, ret)
-+        self.assertEqual(8 + self.store.WRITE_CHUNKSIZE * 2, ret)
-         image.resize.assert_not_called()
- 
-         # Read past the limit triggers another resize
-         ret = self.store._resize_on_write(image, 0, ret + 1, 16)
--        self.assertEqual(8 + self.store.WRITE_CHUNKSIZE * 3, ret)
-+        self.assertEqual(8 + self.store.WRITE_CHUNKSIZE * 6, ret)
-         image.resize.assert_called_once_with(ret)
-         self.assertEqual(self.store.WRITE_CHUNKSIZE * 4,
-                          self.store.resize_amount)
- 
-         # Check that we do not resize past the 8G ceiling.
- 
--        # Start with resize_amount at 4G, 1G read so far
-+        # Start with resize_amount at 2G, 1G read so far
-         image.resize.reset_mock()
--        self.store.resize_amount = 4 * units.Gi
-+        self.store.resize_amount = 2 * units.Gi
-         self.store.size = 1 * units.Gi
- 
--        # First resize happens and we get the 4G,
--        # resize_amount goes to limit of 8G
-+        # First resize happens and we get to 5G,
-+        # resize_amount goes to limit of 4G
-         ret = self.store._resize_on_write(image, 0, 4097 * units.Mi, 16)
--        self.assertEqual(5 * units.Gi, ret)
--        self.assertEqual(8 * units.Gi, self.store.resize_amount)
-+        self.assertEqual(4 * units.Gi, self.store.resize_amount)
-+        self.assertEqual((1 + 4) * units.Gi, ret)
-         self.store.size = ret
- 
--        # Second resize happens and we get to 13G,
-+        # Second resize happens and we stay at 13, no resize
-         # resize amount stays at limit of 8G
-         ret = self.store._resize_on_write(image, 0, 6144 * units.Mi, 16)
--        self.assertEqual((5 + 8) * units.Gi, ret)
-         self.assertEqual(8 * units.Gi, self.store.resize_amount)
-+        self.assertEqual((1 + 4 + 8) * units.Gi, ret)
-         self.store.size = ret
- 
--        # Third resize happens and we get to 21G,
-+        # Third resize happens and we get to 21,
-         # resize amount stays at limit of 8G
-         ret = self.store._resize_on_write(image, 0, 14336 * units.Mi, 16)
--        self.assertEqual((5 + 8 + 8) * units.Gi, ret)
-         self.assertEqual(8 * units.Gi, self.store.resize_amount)
-+        self.assertEqual((1 + 4 + 8 + 8) * units.Gi, ret)
-+        self.store.size = ret
-+
-+        # Fourth resize happens and we get to 29,
-+        # resize amount stays at limit of 8G
-+        ret = self.store._resize_on_write(image, 0, 22528 * units.Mi, 16)
-+        self.assertEqual(8 * units.Gi, self.store.resize_amount)
-+        self.assertEqual((1 + 4 + 8 + 8 + 8) * units.Gi, ret)
- 
-         image.resize.assert_has_calls([
-             mock.call(5 * units.Gi),
-             mock.call(13 * units.Gi),
--            mock.call(21 * units.Gi)])
-+            mock.call(21 * units.Gi),
-+            mock.call(29 * units.Gi)])
- 
- 
- class TestStore(base.StoreBaseTest,
--- 
-2.34.1
diff --git a/images/heat/Dockerfile b/images/heat/Dockerfile
new file mode 100644
index 0000000..d2f088c
--- /dev/null
+++ b/images/heat/Dockerfile
@@ -0,0 +1,33 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG HEAT_GIT_REF=80eea85194825773d1b60ecc4386b2d5ba52a066
+ADD --keep-git-dir=true https://opendev.org/openstack/heat.git#${HEAT_GIT_REF} /src/heat
+RUN git -C /src/heat fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/heat
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    curl jq
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/heat/Earthfile b/images/heat/Earthfile
deleted file mode 100644
index 8c3ebcd..0000000
--- a/images/heat/Earthfile
+++ /dev/null
@@ -1,21 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=heat
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=d1363cc17646893054f9e8daf40de67699078e7c
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "curl jq"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/helm/Earthfile b/images/helm/Earthfile
deleted file mode 100644
index d0b5155..0000000
--- a/images/helm/Earthfile
+++ /dev/null
@@ -1,11 +0,0 @@
-VERSION 0.7
-
-binary:
-  FROM ../curl+image
-  ARG TARGETOS
-  ARG TARGETARCH
-  ARG VERSION=3.14.0
-  RUN curl -LO https://get.helm.sh/helm-v3.14.0-${TARGETOS}-${TARGETARCH}.tar.gz
-  RUN tar -zxvf /tmp/helm-v3.14.0-${TARGETOS}-${TARGETARCH}.tar.gz
-  RUN ${TARGETOS}-${TARGETARCH}/helm version
-  SAVE ARTIFACT ${TARGETOS}-${TARGETARCH}/helm
diff --git a/images/horizon/Dockerfile b/images/horizon/Dockerfile
new file mode 100644
index 0000000..2f80436
--- /dev/null
+++ b/images/horizon/Dockerfile
@@ -0,0 +1,60 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG HORIZON_GIT_REF=3f1f1d46e6e47a3dbe46fb023fe69ff25d6a601b
+ADD --keep-git-dir=true https://opendev.org/openstack/horizon.git#${HORIZON_GIT_REF} /src/horizon
+RUN git -C /src/horizon fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/designate-dashboard.git#master /src/designate-dashboard
+RUN git -C /src/designate-dashboard fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/heat-dashboard.git#master /src/heat-dashboard
+RUN git -C /src/heat-dashboard fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/ironic-ui.git#master /src/ironic-ui
+RUN git -C /src/ironic-ui fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/magnum-ui.git#master /src/magnum-ui
+RUN git -C /src/magnum-ui fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/manila-ui.git#master /src/manila-ui
+RUN git -C /src/manila-ui fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/neutron-vpnaas-dashboard.git#master /src/neutron-vpnaas-dashboard
+RUN git -C /src/neutron-vpnaas-dashboard fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/octavia-dashboard.git#master /src/octavia-dashboard
+RUN git -C /src/octavia-dashboard fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/senlin-dashboard.git#master /src/senlin-dashboard
+RUN git -C /src/senlin-dashboard fetch --unshallow
+COPY patches/magnum-ui /patches/magnum-ui
+RUN git -C /src/magnum-ui apply --verbose /patches/magnum-ui/*
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/designate-dashboard \
+        /src/heat-dashboard \
+        /src/horizon \
+        /src/ironic-ui \
+        /src/magnum-ui \
+        /src/manila-ui \
+        /src/neutron-vpnaas-dashboard \
+        /src/octavia-dashboard \
+        /src/senlin-dashboard \
+        pymemcache
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    apache2 gettext libapache2-mod-wsgi-py3
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/horizon/Earthfile b/images/horizon/Earthfile
deleted file mode 100644
index 4a895ce..0000000
--- a/images/horizon/Earthfile
+++ /dev/null
@@ -1,38 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=horizon
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=4de36bb649c514f50d2a958c9277097a08b23cec
-
-build.plugin:
-  ARG PLUGIN
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+GIT_CHECKOUT \
-    --PROJECT=${PLUGIN} \
-    --PROJECT_REF=stable/${RELEASE}
-  SAVE ARTIFACT /src
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  COPY (+build.plugin/src --PLUGIN=designate-dashboard) /designate-dashboard
-  COPY (+build.plugin/src --PLUGIN=heat-dashboard) /heat-dashboard
-  COPY (+build.plugin/src --PLUGIN=ironic-ui) /ironic-ui
-  COPY (+build.plugin/src --PLUGIN=magnum-ui) /magnum-ui
-  COPY (+build.plugin/src --PLUGIN=manila-ui) /manila-ui
-  COPY (+build.plugin/src --PLUGIN=neutron-vpnaas-dashboard) /neutron-vpnaas-dashboard
-  COPY (+build.plugin/src --PLUGIN=octavia-dashboard) /octavia-dashboard
-  COPY (+build.plugin/src --PLUGIN=senlin-dashboard) /senlin-dashboard
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES "/designate-dashboard /heat-dashboard /ironic-ui /magnum-ui /neutron-vpnaas-dashboard /octavia-dashboard /senlin-dashboard /manila-ui"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "apache2 gettext libapache2-mod-wsgi-py3"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/horizon/patches/horizon/0000-fix-ignore-errors-when-flavors-are-deleted.patch b/images/horizon/patches/horizon/0000-fix-ignore-errors-when-flavors-are-deleted.patch
deleted file mode 100644
index 93fbfcd..0000000
--- a/images/horizon/patches/horizon/0000-fix-ignore-errors-when-flavors-are-deleted.patch
+++ /dev/null
@@ -1,108 +0,0 @@
-From aa21f4baa38fc70549b1c7341361519de6362d9b Mon Sep 17 00:00:00 2001
-From: okozachenko <okozachenko1203@gmail.com>
-Date: Thu, 2 Nov 2023 01:27:20 +1100
-Subject: [PATCH] fix: ignore errors when flavors are deleted
-
-The code used to list flavors when in the admin
-or project side was not consistent and raised
-alerts if viewing in the admin side but not in the
-project side.
-
-This patch moves their behaviour to be consistent
-and refactors the code to use the same code-base.
-
-Closes-Bug: #2042362
-Change-Id: I37cc02102285b1e83ec1343b710a57fb5ac4ba15
----
- .../dashboards/admin/instances/tests.py         |  4 ----
- .../dashboards/admin/instances/views.py         | 17 +++++------------
- .../dashboards/project/instances/tests.py       |  1 +
- .../dashboards/project/instances/views.py       | 11 +++--------
- 4 files changed, 9 insertions(+), 24 deletions(-)
-
-diff --git a/openstack_dashboard/dashboards/admin/instances/tests.py b/openstack_dashboard/dashboards/admin/instances/tests.py
-index 3630cb79a..c6cf65e5d 100644
---- a/openstack_dashboard/dashboards/admin/instances/tests.py
-+++ b/openstack_dashboard/dashboards/admin/instances/tests.py
-@@ -133,10 +133,6 @@ class InstanceViewTest(test.BaseAdminViewTests):
-         res = self.client.get(INDEX_URL)
-         instances = res.context['table'].data
-         self.assertTemplateUsed(res, INDEX_TEMPLATE)
--        # Since error messages produced for each instance are identical,
--        # there will be only one error message for all instances
--        # (messages de-duplication).
--        self.assertMessageCount(res, error=1)
-         self.assertCountEqual(instances, servers)
- 
-         self.assertEqual(self.mock_image_list_detailed.call_count, 4)
-diff --git a/openstack_dashboard/dashboards/admin/instances/views.py b/openstack_dashboard/dashboards/admin/instances/views.py
-index c35527fe4..efa28dd76 100644
---- a/openstack_dashboard/dashboards/admin/instances/views.py
-+++ b/openstack_dashboard/dashboards/admin/instances/views.py
-@@ -33,6 +33,8 @@ from openstack_dashboard.dashboards.admin.instances \
- from openstack_dashboard.dashboards.admin.instances \
-     import tables as project_tables
- from openstack_dashboard.dashboards.admin.instances import tabs
-+from openstack_dashboard.dashboards.project.instances \
-+    import utils as instance_utils
- from openstack_dashboard.dashboards.project.instances import views
- from openstack_dashboard.dashboards.project.instances.workflows \
-     import update_instance
-@@ -215,18 +217,9 @@ class AdminIndexView(tables.PagedTableMixin, tables.DataTableView):
-                 else:
-                     inst.image['name'] = _("-")
- 
--            flavor_id = inst.flavor["id"]
--            try:
--                if flavor_id in flavor_dict:
--                    inst.full_flavor = flavor_dict[flavor_id]
--                else:
--                    # If the flavor_id is not in flavor_dict list,
--                    # gets it via nova api.
--                    inst.full_flavor = api.nova.flavor_get(
--                        self.request, flavor_id)
--            except Exception:
--                msg = _('Unable to retrieve instance size information.')
--                exceptions.handle(self.request, msg)
-+            inst.full_flavor = instance_utils.resolve_flavor(self.request,
-+                                                             inst, flavor_dict)
-+
-             tenant = tenant_dict.get(inst.tenant_id, None)
-             inst.tenant_name = getattr(tenant, "name", None)
-         return instances
-diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
-index 5ab1b4a48..fe2f58c46 100644
---- a/openstack_dashboard/dashboards/project/instances/tests.py
-+++ b/openstack_dashboard/dashboards/project/instances/tests.py
-@@ -316,6 +316,7 @@ class InstanceTableTests(InstanceTestBase, InstanceTableTestMixin):
-         self.mock_is_feature_available.return_value = True
-         self.mock_server_list_paged.return_value = [servers, False, False]
-         self.mock_servers_update_addresses.return_value = None
-+        self.mock_flavor_get.side_effect = self.exceptions.nova
-         self.mock_flavor_list.side_effect = self.exceptions.nova
-         self.mock_image_list_detailed.return_value = (self.images.list(),
-                                                       False, False)
-diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py
-index badf540b8..b848f6fff 100644
---- a/openstack_dashboard/dashboards/project/instances/views.py
-+++ b/openstack_dashboard/dashboards/project/instances/views.py
-@@ -171,14 +171,9 @@ class IndexView(tables.PagedTableMixin, tables.DataTableView):
-         for instance in instances:
-             self._populate_image_info(instance, image_dict, volume_dict)
- 
--            flavor_id = instance.flavor["id"]
--            if flavor_id in flavor_dict:
--                instance.full_flavor = flavor_dict[flavor_id]
--            else:
--                # If the flavor_id is not in flavor_dict,
--                # put info in the log file.
--                LOG.info('Unable to retrieve flavor "%s" for instance "%s".',
--                         flavor_id, instance.id)
-+            instance.full_flavor = instance_utils.resolve_flavor(self.request,
-+                                                                 instance,
-+                                                                 flavor_dict)
- 
-         return instances
- 
--- 
-2.34.1
diff --git a/images/horizon/patches/horizon/0001-Fixing-Incorrect-URL-when-browsing-Swift-containers.patch b/images/horizon/patches/horizon/0001-Fixing-Incorrect-URL-when-browsing-Swift-containers.patch
deleted file mode 100644
index 58eafd8..0000000
--- a/images/horizon/patches/horizon/0001-Fixing-Incorrect-URL-when-browsing-Swift-containers.patch
+++ /dev/null
@@ -1,39 +0,0 @@
-From 4aa347fe196b7b18ff0bf5f4d4f076a6c14cf12e Mon Sep 17 00:00:00 2001
-From: jeremy-boyle <jeremyboylet@gmail.com>
-Date: Sat, 24 Jun 2023 16:59:11 +0000
-Subject: [PATCH] Fixing Incorrect URL when browsing Swift containers
-
-This patch fixes a bug identified in the code that generates the URL for
-the Swift container object. The bug caused the forward slashes (/) in the
-folder parameter to be encoded as %2F instead of being included as '/' in the
-resulting URL.
-
-To resolve this issue, the code has been updated by adding a replace() method
-to replace the %2F sequences with forward slashes. The updated code ensures
-that the URL generated for the folder parameter contains the correct forward
-slash (/) representation.
-
-Closes-Bug: #2009724
-Signed-off-by: jeremy-boyle <jeremyboylet@gmail.com>
-
-Change-Id: I5837e74ddcc71cda6b4686e586dbb8b1386a9cd3
----
- .../static/dashboard/project/containers/objects.controller.js  | 3 ++-
- 1 file changed, 2 insertions(+), 1 deletion(-)
-
-diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/objects.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/objects.controller.js
-index 55262a1fa..c14128cbf 100644
---- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/objects.controller.js
-+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/objects.controller.js
-@@ -60,7 +60,8 @@
-     ctrl.containerURL = containerRoute + encodeURIComponent($routeParams.container) +
-       ctrl.model.DELIMETER;
-     if (angular.isDefined($routeParams.folder)) {
--      ctrl.currentURL = ctrl.containerURL + encodeURIComponent($routeParams.folder) +
-+      ctrl.currentURL = ctrl.containerURL +
-+        encodeURIComponent($routeParams.folder).replace(/%2F/g, '/') +
-         ctrl.model.DELIMETER;
-     } else {
-       ctrl.currentURL = ctrl.containerURL;
--- 
-2.34.1
diff --git a/images/ironic/Dockerfile b/images/ironic/Dockerfile
new file mode 100644
index 0000000..8361034
--- /dev/null
+++ b/images/ironic/Dockerfile
@@ -0,0 +1,35 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG IRONIC_GIT_REF=22aa29b864eecd00bfb7c67cc2075030da1eb1d0
+ADD --keep-git-dir=true https://opendev.org/openstack/ironic.git#${IRONIC_GIT_REF} /src/ironic
+RUN git -C /src/ironic fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/ironic \
+        python-dracclient \
+        sushy
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    ethtool ipmitool iproute2 ipxe lshw qemu-utils tftpd-hpa
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/ironic/Earthfile b/images/ironic/Earthfile
deleted file mode 100644
index 0f775a4..0000000
--- a/images/ironic/Earthfile
+++ /dev/null
@@ -1,22 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=ironic
-ARG --global RELEASE=zed
-ARG --global PROJECT_REF=e38735cb95263b0c54f2fd719ff6b714efbddbb3
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES "python-dracclient sushy"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "ethtool ipmitool iproute2 ipxe lshw qemu-utils tftpd-hpa"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/keystone/Dockerfile b/images/keystone/Dockerfile
new file mode 100644
index 0000000..a589dca
--- /dev/null
+++ b/images/keystone/Dockerfile
@@ -0,0 +1,47 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG KEYSTONE_GIT_REF=8ca73f758bb613a57815fbe4ae78e3d2afa4af49
+ADD --keep-git-dir=true https://opendev.org/openstack/keystone.git#${KEYSTONE_GIT_REF} /src/keystone
+RUN git -C /src/keystone fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/keystone[ldap] \
+        keystone-keycloak-backend==0.1.8
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    apache2 libapache2-mod-wsgi-py3
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+ARG MOD_AUTH_OPENIDC_VERSION=2.4.12.1
+ARG TARGETARCH
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    curl
+curl -LO https://github.com/OpenIDC/mod_auth_openidc/releases/download/v${MOD_AUTH_OPENIDC_VERSION}/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb
+apt-get install -y --no-install-recommends ./libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb
+a2enmod auth_openidc
+apt-get purge -y --auto-remove curl
+apt-get clean
+rm -rfv /var/lib/apt/lists/* libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/keystone/Earthfile b/images/keystone/Earthfile
deleted file mode 100644
index 014401a..0000000
--- a/images/keystone/Earthfile
+++ /dev/null
@@ -1,34 +0,0 @@
-VERSION 0.7
-
-ARG --global RELEASE=2023.2
-ARG --global PROJECT=keystone
-ARG --global PROJECT_REF=5a55e9de15c7f390e43addc5f3ff1a4809ec1a5b
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --EXTRAS "[ldap]" \
-    --PIP_PACKAGES "keystone-keycloak-backend==0.1.8"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "apache2 libapache2-mod-wsgi-py3"
-  ARG MOD_AUTH_OPENIDC_VERSION=2.4.12.1
-  ARG TARGETARCH
-  RUN \
-    apt-get update && \
-    apt-get install -y --no-install-recommends curl && \
-    curl -LO https://github.com/OpenIDC/mod_auth_openidc/releases/download/v${MOD_AUTH_OPENIDC_VERSION}/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb && \
-    apt-get install -y --no-install-recommends ./libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb && \
-    a2enmod auth_openidc && \
-    apt-get purge -y --auto-remove curl && \
-    apt-get clean && \
-    rm -rfv /var/lib/apt/lists/* libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/keystone/patches/keystone/0000-Ensure-application-credentials-take-account-of-impli.patch b/images/keystone/patches/keystone/0000-Ensure-application-credentials-take-account-of-impli.patch
deleted file mode 100644
index 2d0841e..0000000
--- a/images/keystone/patches/keystone/0000-Ensure-application-credentials-take-account-of-impli.patch
+++ /dev/null
@@ -1,46 +0,0 @@
-From 6ee7ea0d63fed272beb3806d722c2dd3585e8212 Mon Sep 17 00:00:00 2001
-From: Andrew Bonney <andrew.bonney@bbc.co.uk>
-Date: Tue, 5 Sep 2023 14:56:51 +0100
-Subject: [PATCH] Ensure application credentials take account of implied roles
-
-Related-Bug: #2030061
-Change-Id: I2aea0b89987b24cf5ddaadeecbd06c32ad81a9bc
----
- keystone/models/token_model.py | 13 +++++++++++--
- 1 file changed, 11 insertions(+), 2 deletions(-)
-
-diff --git a/keystone/models/token_model.py b/keystone/models/token_model.py
-index 78146295d..b152d97c2 100644
---- a/keystone/models/token_model.py
-+++ b/keystone/models/token_model.py
-@@ -429,7 +429,13 @@ class TokenModel(object):
- 
-     def _get_application_credential_roles(self):
-         roles = []
-+        roles_added = list()
-         app_cred_roles = self.application_credential['roles']
-+        app_cred_roles = [{'role_id': r['id']} for r in app_cred_roles]
-+        effective_app_cred_roles = (
-+            PROVIDERS.assignment_api.add_implied_roles(app_cred_roles)
-+        )
-+
-         assignment_list = PROVIDERS.assignment_api.list_role_assignments(
-             user_id=self.user_id,
-             project_id=self.project_id,
-@@ -437,9 +443,12 @@ class TokenModel(object):
-             effective=True)
-         user_roles = list(set([x['role_id'] for x in assignment_list]))
- 
--        for role in app_cred_roles:
--            if role['id'] in user_roles:
-+        for role in effective_app_cred_roles:
-+            if role['role_id'] in user_roles and \
-+                    role['role_id'] not in roles_added:
-+                role = PROVIDERS.role_api.get_role(role['role_id'])
-                 roles.append({'id': role['id'], 'name': role['name']})
-+                roles_added.append(role['id'])
- 
-         return roles
- 
--- 
-2.34.1
diff --git a/images/kubernetes-entrypoint/Dockerfile b/images/kubernetes-entrypoint/Dockerfile
new file mode 100644
index 0000000..e7d0bc5
--- /dev/null
+++ b/images/kubernetes-entrypoint/Dockerfile
@@ -0,0 +1,24 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM golang:1.21 AS build
+ARG KUBERNETES_ENTRYPOINT_GIT_REF=4fbcf7ce324dc66e78480f73035e31434cfea1e8
+ADD https://opendev.org/airship/kubernetes-entrypoint.git#${KUBERNETES_ENTRYPOINT_GIT_REF} /src
+WORKDIR /src
+RUN CGO_ENABLED=0 GOOS=linux go build -o /main
+
+FROM scratch
+COPY --from=build /main /kubernetes-entrypoint
+USER 65534
+ENTRYPOINT ["/kubernetes-entrypoint"]
diff --git a/images/kubernetes-entrypoint/Earthfile b/images/kubernetes-entrypoint/Earthfile
deleted file mode 100644
index 03617f2..0000000
--- a/images/kubernetes-entrypoint/Earthfile
+++ /dev/null
@@ -1,35 +0,0 @@
-VERSION 0.7
-
-ARG --global COMMIT=e8c2b17e1261c6a1b0fed1fcd5e1c337fc014219
-
-build:
-  FROM golang:1.21.5-bookworm
-  DO ../+APT_INSTALL --PACKAGES "patch"
-  GIT CLONE --branch ${COMMIT} https://opendev.org/airship/kubernetes-entrypoint /src
-  WORKDIR /src
-  RUN \
-    curl https://review.opendev.org/changes/airship%2Fkubernetes-entrypoint~904537/revisions/1/patch?download | \
-    base64 --decode | \
-    patch -p1
-  ARG GOARCH
-  RUN \
-    --mount=type=cache,target=/root/.cache/go-build \
-    --mount=type=cache,target=/go/pkg/mod \
-    CGO_ENABLED=0 GOOS=linux go build -o /main
-  SAVE ARTIFACT /main
-
-platform-image:
-  FROM scratch
-  ARG TARGETARCH
-  COPY \
-    --platform=linux/amd64 \
-    (+build/main --GOARCH=$TARGETARCH) /kubernetes-entrypoint
-  USER 65534
-  ENTRYPOINT ["/kubernetes-entrypoint"]
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/kubernetes-entrypoint:${COMMIT} \
-    ${REGISTRY}/kubernetes-entrypoint:latest
-
-image:
-  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/kubernetes/Earthfile b/images/kubernetes/Earthfile
deleted file mode 100644
index d31c1e0..0000000
--- a/images/kubernetes/Earthfile
+++ /dev/null
@@ -1,9 +0,0 @@
-VERSION 0.7
-
-image:
-  FROM ../curl+image
-  ARG TARGETOS
-  ARG TARGETARCH
-  RUN curl -L "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/${TARGETOS}/${TARGETARCH}/kubectl" -o /tmp/kubectl
-  RUN chmod +x /tmp/kubectl && /tmp/kubectl version --client=true
-  SAVE ARTIFACT /tmp/kubectl kubectl
diff --git a/images/libvirtd/Dockerfile b/images/libvirtd/Dockerfile
new file mode 100644
index 0000000..5654edf
--- /dev/null
+++ b/images/libvirtd/Dockerfile
@@ -0,0 +1,26 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-runtime:main
+ADD --chmod=644 https://download.ceph.com/keys/release.gpg /etc/apt/trusted.gpg.d/ceph.gpg
+COPY <<EOF /etc/apt/sources.list.d/ceph.list
+deb http://download.ceph.com/debian-reef/ jammy main
+EOF
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    ceph-common cgroup-tools dmidecode ebtables iproute2 ipxe-qemu kmod libvirt-clients libvirt-daemon-system openssh-client openvswitch-switch ovmf pm-utils qemu-block-extra qemu-efi qemu-kvm seabios
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
diff --git a/images/libvirtd/Earthfile b/images/libvirtd/Earthfile
deleted file mode 100644
index 13e6aeb..0000000
--- a/images/libvirtd/Earthfile
+++ /dev/null
@@ -1,21 +0,0 @@
-VERSION 0.7
-
-platform-image:
-  ARG RELEASE=zed
-  FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
-  COPY keyrings/ceph.gpg /etc/apt/trusted.gpg.d/
-  IF [ "$(lsb_release -sc)" = "focal" ]
-    RUN echo "deb http://download.ceph.com/debian-quincy/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/ceph.list
-  ELSE IF [ "$(lsb_release -sc)" = "jammy" ]
-    RUN echo "deb http://download.ceph.com/debian-reef/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/ceph.list
-  ELSE
-    RUN echo "${RELEASE} is not supported on $(lsb_release -sc)"
-    RUN exit 1
-  END
-  DO ../+APT_INSTALL --PACKAGES="ceph-common cgroup-tools dmidecode ebtables iproute2 ipxe-qemu kmod libvirt-clients libvirt-daemon-system openssh-client openvswitch-switch ovmf pm-utils qemu-block-extra qemu-efi qemu-kvm seabios"
-  DO ../+CREATE_PROJECT_USER --PROJECT=nova
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push ${REGISTRY}/libvirtd:${RELEASE}
-
-image:
-  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/libvirtd/keyrings/ceph.gpg b/images/libvirtd/keyrings/ceph.gpg
deleted file mode 100644
index c5d8bd3..0000000
--- a/images/libvirtd/keyrings/ceph.gpg
+++ /dev/null
Binary files differ
diff --git a/images/magnum/Dockerfile b/images/magnum/Dockerfile
new file mode 100644
index 0000000..eca883d
--- /dev/null
+++ b/images/magnum/Dockerfile
@@ -0,0 +1,43 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/ubuntu:main AS helm
+ARG TARGETOS
+ARG TARGETARCH
+ARG HELM_VERSION=3.14.0
+ADD https://get.helm.sh/helm-v${HELM_VERSION}-${TARGETOS}-${TARGETARCH}.tar.gz /helm.tar.gz
+RUN tar -xzf /helm.tar.gz
+RUN mv /${TARGETOS}-${TARGETARCH}/helm /usr/bin/helm
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG MAGNUM_GIT_REF=c613ea4e419edc0086116da07e93cf19206746e1
+ADD --keep-git-dir=true https://opendev.org/openstack/magnum.git#${MAGNUM_GIT_REF} /src/magnum
+RUN git -C /src/magnum fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/magnum \
+        magnum-cluster-api==0.16.0
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    haproxy
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=helm --link /usr/bin/helm /usr/local/bin/helm
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/magnum/Earthfile b/images/magnum/Earthfile
deleted file mode 100644
index ba8ed33..0000000
--- a/images/magnum/Earthfile
+++ /dev/null
@@ -1,23 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=magnum
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=272fd686d8c8bf5954e9e7d3bc991ff27e46184d
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES="magnum-cluster-api==0.13.4"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "haproxy"
-  COPY ../helm+binary/helm /usr/local/bin/helm
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/magnum/patches/magnum/0000-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch b/images/magnum/patches/magnum/0000-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch
deleted file mode 100644
index 9bee808..0000000
--- a/images/magnum/patches/magnum/0000-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch
+++ /dev/null
@@ -1,28 +0,0 @@
-From 61592d46e7fc5644c4b5148c7ca6bf767131e504 Mon Sep 17 00:00:00 2001
-From: okozachenko1203 <okozachenko1203@gmail.com>
-Date: Fri, 31 Mar 2023 23:41:43 +1100
-Subject: [PATCH] Update chart.metadata.version to reflect breaking change in
- helm v3.5.2
-
-https: //github.com/helm/helm/issues/9342
-Change-Id: I1dbe7b0b85380e713ebb5dcdd7ecbfc6a438b852
-(cherry picked from commit ebee3263b6b3d3fa213ea8f837911b89785a4700)
----
- .../templates/kubernetes/fragments/install-helm-modules.sh    | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh b/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh
-index 475e8dbf6c..a0b3f4bc75 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh
-@@ -72,8 +72,8 @@ else
-         cat << EOF > Chart.yaml
- apiVersion: v1
- name: magnum
--version: metachart
--appVersion: metachart
-+version: 1.0.0
-+appVersion: v1.0.0
- description: Magnum Helm Charts
- EOF
-         sed -i '1i\dependencies:' requirements.yaml
diff --git a/images/manila/Dockerfile b/images/manila/Dockerfile
new file mode 100644
index 0000000..cbf63ec
--- /dev/null
+++ b/images/manila/Dockerfile
@@ -0,0 +1,33 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG MANILA_GIT_REF=d8987589ae88ae9b2769fbe6f26d5b6994098038
+ADD --keep-git-dir=true https://opendev.org/openstack/manila.git#${MANILA_GIT_REF} /src/manila
+RUN git -C /src/manila fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/manila
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    iproute2 openvswitch-switch
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/manila/Earthfile b/images/manila/Earthfile
deleted file mode 100644
index be2bccb..0000000
--- a/images/manila/Earthfile
+++ /dev/null
@@ -1,21 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=manila
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=a9cea65b45f0c0bf148b6ac9db0a8141c74fa2a6
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "iproute2 openvswitch-switch"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/manila/patches/manila/0000-fix-stop-using-batch_op-for-rename_table.patch b/images/manila/patches/manila/0000-fix-stop-using-batch_op-for-rename_table.patch
deleted file mode 100644
index c1f2306..0000000
--- a/images/manila/patches/manila/0000-fix-stop-using-batch_op-for-rename_table.patch
+++ /dev/null
@@ -1,30 +0,0 @@
-From eb7f03c667261557d7f809f7851bad6b3eea4646 Mon Sep 17 00:00:00 2001
-From: Mohammed Naser <mnaser@vexxhost.com>
-Date: Mon, 8 Jan 2024 14:00:37 -0500
-Subject: [PATCH] fix: Stop using batch_op for rename_table
-
-For migrations that rename tables, batch_op is not needed, which
-is also even causing issues with newer versions of Alembic.
-
-Change-Id: Ib43f5a24c497f7d97cb2d852b99489b0c3bd75fb
----
- .../alembic/versions/5077ffcc5f1c_add_share_instances.py      | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/manila/db/migrations/alembic/versions/5077ffcc5f1c_add_share_instances.py b/manila/db/migrations/alembic/versions/5077ffcc5f1c_add_share_instances.py
-index 42d26b75e..373e308d0 100644
---- a/manila/db/migrations/alembic/versions/5077ffcc5f1c_add_share_instances.py
-+++ b/manila/db/migrations/alembic/versions/5077ffcc5f1c_add_share_instances.py
-@@ -245,8 +245,8 @@ def upgrade_export_locations_table(connection):
-     with op.batch_alter_table("share_export_locations") as batch_op:
-         batch_op.drop_constraint('sel_id_fk', type_='foreignkey')
-         batch_op.drop_column('share_id')
--        batch_op.rename_table('share_export_locations',
--                              'share_instance_export_locations')
-+    op.rename_table('share_export_locations',
-+                            'share_instance_export_locations')
- 
- 
- def downgrade_export_locations_table(connection):
--- 
-2.34.1
diff --git a/images/netoffload/Dockerfile b/images/netoffload/Dockerfile
new file mode 100644
index 0000000..93df492
--- /dev/null
+++ b/images/netoffload/Dockerfile
@@ -0,0 +1,29 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM golang:1.20 AS build
+ARG NETOFFLOAD_GIT_REF=94b8c0fdb0b83bd1b7e14b9a58077a047c78a800
+ADD https://github.com/vexxhost/netoffload.git#${NETOFFLOAD_GIT_REF} /src
+WORKDIR /src
+RUN go build -v -o offloadctl ./cmd/offloadctl/main.go
+
+FROM registry.atmosphere.dev/library/ubuntu:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    jq mstflint
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /src/offloadctl /usr/local/bin/offloadctl
diff --git a/images/netoffload/Earthfile b/images/netoffload/Earthfile
deleted file mode 100644
index d3a04be..0000000
--- a/images/netoffload/Earthfile
+++ /dev/null
@@ -1,28 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=netoffload
-ARG --global RELEASE=main
-ARG --global PROJECT_REF=94b8c0fdb0b83bd1b7e14b9a58077a047c78a800
-
-build:
-  FROM golang:1.20
-  WORKDIR /src
-  GIT CLONE --branch ${PROJECT_REF} https://github.com/vexxhost/netoffload /src
-  RUN \
-    --mount=type=cache,target=/root/.cache/go-build \
-    --mount=type=cache,target=/go/pkg/mod \
-      go build -v -o offloadctl cmd/offloadctl/main.go
-  SAVE ARTIFACT offloadctl
-
-platform-image:
-  FROM ../base+image
-  DO ../+APT_INSTALL --PACKAGES="jq mstflint"
-  COPY +build/offloadctl /usr/local/bin/offloadctl
-  ENTRYPOINT ["/usr/local/bin/offloadctl"]
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
-
-image:
-  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/neutron/Dockerfile b/images/neutron/Dockerfile
new file mode 100644
index 0000000..716aa1d
--- /dev/null
+++ b/images/neutron/Dockerfile
@@ -0,0 +1,36 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG NEUTRON_GIT_REF=019294c71d94b788c14b23dc1da3c21f51bcdb0b
+ADD --keep-git-dir=true https://opendev.org/openstack/neutron.git#${NEUTRON_GIT_REF} /src/neutron
+RUN git -C /src/neutron fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/neutron-vpnaas.git#master /src/neutron-vpnaas
+RUN git -C /src/neutron-vpnaas fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/neutron \
+        /src/neutron-vpnaas
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    conntrack dnsmasq dnsmasq-utils ebtables ethtool haproxy iproute2 ipset iptables iputils-arping jq keepalived lshw openvswitch-switch strongswan uuid-runtime
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/neutron/Earthfile b/images/neutron/Earthfile
deleted file mode 100644
index bfff1e2..0000000
--- a/images/neutron/Earthfile
+++ /dev/null
@@ -1,25 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=neutron
-ARG --global RELEASE=zed
-ARG --global PROJECT_REF=b9e3818b6e8905f5717d9888014f7e42141aacf0
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES="git+https://github.com/openstack/neutron-vpnaas.git@256464aea691f8b4957ba668a117963353f34e4c"
-
-platform-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "conntrack dnsmasq dnsmasq-utils ebtables ethtool haproxy iproute2 ipset iptables iputils-arping jq keepalived lshw openvswitch-switch strongswan uuid-runtime"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
-
-image:
-  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/neutron/patches/neutron/0000-fix-netns-deletion-of-broken-namespaces.patch b/images/neutron/patches/neutron/0000-fix-netns-deletion-of-broken-namespaces.patch
deleted file mode 100644
index efa4798..0000000
--- a/images/neutron/patches/neutron/0000-fix-netns-deletion-of-broken-namespaces.patch
+++ /dev/null
@@ -1,144 +0,0 @@
-From 69c49c4ef24648f97d895bfaacd7336917634565 Mon Sep 17 00:00:00 2001
-From: Felix Huettner <felix.huettner@mail.schwarz>
-Date: Fri, 22 Sep 2023 16:25:10 +0200
-Subject: [PATCH] fix netns deletion of broken namespaces
-
-normal network namespaces are bind-mounted to files under
-/var/run/netns. If a process deleting a network namespace gets killed
-during that operation there is the chance that the bind mount to the
-netns has been removed, but the file under /var/run/netns still exists.
-
-When the neutron-ovn-metadata-agent tries to clean up such network
-namespaces it first tires to validate that the network namespace is
-empty. For the cases described above this fails, as this network
-namespace no longer really exists, but is just a stray file laying
-around.
-
-To fix this we treat network namespaces where we get an `OSError` with
-errno 22 (Invalid Argument) as empty. The calls to pyroute2 to delete
-the namespace will then clean up the file.
-
-Additionally we add a guard to teardown_datapath to continue even if
-this fails. failing to remove a datapath is not critical and leaves in
-the worst case a process and a network namespace running, however
-previously it would have also prevented the creation of new datapaths
-which is critical for VM startup.
-
-Closes-Bug: #2037102
-Change-Id: I7c43812fed5903f98a2e491076c24a8d926a59b4
-(cherry picked from commit 566fea3fed837b0130023303c770aade391d3d61)
----
- neutron/agent/linux/ip_lib.py                 | 17 ++++++++++++-
- neutron/agent/ovn/metadata/agent.py           |  5 +++-
- neutron/tests/unit/agent/linux/test_ip_lib.py | 15 +++++++++++
- .../unit/agent/ovn/metadata/test_agent.py     | 25 +++++++++++++++++++
- 4 files changed, 60 insertions(+), 2 deletions(-)
-
-diff --git a/neutron/agent/linux/ip_lib.py b/neutron/agent/linux/ip_lib.py
-index 10bd33d9e1..5d2593da47 100644
---- a/neutron/agent/linux/ip_lib.py
-+++ b/neutron/agent/linux/ip_lib.py
-@@ -259,7 +259,22 @@ class IPWrapper(SubProcessBase):
-         return ip
- 
-     def namespace_is_empty(self):
--        return not self.get_devices()
-+        try:
-+            return not self.get_devices()
-+        except OSError as e:
-+            # This can happen if we previously got terminated in the middle of
-+            # removing this namespace. In this case the bind mount of the
-+            # namespace under /var/run/netns will be removed, but the namespace
-+            # file is still there. As the bind mount is gone we can no longer
-+            # access the namespace to validate that it is empty. But since it
-+            # should have already been removed we are sure that the check has
-+            # passed the last time and since the namespace is unuseable that
-+            # can not have changed.
-+            # Future calls to pyroute2 to remove that namespace will clean up
-+            # the leftover file.
-+            if e.errno == errno.EINVAL:
-+                return True
-+            raise e
- 
-     def garbage_collect_namespace(self):
-         """Conditionally destroy the namespace if it is empty."""
-diff --git a/neutron/agent/ovn/metadata/agent.py b/neutron/agent/ovn/metadata/agent.py
-index 1745239701..861715d8e1 100644
---- a/neutron/agent/ovn/metadata/agent.py
-+++ b/neutron/agent/ovn/metadata/agent.py
-@@ -430,7 +430,10 @@ class MetadataAgent(object):
-                              ns.startswith(NS_PREFIX) and
-                              ns not in metadata_namespaces]
-         for ns in unused_namespaces:
--            self.teardown_datapath(self._get_datapath_name(ns))
-+            try:
-+                self.teardown_datapath(self._get_datapath_name(ns))
-+            except Exception:
-+                LOG.exception('Error unable to destroy namespace: %s', ns)
- 
-         # resync all network namespaces based on the associated datapaths,
-         # even those that are already running. This is to make sure
-diff --git a/neutron/tests/unit/agent/linux/test_ip_lib.py b/neutron/tests/unit/agent/linux/test_ip_lib.py
-index d1c74fb3f7..159cafdb8e 100644
---- a/neutron/tests/unit/agent/linux/test_ip_lib.py
-+++ b/neutron/tests/unit/agent/linux/test_ip_lib.py
-@@ -357,6 +357,21 @@ class TestIpWrapper(base.BaseTestCase):
-                 self.assertNotIn(mock.call().delete('ns'),
-                                  ip_ns_cmd_cls.mock_calls)
- 
-+    def test_garbage_collect_namespace_existing_broken(self):
-+        with mock.patch.object(ip_lib, 'IpNetnsCommand') as ip_ns_cmd_cls:
-+            ip_ns_cmd_cls.return_value.exists.return_value = True
-+
-+            ip = ip_lib.IPWrapper(namespace='ns')
-+
-+            with mock.patch.object(ip, 'get_devices',
-+                                   side_effect=OSError(errno.EINVAL, None)
-+                                   ) as mock_get_devices:
-+                self.assertTrue(ip.garbage_collect_namespace())
-+
-+                mock_get_devices.assert_called_once_with()
-+                expected = [mock.call().delete('ns')]
-+                ip_ns_cmd_cls.assert_has_calls(expected)
-+
-     @mock.patch.object(priv_lib, 'create_interface')
-     def test_add_vlan(self, create):
-         retval = ip_lib.IPWrapper().add_vlan('eth0.1', 'eth0', '1')
-diff --git a/neutron/tests/unit/agent/ovn/metadata/test_agent.py b/neutron/tests/unit/agent/ovn/metadata/test_agent.py
-index 6df7da702d..9bf9f0db52 100644
---- a/neutron/tests/unit/agent/ovn/metadata/test_agent.py
-+++ b/neutron/tests/unit/agent/ovn/metadata/test_agent.py
-@@ -134,6 +134,31 @@ class TestMetadataAgent(base.BaseTestCase):
-             lnn.assert_called_once_with()
-             tdp.assert_called_once_with('3')
- 
-+    def test_sync_teardown_namespace_does_not_crash_on_error(self):
-+        """Test that sync tears down unneeded metadata namespaces.
-+        Even if that fails it continues to provision other datapaths
-+        """
-+        with mock.patch.object(
-+                self.agent, 'provision_datapath') as pdp,\
-+                mock.patch.object(
-+                    ip_lib, 'list_network_namespaces',
-+                    return_value=['ovnmeta-1', 'ovnmeta-2', 'ovnmeta-3',
-+                                  'ns1', 'ns2']) as lnn,\
-+                mock.patch.object(
-+                    self.agent, 'teardown_datapath',
-+                    side_effect=Exception()) as tdp:
-+            self.agent.sync()
-+
-+            pdp.assert_has_calls(
-+                [
-+                    mock.call(p.datapath)
-+                    for p in self.ports
-+                ],
-+                any_order=True
-+            )
-+            lnn.assert_called_once_with()
-+            tdp.assert_called_once_with('3')
-+
-     def test_get_networks_datapaths(self):
-         """Test get_networks_datapaths returns only datapath objects for the
-         networks containing vif ports of type ''(blank) and 'external'.
--- 
-2.34.1
diff --git a/images/nova-ssh/Dockerfile b/images/nova-ssh/Dockerfile
new file mode 100644
index 0000000..dd59388
--- /dev/null
+++ b/images/nova-ssh/Dockerfile
@@ -0,0 +1,26 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    openssh-server \
+    openssh-client
+EOF
+RUN <<EOF bash -xe
+chown -R nova: /etc/ssh
+mkdir /var/run/sshd
+chmod 0755 /var/run/sshd
+EOF
diff --git a/images/nova-ssh/Earthfile b/images/nova-ssh/Earthfile
deleted file mode 100644
index 1b8df66..0000000
--- a/images/nova-ssh/Earthfile
+++ /dev/null
@@ -1,18 +0,0 @@
-VERSION 0.7
-
-platform-image:
-  FROM ../base+image
-  DO ../+CREATE_PROJECT_USER \
-    --PROJECT=nova \
-    --SHELL=/bin/bash
-  DO ../+APT_INSTALL \
-    --PACKAGES "openssh-server openssh-client"
-  RUN \
-    chown -R nova: /etc/ssh && \
-    mkdir /var/run/sshd && \
-    chmod 0755 /var/run/sshd
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push ${REGISTRY}/nova-ssh:latest
-
-image:
-  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/nova/Dockerfile b/images/nova/Dockerfile
new file mode 100644
index 0000000..c626858
--- /dev/null
+++ b/images/nova/Dockerfile
@@ -0,0 +1,34 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG NOVA_GIT_REF=c199becf52267ba37c5191f6f82e29bb5232b607
+ADD --keep-git-dir=true https://opendev.org/openstack/nova.git#${NOVA_GIT_REF} /src/nova
+RUN git -C /src/nova fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/nova
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+ADD https://github.com/novnc/noVNC.git#v1.4.0 /usr/share/novnc
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    ceph-common genisoimage iproute2 libosinfo-bin lsscsi ndctl nvme-cli openssh-client ovmf python3-libvirt python3-rados python3-rbd qemu-efi-aarch64 qemu-utils sysfsutils udev util-linux
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/nova/Earthfile b/images/nova/Earthfile
deleted file mode 100644
index 9e1cc3d..0000000
--- a/images/nova/Earthfile
+++ /dev/null
@@ -1,25 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=nova
-ARG --global RELEASE=zed
-ARG --global PROJECT_REF=226f3e95c1cdadd1845c7adee55f5c5f29f3a7a8
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-
-platform-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "ceph-common genisoimage iproute2 libosinfo-bin lsscsi ndctl nvme-cli openssh-client ovmf python3-libvirt python3-rados python3-rbd qemu-efi-aarch64 qemu-utils sysfsutils udev util-linux"
-  GIT CLONE --branch v1.4.0 https://github.com/novnc/noVNC.git /usr/share/novnc
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
-
-image:
-  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/nova/patches/nova/0000-libvirt-Stop-unconditionally-enabling-evmcs.patch b/images/nova/patches/nova/0000-libvirt-Stop-unconditionally-enabling-evmcs.patch
deleted file mode 100644
index 1191af1..0000000
--- a/images/nova/patches/nova/0000-libvirt-Stop-unconditionally-enabling-evmcs.patch
+++ /dev/null
@@ -1,66 +0,0 @@
-From 86a35e97d286cbb6e23f8cc7bec5a05f022da0cb Mon Sep 17 00:00:00 2001
-From: Artom Lifshitz <alifshit@redhat.com>
-Date: Tue, 31 Oct 2023 22:52:50 -0400
-Subject: [PATCH] libvirt: Stop unconditionally enabling evmcs
-
-In I008841988547573878c4e06e82f0fa55084e51b5 we started enabling a
-bunch of libvirt enlightenments for Windows unconditionally. Turns
-out, the `evmcs` enlightenment only works on Intel hosts, and we broke
-the ability to run Windows guests on AMD machines. Until we become
-smarter about conditionally enabling evmcs (with something like traits
-for host CPU features), just stop enabling it at all.
-
-Change-Id: I2ff4fdecd9dc69de283f0e52e07df1aeaf0a9048
-Closes-bug: 2009280
----
- nova/tests/unit/virt/libvirt/test_driver.py               | 5 ++++-
- nova/virt/libvirt/driver.py                               | 1 -
- ...p-unconditionally-enabling-evmcs-993a825641c4b9f3.yaml | 8 ++++++++
- 3 files changed, 12 insertions(+), 2 deletions(-)
- create mode 100644 releasenotes/notes/libvirt-enlightenments-stop-unconditionally-enabling-evmcs-993a825641c4b9f3.yaml
-
-diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py
-index d01b9c2677..ebba604ffa 100644
---- a/nova/tests/unit/virt/libvirt/test_driver.py
-+++ b/nova/tests/unit/virt/libvirt/test_driver.py
-@@ -27972,7 +27972,10 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
-         self.assertTrue(hv.reenlightenment)
-         self.assertTrue(hv.tlbflush)
-         self.assertTrue(hv.ipi)
--        self.assertTrue(hv.evmcs)
-+        # NOTE(artom) evmcs only works on Intel hosts, so we can't enable it
-+        # unconditionally. Until we become smarter about it, just don't enable
-+        # it at all. See bug 2009280.
-+        self.assertFalse(hv.evmcs)
- 
- 
- class LibvirtVolumeUsageTestCase(test.NoDBTestCase):
-diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py
-index d03dc5fd67..1b28e50355 100644
---- a/nova/virt/libvirt/driver.py
-+++ b/nova/virt/libvirt/driver.py
-@@ -6234,7 +6234,6 @@ class LibvirtDriver(driver.ComputeDriver):
-             hv.reenlightenment = True
-             hv.tlbflush = True
-             hv.ipi = True
--            hv.evmcs = True
- 
-             # NOTE(kosamara): Spoofing the vendor_id aims to allow the nvidia
-             # driver to work on windows VMs. At the moment, the nvidia driver
-diff --git a/releasenotes/notes/libvirt-enlightenments-stop-unconditionally-enabling-evmcs-993a825641c4b9f3.yaml b/releasenotes/notes/libvirt-enlightenments-stop-unconditionally-enabling-evmcs-993a825641c4b9f3.yaml
-new file mode 100644
-index 0000000000..31609f2a2d
---- /dev/null
-+++ b/releasenotes/notes/libvirt-enlightenments-stop-unconditionally-enabling-evmcs-993a825641c4b9f3.yaml
-@@ -0,0 +1,8 @@
-+---
-+fixes:
-+  - |
-+    Bug 2009280 has been fixed by no longer enabling the evmcs enlightenment in
-+    the libvirt driver. evmcs only works on Intel CPUs, and domains with that
-+    enlightenment cannot be started on AMD hosts. There is a possible future
-+    feature to enable support for generating this enlightenment only when
-+    running on Intel hosts.
--- 
-2.34.1
-
diff --git a/images/nova/patches/nova/0001-libvirt-stop-enabling-hyperv-feature-reenlightenment.patch b/images/nova/patches/nova/0001-libvirt-stop-enabling-hyperv-feature-reenlightenment.patch
deleted file mode 100644
index 88ea631..0000000
--- a/images/nova/patches/nova/0001-libvirt-stop-enabling-hyperv-feature-reenlightenment.patch
+++ /dev/null
@@ -1,52 +0,0 @@
-From e618e78edc6293d248a5fa2eb63b3fa636250fca Mon Sep 17 00:00:00 2001
-From: songjie <songjie_yewu@cmss.chinamobile.com>
-Date: Mon, 25 Dec 2023 16:59:36 +0800
-Subject: [PATCH] libvirt: stop enabling hyperv feature reenlightenment
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-The 'reenlightenment' hyperv enlightenment will cause
-instances live-migration to fail (KVM currently doesn’t
-fully support reenlightenment notifications, see
-www.qemu.org/docs/master/system/i386/hyperv.html),
-so don't enable it now.
-
-Change-Id: I6821819450bc96e4304125ea3b76a0e462e6e33f
-Closes-Bug: #2046549
-Related-Bug: #2009280
----
- nova/tests/unit/virt/libvirt/test_driver.py | 4 +++-
- nova/virt/libvirt/driver.py                 | 1 -
- 2 files changed, 3 insertions(+), 2 deletions(-)
-
-diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py
-index 868e024370..2e1d089898 100644
---- a/nova/tests/unit/virt/libvirt/test_driver.py
-+++ b/nova/tests/unit/virt/libvirt/test_driver.py
-@@ -28048,7 +28048,9 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
-         self.assertTrue(hv.synic)
-         self.assertTrue(hv.reset)
-         self.assertTrue(hv.frequencies)
--        self.assertTrue(hv.reenlightenment)
-+        # NOTE(jie) reenlightenment will cause instances live-migration
-+        # failure, so don't enable it now. See bug 2046549.
-+        self.assertFalse(hv.reenlightenment)
-         self.assertTrue(hv.tlbflush)
-         self.assertTrue(hv.ipi)
-         # NOTE(artom) evmcs only works on Intel hosts, so we can't enable it
-diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py
-index 7f5f48c047..f8e3353110 100644
---- a/nova/virt/libvirt/driver.py
-+++ b/nova/virt/libvirt/driver.py
-@@ -6262,7 +6262,6 @@ class LibvirtDriver(driver.ComputeDriver):
-             hv.synic = True
-             hv.reset = True
-             hv.frequencies = True
--            hv.reenlightenment = True
-             hv.tlbflush = True
-             hv.ipi = True
-
--- 
-2.34.1
-
diff --git a/images/octavia/Dockerfile b/images/octavia/Dockerfile
new file mode 100644
index 0000000..d4dd8d3
--- /dev/null
+++ b/images/octavia/Dockerfile
@@ -0,0 +1,36 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG OCTAVIA_GIT_REF=824b51a1dad80292b7a8ad5d61bf3ce706b1fb29
+ADD --keep-git-dir=true https://opendev.org/openstack/octavia.git#${OCTAVIA_GIT_REF} /src/octavia
+RUN git -C /src/octavia fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/ovn-octavia-provider.git#master /src/ovn-octavia-provider
+RUN git -C /src/ovn-octavia-provider fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/octavia \
+        /src/ovn-octavia-provider
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    isc-dhcp-client openssh-client
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/octavia/Earthfile b/images/octavia/Earthfile
deleted file mode 100644
index 0fcb0d5..0000000
--- a/images/octavia/Earthfile
+++ /dev/null
@@ -1,22 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=octavia
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=3f31a50f264c0e58691b1be7e90d324c13588b63
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES="ovn-octavia-provider"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "isc-dhcp-client openssh-client"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/octavia/patches/octavia/0000-fix-specify-endpoint-info.-for-neutron-client.patch b/images/octavia/patches/octavia/0000-fix-specify-endpoint-info.-for-neutron-client.patch
deleted file mode 100644
index f113d64..0000000
--- a/images/octavia/patches/octavia/0000-fix-specify-endpoint-info.-for-neutron-client.patch
+++ /dev/null
@@ -1,37 +0,0 @@
-From efd289b950b32d3e6ad160b7c7f2901bca7c7e55 Mon Sep 17 00:00:00 2001
-From: Mohammed Naser <mnaser@vexxhost.com>
-Date: Tue, 16 Jan 2024 17:13:19 -0500
-Subject: [PATCH] fix: specify endpoint info. for neutron client
-
-Closes bug: #2049551
-
-Change-Id: I80a266e500958415a70d462ddfe57e9e03e6ef13
----
- octavia/common/clients.py | 8 +++++++-
- 1 file changed, 7 insertions(+), 1 deletion(-)
-
-diff --git a/octavia/common/clients.py b/octavia/common/clients.py
-index b13642bb..2abcc67b 100644
---- a/octavia/common/clients.py
-+++ b/octavia/common/clients.py
-@@ -80,10 +80,16 @@ class NeutronAuth(object):
-         if not cls.neutron_client:
-             sess = ksession.get_session()
- 
--            kwargs = {}
-+            kwargs = {
-+                'region_name': CONF.neutron.region_name,
-+                'interface': CONF.neutron.valid_interfaces
-+            }
-             if CONF.neutron.endpoint_override:
-                 kwargs['network_endpoint_override'] = (
-                     CONF.neutron.endpoint_override)
-+                if CONF.neutron.endpoint_override.startswith("https"):
-+                    kwargs['insecure'] = CONF.neutron.insecure
-+                    kwargs['cacert'] = CONF.neutron.cafile
- 
-             conn = openstack.connection.Connection(
-                 session=sess, **kwargs)
--- 
-2.34.1
-
diff --git a/images/openstack-runtime/Dockerfile b/images/openstack-runtime/Dockerfile
new file mode 100644
index 0000000..b5bbf82
--- /dev/null
+++ b/images/openstack-runtime/Dockerfile
@@ -0,0 +1,23 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+ARG FROM=registry.atmosphere.dev/library/ubuntu-cloud-archive:main
+FROM ${FROM}
+ONBUILD ARG PROJECT
+ONBUILD ARG SHELL=/usr/sbin/nologin
+ONBUILD RUN \
+    groupadd -g 42424 ${PROJECT} && \
+    useradd -u 42424 -g 42424 -M -d /var/lib/${PROJECT} -s ${SHELL} -c "${PROJECT} User" ${PROJECT} && \
+    mkdir -p /etc/${PROJECT} /var/log/${PROJECT} /var/lib/${PROJECT} /var/cache/${PROJECT} && \
+    chown -Rv ${PROJECT}:${PROJECT} /etc/${PROJECT} /var/log/${PROJECT} /var/lib/${PROJECT} /var/cache/${PROJECT}
diff --git a/images/openstack-service/Earthfile b/images/openstack-service/Earthfile
deleted file mode 100644
index ae4ab49..0000000
--- a/images/openstack-service/Earthfile
+++ /dev/null
@@ -1,99 +0,0 @@
-VERSION 0.8
-
-PIP_INSTALL:
-  FUNCTION
-  ARG PACKAGES
-  RUN --mount=type=cache,target=/root/.cache \
-    /var/lib/openstack/bin/pip3 install \
-      --constraint /upper-constraints.txt \
-      ${PACKAGES}
-
-GIT_CHECKOUT:
-  FUNCTION
-  ARG PROJECT
-  ARG PROJECT_REPO=https://github.com/openstack/${PROJECT}
-  ARG PROJECT_REF
-  GIT CLONE --branch ${PROJECT_REF} ${PROJECT_REPO} /src
-  WORKDIR /src
-  RUN \
-    git remote set-url origin ${PROJECT_REPO} && \
-    git fetch --unshallow
-  COPY --if-exists patches/${PROJECT} /patches
-  IF [ -d /patches ]
-    RUN git apply --verbose /patches/*.patch
-  END
-
-BUILD_VENV:
-  FUNCTION
-  ARG PROJECT
-  ARG PROJECT_REPO=https://github.com/openstack/${PROJECT}
-  ARG PROJECT_REF
-  DO +GIT_CHECKOUT \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REPO=${PROJECT_REPO} \
-    --PROJECT_REF=${PROJECT_REF}
-  ARG EXTRAS=""
-  ARG PIP_PACKAGES=""
-  DO +PIP_INSTALL --PACKAGES "/src${EXTRAS} ${PIP_PACKAGES}"
-  SAVE ARTIFACT /var/lib/openstack venv
-
-requirements:
-  FROM ../base+image
-  ARG RELEASE
-  IF [ "${RELEASE}" = "master" ]
-    ARG BRANCH=master
-  ELSE
-    ARG BRANCH=stable/${RELEASE}
-  END
-  GIT CLONE --branch ${BRANCH} https://github.com/openstack/requirements /src
-  RUN \
-    sed -i 's/cryptography===36.0.2/cryptography===42.0.4/' /src/upper-constraints.txt && \
-    sed -i 's/cryptography===40.0.2/cryptography===42.0.4/' /src/upper-constraints.txt && \
-    sed -i 's/cryptography===41.0.7/cryptography===42.0.4/' /src/upper-constraints.txt && \
-    sed -i 's/Django===3.2.18/Django===3.2.24/' /src/upper-constraints.txt && \
-    sed -i 's/Flask===2.2.3/Flask===2.2.5/' /src/upper-constraints.txt && \
-    sed -i 's/Jinja2===3.1.2/Jinja2===3.1.3/' /src/upper-constraints.txt && \
-    sed -i 's/paramiko===2.11.0/paramiko===3.4.0/' /src/upper-constraints.txt && \
-    sed -i 's/paramiko===3.1.0/paramiko===3.4.0/' /src/upper-constraints.txt && \
-    sed -i 's/pyOpenSSL===22.0.0/pyOpenSSL===24.0.0/' /src/upper-constraints.txt && \
-    sed -i 's/pyOpenSSL===23.1.1/pyOpenSSL===24.0.0/' /src/upper-constraints.txt && \
-    sed -i 's/requests===2.28.1/requests===2.31.0/' /src/upper-constraints.txt && \
-    sed -i 's/requests===2.28.2/requests===2.31.0/' /src/upper-constraints.txt && \
-    sed -i 's/sqlparse===0.4.2/sqlparse===0.4.4/' /src/upper-constraints.txt && \
-    sed -i 's/urllib3===1.26.12/urllib3===1.26.18/' /src/upper-constraints.txt && \
-    sed -i 's/urllib3===1.26.15/urllib3===1.26.18/' /src/upper-constraints.txt && \
-    sed -i 's/Werkzeug===2.2.3/Werkzeug===2.3.8/' /src/upper-constraints.txt && \
-    sed -i '/glance-store/d' /src/upper-constraints.txt && \
-    sed -i '/horizon/d' /src/upper-constraints.txt
-  SAVE ARTIFACT /src/upper-constraints.txt
-
-builder:
-  ARG RELEASE
-  FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
-  DO ../+APT_INSTALL --PACKAGES "\
-    build-essential \
-    curl \
-    git \
-    libldap2-dev \
-    libpcre3-dev \
-    libsasl2-dev \
-    libssl-dev \
-    lsb-release \
-    openssh-client \
-    python3 \
-    python3-dev \
-    python3-pip \
-    python3-venv"
-  RUN --mount type=cache,target=/root/.cache \
-    python3 -m venv --upgrade-deps --system-site-packages /var/lib/openstack
-  COPY \
-    (+requirements/upper-constraints.txt --RELEASE=${RELEASE}) \
-    /upper-constraints.txt
-  DO +PIP_INSTALL --PACKAGES "cryptography pymysql python-binary-memcached python-memcached uwsgi"
-
-image:
-  ARG --required RELEASE
-  FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
-  ENV PATH=/var/lib/openstack/bin:$PATH
-  ARG --required PROJECT
-  DO ../+CREATE_PROJECT_USER --PROJECT=${PROJECT}
diff --git a/images/openstack-venv-builder/Dockerfile b/images/openstack-venv-builder/Dockerfile
new file mode 100644
index 0000000..057af96
--- /dev/null
+++ b/images/openstack-venv-builder/Dockerfile
@@ -0,0 +1,49 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/ubuntu-cloud-archive:main AS requirements
+ADD https://releases.openstack.org/constraints/upper/master /upper-constraints.txt
+RUN <<EOF sh -xe
+sed -i '/glance-store/d' /upper-constraints.txt
+sed -i '/horizon/d' /upper-constraints.txt
+EOF
+
+FROM registry.atmosphere.dev/library/python-base:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    build-essential \
+    git \
+    libldap2-dev \
+    libpcre3-dev \
+    libsasl2-dev \
+    libssl-dev \
+    lsb-release \
+    openssh-client \
+    python3 \
+    python3-dev \
+    python3-pip \
+    python3-venv
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+RUN python3 -m venv --upgrade-deps --system-site-packages /var/lib/openstack
+COPY --from=requirements --link /upper-constraints.txt /upper-constraints.txt
+RUN pip3 install \
+    --constraint /upper-constraints.txt \
+        cryptography \
+        pymysql \
+        python-binary-memcached \
+        python-memcached \
+        uwsgi
diff --git a/images/openvswitch/Dockerfile b/images/openvswitch/Dockerfile
new file mode 100644
index 0000000..2b360ea
--- /dev/null
+++ b/images/openvswitch/Dockerfile
@@ -0,0 +1,25 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM quay.io/centos/centos:stream9
+ARG OVS_SERIES=3.1
+ARG OVS_VERSION=${OVS_SERIES}.0-65
+RUN <<EOF sh -xe
+dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \
+    centos-release-nfv-openvswitch.noarch
+dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \
+    openvswitch${OVS_SERIES}-${OVS_VERSION}.el9s iptables
+dnf -y clean all
+rm -rf /var/cache/dnf
+EOF
diff --git a/images/openvswitch/Earthfile b/images/openvswitch/Earthfile
deleted file mode 100644
index 35c3b2e..0000000
--- a/images/openvswitch/Earthfile
+++ /dev/null
@@ -1,17 +0,0 @@
-VERSION 0.7
-
-ARG --global SERIES=3.1
-ARG --global VERSION=3.1.0-65
-
-platform-image:
-  FROM quay.io/centos/centos:stream9
-  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
-  DO ../+DNF_INSTALL --PACKAGES "centos-release-nfv-openvswitch.noarch"
-  DO ../+DNF_INSTALL --PACKAGES "openvswitch${SERIES}-${VERSION}.el9s iptables"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/openvswitch:${SERIES} \
-    ${REGISTRY}/openvswitch:${VERSION}
-
-image:
-  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/ovn/Dockerfile b/images/ovn/Dockerfile
new file mode 100644
index 0000000..3f99847
--- /dev/null
+++ b/images/ovn/Dockerfile
@@ -0,0 +1,44 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM golang:1.20 AS ovn-kubernetes
+ARG OVN_KUBERNETES_REF=cbff639b83af00e4887b540fc06b880108662780
+ADD https://github.com/ovn-org/ovn-kubernetes.git#${OVN_KUBERNETES_REF} /src
+COPY patches/ovn-kubernetes /patches/ovn-kubernetes
+RUN git -C /src apply --verbose /patches/ovn-kubernetes/*
+RUN <<EOF bash -xe
+cd /src/go-controller
+go build -o /usr/bin/ovn-kube-util ./cmd/ovn-kube-util
+EOF
+
+FROM registry.atmosphere.dev/library/openvswitch:main
+ADD --chmod=755 https://dl.k8s.io/release/v1.29.3/bin/linux/amd64/kubectl /usr/local/bin/kubectl
+ARG OVN_SERIES=23.03
+ARG OVN_VERSION=${OVN_SERIES}.0-69
+RUN <<EOF sh -xe
+dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \
+    firewalld-filesystem hostname ovn${OVN_SERIES}-${OVN_VERSION}.el9s procps-ng
+dnf -y clean all
+rm -rf /var/cache/dnf
+EOF
+ARG OVN_COMPONENT
+RUN <<EOF sh -xe
+dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \
+    ovn${OVN_SERIES}-${OVN_COMPONENT}-${OVN_VERSION}.el9s
+dnf -y clean all
+rm -rf /var/cache/dnf
+EOF
+COPY --from=ovn-kubernetes --link /src/dist/images/ovndb-raft-functions.sh /root/ovndb-raft-functions.sh
+COPY --from=ovn-kubernetes --link /src/dist/images/ovnkube.sh /root/ovnkube.sh
+COPY --from=ovn-kubernetes --link /usr/bin/ovn-kube-util /usr/bin/ovn-kube-util
diff --git a/images/ovn/Earthfile b/images/ovn/Earthfile
deleted file mode 100644
index 327d73a..0000000
--- a/images/ovn/Earthfile
+++ /dev/null
@@ -1,47 +0,0 @@
-VERSION 0.7
-
-ARG --global SERIES=23.03
-ARG --global VERSION=23.03.0-69
-ARG --global PROJECT_REF=cbff639b83af00e4887b540fc06b880108662780
-
-ovn-kubernetes:
-  FROM --platform=linux/amd64 golang:1.20
-  GIT CLONE --branch ${PROJECT_REF} https://github.com/ovn-org/ovn-kubernetes /src
-  WORKDIR /src
-  COPY patches/ovn-kubernetes /patches
-  DO ../+APT_INSTALL --PACKAGES "git"
-  RUN git apply --verbose /patches/*.patch
-  SAVE ARTIFACT /src/dist/images/ovndb-raft-functions.sh
-  SAVE ARTIFACT /src/dist/images/ovnkube.sh
-  ARG GOOS=linux
-  ARG GOARCH=amd64
-  RUN \
-    cd /src/go-controller && \
-    go build -o /build/ovn-kube-util ./cmd/ovn-kube-util
-  SAVE ARTIFACT /build/ovn-kube-util
-
-component-image:
-  FROM ../openvswitch+platform-image
-  DO ../+DNF_INSTALL --PACKAGES "firewalld-filesystem hostname ovn${SERIES}-${VERSION}.el9s procps-ng"
-  ARG --required NAME
-  DO ../+DNF_INSTALL --PACKAGES "ovn${SERIES}-${NAME}-${VERSION}.el9s"
-  COPY ../kubernetes+image/kubectl /usr/local/bin/kubectl
-  COPY +ovn-kubernetes/ovndb-raft-functions.sh /root
-  COPY +ovn-kubernetes/ovnkube.sh /root
-  ARG TARGETARCH
-  ARG TARGETVARIANT
-  COPY (+ovn-kubernetes/ovn-kube-util --GOARCH=$TARGETARCH --VARIANT=$TARGETVARIANT) /usr/bin
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/ovn-${NAME}:${SERIES} \
-    ${REGISTRY}/ovn-${NAME}:${VERSION}
-
-central:
-  BUILD +component-image --NAME central
-
-host:
-  BUILD +component-image --NAME host
-
-images:
-  BUILD +central
-  BUILD --platform linux/amd64 --platform linux/arm64 +host
diff --git a/images/placement/Dockerfile b/images/placement/Dockerfile
new file mode 100644
index 0000000..d729d15
--- /dev/null
+++ b/images/placement/Dockerfile
@@ -0,0 +1,26 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG PLACEMENT_GIT_REF=96a9aeb3b4a6ffff5bbf247b213409395239fc7a
+ADD --keep-git-dir=true https://opendev.org/openstack/placement.git#${PLACEMENT_GIT_REF} /src/placement
+RUN git -C /src/placement fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/placement
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/placement/Earthfile b/images/placement/Earthfile
deleted file mode 100644
index 5719453..0000000
--- a/images/placement/Earthfile
+++ /dev/null
@@ -1,19 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=placement
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=a361622d749d3b24aad638ec1b03a7d7124a87b3
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/python-base/Dockerfile b/images/python-base/Dockerfile
new file mode 100644
index 0000000..c098401
--- /dev/null
+++ b/images/python-base/Dockerfile
@@ -0,0 +1,26 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/ubuntu-cloud-archive:main
+ENV PATH=/var/lib/openstack/bin:$PATH
+RUN \
+    apt-get update -qq && \
+    apt-get install -qq -y --no-install-recommends \
+        ca-certificates \
+        libpython3.10 \
+        lsb-release \
+        python3-distutils \
+        sudo && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*
diff --git a/images/senlin/Dockerfile b/images/senlin/Dockerfile
new file mode 100644
index 0000000..3f5490b
--- /dev/null
+++ b/images/senlin/Dockerfile
@@ -0,0 +1,26 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG SENLIN_GIT_REF=ec5fae997686c64c3c1192b231b2434e6a6aeb1c
+ADD --keep-git-dir=true https://opendev.org/openstack/senlin.git#${SENLIN_GIT_REF} /src/senlin
+RUN git -C /src/senlin fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/senlin
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/senlin/Earthfile b/images/senlin/Earthfile
deleted file mode 100644
index 6d0e3de..0000000
--- a/images/senlin/Earthfile
+++ /dev/null
@@ -1,19 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=senlin
-ARG --global RELEASE=2023.2
-ARG --global PROJECT_REF=5382259276d6be6807634c58c7b69b03b57ad6f5
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/staffeln/Dockerfile b/images/staffeln/Dockerfile
new file mode 100644
index 0000000..167c978
--- /dev/null
+++ b/images/staffeln/Dockerfile
@@ -0,0 +1,26 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG STAFFELN_GIT_REF=v2.2.3
+ADD --keep-git-dir=true https://github.com/vexxhost/staffeln.git#${STAFFELN_GIT_REF} /src/staffeln
+RUN git -C /src/staffeln fetch --unshallow
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/staffeln
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/staffeln/Earthfile b/images/staffeln/Earthfile
deleted file mode 100644
index 8cef127..0000000
--- a/images/staffeln/Earthfile
+++ /dev/null
@@ -1,20 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=staffeln
-ARG --global RELEASE=master
-ARG --global PROJECT_REF=v2.2.3
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REPO=https://github.com/vexxhost/${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF}
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +build/venv /var/lib/openstack
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/tempest/Dockerfile b/images/tempest/Dockerfile
new file mode 100644
index 0000000..5c40c75
--- /dev/null
+++ b/images/tempest/Dockerfile
@@ -0,0 +1,60 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM golang:1.18 AS octavia-test-server
+ADD --keep-git-dir=true https://opendev.org/openstack/octavia-tempest-plugin.git#master /src
+RUN GO111MODULE=off CGO_ENABLED=0 GOOS=linux go build \
+    -a -ldflags '-s -w -extldflags -static' \
+    -o /build/test_server.bin \
+    /src/octavia_tempest_plugin/contrib/test_server/test_server.go
+
+FROM registry.atmosphere.dev/library/openstack-venv-builder:main AS build
+ARG TEMPEST_GIT_REF=c0da6e843a74c2392c8e87e8ff36d2fea12949c4
+ADD --keep-git-dir=true https://opendev.org/openstack/tempest.git#${TEMPEST_GIT_REF} /src/tempest
+RUN git -C /src/tempest fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/barbican-tempest-plugin.git#master /src/barbican-tempest-plugin
+RUN git -C /src/barbican-tempest-plugin fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/cinder-tempest-plugin.git#master /src/cinder-tempest-plugin
+RUN git -C /src/cinder-tempest-plugin fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/heat-tempest-plugin.git#master /src/heat-tempest-plugin
+RUN git -C /src/heat-tempest-plugin fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/keystone-tempest-plugin.git#master /src/keystone-tempest-plugin
+RUN git -C /src/keystone-tempest-plugin fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/neutron-tempest-plugin.git#master /src/neutron-tempest-plugin
+RUN git -C /src/neutron-tempest-plugin fetch --unshallow
+ADD --keep-git-dir=true https://opendev.org/openstack/octavia-tempest-plugin.git#master /src/octavia-tempest-plugin
+RUN git -C /src/octavia-tempest-plugin fetch --unshallow
+ADD https://releases.openstack.org/constraints/upper/master /upper-constraints.txt
+RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
+pip3 install \
+    --constraint /upper-constraints.txt \
+        /src/tempest \
+        /src/barbican-tempest-plugin \
+        /src/cinder-tempest-plugin \
+        /src/heat-tempest-plugin \
+        /src/keystone-tempest-plugin \
+        /src/neutron-tempest-plugin \
+        /src/octavia-tempest-plugin
+EOF
+
+FROM registry.atmosphere.dev/library/openstack-python-runtime:main
+RUN <<EOF bash -xe
+apt-get update -qq
+apt-get install -qq -y --no-install-recommends \
+    iputils-ping openssh-client
+apt-get clean
+rm -rf /var/lib/apt/lists/*
+EOF
+COPY --from=octavia-test-server --link /build/test_server.bin /opt/octavia-tempest-plugin/test_server.bin
+COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/tempest/Earthfile b/images/tempest/Earthfile
deleted file mode 100644
index 02866ca..0000000
--- a/images/tempest/Earthfile
+++ /dev/null
@@ -1,49 +0,0 @@
-VERSION 0.7
-
-ARG --global PROJECT=tempest
-ARG --global RELEASE=master
-ARG --global PROJECT_REF=699749ec27897efe9bd7824664237c16c3339c03
-
-build.plugin:
-  ARG PLUGIN
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  DO ../openstack-service+GIT_CHECKOUT \
-    --PROJECT=${PLUGIN} \
-    --PROJECT_REF=${RELEASE}
-  SAVE ARTIFACT /src
-
-octavia-test-server:
-  FROM golang:1.18
-  COPY (+build.plugin/src --PLUGIN=octavia-tempest-plugin) /src
-  ENV GO111MODULE=off
-  ENV CGO_ENABLED=0
-  ENV GOOS=linux
-  RUN go build \
-    -a -ldflags '-s -w -extldflags -static' \
-    -o /build/test_server.bin \
-    /src/octavia_tempest_plugin/contrib/test_server/test_server.go
-  SAVE ARTIFACT /build/test_server.bin
-
-build:
-  FROM ../openstack-service+builder --RELEASE=${RELEASE}
-  COPY (+build.plugin/src --PLUGIN=barbican-tempest-plugin) /barbican-tempest-plugin
-  COPY (+build.plugin/src --PLUGIN=cinder-tempest-plugin) /cinder-tempest-plugin
-  COPY (+build.plugin/src --PLUGIN=heat-tempest-plugin) /heat-tempest-plugin
-  COPY (+build.plugin/src --PLUGIN=keystone-tempest-plugin) /keystone-tempest-plugin
-  COPY (+build.plugin/src --PLUGIN=neutron-tempest-plugin) /neutron-tempest-plugin
-  COPY (+build.plugin/src --PLUGIN=octavia-tempest-plugin) /octavia-tempest-plugin
-  DO ../openstack-service+BUILD_VENV \
-    --PROJECT=${PROJECT} \
-    --PROJECT_REF=${PROJECT_REF} \
-    --PIP_PACKAGES="/barbican-tempest-plugin /cinder-tempest-plugin /heat-tempest-plugin /keystone-tempest-plugin /neutron-tempest-plugin /octavia-tempest-plugin junitxml"
-
-image:
-  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
-  COPY +octavia-test-server/test_server.bin /opt/octavia-tempest-plugin/test_server.bin
-  COPY +build/venv /var/lib/openstack
-  DO ../+APT_INSTALL \
-    --PACKAGES "iputils-ping openssh-client"
-  ARG REGISTRY=ghcr.io/vexxhost/atmosphere
-  SAVE IMAGE --push \
-    ${REGISTRY}/${PROJECT}:${RELEASE} \
-    ${REGISTRY}/${PROJECT}:${PROJECT_REF}
diff --git a/images/trivy/.trivyignore b/images/trivy/.trivyignore
deleted file mode 100644
index 5057f16..0000000
--- a/images/trivy/.trivyignore
+++ /dev/null
@@ -1,17 +0,0 @@
-# NOTE(mnaser): OpenStack used to be versioned based on years and Trivy gets
-#               confused since something like 2014.1.3 > 23.0.1.dev6 therefore
-#               we ignore those old CVEs.
-CVE-2012-3542
-CVE-2012-4413
-CVE-2013-2256
-CVE-2013-4179
-CVE-2014-3517
-CVE-2014-3608
-CVE-2014-3641
-CVE-2014-3708
-CVE-2015-0259
-CVE-2015-3221
-CVE-2015-3280
-CVE-2015-5251
-CVE-2015-5286
-CVE-2015-7713
diff --git a/images/trivy/Earthfile b/images/trivy/Earthfile
deleted file mode 100644
index 8f68625..0000000
--- a/images/trivy/Earthfile
+++ /dev/null
@@ -1,8 +0,0 @@
-VERSION 0.7
-
-image:
-  FROM aquasec/trivy:0.48.3
-  COPY .trivyignore /.trivyignore
-  # TODO(mnaser): Add automatic updates
-  RUN trivy image --download-db-only
-  RUN trivy image --download-java-db-only
diff --git a/images/ubuntu-cloud-archive/Dockerfile b/images/ubuntu-cloud-archive/Dockerfile
new file mode 100644
index 0000000..ed2ec3a
--- /dev/null
+++ b/images/ubuntu-cloud-archive/Dockerfile
@@ -0,0 +1,19 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM registry.atmosphere.dev/library/ubuntu:main
+COPY trusted.gpg.d/ubuntu-cloud-keyring.gpg /etc/apt/trusted.gpg.d/ubuntu-cloud-keyring.gpg
+COPY <<EOF /etc/apt/sources.list.d/cloudarchive.list
+deb http://ubuntu-cloud.archive.canonical.com/ubuntu jammy-updates/caracal main
+EOF
diff --git a/images/ubuntu-cloud-archive/trusted.gpg.d/ubuntu-cloud-keyring.gpg b/images/ubuntu-cloud-archive/trusted.gpg.d/ubuntu-cloud-keyring.gpg
new file mode 100644
index 0000000..b59c283
--- /dev/null
+++ b/images/ubuntu-cloud-archive/trusted.gpg.d/ubuntu-cloud-keyring.gpg
Binary files differ
diff --git a/images/ubuntu/Dockerfile b/images/ubuntu/Dockerfile
new file mode 100644
index 0000000..1ad0fc5
--- /dev/null
+++ b/images/ubuntu/Dockerfile
@@ -0,0 +1,16 @@
+# Copyright (c) 2024 VEXXHOST, Inc.
+#
+# 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.
+
+FROM ubuntu:jammy-20240227
+LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere