Feature 11057: Cluster management in Openshift-based infrastructures

Change-Id: I8bdb1efb3ad1e9c8da688f334b3dcf7f49ad047c
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
diff --git a/docker/osm-krm-functions/scripts/library/krm-functions.rc b/docker/osm-krm-functions/scripts/library/krm-functions.rc
index c006729..1fc5a5f 100644
--- a/docker/osm-krm-functions/scripts/library/krm-functions.rc
+++ b/docker/osm-krm-functions/scripts/library/krm-functions.rc
@@ -17,7 +17,6 @@
 #######################################################################################
 
 
-
 function generator_encrypted_secret_cloud_credentials() {
   local CLOUD_CREDENTIALS_FILENAME="$1"
   local SECRET_NAME="$2"
@@ -228,6 +227,7 @@
     "${TARGET_FOLDER}"
 }
 
+
 function scale_nodegroup() {
   local NODEGROUP_NAME="$1"
   local NODEGROUP_KUSTOMIZATION_NAME="$2"
@@ -251,7 +251,7 @@
   local BASE_TEMPLATES_PATH="${15:-"cloud-resources"}"
   local MANIFEST_FILENAME="${16:-"${NODEGROUP_NAME}"}"
 
-   # Is the provider type supported?
+  # Is the provider type supported?
   local VALID_PROVIDERS=("eks" "aks" "gke")
   CLUSTER_TYPE="${CLUSTER_TYPE,,}"
   [[ ! ($(echo ${VALID_PROVIDERS[@]} | grep -w "${CLUSTER_TYPE}")) ]] && return 1
@@ -271,6 +271,7 @@
     "${TARGET_FOLDER}"
 }
 
+
 # Delete nodegroup
 function delete_nodegroup() {
   local NODEGROUP_KUSTOMIZATION_NAME="$1"
@@ -278,13 +279,12 @@
   local PROJECT_NAME="${3:-"${MGMT_PROJECT_NAME}"}"
   local FLEET_REPO_DIR="${4:-"${FLEET_REPO_DIR}"}"
   local MGMT_RESOURCES_DIR="${5:-"${MGMT_RESOURCES_DIR}"}"
-
   local NODEGROUP_DIR="${MGMT_RESOURCES_DIR}/${CLUSTER_NAME}/${NODEGROUP_KUSTOMIZATION_NAME}"
-
   # Delete node Kustomizations
   rm -rf "${NODEGROUP_DIR}"
 }
 
