feat: upgrade to bobcat (#887)

diff --git a/.github/renovate.json b/.github/renovate.json
index 8e4fae4..c9fc620 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -21,9 +21,9 @@
       "packageNameTemplate": "https://github.com/openstack/{{{depName}}}",
       "currentValueTemplate": "stable/{{{version}}}",
       "matchStrings": [
-        "ARG PROJECT=(?<depName>.*)",
-        "ARG RELEASE=(?<version>.*)",
-        "ARG REF=(?<currentDigest>.*)"
+        "ARG --global RELEASE=(?<version>.*)",
+        "ARG --global PROJECT=(?<depName>.*)",
+        "ARG --global PROJECT_REF=(?<currentDigest>.*)"
       ]
     },
     {
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bb25beb..17c6b27 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@
     hooks:
       - id: end-of-file-fixer
       - id: trailing-whitespace
-        exclude: ^images/.*/patches/.*\.patch$
+        exclude: ^images/.*/patches/.*/.*\.patch$
 
   - repo: https://github.com/compilerla/conventional-pre-commit
     rev: v2.0.0
diff --git a/Earthfile b/Earthfile
index 888110c..4ae563e 100644
--- a/Earthfile
+++ b/Earthfile
@@ -27,7 +27,6 @@
     --platform=linux/amd64 \
     (+libvirt-tls-sidecar.build/main --GOARCH=$TARGETARCH --VARIANT=$TARGETVARIANT) /usr/bin/libvirt-tls-sidecar
   ENTRYPOINT ["/usr/bin/libvirt-tls-sidecar"]
-  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
   SAVE IMAGE --push ghcr.io/vexxhost/atmosphere/libvirt-tls-sidecar:latest
 
 libvirt-tls-sidecar.image:
diff --git a/docs/developer/images.md b/docs/developer/images.md
new file mode 100644
index 0000000..4a7acfb
--- /dev/null
+++ b/docs/developer/images.md
@@ -0,0 +1,15 @@
+# Images
+
+The images are built using Earthly and the contents of these files are all
+located in the `images/` directory.
+
+## Adding Gerrit packages
+
+If you need to cherry-pick a specific Gerrit patch, you can use the following
+command to download and extract the patch:
+
+```bash
+earthly ./images+fetch-gerrit-patch \
+  --IMAGE keystone \
+  --CHANGE 893737
+```
diff --git a/docs/developer/repos.md b/docs/developer/repos.md
deleted file mode 100644
index 512b9db..0000000
--- a/docs/developer/repos.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# Repositories
-
-Atmosphere uses a few different Git repositories to host the code for the
-project.  This document explains how to work with the different repositories,
-their purpose, and how to maintain them.
-
-## Creating a new fork
-
-In order to create a new fork of a repository, we'll need to create a fork
-under the `vexxhost` organization.  In this example, we'll assume that you're
-creating a fork of the `openstack/horizon` project.
-
-In order to fork the project, you'll start with the following command which
-assumes that you have the `gh` command line tool installed:
-
-```bash
-./hack/repos/fork openstack/horizon
-```
-
-> **Note**
->
-> If this is an OpenStack project, once you're done, you'll also need to update
-> the `FORKED_PROJECTS` variable in the
-> `internal/pkg/image_repositories/build_workflow.go` file to include the newly
-> forked project.
-
-## Applying patches
-
-The only time that it is necessary to apply patches to the forked repositories
-is when there is a fix that has not yet been merged upstream.  In order to
-apply a patch, you can use the following command which includes the project
-name and either a Gerrit URL or a GitHub pull request URL:
-
-```bash
-./hack/repos/patch horizon https://review.opendev.org/c/openstack/horizon/+/874351/
-```
-
-This command will take care of automatically cloning the project, downloading
-the patch, and applying it to the repository.  Once the patch has been applied,
-it will push it in a new branch to the forked repository and create a pull
-request.
-
-> **Note**
->
-> If the process fails because of a merge conflict, you'll need to resolve the
-> conflict and then run the command again.
-
-## Cherry-picking patches
-
-If you need to cherry-pick a patch from one branch of a forked repository to
-another, you can use the following command:
-
-```bash
-./hack/repos/cherry-pick magnum fbfd3ce9a3 stable/zed
-```
-
-In the example above, this will cherry-pick the commit `fbfd3ce9a3` from the
-`master` branch of the `magnum` repository and create a pull request to apply
-it to the `stable/zed` branch.
-
-## OpenStack
-
-Atmosphere has a few forks of the OpenStack repositories.  These are used to
-apply patches to the upstream code that contain fixes which have not yet been
-merged upstream.  The list of forked repositories is as follows:
-
-* [openstack/horizon](https://github.com/vexxhost/horizon)
-* [openstack/keystone](https://github.com/vexxhost/keystone)
-* [openstack/magnum](https://github.com/vexxhost/magnum)
-* [openstack/magnum-ui](https://github.com/vexxhost/magnum-ui)
diff --git a/images/Earthfile b/images/Earthfile
index f5ff111..21cd0d8 100644
--- a/images/Earthfile
+++ b/images/Earthfile
@@ -29,17 +29,23 @@
     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}
 
-APPLY_PATCHES:
-  COMMAND
-  COPY --if-exists patches /patches
-  IF [ -d /patches ]
-    RUN \
-      apt-get update && \
-      apt-get install -y patch && \
-      for patch in /patches/*.patch; do \
-        patch -d /var/lib/openstack/lib/python3.10/site-packages/ -p1 < $patch; \
-      done && \
-      apt-get purge -y --auto-remove patch && \
-      apt-get clean && \
-      rm -rf /var/lib/apt/lists/*
-  END
+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/Earthfile b/images/barbican/Earthfile
index af81b11..0c843ff 100644
--- a/images/barbican/Earthfile
+++ b/images/barbican/Earthfile
@@ -1,15 +1,19 @@
 VERSION 0.7
 
+ARG --global PROJECT=barbican
+ARG --global RELEASE=2023.2
+ARG --global PROJECT_REF=a00fcade4138ffc52cd9c84b5999297966f019b5
+
+build:
+  FROM ../openstack-service+builder --RELEASE=${RELEASE}
+  DO ../openstack-service+BUILD_VENV \
+    --PROJECT=${PROJECT} \
+    --PROJECT_REF=${PROJECT_REF} \
+    --PIP_PACKAGES="pykmip"
+
 image:
-  ARG PROJECT=barbican
-  ARG RELEASE=zed
-  ARG REF=7d6749fcb1ad16a3350de82cd8e523d5b55306f8
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "pykmip"
-  DO ../+APPLY_PATCHES
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/base/Earthfile b/images/base/Earthfile
index c075abc..89449e0 100644
--- a/images/base/Earthfile
+++ b/images/base/Earthfile
@@ -2,3 +2,4 @@
 
 image:
   FROM ubuntu:jammy
+  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
diff --git a/images/cinder/Earthfile b/images/cinder/Earthfile
index c02de0c..402127f 100644
--- a/images/cinder/Earthfile
+++ b/images/cinder/Earthfile
@@ -1,19 +1,22 @@
 VERSION 0.7
 
+ARG --global PROJECT=cinder
+ARG --global RELEASE=2023.2
+ARG --global PROJECT_REF=7d158234b72f04e780e307b0375cedf30dd8fb90
+
+build:
+  FROM ../openstack-service+builder --RELEASE=${RELEASE}
+  DO ../openstack-service+BUILD_VENV \
+    --PROJECT=${PROJECT} \
+    --PROJECT_REF=${PROJECT_REF} \
+    --PIP_PACKAGES="purestorage"
+
 image:
-  ARG PROJECT=cinder
-  ARG RELEASE=zed
-  ARG REF=011e6549259c0433ceb594d8083f6c838d964311
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "purestorage"
-  DO \
-    ../+APT_INSTALL \
-    --PACKAGES "ceph-common lsscsi nvme-cli python3-rados python3-rbd qemu-utils sysfsutils udev util-linux"
-  DO ../+APPLY_PATCHES
+  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"
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/designate/Earthfile b/images/designate/Earthfile
index f3014be..d88dc10 100644
--- a/images/designate/Earthfile
+++ b/images/designate/Earthfile
@@ -1,17 +1,20 @@
 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:
-  ARG PROJECT=designate
-  ARG RELEASE=zed
-  ARG REF=d247267823034c5e656f74e91b50475aa54d3fa6
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF}
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "bind9utils"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/glance/Earthfile b/images/glance/Earthfile
index 8e203c9..c40c403 100644
--- a/images/glance/Earthfile
+++ b/images/glance/Earthfile
@@ -1,19 +1,31 @@
 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:
-  ARG PROJECT=glance
-  ARG RELEASE=zed
-  ARG REF=06a18202ab52c64803f044b8f848ed1c160905d2
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "glance_store[cinder]"
-  DO \
-    ../+APT_INSTALL \
-    --PACKAGES "ceph-common lsscsi nvme-cli python3-rados python3-rbd qemu-utils sysfsutils udev util-linux"
-  DO ../+APPLY_PATCHES
+  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"
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${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
new file mode 100644
index 0000000..d57f318
--- /dev/null
+++ b/images/glance/patches/glance_store/0000-rbd-compute-appropriate-resize-amount-before-resizin.patch
@@ -0,0 +1,146 @@
+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/Earthfile b/images/heat/Earthfile
index 87ee43b..c55fc42 100644
--- a/images/heat/Earthfile
+++ b/images/heat/Earthfile
@@ -1,17 +1,20 @@
 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:
-  ARG PROJECT=heat
-  ARG RELEASE=zed
-  ARG REF=a2b70a93658ecd2774f22c63a394c5629aefdbe7
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF}
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "curl jq"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/horizon/Earthfile b/images/horizon/Earthfile
index 6ff9199..fe9ac5f 100644
--- a/images/horizon/Earthfile
+++ b/images/horizon/Earthfile
@@ -1,18 +1,37 @@
 VERSION 0.7
 
+ARG --global PROJECT=horizon
+ARG --global RELEASE=2023.2
+ARG --global PROJECT_REF=3c6029cd94846235e25058b71522c13556f41f58
+
+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:
-  ARG PROJECT=horizon
-  ARG RELEASE=2023.2
-  ARG REF=3c6029cd94846235e25058b71522c13556f41f58
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "git+https://github.com/openstack/designate-dashboard.git@stable/${RELEASE} git+https://github.com/openstack/heat-dashboard.git@stable/${RELEASE} git+https://github.com/openstack/ironic-ui.git@stable/${RELEASE} git+https://github.com/openstack/magnum-ui.git@stable/${RELEASE} git+https://github.com/openstack/neutron-vpnaas-dashboard.git@stable/${RELEASE} git+https://github.com/openstack/octavia-dashboard.git@stable/${RELEASE} git+https://github.com/openstack/senlin-dashboard.git@stable/${RELEASE} git+https://github.com/openstack/manila-ui.git@stable/${RELEASE}"
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "apache2 gettext libapache2-mod-wsgi-py3"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/horizon/patches/0000-fix-ignore-errors-when-flavors-are-deleted.patch b/images/horizon/patches/horizon/0000-fix-ignore-errors-when-flavors-are-deleted.patch
similarity index 88%
rename from images/horizon/patches/0000-fix-ignore-errors-when-flavors-are-deleted.patch
rename to images/horizon/patches/horizon/0000-fix-ignore-errors-when-flavors-are-deleted.patch
index 50d68c9..211d055 100644
--- a/images/horizon/patches/0000-fix-ignore-errors-when-flavors-are-deleted.patch
+++ b/images/horizon/patches/horizon/0000-fix-ignore-errors-when-flavors-are-deleted.patch
@@ -1,4 +1,4 @@
-From c62527488bfeab588c4abbc8426688e4feef87a4 Mon Sep 17 00:00:00 2001
+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
@@ -13,7 +13,6 @@
 
 Closes-Bug: #2042362
 Change-Id: I37cc02102285b1e83ec1343b710a57fb5ac4ba15
-(cherry picked from commit 40759aa9cdb9b2162b3f50df751c500db94943b3)
 ---
  .../dashboards/admin/instances/tests.py         |  4 ----
  .../dashboards/admin/instances/views.py         | 17 +++++------------
@@ -22,10 +21,10 @@
  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 3630cb79ade..c6cf65e5dab 100644
+index 3630cb79a..c6cf65e5d 100644
 --- a/openstack_dashboard/dashboards/admin/instances/tests.py
 +++ b/openstack_dashboard/dashboards/admin/instances/tests.py
-@@ -133,10 +133,6 @@ def test_index_flavor_get_exception(self):
+@@ -133,10 +133,6 @@ class InstanceViewTest(test.BaseAdminViewTests):
          res = self.client.get(INDEX_URL)
          instances = res.context['table'].data
          self.assertTemplateUsed(res, INDEX_TEMPLATE)
@@ -37,10 +36,10 @@
  
          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 c35527fe465..efa28dd763e 100644
+index c35527fe4..efa28dd76 100644
 --- a/openstack_dashboard/dashboards/admin/instances/views.py
 +++ b/openstack_dashboard/dashboards/admin/instances/views.py
-@@ -33,6 +33,8 @@
+@@ -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
@@ -49,7 +48,7 @@
  from openstack_dashboard.dashboards.project.instances import views
  from openstack_dashboard.dashboards.project.instances.workflows \
      import update_instance
-@@ -215,18 +217,9 @@ def get_data(self):
+@@ -215,18 +217,9 @@ class AdminIndexView(tables.PagedTableMixin, tables.DataTableView):
                  else:
                      inst.image['name'] = _("-")
  
@@ -72,10 +71,10 @@
              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 70d32bc4b3c..c44dedd5b5a 100644
+index 5ab1b4a48..fe2f58c46 100644
 --- a/openstack_dashboard/dashboards/project/instances/tests.py
 +++ b/openstack_dashboard/dashboards/project/instances/tests.py
-@@ -316,6 +316,7 @@ def test_index_flavor_list_exception(self):
+@@ -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
@@ -84,10 +83,10 @@
          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 badf540b830..b848f6fffd9 100644
+index badf540b8..b848f6fff 100644
 --- a/openstack_dashboard/dashboards/project/instances/views.py
 +++ b/openstack_dashboard/dashboards/project/instances/views.py
-@@ -171,14 +171,9 @@ def get_data(self):
+@@ -171,14 +171,9 @@ class IndexView(tables.PagedTableMixin, tables.DataTableView):
          for instance in instances:
              self._populate_image_info(instance, image_dict, volume_dict)
  
@@ -105,3 +104,6 @@
  
          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
new file mode 100644
index 0000000..2e5b936
--- /dev/null
+++ b/images/horizon/patches/horizon/0001-Fixing-Incorrect-URL-when-browsing-Swift-containers.patch
@@ -0,0 +1,40 @@
+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/horizon/patches/0001-fix-disable-resizing-for-admins.patch b/images/horizon/patches/magnum-ui/0000-fix-disable-resizing-for-admins.patch
similarity index 88%
rename from images/horizon/patches/0001-fix-disable-resizing-for-admins.patch
rename to images/horizon/patches/magnum-ui/0000-fix-disable-resizing-for-admins.patch
index aaa5058..a64b453 100644
--- a/images/horizon/patches/0001-fix-disable-resizing-for-admins.patch
+++ b/images/horizon/patches/magnum-ui/0000-fix-disable-resizing-for-admins.patch
@@ -1,4 +1,4 @@
-From d3ac70fb12dc363a0fbed39bcfd3642e36f4515d Mon Sep 17 00:00:00 2001
+From a3671cc242adb85f792d1c8c57ccc7692f1ec251 Mon Sep 17 00:00:00 2001
 From: Mohammed Naser <mnaser@vexxhost.com>
 Date: Mon, 20 Feb 2023 00:55:14 +0000
 Subject: [PATCH] fix: disable resizing for admins
@@ -11,14 +11,13 @@
 project ID of the current user.
 
 Change-Id: If09c509abdd21a5a7b9bc374af52a06404fb0ff8
-(cherry picked from commit 345f853567d25f1b163025f0295c742582052748)
 ---
  .../clusters/resize/resize.service.js         |  9 ++++---
- .../clusters/resize/resize.service.spec.js    | 27 ++++++++++++++-----
- 2 files changed, 26 insertions(+), 10 deletions(-)
+ .../clusters/resize/resize.service.spec.js    | 25 +++++++++++++++----
+ 2 files changed, 25 insertions(+), 9 deletions(-)
 
 diff --git a/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.js b/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.js
-index ebc6a961..b86833a0 100644
+index ebc6a96..b86833a 100644
 --- a/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.js
 +++ b/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.js
 @@ -32,6 +32,7 @@
@@ -52,10 +51,10 @@
  
      function constructModalConfig(workerNodesList) {
 diff --git a/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.spec.js b/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.spec.js
-index 842df87d..27fa8064 100644
+index 842df87..645b149 100644
 --- a/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.spec.js
 +++ b/magnum_ui/static/dashboard/container-infra/clusters/resize/resize.service.spec.js
-@@ -19,16 +19,17 @@
+@@ -19,9 +19,10 @@
  
    describe('horizon.dashboard.container-infra.clusters.resize.service', function() {
  
@@ -68,14 +67,6 @@
      };
      var modal = {
        open: function(config) {
-         deferred = $q.defer();
-         deferred.resolve(config);
-         modalConfig = config;
--
-+``
-         return deferred.promise;
-       }
-     };
 @@ -50,6 +51,7 @@
          'horizon.dashboard.container-infra.clusters.resize.service');
        magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
@@ -110,3 +101,6 @@
      });
  
      it('should open the modal, hide the loading spinner and check the form model',
+-- 
+2.34.1
+
diff --git a/images/horizon/patches/0002-capi-avoid-going-through-heat-for-worker-list.patch b/images/horizon/patches/magnum-ui/0001-capi-avoid-going-through-heat-for-worker-list.patch
similarity index 100%
rename from images/horizon/patches/0002-capi-avoid-going-through-heat-for-worker-list.patch
rename to images/horizon/patches/magnum-ui/0001-capi-avoid-going-through-heat-for-worker-list.patch
diff --git a/images/ironic/Earthfile b/images/ironic/Earthfile
index 74b2ed9..5edcd7c 100644
--- a/images/ironic/Earthfile
+++ b/images/ironic/Earthfile
@@ -1,18 +1,21 @@
 VERSION 0.7
 
-image:
-  ARG PROJECT=ironic
-  ARG RELEASE=zed
-  ARG REF=e38735cb95263b0c54f2fd719ff6b714efbddbb3
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
+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"
-  DO \
-    ../+APT_INSTALL \
+
+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"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/keystone/Earthfile b/images/keystone/Earthfile
index 9478a03..7aae428 100644
--- a/images/keystone/Earthfile
+++ b/images/keystone/Earthfile
@@ -1,19 +1,22 @@
 VERSION 0.7
 
+ARG --global RELEASE=2023.2
+ARG --global PROJECT=keystone
+ARG --global PROJECT_REF=653d82b1b4e09b2ff37b56868e57d08c8e3af7dd
+
+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.6"
+
 image:
-  ARG PROJECT=keystone
-  ARG RELEASE=zed
-  ARG REF=72a4fc0f3ccf7a5ca9fc40e5364e14f881ec27b2
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "keystone-keycloak-backend==0.1.6" \
-    --EXTRAS "[ldap]"
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "apache2 libapache2-mod-wsgi-py3"
-  DO ../+APPLY_PATCHES
   ARG MOD_AUTH_OPENIDC_VERSION=2.4.12.1
   ARG TARGETARCH
   RUN \
@@ -27,4 +30,4 @@
     rm -rfv /var/lib/apt/lists/* libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${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
new file mode 100644
index 0000000..83689fa
--- /dev/null
+++ b/images/keystone/patches/keystone/0000-Ensure-application-credentials-take-account-of-impli.patch
@@ -0,0 +1,47 @@
+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/libvirtd/Earthfile b/images/libvirtd/Earthfile
index 07c358f..31792d2 100644
--- a/images/libvirtd/Earthfile
+++ b/images/libvirtd/Earthfile
@@ -3,7 +3,6 @@
 platform-image:
   ARG RELEASE=zed
   FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
-  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
   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
diff --git a/images/magnum/Earthfile b/images/magnum/Earthfile
index f7e189f..645d99c 100644
--- a/images/magnum/Earthfile
+++ b/images/magnum/Earthfile
@@ -1,18 +1,21 @@
 VERSION 0.7
 
+ARG --global PROJECT=magnum
+ARG --global RELEASE=2023.2
+ARG --global PROJECT_REF=156f6f52d38a670e6fe10725966aeea4ddf65146
+
+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.3"
+
 image:
-  ARG PROJECT=magnum
-  ARG RELEASE=zed
-  ARG REF=c671d8baf9d6f4705a1b832ae2d96980e5a58db6
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "magnum-cluster-api==0.13.3"
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "haproxy"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/magnum/patches/0000-containerd-cni-plugin-path-in-coreos-35.patch b/images/magnum/patches/0000-containerd-cni-plugin-path-in-coreos-35.patch
deleted file mode 100644
index 8d7c411..0000000
--- a/images/magnum/patches/0000-containerd-cni-plugin-path-in-coreos-35.patch
+++ /dev/null
@@ -1,35 +0,0 @@
-From 7f9f804a766083b65389b4cc2870fbb1a951b29e Mon Sep 17 00:00:00 2001
-From: Mohammed Naser <mnaser@vexxhost.com>
-Date: Thu, 9 Mar 2023 09:45:43 +0100
-Subject: [PATCH] Containerd cni plugin path in CoreOS 35 (#1)
-
-Task: 45387
-Story: 2010041
-
-In Fedora CoreOS 35 default containerd cni bin_dir is set to
-/usr/libexec/cni. Since we're installing our own in /opt/cni/bin need to
-override in containerd config.toml otherwise pods get stuck in
-ContainerCreating state looking for for ex. calico in wrong path.
-
-Change-Id: I3242b718e32c92942ac471bc7e182a42e803005b
-(cherry picked from commit fbfd3ce9a30fed291c96179f409821b7e016d2ba)
-
-Co-authored-by: Jakub Darmach <jakub@stackhpc.com>
----
- .../common/templates/kubernetes/fragments/install-cri.sh       | 3 +++
- 1 file changed, 3 insertions(+)
-
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/install-cri.sh b/magnum/drivers/common/templates/kubernetes/fragments/install-cri.sh
-index f60efe47a8..61204fe47a 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/install-cri.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/install-cri.sh
-@@ -10,6 +10,9 @@ ssh_cmd="ssh -F /srv/magnum/.ssh/config root@localhost"
- if [ "${CONTAINER_RUNTIME}" = "containerd"  ] ; then
-     $ssh_cmd systemctl disable docker.service docker.socket
-     $ssh_cmd systemctl stop docker.service docker.socket
-+    if $ssh_cmd [ -f /etc/containerd/config.toml ] ; then
-+        $ssh_cmd sed -i 's/bin_dir.*$/bin_dir\ =\ \""\/opt\/cni\/bin\/"\"/' /etc/containerd/config.toml
-+    fi
-     if [ -z "${CONTAINERD_TARBALL_URL}"  ] ; then
-         CONTAINERD_TARBALL_URL="https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/cri-containerd-cni-${CONTAINERD_VERSION}-linux-amd64.tar.gz"
-     fi
diff --git a/images/magnum/patches/0002-support-k8s-1-24.patch b/images/magnum/patches/0002-support-k8s-1-24.patch
deleted file mode 100644
index bc69c96..0000000
--- a/images/magnum/patches/0002-support-k8s-1-24.patch
+++ /dev/null
@@ -1,75 +0,0 @@
-From f25b5c0f89dcc16918d5d8636355831ce0dc4091 Mon Sep 17 00:00:00 2001
-From: Daniel Meyerholt <dxm523@gmail.com>
-Date: Sat, 28 May 2022 12:43:45 +0200
-Subject: [PATCH] Support K8s 1.24+
-
-Only specify dockershim options when container runtime is not containerd.
-Those options were ignored in the past when using containerd but since 1.24
-kubelet refuses to start.
-
-Task: 45282
-Story: 2010028
-
-Signed-off-by: Daniel Meyerholt <dxm523@gmail.com>
-Change-Id: Ib44cc30285c8bd4219d4a45dc956696505ddd570
-(cherry picked from commit f7cd2928d6a84e869c87c333b814de76cae9a920)
----
- .../kubernetes/fragments/configure-kubernetes-master.sh      | 3 ++-
- .../kubernetes/fragments/configure-kubernetes-minion.sh      | 3 ++-
- .../notes/support-dockershim-removal-cad104d069f1a50b.yaml   | 5 +++++
- 3 files changed, 9 insertions(+), 2 deletions(-)
- create mode 100644 releasenotes/notes/support-dockershim-removal-cad104d069f1a50b.yaml
-
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh
-index 42267404a1..61ca0a7a59 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh
-@@ -454,7 +454,6 @@ if [ -f /etc/sysconfig/docker ] ; then
-     sed -i -E 's/^OPTIONS=("|'"'"')/OPTIONS=\1'"${DOCKER_OPTIONS}"' /' /etc/sysconfig/docker
- fi
- 
--KUBELET_ARGS="${KUBELET_ARGS} --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"
- KUBELET_ARGS="${KUBELET_ARGS} --register-with-taints=node-role.kubernetes.io/master=:NoSchedule"
- KUBELET_ARGS="${KUBELET_ARGS} --node-labels=magnum.openstack.org/role=${NODEGROUP_ROLE}"
- KUBELET_ARGS="${KUBELET_ARGS} --node-labels=magnum.openstack.org/nodegroup=${NODEGROUP_NAME}"
-@@ -503,6 +502,8 @@ if [ ${CONTAINER_RUNTIME} = "containerd"  ] ; then
-     KUBELET_ARGS="${KUBELET_ARGS} --container-runtime=remote"
-     KUBELET_ARGS="${KUBELET_ARGS} --runtime-request-timeout=15m"
-     KUBELET_ARGS="${KUBELET_ARGS} --container-runtime-endpoint=unix:///run/containerd/containerd.sock"
-+else
-+    KUBELET_ARGS="${KUBELET_ARGS} --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"
- fi
- 
- if [ -z "${KUBE_NODE_IP}" ]; then
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh
-index 46055244ac..60fc1918bc 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh
-@@ -282,6 +282,8 @@ if [ ${CONTAINER_RUNTIME} = "containerd"  ] ; then
-     KUBELET_ARGS="${KUBELET_ARGS} --container-runtime=remote"
-     KUBELET_ARGS="${KUBELET_ARGS} --runtime-request-timeout=15m"
-     KUBELET_ARGS="${KUBELET_ARGS} --container-runtime-endpoint=unix:///run/containerd/containerd.sock"
-+else
-+    KUBELET_ARGS="${KUBELET_ARGS} --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"
- fi
- 
- auto_healing_enabled=$(echo ${AUTO_HEALING_ENABLED} | tr '[:upper:]' '[:lower:]')
-@@ -290,7 +292,6 @@ if [[ "${auto_healing_enabled}" = "true" && "${autohealing_controller}" = "drain
-     KUBELET_ARGS="${KUBELET_ARGS} --node-labels=draino-enabled=true"
- fi
- 
--KUBELET_ARGS="${KUBELET_ARGS} --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"
- 
- sed -i '
-     /^KUBELET_ADDRESS=/ s/=.*/="--address=0.0.0.0"/
-diff --git a/releasenotes/notes/support-dockershim-removal-cad104d069f1a50b.yaml b/releasenotes/notes/support-dockershim-removal-cad104d069f1a50b.yaml
-new file mode 100644
-index 0000000000..f228db6321
---- /dev/null
-+++ b/releasenotes/notes/support-dockershim-removal-cad104d069f1a50b.yaml
-@@ -0,0 +1,5 @@
-+---
-+fixes:
-+  - |
-+    Support K8s 1.24 which removed support of dockershim. Needs containerd as
-+    container runtime.
diff --git a/images/magnum/patches/0003-fix-kubelet-for-fedora-coreos-36-to-provide-real-resolvconf-to-containers.patch b/images/magnum/patches/0003-fix-kubelet-for-fedora-coreos-36-to-provide-real-resolvconf-to-containers.patch
deleted file mode 100644
index a79d935..0000000
--- a/images/magnum/patches/0003-fix-kubelet-for-fedora-coreos-36-to-provide-real-resolvconf-to-containers.patch
+++ /dev/null
@@ -1,48 +0,0 @@
-From 34564ae02c1e7bef3b69967c7497f201058c82a5 Mon Sep 17 00:00:00 2001
-From: Dale Smith <dale@catalystcloud.nz>
-Date: Thu, 22 Dec 2022 16:06:07 +1300
-Subject: [PATCH] Fix kubelet for Fedora CoreOS 36 to provide real resolvconf
- to containers.
-
-In Fedora CoreOS 36 CoreDNS cannot start correctly due to a loopback issue
-where /etc/resolv.conf is mounted and points to localhost.
-
-Tested on Fedora CoreOS 35,36,37, with Docker and containerd.
-
-https://coredns.io/plugins/loop/#troubleshooting-loops-in-kubernetes-clusters
-https://fedoraproject.org/wiki/Changes/systemd-resolved#Detailed_Description
-
-Story: 2010519
-Depends-On: I3242b718e32c92942ac471bc7e182a42e803005b
-
-Change-Id: I8106324ce71d6c22fa99e1a84b5a09743315811a
-(cherry picked from commit 5061dc5bb5c9aaba8fcfb3cb06404ada084a1908)
----
- .../kubernetes/fragments/configure-kubernetes-master.sh          | 1 +
- .../kubernetes/fragments/configure-kubernetes-minion.sh          | 1 +
- 2 files changed, 2 insertions(+)
-
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh
-index 61ca0a7a59..24d7e48f4f 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-master.sh
-@@ -435,6 +435,7 @@ $ssh_cmd mkdir -p /etc/kubernetes/manifests
- KUBELET_ARGS="--register-node=true --pod-manifest-path=/etc/kubernetes/manifests --hostname-override=${INSTANCE_NAME}"
- KUBELET_ARGS="${KUBELET_ARGS} --pod-infra-container-image=${CONTAINER_INFRA_PREFIX:-gcr.io/google_containers/}pause:3.1"
- KUBELET_ARGS="${KUBELET_ARGS} --cluster_dns=${DNS_SERVICE_IP} --cluster_domain=${DNS_CLUSTER_DOMAIN}"
-+KUBELET_ARGS="${KUBELET_ARGS} --resolv-conf=/run/systemd/resolve/resolv.conf"
- KUBELET_ARGS="${KUBELET_ARGS} --volume-plugin-dir=/var/lib/kubelet/volumeplugins"
- KUBELET_ARGS="${KUBELET_ARGS} ${KUBELET_OPTIONS}"
- 
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh
-index 60fc1918bc..6508ac3ef0 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/configure-kubernetes-minion.sh
-@@ -250,6 +250,7 @@ mkdir -p /etc/kubernetes/manifests
- KUBELET_ARGS="--pod-manifest-path=/etc/kubernetes/manifests --kubeconfig ${KUBELET_KUBECONFIG} --hostname-override=${INSTANCE_NAME}"
- KUBELET_ARGS="${KUBELET_ARGS} --address=${KUBE_NODE_IP} --port=10250 --read-only-port=0 --anonymous-auth=false --authorization-mode=Webhook --authentication-token-webhook=true"
- KUBELET_ARGS="${KUBELET_ARGS} --cluster_dns=${DNS_SERVICE_IP} --cluster_domain=${DNS_CLUSTER_DOMAIN}"
-+KUBELET_ARGS="${KUBELET_ARGS} --resolv-conf=/run/systemd/resolve/resolv.conf"
- KUBELET_ARGS="${KUBELET_ARGS} --volume-plugin-dir=/var/lib/kubelet/volumeplugins"
- KUBELET_ARGS="${KUBELET_ARGS} --node-labels=magnum.openstack.org/role=${NODEGROUP_ROLE}"
- KUBELET_ARGS="${KUBELET_ARGS} --node-labels=magnum.openstack.org/nodegroup=${NODEGROUP_NAME}"
diff --git a/images/magnum/patches/0004-adapt-cinder-csi-to-upstream-manifest.patch b/images/magnum/patches/0004-adapt-cinder-csi-to-upstream-manifest.patch
deleted file mode 100644
index 7d302cf..0000000
--- a/images/magnum/patches/0004-adapt-cinder-csi-to-upstream-manifest.patch
+++ /dev/null
@@ -1,860 +0,0 @@
-From b13335fc56d4938346619229bb2c23c128a1d58a Mon Sep 17 00:00:00 2001
-From: Michal Nasiadka <mnasiadka@gmail.com>
-Date: Fri, 11 Mar 2022 13:33:15 +0100
-Subject: [PATCH] Adapt Cinder CSI to upstream manifest
-
-- Bump also components to upstream manifest versions.
-- Add small tool to sync Cinder CSI manifests automatically
-
-Change-Id: Icd19b41d03b7aa200965a3357a8ddf8b4b40794a
-(cherry picked from commit ac5702c40653942634e259788434037e1e8c980a)
----
- doc/source/user/index.rst                     |  11 +
- .../kubernetes/fragments/enable-cinder-csi.sh | 237 +++++++++---------
- .../fragments/write-heat-params-master.sh     |   1 +
- .../drivers/heat/k8s_fedora_template_def.py   |   1 +
- .../templates/kubecluster.yaml                |  19 +-
- .../templates/kubemaster.yaml                 |   6 +
- .../unit/drivers/test_template_definition.py  |   6 +
- tools/sync/cinder-csi                         | 162 ++++++++++++
- 8 files changed, 322 insertions(+), 121 deletions(-)
- create mode 100755 tools/sync/cinder-csi
-
-diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst
-index 20c56400f8..9d8d747204 100644
---- a/doc/source/user/index.rst
-+++ b/doc/source/user/index.rst
-@@ -1400,30 +1400,35 @@ _`cinder_csi_plugin_tag`
-   <https://hub.docker.com/r/k8scloudprovider/cinder-csi-plugin/tags>`_.
-   Train default: v1.16.0
-   Ussuri default: v1.18.0
-+  Yoga default: v1.23.0
- 
- _`csi_attacher_tag`
-   This label allows users to override the default container tag for CSI attacher.
-   For additional tags, `refer to CSI attacher page
-   <https://quay.io/repository/k8scsi/csi-attacher?tab=tags>`_.
-   Ussuri-default: v2.0.0
-+  Yoga-default: v3.3.0
- 
- _`csi_provisioner_tag`
-   This label allows users to override the default container tag for CSI provisioner.
-   For additional tags, `refer to CSI provisioner page
-   <https://quay.io/repository/k8scsi/csi-provisioner?tab=tags>`_.
-   Ussuri-default: v1.4.0
-+  Yoga-default: v3.0.0
- 
- _`csi_snapshotter_tag`
-   This label allows users to override the default container tag for CSI snapshotter.
-   For additional tags, `refer to CSI snapshotter page
-   <https://quay.io/repository/k8scsi/csi-snapshotter?tab=tags>`_.
-   Ussuri-default: v1.2.2
-+  Yoga-default: v4.2.1
- 
- _`csi_resizer_tag`
-   This label allows users to override the default container tag for CSI resizer.
-   For additional tags, `refer to CSI resizer page
-   <https://quay.io/repository/k8scsi/csi-resizer?tab=tags>`_.
-   Ussuri-default: v0.3.0
-+  Yoga-default: v1.3.0
- 
- _`csi_node_driver_registrar_tag`
-   This label allows users to override the default container tag for CSI node
-@@ -1431,6 +1436,12 @@ _`csi_node_driver_registrar_tag`
-   page
-   <https://quay.io/repository/k8scsi/csi-node-driver-registrar?tab=tags>`_.
-   Ussuri-default: v1.1.0
-+  Yoga-default: v2.4.0
-+
-+-`csi_liveness_probe_tag`
-+  This label allows users to override the default container tag for CSI
-+  liveness probe.
-+  Yoga-default: v2.5.0
- 
- _`keystone_auth_enabled`
-   If this label is set to True, Kubernetes will support use Keystone for
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/enable-cinder-csi.sh b/magnum/drivers/common/templates/kubernetes/fragments/enable-cinder-csi.sh
-index b85258a5f3..524b5e98ed 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/enable-cinder-csi.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/enable-cinder-csi.sh
-@@ -12,15 +12,15 @@ if [ "${volume_driver}" = "cinder" ] && [ "${cinder_csi_enabled}" = "true" ]; th
-     echo "Writing File: $CINDER_CSI_DEPLOY"
-     mkdir -p $(dirname ${CINDER_CSI_DEPLOY})
-     cat << EOF > ${CINDER_CSI_DEPLOY}
-----
- # This YAML file contains RBAC API objects,
- # which are necessary to run csi controller plugin
-----
-+
- apiVersion: v1
- kind: ServiceAccount
- metadata:
-   name: csi-cinder-controller-sa
-   namespace: kube-system
-+
- ---
- # external attacher
- kind: ClusterRole
-@@ -30,16 +30,20 @@ metadata:
- rules:
-   - apiGroups: [""]
-     resources: ["persistentvolumes"]
--    verbs: ["get", "list", "watch", "update", "patch"]
--  - apiGroups: [""]
--    resources: ["nodes"]
-+    verbs: ["get", "list", "watch", "patch"]
-+  - apiGroups: ["storage.k8s.io"]
-+    resources: ["csinodes"]
-     verbs: ["get", "list", "watch"]
-   - apiGroups: ["storage.k8s.io"]
-     resources: ["volumeattachments"]
--    verbs: ["get", "list", "watch", "update", "patch"]
-+    verbs: ["get", "list", "watch", "patch"]
-   - apiGroups: ["storage.k8s.io"]
--    resources: ["csinodes"]
--    verbs: ["get", "list", "watch"]
-+    resources: ["volumeattachments/status"]
-+    verbs: ["patch"]
-+  - apiGroups: ["coordination.k8s.io"]
-+    resources: ["leases"]
-+    verbs: ["get", "watch", "list", "delete", "update", "create"]
-+
- ---
- kind: ClusterRoleBinding
- apiVersion: rbac.authorization.k8s.io/v1
-@@ -53,6 +57,7 @@ roleRef:
-   kind: ClusterRole
-   name: csi-attacher-role
-   apiGroup: rbac.authorization.k8s.io
-+
- ---
- # external Provisioner
- kind: ClusterRole
-@@ -84,6 +89,12 @@ rules:
-   - apiGroups: ["snapshot.storage.k8s.io"]
-     resources: ["volumesnapshotcontents"]
-     verbs: ["get", "list"]
-+  - apiGroups: ["storage.k8s.io"]
-+    resources: ["volumeattachments"]
-+    verbs: ["get", "list", "watch"]
-+  - apiGroups: ["coordination.k8s.io"]
-+    resources: ["leases"]
-+    verbs: ["get", "watch", "list", "delete", "update", "create"]
- ---
- kind: ClusterRoleBinding
- apiVersion: rbac.authorization.k8s.io/v1
-@@ -97,6 +108,7 @@ roleRef:
-   kind: ClusterRole
-   name: csi-provisioner-role
-   apiGroup: rbac.authorization.k8s.io
-+
- ---
- # external snapshotter
- kind: ClusterRole
-@@ -104,36 +116,28 @@ apiVersion: rbac.authorization.k8s.io/v1
- metadata:
-   name: csi-snapshotter-role
- rules:
--  - apiGroups: [""]
--    resources: ["persistentvolumes"]
--    verbs: ["get", "list", "watch"]
--  - apiGroups: [""]
--    resources: ["persistentvolumeclaims"]
--    verbs: ["get", "list", "watch"]
--  - apiGroups: ["storage.k8s.io"]
--    resources: ["storageclasses"]
--    verbs: ["get", "list", "watch"]
-   - apiGroups: [""]
-     resources: ["events"]
-     verbs: ["list", "watch", "create", "update", "patch"]
--  - apiGroups: [""]
--    resources: ["secrets"]
--    verbs: ["get", "list"]
-+  # Secret permission is optional.
-+  # Enable it if your driver needs secret.
-+  # For example, `csi.storage.k8s.io/snapshotter-secret-name` is set in VolumeSnapshotClass.
-+  # See https://kubernetes-csi.github.io/docs/secrets-and-credentials.html for more details.
-+  #  - apiGroups: [""]
-+  #    resources: ["secrets"]
-+  #    verbs: ["get", "list"]
-   - apiGroups: ["snapshot.storage.k8s.io"]
-     resources: ["volumesnapshotclasses"]
-     verbs: ["get", "list", "watch"]
-   - apiGroups: ["snapshot.storage.k8s.io"]
-     resources: ["volumesnapshotcontents"]
--    verbs: ["create", "get", "list", "watch", "update", "delete"]
-+    verbs: ["create", "get", "list", "watch", "update", "delete", "patch"]
-   - apiGroups: ["snapshot.storage.k8s.io"]
--    resources: ["volumesnapshots"]
--    verbs: ["get", "list", "watch", "update"]
--  - apiGroups: ["snapshot.storage.k8s.io"]
--    resources: ["volumesnapshots/status"]
--    verbs: ["update"]
--  - apiGroups: ["apiextensions.k8s.io"]
--    resources: ["customresourcedefinitions"]
--    verbs: ["create", "list", "watch", "delete"]
-+    resources: ["volumesnapshotcontents/status"]
-+    verbs: ["update", "patch"]
-+  - apiGroups: ["coordination.k8s.io"]
-+    resources: ["leases"]
-+    verbs: ["get", "watch", "list", "delete", "update", "create"]
- ---
- kind: ClusterRoleBinding
- apiVersion: rbac.authorization.k8s.io/v1
-@@ -148,6 +152,7 @@ roleRef:
-   name: csi-snapshotter-role
-   apiGroup: rbac.authorization.k8s.io
- ---
-+
- # External Resizer
- kind: ClusterRole
- apiVersion: rbac.authorization.k8s.io/v1
-@@ -161,19 +166,22 @@ rules:
-   #   verbs: ["get", "list", "watch"]
-   - apiGroups: [""]
-     resources: ["persistentvolumes"]
--    verbs: ["get", "list", "watch", "update", "patch"]
-+    verbs: ["get", "list", "watch", "patch"]
-   - apiGroups: [""]
-     resources: ["persistentvolumeclaims"]
-     verbs: ["get", "list", "watch"]
-   - apiGroups: [""]
--    resources: ["persistentvolumeclaims/status"]
--    verbs: ["update", "patch"]
--  - apiGroups: ["storage.k8s.io"]
--    resources: ["storageclasses"]
-+    resources: ["pods"]
-     verbs: ["get", "list", "watch"]
-+  - apiGroups: [""]
-+    resources: ["persistentvolumeclaims/status"]
-+    verbs: ["patch"]
-   - apiGroups: [""]
-     resources: ["events"]
-     verbs: ["list", "watch", "create", "update", "patch"]
-+  - apiGroups: ["coordination.k8s.io"]
-+    resources: ["leases"]
-+    verbs: ["get", "watch", "list", "delete", "update", "create"]
- ---
- kind: ClusterRoleBinding
- apiVersion: rbac.authorization.k8s.io/v1
-@@ -187,56 +195,24 @@ roleRef:
-   kind: ClusterRole
-   name: csi-resizer-role
-   apiGroup: rbac.authorization.k8s.io
-----
--kind: Role
--apiVersion: rbac.authorization.k8s.io/v1
--metadata:
--  namespace: kube-system
--  name: external-resizer-cfg
--rules:
--- apiGroups: ["coordination.k8s.io"]
--  resources: ["leases"]
--  verbs: ["get", "watch", "list", "delete", "update", "create"]
-----
--kind: RoleBinding
--apiVersion: rbac.authorization.k8s.io/v1
--metadata:
--  name: csi-resizer-role-cfg
--  namespace: kube-system
--subjects:
--  - kind: ServiceAccount
--    name: csi-cinder-controller-sa
--    namespace: kube-system
--roleRef:
--  kind: Role
--  name: external-resizer-cfg
--  apiGroup: rbac.authorization.k8s.io
-+
- ---
- # This YAML file contains CSI Controller Plugin Sidecars
- # external-attacher, external-provisioner, external-snapshotter
-----
--kind: Service
--apiVersion: v1
--metadata:
--  name: csi-cinder-controller-service
--  namespace: kube-system
--  labels:
--    app: csi-cinder-controllerplugin
--spec:
--  selector:
--    app: csi-cinder-controllerplugin
--  ports:
--    - name: dummy
--      port: 12345
-----
--kind: StatefulSet
-+# external-resize, liveness-probe
-+
-+kind: Deployment
- apiVersion: apps/v1
- metadata:
-   name: csi-cinder-controllerplugin
-   namespace: kube-system
- spec:
--  serviceName: "csi-cinder-controller-service"
-   replicas: 1
-+  strategy:
-+    type: RollingUpdate
-+    rollingUpdate:
-+      maxUnavailable: 0
-+      maxSurge: 1
-   selector:
-     matchLabels:
-       app: csi-cinder-controllerplugin
-@@ -246,6 +222,7 @@ spec:
-         app: csi-cinder-controllerplugin
-     spec:
-       serviceAccount: csi-cinder-controller-sa
-+      hostNetwork: true
-       tolerations:
-         # Make sure the pod can be scheduled on master kubelet.
-         - effect: NoSchedule
-@@ -257,11 +234,11 @@ spec:
-         node-role.kubernetes.io/master: ""
-       containers:
-         - name: csi-attacher
--          image: ${CONTAINER_INFRA_PREFIX:-quay.io/k8scsi/}csi-attacher:${CSI_ATTACHER_TAG}
-+          image: ${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}csi-attacher:${CSI_ATTACHER_TAG}
-           args:
--            - "--v=5"
-             - "--csi-address=\$(ADDRESS)"
-             - "--timeout=3m"
-+            - "--leader-election=true"
-           resources:
-             requests:
-               cpu: 20m
-@@ -273,10 +250,14 @@ spec:
-             - name: socket-dir
-               mountPath: /var/lib/csi/sockets/pluginproxy/
-         - name: csi-provisioner
--          image: ${CONTAINER_INFRA_PREFIX:-quay.io/k8scsi/}csi-provisioner:${CSI_PROVISIONER_TAG}
-+          image: ${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}csi-provisioner:${CSI_PROVISIONER_TAG}
-           args:
-             - "--csi-address=\$(ADDRESS)"
-             - "--timeout=3m"
-+            - "--default-fstype=ext4"
-+            - "--feature-gates=Topology=true"
-+            - "--extra-create-metadata"
-+            - "--leader-election=true"
-           resources:
-             requests:
-               cpu: 20m
-@@ -288,9 +269,12 @@ spec:
-             - name: socket-dir
-               mountPath: /var/lib/csi/sockets/pluginproxy/
-         - name: csi-snapshotter
--          image: ${CONTAINER_INFRA_PREFIX:-quay.io/k8scsi/}csi-snapshotter:${CSI_SNAPSHOTTER_TAG}
-+          image: ${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}csi-snapshotter:${CSI_SNAPSHOTTER_TAG}
-           args:
-             - "--csi-address=\$(ADDRESS)"
-+            - "--timeout=3m"
-+            - "--extra-create-metadata"
-+            - "--leader-election=true"
-           resources:
-             requests:
-               cpu: 20m
-@@ -302,10 +286,12 @@ spec:
-             - mountPath: /var/lib/csi/sockets/pluginproxy/
-               name: socket-dir
-         - name: csi-resizer
--          image: ${CONTAINER_INFRA_PREFIX:-quay.io/k8scsi/}csi-resizer:${CSI_RESIZER_TAG}
-+          image: ${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}csi-resizer:${CSI_RESIZER_TAG}
-           args:
--            - "--v=5"
-             - "--csi-address=\$(ADDRESS)"
-+            - "--timeout=3m"
-+            - "--handle-volume-inuse-error=false"
-+            - "--leader-election=true"
-           resources:
-             requests:
-               cpu: 20m
-@@ -316,22 +302,27 @@ spec:
-           volumeMounts:
-             - name: socket-dir
-               mountPath: /var/lib/csi/sockets/pluginproxy/
-+        - name: liveness-probe
-+          image: ${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}livenessprobe:${CSI_LIVENESS_PROBE_TAG}
-+          args:
-+            - "--csi-address=\$(ADDRESS)"
-+          resources:
-+            requests:
-+              cpu: 20m
-+          env:
-+            - name: ADDRESS
-+              value: /var/lib/csi/sockets/pluginproxy/csi.sock
-+          volumeMounts:
-+            - mountPath: /var/lib/csi/sockets/pluginproxy/
-+              name: socket-dir
-         - name: cinder-csi-plugin
-           image: ${CONTAINER_INFRA_PREFIX:-docker.io/k8scloudprovider/}cinder-csi-plugin:${CINDER_CSI_PLUGIN_TAG}
--          args :
-+          args:
-             - /bin/cinder-csi-plugin
--            - "--nodeid=\$(NODE_ID)"
-             - "--endpoint=\$(CSI_ENDPOINT)"
-             - "--cloud-config=\$(CLOUD_CONFIG)"
-             - "--cluster=\$(CLUSTER_NAME)"
--          resources:
--            requests:
--              cpu: 20m
-           env:
--            - name: NODE_ID
--              valueFrom:
--                fieldRef:
--                  fieldPath: spec.nodeName
-             - name: CSI_ENDPOINT
-               value: unix://csi/csi.sock
-             - name: CLOUD_CONFIG
-@@ -339,6 +330,19 @@ spec:
-             - name: CLUSTER_NAME
-               value: kubernetes
-           imagePullPolicy: "IfNotPresent"
-+          ports:
-+            - containerPort: 9808
-+              name: healthz
-+              protocol: TCP
-+          # The probe
-+          livenessProbe:
-+            failureThreshold: 5
-+            httpGet:
-+              path: /healthz
-+              port: healthz
-+            initialDelaySeconds: 10
-+            timeoutSeconds: 10
-+            periodSeconds: 60
-           volumeMounts:
-             - name: socket-dir
-               mountPath: /csi
-@@ -360,7 +364,7 @@ spec:
-             type: File
- ---
- # This YAML defines all API objects to create RBAC roles for csi node plugin.
-----
-+
- apiVersion: v1
- kind: ServiceAccount
- metadata:
-@@ -375,6 +379,7 @@ rules:
-   - apiGroups: [""]
-     resources: ["events"]
-     verbs: ["get", "list", "watch", "create", "update", "patch"]
-+
- ---
- kind: ClusterRoleBinding
- apiVersion: rbac.authorization.k8s.io/v1
-@@ -391,7 +396,7 @@ roleRef:
- ---
- # This YAML file contains driver-registrar & csi driver nodeplugin API objects,
- # which are necessary to run csi nodeplugin for cinder.
-----
-+
- kind: DaemonSet
- apiVersion: apps/v1
- metadata:
-@@ -412,17 +417,10 @@ spec:
-       hostNetwork: true
-       containers:
-         - name: node-driver-registrar
--          image: ${CONTAINER_INFRA_PREFIX:-quay.io/k8scsi/}csi-node-driver-registrar:${CSI_NODE_DRIVER_REGISTRAR_TAG}
-+          image: ${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}csi-node-driver-registrar:${CSI_NODE_DRIVER_REGISTRAR_TAG}
-           args:
-             - "--csi-address=\$(ADDRESS)"
-             - "--kubelet-registration-path=\$(DRIVER_REG_SOCK_PATH)"
--          resources:
--            requests:
--              cpu: 25m
--          lifecycle:
--            preStop:
--              exec:
--                command: ["/bin/sh", "-c", "rm -rf /registration/cinder.csi.openstack.org /registration/cinder.csi.openstack.org-reg.sock"]
-           env:
-             - name: ADDRESS
-               value: /csi/csi.sock
-@@ -438,6 +436,16 @@ spec:
-               mountPath: /csi
-             - name: registration-dir
-               mountPath: /registration
-+        - name: liveness-probe
-+          image: ${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}livenessprobe:${CSI_LIVENESS_PROBE_TAG}
-+          args:
-+            - --csi-address=/csi/csi.sock
-+          resources:
-+            requests:
-+              cpu: 20m
-+          volumeMounts:
-+            - name: socket-dir
-+              mountPath: /csi
-         - name: cinder-csi-plugin
-           securityContext:
-             privileged: true
-@@ -445,33 +453,35 @@ spec:
-               add: ["SYS_ADMIN"]
-             allowPrivilegeEscalation: true
-           image: ${CONTAINER_INFRA_PREFIX:-docker.io/k8scloudprovider/}cinder-csi-plugin:${CINDER_CSI_PLUGIN_TAG}
--          args :
-+          args:
-             - /bin/cinder-csi-plugin
--            - "--nodeid=\$(NODE_ID)"
-             - "--endpoint=\$(CSI_ENDPOINT)"
-             - "--cloud-config=\$(CLOUD_CONFIG)"
--          resources:
--            requests:
--              cpu: 25m
-           env:
--            - name: NODE_ID
--              valueFrom:
--                fieldRef:
--                  fieldPath: spec.nodeName
-             - name: CSI_ENDPOINT
-               value: unix://csi/csi.sock
-             - name: CLOUD_CONFIG
-               value: /etc/config/cloud-config
-           imagePullPolicy: "IfNotPresent"
-+          ports:
-+            - containerPort: 9808
-+              name: healthz
-+              protocol: TCP
-+          # The probe
-+          livenessProbe:
-+            failureThreshold: 5
-+            httpGet:
-+              path: /healthz
-+              port: healthz
-+            initialDelaySeconds: 10
-+            timeoutSeconds: 3
-+            periodSeconds: 10
-           volumeMounts:
-             - name: socket-dir
-               mountPath: /csi
-             - name: kubelet-dir
-               mountPath: /var/lib/kubelet
-               mountPropagation: "Bidirectional"
--            - name: pods-cloud-data
--              mountPath: /var/lib/cloud/data
--              readOnly: true
-             - name: pods-probe-dir
-               mountPath: /dev
-               mountPropagation: "HostToContainer"
-@@ -494,9 +504,6 @@ spec:
-           hostPath:
-             path: /var/lib/kubelet
-             type: Directory
--        - name: pods-cloud-data
--          hostPath:
--            path: /var/lib/cloud/data
-         - name: pods-probe-dir
-           hostPath:
-             path: /dev
-diff --git a/magnum/drivers/common/templates/kubernetes/fragments/write-heat-params-master.sh b/magnum/drivers/common/templates/kubernetes/fragments/write-heat-params-master.sh
-index a50b184558..0cd02bf95b 100644
---- a/magnum/drivers/common/templates/kubernetes/fragments/write-heat-params-master.sh
-+++ b/magnum/drivers/common/templates/kubernetes/fragments/write-heat-params-master.sh
-@@ -143,6 +143,7 @@ CSI_PROVISIONER_TAG="$CSI_PROVISIONER_TAG"
- CSI_SNAPSHOTTER_TAG="$CSI_SNAPSHOTTER_TAG"
- CSI_RESIZER_TAG="$CSI_RESIZER_TAG"
- CSI_NODE_DRIVER_REGISTRAR_TAG="$CSI_NODE_DRIVER_REGISTRAR_TAG"
-+CSI_LIVENESS_PROBE_TAG="$CSI_LIVENESS_PROBE_TAG"
- DRAINO_TAG="$DRAINO_TAG"
- MAGNUM_AUTO_HEALER_TAG="$MAGNUM_AUTO_HEALER_TAG"
- AUTOSCALER_TAG="$AUTOSCALER_TAG"
-diff --git a/magnum/drivers/heat/k8s_fedora_template_def.py b/magnum/drivers/heat/k8s_fedora_template_def.py
-index 659069bc28..a4ec6250ab 100644
---- a/magnum/drivers/heat/k8s_fedora_template_def.py
-+++ b/magnum/drivers/heat/k8s_fedora_template_def.py
-@@ -90,6 +90,7 @@ def get_params(self, context, cluster_template, cluster, **kwargs):
-                       'csi_attacher_tag', 'csi_provisioner_tag',
-                       'csi_snapshotter_tag', 'csi_resizer_tag',
-                       'csi_node_driver_registrar_tag',
-+                      'csi_liveness_probe_tag',
-                       'etcd_tag', 'flannel_tag', 'flannel_cni_tag',
-                       'cloud_provider_tag',
-                       'prometheus_tag', 'grafana_tag',
-diff --git a/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml b/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml
-index 35ca781d42..15bfd9af25 100644
---- a/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml
-+++ b/magnum/drivers/k8s_fedora_coreos_v1/templates/kubecluster.yaml
-@@ -866,32 +866,38 @@ parameters:
-     description: tag of cinder csi plugin
-       tag of the k8scloudprovider/cinder-csi-plugin container
-       https://hub.docker.com/r/k8scloudprovider/cinder-csi-plugin/tags/
--    default: v1.18.0
-+    default: v1.23.0
- 
-   csi_attacher_tag:
-     type: string
-     description: tag of csi attacher
--    default: v2.0.0
-+    default: v3.3.0
- 
-   csi_provisioner_tag:
-     type: string
-     description: tag of csi provisioner
--    default: v1.4.0
-+    default: v3.0.0
- 
-   csi_snapshotter_tag:
-     type: string
-     description: tag of csi snapshotter
--    default: v1.2.2
-+    default: v4.2.1
- 
-   csi_resizer_tag:
-     type: string
-     description: tag of csi resizer
--    default: v0.3.0
-+    default: v1.3.0
- 
-   csi_node_driver_registrar_tag:
-     type: string
-     description: tag of csi node driver registrar
--    default: v1.1.0
-+    default: v2.4.0
-+
-+  csi_liveness_probe_tag:
-+    type: string
-+    description: tag of cinder csi liveness probe
-+      tag of the k8s.gcr.io/sig-storage/liveness-probe container
-+    default: v2.5.0
- 
-   node_problem_detector_tag:
-     type: string
-@@ -1384,6 +1390,7 @@ resources:
-           csi_snapshotter_tag: {get_param: csi_snapshotter_tag}
-           csi_resizer_tag: {get_param: csi_resizer_tag}
-           csi_node_driver_registrar_tag: {get_param: csi_node_driver_registrar_tag}
-+          csi_liveness_probe_tag: {get_param: csi_liveness_probe_tag}
-           draino_tag: {get_param: draino_tag}
-           autoscaler_tag: {get_param: autoscaler_tag}
-           min_node_count: {get_param: min_node_count}
-diff --git a/magnum/drivers/k8s_fedora_coreos_v1/templates/kubemaster.yaml b/magnum/drivers/k8s_fedora_coreos_v1/templates/kubemaster.yaml
-index a038f144d0..917f010db8 100644
---- a/magnum/drivers/k8s_fedora_coreos_v1/templates/kubemaster.yaml
-+++ b/magnum/drivers/k8s_fedora_coreos_v1/templates/kubemaster.yaml
-@@ -621,6 +621,11 @@ parameters:
-     type: string
-     description: tag of csi node driver registrar
- 
-+  csi_liveness_probe_tag:
-+    type: string
-+    description: >
-+      Tag of liveness-probe for cinder csi.
-+
-   node_problem_detector_tag:
-     type: string
-     description: tag of the node problem detector container
-@@ -910,6 +915,7 @@ resources:
-                   "$CSI_SNAPSHOTTER_TAG": {get_param: csi_snapshotter_tag}
-                   "$CSI_RESIZER_TAG": {get_param: csi_resizer_tag}
-                   "$CSI_NODE_DRIVER_REGISTRAR_TAG": {get_param: csi_node_driver_registrar_tag}
-+                  "$CSI_LIVENESS_PROBE_TAG":  {get_param: csi_liveness_probe_tag}
-                   "$DRAINO_TAG": {get_param: draino_tag}
-                   "$AUTOSCALER_TAG": {get_param: autoscaler_tag}
-                   "$MIN_NODE_COUNT": {get_param: min_node_count}
-diff --git a/magnum/tests/unit/drivers/test_template_definition.py b/magnum/tests/unit/drivers/test_template_definition.py
-index b523744597..7b08196bf1 100644
---- a/magnum/tests/unit/drivers/test_template_definition.py
-+++ b/magnum/tests/unit/drivers/test_template_definition.py
-@@ -600,6 +600,8 @@ def test_k8s_get_params(self, mock_generate_csr_and_key,
-             'csi_resizer_tag')
-         csi_node_driver_registrar_tag = mock_cluster.labels.get(
-             'csi_node_driver_registrar_tag')
-+        csi_liveness_probe_tag = mock_cluster.labels.get(
-+            'csi_liveness_probe_tag')
-         draino_tag = mock_cluster.labels.get('draino_tag')
-         autoscaler_tag = mock_cluster.labels.get('autoscaler_tag')
-         min_node_count = mock_cluster.labels.get('min_node_count')
-@@ -725,6 +727,7 @@ def test_k8s_get_params(self, mock_generate_csr_and_key,
-             'csi_snapshotter_tag': csi_snapshotter_tag,
-             'csi_resizer_tag': csi_resizer_tag,
-             'csi_node_driver_registrar_tag': csi_node_driver_registrar_tag,
-+            'csi_liveness_probe_tag': csi_liveness_probe_tag,
-             'draino_tag': draino_tag,
-             'autoscaler_tag': autoscaler_tag,
-             'min_node_count': min_node_count,
-@@ -1161,6 +1164,8 @@ def test_k8s_get_params_insecure(self, mock_generate_csr_and_key,
-             'csi_resizer_tag')
-         csi_node_driver_registrar_tag = mock_cluster.labels.get(
-             'csi_node_driver_registrar_tag')
-+        csi_liveness_probe_tag = mock_cluster.labels.get(
-+            'csi_liveness_probe_tag')
-         draino_tag = mock_cluster.labels.get('draino_tag')
-         autoscaler_tag = mock_cluster.labels.get('autoscaler_tag')
-         min_node_count = mock_cluster.labels.get('min_node_count')
-@@ -1290,6 +1295,7 @@ def test_k8s_get_params_insecure(self, mock_generate_csr_and_key,
-             'csi_snapshotter_tag': csi_snapshotter_tag,
-             'csi_resizer_tag': csi_resizer_tag,
-             'csi_node_driver_registrar_tag': csi_node_driver_registrar_tag,
-+            'csi_liveness_probe_tag': csi_liveness_probe_tag,
-             'draino_tag': draino_tag,
-             'autoscaler_tag': autoscaler_tag,
-             'min_node_count': min_node_count,
-diff --git a/tools/sync/cinder-csi b/tools/sync/cinder-csi
-new file mode 100755
-index 0000000000..5789631d52
---- /dev/null
-+++ b/tools/sync/cinder-csi
-@@ -0,0 +1,162 @@
-+#!/usr/bin/env python3.9
-+
-+import requests
-+
-+manifest_data = []
-+
-+files = requests.get("https://api.github.com/repos/kubernetes/cloud-provider-openstack/contents/manifests/cinder-csi-plugin").json()
-+for file in files:
-+    if file['name'] == 'csi-secret-cinderplugin.yaml':
-+        continue
-+
-+    r = requests.get(file['download_url'])
-+    manifest_data.append(r.text)
-+
-+manifests = "---\n".join(manifest_data)
-+
-+# Clean-ups
-+manifests = manifests.replace(
-+"""
-+            # - name: cacert
-+            #   mountPath: /etc/cacert
-+            #   readOnly: true
-+""",
-+"""
-+            - name: cacert
-+              mountPath: /etc/kubernetes/ca-bundle.crt
-+              readOnly: true
-+""").replace(
-+"""
-+            secretName: cloud-config
-+        # - name: cacert
-+        #   hostPath:
-+        #     path: /etc/cacert
-+""",
-+"""
-+            secretName: cinder-csi-cloud-config
-+        - name: cacert
-+          hostPath:
-+            path: /etc/kubernetes/ca-bundle.crt
-+            type: File
-+""").replace(
-+"""
-+      serviceAccount: csi-cinder-controller-sa
-+""",
-+"""
-+      serviceAccount: csi-cinder-controller-sa
-+      hostNetwork: true
-+      tolerations:
-+        # Make sure the pod can be scheduled on master kubelet.
-+        - effect: NoSchedule
-+          operator: Exists
-+        # Mark the pod as a critical add-on for rescheduling.
-+        - key: CriticalAddonsOnly
-+          operator: Exists
-+      nodeSelector:
-+        node-role.kubernetes.io/master: ""
-+""").replace(
-+"""
-+            - --csi-address=/csi/csi.sock
-+""",
-+"""
-+            - --csi-address=/csi/csi.sock
-+          resources:
-+            requests:
-+              cpu: 20m
-+""").replace(
-+"""
-+          env:
-+            - name: ADDRESS
-+              value: /var/lib/csi/sockets/pluginproxy/csi.sock
-+""",
-+"""
-+          resources:
-+            requests:
-+              cpu: 20m
-+          env:
-+            - name: ADDRESS
-+              value: /var/lib/csi/sockets/pluginproxy/csi.sock
-+""").replace(
-+    "$(",
-+    "\$("
-+).replace(
-+    "k8s.gcr.io/sig-storage/",
-+    "${CONTAINER_INFRA_PREFIX:-k8s.gcr.io/sig-storage/}"
-+).replace(
-+    "docker.io/k8scloudprovider/",
-+    "${CONTAINER_INFRA_PREFIX:-docker.io/k8scloudprovider/}",
-+).replace(
-+    "csi-attacher:v3.4.0",
-+    "csi-attacher:${CSI_ATTACHER_TAG}",
-+).replace(
-+    "csi-provisioner:v3.1.0",
-+    "csi-provisioner:${CSI_PROVISIONER_TAG}",
-+).replace(
-+    "csi-snapshotter:v6.0.1",
-+    "csi-snapshotter:${CSI_SNAPSHOTTER_TAG}",
-+).replace(
-+    "csi-resizer:v1.4.0",
-+    "csi-resizer:${CSI_RESIZER_TAG}",
-+).replace(
-+    "livenessprobe:v2.7.0",
-+    "livenessprobe:${CSI_LIVENESS_PROBE_TAG}",
-+).replace(
-+    "cinder-csi-plugin:latest",
-+    "cinder-csi-plugin:${CINDER_CSI_PLUGIN_TAG}",
-+).replace(
-+    "csi-node-driver-registrar:v2.5.1",
-+    "csi-node-driver-registrar:${CSI_NODE_DRIVER_REGISTRAR_TAG}",
-+).replace(
-+    "/etc/config/cloud.conf",
-+    "/etc/config/cloud-config"
-+)
-+
-+template = f"""step="enable-cinder-csi"
-+printf "Starting to run ${{step}}\\n"
-+
-+. /etc/sysconfig/heat-params
-+
-+volume_driver=$(echo "${{VOLUME_DRIVER}}" | tr '[:upper:]' '[:lower:]')
-+cinder_csi_enabled=$(echo $CINDER_CSI_ENABLED | tr '[:upper:]' '[:lower:]')
-+
-+if [ "${{volume_driver}}" = "cinder" ] && [ "${{cinder_csi_enabled}}" = "true" ]; then
-+    # Generate Cinder CSI manifest file
-+    CINDER_CSI_DEPLOY=/srv/magnum/kubernetes/manifests/cinder-csi.yaml
-+    echo "Writing File: $CINDER_CSI_DEPLOY"
-+    mkdir -p $(dirname ${{CINDER_CSI_DEPLOY}})
-+    cat << EOF > ${{CINDER_CSI_DEPLOY}}
-+{manifests.strip()}
-+EOF
-+
-+    echo "Waiting for Kubernetes API..."
-+    until  [ "ok" = "$(kubectl get --raw='/healthz')" ]
-+    do
-+        sleep 5
-+    done
-+
-+    cat <<EOF | kubectl apply -f -
-+---
-+apiVersion: v1
-+kind: Secret
-+metadata:
-+  name: cinder-csi-cloud-config
-+  namespace: kube-system
-+type: Opaque
-+stringData:
-+  cloud-config: |-
-+    [Global]
-+    auth-url=$AUTH_URL
-+    user-id=$TRUSTEE_USER_ID
-+    password=$TRUSTEE_PASSWORD
-+    trust-id=$TRUST_ID
-+    region=$REGION_NAME
-+    ca-file=/etc/kubernetes/ca-bundle.crt
-+EOF
-+
-+    kubectl apply -f ${{CINDER_CSI_DEPLOY}}
-+fi
-+printf "Finished running ${{step}}\\n"
-+"""
-+
-+with open("magnum/drivers/common/templates/kubernetes/fragments/enable-cinder-csi.sh", "w") as fd:
-+    fd.write(template)
diff --git a/images/magnum/patches/0005-secure-rbac.patch b/images/magnum/patches/0005-secure-rbac.patch
deleted file mode 100644
index 4f4ea6e..0000000
--- a/images/magnum/patches/0005-secure-rbac.patch
+++ /dev/null
@@ -1,1969 +0,0 @@
-From 7ffb23c87d04ea2c7f5b07a0af98573cb69379e0 Mon Sep 17 00:00:00 2001
-From: Rico Lin <ricolin@ricolky.com>
-Date: Tue, 11 Jul 2023 05:40:01 -0700
-Subject: [PATCH] Secure Rbac (#10)
-
-* Support enables rbac policies new defaults
-
-The Magnum service allow enables policies (RBAC) new defaults and scope by
-default. The Default value of config options ``[oslo_policy] enforce_scope``
-and ``[oslo_policy] oslo_policy.enforce_new_defaults`` are both to
-``False``, but will change to ``True`` in following cycles.
-
-To enable them then modify the below config options value in
-``magnum.conf`` file::
-
-  [oslo_policy]
-  enforce_new_defaults=True
-  enforce_scope=True
-
-reference tc goal for more detail:
-https://governance.openstack.org/tc/goals/selected/consistent-and-secure-rbac.html
-
-Related blueprint secure-rbac
-
-Change-Id: I249942a355577c4f1ef51b3988f0cc4979959d0b
-
-* Allow Admin to perform all API requests
-
-This propose changes is base on same concerns as this bug in neutron
-https://bugs.launchpad.net/neutron/+bug/1997089
-
-This propose to keep and make sure ADMIN can perform all API requests.
-
-Change-Id: I9a3003963bf13a591cc363fa04ec8e5719ae9114
-
-* Add policies unit tests (Part one)
-
-Add plicies unit test base function
-and tests for federation, quotas and stats.
-
-Change-Id: I0eb12bf77e0e786652e674c787b2821415bd4506
-
-* Add policies unit tests (Part two)
-
-Add plicies unit test base function
-and tests for certificate, and magnum service.
-
-Change-Id: Ib4047cb5a84647ff2848f06de71181673cc0627a
-
-* Add policies unit tests (Part three)
-
-Add plicies unit test base function
-and tests for cluster, cluster template, and nodegroup.
-
-Change-Id: I0555e557725b02f3ec9812f0adf84d283f7389b0
----
- magnum/api/hooks.py                           |   8 +-
- magnum/common/context.py                      |  12 +-
- magnum/common/policies/base.py                | 169 +++++++++++++++++-
- magnum/common/policies/certificate.py         |  11 +-
- magnum/common/policies/cluster.py             |  27 ++-
- magnum/common/policies/cluster_template.py    |  20 ++-
- magnum/common/policies/federation.py          |  18 +-
- magnum/common/policies/nodegroup.py           |  15 +-
- magnum/common/policies/quota.py               |   3 +-
- magnum/common/policies/stats.py               |   3 +-
- magnum/common/policy.py                       |  12 +-
- magnum/tests/fakes.py                         |   2 +-
- magnum/tests/unit/api/base.py                 |  16 ++
- .../tests/unit/api/controllers/test_root.py   |   4 +-
- .../api/controllers/v1/test_certificate.py    |  23 ++-
- .../unit/api/controllers/v1/test_cluster.py   |  34 ++--
- .../controllers/v1/test_cluster_actions.py    |  48 +++--
- .../unit/api/controllers/v1/test_nodegroup.py |  12 +-
- .../unit/api/controllers/v1/test_quota.py     |   2 +-
- .../unit/api/controllers/v1/test_stats.py     |  15 +-
- magnum/tests/unit/api/test_hooks.py           |  10 +-
- magnum/tests/unit/common/policies/__init__.py |   0
- magnum/tests/unit/common/policies/base.py     |  37 ++++
- .../policies/test_certificate_policy.py       |  72 ++++++++
- .../common/policies/test_cluster_policy.py    |  65 +++++++
- .../policies/test_cluster_template_policy.py  |  74 ++++++++
- .../common/policies/test_federation_policy.py |  67 +++++++
- .../policies/test_magnum_service_policy.py    |  26 +++
- .../common/policies/test_nodegroup_policy.py  |  74 ++++++++
- .../unit/common/policies/test_quota_policy.py |  74 ++++++++
- .../unit/common/policies/test_stats_policy.py |  33 ++++
- magnum/tests/unit/common/test_context.py      |  43 ++---
- ...dmin_perform_acitons-cc988655bb72b3f3.yaml |   9 +
- ...ope-and-new-defaults-7e6e503f74283071.yaml |  13 ++
- 36 files changed, 943 insertions(+), 124 deletions(-)
- create mode 100644 magnum/tests/unit/common/policies/__init__.py
- create mode 100644 magnum/tests/unit/common/policies/base.py
- create mode 100644 magnum/tests/unit/common/policies/test_certificate_policy.py
- create mode 100644 magnum/tests/unit/common/policies/test_cluster_policy.py
- create mode 100644 magnum/tests/unit/common/policies/test_cluster_template_policy.py
- create mode 100644 magnum/tests/unit/common/policies/test_federation_policy.py
- create mode 100644 magnum/tests/unit/common/policies/test_magnum_service_policy.py
- create mode 100644 magnum/tests/unit/common/policies/test_nodegroup_policy.py
- create mode 100644 magnum/tests/unit/common/policies/test_quota_policy.py
- create mode 100644 magnum/tests/unit/common/policies/test_stats_policy.py
- create mode 100644 releasenotes/notes/allow_admin_perform_acitons-cc988655bb72b3f3.yaml
- create mode 100644 releasenotes/notes/enable-enforce-scope-and-new-defaults-7e6e503f74283071.yaml
-
-diff --git a/magnum/api/hooks.py b/magnum/api/hooks.py
-index e0d36a9a88..f5a9049795 100644
---- a/magnum/api/hooks.py
-+++ b/magnum/api/hooks.py
-@@ -52,8 +52,8 @@ def before(self, state):
-         user_id = headers.get('X-User-Id')
-         project = headers.get('X-Project-Name')
-         project_id = headers.get('X-Project-Id')
--        domain_id = headers.get('X-User-Domain-Id')
--        domain_name = headers.get('X-User-Domain-Name')
-+        user_domain_id = headers.get('X-User-Domain-Id')
-+        user_domain_name = headers.get('X-User-Domain-Name')
-         auth_token = headers.get('X-Auth-Token')
-         roles = headers.get('X-Roles', '').split(',')
-         auth_token_info = state.request.environ.get('keystone.token_info')
-@@ -72,8 +72,8 @@ def before(self, state):
-             user_id=user_id,
-             project_name=project,
-             project_id=project_id,
--            domain_id=domain_id,
--            domain_name=domain_name,
-+            user_domain_id=user_domain_id,
-+            user_domain_name=user_domain_name,
-             roles=roles)
- 
- 
-diff --git a/magnum/common/context.py b/magnum/common/context.py
-index 547c9cc9b4..c2c3be1e23 100644
---- a/magnum/common/context.py
-+++ b/magnum/common/context.py
-@@ -42,7 +42,7 @@ def __init__(self, auth_token=None, auth_url=None, domain_id=None,
-         """
-         super(RequestContext, self).__init__(auth_token=auth_token,
-                                              user_id=user_name,
--                                             project_id=project_name,
-+                                             project_id=project_id,
-                                              is_admin=is_admin,
-                                              read_only=read_only,
-                                              show_deleted=show_deleted,
-@@ -53,8 +53,12 @@ def __init__(self, auth_token=None, auth_url=None, domain_id=None,
-         self.user_id = user_id
-         self.project_name = project_name
-         self.project_id = project_id
--        self.domain_id = domain_id
--        self.domain_name = domain_name
-+        # (ricolin) Rmove domain_id because oslo_policy use this args to
-+        # judge if this request is a domain scope or not. We might be consider
-+        # bring this back only if that judge in oslo_policy is no longer affect
-+        # project scope enforce.
-+        # self.domain_id = domain_id
-+        # self.domain_name = domain_name
-         self.user_domain_id = user_domain_id
-         self.user_domain_name = user_domain_name
-         self.auth_url = auth_url
-@@ -71,8 +75,6 @@ def to_dict(self):
-         value = super(RequestContext, self).to_dict()
-         value.update({'auth_token': self.auth_token,
-                       'auth_url': self.auth_url,
--                      'domain_id': self.domain_id,
--                      'domain_name': self.domain_name,
-                       'user_domain_id': self.user_domain_id,
-                       'user_domain_name': self.user_domain_name,
-                       'user_name': self.user_name,
-diff --git a/magnum/common/policies/base.py b/magnum/common/policies/base.py
-index 44c75b7daf..05ac11728b 100644
---- a/magnum/common/policies/base.py
-+++ b/magnum/common/policies/base.py
-@@ -13,12 +13,79 @@
- #    under the License.
- from oslo_policy import policy
- 
--ROLE_ADMIN = 'rule:context_is_admin'
-+
- RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
--RULE_ADMIN_API = 'rule:admin_api'
-+RULE_ADMIN_API = 'rule:context_is_admin'
- RULE_ADMIN_OR_USER = 'rule:admin_or_user'
- RULE_CLUSTER_USER = 'rule:cluster_user'
- RULE_DENY_CLUSTER_USER = 'rule:deny_cluster_user'
-+RULE_USER = "rule:is_user"
-+# Generic check string for checking if a user is authorized on a particular
-+# project, specifically with the member role.
-+RULE_PROJECT_MEMBER = 'rule:project_member'
-+# Generic check string for checking if a user is authorized on a particular
-+# project but with read-only access. For example, this persona would be able to
-+# list private images owned by a project but cannot make any writeable changes
-+# to those images.
-+RULE_PROJECT_READER = 'rule:project_reader'
-+
-+RULE_USER_OR_CLUSTER_USER = (
-+    'rule:user_or_cluster_user')
-+RULE_ADMIN_OR_PROJECT_READER = (
-+    'rule:admin_or_project_reader')
-+RULE_ADMIN_OR_PROJECT_MEMBER = (
-+    'rule:admin_or_project_member')
-+RULE_ADMIN_OR_PROJECT_MEMBER_USER = (
-+    'rule:admin_or_project_member_user')
-+RULE_ADMIN_OR_PROJECT_MEMBER_USER_OR_CLUSTER_USER = (
-+    'rule:admin_or_project_member_user_or_cluster_user')
-+RULE_PROJECT_MEMBER_DENY_CLUSTER_USER = (
-+    'rule:project_member_deny_cluster_user')
-+RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER = (
-+    'rule:admin_or_project_member_deny_cluster_user')
-+RULE_PROJECT_READER_DENY_CLUSTER_USER = (
-+    'rule:project_reader_deny_cluster_user')
-+RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER = (
-+    'rule:admin_or_project_reader_deny_cluster_user')
-+RULE_ADMIN_OR_PROJECT_READER_USER_OR_CLUSTER_USER = (
-+    'rule:admin_or_project_reader_user_or_cluster_user')
-+
-+# ==========================================================
-+# Deprecated Since OpenStack 2023.2(Magnum 17.0.0) and should be removed in
-+# The following cycle.
-+
-+DEPRECATED_REASON = """
-+The Magnum API now enforces scoped tokens and default reader and member roles.
-+"""
-+
-+DEPRECATED_SINCE = 'OpenStack 2023.2(Magnum 17.0.0)'
-+
-+
-+DEPRECATED_DENY_CLUSTER_USER = policy.DeprecatedRule(
-+    name=RULE_DENY_CLUSTER_USER,
-+    check_str='not domain_id:%(trustee_domain_id)s',
-+    deprecated_reason=DEPRECATED_REASON,
-+    deprecated_since=DEPRECATED_SINCE
-+)
-+
-+DEPRECATED_RULE_ADMIN_OR_OWNER = policy.DeprecatedRule(
-+    name=RULE_ADMIN_OR_OWNER,
-+    check_str='is_admin:True or project_id:%(project_id)s',
-+    deprecated_reason=DEPRECATED_REASON,
-+    deprecated_since=DEPRECATED_SINCE
-+)
-+
-+# Only used for DEPRECATED_RULE_ADMIN_OR_USER_OR_CLUSTER_USER
-+RULE_ADMIN_OR_USER_OR_CLUSTER_USER = (
-+    'rule:admin_or_user_or_cluster_user')
-+
-+DEPRECATED_RULE_ADMIN_OR_USER_OR_CLUSTER_USER = policy.DeprecatedRule(
-+    name=RULE_ADMIN_OR_USER_OR_CLUSTER_USER,
-+    check_str=f"(({RULE_ADMIN_API}) or ({RULE_USER_OR_CLUSTER_USER}))",
-+    deprecated_reason=DEPRECATED_REASON,
-+    deprecated_since=DEPRECATED_SINCE
-+)
-+# ==========================================================
- 
- rules = [
-     policy.RuleDefault(
-@@ -29,14 +96,14 @@
-         name='admin_or_owner',
-         check_str='is_admin:True or project_id:%(project_id)s'
-     ),
--    policy.RuleDefault(
--        name='admin_api',
--        check_str='rule:context_is_admin'
--    ),
-     policy.RuleDefault(
-         name='admin_or_user',
-         check_str='is_admin:True or user_id:%(user_id)s'
-     ),
-+    policy.RuleDefault(
-+        name='is_user',
-+        check_str='user_id:%(user_id)s'
-+    ),
-     policy.RuleDefault(
-         name='cluster_user',
-         check_str='user_id:%(trustee_user_id)s'
-@@ -44,7 +111,95 @@
-     policy.RuleDefault(
-         name='deny_cluster_user',
-         check_str='not domain_id:%(trustee_domain_id)s'
--    )
-+    ),
-+    policy.RuleDefault(
-+        name='project_member',
-+        check_str='role:member and project_id:%(project_id)s'
-+    ),
-+    policy.RuleDefault(
-+        name='project_reader',
-+        check_str='role:reader and project_id:%(project_id)s'
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_reader',
-+        check_str=f"({RULE_ADMIN_API}) or ({RULE_PROJECT_READER})",
-+        deprecated_rule=DEPRECATED_RULE_ADMIN_OR_OWNER
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_member',
-+        check_str=f"({RULE_ADMIN_API}) or ({RULE_PROJECT_MEMBER})",
-+        deprecated_rule=DEPRECATED_RULE_ADMIN_OR_OWNER
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_member_user',
-+        check_str=(
-+            f"({RULE_ADMIN_API}) or (({RULE_PROJECT_MEMBER}) and "
-+            f"({RULE_USER}))"
-+        )
-+    ),
-+    policy.RuleDefault(
-+        name='user_or_cluster_user',
-+        check_str=(
-+            f"(({RULE_USER}) or ({RULE_CLUSTER_USER}))"
-+        )
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_user_or_cluster_user',
-+        check_str=(
-+            f"(({RULE_ADMIN_API}) or ({RULE_USER_OR_CLUSTER_USER}))"
-+        )
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_member_cluster_user',
-+        check_str=(
-+            f"({RULE_ADMIN_API}) or (({RULE_PROJECT_MEMBER}) "
-+            f"and ({RULE_CLUSTER_USER}))"
-+        )
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_member_user_or_cluster_user',
-+        check_str=(
-+            f"({RULE_ADMIN_API}) or (({RULE_PROJECT_MEMBER}) and "
-+            f"({RULE_USER_OR_CLUSTER_USER}))"
-+        ),
-+        deprecated_rule=DEPRECATED_RULE_ADMIN_OR_USER_OR_CLUSTER_USER
-+    ),
-+    policy.RuleDefault(
-+        name='project_member_deny_cluster_user',
-+        check_str=(
-+            f"(({RULE_PROJECT_MEMBER}) and ({RULE_DENY_CLUSTER_USER}))"
-+        ),
-+        deprecated_rule=DEPRECATED_DENY_CLUSTER_USER
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_member_deny_cluster_user',
-+        check_str=(
-+            f"({RULE_ADMIN_API}) or ({RULE_PROJECT_MEMBER_DENY_CLUSTER_USER})"
-+        ),
-+        deprecated_rule=DEPRECATED_DENY_CLUSTER_USER
-+    ),
-+    policy.RuleDefault(
-+        name='project_reader_deny_cluster_user',
-+        check_str=(
-+            f"(({RULE_PROJECT_READER}) and ({RULE_DENY_CLUSTER_USER}))"
-+        ),
-+        deprecated_rule=DEPRECATED_DENY_CLUSTER_USER
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_reader_deny_cluster_user',
-+        check_str=(
-+            f"({RULE_ADMIN_API}) or ({RULE_PROJECT_READER_DENY_CLUSTER_USER})"
-+        ),
-+        deprecated_rule=DEPRECATED_DENY_CLUSTER_USER
-+    ),
-+    policy.RuleDefault(
-+        name='admin_or_project_reader_user_or_cluster_user',
-+        check_str=(
-+            f"({RULE_ADMIN_API}) or (({RULE_PROJECT_READER}) and "
-+            f"({RULE_USER_OR_CLUSTER_USER}))"
-+        ),
-+        deprecated_rule=DEPRECATED_RULE_ADMIN_OR_USER_OR_CLUSTER_USER
-+    ),
- ]
- 
- 
-diff --git a/magnum/common/policies/certificate.py b/magnum/common/policies/certificate.py
-index 5e96b64f5b..32a7047a4b 100644
---- a/magnum/common/policies/certificate.py
-+++ b/magnum/common/policies/certificate.py
-@@ -16,13 +16,12 @@
- from magnum.common.policies import base
- 
- CERTIFICATE = 'certificate:%s'
--RULE_ADMIN_OR_USER_OR_CLUSTER_USER = base.RULE_ADMIN_OR_USER + " or " + \
--    base.RULE_CLUSTER_USER
- 
- rules = [
-     policy.DocumentedRuleDefault(
-         name=CERTIFICATE % 'create',
--        check_str=RULE_ADMIN_OR_USER_OR_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_USER_OR_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Sign a new certificate by the CA.',
-         operations=[
-             {
-@@ -33,7 +32,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CERTIFICATE % 'get',
--        check_str=RULE_ADMIN_OR_USER_OR_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_USER_OR_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve CA information about the given bay/cluster.',
-         operations=[
-             {
-@@ -44,7 +44,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CERTIFICATE % 'rotate_ca',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER,
-+        scope_types=["project"],
-         description='Rotate the CA certificate on the given bay/cluster.',
-         operations=[
-             {
-diff --git a/magnum/common/policies/cluster.py b/magnum/common/policies/cluster.py
-index 15b63226b2..5e1864c377 100644
---- a/magnum/common/policies/cluster.py
-+++ b/magnum/common/policies/cluster.py
-@@ -20,7 +20,8 @@
- rules = [
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'create',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Create a new cluster.',
-         operations=[
-             {
-@@ -31,7 +32,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'delete',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Delete a cluster.',
-         operations=[
-             {
-@@ -53,7 +55,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'detail',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve a list of clusters with detail.',
-         operations=[
-             {
-@@ -75,7 +78,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'get',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve information about the given cluster.',
-         operations=[
-             {
-@@ -98,7 +102,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'get_all',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve a list of clusters.',
-         operations=[
-             {
-@@ -120,7 +125,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'update',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Update an existing cluster.',
-         operations=[
-             {
-@@ -131,7 +137,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'update_health_status',
--        check_str=base.RULE_ADMIN_OR_USER + " or " + base.RULE_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_USER_OR_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Update the health status of an existing cluster.',
-         operations=[
-             {
-@@ -153,7 +160,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'resize',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Resize an existing cluster.',
-         operations=[
-             {
-@@ -164,7 +172,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER % 'upgrade',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Upgrade an existing cluster.',
-         operations=[
-             {
-diff --git a/magnum/common/policies/cluster_template.py b/magnum/common/policies/cluster_template.py
-index d9b51737ad..c0d8337051 100644
---- a/magnum/common/policies/cluster_template.py
-+++ b/magnum/common/policies/cluster_template.py
-@@ -20,18 +20,20 @@
- rules = [
-     policy.DocumentedRuleDefault(
-         name=CLUSTER_TEMPLATE % 'create',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Create a new cluster template.',
-         operations=[
-             {
-                 'path': '/v1/clustertemplates',
-                 'method': 'POST'
-             }
--        ]
-+        ],
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER_TEMPLATE % 'delete',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER,
-+        scope_types=["project"],
-         description='Delete a cluster template.',
-         operations=[
-             {
-@@ -65,7 +67,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER_TEMPLATE % 'detail',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve a list of cluster templates with detail.',
-         operations=[
-             {
-@@ -76,7 +79,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER_TEMPLATE % 'get',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve information about the given cluster template.',
-         operations=[
-             {
-@@ -99,7 +103,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER_TEMPLATE % 'get_all',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve a list of cluster templates.',
-         operations=[
-             {
-@@ -121,7 +126,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=CLUSTER_TEMPLATE % 'update',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER,
-+        scope_types=["project"],
-         description='Update an existing cluster template.',
-         operations=[
-             {
-diff --git a/magnum/common/policies/federation.py b/magnum/common/policies/federation.py
-index b78b1a1b1e..4c347993c3 100644
---- a/magnum/common/policies/federation.py
-+++ b/magnum/common/policies/federation.py
-@@ -20,7 +20,8 @@
- rules = [
-     policy.DocumentedRuleDefault(
-         name=FEDERATION % 'create',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Create a new federation.',
-         operations=[
-             {
-@@ -31,7 +32,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=FEDERATION % 'delete',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Delete a federation.',
-         operations=[
-             {
-@@ -42,7 +44,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=FEDERATION % 'detail',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve a list of federations with detail.',
-         operations=[
-             {
-@@ -53,7 +56,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=FEDERATION % 'get',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve information about the given federation.',
-         operations=[
-             {
-@@ -64,7 +68,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=FEDERATION % 'get_all',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Retrieve a list of federations.',
-         operations=[
-             {
-@@ -75,7 +80,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=FEDERATION % 'update',
--        check_str=base.RULE_DENY_CLUSTER_USER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER_DENY_CLUSTER_USER,
-+        scope_types=["project"],
-         description='Update an existing federation.',
-         operations=[
-             {
-diff --git a/magnum/common/policies/nodegroup.py b/magnum/common/policies/nodegroup.py
-index 64b2d670ea..25bad88579 100644
---- a/magnum/common/policies/nodegroup.py
-+++ b/magnum/common/policies/nodegroup.py
-@@ -24,7 +24,8 @@
- rules = [
-     policy.DocumentedRuleDefault(
-         name=NODEGROUP % 'get',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER,
-+        scope_types=["project"],
-         description='Retrieve information about the given nodegroup.',
-         operations=[
-             {
-@@ -35,7 +36,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=NODEGROUP % 'get_all',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER,
-+        scope_types=["project"],
-         description='Retrieve a list of nodegroups that belong to a cluster.',
-         operations=[
-             {
-@@ -68,7 +70,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=NODEGROUP % 'create',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER,
-+        scope_types=["project"],
-         description='Create a new nodegroup.',
-         operations=[
-             {
-@@ -79,7 +82,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=NODEGROUP % 'delete',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER,
-+        scope_types=["project"],
-         description='Delete a nodegroup.',
-         operations=[
-             {
-@@ -90,7 +94,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=NODEGROUP % 'update',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_MEMBER,
-+        scope_types=["project"],
-         description='Update an existing nodegroup.',
-         operations=[
-             {
-diff --git a/magnum/common/policies/quota.py b/magnum/common/policies/quota.py
-index 4baecf7d84..574857b1a4 100644
---- a/magnum/common/policies/quota.py
-+++ b/magnum/common/policies/quota.py
-@@ -42,7 +42,8 @@
-     ),
-     policy.DocumentedRuleDefault(
-         name=QUOTA % 'get',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER,
-+        scope_types=["project"],
-         description='Retrieve Quota information for the given project_id.',
-         operations=[
-             {
-diff --git a/magnum/common/policies/stats.py b/magnum/common/policies/stats.py
-index c37164094b..64996443b7 100644
---- a/magnum/common/policies/stats.py
-+++ b/magnum/common/policies/stats.py
-@@ -20,7 +20,8 @@
- rules = [
-     policy.DocumentedRuleDefault(
-         name=STATS % 'get_all',
--        check_str=base.RULE_ADMIN_OR_OWNER,
-+        check_str=base.RULE_ADMIN_OR_PROJECT_READER,
-+        scope_types=["project"],
-         description='Retrieve magnum stats.',
-         operations=[
-             {
-diff --git a/magnum/common/policy.py b/magnum/common/policy.py
-index d4bfff77b5..989676efb1 100644
---- a/magnum/common/policy.py
-+++ b/magnum/common/policy.py
-@@ -17,6 +17,7 @@
- 
- import decorator
- from oslo_config import cfg
-+from oslo_log import log as logging
- from oslo_policy import opts
- from oslo_policy import policy
- from oslo_utils import importutils
-@@ -27,6 +28,7 @@
- from magnum.common import policies
- 
- 
-+LOG = logging.getLogger(__name__)
- _ENFORCER = None
- CONF = cfg.CONF
- 
-@@ -105,8 +107,14 @@ def enforce(context, rule=None, target=None,
-         target = {'project_id': context.project_id,
-                   'user_id': context.user_id}
-     add_policy_attributes(target)
--    return enforcer.enforce(rule, target, credentials,
--                            do_raise=do_raise, exc=exc, *args, **kwargs)
-+
-+    try:
-+        result = enforcer.enforce(rule, target, credentials,
-+                                  do_raise=do_raise, exc=exc, *args, **kwargs)
-+    except policy.InvalidScope as ex:
-+        LOG.debug(f"Invalide scope while enforce policy :{str(ex)}")
-+        raise exc(action=rule)
-+    return result
- 
- 
- def add_policy_attributes(target):
-diff --git a/magnum/tests/fakes.py b/magnum/tests/fakes.py
-index 4407975306..3a64078ce8 100644
---- a/magnum/tests/fakes.py
-+++ b/magnum/tests/fakes.py
-@@ -25,7 +25,7 @@
-                         'X-Roles': 'role1,role2',
-                         'X-Auth-Url': 'fake_auth_url',
-                         'X-Identity-Status': 'Confirmed',
--                        'X-User-Domain-Name': 'domain',
-+                        'X-User-Domain-Name': 'user_domain_name',
-                         'X-Project-Domain-Id': 'project_domain_id',
-                         'X-User-Domain-Id': 'user_domain_id',
-                         'OpenStack-API-Version': 'container-infra 1.0'
-diff --git a/magnum/tests/unit/api/base.py b/magnum/tests/unit/api/base.py
-index a4dd3fef63..ddf41277e4 100644
---- a/magnum/tests/unit/api/base.py
-+++ b/magnum/tests/unit/api/base.py
-@@ -128,6 +128,9 @@ def put_json(self, path, params, expect_errors=False, headers=None,
-                               with the request
-         :param status: expected status code of response
-         """
-+        # Provide member role for put request
-+        if not headers:
-+            headers = {"X-Roles": "member"}
-         return self._request_json(path=path, params=params,
-                                   expect_errors=expect_errors,
-                                   headers=headers, extra_environ=extra_environ,
-@@ -146,6 +149,9 @@ def post_json(self, path, params, expect_errors=False, headers=None,
-                               with the request
-         :param status: expected status code of response
-         """
-+        # Provide member role for post request
-+        if not headers:
-+            headers = {"X-Roles": "member"}
-         return self._request_json(path=path, params=params,
-                                   expect_errors=expect_errors,
-                                   headers=headers, extra_environ=extra_environ,
-@@ -164,6 +170,9 @@ def patch_json(self, path, params, expect_errors=False, headers=None,
-                               with the request
-         :param status: expected status code of response
-         """
-+        # Provide member role for patch request
-+        if not headers:
-+            headers = {"X-Roles": "member"}
-         return self._request_json(path=path, params=params,
-                                   expect_errors=expect_errors,
-                                   headers=headers, extra_environ=extra_environ,
-@@ -184,6 +193,9 @@ def delete(self, path, expect_errors=False, headers=None,
-         """
-         full_path = path_prefix + path
-         print('DELETE: %s' % (full_path))
-+        # Provide member role for delete request
-+        if not headers:
-+            headers = {"X-Roles": "member"}
-         response = self.app.delete(str(full_path),
-                                    headers=headers,
-                                    status=status,
-@@ -215,6 +227,10 @@ def get_json(self, path, expect_errors=False, headers=None,
-                         'q.value': [],
-                         'q.op': [],
-                         }
-+
-+        # Provide reader role for get request
-+        if not headers:
-+            headers = {"X-Roles": "reader"}
-         for query in q:
-             for name in ['field', 'op', 'value']:
-                 query_params['q.%s' % name].append(query.get(name, ''))
-diff --git a/magnum/tests/unit/api/controllers/test_root.py b/magnum/tests/unit/api/controllers/test_root.py
-index e187715016..31700761fd 100644
---- a/magnum/tests/unit/api/controllers/test_root.py
-+++ b/magnum/tests/unit/api/controllers/test_root.py
-@@ -140,7 +140,9 @@ def test_noauth(self):
-         response = app.get('/v1/')
-         self.assertEqual(self.v1_expected, response.json)
- 
--        response = app.get('/v1/clustertemplates')
-+        response = app.get('/v1/clustertemplates',
-+                           headers={"X-Roles": "reader"}
-+                           )
-         self.assertEqual(200, response.status_int)
- 
-     def test_auth_with_no_public_routes(self):
-diff --git a/magnum/tests/unit/api/controllers/v1/test_certificate.py b/magnum/tests/unit/api/controllers/v1/test_certificate.py
-index 02fcfb40a2..ecd14f0187 100644
---- a/magnum/tests/unit/api/controllers/v1/test_certificate.py
-+++ b/magnum/tests/unit/api/controllers/v1/test_certificate.py
-@@ -21,7 +21,14 @@
- from magnum.tests.unit.objects import utils as obj_utils
- 
- 
--HEADERS = {'OpenStack-API-Version': 'container-infra latest'}
-+READER_HEADERS = {
-+    'OpenStack-API-Version': 'container-infra latest',
-+    "X-Roles": "reader"
-+}
-+HEADERS = {
-+    'OpenStack-API-Version': 'container-infra latest',
-+    "X-Roles": "member"
-+}
- 
- 
- class TestCertObject(base.TestCase):
-@@ -59,7 +66,7 @@ def test_get_one(self):
-         self.conductor_api.get_ca_certificate.return_value = mock_cert
- 
-         response = self.get_json('/certificates/%s' % self.cluster.uuid,
--                                 headers=HEADERS)
-+                                 headers=READER_HEADERS)
- 
-         self.assertEqual(self.cluster.uuid, response['cluster_uuid'])
-         # check that bay is still valid as well
-@@ -74,7 +81,7 @@ def test_get_one_by_name(self):
-         self.conductor_api.get_ca_certificate.return_value = mock_cert
- 
-         response = self.get_json('/certificates/%s' % self.cluster.name,
--                                 headers=HEADERS)
-+                                 headers=READER_HEADERS)
- 
-         self.assertEqual(self.cluster.uuid, response['cluster_uuid'])
-         # check that bay is still valid as well
-@@ -84,7 +91,8 @@ def test_get_one_by_name(self):
- 
-     def test_get_one_by_name_not_found(self):
-         response = self.get_json('/certificates/not_found',
--                                 expect_errors=True, headers=HEADERS)
-+                                 expect_errors=True,
-+                                 headers=READER_HEADERS)
- 
-         self.assertEqual(404, response.status_int)
-         self.assertEqual('application/json', response.content_type)
-@@ -97,7 +105,8 @@ def test_get_one_by_name_multiple_cluster(self):
-                                       uuid=uuidutils.generate_uuid())
- 
-         response = self.get_json('/certificates/test_cluster',
--                                 expect_errors=True, headers=HEADERS)
-+                                 expect_errors=True,
-+                                 headers=READER_HEADERS)
- 
-         self.assertEqual(409, response.status_int)
-         self.assertEqual('application/json', response.content_type)
-@@ -110,7 +119,7 @@ def test_links(self):
-         self.conductor_api.get_ca_certificate.return_value = mock_cert
- 
-         response = self.get_json('/certificates/%s' % self.cluster.uuid,
--                                 headers=HEADERS)
-+                                 headers=READER_HEADERS)
- 
-         self.assertIn('links', response.keys())
-         self.assertEqual(2, len(response['links']))
-@@ -265,7 +274,7 @@ def test_policy_disallow_get_one(self):
-         self._common_policy_check(
-             "certificate:get", self.get_json,
-             '/certificates/%s' % cluster.uuid,
--            expect_errors=True, headers=HEADERS)
-+            expect_errors=True, headers=READER_HEADERS)
- 
-     def test_policy_disallow_create(self):
-         cluster = obj_utils.create_test_cluster(self.context)
-diff --git a/magnum/tests/unit/api/controllers/v1/test_cluster.py b/magnum/tests/unit/api/controllers/v1/test_cluster.py
-index 016f8cc173..9ff2439f36 100755
---- a/magnum/tests/unit/api/controllers/v1/test_cluster.py
-+++ b/magnum/tests/unit/api/controllers/v1/test_cluster.py
-@@ -494,7 +494,9 @@ def test_update_cluster_with_rollback_enabled(self):
-             '/clusters/%s/?rollback=True' % self.cluster_obj.uuid,
-             [{'path': '/node_count', 'value': node_count,
-               'op': 'replace'}],
--            headers={'OpenStack-API-Version': 'container-infra 1.3'})
-+            headers={'OpenStack-API-Version': 'container-infra 1.3',
-+                     "X-Roles": "member"
-+                     })
- 
-         self.mock_cluster_update.assert_called_once_with(
-             mock.ANY, node_count, self.cluster_obj.health_status,
-@@ -507,7 +509,9 @@ def test_update_cluster_with_rollback_disabled(self):
-             '/clusters/%s/?rollback=False' % self.cluster_obj.uuid,
-             [{'path': '/node_count', 'value': node_count,
-               'op': 'replace'}],
--            headers={'OpenStack-API-Version': 'container-infra 1.3'})
-+            headers={'OpenStack-API-Version': 'container-infra 1.3',
-+                     "X-Roles": "member"
-+                     })
- 
-         self.mock_cluster_update.assert_called_once_with(
-             mock.ANY, node_count, self.cluster_obj.health_status,
-@@ -520,7 +524,9 @@ def test_update_cluster_with_zero_node_count_fail(self):
-             '/clusters/%s' % self.cluster_obj.uuid,
-             [{'path': '/node_count', 'value': node_count,
-               'op': 'replace'}],
--            headers={'OpenStack-API-Version': 'container-infra 1.9'},
-+            headers={'OpenStack-API-Version': 'container-infra 1.9',
-+                     "X-Roles": "member"
-+                     },
-             expect_errors=True)
- 
-         self.assertEqual(400, response.status_code)
-@@ -531,7 +537,9 @@ def test_update_cluster_with_zero_node_count(self):
-             '/clusters/%s' % self.cluster_obj.uuid,
-             [{'path': '/node_count', 'value': node_count,
-               'op': 'replace'}],
--            headers={'OpenStack-API-Version': 'container-infra 1.10'})
-+            headers={'OpenStack-API-Version': 'container-infra 1.10',
-+                     "X-Roles": "member"
-+                     })
- 
-         self.mock_cluster_update.assert_called_once_with(
-             mock.ANY, node_count, self.cluster_obj.health_status,
-@@ -708,18 +716,24 @@ def test_create_cluster_with_cluster_template_name(self):
-     def test_create_cluster_with_zero_node_count_fail(self):
-         bdict = apiutils.cluster_post_data()
-         bdict['node_count'] = 0
--        response = self.post_json('/clusters', bdict, expect_errors=True,
--                                  headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"})
-+        response = self.post_json(
-+            '/clusters', bdict, expect_errors=True,
-+            headers={
-+                "Openstack-Api-Version": "container-infra 1.9",
-+                "X-Roles": "member"
-+            })
-         self.assertEqual('application/json', response.content_type)
-         self.assertEqual(400, response.status_int)
- 
-     def test_create_cluster_with_zero_node_count(self):
-         bdict = apiutils.cluster_post_data()
-         bdict['node_count'] = 0
--        response = self.post_json('/clusters', bdict,
--                                  headers={"Openstack-Api-Version":
--                                           "container-infra 1.10"})
-+        response = self.post_json(
-+            '/clusters', bdict,
-+            headers={
-+                "Openstack-Api-Version": "container-infra 1.10",
-+                "X-Roles": "member"
-+            })
-         self.assertEqual('application/json', response.content_type)
-         self.assertEqual(202, response.status_int)
- 
-diff --git a/magnum/tests/unit/api/controllers/v1/test_cluster_actions.py b/magnum/tests/unit/api/controllers/v1/test_cluster_actions.py
-index ba9304fe1b..22baf556ce 100644
---- a/magnum/tests/unit/api/controllers/v1/test_cluster_actions.py
-+++ b/magnum/tests/unit/api/controllers/v1/test_cluster_actions.py
-@@ -46,7 +46,8 @@ def test_resize(self):
-                                   self.cluster_obj.uuid,
-                                   {"node_count": new_node_count},
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.7"})
-+                                           "container-infra 1.7",
-+                                           "X-Roles": "member"})
-         self.assertEqual(202, response.status_code)
- 
-         response = self.get_json('/clusters/%s' % self.cluster_obj.uuid)
-@@ -69,7 +70,8 @@ def test_resize_with_nodegroup(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_resize_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"})
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"})
-         self.assertEqual(202, response.status_code)
- 
-         response = self.get_json('/clusters/%s' % self.cluster_obj.uuid)
-@@ -89,7 +91,8 @@ def test_resize_with_master_nodegroup(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_resize_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"},
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(400, response.status_code)
- 
-@@ -106,7 +109,8 @@ def test_resize_with_node_count_greater_than_max(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_resize_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"},
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(400, response.status_code)
- 
-@@ -123,7 +127,8 @@ def test_resize_with_node_count_less_than_min(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_resize_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"},
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(400, response.status_code)
- 
-@@ -140,7 +145,8 @@ def test_resize_with_zero_node_count_fail(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_resize_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"},
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(400, response.status_code)
- 
-@@ -157,7 +163,8 @@ def test_resize_with_zero_node_count(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_resize_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.10"})
-+                                           "container-infra 1.10",
-+                                           "X-Roles": "member"})
-         self.assertEqual(202, response.status_code)
- 
- 
-@@ -195,7 +202,8 @@ def test_upgrade(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.8"})
-+                                           "container-infra 1.8",
-+                                           "X-Roles": "member"})
-         self.assertEqual(202, response.status_code)
- 
-     def test_upgrade_cluster_as_admin(self):
-@@ -226,7 +234,8 @@ def test_upgrade_cluster_as_admin(self):
-             '/clusters/%s/actions/upgrade' %
-             cluster_uuid,
-             cluster_upgrade_req,
--            headers={"Openstack-Api-Version": "container-infra 1.8"})
-+            headers={"Openstack-Api-Version": "container-infra 1.8",
-+                     "X-Roles": "member"})
- 
-         self.assertEqual(202, response.status_int)
- 
-@@ -239,7 +248,8 @@ def test_upgrade_default_worker(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"})
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"})
-         self.assertEqual(202, response.status_code)
- 
-     def test_upgrade_default_master(self):
-@@ -251,7 +261,8 @@ def test_upgrade_default_master(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"})
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"})
-         self.assertEqual(202, response.status_code)
- 
-     def test_upgrade_non_default_ng(self):
-@@ -263,7 +274,8 @@ def test_upgrade_non_default_ng(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"})
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"})
-         self.assertEqual(202, response.status_code)
- 
-     def test_upgrade_cluster_not_found(self):
-@@ -273,7 +285,8 @@ def test_upgrade_cluster_not_found(self):
-         response = self.post_json('/clusters/not_there/actions/upgrade',
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.8"},
-+                                           "container-infra 1.8",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(404, response.status_code)
- 
-@@ -285,7 +298,8 @@ def test_upgrade_ct_not_found(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.8"},
-+                                           "container-infra 1.8",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(404, response.status_code)
- 
-@@ -298,7 +312,8 @@ def test_upgrade_ng_not_found(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"},
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(404, response.status_code)
- 
-@@ -311,6 +326,7 @@ def test_upgrade_non_default_ng_invalid_ct(self):
-                                   self.cluster_obj.uuid,
-                                   cluster_upgrade_req,
-                                   headers={"Openstack-Api-Version":
--                                           "container-infra 1.9"},
-+                                           "container-infra 1.9",
-+                                           "X-Roles": "member"},
-                                   expect_errors=True)
-         self.assertEqual(409, response.status_code)
-diff --git a/magnum/tests/unit/api/controllers/v1/test_nodegroup.py b/magnum/tests/unit/api/controllers/v1/test_nodegroup.py
-index a6f73d54b2..68304a10f6 100644
---- a/magnum/tests/unit/api/controllers/v1/test_nodegroup.py
-+++ b/magnum/tests/unit/api/controllers/v1/test_nodegroup.py
-@@ -47,24 +47,26 @@ def test_nodegroup_init(self):
- class NodeGroupControllerTest(api_base.FunctionalTest):
-     headers = {"Openstack-Api-Version": "container-infra latest"}
- 
--    def _add_headers(self, kwargs):
-+    def _add_headers(self, kwargs, roles=None):
-         if 'headers' not in kwargs:
-             kwargs['headers'] = self.headers
-+            if roles:
-+                kwargs['headers']['X-Roles'] = ",".join(roles)
- 
-     def get_json(self, *args, **kwargs):
--        self._add_headers(kwargs)
-+        self._add_headers(kwargs, roles=['reader'])
-         return super(NodeGroupControllerTest, self).get_json(*args, **kwargs)
- 
-     def post_json(self, *args, **kwargs):
--        self._add_headers(kwargs)
-+        self._add_headers(kwargs, roles=['member'])
-         return super(NodeGroupControllerTest, self).post_json(*args, **kwargs)
- 
-     def delete(self, *args, **kwargs):
--        self._add_headers(kwargs)
-+        self._add_headers(kwargs, roles=['member'])
-         return super(NodeGroupControllerTest, self).delete(*args, **kwargs)
- 
-     def patch_json(self, *args, **kwargs):
--        self._add_headers(kwargs)
-+        self._add_headers(kwargs, roles=['member'])
-         return super(NodeGroupControllerTest, self).patch_json(*args, **kwargs)
- 
- 
-diff --git a/magnum/tests/unit/api/controllers/v1/test_quota.py b/magnum/tests/unit/api/controllers/v1/test_quota.py
-index b6b47c481a..07e78857ed 100644
---- a/magnum/tests/unit/api/controllers/v1/test_quota.py
-+++ b/magnum/tests/unit/api/controllers/v1/test_quota.py
-@@ -207,7 +207,7 @@ def test_get_all_non_admin(self, mock_policy):
-                                                 project_id="proj-id-"+str(i))
-             quota_list.append(quota)
- 
--        headers = {'X-Project-Id': 'proj-id-2'}
-+        headers = {'X-Project-Id': 'proj-id-2', "X-Roles": "member"}
-         response = self.get_json('/quotas', headers=headers)
-         self.assertEqual(1, len(response['quotas']))
-         self.assertEqual('proj-id-2', response['quotas'][0]['project_id'])
-diff --git a/magnum/tests/unit/api/controllers/v1/test_stats.py b/magnum/tests/unit/api/controllers/v1/test_stats.py
-index bb7aac28f4..2e41222d34 100644
---- a/magnum/tests/unit/api/controllers/v1/test_stats.py
-+++ b/magnum/tests/unit/api/controllers/v1/test_stats.py
-@@ -21,7 +21,14 @@
- class TestStatsController(api_base.FunctionalTest):
- 
-     def setUp(self):
--        self.base_headers = {'OpenStack-API-Version': 'container-infra 1.4'}
-+        self.base_headers = {
-+            "X-Roles": "reader",
-+            "OpenStack-API-Version": "container-infra 1.4"
-+        }
-+        self.base_admin_headers = {
-+            "X-Roles": "admin",
-+            "OpenStack-API-Version": "container-infra 1.4"
-+        }
-         super(TestStatsController, self).setUp()
-         obj_utils.create_test_cluster_template(self.context)
- 
-@@ -39,7 +46,7 @@ def test_admin_get_all_stats(self, mock_context, mock_policy):
-         obj_utils.create_test_cluster(self.context,
-                                       project_id=234,
-                                       uuid='uuid2')
--        response = self.get_json('/stats', headers=self.base_headers)
-+        response = self.get_json('/stats', headers=self.base_admin_headers)
-         expected = {u'clusters': 2, u'nodes': 12}
-         self.assertEqual(expected, response)
- 
-@@ -54,7 +61,7 @@ def test_admin_get_tenant_stats(self, mock_context, mock_policy):
-                                       uuid='uuid2')
-         self.context.is_admin = True
-         response = self.get_json('/stats?project_id=234',
--                                 headers=self.base_headers)
-+                                 headers=self.base_admin_headers)
-         expected = {u'clusters': 1, u'nodes': 6}
-         self.assertEqual(expected, response)
- 
-@@ -69,7 +76,7 @@ def test_admin_get_invalid_tenant_stats(self, mock_context, mock_policy):
-                                       uuid='uuid2')
-         self.context.is_admin = True
-         response = self.get_json('/stats?project_id=34',
--                                 headers=self.base_headers)
-+                                 headers=self.base_admin_headers)
-         expected = {u'clusters': 0, u'nodes': 0}
-         self.assertEqual(expected, response)
- 
-diff --git a/magnum/tests/unit/api/test_hooks.py b/magnum/tests/unit/api/test_hooks.py
-index 9332c93120..3cbfde4363 100644
---- a/magnum/tests/unit/api/test_hooks.py
-+++ b/magnum/tests/unit/api/test_hooks.py
-@@ -34,7 +34,8 @@ def setUp(self):
-         super(TestContextHook, self).setUp()
-         self.app = fakes.FakeApp()
- 
--    def test_context_hook_before_method(self):
-+    @mock.patch("magnum.common.policy.check_is_admin")
-+    def test_context_hook_before_method(self, m_c):
-         state = mock.Mock(request=fakes.FakePecanRequest())
-         hook = hooks.ContextHook()
-         hook.before(state)
-@@ -51,12 +52,13 @@ def test_context_hook_before_method(self):
-         self.assertEqual(fakes.fakeAuthTokenHeaders['X-Roles'],
-                          ','.join(ctx.roles))
-         self.assertEqual(fakes.fakeAuthTokenHeaders['X-User-Domain-Name'],
--                         ctx.domain_name)
-+                         ctx.user_domain_name)
-         self.assertEqual(fakes.fakeAuthTokenHeaders['X-User-Domain-Id'],
--                         ctx.domain_id)
-+                         ctx.user_domain_id)
-         self.assertIsNone(ctx.auth_token_info)
- 
--    def test_context_hook_before_method_auth_info(self):
-+    @mock.patch("magnum.common.policy.check_is_admin")
-+    def test_context_hook_before_method_auth_info(self, c_m):
-         state = mock.Mock(request=fakes.FakePecanRequest())
-         state.request.environ['keystone.token_info'] = 'assert_this'
-         hook = hooks.ContextHook()
-diff --git a/magnum/tests/unit/common/policies/__init__.py b/magnum/tests/unit/common/policies/__init__.py
-new file mode 100644
-index 0000000000..e69de29bb2
-diff --git a/magnum/tests/unit/common/policies/base.py b/magnum/tests/unit/common/policies/base.py
-new file mode 100644
-index 0000000000..22572c0a46
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/base.py
-@@ -0,0 +1,37 @@
-+#    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 oslo_config import cfg
-+
-+from magnum.tests.unit.api import base as api_base
-+
-+
-+CONF = cfg.CONF
-+
-+
-+class PolicyFunctionalTest(api_base.FunctionalTest):
-+    def setUp(self):
-+        super(PolicyFunctionalTest, self).setUp()
-+        CONF.set_override('enforce_scope', True, group='oslo_policy')
-+        CONF.set_override('enforce_new_defaults', True, group='oslo_policy')
-+        self.reader_headers = {
-+            "X-Roles": "reader",
-+        }
-+        self.member_headers = {
-+            "X-Roles": "member",
-+        }
-+        self.admin_headers = {
-+            "X-Roles": "admin",
-+        }
-+        self.foo_headers = {
-+            "X-Roles": "foo",
-+        }
-diff --git a/magnum/tests/unit/common/policies/test_certificate_policy.py b/magnum/tests/unit/common/policies/test_certificate_policy.py
-new file mode 100644
-index 0000000000..cc53a71645
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_certificate_policy.py
-@@ -0,0 +1,72 @@
-+#    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 unittest import mock
-+from webtest.app import AppError
-+
-+from magnum.tests.unit.api import utils as apiutils
-+from magnum.tests.unit.common.policies import base
-+from magnum.tests.unit.objects import utils as obj_utils
-+
-+READER_HEADERS = {
-+    'OpenStack-API-Version': 'container-infra latest',
-+    "X-Roles": "reader"
-+}
-+HEADERS = {
-+    'OpenStack-API-Version': 'container-infra latest',
-+    "X-Roles": "member"
-+}
-+
-+
-+class TestCertifiactePolicy(base.PolicyFunctionalTest):
-+    def setUp(self):
-+        super(TestCertifiactePolicy, self).setUp()
-+        self.cluster = obj_utils.create_test_cluster(self.context)
-+
-+        conductor_api_patcher = mock.patch('magnum.conductor.api.API')
-+        self.conductor_api_class = conductor_api_patcher.start()
-+        self.conductor_api = mock.MagicMock()
-+        self.conductor_api_class.return_value = self.conductor_api
-+        self.addCleanup(conductor_api_patcher.stop)
-+
-+        self.conductor_api.sign_certificate.side_effect = self._fake_sign
-+
-+    @staticmethod
-+    def _fake_sign(cluster, cert):
-+        cert.pem = 'fake-pem'
-+        return cert
-+
-+    def test_get_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError,
-+            self.get_json,
-+            f"/certificates/{self.cluster.uuid}",
-+            headers=HEADERS)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_create_no_permission(self):
-+        new_cert = apiutils.cert_post_data(cluster_uuid=self.cluster.uuid)
-+        del new_cert['pem']
-+
-+        exc = self.assertRaises(
-+            AppError, self.post_json,
-+            '/certificates', new_cert,
-+            headers=READER_HEADERS)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_update_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.patch_json,
-+            f"/certificates/{self.cluster.uuid}", {},
-+            headers=READER_HEADERS
-+        )
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/policies/test_cluster_policy.py b/magnum/tests/unit/common/policies/test_cluster_policy.py
-new file mode 100644
-index 0000000000..01cfd25c5c
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_cluster_policy.py
-@@ -0,0 +1,65 @@
-+#    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 webtest.app import AppError
-+
-+from magnum.tests.unit.api import utils as apiutils
-+from magnum.tests.unit.common.policies import base
-+from magnum.tests.unit.objects import utils as obj_utils
-+
-+
-+class TestClusterPolicy(base.PolicyFunctionalTest):
-+    def setUp(self):
-+        super(TestClusterPolicy, self).setUp()
-+        self.cluster = obj_utils.create_test_cluster(
-+            self.context, name='cluster_example_A', node_count=3
-+        )
-+
-+    def test_get_all_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.get_json, '/clusters',
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_get_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError,
-+            self.get_json,
-+            f"/clusters/{self.cluster.uuid}",
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_create_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.post_json,
-+            '/clusters', apiutils.cluster_post_data(),
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_update_no_permission(self):
-+        cluster_dict = [
-+            {'path': '/node_count', 'value': 4, 'op': 'replace'}
-+        ]
-+        exc = self.assertRaises(
-+            AppError, self.patch_json,
-+            f"/clusters/{self.cluster.name}", cluster_dict,
-+            headers=self.reader_headers
-+        )
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_delete_no_permission(self):
-+        # delete cluster
-+        exc = self.assertRaises(
-+            AppError, self.delete, f"/clusters/{self.cluster.uuid}",
-+            headers=self.reader_headers
-+        )
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/policies/test_cluster_template_policy.py b/magnum/tests/unit/common/policies/test_cluster_template_policy.py
-new file mode 100644
-index 0000000000..c6eb9b60a6
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_cluster_template_policy.py
-@@ -0,0 +1,74 @@
-+#    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 webtest.app import AppError
-+
-+from magnum.tests.unit.api import utils as apiutils
-+from magnum.tests.unit.common.policies import base
-+from magnum.tests.unit.objects import utils as obj_utils
-+
-+
-+class TestClusterTemplatePolicy(base.PolicyFunctionalTest):
-+    def setUp(self):
-+        super(TestClusterTemplatePolicy, self).setUp()
-+        self.clustertemplate = obj_utils.create_test_cluster_template(
-+            self.context
-+        )
-+
-+    def test_get_all_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.get_json, '/clustertemplates',
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_get_detail_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.get_json,
-+            '/clustertemplates/detail',
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_get_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError,
-+            self.get_json,
-+            f"/clustertemplates/{self.clustertemplate.uuid}",
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_create_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.post_json,
-+            '/clustertemplates',
-+            apiutils.cluster_template_post_data(),
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_update_no_permission(self):
-+        clustertemplate_data = [
-+            {'path': '/dns_nameserver', 'op': 'remove'}]
-+        exc = self.assertRaises(
-+            AppError,
-+            self.patch_json,
-+            f"/clustertemplates/{self.clustertemplate.uuid}",
-+            clustertemplate_data,
-+            headers=self.reader_headers
-+        )
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_delete_no_permission(self):
-+        # delete clustertemplate
-+        exc = self.assertRaises(
-+            AppError, self.delete,
-+            f"/clustertemplates/{self.clustertemplate.uuid}",
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/policies/test_federation_policy.py b/magnum/tests/unit/common/policies/test_federation_policy.py
-new file mode 100644
-index 0000000000..68eb1d6212
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_federation_policy.py
-@@ -0,0 +1,67 @@
-+#    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 oslo_utils import uuidutils
-+from webtest.app import AppError
-+
-+from magnum.tests.unit.common.policies import base
-+from magnum.tests.unit.objects import utils as obj_utils
-+
-+
-+class TestFederationPolicy(base.PolicyFunctionalTest):
-+    def setUp(self):
-+        super(TestFederationPolicy, self).setUp()
-+        self.create_frederation()
-+
-+    def create_frederation(self):
-+        self.fake_uuid = uuidutils.generate_uuid()
-+        self.federation = obj_utils.create_test_federation(
-+            self.context, uuid=self.fake_uuid)
-+
-+    def test_get_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.get_json, '/federations',
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_get_reader(self):
-+        response = self.get_json('/federations')
-+        self.assertEqual(self.fake_uuid, response['federations'][0]['uuid'])
-+
-+    def test_create_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.post_json, '/federations', {},
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_update_no_permission(self):
-+        new_member = obj_utils.create_test_cluster(self.context)
-+        exc = self.assertRaises(
-+            AppError, self.patch_json, '/federations/%s' % self.fake_uuid,
-+            [{'path': '/member_ids', 'value': new_member.uuid, 'op': 'add'}],
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_delete_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.delete,
-+            '/federations/%s' % self.fake_uuid,
-+            headers=self.reader_headers
-+        )
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_detail_list_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.get_json,
-+            '/federations/detail',
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/policies/test_magnum_service_policy.py b/magnum/tests/unit/common/policies/test_magnum_service_policy.py
-new file mode 100644
-index 0000000000..9f8153d3a4
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_magnum_service_policy.py
-@@ -0,0 +1,26 @@
-+#    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 webtest.app import AppError
-+
-+from magnum.tests.unit.common.policies import base
-+
-+
-+class TestMagnumServicePolicy(base.PolicyFunctionalTest):
-+    def setUp(self):
-+        super(TestMagnumServicePolicy, self).setUp()
-+
-+    def test_get_all_no_permission(self):
-+        exc = self.assertRaises(AppError,
-+                                self.get_json, "/mservices",
-+                                headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/policies/test_nodegroup_policy.py b/magnum/tests/unit/common/policies/test_nodegroup_policy.py
-new file mode 100644
-index 0000000000..73f3e107e4
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_nodegroup_policy.py
-@@ -0,0 +1,74 @@
-+#    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 oslo_utils import uuidutils
-+from webtest.app import AppError
-+
-+from magnum import objects
-+from magnum.tests.unit.api import utils as apiutils
-+from magnum.tests.unit.common.policies import base
-+from magnum.tests.unit.objects import utils as obj_utils
-+
-+
-+class TestNodeGroupPolicy(base.PolicyFunctionalTest):
-+    def setUp(self):
-+        super(TestNodeGroupPolicy, self).setUp()
-+        obj_utils.create_test_cluster_template(self.context)
-+        self.cluster_uuid = uuidutils.generate_uuid()
-+        obj_utils.create_test_cluster(
-+            self.context, uuid=self.cluster_uuid)
-+        self.cluster = objects.Cluster.get_by_uuid(self.context,
-+                                                   self.cluster_uuid)
-+        self.nodegroup = obj_utils.create_test_nodegroup(
-+            self.context, cluster_id=self.cluster.uuid, is_default=False)
-+        self.url = f"/clusters/{self.cluster.uuid}/nodegroups/"
-+        self.member = {"Openstack-Api-Version": "container-infra latest"}
-+        self.member.update(self.member_headers)
-+        self.reader = {"Openstack-Api-Version": "container-infra latest"}
-+        self.reader.update(self.reader_headers)
-+
-+    def test_get_all_no_permission(self):
-+        exc = self.assertRaises(AppError,
-+                                self.get_json, self.url,
-+                                headers=self.member)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_get_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError,
-+            self.get_json,
-+            f"{self.url}foo",
-+            headers=self.member)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_create_no_permission(self):
-+        exc = self.assertRaises(AppError,
-+                                self.post_json, self.url,
-+                                apiutils.nodegroup_post_data(),
-+                                headers=self.reader)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_update_no_permission(self):
-+        ng_dict = [
-+            {'path': '/max_node_count', 'value': 4, 'op': 'replace'}]
-+        exc = self.assertRaises(
-+            AppError, self.patch_json,
-+            self.url + self.nodegroup.uuid, ng_dict,
-+            headers=self.reader)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_delete_no_permission(self):
-+        # delete cluster
-+        exc = self.assertRaises(
-+                  AppError, self.delete, self.url + self.nodegroup.uuid,
-+                  headers=self.reader)
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/policies/test_quota_policy.py b/magnum/tests/unit/common/policies/test_quota_policy.py
-new file mode 100644
-index 0000000000..48d4a09c2c
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_quota_policy.py
-@@ -0,0 +1,74 @@
-+#    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 unittest import mock
-+from webtest.app import AppError
-+
-+from magnum.common import clients
-+from magnum.tests.unit.api import utils as apiutils
-+from magnum.tests.unit.common.policies import base
-+from magnum.tests.unit.objects import utils as obj_utils
-+
-+
-+class TestQuotaPolicy(base.PolicyFunctionalTest):
-+    def setUp(self):
-+        super(TestQuotaPolicy, self).setUp()
-+
-+    def test_get_all_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.get_json, '/quotas',
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    def test_get_no_permission(self):
-+        quota = obj_utils.create_test_quota(self.context)
-+        exc = self.assertRaises(
-+            AppError,
-+            self.get_json,
-+            f"/quotas/{quota['project_id']}/{quota['resource']}",
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    @mock.patch.object(clients.OpenStackClients, 'keystone')
-+    def test_create_no_permission(self, mock_keystone):
-+        exc = self.assertRaises(
-+            AppError, self.post_json,
-+            '/quotas', apiutils.quota_post_data(),
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    @mock.patch.object(clients.OpenStackClients, 'keystone')
-+    def test_update_no_permission(self, mock_keystone):
-+        with mock.patch("magnum.common.policy.enforce"):
-+            quota_dict = apiutils.quota_post_data(hard_limit=5)
-+            self.post_json('/quotas', quota_dict)
-+        quota_dict['hard_limit'] = 20
-+        exc = self.assertRaises(
-+            AppError, self.patch_json, '/quotas', quota_dict,
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-+
-+    @mock.patch.object(clients.OpenStackClients, 'keystone')
-+    def test_delete_no_permission(self, mock_keystone):
-+        with mock.patch("magnum.common.policy.enforce"):
-+            quota_dict = apiutils.quota_post_data()
-+            response = self.post_json('/quotas', quota_dict)
-+        self.assertEqual('application/json', response.content_type)
-+        self.assertEqual(201, response.status_int)
-+
-+        project_id = quota_dict['project_id']
-+        resource = quota_dict['resource']
-+        # delete quota
-+        exc = self.assertRaises(
-+            AppError, self.delete, f"/quotas/{project_id}/{resource}",
-+            headers=self.reader_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/policies/test_stats_policy.py b/magnum/tests/unit/common/policies/test_stats_policy.py
-new file mode 100644
-index 0000000000..20cf1bee5c
---- /dev/null
-+++ b/magnum/tests/unit/common/policies/test_stats_policy.py
-@@ -0,0 +1,33 @@
-+#    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 webtest.app import AppError
-+
-+from magnum.tests.unit.common.policies import base
-+
-+
-+class TestStatsPolicy(base.PolicyFunctionalTest):
-+    def test_stat_reader(self):
-+        response = self.get_json('/stats', headers=self.reader_headers)
-+        expected = {u'clusters': 0, u'nodes': 0}
-+        self.assertEqual(expected, response)
-+
-+    def test_stat_admin(self):
-+        response = self.get_json('/stats', headers=self.admin_headers)
-+        expected = {u'clusters': 0, u'nodes': 0}
-+        self.assertEqual(expected, response)
-+
-+    def test_stat_no_permission(self):
-+        exc = self.assertRaises(
-+            AppError, self.get_json, '/stats',
-+            headers=self.member_headers)
-+        self.assertIn("403 Forbidden", str(exc))
-diff --git a/magnum/tests/unit/common/test_context.py b/magnum/tests/unit/common/test_context.py
-index c72c2c763d..aed4d33ebd 100644
---- a/magnum/tests/unit/common/test_context.py
-+++ b/magnum/tests/unit/common/test_context.py
-@@ -19,29 +19,30 @@
- class ContextTestCase(base.TestCase):
- 
-     def _create_context(self, roles=None):
--        return magnum_context.RequestContext(auth_token='auth_token1',
--                                             auth_url='auth_url1',
--                                             domain_id='domain_id1',
--                                             domain_name='domain_name1',
--                                             user_name='user1',
--                                             user_id='user-id1',
--                                             project_name='tenant1',
--                                             project_id='tenant-id1',
--                                             roles=roles,
--                                             is_admin=True,
--                                             read_only=True,
--                                             show_deleted=True,
--                                             request_id='request_id1',
--                                             trust_id='trust_id1',
--                                             auth_token_info='token_info1')
-+        return magnum_context.RequestContext(
-+            auth_token='auth_token1',
-+            auth_url='auth_url1',
-+            user_domain_id='user_domain_id1',
-+            user_domain_name='user_domain_name1',
-+            user_name='user1',
-+            user_id='user-id1',
-+            project_name='tenant1',
-+            project_id='tenant-id1',
-+            roles=roles,
-+            is_admin=True,
-+            read_only=True,
-+            show_deleted=True,
-+            request_id='request_id1',
-+            trust_id='trust_id1',
-+            auth_token_info='token_info1')
- 
-     def test_context(self):
-         ctx = self._create_context()
- 
-         self.assertEqual("auth_token1", ctx.auth_token)
-         self.assertEqual("auth_url1", ctx.auth_url)
--        self.assertEqual("domain_id1", ctx.domain_id)
--        self.assertEqual("domain_name1", ctx.domain_name)
-+        self.assertEqual("user_domain_id1", ctx.user_domain_id)
-+        self.assertEqual("user_domain_name1", ctx.user_domain_name)
-         self.assertEqual("user1", ctx.user_name)
-         self.assertEqual("user-id1", ctx.user_id)
-         self.assertEqual("tenant1", ctx.project_name)
-@@ -59,8 +60,8 @@ def test_context_with_roles(self):
- 
-         self.assertEqual("auth_token1", ctx.auth_token)
-         self.assertEqual("auth_url1", ctx.auth_url)
--        self.assertEqual("domain_id1", ctx.domain_id)
--        self.assertEqual("domain_name1", ctx.domain_name)
-+        self.assertEqual("user_domain_id1", ctx.user_domain_id)
-+        self.assertEqual("user_domain_name1", ctx.user_domain_name)
-         self.assertEqual("user1", ctx.user_name)
-         self.assertEqual("user-id1", ctx.user_id)
-         self.assertEqual("tenant1", ctx.project_name)
-@@ -80,8 +81,8 @@ def test_to_dict_from_dict(self):
- 
-         self.assertEqual(ctx.auth_token, ctx2.auth_token)
-         self.assertEqual(ctx.auth_url, ctx2.auth_url)
--        self.assertEqual(ctx.domain_id, ctx2.domain_id)
--        self.assertEqual(ctx.domain_name, ctx2.domain_name)
-+        self.assertEqual(ctx.user_domain_id, ctx2.user_domain_id)
-+        self.assertEqual(ctx.user_domain_name, ctx2.user_domain_name)
-         self.assertEqual(ctx.user_name, ctx2.user_name)
-         self.assertEqual(ctx.user_id, ctx2.user_id)
-         self.assertEqual(ctx.project_id, ctx2.project_id)
-diff --git a/releasenotes/notes/allow_admin_perform_acitons-cc988655bb72b3f3.yaml b/releasenotes/notes/allow_admin_perform_acitons-cc988655bb72b3f3.yaml
-new file mode 100644
-index 0000000000..6cb516451c
---- /dev/null
-+++ b/releasenotes/notes/allow_admin_perform_acitons-cc988655bb72b3f3.yaml
-@@ -0,0 +1,9 @@
-+---
-+upgrade:
-+  - |
-+    To make sure better have backward compatibility,
-+    we set specific rule to allow admin perform all actions.
-+    This will apply on part of APIs in
-+    * Cluster
-+    * Cluster Template
-+    * federation
-diff --git a/releasenotes/notes/enable-enforce-scope-and-new-defaults-7e6e503f74283071.yaml b/releasenotes/notes/enable-enforce-scope-and-new-defaults-7e6e503f74283071.yaml
-new file mode 100644
-index 0000000000..69b9fec5eb
---- /dev/null
-+++ b/releasenotes/notes/enable-enforce-scope-and-new-defaults-7e6e503f74283071.yaml
-@@ -0,0 +1,13 @@
-+---
-+upgrade:
-+  - |
-+    The Magnum service now allows enables policies (RBAC) new defaults
-+    and scope checks. These are controlled by the following (default) config
-+    options in ``magnum.conf`` file::
-+
-+      [oslo_policy]
-+      enforce_new_defaults=False
-+      enforce_scope=False
-+
-+    We will change the default to True in the following cycle.
-+    If you want to enable them then modify both values to True.
diff --git a/images/magnum/patches/0001-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
similarity index 100%
rename from images/magnum/patches/0001-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch
rename to images/magnum/patches/magnum/0000-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch
diff --git a/images/manila/Earthfile b/images/manila/Earthfile
index 320fc8f..5718142 100644
--- a/images/manila/Earthfile
+++ b/images/manila/Earthfile
@@ -1,17 +1,20 @@
 VERSION 0.7
 
+ARG --global PROJECT=manila
+ARG --global RELEASE=2023.2
+ARG --global PROJECT_REF=98bc755673fe48a9c67203e4262315b048efe25d
+
+build:
+  FROM ../openstack-service+builder --RELEASE=${RELEASE}
+  DO ../openstack-service+BUILD_VENV \
+    --PROJECT=${PROJECT} \
+    --PROJECT_REF=${PROJECT_REF}
+
 image:
-  ARG PROJECT=manila
-  ARG RELEASE=zed
-  ARG REF=9ea49e2b9df7da16d5700810eee18710dc90e6a4
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF}
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "iproute2 openvswitch-switch"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/manila/patches/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
similarity index 80%
rename from images/manila/patches/0000-fix-stop-using-batch_op-for-rename_table.patch
rename to images/manila/patches/manila/0000-fix-stop-using-batch_op-for-rename_table.patch
index a55e4e6..c904b26 100644
--- a/images/manila/patches/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
@@ -1,6 +1,6 @@
 From eb7f03c667261557d7f809f7851bad6b3eea4646 Mon Sep 17 00:00:00 2001
 From: Mohammed Naser <mnaser@vexxhost.com>
-Date: Mon, 08 Jan 2024 14:00:37 -0500
+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
@@ -8,12 +8,14 @@
 
 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 42d26b7..373e308 100644
+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 @@
+@@ -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')
@@ -24,3 +26,6 @@
  
  
  def downgrade_export_locations_table(connection):
+-- 
+2.34.1
+
diff --git a/images/neutron/Earthfile b/images/neutron/Earthfile
index 8b61017..a9b4712 100644
--- a/images/neutron/Earthfile
+++ b/images/neutron/Earthfile
@@ -1,21 +1,24 @@
 VERSION 0.7
 
+ARG --global PROJECT=neutron
+ARG --global RELEASE=zed
+ARG --global PROJECT_REF=222c997022392561c2de2cb493f0f5214eb20dfc
+
+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:
-  ARG PROJECT=neutron
-  ARG RELEASE=zed
-  ARG REF=222c997022392561c2de2cb493f0f5214eb20dfc
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "git+https://github.com/openstack/neutron-vpnaas.git@256464aea691f8b4957ba668a117963353f34e4c"
-  DO \
-    ../+APT_INSTALL \
+  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"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
 
 image:
   BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/neutron/patches/0000-fix-netns-deletion-of-broken-namespaces.patch b/images/neutron/patches/neutron/0000-fix-netns-deletion-of-broken-namespaces.patch
similarity index 85%
rename from images/neutron/patches/0000-fix-netns-deletion-of-broken-namespaces.patch
rename to images/neutron/patches/neutron/0000-fix-netns-deletion-of-broken-namespaces.patch
index a1540ad..a20200b 100644
--- a/images/neutron/patches/0000-fix-netns-deletion-of-broken-namespaces.patch
+++ b/images/neutron/patches/neutron/0000-fix-netns-deletion-of-broken-namespaces.patch
@@ -1,4 +1,4 @@
-From f8130f36e3cdb67fd9be64a61ac22b487200e2bc Mon Sep 17 00:00:00 2001
+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
@@ -26,13 +26,19 @@
 
 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 9953729..196dc17 100644
+index 10bd33d9e1..5d2593da47 100644
 --- a/neutron/agent/linux/ip_lib.py
 +++ b/neutron/agent/linux/ip_lib.py
-@@ -259,7 +259,22 @@
+@@ -259,7 +259,22 @@ class IPWrapper(SubProcessBase):
          return ip
  
      def namespace_is_empty(self):
@@ -57,10 +63,10 @@
      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 7a09145..888ab15 100644
+index 1745239701..861715d8e1 100644
 --- a/neutron/agent/ovn/metadata/agent.py
 +++ b/neutron/agent/ovn/metadata/agent.py
-@@ -478,7 +478,10 @@
+@@ -430,7 +430,10 @@ class MetadataAgent(object):
                               ns.startswith(NS_PREFIX) and
                               ns not in metadata_namespaces]
          for ns in unused_namespaces:
@@ -73,10 +79,10 @@
          # 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 c488e90..3956142 100644
+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,23 @@
+@@ -357,6 +357,21 @@ class TestIpWrapper(base.BaseTestCase):
                  self.assertNotIn(mock.call().delete('ns'),
                                   ip_ns_cmd_cls.mock_calls)
  
@@ -92,19 +98,17 @@
 +                self.assertTrue(ip.garbage_collect_namespace())
 +
 +                mock_get_devices.assert_called_once_with()
-+                expected = [mock.call(ip),
-+                            mock.call().exists('ns'),
-+                            mock.call().delete('ns')]
-+                self.assertEqual(ip_ns_cmd_cls.mock_calls, expected)
++                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 73487e1..cb6ee43 100644
+index 6df7da702d..9bf9f0db52 100644
 --- a/neutron/tests/unit/agent/ovn/metadata/test_agent.py
 +++ b/neutron/tests/unit/agent/ovn/metadata/test_agent.py
-@@ -138,6 +138,31 @@
+@@ -134,6 +134,31 @@ class TestMetadataAgent(base.BaseTestCase):
              lnn.assert_called_once_with()
              tdp.assert_called_once_with('3')
  
@@ -136,3 +140,6 @@
      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/Earthfile b/images/nova/Earthfile
index 3d86b60..2780578 100644
--- a/images/nova/Earthfile
+++ b/images/nova/Earthfile
@@ -1,21 +1,24 @@
 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:
-  ARG PROJECT=nova
-  ARG RELEASE=zed
-  ARG REF=226f3e95c1cdadd1845c7adee55f5c5f29f3a7a8
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF}
-  DO \
-    ../+APT_INSTALL \
+  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"
-  DO ../+APPLY_PATCHES
   GIT CLONE --branch v1.4.0 https://github.com/novnc/noVNC.git /usr/share/novnc
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
 
 image:
   BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/octavia/Earthfile b/images/octavia/Earthfile
index 16e5799..d37ba36 100644
--- a/images/octavia/Earthfile
+++ b/images/octavia/Earthfile
@@ -1,18 +1,21 @@
 VERSION 0.7
 
+ARG --global PROJECT=octavia
+ARG --global RELEASE=2023.2
+ARG --global PROJECT_REF=88d7315a60314e44fcce88ad198ceb3c0c107fe6
+
+build:
+  FROM ../openstack-service+builder --RELEASE=${RELEASE}
+  DO ../openstack-service+BUILD_VENV \
+    --PROJECT=${PROJECT} \
+    --PROJECT_REF=${PROJECT_REF} \
+    --PIP_PACKAGES="ovn-octavia-provider"
+
 image:
-  ARG PROJECT=octavia
-  ARG RELEASE=zed
-  ARG REF=07c33fec4578e2529a2a5fc8fad4cde929597849
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "ovn-octavia-provider"
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "isc-dhcp-client openssh-client"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/openstack-service/2023.1/upper-constraints.txt b/images/openstack-service/2023.1/upper-constraints.txt
index 1962f42..beede4e 100644
--- a/images/openstack-service/2023.1/upper-constraints.txt
+++ b/images/openstack-service/2023.1/upper-constraints.txt
@@ -540,7 +540,6 @@
 python-blazarclient==3.7.0
 alembic==1.13.1
 execnet==1.9.0
-glance-store==4.6.1
 sphinxcontrib-programoutput==0.17
 storpool.spopenstack==3.2.0
 sphinx-testing==1.0.1
diff --git a/images/openstack-service/2023.2/upper-constraints.txt b/images/openstack-service/2023.2/upper-constraints.txt
index 52c2338..2e62bc7 100644
--- a/images/openstack-service/2023.2/upper-constraints.txt
+++ b/images/openstack-service/2023.2/upper-constraints.txt
@@ -549,7 +549,6 @@
 python-blazarclient==3.7.0
 alembic==1.13.1
 execnet==1.9.0
-glance-store==4.6.1
 sphinxcontrib-programoutput==0.17
 storpool.spopenstack==3.2.0
 sphinx-testing==1.0.1
diff --git a/images/openstack-service/Earthfile b/images/openstack-service/Earthfile
index 20c1f50..7029432 100644
--- a/images/openstack-service/Earthfile
+++ b/images/openstack-service/Earthfile
@@ -1,6 +1,45 @@
 VERSION 0.7
 
-build:
+PIP_INSTALL:
+  COMMAND
+  ARG PACKAGES
+  RUN --mount=type=cache,target=/root/.cache \
+    /var/lib/openstack/bin/pip3 install \
+      --constraint /upper-constraints.txt \
+      ${PACKAGES}
+
+GIT_CHECKOUT:
+  COMMAND
+  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:
+  COMMAND
+  ARG PROJECT
+  ARG PROJECT_REF
+  DO +GIT_CHECKOUT --PROJECT=${PROJECT} --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
+  COPY ${RELEASE}/upper-constraints.txt /upper-constraints.txt
+  SAVE ARTIFACT /upper-constraints.txt
+
+builder:
   ARG RELEASE
   FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
   DO ../+APT_INSTALL --PACKAGES "\
@@ -19,41 +58,14 @@
     python3-venv"
   RUN --mount type=cache,target=/root/.cache \
     python3 -m venv --upgrade --system-site-packages /var/lib/openstack
-  ENV UWSGI_PROFILE_OVERRIDE=ssl=true
-  RUN --mount type=cache,target=/root/.cache \
-    mkdir -p /wheels && \
-    /var/lib/openstack/bin/pip3 wheel --wheel-dir /wheels uwsgi
-  COPY ${RELEASE}/upper-constraints.txt /upper-constraints.txt
-  ARG PROJECT
-  ARG PROJECT_REF
-  ARG PROJECT_REPO=https://opendev.org/openstack/${PROJECT}
-  GIT CLONE --branch ${PROJECT_REF} ${PROJECT_REPO} /src
-  RUN \
-    cd /src && \
-    git fetch --unshallow
-  ARG EXTRAS=""
-  ARG PIP_PACKAGES=""
-  RUN --mount=type=cache,target=/root/.cache \
-    /var/lib/openstack/bin/pip3 install \
-      --constraint /upper-constraints.txt \
-      --find-links /wheels/ \
-      pymysql \
-      python-memcached \
-      cryptography \
-      uwsgi \
-      /src${EXTRAS} \
-      ${PIP_PACKAGES}
-  SAVE ARTIFACT /var/lib/openstack venv
+  COPY \
+    (+requirements/upper-constraints.txt --RELEASE=${RELEASE}) \
+    /upper-constraints.txt
+  DO +PIP_INSTALL --PACKAGES "pymysql python-memcached cryptography uwsgi"
 
 image:
-  ARG RELEASE
+  ARG --required RELEASE
   FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
-  ARG PROJECT
-  ARG PROJECT_REF
-  ARG PIP_PACKAGES
-  DO ../+CREATE_PROJECT_USER --PROJECT=${PROJECT}
   ENV PATH=/var/lib/openstack/bin:$PATH
-  COPY \
-    (+build/venv --RELEASE=${RELEASE} --PROJECT=${PROJECT} --PROJECT_REF=${PROJECT_REF} --PIP_PACKAGES=${PIP_PACKAGES}) \
-    /var/lib/openstack
-  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
+  ARG --required PROJECT
+  DO ../+CREATE_PROJECT_USER --PROJECT=${PROJECT}
diff --git a/images/openstack-service/master/upper-constraints.txt b/images/openstack-service/master/upper-constraints.txt
index 1b044b9..667fcb5 100644
--- a/images/openstack-service/master/upper-constraints.txt
+++ b/images/openstack-service/master/upper-constraints.txt
@@ -553,7 +553,6 @@
 python-blazarclient==3.7.0
 alembic==1.13.1
 execnet==2.0.2
-glance-store==4.6.1
 sphinxcontrib-programoutput==0.17
 storpool.spopenstack==3.2.0
 sphinx-testing==1.0.1
diff --git a/images/openstack-service/zed/upper-constraints.txt b/images/openstack-service/zed/upper-constraints.txt
index 1d844e3..6d94a1a 100644
--- a/images/openstack-service/zed/upper-constraints.txt
+++ b/images/openstack-service/zed/upper-constraints.txt
@@ -541,7 +541,6 @@
 python-blazarclient==3.7.0
 alembic==1.13.1
 execnet==1.9.0
-glance-store==4.6.1
 sphinxcontrib-programoutput==0.17
 storpool.spopenstack==3.2.0
 sphinx-testing==1.0.1
diff --git a/images/placement/Earthfile b/images/placement/Earthfile
index 81a3369..64d6afd 100644
--- a/images/placement/Earthfile
+++ b/images/placement/Earthfile
@@ -1,14 +1,18 @@
 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:
-  ARG PROJECT=placement
-  ARG RELEASE=zed
-  ARG REF=d7ced6bd2fc82caf458f20b5652888164b1bbb70
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF}
-  DO ../+APPLY_PATCHES
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/senlin/Earthfile b/images/senlin/Earthfile
index 60da266..7e67788 100644
--- a/images/senlin/Earthfile
+++ b/images/senlin/Earthfile
@@ -1,14 +1,18 @@
 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:
-  ARG PROJECT=senlin
-  ARG RELEASE=zed
-  ARG REF=b6ef17b0f787fb7a0609ba36dc13097882a6a3ff
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF}
-  DO ../+APPLY_PATCHES
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/images/tempest/Earthfile b/images/tempest/Earthfile
index 062d82a..9f180c7 100644
--- a/images/tempest/Earthfile
+++ b/images/tempest/Earthfile
@@ -1,18 +1,21 @@
 VERSION 0.7
 
+ARG --global PROJECT=tempest
+ARG --global RELEASE=master
+ARG --global PROJECT_REF=699749ec27897efe9bd7824664237c16c3339c03
+
+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/barbican-tempest-plugin.git git+https://github.com/openstack/cinder-tempest-plugin.git git+https://github.com/openstack/heat-tempest-plugin.git git+https://github.com/openstack/keystone-tempest-plugin.git git+https://github.com/openstack/neutron-tempest-plugin.git"
+
 image:
-  ARG PROJECT=tempest
-  ARG RELEASE=master
-  ARG REF=699749ec27897efe9bd7824664237c16c3339c03
-  FROM ../openstack-service+image \
-    --PROJECT ${PROJECT} \
-    --RELEASE ${RELEASE} \
-    --PROJECT_REF ${REF} \
-    --PIP_PACKAGES "git+https://github.com/openstack/barbican-tempest-plugin.git git+https://github.com/openstack/cinder-tempest-plugin.git git+https://github.com/openstack/heat-tempest-plugin.git git+https://github.com/openstack/keystone-tempest-plugin.git git+https://github.com/openstack/neutron-tempest-plugin.git"
-  DO \
-    ../+APT_INSTALL \
+  FROM ../openstack-service+image --RELEASE ${RELEASE} --PROJECT ${PROJECT}
+  COPY +build/venv /var/lib/openstack
+  DO ../+APT_INSTALL \
     --PACKAGES "iputils-ping"
-  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
-    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${PROJECT_REF}
diff --git a/roles/defaults/vars/main.yml b/roles/defaults/vars/main.yml
index 7ff5bcc..dc34073 100644
--- a/roles/defaults/vars/main.yml
+++ b/roles/defaults/vars/main.yml
@@ -14,9 +14,9 @@
 
 _atmosphere_images:
   alertmanager: quay.io/prometheus/alertmanager:v0.26.0@sha256:361db356b33041437517f1cd298462055580585f26555c317df1a3caf2868552
-  barbican_api: ghcr.io/vexxhost/atmosphere/barbican:zed@sha256:1cdfcefaf7def5031b97148f5d9d3d280b19c90ad63b3857e6554f258555311a
-  barbican_db_sync: ghcr.io/vexxhost/atmosphere/barbican:zed@sha256:1cdfcefaf7def5031b97148f5d9d3d280b19c90ad63b3857e6554f258555311a
-  bootstrap: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
+  barbican_api: ghcr.io/vexxhost/atmosphere/barbican:2023.2@sha256:e36e6e5882bfc6d8a20497206a69cf8d7abe9450a50a6166759f65295ae021ab
+  barbican_db_sync: ghcr.io/vexxhost/atmosphere/barbican:2023.2@sha256:e36e6e5882bfc6d8a20497206a69cf8d7abe9450a50a6166759f65295ae021ab
+  bootstrap: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
   ceph_config_helper: ghcr.io/vexxhost/atmosphere/libvirtd@sha256:68274a76b635cf78a513e0b9324e49efdc653714bf974161e5940ddfda206114
   ceph: quay.io/ceph/ceph:v16.2.11@sha256:1b9803c8984bef8b82f05e233e8fe8ed8f0bba8e5cc2c57f6efaccbeea682add
   cert_manager_cainjector: quay.io/jetstack/cert-manager-cainjector:v1.7.1@sha256:985743eeed2b62f68ee06e583f1d5a371e1c35af4b1980a1b2571d29174cce47
@@ -25,14 +25,14 @@
   cert_manager_webhook: quay.io/jetstack/cert-manager-webhook:v1.7.1@sha256:a926d60b6f23553ca5d11ac9cd66bcc692136e838613c8bc0d60c6c35a3cbcfc
   cilium_node: quay.io/cilium/cilium:v1.13.3@sha256:77176464a1e11ea7e89e984ac7db365e7af39851507e94f137dcf56c87746314
   cilium_operator: quay.io/cilium/operator-generic:v1.13.3@sha256:fa7003cbfdf8358cb71786afebc711b26e5e44a2ed99bd4944930bba915b8910
-  cinder_api: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
-  cinder_backup_storage_init: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
-  cinder_backup: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
-  cinder_db_sync: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
-  cinder_scheduler: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
-  cinder_storage_init: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
-  cinder_volume_usage_audit: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
-  cinder_volume: ghcr.io/vexxhost/atmosphere/cinder:zed@sha256:79a803e82b98b1822268f1451394d4d52cf289c8ed661a783461db4ba03b0a95
+  cinder_api: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
+  cinder_backup_storage_init: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
+  cinder_backup: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
+  cinder_db_sync: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
+  cinder_scheduler: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
+  cinder_storage_init: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
+  cinder_volume_usage_audit: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
+  cinder_volume: ghcr.io/vexxhost/atmosphere/cinder:2023.2@sha256:d07e77592f478a373b4043865947dc436babe8fbc37d7fe4a0381c3fea27fc55
   cluster_api_controller: registry.k8s.io/cluster-api/cluster-api-controller:v1.5.1@sha256:5210087161fdc09c98e47f847c07ed3ff93470e774cb1d5a792e2f76eaa5cf12
   cluster_api_kubeadm_bootstrap_controller: registry.k8s.io/cluster-api/kubeadm-bootstrap-controller:v1.5.1@sha256:6d73f991862d0df9724fab31a4a694681d9181e772c265d2c5b9b0b26b572479
   cluster_api_kubeadm_control_plane_controller: registry.k8s.io/cluster-api/kubeadm-control-plane-controller:v1.5.1@sha256:8d926bcd3e0ca6be6cb9212f692f0ea6790f83862f4dc02fae0c7e0b35e76907
@@ -43,31 +43,31 @@
   csi_rbd_provisioner: registry.k8s.io/sig-storage/csi-provisioner:v3.1.0@sha256:92107bb668a9de58a09247596c337bc5b46a1d145685eb55ef489ae16952f5bd
   csi_rbd_resizer: registry.k8s.io/sig-storage/csi-resizer:v1.3.0@sha256:35ec0c736ec8266bd4a46f9e942315f148f3139beed99879d0ad8b8e5074d641
   csi_rbd_snapshotter: registry.k8s.io/sig-storage/csi-snapshotter:v4.2.0@sha256:bd7dafbd0d4fe81f23f01c9a7432de067bdf085f70d61492f5ffddd9c5264358
-  db_drop: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  db_init: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
+  db_drop: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  db_init: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
   dep_check: ghcr.io/vexxhost/atmosphere/kubernetes-entrypoint:latest@sha256:a1993b58da2afb16b44d4e510bd217ab872a4c10f598909edc39e72cda92f0b5
-  designate_api: ghcr.io/vexxhost/atmosphere/designate:zed@sha256:5f12564675b8f111cb334af27d69c75bde77ebf5899f85ddcb484c481df5a8bc
-  designate_central: ghcr.io/vexxhost/atmosphere/designate:zed@sha256:5f12564675b8f111cb334af27d69c75bde77ebf5899f85ddcb484c481df5a8bc
-  designate_db_sync: ghcr.io/vexxhost/atmosphere/designate:zed@sha256:5f12564675b8f111cb334af27d69c75bde77ebf5899f85ddcb484c481df5a8bc
-  designate_mdns: ghcr.io/vexxhost/atmosphere/designate:zed@sha256:5f12564675b8f111cb334af27d69c75bde77ebf5899f85ddcb484c481df5a8bc
-  designate_producer: ghcr.io/vexxhost/atmosphere/designate:zed@sha256:5f12564675b8f111cb334af27d69c75bde77ebf5899f85ddcb484c481df5a8bc
-  designate_sink: ghcr.io/vexxhost/atmosphere/designate:zed@sha256:5f12564675b8f111cb334af27d69c75bde77ebf5899f85ddcb484c481df5a8bc
-  designate_worker: ghcr.io/vexxhost/atmosphere/designate:zed@sha256:5f12564675b8f111cb334af27d69c75bde77ebf5899f85ddcb484c481df5a8bc
-  glance_api: ghcr.io/vexxhost/atmosphere/glance:zed@sha256:209722d4e9b1cbbd834e166a8ea090f07bc7aa4083d21bb21bfc137a70f6536f
-  glance_db_sync: ghcr.io/vexxhost/atmosphere/glance:zed@sha256:209722d4e9b1cbbd834e166a8ea090f07bc7aa4083d21bb21bfc137a70f6536f
-  glance_metadefs_load: ghcr.io/vexxhost/atmosphere/glance:zed@sha256:209722d4e9b1cbbd834e166a8ea090f07bc7aa4083d21bb21bfc137a70f6536f
-  glance_registry: ghcr.io/vexxhost/atmosphere/glance:zed@sha256:209722d4e9b1cbbd834e166a8ea090f07bc7aa4083d21bb21bfc137a70f6536f
-  glance_storage_init: ghcr.io/vexxhost/atmosphere/glance:zed@sha256:209722d4e9b1cbbd834e166a8ea090f07bc7aa4083d21bb21bfc137a70f6536f
+  designate_api: ghcr.io/vexxhost/atmosphere/designate:2023.2@sha256:3d40cdcba4c806d68fd58ea15c263b783b5a7e64aa87228ed3c5ddf14f4ab240
+  designate_central: ghcr.io/vexxhost/atmosphere/designate:2023.2@sha256:3d40cdcba4c806d68fd58ea15c263b783b5a7e64aa87228ed3c5ddf14f4ab240
+  designate_db_sync: ghcr.io/vexxhost/atmosphere/designate:2023.2@sha256:3d40cdcba4c806d68fd58ea15c263b783b5a7e64aa87228ed3c5ddf14f4ab240
+  designate_mdns: ghcr.io/vexxhost/atmosphere/designate:2023.2@sha256:3d40cdcba4c806d68fd58ea15c263b783b5a7e64aa87228ed3c5ddf14f4ab240
+  designate_producer: ghcr.io/vexxhost/atmosphere/designate:2023.2@sha256:3d40cdcba4c806d68fd58ea15c263b783b5a7e64aa87228ed3c5ddf14f4ab240
+  designate_sink: ghcr.io/vexxhost/atmosphere/designate:2023.2@sha256:3d40cdcba4c806d68fd58ea15c263b783b5a7e64aa87228ed3c5ddf14f4ab240
+  designate_worker: ghcr.io/vexxhost/atmosphere/designate:2023.2@sha256:3d40cdcba4c806d68fd58ea15c263b783b5a7e64aa87228ed3c5ddf14f4ab240
+  glance_api: ghcr.io/vexxhost/atmosphere/glance:2023.2@sha256:90a212dc90be925b33809a8488ab50cd7c1b09bf619b045c4cd24c409784bfcc
+  glance_db_sync: ghcr.io/vexxhost/atmosphere/glance:2023.2@sha256:90a212dc90be925b33809a8488ab50cd7c1b09bf619b045c4cd24c409784bfcc
+  glance_metadefs_load: ghcr.io/vexxhost/atmosphere/glance:2023.2@sha256:90a212dc90be925b33809a8488ab50cd7c1b09bf619b045c4cd24c409784bfcc
+  glance_registry: ghcr.io/vexxhost/atmosphere/glance:2023.2@sha256:90a212dc90be925b33809a8488ab50cd7c1b09bf619b045c4cd24c409784bfcc
+  glance_storage_init: ghcr.io/vexxhost/atmosphere/glance:2023.2@sha256:90a212dc90be925b33809a8488ab50cd7c1b09bf619b045c4cd24c409784bfcc
   grafana_sidecar: quay.io/kiwigrid/k8s-sidecar:1.24.6@sha256:3b70b9f1a81e67c97e4cd32c9a918fa44fd2c9f66bdd0d28d8b82d7b502cb5e4
   grafana: docker.io/grafana/grafana:10.1.0@sha256:047c1c5aa6fef257b6c2516f95c8fcd9f28707c201c6413dd78328b6cbedff6f
   haproxy: docker.io/library/haproxy:2.5@sha256:ea38b570dd7836aa6b85ef1fb19d1e03f5322cccd62e688f0c2b79e38ac4f391
-  heat_api: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  heat_cfn: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  heat_cloudwatch: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  heat_db_sync: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  heat_engine_cleaner: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  heat_engine: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  heat_purge_deleted: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
+  heat_api: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  heat_cfn: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  heat_cloudwatch: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  heat_db_sync: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  heat_engine_cleaner: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  heat_engine: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  heat_purge_deleted: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
   horizon_db_sync: ghcr.io/vexxhost/atmosphere/horizon:2023.2@sha256:ef3876e0425182a2b741ebb696ebb4c9e79044d0fabf7afbf8d0025898db58a4
   horizon: ghcr.io/vexxhost/atmosphere/horizon:2023.2@sha256:ef3876e0425182a2b741ebb696ebb4c9e79044d0fabf7afbf8d0025898db58a4
   ingress_nginx_controller: registry.k8s.io/ingress-nginx/controller:v1.1.1@sha256:e16123f3932f44a2bba8bc3cf1c109cea4495ee271d6d16ab99228b58766d3ab
@@ -75,17 +75,17 @@
   ingress_nginx_kube_webhook_certgen: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:23a03c9c381fba54043d0f6148efeaf4c1ca2ed176e43455178b5c5ebf15ad70 # noqa: yaml[line-length]
   keepalived: us-docker.pkg.dev/vexxhost-infra/openstack/keepalived:2.0.19@sha256:4fe20cd5c200e301e1a790c9aca8c3fc651c8461afea9d37c56a462d3bfa48bb
   keycloak: quay.io/keycloak/keycloak:22.0.1-0@sha256:5b872e841ea9e394d89bdf250146434532d9c2001404540d46621d60f87494e7
-  keystone_api: ghcr.io/vexxhost/atmosphere/keystone:zed@sha256:e2201904496c4eeaaf42cc90ccfb2bdbef3a70310b3ab7cf6d981d0b7aa5d78c
-  keystone_credential_cleanup: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  keystone_credential_rotate: ghcr.io/vexxhost/atmosphere/keystone:zed@sha256:e2201904496c4eeaaf42cc90ccfb2bdbef3a70310b3ab7cf6d981d0b7aa5d78c
-  keystone_credential_setup: ghcr.io/vexxhost/atmosphere/keystone:zed@sha256:e2201904496c4eeaaf42cc90ccfb2bdbef3a70310b3ab7cf6d981d0b7aa5d78c
-  keystone_db_sync: ghcr.io/vexxhost/atmosphere/keystone:zed@sha256:e2201904496c4eeaaf42cc90ccfb2bdbef3a70310b3ab7cf6d981d0b7aa5d78c
-  keystone_domain_manage: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  keystone_fernet_rotate: ghcr.io/vexxhost/atmosphere/keystone:zed@sha256:e2201904496c4eeaaf42cc90ccfb2bdbef3a70310b3ab7cf6d981d0b7aa5d78c
-  keystone_fernet_setup: ghcr.io/vexxhost/atmosphere/keystone:zed@sha256:e2201904496c4eeaaf42cc90ccfb2bdbef3a70310b3ab7cf6d981d0b7aa5d78c
-  ks_endpoints: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  ks_service: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  ks_user: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
+  keystone_api: ghcr.io/vexxhost/atmosphere/keystone:2023.2@sha256:1e46e5a6e67c10095d3ca597fb166fc5fbb07fb38034a5020244cb0ee200091d
+  keystone_credential_cleanup: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  keystone_credential_rotate: ghcr.io/vexxhost/atmosphere/keystone:2023.2@sha256:1e46e5a6e67c10095d3ca597fb166fc5fbb07fb38034a5020244cb0ee200091d
+  keystone_credential_setup: ghcr.io/vexxhost/atmosphere/keystone:2023.2@sha256:1e46e5a6e67c10095d3ca597fb166fc5fbb07fb38034a5020244cb0ee200091d
+  keystone_db_sync: ghcr.io/vexxhost/atmosphere/keystone:2023.2@sha256:1e46e5a6e67c10095d3ca597fb166fc5fbb07fb38034a5020244cb0ee200091d
+  keystone_domain_manage: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  keystone_fernet_rotate: ghcr.io/vexxhost/atmosphere/keystone:2023.2@sha256:1e46e5a6e67c10095d3ca597fb166fc5fbb07fb38034a5020244cb0ee200091d
+  keystone_fernet_setup: ghcr.io/vexxhost/atmosphere/keystone:2023.2@sha256:1e46e5a6e67c10095d3ca597fb166fc5fbb07fb38034a5020244cb0ee200091d
+  ks_endpoints: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  ks_service: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  ks_user: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
   kube_apiserver: registry.k8s.io/kube-apiserver:v1.22.17@sha256:d88d1c8f972e10ff4b4176f3185434e2832d3805c457fa9e8816f1da2fdf3b93
   kube_controller_manager: registry.k8s.io/kube-controller-manager:v1.22.17@sha256:c3e041c8c8c9ffd33d421c8c1de1f42da52b616bfcf61880498e9efc9ec88005
   kube_coredns: registry.k8s.io/coredns/coredns:v1.8.4@sha256:10683d82b024a58cc248c468c2632f9d1b260500f7cd9bb8e73f751048d7d6d4
@@ -101,16 +101,16 @@
   local_path_provisioner: docker.io/rancher/local-path-provisioner:v0.0.24@sha256:5bb33992a4ec3034c28b5e0b3c4c2ac35d3613b25b79455eb4b1a95adc82cdc0
   loki_gateway: docker.io/nginxinc/nginx-unprivileged:1.19-alpine@sha256:bbd46452aae30a7cc7bc438f267af812c7a2b0f3b5bcd4cc55eb99669cea3f28
   loki: docker.io/grafana/loki:2.7.3@sha256:8e3abbd89173066721fa07bddfee1c1a7a8fe59bed5b00a2fa09d2b3cef8758c
-  magnum_api: ghcr.io/vexxhost/atmosphere/magnum:zed@sha256:df5a2662803e423cf6d158254e519095930a70ea050aa51d1871f1c7d19aa060
-  magnum_cluster_api_proxy: ghcr.io/vexxhost/atmosphere/magnum:zed@sha256:df5a2662803e423cf6d158254e519095930a70ea050aa51d1871f1c7d19aa060
-  magnum_conductor: ghcr.io/vexxhost/atmosphere/magnum:zed@sha256:df5a2662803e423cf6d158254e519095930a70ea050aa51d1871f1c7d19aa060
-  magnum_db_sync: ghcr.io/vexxhost/atmosphere/magnum:zed@sha256:df5a2662803e423cf6d158254e519095930a70ea050aa51d1871f1c7d19aa060
+  magnum_api: ghcr.io/vexxhost/atmosphere/magnum:2023.2@sha256:0712bdda7f3bf80a42acdae3fe29d526fa20292ed8dfc265655e91280c8d8244
+  magnum_cluster_api_proxy: ghcr.io/vexxhost/atmosphere/magnum:2023.2@sha256:0712bdda7f3bf80a42acdae3fe29d526fa20292ed8dfc265655e91280c8d8244
+  magnum_conductor: ghcr.io/vexxhost/atmosphere/magnum:2023.2@sha256:0712bdda7f3bf80a42acdae3fe29d526fa20292ed8dfc265655e91280c8d8244
+  magnum_db_sync: ghcr.io/vexxhost/atmosphere/magnum:2023.2@sha256:0712bdda7f3bf80a42acdae3fe29d526fa20292ed8dfc265655e91280c8d8244
   magnum_registry: quay.io/vexxhost/magnum-cluster-api-registry:latest@sha256:b954f23ccdbfb2b5b43f6a4e6f7ef5f2ba7bfc81f31de54cf141a56b26628c41
-  manila_api: ghcr.io/vexxhost/atmosphere/manila:zed@sha256:5c590bf7fc738e8ab11dc267285c207a06098bc113bd70696f6928a7a887e844
-  manila_data: ghcr.io/vexxhost/atmosphere/manila:zed@sha256:5c590bf7fc738e8ab11dc267285c207a06098bc113bd70696f6928a7a887e844
-  manila_db_sync: ghcr.io/vexxhost/atmosphere/manila:zed@sha256:5c590bf7fc738e8ab11dc267285c207a06098bc113bd70696f6928a7a887e844
-  manila_scheduler: ghcr.io/vexxhost/atmosphere/manila:zed@sha256:5c590bf7fc738e8ab11dc267285c207a06098bc113bd70696f6928a7a887e844
-  manila_share: ghcr.io/vexxhost/atmosphere/manila:zed@sha256:5c590bf7fc738e8ab11dc267285c207a06098bc113bd70696f6928a7a887e844
+  manila_api: ghcr.io/vexxhost/atmosphere/manila:2023.2@sha256:cf30be0f2ed730274147e9e6d1d68d419fc3bb62f3e6bf201d7d37636d8eda7c
+  manila_data: ghcr.io/vexxhost/atmosphere/manila:2023.2@sha256:cf30be0f2ed730274147e9e6d1d68d419fc3bb62f3e6bf201d7d37636d8eda7c
+  manila_db_sync: ghcr.io/vexxhost/atmosphere/manila:2023.2@sha256:cf30be0f2ed730274147e9e6d1d68d419fc3bb62f3e6bf201d7d37636d8eda7c
+  manila_scheduler: ghcr.io/vexxhost/atmosphere/manila:2023.2@sha256:cf30be0f2ed730274147e9e6d1d68d419fc3bb62f3e6bf201d7d37636d8eda7c
+  manila_share: ghcr.io/vexxhost/atmosphere/manila:2023.2@sha256:cf30be0f2ed730274147e9e6d1d68d419fc3bb62f3e6bf201d7d37636d8eda7c
   memcached: docker.io/library/memcached:1.6.17@sha256:d20c577c08863b09b21ecd21d0384d0a800f39d82f37045b3608f677a0a9400f
   netoffload: ghcr.io/vexxhost/netoffload:v1.0.1@sha256:60f092e5d5f156c2f933c199ea72274f80eb758d3e0dc2f2b1be62174c3f7183
   neutron_bagpipe_bgp: ghcr.io/vexxhost/atmosphere/neutron:zed@sha256:14a8bf03360ddf6f83926bf144fdb2ae9db80a08eb0b6a54cf6a44fe48bd5887
@@ -132,7 +132,7 @@
   node_feature_discovery: registry.k8s.io/nfd/node-feature-discovery:v0.11.2@sha256:24b2abfb5956b6a2a9a0f4248232838d02235d65044078c43d8bdcf29344f141
   nova_api: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
   nova_archive_deleted_rows: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
-  nova_cell_setup_init: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
+  nova_cell_setup_init: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
   nova_cell_setup: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
   nova_compute_ironic: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
   nova_compute_ssh: ghcr.io/vexxhost/atmosphere/nova-ssh:latest@sha256:902ef58f699de769b6809e8b8bb43c47daec14478c9b8952bec1dc7671f4d2cc
@@ -144,15 +144,15 @@
   nova_novncproxy: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
   nova_placement: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
   nova_scheduler: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
-  nova_service_cleaner: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
+  nova_service_cleaner: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
   nova_spiceproxy_assets: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
   nova_spiceproxy: ghcr.io/vexxhost/atmosphere/nova:zed@sha256:584c9e0a1c503110c95ff511610993e9b41d99091579291c7726db155b6fa0ca
-  octavia_api: ghcr.io/vexxhost/atmosphere/octavia:zed@sha256:37bdfe3d4cf538301afd5467b949bd2b624018d3a0afd6963393ad3a160d0283
-  octavia_db_sync: ghcr.io/vexxhost/atmosphere/octavia:zed@sha256:37bdfe3d4cf538301afd5467b949bd2b624018d3a0afd6963393ad3a160d0283
-  octavia_health_manager_init: ghcr.io/vexxhost/atmosphere/heat:zed@sha256:3b3c2da31dec32ac7f1e40e23ac242e07071cc89bd8e54a6e006294b15da191e
-  octavia_health_manager: ghcr.io/vexxhost/atmosphere/octavia:zed@sha256:37bdfe3d4cf538301afd5467b949bd2b624018d3a0afd6963393ad3a160d0283
-  octavia_housekeeping: ghcr.io/vexxhost/atmosphere/octavia:zed@sha256:37bdfe3d4cf538301afd5467b949bd2b624018d3a0afd6963393ad3a160d0283
-  octavia_worker: ghcr.io/vexxhost/atmosphere/octavia:zed@sha256:37bdfe3d4cf538301afd5467b949bd2b624018d3a0afd6963393ad3a160d0283
+  octavia_api: ghcr.io/vexxhost/atmosphere/octavia:2023.2@sha256:7b865b938379bcd75eeb90a457a2a19a6a70c0cf28fea250777ca75974ea1389
+  octavia_db_sync: ghcr.io/vexxhost/atmosphere/octavia:2023.2@sha256:7b865b938379bcd75eeb90a457a2a19a6a70c0cf28fea250777ca75974ea1389
+  octavia_health_manager_init: ghcr.io/vexxhost/atmosphere/heat:2023.2@sha256:c5118e27245b53db06e5098e980816d5f2a8f2615dde49d5e0c5b3172ee69bf6
+  octavia_health_manager: ghcr.io/vexxhost/atmosphere/octavia:2023.2@sha256:7b865b938379bcd75eeb90a457a2a19a6a70c0cf28fea250777ca75974ea1389
+  octavia_housekeeping: ghcr.io/vexxhost/atmosphere/octavia:2023.2@sha256:7b865b938379bcd75eeb90a457a2a19a6a70c0cf28fea250777ca75974ea1389
+  octavia_worker: ghcr.io/vexxhost/atmosphere/octavia:2023.2@sha256:7b865b938379bcd75eeb90a457a2a19a6a70c0cf28fea250777ca75974ea1389
   openvswitch_db_server: ghcr.io/vexxhost/atmosphere/openvswitch:3.1.0-65@sha256:c68347b6b1479fda5ccf3165492b989ebe49985fa30661ed4f1ea208fa2a110e
   openvswitch_vswitchd: ghcr.io/vexxhost/atmosphere/openvswitch:3.1.0-65@sha256:c68347b6b1479fda5ccf3165492b989ebe49985fa30661ed4f1ea208fa2a110e
   ovn_controller: ghcr.io/vexxhost/atmosphere/ovn-host:23.03.0-69@sha256:03b4174e347d14e370aff7399a34f5fcbab1176dcf72c22ffbb0e8c1f66628a6
@@ -164,8 +164,8 @@
   percona_xtradb_cluster_operator: docker.io/percona/percona-xtradb-cluster-operator:1.13.0@sha256:c674d63242f1af521edfbaffae2ae02fb8d010c0557a67a9c42d2b4a50db5243
   percona_xtradb_cluster: docker.io/percona/percona-xtradb-cluster:8.0.32-24.2@sha256:1f978ab8912e1b5fc66570529cb7e7a4ec6a38adbfce1ece78159b0fcfa7d47a
   percona_version_service: docker.io/perconalab/version-service:main-3325140@sha256:b7928130fca1e35ce7feaeec326fef836229a8b4de2f6f6ea5b6d2c7a48cd071
-  placement_db_sync: ghcr.io/vexxhost/atmosphere/placement:zed@sha256:015e7b312d7efee6db95e6942d76ca7941e7a13473410c353913fb7d244d79fd
-  placement: ghcr.io/vexxhost/atmosphere/placement:zed@sha256:015e7b312d7efee6db95e6942d76ca7941e7a13473410c353913fb7d244d79fd
+  placement_db_sync: ghcr.io/vexxhost/atmosphere/placement:2023.2@sha256:439732fb29bdda398de9889e06a0e6052f13e2f9f5a9ccd115fc19c77cebe03c
+  placement: ghcr.io/vexxhost/atmosphere/placement:2023.2@sha256:439732fb29bdda398de9889e06a0e6052f13e2f9f5a9ccd115fc19c77cebe03c
   prometheus_config_reloader: quay.io/prometheus-operator/prometheus-config-reloader:v0.67.1@sha256:0fe3cf36985e0e524801a0393f88fa4b5dd5ffdf0f091ff78ee02f2d281631b5
   prometheus_ipmi_exporter: us-docker.pkg.dev/vexxhost-infra/openstack/ipmi-exporter:1.4.0@sha256:4898da9cc11961a56363e8b3f3437d0f45b46585b20c79e33e97fbe7232e05d2
   prometheus_memcached_exporter: quay.io/prometheus/memcached-exporter:v0.10.0@sha256:fa5a2de1a4744da66fb369bee81232f5ea52208bc643e409a60f66d699ac27b2
@@ -182,12 +182,12 @@
   rabbitmq_server: docker.io/library/rabbitmq:3.10.2-management@sha256:350ab6d773e3af45183466488fe3259df36cd6ade437b4366a59e8052458cc3a
   rabbitmq_topology_operator: docker.io/rabbitmqoperator/messaging-topology-operator:1.6.0@sha256:5052e8bdb26996c62315f0707c6fb291fd84492e360cca7351e2c3fdf659be43
   rook_ceph: docker.io/rook/ceph:v1.10.10@sha256:9ae0eca578ef6e38492e5f90073050491382d8772914ddb8ffe4fca8d365b850
-  senlin_api: ghcr.io/vexxhost/atmosphere/senlin:zed@sha256:df168fbf7dce6f4bc6de334b596d832237a4a4f3e6da235f5929282142b35112
-  senlin_conductor: ghcr.io/vexxhost/atmosphere/senlin:zed@sha256:df168fbf7dce6f4bc6de334b596d832237a4a4f3e6da235f5929282142b35112
-  senlin_db_sync: ghcr.io/vexxhost/atmosphere/senlin:zed@sha256:df168fbf7dce6f4bc6de334b596d832237a4a4f3e6da235f5929282142b35112
-  senlin_engine_cleaner: ghcr.io/vexxhost/atmosphere/senlin:zed@sha256:df168fbf7dce6f4bc6de334b596d832237a4a4f3e6da235f5929282142b35112
-  senlin_engine: ghcr.io/vexxhost/atmosphere/senlin:zed@sha256:df168fbf7dce6f4bc6de334b596d832237a4a4f3e6da235f5929282142b35112
-  senlin_health_manager: ghcr.io/vexxhost/atmosphere/senlin:zed@sha256:df168fbf7dce6f4bc6de334b596d832237a4a4f3e6da235f5929282142b35112
+  senlin_api: ghcr.io/vexxhost/atmosphere/senlin:2023.2@sha256:f718174547f3f66e5af758a10c87f7a75e9cbe1fe277cef27130fd512f727401
+  senlin_conductor: ghcr.io/vexxhost/atmosphere/senlin:2023.2@sha256:f718174547f3f66e5af758a10c87f7a75e9cbe1fe277cef27130fd512f727401
+  senlin_db_sync: ghcr.io/vexxhost/atmosphere/senlin:2023.2@sha256:f718174547f3f66e5af758a10c87f7a75e9cbe1fe277cef27130fd512f727401
+  senlin_engine_cleaner: ghcr.io/vexxhost/atmosphere/senlin:2023.2@sha256:f718174547f3f66e5af758a10c87f7a75e9cbe1fe277cef27130fd512f727401
+  senlin_engine: ghcr.io/vexxhost/atmosphere/senlin:2023.2@sha256:f718174547f3f66e5af758a10c87f7a75e9cbe1fe277cef27130fd512f727401
+  senlin_health_manager: ghcr.io/vexxhost/atmosphere/senlin:2023.2@sha256:f718174547f3f66e5af758a10c87f7a75e9cbe1fe277cef27130fd512f727401
   staffeln_db_sync: ghcr.io/vexxhost/staffeln:v2.2.3@sha256:ee3d8ab2c17d21b4a64a48abfb089df98700b6bc7cee5db36b5ef9c357317736
   staffeln_conductor: ghcr.io/vexxhost/staffeln:v2.2.3@sha256:ee3d8ab2c17d21b4a64a48abfb089df98700b6bc7cee5db36b5ef9c357317736
   staffeln_api: ghcr.io/vexxhost/staffeln:v2.2.3@sha256:ee3d8ab2c17d21b4a64a48abfb089df98700b6bc7cee5db36b5ef9c357317736