chore: Switch to new images (#830)

Co-authored-by: Oleksandr K <okozachenko@vexxhost.com>
Co-authored-by: okozachenko1203 <okozachenko1203@users.noreply.github.com>
diff --git a/images/Earthfile b/images/Earthfile
index b19d68b..4c0e423 100644
--- a/images/Earthfile
+++ b/images/Earthfile
@@ -17,3 +17,18 @@
     useradd -u 42424 -g 42424 -M -d /var/lib/${PROJECT} -s /usr/sbin/nologin -c "${PROJECT} User" ${PROJECT} && \
     mkdir -p /etc/${PROJECT} /var/log/${PROJECT} /var/lib/${PROJECT} /var/cache/${PROJECT} && \
     chown -Rv ${PROJECT}:${PROJECT} /etc/${PROJECT} /var/log/${PROJECT} /var/lib/${PROJECT} /var/cache/${PROJECT}
+
+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
diff --git a/images/barbican/Earthfile b/images/barbican/Earthfile
index 3b37bea..af81b11 100644
--- a/images/barbican/Earthfile
+++ b/images/barbican/Earthfile
@@ -4,8 +4,12 @@
   ARG PROJECT=barbican
   ARG RELEASE=zed
   ARG REF=7d6749fcb1ad16a3350de82cd8e523d5b55306f8
-  ARG PIP_PACKAGES="pykmip"
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF} --PIP_PACKAGES "${PIP_PACKAGES}"
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF} \
+    --PIP_PACKAGES "pykmip"
+  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
diff --git a/images/cinder/Earthfile b/images/cinder/Earthfile
index 1b85d72..62bb8e3 100644
--- a/images/cinder/Earthfile
+++ b/images/cinder/Earthfile
@@ -4,11 +4,15 @@
   ARG PROJECT=cinder
   ARG RELEASE=zed
   ARG REF=002abc4ba004d0dc4fc8327751afec9cc7e5a326
-  ARG PIP_PACKAGES="purestorage"
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF} --PIP_PACKAGES "${PIP_PACKAGES}"
+  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
   COPY ../kubernetes+image/kubectl /usr/local/bin/kubectl
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
diff --git a/images/cloud-archive-base/Earthfile b/images/cloud-archive-base/Earthfile
index 75e2c73..ab0f46a 100644
--- a/images/cloud-archive-base/Earthfile
+++ b/images/cloud-archive-base/Earthfile
@@ -2,7 +2,7 @@
 
 image:
   FROM ../base+image
-  DO ../+APT_INSTALL --PACKAGES "ca-certificates lsb-release python3-distutils sudo ubuntu-cloud-keyring"
+  DO ../+APT_INSTALL --PACKAGES "ca-certificates libpython3.10 lsb-release python3-distutils sudo ubuntu-cloud-keyring"
   ARG RELEASE
   IF [ "$(lsb_release -sc)" = "jammy" ]
     IF [ "${RELEASE}" = "yoga" ]