+
 # TODO: Deprecated
 # Create AKS cluster (without bootstrap)
 function create_cluster_aks() {
@@ -505,16 +505,15 @@
   local SW_CATALOGS_REPO_URL="$3"
   local PROJECT_NAME="${4:-"${MGMT_PROJECT_NAME}"}"
   local SW_CATALOGS_REPO_DIR="${5:-"${SW_CATALOGS_REPO_DIR}"}"
-
+  # Path for the source templates
+  local TEMPLATES="${6:-"${SW_CATALOGS_REPO_DIR}/cloud-resources/flux-remote-bootstrap/cluster-base/templates"}"
+  
   # Optional inputs:
   # Paths for each profile in the Git repo
-  local INFRA_CONTROLLERS_PATH="${6:-"${PROJECT_NAME}/infra-controller-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
-  local INFRA_CONFIGS_PATH="${7:-"${PROJECT_NAME}/infra-config-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
-  local MANAGED_RESOURCES_PATH="${8:-"${PROJECT_NAME}/managed-resources/${CLUSTER_KUSTOMIZATION_NAME}"}"
-  local APPS_PATH="${9:-"${PROJECT_NAME}/app-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
-
-  # Path for the source templates
-  local TEMPLATES="${SW_CATALOGS_REPO_DIR}/cloud-resources/flux-remote-bootstrap/cluster-base/templates"
+  local INFRA_CONTROLLERS_PATH="${7:-"${PROJECT_NAME}/infra-controller-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local INFRA_CONFIGS_PATH="${8:-"${PROJECT_NAME}/infra-config-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local MANAGED_RESOURCES_PATH="${9:-"${PROJECT_NAME}/managed-resources/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local APPS_PATH="${10:-"${PROJECT_NAME}/app-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
 
   # Generate
   export CLUSTER_KUSTOMIZATION_NAME
@@ -556,6 +555,7 @@
   local PRIVATE_KEY_NEW_CLUSTER="$1"
   local PUBLIC_KEY_MGMT="$2"
   local CLUSTER_AGE_SECRET_NAME="${3:-$(safe_name "sops-age-${CLUSTER_KUSTOMIZATION_NAME}")}"
+  local CLUSTER_AGE_SECRET_NAMESPACE="${4:-"managed-resources"}"
 
   join_lists \
     <(cat) \
@@ -563,7 +563,7 @@
       echo "${PRIVATE_KEY_NEW_CLUSTER}" | \
       grep -v '^#' | \
       kubectl create secret generic "${CLUSTER_AGE_SECRET_NAME}" \
-        --namespace=managed-resources \
+        --namespace="${CLUSTER_AGE_SECRET_NAMESPACE}" \
         --from-file=agekey=/dev/stdin \
         -o yaml --dry-run=client | \
       encrypt_secret_from_stdin \
@@ -580,16 +580,28 @@
   local CLUSTER_KUSTOMIZATION_NAME="${2:-$(safe_name ${CLUSTER_NAME})}"
   local CLUSTER_AGE_SECRET_NAME="${3:-$(safe_name "sops-age-${CLUSTER_KUSTOMIZATION_NAME}")}"
   local SW_CATALOGS_REPO_DIR="${4:-"${SW_CATALOGS_REPO_DIR}"}"
+  local BOOTSTRAP_KUSTOMIZATION_NAMESPACE="${5:-"managed-resources"}"
+  local CLUSTER_KUSTOMIZATION_NAMESPACE="${6:-"managed-resources"}"
+  local BOOTSTRAP_SECRET_NAMESPACE="${7:-"managed-resources"}"
 
   # Paths and names for the templates
-  local MANIFEST_FILENAME="${5:-"cluster-bootstrap-${CLUSTER_KUSTOMIZATION_NAME}.yaml"}"
-  local TEMPLATES="${6:-"${SW_CATALOGS_REPO_DIR}/cloud-resources/flux-remote-bootstrap/bootstrap/templates"}"
-  local TEMPLATE_MANIFEST_FILENAME="${7:-"remote-cluster-bootstrap.yaml"}"
+  local MANIFEST_FILENAME="${7:-"cluster-bootstrap-${CLUSTER_KUSTOMIZATION_NAME}.yaml"}"
+  local TEMPLATES="${8:-"${SW_CATALOGS_REPO_DIR}/cloud-resources/flux-remote-bootstrap/bootstrap/templates"}"
+  local TEMPLATE_MANIFEST_FILENAME="${9:-"remote-cluster-bootstrap.yaml"}"
+
+  # Variables for kubeconfig secret configuration
+  local CLUSTER_KUBECONFIG_SECRET_KEY=${CLUSTER_KUBECONFIG_SECRET_KEY:-"kubeconfig"}
+  local CLUSTER_KUBECONFIG_SECRET_NAME=${CLUSTER_KUBECONFIG_SECRET_NAME:-"kubeconfig-${CLUSTER_KUSTOMIZATION_NAME}"}
 
   # Generate manifests
   export CLUSTER_KUSTOMIZATION_NAME
   export CLUSTER_NAME
   export CLUSTER_AGE_SECRET_NAME
+  export CLUSTER_KUBECONFIG_SECRET_KEY
+  export CLUSTER_KUBECONFIG_SECRET_NAME
+  export BOOTSTRAP_KUSTOMIZATION_NAMESPACE
+  export CLUSTER_KUSTOMIZATION_NAMESPACE
+  export BOOTSTRAP_SECRET_NAMESPACE
 
   join_lists \
     <(cat) \
@@ -600,7 +612,7 @@
         "${TEMPLATE_MANIFEST_FILENAME}" \
         "${MANIFEST_FILENAME}" | \
       replace_env_vars \
-        '${CLUSTER_KUSTOMIZATION_NAME},${CLUSTER_NAME},${CLUSTER_AGE_SECRET_NAME}'
+        '${CLUSTER_KUSTOMIZATION_NAME},${CLUSTER_NAME},${CLUSTER_AGE_SECRET_NAME},${CLUSTER_KUBECONFIG_SECRET_KEY},${CLUSTER_KUBECONFIG_SECRET_NAME},${CLUSTER_KUSTOMIZATION_NAMESPACE},${BOOTSTRAP_KUSTOMIZATION_NAMESPACE},${BOOTSTRAP_SECRET_NAMESPACE}'
       )
 }
 
@@ -670,10 +682,16 @@
   local PUBLIC_KEY_NEW_CLUSTER="$9"
   local PRIVATE_KEY_NEW_CLUSTER="${10:-${PRIVATE_KEY_NEW_CLUSTER}}"
   local IMPORTED_CLUSTER="${11:-"false"}"
+  local MGMT_CLUSTER_NAME="${12:-"_management"}"
+  local CLUSTER_KUBECONFIG_SECRET_NAME=${13:-"kubeconfig-${CLUSTER_KUSTOMIZATION_NAME}"}
+  local CLUSTER_KUBECONFIG_SECRET_KEY=${14:-"kubeconfig"}
+  local TEMPLATES_DIR="${15:-"${SW_CATALOGS_REPO_DIR}/cloud-resources/flux-remote-bootstrap/cluster-base/templates"}"
+  local BOOTSTRAP_KUSTOMIZATION_NAMESPACE="${16:-"managed-resources"}"
+  local CLUSTER_KUSTOMIZATION_NAMESPACE="${17:-"managed-resources"}"
+  local BOOTSTRAP_SECRET_NAMESPACE="${18:-"${BOOTSTRAP_KUSTOMIZATION_NAMESPACE}"}"
 
-
-  # Calculates the folder where managed resources area defined
-  local MGMT_RESOURCES_DIR="${FLEET_REPO_DIR}/${MGMT_PROJECT_NAME}/managed-resources/_management"
+  # Calculates the folder where managed resources are defined
+  local MGMT_RESOURCES_DIR="${FLEET_REPO_DIR}/${MGMT_PROJECT_NAME}/managed-resources/${MGMT_CLUSTER_NAME}"
 
   # Create profile folders
   echo "" | \
@@ -692,7 +710,8 @@
     "${FLEET_REPO_URL}" \
     "${SW_CATALOGS_REPO_URL}" \
     "${MGMT_PROJECT_NAME}" \
