fix: skip port deletion when instances have no port (#779)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b65b5fb..bb25beb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,6 +6,7 @@
     hooks:
       - id: end-of-file-fixer
       - id: trailing-whitespace
+        exclude: ^images/.*/patches/.*\.patch$
 
   - repo: https://github.com/compilerla/conventional-pre-commit
     rev: v2.0.0
diff --git a/Earthfile b/Earthfile
index fa14654..01e536c 100644
--- a/Earthfile
+++ b/Earthfile
@@ -1,12 +1,7 @@
 VERSION --use-copy-link 0.7
-FROM python:3.10
-
-poetry:
-  RUN pip3 install poetry==1.4.2
-  SAVE IMAGE --cache-hint
 
 build.wheels:
-  FROM +poetry
+  FROM ./images/builder+image
   COPY pyproject.toml poetry.lock ./
   ARG --required only
   RUN poetry export --only=${only} -f requirements.txt --without-hashes > requirements.txt
@@ -44,20 +39,18 @@
   SAVE IMAGE --cache-hint
 
 image:
-  FROM python:3.10-slim
+  FROM ./images/base+image
   ENV ANSIBLE_PIPELINING=True
-  RUN \
-    apt-get update && \
-    apt-get install --no-install-recommends -y rsync openssh-client && \
-    apt-get clean && \
-    rm -rf /var/lib/apt/lists/*
-  CMD ["/bin/bash"]
+  DO ./images+APT_INSTALL --packages "rsync openssh-client"
   COPY +build.venv.runtime/venv /venv
   ENV PATH=/venv/bin:$PATH
   COPY +build.collections/ /usr/share/ansible
   ARG tag=latest
   SAVE IMAGE --push ghcr.io/vexxhost/atmosphere:${tag}
 
+images:
+  BUILD ./images/cluster-api-provider-openstack+image
+
 pin-images:
   FROM +build.venv.dev
   COPY roles/defaults/vars/main.yml /defaults.yml
diff --git a/images/Earthfile b/images/Earthfile
new file mode 100644
index 0000000..c87a870
--- /dev/null
+++ b/images/Earthfile
@@ -0,0 +1,10 @@
+VERSION 0.7
+
+APT_INSTALL:
+  COMMAND
+  ARG packages
+  RUN \
+    apt-get update && \
+    apt-get install --no-install-recommends -y ${packages} && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*
diff --git a/images/base/Earthfile b/images/base/Earthfile
new file mode 100644
index 0000000..c075abc
--- /dev/null
+++ b/images/base/Earthfile
@@ -0,0 +1,4 @@
+VERSION 0.7
+
+image:
+  FROM ubuntu:jammy
diff --git a/images/builder/Earthfile b/images/builder/Earthfile
new file mode 100644
index 0000000..b7025b4
--- /dev/null
+++ b/images/builder/Earthfile
@@ -0,0 +1,7 @@
+VERSION 0.7
+
+image:
+  FROM ../base+image
+  DO ../+APT_INSTALL --packages "build-essential git python3-dev python3-pip python3-venv"
+  ARG POETRY_VERSION=1.4.2
+  RUN pip3 install --no-cache-dir poetry==${POETRY_VERSION}
diff --git a/images/cluster-api-provider-openstack/Earthfile b/images/cluster-api-provider-openstack/Earthfile
new file mode 100644
index 0000000..7a03308
--- /dev/null
+++ b/images/cluster-api-provider-openstack/Earthfile
@@ -0,0 +1,17 @@
+VERSION 0.7
+
+ARG --global CAPO_VERSION=v0.8.0
+ARG --global EPOCH=1
+
+clone:
+  FROM ../builder+image
+  GIT CLONE --branch ${CAPO_VERSION} https://github.com/kubernetes-sigs/cluster-api-provider-openstack /workspace/src
+  WORKDIR /workspace/src
+  COPY patches /workspace/patches
+  RUN git apply --verbose /workspace/patches/*.patch
+  SAVE ARTIFACT /workspace/src
+
+image:
+  FROM DOCKERFILE -f +clone/src/Dockerfile +clone/src/*
+  LABEL org.opencontainers.image.source=https://github.com/vexxhost/atmosphere
+  SAVE IMAGE --push ghcr.io/vexxhost/atmosphere/cluster-api-provider-openstack:${CAPO_VERSION}-${EPOCH}
diff --git a/images/cluster-api-provider-openstack/patches/0000-fix-skip-port-deletion-when-instances-have-no-port.patch b/images/cluster-api-provider-openstack/patches/0000-fix-skip-port-deletion-when-instances-have-no-port.patch
new file mode 100644
index 0000000..6174898
--- /dev/null
+++ b/images/cluster-api-provider-openstack/patches/0000-fix-skip-port-deletion-when-instances-have-no-port.patch
@@ -0,0 +1,24 @@
+From 294b2d3ca34f7d327da3b27bd07edde7f5bbac43 Mon Sep 17 00:00:00 2001
+From: okozachenko <okozachenko1203@users.noreply.github.com>
+Date: Tue, 19 Dec 2023 01:07:36 +1100
+Subject: [PATCH] fix: skip port deletion when instances have no port
+
+---
+ pkg/cloud/services/networking/port.go | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/pkg/cloud/services/networking/port.go b/pkg/cloud/services/networking/port.go
+index 4c213851f8..84b9bfc618 100644
+--- a/pkg/cloud/services/networking/port.go
++++ b/pkg/cloud/services/networking/port.go
+@@ -315,6 +315,10 @@ func (s *Service) GarbageCollectErrorInstancesPort(eventObject runtime.Object, i
+ 			return fmt.Errorf("garbage collection of port %s failed, found %d ports with the same name", portName, len(portList))
+ 		}
+ 
++		if len(portList) == 0 {
++			continue
++		}
++
+ 		if err := s.DeletePort(eventObject, portList[0].ID); err != nil {
+ 			return err
+ 		}
diff --git a/roles/defaults/vars/main.yml b/roles/defaults/vars/main.yml
index 7616e63..6e53e5a 100644
--- a/roles/defaults/vars/main.yml
+++ b/roles/defaults/vars/main.yml
@@ -36,7 +36,7 @@
   cluster_api_controller: registry.k8s.io/cluster-api/cluster-api-controller:v1.5.1
   cluster_api_kubeadm_bootstrap_controller: registry.k8s.io/cluster-api/kubeadm-bootstrap-controller:v1.5.1
   cluster_api_kubeadm_control_plane_controller: registry.k8s.io/cluster-api/kubeadm-control-plane-controller:v1.5.1
-  cluster_api_openstack_controller: registry.k8s.io/capi-openstack/capi-openstack-controller:v0.8.0
+  cluster_api_openstack_controller: ghcr.io/vexxhost/atmosphere/cluster-api-provider-openstack:v0.8.0-1
   csi_node_driver_registrar: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.4.0
   csi_rbd_attacher: registry.k8s.io/sig-storage/csi-attacher:v3.4.0
   csi_rbd_plugin: quay.io/cephcsi/cephcsi:v3.5.1