diff --git a/images/cloud-archive-builder/Earthfile b/images/cloud-archive-builder/Earthfile
deleted file mode 100644
index 1d9bfa1..0000000
--- a/images/cloud-archive-builder/Earthfile
+++ /dev/null
@@ -1,41 +0,0 @@
-VERSION 0.7
-
-image:
-  ARG RELEASE
-  FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
-  DO ../+APT_INSTALL --PACKAGES "\
-    build-essential \
-    curl \
-    git \
-    libssl-dev \
-    libpcre3-dev \
-    lsb-release \
-    openssh-client \
-    python3 \
-    python3-dev \
-    python3-pip \
-    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
-  # TODO(mnaser): patches
-  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 \
-      uwsgi \
-      /src${EXTRAS} \
-      ${PIP_PACKAGES}
-  SAVE ARTIFACT /var/lib/openstack venv
diff --git a/images/cluster-api-provider-openstack/Earthfile b/images/cluster-api-provider-openstack/Earthfile
index 7a03308..92c0b2e 100644
--- a/images/cluster-api-provider-openstack/Earthfile
+++ b/images/cluster-api-provider-openstack/Earthfile
@@ -14,4 +14,4 @@
 image:
   FROM DOCKERFILE -f +clone/src/Dockerfile +clone/src/*
   LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
-  SAVE IMAGE --push ghcr.io/vexxhost/atmosphere/cluster-api-provider-openstack:${CAPO_VERSION}-${EPOCH}
+  SAVE IMAGE --push ghcr.io/vexxhost/atmosphere/capi-openstack-controller:${CAPO_VERSION}-${EPOCH}
diff --git a/images/designate/Earthfile b/images/designate/Earthfile
index a26180b..50527b5 100644
--- a/images/designate/Earthfile
+++ b/images/designate/Earthfile
@@ -4,10 +4,14 @@
   ARG PROJECT=designate
   ARG RELEASE=zed
   ARG REF=4012425843529b02486c39421dc7593fe1803367
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF}
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF}
   DO \
     ../+APT_INSTALL \
     --PACKAGES "bind9utils"
+  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
diff --git a/images/glance/Earthfile b/images/glance/Earthfile
index 55d6169..8e203c9 100644
--- a/images/glance/Earthfile
+++ b/images/glance/Earthfile
@@ -4,11 +4,15 @@
   ARG PROJECT=glance
   ARG RELEASE=zed
   ARG REF=06a18202ab52c64803f044b8f848ed1c160905d2
-  ARG PIP_PACKAGES="glance_store[cinder]"
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF} --PIP_PACKAGES "${PIP_PACKAGES}"
+  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
   COPY ../kubernetes+image/kubectl /usr/local/bin/kubectl
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
diff --git a/images/heat/Earthfile b/images/heat/Earthfile
index 4fde452..87ee43b 100644
--- a/images/heat/Earthfile
+++ b/images/heat/Earthfile
@@ -4,10 +4,14 @@
   ARG PROJECT=heat
   ARG RELEASE=zed
   ARG REF=a2b70a93658ecd2774f22c63a394c5629aefdbe7
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF}
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF}
   DO \
     ../+APT_INSTALL \
-    --PACKAGES "curl"
+    --PACKAGES "curl jq"
+  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
diff --git a/images/horizon/Earthfile b/images/horizon/Earthfile
new file mode 100644
index 0000000..6ff9199
--- /dev/null
+++ b/images/horizon/Earthfile
@@ -0,0 +1,18 @@
+VERSION 0.7
+
+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 \
+    --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}
diff --git a/images/horizon/patches/0000-fix-ignore-errors-when-flavors-are-deleted.patch b/images/horizon/patches/0000-fix-ignore-errors-when-flavors-are-deleted.patch
new file mode 100644
index 0000000..50d68c9
--- /dev/null
+++ b/images/horizon/patches/0000-fix-ignore-errors-when-flavors-are-deleted.patch
@@ -0,0 +1,107 @@
+From c62527488bfeab588c4abbc8426688e4feef87a4 Mon Sep 17 00:00:00 2001
+From: okozachenko <okozachenko1203@gmail.com>
+Date: Thu, 2 Nov 2023 01:27:20 +1100
+Subject: [PATCH] fix: ignore errors when flavors are deleted
+
+The code used to list flavors when in the admin
+or project side was not consistent and raised
+alerts if viewing in the admin side but not in the
+project side.
+
+This patch moves their behaviour to be consistent
+and refactors the code to use the same code-base.
+
+Closes-Bug: #2042362
+Change-Id: I37cc02102285b1e83ec1343b710a57fb5ac4ba15
+(cherry picked from commit 40759aa9cdb9b2162b3f50df751c500db94943b3)
+---
+ .../dashboards/admin/instances/tests.py         |  4 ----
+ .../dashboards/admin/instances/views.py         | 17 +++++------------
+ .../dashboards/project/instances/tests.py       |  1 +
+ .../dashboards/project/instances/views.py       | 11 +++--------
+ 4 files changed, 9 insertions(+), 24 deletions(-)
+
+diff --git a/openstack_dashboard/dashboards/admin/instances/tests.py b/openstack_dashboard/dashboards/admin/instances/tests.py
+index 3630cb79ade..c6cf65e5dab 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):
+         res = self.client.get(INDEX_URL)
+         instances = res.context['table'].data
+         self.assertTemplateUsed(res, INDEX_TEMPLATE)
+-        # Since error messages produced for each instance are identical,
+-        # there will be only one error message for all instances
+-        # (messages de-duplication).
+-        self.assertMessageCount(res, error=1)
+         self.assertCountEqual(instances, servers)
+ 
+         self.assertEqual(self.mock_image_list_detailed.call_count, 4)
+diff --git a/openstack_dashboard/dashboards/admin/instances/views.py b/openstack_dashboard/dashboards/admin/instances/views.py
+index c35527fe465..efa28dd763e 100644
+--- a/openstack_dashboard/dashboards/admin/instances/views.py
++++ b/openstack_dashboard/dashboards/admin/instances/views.py
+@@ -33,6 +33,8 @@
+ from openstack_dashboard.dashboards.admin.instances \
+     import tables as project_tables
+ from openstack_dashboard.dashboards.admin.instances import tabs
++from openstack_dashboard.dashboards.project.instances \
++    import utils as instance_utils
+ from openstack_dashboard.dashboards.project.instances import views
+ from openstack_dashboard.dashboards.project.instances.workflows \
+     import update_instance
+@@ -215,18 +217,9 @@ def get_data(self):
+                 else:
+                     inst.image['name'] = _("-")
+ 
+-            flavor_id = inst.flavor["id"]
+-            try:
+-                if flavor_id in flavor_dict:
+-                    inst.full_flavor = flavor_dict[flavor_id]
+-                else:
+-                    # If the flavor_id is not in flavor_dict list,
+-                    # gets it via nova api.
+-                    inst.full_flavor = api.nova.flavor_get(
+-                        self.request, flavor_id)
+-            except Exception:
+-                msg = _('Unable to retrieve instance size information.')
+-                exceptions.handle(self.request, msg)
++            inst.full_flavor = instance_utils.resolve_flavor(self.request,
++                                                             inst, flavor_dict)
++
+             tenant = tenant_dict.get(inst.tenant_id, None)
+             inst.tenant_name = getattr(tenant, "name", None)
+         return instances
+diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
+index 70d32bc4b3c..c44dedd5b5a 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):
+         self.mock_is_feature_available.return_value = True
+         self.mock_server_list_paged.return_value = [servers, False, False]
+         self.mock_servers_update_addresses.return_value = None
++        self.mock_flavor_get.side_effect = self.exceptions.nova
+         self.mock_flavor_list.side_effect = self.exceptions.nova
+         self.mock_image_list_detailed.return_value = (self.images.list(),
+                                                       False, False)
+diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py
+index badf540b830..b848f6fffd9 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):
+         for instance in instances:
+             self._populate_image_info(instance, image_dict, volume_dict)
+ 
+-            flavor_id = instance.flavor["id"]
+-            if flavor_id in flavor_dict:
+-                instance.full_flavor = flavor_dict[flavor_id]
+-            else:
+-                # If the flavor_id is not in flavor_dict,
+-                # put info in the log file.
+-                LOG.info('Unable to retrieve flavor "%s" for instance "%s".',
+-                         flavor_id, instance.id)
++            instance.full_flavor = instance_utils.resolve_flavor(self.request,
++                                                                 instance,
++                                                                 flavor_dict)
+ 
+         return instances
+ 
diff --git a/images/horizon/patches/0001-fix-disable-resizing-for-admins.patch b/images/horizon/patches/0001-fix-disable-resizing-for-admins.patch
new file mode 100644
index 0000000..aaa5058
--- /dev/null
+++ b/images/horizon/patches/0001-fix-disable-resizing-for-admins.patch
@@ -0,0 +1,112 @@
+From d3ac70fb12dc363a0fbed39bcfd3642e36f4515d 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
+
+By default, admins see all clusters and they are allowed to do all
+actions however the resize function will not work so we're displaying
+something for admins that they can't use.
+
+This will hide the resize button for clusters that don't match the
+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(-)
+
+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
+--- 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 @@
+   resizeService.$inject = [
+     '$rootScope',
+     '$q',
++    'horizon.app.core.openstack-service-api.userSession',
+     'horizon.app.core.openstack-service-api.magnum',
+     'horizon.framework.util.actions.action-result.service',
+     'horizon.framework.util.i18n.gettext',
+@@ -43,8 +44,8 @@
+   ];
+ 
+   function resizeService(
+-    $rootScope, $q, magnum, actionResult, gettext, $qExtensions, modal, toast, spinnerModal,
+-    resourceType
++    $rootScope, $q, userSession, magnum, actionResult, gettext, $qExtensions,
++    modal, toast, spinnerModal, resourceType
+   ) {
+ 
+     var modalConfig, formModel;
+@@ -87,8 +88,8 @@
+       return deferred.promise;
+     }
+ 
+-    function allowed() {
+-      return $qExtensions.booleanAsPromise(true);
++    function allowed(selected) {
++      return userSession.isCurrentProject(selected.project_id);
+     }
+ 
+     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
+--- 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 @@
+ 
+   describe('horizon.dashboard.container-infra.clusters.resize.service', function() {
+ 
+-    var service, $scope, $q, deferred, magnum, spinnerModal, modalConfig;
++    var service, $scope, $q, deferred, magnum, spinnerModal, modalConfig, userSession;
+     var selected = {
+-      id: 1
++      id: 1,
++      project_id: "f5ed2d21437644adb2669f9ade9c949b"
+     };
+     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');
+       spinnerModal = $injector.get('horizon.framework.widgets.modal-wait-spinner.service');
++      userSession = $injector.get('horizon.app.core.openstack-service-api.userSession');
+ 
+       spyOn(spinnerModal, 'showModalSpinner').and.callFake(function() {});
+       spyOn(spinnerModal, 'hideModalSpinner').and.callFake(function() {});
+@@ -60,9 +62,22 @@
+       spyOn(modal, 'open').and.callThrough();
+     }));
+ 
+-    it('should check the policy if the user is allowed to update cluster', function() {
+-      var allowed = service.allowed();
+-      expect(allowed).toBeTruthy();
++    it('should allow user to resize cluster if they are in the same project', async function() {
++      spyOn(userSession, 'get').and.returnValue({project_id: selected.project_id});
++
++      await service.allowed(selected);
++    });
++
++    it('should not allow user to resize cluster if they are in a different project', async function() {
++      spyOn(userSession, 'get').and.returnValue({project_id: 'different_project'});
++
++      try {
++        await service.allowed(selected);
++      } catch (err) {
++        return;
++      }
++
++      throw new Error('User should not be allowed to resize cluster');
+     });
+ 
+     it('should open the modal, hide the loading spinner and check the form model',
diff --git a/images/horizon/patches/0002-capi-avoid-going-through-heat-for-worker-list.patch b/images/horizon/patches/0002-capi-avoid-going-through-heat-for-worker-list.patch
new file mode 100644
index 0000000..5970ef2
--- /dev/null
+++ b/images/horizon/patches/0002-capi-avoid-going-through-heat-for-worker-list.patch
@@ -0,0 +1,68 @@
+From 6ecbb870f24f5c5c4a5b548166ac292801adda84 Mon Sep 17 00:00:00 2001
+From: Mohammed Naser <mnaser@vexxhost.com>
+Date: Sun, 19 Feb 2023 21:39:46 +0000
+Subject: [PATCH] [capi] Avoid going through Heat for worker list
+
+By default, Magnum UI goes through Heat to get the list of nodes
+which is not correct since it's making an assumption that Heat
+is always in use.
+
+The fix for this would be to make sure that Magnum has a list of
+all the VMs in it's database (or some sort of API call that
+returns them all from the driver) but that's quite a big amount
+of work to implement for now.
+
+So for now, if stack_id doesn't look like a UUID, we assume it
+is deployed using Clsuter API driver for Magnum and look up with
+that alternative method instead.
+
+(cherry picked from commit 6f31cc5cacf23398b76392922ee9863d50aa9e7e)
+(cherry picked from commit d44f16f13a89d7fb00d3d949a392d638ce2d0cc8)
+(cherry picked from commit 72122e350429590e9002058e7e35c4dcc94d2d4f)
+---
+ magnum_ui/api/rest/magnum.py | 18 ++++++++++++++++++
+ 1 file changed, 19 insertions(+)
+
+diff --git a/magnum_ui/api/rest/magnum.py b/magnum_ui/api/rest/magnum.py
+index ba66e0e..bf331bc 100644
+--- a/magnum_ui/api/rest/magnum.py
++++ b/magnum_ui/api/rest/magnum.py
+@@ -17,6 +17,8 @@
+ 
+ from collections import defaultdict
+ 
++from oslo_utils import uuidutils
++
+ from django.conf import settings
+ from django.http import HttpResponse
+ from django.http import HttpResponseNotFound
+@@ -228,6 +230,19 @@ class ClusterResize(generic.View):
+ 
+     url_regex = r'container_infra/clusters/(?P<cluster_id>[^/]+)/resize$'
+ 
++    def _cluster_api_resize_get(self, request, cluster):
++        search_opts = {"name": "%s-" % cluster["stack_id"]}
++        servers = api.nova.server_list(request, search_opts=search_opts)[0]
++
++        worker_nodes = []
++        for server in servers:
++            control_plane_prefix = "%s-control-plane" % cluster["stack_id"]
++            if not server.name.startswith(control_plane_prefix):
++                worker_nodes.append({"name": server.name, "id": server.id})
++
++        return {"cluster": change_to_id(cluster),
++                "worker_nodes": worker_nodes}
++
+     @rest_utils.ajax()
+     def get(self, request, cluster_id):
+         """Get cluster details for resize"""
+@@ -237,6 +252,9 @@ def get(self, request, cluster_id):
+             print(e)
+             return HttpResponseNotFound()
+ 
++        if not uuidutils.is_uuid_like(cluster["stack_id"]):
++            return self._cluster_api_resize_get(request, cluster)
++
+         stack = heat.stack_get(request, cluster["stack_id"])
+         search_opts = {"name": "%s-" % stack.stack_name}
+         servers = api.nova.server_list(request, search_opts=search_opts)[0]
diff --git a/images/ironic/Earthfile b/images/ironic/Earthfile
new file mode 100644
index 0000000..74b2ed9
--- /dev/null
+++ b/images/ironic/Earthfile
@@ -0,0 +1,18 @@
+VERSION 0.7
+
+image:
+  ARG PROJECT=ironic
+  ARG RELEASE=zed
+  ARG REF=e38735cb95263b0c54f2fd719ff6b714efbddbb3
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF} \
+    --PIP_PACKAGES "python-dracclient sushy"
+  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}
diff --git a/images/keystone/Earthfile b/images/keystone/Earthfile
new file mode 100644
index 0000000..9478a03
--- /dev/null
+++ b/images/keystone/Earthfile
@@ -0,0 +1,30 @@
+VERSION 0.7
+
+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 \
+    --PACKAGES "apache2 libapache2-mod-wsgi-py3"
+  DO ../+APPLY_PATCHES
+  ARG MOD_AUTH_OPENIDC_VERSION=2.4.12.1
+  ARG TARGETARCH
+  RUN \
+    apt-get update && \
+    apt-get install -y --no-install-recommends curl && \
+    curl -LO https://github.com/OpenIDC/mod_auth_openidc/releases/download/v${MOD_AUTH_OPENIDC_VERSION}/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb && \
+    apt-get install -y --no-install-recommends ./libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb && \
+    a2enmod auth_openidc && \
+    apt-get purge -y --auto-remove curl && \
+    apt-get clean && \
+    rm -rfv /var/lib/apt/lists/* libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.$(lsb_release -sc)_${TARGETARCH}.deb
+  SAVE IMAGE --push \
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
diff --git a/images/kubernetes-entrypoint/Earthfile b/images/kubernetes-entrypoint/Earthfile
new file mode 100644
index 0000000..2191e77
--- /dev/null
+++ b/images/kubernetes-entrypoint/Earthfile
@@ -0,0 +1,32 @@
+VERSION 0.7
+ARG --global COMMIT=e8c2b17e1261c6a1b0fed1fcd5e1c337fc014219
+
+build:
+  FROM golang:1.21.5-bookworm
+  DO ../+APT_INSTALL --PACKAGES "patch"
+  GIT CLONE --branch ${COMMIT} https://opendev.org/airship/kubernetes-entrypoint /src
+  WORKDIR /src
+  RUN \
+    curl https://review.opendev.org/changes/airship%2Fkubernetes-entrypoint~904537/revisions/1/patch?download | \
+    base64 --decode | \
+    patch -p1
+  ARG GOARCH
+  RUN \
+    --mount=type=cache,mode=0755,target=/go/pkg/mod \
+    CGO_ENABLED=0 GOOS=linux go build -o /main
+  SAVE ARTIFACT /main
+
+platform-image:
+  FROM scratch
+  ARG TARGETARCH
+  COPY \
+    --platform=linux/amd64 \
+    (+build/main --GOARCH=$TARGETARCH) /kubernetes-entrypoint
+  USER 65534
+  ENTRYPOINT ["/kubernetes-entrypoint"]
+  SAVE IMAGE --push \
+    ghcr.io/vexxhost/atmosphere/kubernetes-entrypoint:${COMMIT} \
+    ghcr.io/vexxhost/atmosphere/kubernetes-entrypoint:latest
+
+image:
+  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/kubernetes/Earthfile b/images/kubernetes/Earthfile
index bf613b2..4a11937 100644
--- a/images/kubernetes/Earthfile
+++ b/images/kubernetes/Earthfile
@@ -2,6 +2,8 @@
 
 image:
   FROM curlimages/curl:7.78.0
+  ARG TARGETOS
+  ARG TARGETARCH
   RUN curl -L "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/${TARGETOS}/${TARGETARCH}/kubectl" -o /tmp/kubectl
-  RUN chmod +x /tmp/kubectl
+  RUN chmod +x /tmp/kubectl && /tmp/kubectl version --client=true
   SAVE ARTIFACT /tmp/kubectl kubectl
diff --git a/images/magnum/Earthfile b/images/magnum/Earthfile
new file mode 100644
index 0000000..f7e189f
--- /dev/null
+++ b/images/magnum/Earthfile
@@ -0,0 +1,18 @@
+VERSION 0.7
+
+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 \
+    --PACKAGES "haproxy"
+  DO ../+APPLY_PATCHES
+  SAVE IMAGE --push \
+    ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
+    ghcr.io/vexxhost/atmosphere/${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
new file mode 100644
index 0000000..8d7c411
--- /dev/null
+++ b/images/magnum/patches/0000-containerd-cni-plugin-path-in-coreos-35.patch
@@ -0,0 +1,35 @@
+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/0001-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch b/images/magnum/patches/0001-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch
new file mode 100644
index 0000000..9bee808
--- /dev/null
+++ b/images/magnum/patches/0001-update-chart-metadata-version-to-reflect-breaking-change-in-helm-v3-5-2.patch
@@ -0,0 +1,28 @@
+From 61592d46e7fc5644c4b5148c7ca6bf767131e504 Mon Sep 17 00:00:00 2001
+From: okozachenko1203 <okozachenko1203@gmail.com>
+Date: Fri, 31 Mar 2023 23:41:43 +1100
+Subject: [PATCH] Update chart.metadata.version to reflect breaking change in
+ helm v3.5.2
+
+https: //github.com/helm/helm/issues/9342
+Change-Id: I1dbe7b0b85380e713ebb5dcdd7ecbfc6a438b852
+(cherry picked from commit ebee3263b6b3d3fa213ea8f837911b89785a4700)
+---
+ .../templates/kubernetes/fragments/install-helm-modules.sh    | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh b/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh
+index 475e8dbf6c..a0b3f4bc75 100644
+--- a/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh
++++ b/magnum/drivers/common/templates/kubernetes/fragments/install-helm-modules.sh
+@@ -72,8 +72,8 @@ else
+         cat << EOF > Chart.yaml
+ apiVersion: v1
+ name: magnum
+-version: metachart
+-appVersion: metachart
++version: 1.0.0
++appVersion: v1.0.0
+ description: Magnum Helm Charts
+ EOF
+         sed -i '1i\dependencies:' requirements.yaml
diff --git a/images/magnum/patches/0002-support-k8s-1-24.patch b/images/magnum/patches/0002-support-k8s-1-24.patch
new file mode 100644
index 0000000..bc69c96
--- /dev/null
+++ b/images/magnum/patches/0002-support-k8s-1-24.patch
@@ -0,0 +1,75 @@
+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
new file mode 100644
index 0000000..a79d935
--- /dev/null
+++ b/images/magnum/patches/0003-fix-kubelet-for-fedora-coreos-36-to-provide-real-resolvconf-to-containers.patch
@@ -0,0 +1,48 @@
+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
new file mode 100644
index 0000000..7d302cf
--- /dev/null
+++ b/images/magnum/patches/0004-adapt-cinder-csi-to-upstream-manifest.patch
@@ -0,0 +1,860 @@
+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
new file mode 100644
index 0000000..4f4ea6e
--- /dev/null
+++ b/images/magnum/patches/0005-secure-rbac.patch
@@ -0,0 +1,1969 @@
+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/manila/Earthfile b/images/manila/Earthfile
new file mode 100644
index 0000000..320fc8f
--- /dev/null
+++ b/images/manila/Earthfile
@@ -0,0 +1,17 @@
+VERSION 0.7
+
+image:
+  ARG PROJECT=manila
+  ARG RELEASE=zed
+  ARG REF=9ea49e2b9df7da16d5700810eee18710dc90e6a4
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF}
+  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}
diff --git a/images/neutron/Earthfile b/images/neutron/Earthfile
new file mode 100644
index 0000000..e8fbf6d
--- /dev/null
+++ b/images/neutron/Earthfile
@@ -0,0 +1,21 @@
+VERSION 0.7
+
+platform-image:
+  ARG PROJECT=neutron
+  ARG RELEASE=zed
+  ARG REF=4575136fe99f67dc140987601c90493cf62c0330
+  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 \
+    --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}
+
+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/0000-fix-netns-deletion-of-broken-namespaces.patch
new file mode 100644
index 0000000..a1540ad
--- /dev/null
+++ b/images/neutron/patches/0000-fix-netns-deletion-of-broken-namespaces.patch
@@ -0,0 +1,138 @@
+From f8130f36e3cdb67fd9be64a61ac22b487200e2bc Mon Sep 17 00:00:00 2001
+From: Felix Huettner <felix.huettner@mail.schwarz>
+Date: Fri, 22 Sep 2023 16:25:10 +0200
+Subject: [PATCH] fix netns deletion of broken namespaces
+
+normal network namespaces are bind-mounted to files under
+/var/run/netns. If a process deleting a network namespace gets killed
+during that operation there is the chance that the bind mount to the
+netns has been removed, but the file under /var/run/netns still exists.
+
+When the neutron-ovn-metadata-agent tries to clean up such network
+namespaces it first tires to validate that the network namespace is
+empty. For the cases described above this fails, as this network
+namespace no longer really exists, but is just a stray file laying
+around.
+
+To fix this we treat network namespaces where we get an `OSError` with
+errno 22 (Invalid Argument) as empty. The calls to pyroute2 to delete
+the namespace will then clean up the file.
+
+Additionally we add a guard to teardown_datapath to continue even if
+this fails. failing to remove a datapath is not critical and leaves in
+the worst case a process and a network namespace running, however
+previously it would have also prevented the creation of new datapaths
+which is critical for VM startup.
+
+Closes-Bug: #2037102
+Change-Id: I7c43812fed5903f98a2e491076c24a8d926a59b4
+---
+
+diff --git a/neutron/agent/linux/ip_lib.py b/neutron/agent/linux/ip_lib.py
+index 9953729..196dc17 100644
+--- a/neutron/agent/linux/ip_lib.py
++++ b/neutron/agent/linux/ip_lib.py
+@@ -259,7 +259,22 @@
+         return ip
+ 
+     def namespace_is_empty(self):
+-        return not self.get_devices()
++        try:
++            return not self.get_devices()
++        except OSError as e:
++            # This can happen if we previously got terminated in the middle of
++            # removing this namespace. In this case the bind mount of the
++            # namespace under /var/run/netns will be removed, but the namespace
++            # file is still there. As the bind mount is gone we can no longer
++            # access the namespace to validate that it is empty. But since it
++            # should have already been removed we are sure that the check has
++            # passed the last time and since the namespace is unuseable that
++            # can not have changed.
++            # Future calls to pyroute2 to remove that namespace will clean up
++            # the leftover file.
++            if e.errno == errno.EINVAL:
++                return True
++            raise e
+ 
+     def garbage_collect_namespace(self):
+         """Conditionally destroy the namespace if it is empty."""
+diff --git a/neutron/agent/ovn/metadata/agent.py b/neutron/agent/ovn/metadata/agent.py
+index 7a09145..888ab15 100644
+--- a/neutron/agent/ovn/metadata/agent.py
++++ b/neutron/agent/ovn/metadata/agent.py
+@@ -478,7 +478,10 @@
+                              ns.startswith(NS_PREFIX) and
+                              ns not in metadata_namespaces]
+         for ns in unused_namespaces:
+-            self.teardown_datapath(self._get_datapath_name(ns))
++            try:
++                self.teardown_datapath(self._get_datapath_name(ns))
++            except Exception:
++                LOG.exception('Error unable to destroy namespace: %s', ns)
+ 
+         # resync all network namespaces based on the associated datapaths,
+         # even those that are already running. This is to make sure
+diff --git a/neutron/tests/unit/agent/linux/test_ip_lib.py b/neutron/tests/unit/agent/linux/test_ip_lib.py
+index c488e90..3956142 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 @@
+                 self.assertNotIn(mock.call().delete('ns'),
+                                  ip_ns_cmd_cls.mock_calls)
+ 
++    def test_garbage_collect_namespace_existing_broken(self):
++        with mock.patch.object(ip_lib, 'IpNetnsCommand') as ip_ns_cmd_cls:
++            ip_ns_cmd_cls.return_value.exists.return_value = True
++
++            ip = ip_lib.IPWrapper(namespace='ns')
++
++            with mock.patch.object(ip, 'get_devices',
++                                   side_effect=OSError(errno.EINVAL, None)
++                                   ) as mock_get_devices:
++                self.assertTrue(ip.garbage_collect_namespace())
++
++                mock_get_devices.assert_called_once_with()
++                expected = [mock.call(ip),
++                            mock.call().exists('ns'),
++                            mock.call().delete('ns')]
++                self.assertEqual(ip_ns_cmd_cls.mock_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
+--- a/neutron/tests/unit/agent/ovn/metadata/test_agent.py
++++ b/neutron/tests/unit/agent/ovn/metadata/test_agent.py
+@@ -138,6 +138,31 @@
+             lnn.assert_called_once_with()
+             tdp.assert_called_once_with('3')
+ 
++    def test_sync_teardown_namespace_does_not_crash_on_error(self):
++        """Test that sync tears down unneeded metadata namespaces.
++        Even if that fails it continues to provision other datapaths
++        """
++        with mock.patch.object(
++                self.agent, 'provision_datapath') as pdp,\
++                mock.patch.object(
++                    ip_lib, 'list_network_namespaces',
++                    return_value=['ovnmeta-1', 'ovnmeta-2', 'ovnmeta-3',
++                                  'ns1', 'ns2']) as lnn,\
++                mock.patch.object(
++                    self.agent, 'teardown_datapath',
++                    side_effect=Exception()) as tdp:
++            self.agent.sync()
++
++            pdp.assert_has_calls(
++                [
++                    mock.call(p.datapath)
++                    for p in self.ports
++                ],
++                any_order=True
++            )
++            lnn.assert_called_once_with()
++            tdp.assert_called_once_with('3')
++
+     def test_get_networks_datapaths(self):
+         """Test get_networks_datapaths returns only datapath objects for the
+         networks containing vif ports of type ''(blank) and 'external'.
diff --git a/images/nova-ssh/Earthfile b/images/nova-ssh/Earthfile
new file mode 100644
index 0000000..9988902
--- /dev/null
+++ b/images/nova-ssh/Earthfile
@@ -0,0 +1,15 @@
+VERSION 0.7
+
+clone:
+  FROM ../builder+image
+  GIT CLONE --branch a2e563b08ae633d75084c1bb40c97dbf1a539950 https://opendev.org/openstack/openstack-helm-images /src
+  WORKDIR /src
+  SAVE ARTIFACT /src
+
+platform-image:
+  FROM DOCKERFILE -f +clone/src/nova-ssh/Dockerfile.ubuntu_focal +clone/src/nova-ssh
+  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
+  SAVE IMAGE --push ghcr.io/vexxhost/atmosphere/nova-ssh:latest
+
+image:
+  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/nova/Earthfile b/images/nova/Earthfile
new file mode 100644
index 0000000..263ae99
--- /dev/null
+++ b/images/nova/Earthfile
@@ -0,0 +1,21 @@
+VERSION 0.7
+
+platform-image:
+  ARG PROJECT=nova
+  ARG RELEASE=zed
+  ARG REF=787839f6637f292fb5656725e5dae12fbe6e3c3e
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF}
+  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}
+
+image:
+  BUILD --platform linux/amd64 --platform linux/arm64 +platform-image
diff --git a/images/octavia/Earthfile b/images/octavia/Earthfile
index 4847ee2..8b3ca58 100644
--- a/images/octavia/Earthfile
+++ b/images/octavia/Earthfile
@@ -4,11 +4,15 @@
   ARG PROJECT=octavia
   ARG RELEASE=zed
   ARG REF=4c97b585ce6e519a5ff090c2cd97fab0e801be0c
-  ARG PIP_PACKAGES="ovn-octavia-provider"
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF} --PIP_PACKAGES "${PIP_PACKAGES}"
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF} \
+    --PIP_PACKAGES "ovn-octavia-provider"
   DO \
     ../+APT_INSTALL \
     --PACKAGES "isc-dhcp-client"
+  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
diff --git a/images/cloud-archive-builder/2023.1/upper-constraints.txt b/images/openstack-service/2023.1/upper-constraints.txt
similarity index 98%
rename from images/cloud-archive-builder/2023.1/upper-constraints.txt
rename to images/openstack-service/2023.1/upper-constraints.txt
index a8eade5..29eab79 100644
--- a/images/cloud-archive-builder/2023.1/upper-constraints.txt
+++ b/images/openstack-service/2023.1/upper-constraints.txt
@@ -53,13 +53,13 @@
 python-mistralclient==5.1.0
 oslo.context==5.3.0
 python-senlinclient==3.1.0
-rcssmin==1.1.2
+rcssmin===1.1.1
 pycadf==3.1.1
 grpcio==1.60.0
 pysendfile==2.0.1
 sniffio==1.3.0
 fixtures==4.1.0
-neutron-lib==3.9.0
+neutron-lib==3.7.0
 XStatic-FileSaver==1.3.2.0
 oslo.metrics==0.7.0
 storage-interfaces==1.0.5
@@ -117,7 +117,6 @@
 requests-unixsocket==0.3.0
 responses==0.24.1
 croniter==1.4.1
-horizon==23.3.0
 octavia-lib==3.4.0
 python-watcherclient==4.2.0
 MarkupSafe==2.1.3
@@ -204,7 +203,6 @@
 python-3parclient==4.2.12
 unittest2==1.1.0
 django-compressor==4.4
-libvirt-python==8.10.0
 python-zunclient==4.7.0
 tzlocal==4.3.1
 sphinxcontrib-jsmath==1.0.1
@@ -331,7 +329,7 @@
 automaton==3.2.0
 os-service-types==1.7.0
 keyring==23.13.1
-elementpath==3.0.2
+elementpath==4.1.5
 testscenarios==0.5.0
 sphinxcontrib-pecanwsme==0.11.0
 sadisplay==0.4.9
@@ -530,7 +528,7 @@
 XStatic-Angular-lrdragndrop==1.0.2.6
 ovsdbapp==2.5.0
 aniso8601==9.0.1
-rjsmin==1.2.2
+rjsmin===1.2.1
 icalendar==4.1.1
 decorator==5.1.1
 DateTimeRange==1.2.0
diff --git a/images/cloud-archive-builder/2023.2/upper-constraints.txt b/images/openstack-service/2023.2/upper-constraints.txt
similarity index 99%
rename from images/cloud-archive-builder/2023.2/upper-constraints.txt
rename to images/openstack-service/2023.2/upper-constraints.txt
index 3cefa30..8b87a9f 100644
--- a/images/cloud-archive-builder/2023.2/upper-constraints.txt
+++ b/images/openstack-service/2023.2/upper-constraints.txt
@@ -52,7 +52,7 @@
 python-mistralclient==5.1.0
 oslo.context==5.3.0
 python-senlinclient==3.1.0
-rcssmin==1.1.2
+rcssmin===1.1.1
 pycadf==3.1.1
 grpcio==1.60.0
 pysendfile==2.0.1
@@ -119,7 +119,6 @@
 requests-unixsocket==0.3.0
 responses==0.24.1
 croniter==1.4.1
-horizon==23.3.0
 octavia-lib==3.4.0
 python-watcherclient==4.2.0
 MarkupSafe==2.1.3
@@ -207,7 +206,6 @@
 python-3parclient==4.2.12
 unittest2==1.1.0
 django-compressor==4.4
-libvirt-python==9.10.0
 python-zunclient==4.7.0
 tzlocal==4.3.1
 sphinxcontrib-jsmath==1.0.1
@@ -539,7 +537,7 @@
 XStatic-Angular-lrdragndrop==1.0.2.6
 ovsdbapp==2.5.0
 aniso8601==9.0.1
-rjsmin==1.2.2
+rjsmin===1.2.1
 icalendar==5.0.11
 decorator==5.1.1
 DateTimeRange==2.2.0
diff --git a/images/openstack-service/Earthfile b/images/openstack-service/Earthfile
index 928694c..20c1f50 100644
--- a/images/openstack-service/Earthfile
+++ b/images/openstack-service/Earthfile
@@ -1,5 +1,50 @@
 VERSION 0.7
 
+build:
+  ARG RELEASE
+  FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
+  DO ../+APT_INSTALL --PACKAGES "\
+    build-essential \
+    curl \
+    git \
+    libldap2-dev \
+    libpcre3-dev \
+    libsasl2-dev \
+    libssl-dev \
+    lsb-release \
+    openssh-client \
+    python3 \
+    python3-dev \
+    python3-pip \
+    python3-venv"
+  RUN --mount type=cache,target=/root/.cache \
+    python3 -m venv --upgrade --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
+
 image:
   ARG RELEASE
   FROM ../cloud-archive-base+image --RELEASE=${RELEASE}
@@ -9,5 +54,6 @@
   DO ../+CREATE_PROJECT_USER --PROJECT=${PROJECT}
   ENV PATH=/var/lib/openstack/bin:$PATH
   COPY \
-    (../cloud-archive-builder+image/venv --RELEASE=${RELEASE} --PROJECT=${PROJECT} --PROJECT_REF=${PROJECT_REF} --PIP_PACKAGES=${PIP_PACKAGES}) \
+    (+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
diff --git a/images/cloud-archive-builder/zed/upper-constraints.txt b/images/openstack-service/zed/upper-constraints.txt
similarity index 98%
rename from images/cloud-archive-builder/zed/upper-constraints.txt
rename to images/openstack-service/zed/upper-constraints.txt
index 3237f89..b580082 100644
--- a/images/cloud-archive-builder/zed/upper-constraints.txt
+++ b/images/openstack-service/zed/upper-constraints.txt
@@ -53,13 +53,13 @@
 python-mistralclient==4.5.0
 oslo.context==5.3.0
 python-senlinclient==2.5.0
-rcssmin==1.1.2
+rcssmin===1.1.1
 pycadf==3.1.1
 grpcio==1.60.0
 pysendfile==2.0.1
 sniffio==1.3.0
 fixtures==4.1.0
-neutron-lib==3.9.0
+neutron-lib==3.7.0
 XStatic-FileSaver==1.3.2.0
 oslo.metrics==0.7.0
 storage-interfaces==1.0.5
@@ -116,7 +116,6 @@
 requests-unixsocket==0.3.0
 responses==0.24.1
 croniter==1.4.1
-horizon==23.3.0
 octavia-lib==3.4.0
 python-watcherclient==4.2.0
 MarkupSafe==2.1.3
@@ -203,7 +202,6 @@
 python-3parclient==4.2.12
 unittest2==1.1.0
 django-compressor==4.4
-libvirt-python==8.10.0
 python-zunclient==4.7.0
 tzlocal==4.3.1
 sphinxcontrib-jsmath==1.0.1
@@ -331,7 +329,7 @@
 automaton==3.2.0
 os-service-types==1.7.0
 keyring==23.13.1
-elementpath==3.0.2
+elementpath==4.1.5
 testscenarios==0.5.0
 sphinxcontrib-pecanwsme==0.11.0
 sadisplay==0.4.9
@@ -531,7 +529,7 @@
 XStatic-Angular-lrdragndrop==1.0.2.6
 ovsdbapp==2.5.0
 aniso8601==9.0.1
-rjsmin==1.2.2
+rjsmin===1.2.1
 icalendar==4.1.1
 decorator==5.1.1
 DateTimeRange==1.2.0
diff --git a/images/placement/Earthfile b/images/placement/Earthfile
index ed72871..81a3369 100644
--- a/images/placement/Earthfile
+++ b/images/placement/Earthfile
@@ -4,7 +4,11 @@
   ARG PROJECT=placement
   ARG RELEASE=zed
   ARG REF=d7ced6bd2fc82caf458f20b5652888164b1bbb70
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF}
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF}
+  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}
diff --git a/images/senlin/Earthfile b/images/senlin/Earthfile
index d2f2480..60da266 100644
--- a/images/senlin/Earthfile
+++ b/images/senlin/Earthfile
@@ -4,7 +4,11 @@
   ARG PROJECT=senlin
   ARG RELEASE=zed
   ARG REF=b6ef17b0f787fb7a0609ba36dc13097882a6a3ff
-  FROM ../openstack-service+image --PROJECT ${PROJECT} --RELEASE ${RELEASE} --PROJECT_REF ${REF}
+  FROM ../openstack-service+image \
+    --PROJECT ${PROJECT} \
+    --RELEASE ${RELEASE} \
+    --PROJECT_REF ${REF}
+  DO ../+APPLY_PATCHES
   SAVE IMAGE --push \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${RELEASE} \
     ghcr.io/vexxhost/atmosphere/${PROJECT}:${REF}