-    "${SW_CATALOGS_REPO_DIR}" | \
+    "${SW_CATALOGS_REPO_DIR}" \
+    "${TEMPLATES_DIR}" | \
   list2folder_cp_over \
     "${CLUSTER_FOLDER}"
 
@@ -715,11 +734,15 @@
     "${CLUSTER_NAME}" \
     "${CLUSTER_KUSTOMIZATION_NAME}" \
     "${CLUSTER_AGE_SECRET_NAME}" \
-    "${SW_CATALOGS_REPO_DIR}" | \
+    "${SW_CATALOGS_REPO_DIR}" \
+    "${BOOTSTRAP_KUSTOMIZATION_NAMESPACE}" \
+    "${CLUSTER_KUSTOMIZATION_NAMESPACE}" \
+    "${BOOTSTRAP_SECRET_NAMESPACE}" | \
   generator_k8s_age_secret_new_cluster \
     "${PRIVATE_KEY_NEW_CLUSTER}" \
     "${PUBLIC_KEY_MGMT}" \
-    "${CLUSTER_AGE_SECRET_NAME}" | \
+    "${CLUSTER_AGE_SECRET_NAME}" \
+    "${BOOTSTRAP_SECRET_NAMESPACE}" | \
   prepend_folder_path "${CLUSTER_KUSTOMIZATION_NAME}/" | \
   list2folder_cp_over \
     "${MGMT_RESOURCES_DIR}"
@@ -802,7 +825,6 @@
   local TEMPLATE_MANIFEST_FILENAME="${26:-"${CLUSTER_TYPE,,}01.yaml"}"
   local MANIFEST_FILENAME="${27:-"${CLUSTER_TYPE,,}-${CLUSTER_NAME}.yaml"}"
 
-
   # Is the provider type supported?
   local VALID_PROVIDERS=("eks" "aks" "gke")
   CLUSTER_TYPE="${CLUSTER_TYPE,,}"
@@ -983,22 +1005,23 @@
   local PROJECT_NAME="${2:-"${MGMT_PROJECT_NAME}"}"
   local FLEET_REPO_DIR="${3:-"${FLEET_REPO_DIR}"}"
   local MGMT_RESOURCES_DIR="${4:-"${MGMT_RESOURCES_DIR}"}"
+  local MGMT_CLUSTER_DIR="${5:-"${MGMT_CLUSTER_DIR}"}"
 
   # Optional inputs: Paths for each profile in the Git repo
-  local INFRA_CONTROLLERS_DIR="${5:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/infra-controller-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
-  local INFRA_CONFIGS_DIR="${6:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/infra-config-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
-  local MANAGED_RESOURCES_DIR="${7:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/managed-resources/${CLUSTER_KUSTOMIZATION_NAME}"}"
-  local APPS_DIR="${8:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/app-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
-  local CLUSTER_DIR="${9:-"${FLEET_REPO_DIR}/clusters/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local INFRA_CONTROLLERS_DIR="${6:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/infra-controller-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local INFRA_CONFIGS_DIR="${7:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/infra-config-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local MANAGED_RESOURCES_DIR="${8:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/managed-resources/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local MGMT_CLUSTER_DIR="${9:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/managed-resources/${MGMT_CLUSTER_DIR}"}"
+  local APPS_DIR="${10:-"${FLEET_REPO_DIR}/${PROJECT_NAME}/app-profiles/${CLUSTER_KUSTOMIZATION_NAME}"}"
+  local CLUSTER_DIR="${11:-"${FLEET_REPO_DIR}/clusters/${CLUSTER_KUSTOMIZATION_NAME}"}"
 
   # Optional input: Do I need a purge operation first?
-  local PURGE="${10:-"false"}"
+  local PURGE="${12:-"false"}"
 
 
   # Perform the purge if needed
   if [[ "${PURGE,,}" == "true" ]]; then
     echo "Purging the remote Flux instalation..."
-    flux uninstall -s --namespace=flux-system
   fi
 
   echo "Deleting cluster profiles and (when applicable) its cloud resources..."
@@ -1007,6 +1030,7 @@
   rm -rf "${INFRA_CONTROLLERS_DIR}"
   rm -rf "${INFRA_CONFIGS_DIR}"
   rm -rf "${MANAGED_RESOURCES_DIR}"
+  rm -rf "${MGMT_CLUSTER_DIR}"
   rm -rf "${APPS_DIR}"
 
   # Delete base cluster Kustomizations
@@ -1094,6 +1118,459 @@
     "${MANIFEST_FILENAME}"
 }
 
+# Create remote CAPI cluster for Openstack
+function create_capi_openstack_cluster() {
+  local CLUSTER_KUSTOMIZATION_NAME="${1}"
+  local CLUSTER_NAME="${2}"
+  local VM_SIZE="${3}"
+  local VM_SIZE_CONTROL_PLANE="${4:-"${VM_SIZE}"}"
+  local NODE_COUNT="${5}"
+  local NODE_COUNT_CONTROLPLANE="${6:-"1"}"
+  local K8S_VERSION="${7}"
+  # OpenStack specific
+  local OPENSTACK_CLOUD_NAME="${8}"
+  local OPENSTACK_DNS_NAMESERVERS="${9}"
+  local OPENSTACK_EXTERNAL_NETWORK_ID="${10}"
+  local OPENSTACK_FAILURE_DOMAIN="${11}"
+  local OPENSTACK_SSH_KEY_NAME="${12}"
+  local CNI="${13:-"calico"}"
+  local OPENSTACK_WORKER_IMAGE_NAME="${14:-"osm-capo-node-${K8S_VERSION}"}"
+  local OPENSTACK_CONTROL_PLANE_IMAGE_NAME="${15:-"${OPENSTACK_WORKER_IMAGE_NAME}"}"
+  # SOPS-AGE related
+  local PUBLIC_KEY_MGMT="${16:-"${PUBLIC_KEY_MGMT}"}"
+  local PUBLIC_KEY_NEW_CLUSTER="${17:-"${PUBLIC_KEY_NEW_CLUSTER}"}"
+  local PRIVATE_KEY_NEW_CLUSTER="${18:-"${PRIVATE_KEY_NEW_CLUSTER}"}"
+  # GitOps retaled
+  local FLEET_REPO_DIR="${19:-"${FLEET_REPO_DIR}"}"
+  local FLEET_REPO_URL="${20:-"${FLEET_REPO_URL}"}"
+  local SW_CATALOGS_REPO_DIR="${21:-"${SW_CATALOGS_REPO_DIR}"}"
+  local SW_CATALOGS_REPO_URL="${22:-"${SW_CATALOGS_REPO_URL}"}"
+  local SKIP_BOOTSTRAP="${23:-"false"}"
+  local MGMT_PROJECT_NAME="${24:-"osm_admin"}"
+  local MGMT_CLUSTER_NAME="${25:-"_management"}"
+  local BASE_TEMPLATES_PATH="${26:-"cloud-resources/capi"}"
+  local NAMESPACE="${27:-"managed-resources"}"
+
+  # Varibles with valus from convention.
+  local CLUSTER_TYPE="openstack"
+  local TEMPLATE_MANIFEST_FILENAME="capi-cluster.yaml"
+  local MANIFEST_FILENAME="openstack-${CLUSTER_NAME}.yaml"
+  local CLOUD_CREDENTIALS="${OPENSTACK_CLOUD_NAME}-capo-config"
+
+  # Determines the source dir for the templates and the target folder in Fleet
+  local TEMPLATES_DIR="${SW_CATALOGS_REPO_DIR}/${BASE_TEMPLATES_PATH}/openstack-kubeadm/templates"
+  local TARGET_FOLDER="${FLEET_REPO_DIR}/${MGMT_PROJECT_NAME}/managed-resources/${MGMT_CLUSTER_NAME}"
+  export CNI=${CNI,,}
+
+  # Variables for kubeconfig secret reference
+  export CLUSTER_KUBECONFIG_SECRET_NAME="${CLUSTER_KUSTOMIZATION_NAME}-kubeconfig"
+  export CLUSTER_KUBECONFIG_SECRET_KEY="value"
+
+  export CLUSTER_KUSTOMIZATION_NAME
+  export OPENSTACK_CLOUD_NAME
+
+  folder2list \
+    "${TEMPLATES_DIR}" | \
+  replace_env_vars \
+    '${CLUSTER_KUSTOMIZATION_NAME},${CNI},${CLUSTER_KUBECONFIG_SECRET_NAME},${CLUSTER_KUBECONFIG_SECRET_KEY},${OPENSTACK_CLOUD_NAME}' | \
+  patch_replace \
+    ".spec.postBuild.substitute.cluster_name" \
+    "${CLUSTER_NAME}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.cni" \
+    "${CNI}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.control_plane_machine_count" \
+    "${NODE_COUNT_CONTROLPLANE}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.kubernetes_version" \
+    "v${K8S_VERSION}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.namespace" \
+    "${NAMESPACE}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.worker_machine_count" \
+    "${NODE_COUNT}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_cloud" \
+    "${OPENSTACK_CLOUD_NAME}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_cloud_conf" \
+    "${CLOUD_CREDENTIALS}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_control_plane_machine_flavor" \
+    "${VM_SIZE_CONTROL_PLANE}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_dns_nameservers" \
+    "${OPENSTACK_DNS_NAMESERVERS}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_external_network_id" \
+    "${OPENSTACK_EXTERNAL_NETWORK_ID}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_failure_domain" \
+    "${OPENSTACK_FAILURE_DOMAIN}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_worker_image_name" \
+    "${OPENSTACK_WORKER_IMAGE_NAME}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_control_plane_image_name" \
+    "${OPENSTACK_CONTROL_PLANE_IMAGE_NAME}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_node_machine_flavor" \
+    "${VM_SIZE}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openstack_ssh_key_name" \
+    "${OPENSTACK_SSH_KEY_NAME}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  rename_file_in_items \
+    "${TEMPLATE_MANIFEST_FILENAME}" \
+    "${MANIFEST_FILENAME}" | \
+  prepend_folder_path "${CLUSTER_KUSTOMIZATION_NAME}/" | \
+  list2folder_cp_over \
+    "${TARGET_FOLDER}"
+  
+  # Bootstrap (unless asked to skip)
+  if [[ "${SKIP_BOOTSTRAP,,}" == "true" ]]; then
+    return 0
+  fi
+  
+  create_bootstrap_for_remote_cluster \
+    "${CLUSTER_NAME}" \
+    "${CLUSTER_KUSTOMIZATION_NAME}" \
+    "${FLEET_REPO_DIR}" \
+    "${SW_CATALOGS_REPO_DIR}" \
+    "${FLEET_REPO_URL}" \
+    "${SW_CATALOGS_REPO_URL}" \
+    "${MGMT_PROJECT_NAME}" \
+    "${PUBLIC_KEY_MGMT}" \
+    "${PUBLIC_KEY_NEW_CLUSTER}" \
+    "${PRIVATE_KEY_NEW_CLUSTER}" \
+    "false" \
+    '' \
+    "${CLUSTER_KUBECONFIG_SECRET_NAME}" \
+    "${CLUSTER_KUBECONFIG_SECRET_KEY}"
+
+}
+
+# Update remote CAPI cluster for Openstack
+function update_capi_openstack_cluster() {
+  local CLUSTER_KUSTOMIZATION_NAME="${1}"
+  local CLUSTER_NAME="${2}"
+  local VM_SIZE="${3}"
+  local VM_SIZE_CONTROL_PLANE="${4}"
+  local NODE_COUNT="${5}"
+  local NODE_COUNT_CONTROLPLANE="${6}"
+  local K8S_VERSION="${7}"
+  # OpenStack specific
+  local OPENSTACK_CLOUD_NAME="${8}"
+  local OPENSTACK_DNS_NAMESERVERS="${9}"
+  local OPENSTACK_EXTERNAL_NETWORK_ID="${10}"
+  local OPENSTACK_FAILURE_DOMAIN="${11}"
+  local OPENSTACK_SSH_KEY_NAME="${12}"
+  local CNI="${13:-"calico"}"
+  local OPENSTACK_WORKER_IMAGE_NAME="${14:-"osm-capo-node-${K8S_VERSION}"}"
+  local OPENSTACK_CONTROL_PLANE_IMAGE_NAME="${15:-"${OPENSTACK_WORKER_IMAGE_NAME}"}"
+  # SOPS-AGE related
+  local PUBLIC_KEY_MGMT="${16:-"${PUBLIC_KEY_MGMT}"}"
+  local PUBLIC_KEY_NEW_CLUSTER="${17:-"${PUBLIC_KEY_NEW_CLUSTER}"}"
+  local PRIVATE_KEY_NEW_CLUSTER="${18:-"${PRIVATE_KEY_NEW_CLUSTER}"}"
+  # GitOps retaled
+  local FLEET_REPO_DIR="${19:-"${FLEET_REPO_DIR}"}"
+  local FLEET_REPO_URL="${20:-"${FLEET_REPO_URL}"}"
+  local SW_CATALOGS_REPO_DIR="${21:-"${SW_CATALOGS_REPO_DIR}"}"
+  local SW_CATALOGS_REPO_URL="${22:-"${SW_CATALOGS_REPO_URL}"}"
+  local MGMT_PROJECT_NAME="${23:-"osm_admin"}"
+  local MGMT_CLUSTER_NAME="${24:-"_management"}"
+  local BASE_TEMPLATES_PATH="${25:-"cloud-resources/capi"}"
+  local NAMESPACE="${26:-"managed-resources"}"
+  
+  # Determine key folders in Fleet
+  local MGMT_RESOURCES_DIR="${FLEET_REPO_DIR}/${MGMT_PROJECT_NAME}/managed-resources/${MGMT_CLUSTER_NAME}"
+
+  # Updating no new cluster
+  local SKIP_BOOTSTRAP="true"
+  
+  create_capi_openstack_cluster \
+    "${CLUSTER_KUSTOMIZATION_NAME}" \
+    "${CLUSTER_NAME}" \
+    "${VM_SIZE}" \
+    "${VM_SIZE_CONTROL_PLANE}" \
+    "${NODE_COUNT}" \
+    "${NODE_COUNT_CONTROLPLANE}" \
+    "${K8S_VERSION}" \
+    "${OPENSTACK_CLOUD_NAME}" \
+    "${OPENSTACK_DNS_NAMESERVERS}" \
+    "${OPENSTACK_EXTERNAL_NETWORK_ID}" \
+    "${OPENSTACK_FAILURE_DOMAIN}" \
+    "${OPENSTACK_SSH_KEY_NAME}" \
+    "${CNI}" \
+    "${OPENSTACK_WORKER_IMAGE_NAME}" \
+    "${OPENSTACK_CONTROL_PLANE_IMAGE_NAME}" \
+    "${PUBLIC_KEY_MGMT}" \
+    "${PUBLIC_KEY_NEW_CLUSTER}" \
+    "${PRIVATE_KEY_NEW_CLUSTER}" \
+    "${FLEET_REPO_DIR}" \
+    "${FLEET_REPO_URL}" \
+    "${SW_CATALOGS_REPO_DIR}" \
+    "${SW_CATALOGS_REPO_URL}" \
+    "${SKIP_BOOTSTRAP}" \
+    "${MGMT_PROJECT_NAME}" \
+    "${MGMT_CLUSTER_NAME}" \
+    "${BASE_TEMPLATES_PATH}" \
+    "${NAMESPACE}"
+}
+
+# Create remote Openshift cluster via ACM
+function create_openshift_cluster {
+  local CLUSTER_KUSTOMIZATION_NAME="${1}"
+  local CLUSTER_NAME="${2}"
+  # This has to be void. Stored in database
+  local K8S_VERSION="${3:-"''"}"
+  # SOPS-AGE related
+  local PUBLIC_KEY_ACM="${4}"
+  local PUBLIC_KEY_NEW_CLUSTER="${5:-"${PUBLIC_KEY_NEW_CLUSTER}"}"
+  local PRIVATE_KEY_NEW_CLUSTER="${6:-"${PRIVATE_KEY_NEW_CLUSTER}"}"
+  # OpenShift
+  local OPENSHIFT_RELEASE="${7}"
+  local INFRA_PUBLIC_SSH_KEY="${8}"
+  local CONTROL_PLANE_AVAILABILITY="${9}"
+  local WORKER_COUNT="${10}"
+  local WORKER_CORES="${11}"
+  local WORKER_MEMORY="${12}"
+  local WORKER_VOLUME_SIZE="${13}"
+  local STORAGE_CLASS="${14}"
+  local BASE_DOMAIN="${15}"
+  local MGMT_CLUSTER_NAME="${16}"
+  local HOSTED_CLUSTERS_PROJECT="${17:-"clusters"}"
+  local ETCD_VOLUME_SIZE="${18:-"8"}"
+  # GitOps retaled
+  local FLEET_REPO_DIR="${19:-"${FLEET_REPO_DIR}"}"
+  local FLEET_REPO_URL="${20:-"${FLEET_REPO_URL}"}"
+  local SW_CATALOGS_REPO_DIR="${21:-"${SW_CATALOGS_REPO_DIR}"}"
+  local SW_CATALOGS_REPO_URL="${22:-"${SW_CATALOGS_REPO_URL}"}"
+  local SKIP_BOOTSTRAP="${23:-"false"}"
+   # Only change if absolutely needeed
+  local MGMT_PROJECT_NAME="${24:-"osm_admin"}"
+  local BASE_TEMPLATES_PATH="${25:-"cloud-resources"}"
+  local TEMPLATE_MANIFEST_FILENAME="${26:-"openshift01.yaml"}"
+  local MANIFEST_FILENAME="${27:-"openshift-${CLUSTER_NAME}.yaml"}"
+  
+  local TEMPLATES_DIR="${SW_CATALOGS_REPO_DIR}/cloud-resources/openshift/templates"
+  local TARGET_FOLDER="${FLEET_REPO_DIR}/${MGMT_PROJECT_NAME}/managed-resources/${MGMT_CLUSTER_NAME}"
+
+  # Internally ACM creates several projects for each cluster.
+  # Specifically the klusterletaddonconfig must land in a project with the same name as the cluster.
+  # This will be specifically controlled by the variable `CLUSTER_PROJECT`.
+  #
+  # It must be notes that CLUSTER_NAME, CLUSTER_KUSTOMIZATION_NAME and CLUSTER_PROJECT have the same value,
+  # but they are conceptually different.
+  local CLUSTER_PROJECT="${CLUSTER_KUSTOMIZATION_NAME}"
+
+  export CLUSTER_KUSTOMIZATION_NAME
+
+  folder2list \
+    "${TEMPLATES_DIR}" | \
+  replace_env_vars \
+    '${CLUSTER_KUSTOMIZATION_NAME}' | \
+  patch_replace \
+    ".spec.postBuild.substitute.base_domain" \
+    "${BASE_DOMAIN}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.cluster_name" \
+    "${CLUSTER_NAME}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.cluster_project" \
+    "${CLUSTER_PROJECT}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.hosted_cluster_project" \
+    "${HOSTED_CLUSTERS_PROJECT}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.etcd_volume_size" \
+    "${ETCD_VOLUME_SIZE}Gi" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.openshift_release" \
+    "${OPENSHIFT_RELEASE}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.storage_class" \
+    "${STORAGE_CLASS}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.control_plane_availability" \
+    "${CONTROL_PLANE_AVAILABILITY}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.worker_count" \
+    "${WORKER_COUNT}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.worker_cores" \
+    "${WORKER_CORES}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.worker_memory" \
+    "${WORKER_MEMORY}Gi" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.worker_volume_size" \
+    "${WORKER_VOLUME_SIZE}Gi" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}\")" | \
+  patch_replace \
+    ".spec.postBuild.substitute.cluster_project" \
+    "${CLUSTER_PROJECT}" \
+    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${CLUSTER_KUSTOMIZATION_NAME}-ns\")" | \
+  rename_file_in_items \
+    "${TEMPLATE_MANIFEST_FILENAME}" \
+    "${MANIFEST_FILENAME}" | \
+  prepend_folder_path "${CLUSTER_KUSTOMIZATION_NAME}/" | \
+  list2folder_cp_over \
+    "${TARGET_FOLDER}"
+
+  echo "" | \
+  make_generator \
+    "pull-secret.yaml" \
+    kubectl_encrypt \
+      "${PUBLIC_KEY_ACM}" \
+      create \
+      secret \
+      generic \
+      "pullsecret-cluster-${CLUSTER_NAME}" \
+      --namespace="${HOSTED_CLUSTERS_PROJECT}" \
+      --from-file=".dockerconfigjson"=<(echo "${DOCKERCONFIGJSON}") \
+      -o=yaml \
+      --dry-run=client | \
+  make_generator \
+    "ssh-key-secret.yaml" \
+    kubectl_encrypt \
+      "${PUBLIC_KEY_ACM}" \
+      create \
+      secret \
+      generic \
+      "sshkey-cluster-${CLUSTER_NAME}" \
+      --namespace="${HOSTED_CLUSTERS_PROJECT}" \
+      --from-file='id_rsa.pub'=<(echo "${INFRA_PUBLIC_SSH_KEY}") \
+      -o=yaml \
+      --dry-run=client | \
+  list2folder_cp_over \
+    "${TARGET_FOLDER}/${CLUSTER_KUSTOMIZATION_NAME}"
+
+  # Bootstrap (unless asked to skip)
+  if [[ "${SKIP_BOOTSTRAP,,}" == "true" ]]; then
+    return 0
+  fi
+
+  local CLUSTER_KUBECONFIG_SECRET_NAME="${CLUSTER_KUSTOMIZATION_NAME}-admin-kubeconfig"
+  local CLUSTER_KUBECONFIG_SECRET_KEY="kubeconfig"
+  local BOOTSTRAP_KUSTOMIZATION_NAMESPACE="${HOSTED_CLUSTERS_PROJECT}"
+  local CLUSTER_KUSTOMIZATION_NAMESPACE="managed-resources"
+  local BOOTSTRAP_SECRET_NAMESPACE="managed-resources"
+
+  create_bootstrap_for_remote_cluster \
+    "${CLUSTER_NAME}" \
+    "${CLUSTER_KUSTOMIZATION_NAME}" \
+    "${FLEET_REPO_DIR}" \
+    "${SW_CATALOGS_REPO_DIR}" \
+    "${FLEET_REPO_URL}" \
+    "${SW_CATALOGS_REPO_URL}" \
+    "${MGMT_PROJECT_NAME}" \
+    "${PUBLIC_KEY_ACM}" \
+    "${PUBLIC_KEY_NEW_CLUSTER}" \
+    "${PRIVATE_KEY_NEW_CLUSTER}" \
+    "false" \
+    "${MGMT_CLUSTER_NAME}" \
+    "${CLUSTER_KUBECONFIG_SECRET_NAME}" \
+    "${CLUSTER_KUBECONFIG_SECRET_KEY}" \
+    "${SW_CATALOGS_REPO_DIR}/cloud-resources/flux-remote-bootstrap/cluster-base-openshift/templates" \
+    "${BOOTSTRAP_KUSTOMIZATION_NAMESPACE}" \
+    "${CLUSTER_KUSTOMIZATION_NAMESPACE}" \
+    "${BOOTSTRAP_SECRET_NAMESPACE}"
+
+}
+
+# Update remote Openshift cluster via ACM
+function update_openshift_cluster {
+  local CLUSTER_KUSTOMIZATION_NAME="${1}"
+  local CLUSTER_NAME="${2}"
+  # This has to be void. Stored in database
+  local K8S_VERSION="${3:-"''"}"
+  # SOPS-AGE related
+  local PUBLIC_KEY_ACM="${4}"
+  local PUBLIC_KEY_NEW_CLUSTER="${5:-"${PUBLIC_KEY_NEW_CLUSTER}"}"
+  local PRIVATE_KEY_NEW_CLUSTER="${6:-"${PRIVATE_KEY_NEW_CLUSTER}"}"
+  # OpenShift specific
+  local OPENSHIFT_RELEASE="${7}"
+  local INFRA_PUBLIC_SSH_KEY="${8}"
+  local CONTROL_PLANE_AVAILABILITY="${9}"
+  local WORKER_COUNT="${10}"
+  local WORKER_CORES="${11}"
+  local WORKER_MEMORY="${12}"
+  local WORKER_VOLUME_SIZE="${13}"
+  local STORAGE_CLASS="${14}"
+  local BASE_DOMAIN="${15}"
+  local MGMT_CLUSTER_NAME="${16}"
+  local HOSTED_CLUSTERS_PROJECT="${17:-"clusters"}"
+  local ETCD_VOLUME_SIZE="${18:-"8"}"
+  # GitOps retaled
+  local FLEET_REPO_DIR="${19:-"${FLEET_REPO_DIR}"}"
+  local FLEET_REPO_URL="${20:-"${FLEET_REPO_URL}"}"
+  local SW_CATALOGS_REPO_DIR="${21:-"${SW_CATALOGS_REPO_DIR}"}"
+  local SW_CATALOGS_REPO_URL="${22:-"${SW_CATALOGS_REPO_URL}"}"
+  local SKIP_BOOTSTRAP="${23:-"false"}"
+   # Only change if absolutely needeed
+  local MGMT_PROJECT_NAME="${24:-"osm_admin"}"
+  
+  # Determine key folders in Fleet
+  local MGMT_RESOURCES_DIR="${FLEET_REPO_DIR}/${MGMT_PROJECT_NAME}/managed-resources/${MGMT_CLUSTER_NAME}"
+
+  # Updating no new cluster
+  local SKIP_BOOTSTRAP="true"
+
+  create_openshift_cluster \
+    "${CLUSTER_KUSTOMIZATION_NAME}" \
+    "${CLUSTER_NAME}" \
+    "${K8S_VERSION}" \
+    "${PUBLIC_KEY_ACM}" \
+    "${PUBLIC_KEY_NEW_CLUSTER}" \
+    "${PRIVATE_KEY_NEW_CLUSTER}" \
+    "${OPENSHIFT_RELEASE}" \
+    "${INFRA_PUBLIC_SSH_KEY}" \
+    "${CONTROL_PLANE_AVAILABILITY}" \
+    "${WORKER_COUNT}" \
+    "${WORKER_CORES}" \
+    "${WORKER_MEMORY}" \
+    "${WORKER_VOLUME_SIZE}" \
+    "${STORAGE_CLASS}" \
+    "${BASE_DOMAIN}" \
+    "${MGMT_CLUSTER_NAME}" \
+    "${HOSTED_CLUSTERS_PROJECT}" \
+    "${ETCD_VOLUME_SIZE}" \
+    "${FLEET_REPO_DIR}" \
+    "${FLEET_REPO_URL}" \
+    "${SW_CATALOGS_REPO_DIR}" \
+    "${SW_CATALOGS_REPO_URL}" \
+    "${SKIP_BOOTSTRAP}" \
+    "${MGMT_PROJECT_NAME}"
+}
 
 # ----- Helper functions for adding/removing a profile from a cluster -----
 
@@ -2186,6 +2663,91 @@
 }
 
 
+# Create a CloudConfig for CAPI provider
+function create_capi_openstack_cloudconf() {
+  local OPENSTACK_CLOUD_NAME="${1}"
+  local PUBLIC_KEY="${2:-"${PUBLIC_KEY_MGMT}"}"
+  local CONFIG_DIR="${3:-"${MGMT_ADDON_CONFIG_DIR}"}"
+
+  local NAMESPACE="managed-resources"
+
+  local CLOUDS_YAML="${OPENSTACK_CLOUDS_YAML}"
+  local CACERT="${OPENSTACK_CACERT}"
+
+  local CLOUD_CREDENTIALS_SECRET_NAME="${OPENSTACK_CLOUD_NAME}-capo-config"
+  local CLOUD_CREDENTIALS_CLOUDS_KEY="clouds.yaml"
+  local CLOUD_CREDENTIALS_CACERT_KEY="cacert"
+  local CLOUD_CREDENTIALS_FILENAME="credentials-secret.yaml"
+  
+  local CLOUD_CREDENTIALS_TOML_SECRET_NAME="${OPENSTACK_CLOUD_NAME}-capo-config-toml"
+  local CLOUD_CREDENTIALS_TOML_FILENAME="credentials-toml-secret.yaml"
+
+  local TARGET_FOLDER="${CONFIG_DIR}/capi-providerconfigs/capo/${OPENSTACK_CLOUD_NAME}-config"
+  mkdir -p "${TARGET_FOLDER}"
+
+  echo "" | \
+  make_generator \
+    "${CLOUD_CREDENTIALS_FILENAME}" \
+    kubectl_encrypt \
+      "${PUBLIC_KEY}" \
+      create \
+      secret \
+      generic \
+      "${CLOUD_CREDENTIALS_SECRET_NAME}" \
+      --namespace="${NAMESPACE}" \
+      --from-file="${CLOUD_CREDENTIALS_CLOUDS_KEY}"=<(echo "${CLOUDS_YAML}") \
+      --from-file="${CLOUD_CREDENTIALS_CACERT_KEY}"=<(echo "${CACERT}") \
+      -o=yaml \
+      --dry-run=client | \
+  make_generator \
+    "${CLOUD_CREDENTIALS_TOML_FILENAME}" \
+    kubectl_encrypt \
+      "${PUBLIC_KEY}" \
+      create \
+      secret \
+      generic \
+      "${CLOUD_CREDENTIALS_TOML_SECRET_NAME}" \
+      --namespace="${NAMESPACE}" \
+      --from-file="os_auth_url"=<(echo "${OS_AUTH_URL}") \
+      --from-file="os_region_name"=<(echo "${OS_REGION_NAME}") \
+      --from-file="os_username"=<(echo "${OS_USERNAME}") \
+      --from-file="os_password"=<(echo "${OS_PASSWORD}") \
+      --from-file="os_project_id"=<(echo "${OS_PROJECT_ID}") \
+      --from-file="os_project_domain_id"=<(echo "${OS_PROJECT_DOMAIN_ID}") \
+      -o=yaml \
+      --dry-run=client | \
+  list2folder_cp_over \
+    "${TARGET_FOLDER}"
+}
+
+# Update a CloudConfig for CAPI provider
+function update_capi_openstack_cloudconf() {
+  local CLOUD_CONFIG_NAME="${1}"
+  local PUBLIC_KEY="${2:-"${PUBLIC_KEY_MGMT}"}"
+  local CONFIG_DIR="${3:-"${MGMT_ADDON_CONFIG_DIR}"}"
+
+  delete_capi_openstack_cloudconf \
+    "${CLOUD_CONFIG_NAME}" \
+    "${CONFIG_DIR}"
+  
+  create_capi_openstack_cloudconf \
+    "${CLOUD_CONFIG_NAME}" \
+    "${PUBLIC_KEY}" \
+    "${CONFIG_DIR}"
+}
+
+
+# Delete a CloudConfig for CAPI provider
+function delete_capi_openstack_cloudconf() {
+  local OPENSTACK_CLOUD_NAME="$1"
+  local CONFIG_DIR="${2:-"${MGMT_ADDON_CONFIG_DIR}"}"
+
+  local TARGET_FOLDER="${CONFIG_DIR}/capi-providerconfigs/capo/${OPENSTACK_CLOUD_NAME}-config"
+  
+  # Delete the encrypted secrets files.
+  rm -rf "${TARGET_FOLDER}"
+}
+
 # Helper function to return the relative path of a location in SW Catalogs for an OKA
 function path_to_catalog() {
   local OKA_TYPE="$1"