Installation on Exoscale

Steps to install an OpenShift 4 cluster on Exoscale.

These steps follow the Installing a cluster on bare metal docs to set up a user provisioned installation (UPI). Terraform is used to provision the cloud infrastructure.

This how-to guide is still a work in progress and will change. It’s currently very specific to VSHN and needs further changes to be more generic.

This guide is currently assuming that you’re using terraform-openshift4-exoscale v6 (component openshift4-terraform v8)

Starting situation

  • You already have a Tenant and its Git repository

  • You have a CCSP Red Hat login and are logged into Red Hat Openshift Cluster Manager

    Don’t use your personal account to login to the cluster manager for installation.
  • You want to register a new cluster in Lieutenant and are about to install Openshift 4 on Exoscale

Guided Setup

This how-to guide is exported from the Guided Setup automation tool. It’s highly recommended to run these instructions using said tool, as opposed to running them manually.

Guided Setup can be run easily in docker using the following aliases:

These shell scripts depend on socat. Please ensure you have socat installed.

guided-setup-base() {
  local OPTIND opt extra_env extra_volume extra_groups docker_group docker_path sshop_volumes ecr_volume
  extra_env=()
  extra_volume=()
  sshop_volumes=()
  ecr_volume=()
  while getopts 'h?v:e:' opt; do
      case "$opt" in
      h|\?)
          echo "usage: $0 [-y] [-v EXTRA_VOLUME_MOUNT] [-e EXTRA_ENV_VAR]"
          ;;
      e)
          extra_env+=(--env "$OPTARG")
          ;;
      v)
          extra_volume+=(--volume "$OPTARG")
          ;;
      esac
  done
  shift $((OPTIND-1))

  local pubring="${HOME}/.gnupg/pubring.kbx"
  if command -v gpgconf &>/dev/null && test -f "${pubring}"; then
    gpg_opts=(--volume "${pubring}:/app/.gnupg/pubring.kbx:ro" --volume "$(gpgconf --list-dir agent-extra-socket):/app/.gnupg/S.gpg-agent:ro")
  else
    gpg_opts=()
  fi

  readonly GANDALF_CONFIG="${XDG_CONFIG_HOME:-~/.config}/gandalf"
  if [[ -f "${GANDALF_CONFIG}/env" ]]
  then
    extra_env+=(--env-file "${GANDALF_CONFIG}/env")
  fi

  docker_group="$( getent group docker | cut --delimiter ':' --fields 3 )"
  docker_path=${DOCKER_HOST:-/var/run/docker.sock}

  if [[ -n "$docker_group" ]]; then
    extra_groups=("--group-add=$( getent group docker | cut --delimiter ':' --fields 3 )")
  fi

  if [[ "$OSTYPE" == "linux-gnu"* ]]; then
      open="xdg-open"
  elif [[ "$OSTYPE" == "darwin"* ]]; then
      open="open"
  fi

  if [[ -e "${HOME}/.ssh/sshop_config" ]]
  then
    sshop_volumes=("--volume" "${HOME}/.ssh/sshop_config:/app/.ssh/sshop_config" "--volume" "${HOME}/.ssh/sshop_known_hosts:/app/.ssh/sshop_known_hosts")
  fi

  if [[ -d "${HOME}/.config/emergency-credentials-receive/" ]]
  then
    ecr_volume=("--volume" "${HOME}/.config/emergency-credentials-receive/:/app/.config/emergency-credentials-receive:ro")
  fi
  socat \
    tcp-listen:8105,fork,reuseaddr,bind=127.0.0.1 \
    system:"xargs $open" &

  # NOTE(aa): Host network is required for the Vault OIDC callback, since Vault only binds the callback handler to 127.0.0.1
  # cf. https://github.com/hashicorp/vault/issues/29064
  docker run \
    --interactive=true \
    --tty \
    --rm \
    --user="$(id -u)" \
    "${extra_groups[@]}" \
    --env SSH_AUTH_SOCK=/tmp/ssh_agent.sock \
    --env GLAB_CONFIG_DIR="${PWD}" \
    --network host \
    --volume "${SSH_AUTH_SOCK}:/tmp/ssh_agent.sock" \
    --volume "${HOME}/.ssh/config:/app/.ssh/config:ro" \
    --volume "${HOME}/.ssh/known_hosts:/app/.ssh/known_hosts:ro" \
    --volume "${HOME}/.gitconfig:/app/.gitconfig:ro" \
    --volume "${HOME}/.cache:/app/.cache" \
    --volume "${HOME}/.gandalf:/app/.gandalf" \
    --volume "${docker_path}":/var/run/docker.sock \
    "${sshop_volumes[@]}" \
    "${ecr_volume[@]}" \
    "${extra_volume[@]}" \
    "${extra_env[@]}" \
    "${gpg_opts[@]}" \
    --volume "${PWD}:${PWD}" \
    --workdir "${PWD}" \
    ghcr.io/appuio/guided-setup:latest  \
    "${@}"

  kill %socat
}

guided-setup() {
  local OPTIND opt workflow_dir
  workflow_dir=
  while getopts 'h?w:' opt; do
      case "$opt" in
      h|\?)
          echo "usage: $0 [-y] [-w LOCAL_WORKFLOW_DIR]"
          ;;
      w)
          workflow_dir="$OPTARG"
          ;;
      esac
  done
  shift $((OPTIND-1))

  if [[ -z $workflow_dir ]]
  then
    guided-setup-base "${1}" "/workflows/${2}.workflow" "/workflows/${2}/*.yml" "/workflows/shared/*.yml" "${@:3}"
  else
    guided-setup-base -v "$workflow_dir":/workflows "${1}" "/workflows/${2}.workflow" "/workflows/${2%%-*}/*.yml" "/workflows/shared/*.yml" "${@:3}"
  fi
}

To run the workflows detailed below, simply source the above aliases and run:

guided-setup run exoscale

It’s recommended to run guided-setup in a new empty directory, as it creates a number of local files.

Prerequisites

Make sure the version of openshift-install and the rhcos image is the same, otherwise ignition will fail.

Workflow

Given I have all prerequisites installed

This step checks if all necessary prerequisites are installed on your system, including 'yq' (version 4 or higher, by Mike Farah) and 'oc' (OpenShift CLI).

Script

OUTPUT=$(mktemp)


set -euo pipefail
echo "Checking prerequisites..."

if which yq >/dev/null 2>&1 ; then { echo "✅ yq is installed."; } ; else { echo "❌ yq is not installed. Please install yq to proceed."; exit 1; } ; fi
if yq --version | grep -E 'version v[4-9]\.' | grep 'mikefarah' >/dev/null 2>&1 ; then { echo "✅ yq by mikefarah version 4 or higher is installed."; } ; else { echo "❌ yq version 4 or higher is required. Please upgrade yq to proceed."; exit 1; } ; fi

if which jq >/dev/null 2>&1 ; then { echo "✅ jq is installed."; } ; else { echo "❌ jq is not installed. Please install jq to proceed."; exit 1; } ; fi

if which oc >/dev/null 2>&1 ; then { echo "✅ oc (OpenShift CLI) is installed."; } ; else { echo "❌ oc (OpenShift CLI) is not installed. Please install oc to proceed."; exit 1; } ; fi

if which vault >/dev/null 2>&1 ; then { echo "✅ vault (HashiCorp Vault) is installed."; } ; else { echo "❌ vault (HashiCorp Vault) is not installed. Please install vault to proceed."; exit 1; } ; fi

if which curl >/dev/null 2>&1 ; then { echo "✅ curl is installed."; } ; else { echo "❌ curl is not installed. Please install curl to proceed."; exit 1; } ; fi

if which docker >/dev/null 2>&1 ; then { echo "✅ docker is installed."; } ; else { echo "❌ docker is not installed. Please install docker to proceed."; exit 1; } ; fi

if which glab >/dev/null 2>&1 ; then { echo "✅ glab (GitLab CLI) is installed."; } ; else { echo "❌ glab (GitLab CLI) is not installed. Please install glab to proceed."; exit 1; } ; fi

if which host >/dev/null 2>&1 ; then { echo "✅ host (DNS lookup utility) is installed."; } ; else { echo "❌ host (DNS lookup utility) is not installed. Please install host to proceed."; exit 1; } ; fi

if which mc >/dev/null 2>&1 ; then { echo "✅ mc (MinIO Client) is installed."; } ; else { echo "❌ mc (MinIO Client) is not installed. Please install mc >= RELEASE.2024-01-18T07-03-39Z to proceed."; exit 1; } ; fi
mc_version=$(mc --version | grep -Eo 'RELEASE[^ ]+')
if echo "$mc_version" | grep -E 'RELEASE\.202[4-9]-' >/dev/null 2>&1 ; then { echo "✅ mc version ${mc_version} is sufficient."; } ; else { echo "❌ mc version ${mc_version} is insufficient. Please upgrade mc to >= RELEASE.2024-01-18T07-03-39Z to proceed."; exit 1; } ; fi

if which aws >/dev/null 2>&1 ; then { echo "✅ aws (AWS CLI) is installed."; } ; else { echo "❌ aws (AWS CLI) is not installed. Please install aws to proceed. Our recommended installer is uv: 'uv tool install awscli'"; exit 1; } ; fi

if which exo >/dev/null 2>&1 ; then { echo "✅ exo (Exoscale CLI) is installed."; } ; else { echo "❌ exo (Exoscale CLI) is not installed. Please install exo to proceed. See: https://community.exoscale.com/tools/command-line-interface/"; exit 1; } ; fi

if which restic >/dev/null 2>&1 ; then { echo "✅ restic (Backup CLI) is installed."; } ; else { echo "❌ restic (Backup CLI) is not installed. Please install restic to proceed."; exit 1; } ; fi

if which gzip >/dev/null 2>&1 ; then { echo "✅ gzip (file compression) is installed."; } ; else { echo "❌ gzip (file compression) is not installed. Please install gzip to proceed."; exit 1; } ; fi

if which md5sum >/dev/null 2>&1 ; then { echo "✅ md5sum (file checksums) is installed."; } ; else { echo "❌ md5sum (file checksums) is not installed. Please install md5sum to proceed."; exit 1; } ; fi

if which virt-edit >/dev/null 2>&1 ; then { echo "✅ virt-edit (VM image editing) is installed."; } ; else { echo "❌ virt-edit (VM image editing) is not installed. Please install virt-edit to proceed."; exit 1; } ; fi

if which cpio >/dev/null 2>&1 ; then { echo "✅ cpio (file archiving) is installed."; } ; else { echo "❌ cpio (file archiving) is not installed. Please install cpio to proceed."; exit 1; } ; fi

if which emergency-credentials-receive >/dev/null 2>&1 ; then { echo "✅ emergency-credentials-receive (Cluster emergency access helper) is installed."; } ; else { echo "❌ emergency-credentials-receive is not installed. Please install it from https://github.com/vshn/emergency-credentials-receive ."; exit 1; } ; fi

echo "✅ All prerequisites are met."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I download the openshift-install binary for version "4.20"

This step downloads the openshift-install binary for the specified OpenShift version.

Outputs

  • openshift_install_bin

Script

OUTPUT=$(mktemp)


set -euo pipefail

openshift-install() {
  ./openshift-install "${@}"
}

if [[ -f openshift-install ]]
then
  echo "Found existing openshift-install binary, checking version ..."
  INSTALLED_VERSION=$(openshift-install version | sed -E -n '/openshift-install/s/^[^ ]+ v?//;s/\.[0-9]{1,2}$//p')
  if [ "$INSTALLED_VERSION" = "$MATCH_ocp_version" ]; then
    echo "✅ openshift-install version ${MATCH_ocp_version}.XX is present."
    env -i "openshift_install_bin=$( pwd )/openshift-install" >> "$OUTPUT"
    exit 0
  else
    echo "⚠️ openshift-install version $INSTALLED_VERSION is present, but version $MATCH_ocp_version is required. Deleting local binary and installing $MATCH_ocp_version"
    rm openshift-install
  fi
fi

rm -f openshift-install-linux.tar.gz
echo "Downloading openshift-install for version ${MATCH_ocp_version} ..."
curl -LO https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-${MATCH_ocp_version}/openshift-install-linux.tar.gz
tar -xf openshift-install-linux.tar.gz openshift-install

env -i "openshift_install_bin=$( pwd )/openshift-install" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And a lieutenant cluster

This step retrieves the Commodore tenant ID associated with the given lieutenant cluster ID.

Use api.syn.vshn.net as the Commodore API URL for production clusters. You might use the WebUI at control.vshn.net/syn/lieutenantapiendpoints to create and manage your clusters.

For customer clusters ensure the following facts are set:

  • sales_order: Name of the sales order to which the cluster is billed, such as S10000

  • service_level: Name of the service level agreement for this cluster, such as guaranteed-availability

  • access_policy: Access-Policy of the cluster, such as regular or swissonly

  • release_channel: Name of the syn component release channel to use, such as stable

  • maintenance_window: Pick the appropriate upgrade schedule, such as monday-1400 for test clusters, tuesday-1000 for prod or custom to not (yet) enable maintenance

  • cilium_addons: Comma-separated list of cilium addons the customer gets billed for, such as advanced_networking or tetragon. Set to NONE if no addons should be billed.

This step checks that you have access to the Commodore API and the cluster ID is valid.

Inputs

  • commodore_api_url: URL of the Commodore API to use for retrieving cluster information.

Use api.syn.vshn.net as the Commodore API URL for production clusters. Use api-int.syn.vshn.net for test clusters.

You might use the WebUI at control.vshn.net/syn/lieutenantapiendpoints to create and manage your clusters.

  • commodore_cluster_id: Project Syn cluster ID for the cluster to be set up.

In the form of c-example-infra-prod1.

You might use the WebUI at control.vshn.net/syn/lieutenantapiendpoints to create and manage your clusters.

Outputs

  • commodore_tenant_id

  • csp_region

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_api_url=
# export INPUT_commodore_cluster_id=

set -euo pipefail
export COMMODORE_API_URL="${INPUT_commodore_api_url}"

echo "Retrieving Commodore tenant ID for cluster ID '$INPUT_commodore_cluster_id' from API at '$INPUT_commodore_api_url'..."
tenant_id=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${INPUT_commodore_cluster_id} | jq -r .tenant)
if echo "$tenant_id" | grep 't-' >/dev/null 2>&1 ; then { echo "✅ Retrieved tenant ID '$tenant_id' for cluster ID '$INPUT_commodore_cluster_id'."; } else { echo "❌ Failed to retrieve valid tenant ID for cluster ID '$INPUT_commodore_cluster_id'. Got '$tenant_id'. Please check your Commodore API access and cluster ID."; exit 1; } ; fi
env -i "commodore_tenant_id=$tenant_id" >> "$OUTPUT"

region=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${INPUT_commodore_cluster_id} | jq -r .facts.region)
if test -z "$region" && test "$region" != "null" ; then { echo "❌ Failed to retrieve CSP region for cluster ID '$INPUT_commodore_cluster_id'."; exit 1; } ; else { echo "✅ Retrieved CSP region '$region' for cluster ID '$INPUT_commodore_cluster_id'."; } ; fi
env -i "csp_region=$region" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And a Keycloak service

In this step, you have to create a Keycloak service for the new cluster via the VSHN Control Web UI at control.vshn.net/vshn/services/_create

Inputs

  • commodore_cluster_id

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_cluster_id=

echo '#########################################################'
echo '#                                                       #'
echo "#  Please create a Keycloak service with the cluster's  #"
echo '#  ID as Service Name via the VSHN Control Web UI.      #'
echo '#                                                       #'
echo '#########################################################'
echo
echo "The name and ID of the service should be ${INPUT_commodore_cluster_id}."
echo "You can go to https://control.vshn.net/vshn/services/_create"
sleep 2


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And Exoscale API tokens

Create a new Exoscale API key in the correct project, with IAMv3 role "Owner".

This step currently does not validate the token’s access scope.

Inputs

  • exoscale_key: Exoscale API Key name

Used for setting up or tearing down the cluster with terraform

  • exoscale_secret: Exoscale API secret

Used for setting up or tearing down the cluster with terraform

  • csp_region: Name of the exoscale zone in which to set up the cluster.

All lowercase. For example: ch-dk-2

Outputs

  • exoscale_s3_endpoint

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_csp_region=

set -euo pipefail
export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"
exo limits > /dev/null || { echo "Invalid token!" ; exit 1 ; }
echo "Token is valid"
env -i "exoscale_s3_endpoint=sos-${INPUT_csp_region}.exo.io" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And a personal VSHN GitLab access token

This step ensures that you have provided a personal access token for VSHN GitLab.

Create the token at git.vshn.net/-/user_settings/personal_access_tokens with the "api" scope.

This step currently does not validate the token’s scope.

Inputs

  • gitlab_api_token: Personal access token for VSHN GitLab with the "api" scope.

Create the token at git.vshn.net/-/user_settings/personal_access_tokens with the "api" scope.

Outputs

  • gitlab_user_name: Your GitLab user name.

Script

OUTPUT=$(mktemp)

# export INPUT_gitlab_api_token=

set -euo pipefail
user="$( curl -sH "Authorization: Bearer ${INPUT_gitlab_api_token}" "https://git.vshn.net/api/v4/user" | jq -r .username )"
if [[ "$user" == "null" ]]
then
  echo "Error validating GitLab token. Are you sure it is valid?"
  exit 1
fi
env -i "gitlab_user_name=$user" >> "$OUTPUT"
echo "Token is valid."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And a control.vshn.net Servers API token

This step ensures that you have provided an API token for control.vshn.net Servers API.

Create the token at control.vshn.net/tokens/_create/servers and ensure your IP is allowlisted.

Inputs

  • control_vshn_api_token: API token for control.vshn.net Servers API.

Used to create the puppet based LBs.

Be extra careful with the IP allowlist.

Script

OUTPUT=$(mktemp)

# export INPUT_control_vshn_api_token=

set -euo pipefail

AUTH="X-AccessToken: ${INPUT_control_vshn_api_token}"

code="$( curl -H"$AUTH" https://control.vshn.net/api/servers/1/appuio/ -o /dev/null -w"%{http_code}" )"

if [[ "$code" != 200 ]]
then
  echo "ERROR: could not access Server API (Status $code)"
  echo "Please ensure your token is valid and your IP is on the allowlist."
  exit 1
fi


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And basic cluster information

This step collects two essential pieces of information required for cluster setup: the base domain and the Red Hat pull secret.

See kb.vshn.ch/oc4/explanations/dns_scheme.html for more information about the base domain. Get a pull secret from cloud.redhat.com/openshift/install/pull-secret.

Inputs

  • base_domain: The base domain for the cluster without the cluster ID prefix and the last dot.

Example: appuio-beta.ch

See kb.vshn.ch/oc4/explanations/dns_scheme.html for more information about the base domain.

  • redhat_pull_secret: Red Hat pull secret for accessing Red Hat container images.

Script

OUTPUT=$(mktemp)

# export INPUT_base_domain=
# export INPUT_redhat_pull_secret=

set -euo pipefail
echo "Base domain set to ${INPUT_base_domain}".
echo "${INPUT_redhat_pull_secret}" | jq > /dev/null
echo "Pull secret appears legit."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I check the Exoscale resource quotas

Checks the quotas in your Exoscale org for DNS and instances.

If the quotas are too low, you must manually increase them.

Inputs

  • exoscale_key

  • exoscale_secret

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=

set -euo pipefail
export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"

instances_used="$( exo limits -Ojson | jq '.[] | select(.resource == "Compute instances").used'  )"
instances_max="$( exo limits -Ojson | jq '.[] | select(.resource == "Compute instances").max'  )"

if (( instances_max - instances_used < 15 ))
then
  echo "Instance quota is insufficient! Please increase the instance quota."
  echo "Visit https://portal.exoscale.com/u/$( exo x get-organization | jq -r .id )/organization/quotas "
  exit 1
fi
echo "Instance quota is sufficient"

echo '###################################################################################'
echo '#                                                                                 #'
echo '#  Please check the DNS subscription and upgrade it if no DNS Domains are left.   #'
echo '#                                                                                 #'
echo '###################################################################################'
echo
echo "See https://portal.exoscale.com/u/$( exo x get-organization | jq -r .id )/dns"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I create the necessary Exoscale IAM keys

This step creates restricted API keys for object storage, CSI driver, and CCM.

Inputs

  • exoscale_key

  • exoscale_secret

  • commodore_cluster_id

Outputs

  • s3_key

  • s3_secret

  • csi_key

  • csi_secret

  • ccm_key

  • ccm_secret

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_commodore_cluster_id=

set -euo pipefail
export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"

echo "Create SOS IAM role, if it doesn't exist yet in the organization"
sos_iam_role_id=$(exo iam role list -O json | \
  jq -r '.[] | select(.name=="sos-full-access") | .key')
if [ -z "${sos_iam_role_id}" ]; then
echo '{
  "default-service-strategy": "deny",
  "services": {
    "sos": {"type": "allow"}
  }
}' | \
exo iam role create sos-full-access \
  --description "Full access to object storage service" \
  --policy -
fi
echo "TODO: Check whether the key with that name already exists"
# Create access key
exoscale_s3_credentials=$(exo iam api-key create -O json \
  "${INPUT_commodore_cluster_id}_object_storage" sos-full-access)
EXOSCALE_S3_ACCESSKEY=$(echo "${exoscale_s3_credentials}" | jq -r '.key')
EXOSCALE_S3_SECRETKEY=$(echo "${exoscale_s3_credentials}" | jq -r '.secret')

echo
echo "Create Exoscale CSI driver Exoscale IAM role, if it doesn't exist yet in the organization"
csidriver_role_id=$(exo iam role list -O json | \
  jq -r '.[] | select(.name=="csi-driver-exoscale") | .key')
if [ -z "${csidriver_role_id}" ]; then
cat << EOF | exo iam role create csi-driver-exoscale \
  --description "Exoscale CSI Driver: Access to storage operations and zone list" \
  --policy -
{
  "default-service-strategy": "deny",
  "services": {
    "compute": {
      "type": "rules",
      "rules": [
        {
          "expression": "operation in ['list-zones', 'get-block-storage-volume', 'list-block-storage-volumes', 'create-block-storage-volume', 'delete-block-storage-volume', 'attach-block-storage-volume-to-instance', 'detach-block-storage-volume', 'update-block-storage-volume-labels', 'resize-block-storage-volume', 'get-block-storage-snapshot', 'list-block-storage-snapshots', 'create-block-storage-snapshot', 'delete-block-storage-snapshot']",
          "action": "allow"
        }
      ]
    }
  }
}
EOF
fi
echo "TODO: Check whether the key with that name already exists"
# Create access key
csi_credentials=$(exo iam api-key create -O json \
  "${INPUT_commodore_cluster_id}_csi-driver-exoscale" csi-driver-exoscale)
CSI_ACCESSKEY=$(echo "${csi_credentials}" | jq -r '.key')
CSI_SECRETKEY=$(echo "${csi_credentials}" | jq -r '.secret')

echo
echo "Create Exoscale CCM Exoscale IAM role, if it doesn't exist yet in the organization"
ccm_role_id=$(exo iam role list -O json | \
  jq -r '.[] | select(.name=="ccm-exoscale") | .key')
if [ -z "${ccm_role_id}" ]; then
cat <<EOF | exo iam role create ccm-exoscale \
  --description "Exoscale CCM: Allow managing NLBs and reading instances/instance pools" \
  --policy -
{
  "default-service-strategy": "deny",
  "services": {
    "compute": {
      "type": "rules",
      "rules": [
        {
          "expression": "operation in ['add-service-to-load-balancer', 'create-load-balancer', 'delete-load-balancer', 'delete-load-balancer-service', 'get-load-balancer', 'get-load-balancer-service', 'get-operation', 'list-load-balancers', 'reset-load-balancer-field', 'reset-load-balancer-service-field', 'update-load-balancer', 'update-load-balancer-service']",
          "action": "allow"
        },
        {
          "expression": "operation in ['get-instance', 'get-instance-pool', 'get-instance-type', 'list-instances', 'list-instance-pools', 'list-zones']",
          "action": "allow"
        }
      ]
    }
  }
}
EOF
fi
echo "TODO: Check whether the key with that name already exists"
# Create access key
ccm_credentials=$(exo iam api-key create -O json \
  "${INPUT_commodore_cluster_id}_ccm-exoscale" ccm-exoscale)
CCM_ACCESSKEY=$(echo "${ccm_credentials}" | jq -r '.key')
CCM_SECRETKEY=$(echo "${ccm_credentials}" | jq -r '.secret')

env -i "s3_key=${EXOSCALE_S3_ACCESSKEY}" >> "$OUTPUT"
env -i "s3_secret=${EXOSCALE_S3_SECRETKEY}" >> "$OUTPUT"
env -i "csi_key=${CSI_ACCESSKEY}" >> "$OUTPUT"
env -i "csi_secret=${CSI_SECRETKEY}" >> "$OUTPUT"
env -i "ccm_key=${CCM_ACCESSKEY}" >> "$OUTPUT"
env -i "ccm_secret=${CCM_SECRETKEY}" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I set up required S3 buckets

This step sets up the required S3 buckets for the OpenShift cluster installation.

Inputs

  • exoscale_key

  • exoscale_secret

  • commodore_cluster_id

  • commodore_api_url

  • csp_region

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_api_url=
# export INPUT_csp_region=

set -euo pipefail
export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"
export COMMODORE_API_URL="${INPUT_commodore_api_url}"

exo storage create "sos://${INPUT_commodore_cluster_id}-bootstrap" --zone "${INPUT_csp_region}"

distribution="$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${INPUT_commodore_cluster_id} | jq -r .facts.distribution)"
if [[ "$distribution" != "oke" ]]
then
  exo storage create "sos://${INPUT_commodore_cluster_id}-logstore" --zone "${INPUT_csp_region}"
fi


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I download the OpenShift image for version "4.20.0"

This step downloads the OpenShift image for the version specified by in the step.

If the image already exists locally, it skips the download.

Outputs

  • image_path

  • image_major

  • image_minor

  • image_patch

Script

OUTPUT=$(mktemp)


set -euo pipefail

# TODO(aa): this is not generic; it makes assumptions on the location of the script on the host.
# We will need a solution where gandalf provides the workflow base dir, e.g. in an env var
# shellcheck disable=1091
. /workflows/shared/scripts/semver.sh

MAJOR=0
MINOR=0
PATCH=0
# shellcheck disable=2034
SPECIAL=""
semverParseInto "$MATCH_image_name" MAJOR MINOR PATCH SPECIAL

image_path="rhcos-$MAJOR.$MINOR.qcow2"

env -i "image_major=$MAJOR" >> "$OUTPUT"
env -i "image_minor=$MINOR" >> "$OUTPUT"
env -i "image_patch=$PATCH" >> "$OUTPUT"

echo "Image is $image_path"

if [ -f "$image_path" ]; then
  echo "Image $image_path already exists, skipping download."
  env -i "image_path=$image_path" >> "$OUTPUT"
  exit 0
fi

echo Downloading OpenShift image "$MATCH_image_name" to "$image_path"

curl -L "https://mirror.openshift.com/pub/openshift-v4/dependencies/rhcos/${MAJOR}.${MINOR}/${MATCH_image_name}/rhcos-${MATCH_image_name}-x86_64-openstack.x86_64.qcow2.gz" | gzip -d > "$image_path"
env -i "image_path=$image_path" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I patch the OpenShift image

In this step you need to modify the OpenShift image to make it work with exoscale.

Inputs

  • image_path

Outputs

  • patched_image_path

Script

OUTPUT=$(mktemp)

# export INPUT_image_path=

set -euo pipefail

patched_path="${INPUT_image_path%.*}-patched.qcow2"
cp "${INPUT_image_path}" "${patched_path}"
env -i "patched_image_path=$patched_path" >> "$OUTPUT"

echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
echo "@                                                                                       @"
echo "@  Please run the following command manually. (This step is not yet automated)          @"
echo "@                                                                                       @"
echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
echo
echo "sudo virt-edit -a ${patched_path} -m /dev/sda3:/ /loader/entries/ostree-1.conf -e 's/openstack/exoscale/'"
echo
echo '(If you have some time, feel free to implement automation for this step)'
sleep 3


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I import the image in Exoscale

This step uploads the Red Hat CoreOS image to the S3 bucket for the image registry.

It then imports the image into Exoscale as a custom image.

Inputs

  • patched_image_path

  • commodore_cluster_id

  • csp_region

  • image_major

  • image_minor

  • image_patch

  • exoscale_key

  • exoscale_secret

  • exoscale_s3_endpoint

Outputs

  • rhcos_template

Script

OUTPUT=$(mktemp)

# export INPUT_patched_image_path=
# export INPUT_commodore_cluster_id=
# export INPUT_csp_region=
# export INPUT_image_major=
# export INPUT_image_minor=
# export INPUT_image_patch=
# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_exoscale_s3_endpoint=

set -euo pipefail

export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"

RHCOS_VERSION="${INPUT_image_major}.${INPUT_image_minor}.${INPUT_image_patch}"

slug="$( exo compute instance-template list -v private -z "${INPUT_csp_region}" -Ojson | jq -r '.[] | select(.name | startswith("rhcos-'${INPUT_image_major}.${INPUT_image_minor}'")) | .name' )"

if [ -n "$slug" ] && [ "$slug" != "null" ]; then
  echo "Image '$slug' already exists in Exoscale, skipping upload."
  env -i "rhcos_template=$slug" >> "$OUTPUT"
  exit 0
fi


exo storage upload "${INPUT_patched_image_path}" "sos://${INPUT_commodore_cluster_id}-bootstrap" --acl public-read

sleep 3

exo compute instance-template register "rhcos-${RHCOS_VERSION}" \
  "https://${INPUT_exoscale_s3_endpoint}/${INPUT_commodore_cluster_id}-bootstrap/rhcos-${RHCOS_VERSION}.qcow2" \
  "$(md5sum ${INPUT_patched_image_path} | awk '{ print $1 }')" \
  --zone "${INPUT_csp_region}" \
  --boot-mode uefi \
  --disable-password \
  --username core \
  --description "Red Hat Enterprise Linux CoreOS (RHCOS) ${RHCOS_VERSION}"

exo storage delete -f "sos://${CLUSTER_ID}-bootstrap/rhcos-${RHCOS_VERSION}.qcow2"

export RHCOS_TEMPLATE="rhcos-${RHCOS_VERSION}"

env -i "rhcos_template=$RHCOS_TEMPLATE" >> "$OUTPUT"

echo "Image import initiated. ⚠️ TODO: Poll for completion."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I set secrets in Vault

This step stores the collected secrets and tokens in the ProjectSyn Vault.

Inputs

  • vault_address: Address of the Vault server associated with the Lieutenant API to store cluster secrets.

vault-prod.syn.vshn.net/ for production clusters.

  • commodore_cluster_id

  • commodore_tenant_id

  • s3_key

  • s3_secret

  • csi_key

  • csi_secret

  • ccm_key

  • ccm_secret

Outputs

  • hieradata_repo_user

  • hieradata_repo_token

Script

OUTPUT=$(mktemp)

# export INPUT_vault_address=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_tenant_id=
# export INPUT_s3_key=
# export INPUT_s3_secret=
# export INPUT_csi_key=
# export INPUT_csi_secret=
# export INPUT_ccm_key=
# export INPUT_ccm_secret=

set -euo pipefail

export VAULT_ADDR=${INPUT_vault_address}
vault login -method=oidc

# Set the Exoscale object storage API key
vault kv put clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/exoscale/storage_iam \
  s3_access_key=${INPUT_s3_key} \
  s3_secret_key=${INPUT_s3_secret}

# Generate an HTTP secret for the registry
vault kv put clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/registry \
  httpSecret="$(LC_ALL=C tr -cd "A-Za-z0-9" </dev/urandom | head -c 128)"

# Generate a master password for K8up backups
vault kv put clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/global-backup \
  password="$(LC_ALL=C tr -cd "A-Za-z0-9" </dev/urandom | head -c 32)"

# Generate a password for the cluster object backups
vault kv put clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/cluster-backup \
  password="$(LC_ALL=C tr -cd "A-Za-z0-9" </dev/urandom | head -c 32)"

# Set the CSI Driver Exoscale Credentials
vault kv put clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/exoscale/csi_driver \
  access_key=${INPUT_csi_key} \
  secret_key=${INPUT_csi_secret}

# Set the CCM Exoscale Credentials
vault kv put clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/exoscale/ccm \
  access_key=${INPUT_ccm_key} \
  secret_key=${INPUT_ccm_secret}


hieradata_repo_secret=$(vault kv get \
  -format=json "clusters/kv/lbaas/hieradata_repo_token" | jq '.data.data')
env -i "hieradata_repo_user=$(echo "${hieradata_repo_secret}" | jq -r '.user')" >> "$OUTPUT"
env -i "hieradata_repo_token=$(echo "${hieradata_repo_secret}" | jq -r '.token')" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I check the cluster domain

Please verify that the base domain generated is correct for your setup.

Inputs

  • commodore_cluster_id

  • base_domain

Outputs

  • cluster_domain

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_cluster_id=
# export INPUT_base_domain=

set -euo pipefail

cluster_domain="${INPUT_commodore_cluster_id}.${INPUT_base_domain}"
echo "Cluster domain is set to '$cluster_domain'"
echo "cluster_domain=$cluster_domain" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I prepare the cluster repository

This step prepares the local cluster repository by cloning the Commodore hieradata repository and setting up the necessary configuration for the specified cluster.

Inputs

  • commodore_api_url

  • commodore_cluster_id

  • commodore_tenant_id

  • cluster_domain

  • image_major

  • image_minor

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_api_url=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_tenant_id=
# export INPUT_cluster_domain=
# export INPUT_image_major=
# export INPUT_image_minor=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"

rm -rf inventory/classes/
mkdir -p inventory/classes/
git clone "$(curl -sH"Authorization: Bearer $(commodore fetch-token)" "${INPUT_commodore_api_url}/tenants/${INPUT_commodore_tenant_id}" | jq -r '.gitRepo.url')" inventory/classes/${INPUT_commodore_tenant_id}

pushd "inventory/classes/${INPUT_commodore_tenant_id}/"

yq eval -i ".parameters.openshift.baseDomain = \"${INPUT_cluster_domain}\"" \
  ${INPUT_commodore_cluster_id}.yml

git diff --exit-code --quiet || git commit -a -m "Configure cluster domain for ${INPUT_commodore_cluster_id}"

if ls openshift4.y*ml 1>/dev/null 2>&1; then
  yq eval -i '.classes += ".openshift4"' ${INPUT_commodore_cluster_id}.yml;
  git diff --exit-code --quiet || git commit -a -m "Include openshift4 class for ${INPUT_commodore_cluster_id}"
fi

yq eval -i '.applications += ["exoscale-cloud-controller-manager"]' ${INPUT_commodore_cluster_id}.yml
yq eval -i '.applications = (.applications | unique)' ${INPUT_commodore_cluster_id}.yml
git diff --exit-code --quiet || git commit -a -m "Deploy Exoscale cloud-controller-manager on ${INPUT_commodore_cluster_id}"


yq eval -i '.applications += ["cilium"]' ${INPUT_commodore_cluster_id}.yml
yq eval -i '.applications = (.applications | unique)' ${INPUT_commodore_cluster_id}.yml

yq eval -i '.parameters.openshift.infraID = "TO_BE_DEFINED"' ${INPUT_commodore_cluster_id}.yml
yq eval -i '.parameters.openshift.clusterID = "TO_BE_DEFINED"' ${INPUT_commodore_cluster_id}.yml

yq eval -i '.parameters.cilium.olm.generate_olm_deployment = true' ${INPUT_commodore_cluster_id}.yml

git diff --exit-code --quiet || git commit -a -m "Add Cilium addon to ${INPUT_commodore_cluster_id}"

git push

popd

commodore catalog compile ${INPUT_commodore_cluster_id} --push \
  --dynamic-fact kubernetesVersion.major=1 \
  --dynamic-fact kubernetesVersion.minor="$((INPUT_image_minor+13))" \
  --dynamic-fact openshiftVersion.Major=${INPUT_image_major} \
  --dynamic-fact openshiftVersion.Minor=${INPUT_image_minor}


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I configure the OpenShift installer

This step configures the OpenShift installer for the Exoscale cluster by generating the necessary installation files using Commodore.

Inputs

  • exoscale_key

  • exoscale_secret

  • commodore_cluster_id

  • commodore_tenant_id

  • base_domain

  • cluster_domain

  • vault_address

  • redhat_pull_secret

  • ccm_key

  • ccm_secret

  • openshift_install_bin

Outputs

  • ignition_bootstrap

  • ssh_public_key_path

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_tenant_id=
# export INPUT_base_domain=
# export INPUT_cluster_domain=
# export INPUT_vault_address=
# export INPUT_redhat_pull_secret=
# export INPUT_ccm_key=
# export INPUT_ccm_secret=
# export INPUT_openshift_install_bin=

set -euo pipefail
export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"

openshift-install() {
  "${INPUT_openshift_install_bin}" "${@}"
}

export VAULT_ADDR="${INPUT_vault_address}"
vault login -method=oidc

ssh_private_key="$(pwd)/ssh_${INPUT_commodore_cluster_id}"
ssh_public_key="${ssh_private_key}.pub"

env -i "ssh_public_key_path=$ssh_public_key" >> "$OUTPUT"

if vault kv get -format=json clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/exoscale/ssh >/dev/null 2>&1; then
  echo "SSH keypair for cluster ${INPUT_commodore_cluster_id} already exists in Vault, skipping generation."

  vault kv get -format=json clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/exoscale/ssh | \
    jq -r '.data.data.private_key|@base64d' > "${ssh_private_key}"

  chmod 600 "${ssh_private_key}"
  ssh-keygen -f "${ssh_private_key}" -y > "${ssh_public_key}"

else
  echo "Generating new SSH keypair for cluster ${INPUT_commodore_cluster_id}."

  ssh-keygen -C "vault@${INPUT_commodore_cluster_id}" -t ed25519 -f "$ssh_private_key" -N ''

  base64_no_wrap='base64'
  if [[ "$OSTYPE" == "linux"* ]]; then
    base64_no_wrap='base64 --wrap 0'
  fi

  vault kv put clusters/kv/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}/exoscale/ssh \
    private_key="$(cat "$ssh_private_key" | eval "$base64_no_wrap")"
fi

echo Adding SSH private key to ssh-agent...
echo You might need to start the ssh-agent first using: eval "\$(ssh-agent)"
echo ssh-add "$ssh_private_key"
ssh-add "$ssh_private_key"

installer_dir="$(pwd)/target"
rm -rf "${installer_dir}"
mkdir -p "${installer_dir}"

cat > "${installer_dir}/install-config.yaml" <<EOF
apiVersion: v1
metadata:
  name: ${INPUT_commodore_cluster_id}
baseDomain: ${INPUT_base_domain}
platform:
  external:
    platformName: exoscale
    cloudControllerManager: External
networking:
  networkType: Cilium
pullSecret: |
  ${INPUT_redhat_pull_secret}
sshKey: "$(cat "$ssh_public_key")"
EOF

echo Running OpenShift installer to create manifests...
openshift-install --dir "${installer_dir}" create manifests

echo Copying machineconfigs...
machineconfigs=catalog/manifests/openshift4-nodes/10_machineconfigs.yaml
if [ -f $machineconfigs ];  then
  yq --no-doc -s \
    "\"${installer_dir}/openshift/99x_openshift-machineconfig_\" + .metadata.name" \
    $machineconfigs
fi

echo Copying Exoscale CCM manifests...
for f in catalog/manifests/exoscale-cloud-controller-manager/manager/*; do
  cp "$f" "${installer_dir}/manifests/exoscale_ccm_$(basename "$f")"
done

yq -i e ".stringData.api-key=\"${INPUT_ccm_key}\",.stringData.api-secret=\"${INPUT_ccm_secret}\"" \
  "${installer_dir}/manifests/exoscale_ccm_01_secret.yaml"

echo Copying Cilium OLM manifests...
for f in catalog/manifests/cilium/olm/[a-z]*; do
  cp "$f" "${installer_dir}/manifests/cilium_$(basename "$f")"
done

# shellcheck disable=2016
# We don't want the shell to execute network.operator.openshift.io as a
# command, so we need single quotes here.
echo 'Generating initial `network.operator.openshift.io` resource...'
yq '{
"apiVersion": "operator.openshift.io/v1",
"kind": "Network",
"metadata": {
  "name": "cluster"
},
"spec": {
  "deployKubeProxy": false,
  "clusterNetwork": .spec.clusterNetwork,
  "externalIP": {
    "policy": {}
  },
  "networkType": "Cilium",
  "serviceNetwork": .spec.serviceNetwork
}}' "${installer_dir}/manifests/cluster-network-02-config.yml" \
> "${installer_dir}/manifests/cilium_cluster-network-operator.yaml"

gen_cluster_domain=$(yq e '.spec.baseDomain' \
  "${installer_dir}/manifests/cluster-dns-02-config.yml")
if [ "$gen_cluster_domain" != "$INPUT_cluster_domain" ]; then
  echo -e "\033[0;31mGenerated cluster domain doesn't match expected cluster domain: Got '$gen_cluster_domain', want '$INPUT_cluster_domain'\033[0;0m"
  exit 1
else
  echo -e "\033[0;32mGenerated cluster domain matches expected cluster domain.\033[0;0m"
fi

echo Running OpenShift installer to create ignition configs...
openshift-install --dir "${installer_dir}" \
  create ignition-configs

exo storage upload "${installer_dir}/bootstrap.ign" "sos://${INPUT_commodore_cluster_id}-bootstrap" --acl public-read

echo "✅ OpenShift installer configured successfully."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I configure Terraform for team "aldebaran"

This step configures Terraform the Commodore rendered terraform configuration.

Inputs

  • commodore_api_url

  • commodore_cluster_id

  • commodore_tenant_id

  • ssh_public_key_path

  • hieradata_repo_user

  • base_domain

  • image_major

  • image_minor

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_api_url=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_tenant_id=
# export INPUT_ssh_public_key_path=
# export INPUT_hieradata_repo_user=
# export INPUT_base_domain=
# export INPUT_image_major=
# export INPUT_image_minor=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"

installer_dir="$(pwd)/target"

pushd "inventory/classes/${INPUT_commodore_tenant_id}/"

yq eval -i '.classes += ["global.distribution.openshift4.no-opsgenie"]' ${INPUT_commodore_cluster_id}.yml;
yq eval -i '.classes = (.classes | unique)' ${INPUT_commodore_cluster_id}.yml

yq eval -i ".parameters.openshift.infraID = \"$(jq -r .infraID "${installer_dir}/metadata.json")\"" \
  ${INPUT_commodore_cluster_id}.yml

yq eval -i ".parameters.openshift.clusterID = \"$(jq -r .clusterID "${installer_dir}/metadata.json")\"" \
  ${INPUT_commodore_cluster_id}.yml

yq eval -i 'del(.parameters.cilium.olm.generate_olm_deployment)' \
  ${INPUT_commodore_cluster_id}.yml

yq eval -i ".parameters.openshift.ssh_key = \"$(cat ${INPUT_ssh_public_key_path})\"" \
  ${INPUT_commodore_cluster_id}.yml

ca_cert=$(jq -r '.ignition.security.tls.certificateAuthorities[0].source' \
  "${installer_dir}/master.ign" | \
  awk -F ',' '{ print $2 }' | \
  base64 --decode)

yq eval -i ".parameters.openshift4_terraform.terraform_variables.base_domain = \"${INPUT_base_domain}\"" \
  ${INPUT_commodore_cluster_id}.yml

yq eval -i ".parameters.openshift4_terraform.terraform_variables.ignition_ca = \"${ca_cert}\"" \
  ${INPUT_commodore_cluster_id}.yml

yq eval -i ".parameters.openshift4_terraform.terraform_variables.team = \"${MATCH_team_name}\"" \
  ${INPUT_commodore_cluster_id}.yml

yq eval -i ".parameters.openshift4_terraform.terraform_variables.hieradata_repo_user = \"${INPUT_hieradata_repo_user}\"" \
  ${INPUT_commodore_cluster_id}.yml

git commit -a -m "Setup cluster ${INPUT_commodore_cluster_id}"
git push

popd

commodore catalog compile ${INPUT_commodore_cluster_id} --push \
  --dynamic-fact kubernetesVersion.major=1 \
  --dynamic-fact kubernetesVersion.minor="$((INPUT_image_minor+13))" \
  --dynamic-fact openshiftVersion.Major=${INPUT_image_major} \
  --dynamic-fact openshiftVersion.Minor=${INPUT_image_minor}


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I configure Terraform for Exoscale

Review the file ./inventory/classes/${INPUT_commodore_tenant_id}/${INPUT_commodore_cluster_id}.yml. Override default parameters or add more component configurations as required for your cluster.

Then press enter to commit and compile.

Inputs

  • commodore_api_url

  • commodore_cluster_id

  • commodore_tenant_id

  • image_major

  • image_minor

  • rhcos_template

  • ssh_public_key_path

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_api_url=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_tenant_id=
# export INPUT_image_major=
# export INPUT_image_minor=
# export INPUT_rhcos_template=
# export INPUT_ssh_public_key_path=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export CLUSTER_ID="${INPUT_commodore_cluster_id}"
export TENANT_ID="${INPUT_commodore_tenant_id}"

pushd "inventory/classes/${TENANT_ID}/"

yq eval -i ".parameters.openshift4_terraform.terraform_variables.ssh_key = \"$(cat ${INPUT_ssh_public_key_path})\"" \
  ${INPUT_commodore_cluster_id}.yml

yq eval -i ".parameters.openshift4_terraform.terraform_variables.rhcos_template = \"${INPUT_rhcos_template}\"" ${CLUSTER_ID}.yml

echo "Commit changes"
git commit -a -m "Set Exoscale specific values in cluster ${CLUSTER_ID}"
git push

popd

echo "Compile and push cluster catalog"
commodore catalog compile ${CLUSTER_ID} --push \
  --dynamic-fact kubernetesVersion.major=1 \
  --dynamic-fact kubernetesVersion.minor="$((INPUT_image_minor+13))" \
  --dynamic-fact openshiftVersion.Major=${INPUT_image_major} \
  --dynamic-fact openshiftVersion.Minor=${INPUT_image_minor}

echo "✅ Changes committed and cluster catalog compiled successfully."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I provision the loadbalancers

This step provisions the load balancers for the Exoscale OpenShift cluster using Terraform.

Inputs

  • exoscale_key

  • exoscale_secret

  • commodore_cluster_id

  • control_vshn_api_token

  • gitlab_user_name

  • gitlab_api_token

  • commodore_api_url

  • cluster_domain

  • rhcos_template

  • csp_region

Outputs

  • lb_fqdn_1

  • lb_fqdn_2

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_commodore_cluster_id=
# export INPUT_control_vshn_api_token=
# export INPUT_gitlab_user_name=
# export INPUT_gitlab_api_token=
# export INPUT_commodore_api_url=
# export INPUT_cluster_domain=
# export INPUT_rhcos_template=
# export INPUT_csp_region=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export CLUSTER_ID="${INPUT_commodore_cluster_id}"
export GITLAB_TOKEN="${INPUT_gitlab_api_token}"
export GITLAB_USER="${INPUT_gitlab_user_name}"

cat <<EOF > ./terraform.env
EXOSCALE_API_KEY=${INPUT_exoscale_key}
EXOSCALE_API_SECRET=${INPUT_exoscale_secret}
TF_VAR_control_vshn_net_token=${INPUT_control_vshn_api_token}
TF_VAR_region=${INPUT_csp_region}
TF_VAR_rhcos_template=${INPUT_rhcos_template}
GIT_AUTHOR_NAME=$(git config --global user.name)
GIT_AUTHOR_EMAIL=$(git config --global user.email)
HIERADATA_REPO_TOKEN=${INPUT_gitlab_api_token}
EOF

# Set terraform image and tag to be used
tf_image=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.image" \
  dependencies/openshift4-terraform/class/defaults.yml)
tf_tag=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.tag" \
  dependencies/openshift4-terraform/class/defaults.yml)

# Generate the terraform alias
base_dir=$(pwd)
terraform() {
  touch .terraformrc
  docker run --rm -e REAL_UID="$(id -u)" -e TF_CLI_CONFIG_FILE=/tf/.terraformrc --env-file "${base_dir}/terraform.env" -w /tf -v "$(pwd):/tf" --ulimit memlock=-1 "${tf_image}:${tf_tag}" /tf/terraform.sh "${@}"
}

GITLAB_REPOSITORY_URL=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r '.gitRepo.url' | sed 's|ssh://||; s|/|:|')
GITLAB_REPOSITORY_NAME=${GITLAB_REPOSITORY_URL##*/}
GITLAB_CATALOG_PROJECT_ID=$(curl -sH "Authorization: Bearer ${GITLAB_TOKEN}" "https://git.vshn.net/api/v4/projects?simple=true&search=${GITLAB_REPOSITORY_NAME/.git}" | jq -r ".[] | select(.ssh_url_to_repo == \"${GITLAB_REPOSITORY_URL}\") | .id")
GITLAB_STATE_URL="https://git.vshn.net/api/v4/projects/${GITLAB_CATALOG_PROJECT_ID}/terraform/state/cluster"

pushd catalog/manifests/openshift4-terraform/

terraform init \
  "-backend-config=address=${GITLAB_STATE_URL}" \
  "-backend-config=lock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=unlock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=username=${GITLAB_USER}" \
  "-backend-config=password=${GITLAB_TOKEN}" \
  "-backend-config=lock_method=POST" \
  "-backend-config=unlock_method=DELETE" \
  "-backend-config=retry_wait_min=5"

if [ -f override.tf ]; then
  echo "override.tf already exists, not changing it"
else
  cat > override.tf <<EOF
  module "cluster" {
    bootstrap_count          = 0
    lb_count                 = 0
    master_count             = 0
    infra_count              = 0
    storage_count            = 0
    worker_count             = 0
    additional_worker_groups = {}
  }
EOF
fi

terraform apply -auto-approve

cat > override.tf <<EOF
module "cluster" {
  bootstrap_count          = 0
  master_count             = 0
  infra_count              = 0
  storage_count            = 0
  worker_count             = 0
  additional_worker_groups = {}
}
EOF
terraform apply -auto-approve -target "module.cluster.module.lb.module.hiera"

echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
echo "@                                                                                       @"
echo "@  Please review and merge the LB hieradata MR listed in Terraform output hieradata_mr. @"
echo "@                                                                                       @"
echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"

sleep 3
while (GITLAB_HOST=git.vshn.net GITLAB_TOKEN="${INPUT_gitlab_api_token}" glab mr list -R=appuio/appuio_hieradata | grep "${INPUT_commodore_cluster_id}")
do
  sleep 10
done
echo PR merged, waiting for CI to finish...
sleep 10
while (GITLAB_HOST=git.vshn.net GITLAB_TOKEN="${INPUT_gitlab_api_token}" glab mr list -R=appuio/appuio_hieradata | grep "running")
do
  sleep 10
done

terraform apply -auto-approve
terraform output -raw cluster_dns > ../../../dns.txt
echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
echo "@                                                                                       @"
echo "@  Please add the DNS records shown in the Terraform output to your DNS provider.       @"
echo "@  Most probably in https://git.vshn.net/vshn/vshn_zonefiles                            @"
echo "@                                                                                       @"
echo "@  If terminal selection does not work the entries can also be copied from              @"
echo "@    dns.txt                                                                            @"
echo "@                                                                                       @"
echo "@  Waiting for record to propagate...                                                   @"
echo "@                                                                                       @"
echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
while ! (host "api.${INPUT_cluster_domain}")
do
  sleep 15
done
rm ../../../dns.txt

lb1=$(terraform state show "module.cluster.module.lb.exoscale_domain_record.lb[0]" | grep hostname | awk '{print $3}' | tr -d ' "\r\n')
lb2=$(terraform state show "module.cluster.module.lb.exoscale_domain_record.lb[1]" | grep hostname | awk '{print $3}' | tr -d ' "\r\n')

echo "Loadbalancer FQDNs: $lb1 , $lb2"

echo "Waiting for HAproxy ..."
while true; do
  curl --connect-timeout 1 "http://api.${INPUT_cluster_domain}:6443" &>/dev/null || exit_code=$?
  if [ "$exit_code" -eq 52 ]; then
    echo "  HAproxy up!"
    break
  else
    echo -n "."
    sleep 5
  fi
done

echo "updating ssh config..."
ssh management2.corp.vshn.net "sshop --output-archive /dev/stdout" | tar -C ~ --overwrite -xzf -
echo "done"

echo "waiting for ssh access ..."
ssh "${lb1}" hostname -f
ssh "${lb2}" hostname -f

env -i "lb_fqdn_1=$lb1" >> "$OUTPUT"
env -i "lb_fqdn_2=$lb2" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I provision the bootstrap node

This step provisions the bootstrap node for the Exoscale OpenShift cluster using Terraform.

First, check the "Server created" tickets for the LBs and link them to the cluster setup ticket: ticket.vshn.net/issues/?jql=project%20%3D%20APPU%20AND%20status%20%3D%20New%20AND%20text%20~%20%22server%20created%22

Then, press enter to continue

Inputs

  • exoscale_key

  • exoscale_secret

  • control_vshn_api_token

  • gitlab_user_name

  • gitlab_api_token

  • commodore_cluster_id

  • commodore_api_url

  • lb_fqdn_1

  • lb_fqdn_2

Outputs

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_control_vshn_api_token=
# export INPUT_gitlab_user_name=
# export INPUT_gitlab_api_token=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_api_url=
# export INPUT_lb_fqdn_1=
# export INPUT_lb_fqdn_2=

set -euo pipefail

installer_dir="$(pwd)/target"
export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export CLUSTER_ID="${INPUT_commodore_cluster_id}"
export GITLAB_TOKEN="${INPUT_gitlab_api_token}"
export GITLAB_USER="${INPUT_gitlab_user_name}"

cat <<EOF > ./terraform.env
EXOSCALE_API_KEY=${INPUT_exoscale_key}
EXOSCALE_API_SECRET=${INPUT_exoscale_secret}
TF_VAR_control_vshn_net_token=${INPUT_control_vshn_api_token}
GIT_AUTHOR_NAME=$(git config --global user.name)
GIT_AUTHOR_EMAIL=$(git config --global user.email)
HIERADATA_REPO_TOKEN=${INPUT_gitlab_api_token}
EOF

# Set terraform image and tag to be used
tf_image=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.image" \
  dependencies/openshift4-terraform/class/defaults.yml)
tf_tag=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.tag" \
  dependencies/openshift4-terraform/class/defaults.yml)

# Generate the terraform alias
base_dir=$(pwd)
terraform() {
  touch .terraformrc
  docker run --rm -e REAL_UID="$(id -u)" -e TF_CLI_CONFIG_FILE=/tf/.terraformrc --env-file "${base_dir}/terraform.env" -w /tf -v "$(pwd):/tf" --ulimit memlock=-1 "${tf_image}:${tf_tag}" /tf/terraform.sh "${@}"
}


GITLAB_REPOSITORY_URL=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r '.gitRepo.url' | sed 's|ssh://||; s|/|:|')
GITLAB_REPOSITORY_NAME=${GITLAB_REPOSITORY_URL##*/}
GITLAB_CATALOG_PROJECT_ID=$(curl -sH "Authorization: Bearer ${GITLAB_TOKEN}" "https://git.vshn.net/api/v4/projects?simple=true&search=${GITLAB_REPOSITORY_NAME/.git}" | jq -r ".[] | select(.ssh_url_to_repo == \"${GITLAB_REPOSITORY_URL}\") | .id")
GITLAB_STATE_URL="https://git.vshn.net/api/v4/projects/${GITLAB_CATALOG_PROJECT_ID}/terraform/state/cluster"

pushd catalog/manifests/openshift4-terraform/

terraform init \
  "-backend-config=address=${GITLAB_STATE_URL}" \
  "-backend-config=lock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=unlock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=username=${GITLAB_USER}" \
  "-backend-config=password=${GITLAB_TOKEN}" \
  "-backend-config=lock_method=POST" \
  "-backend-config=unlock_method=DELETE" \
  "-backend-config=retry_wait_min=5"

cat > override.tf <<EOF
module "cluster" {
  bootstrap_count          = 1
  master_count             = 0
  infra_count              = 0
  storage_count            = 0
  worker_count             = 0
  additional_worker_groups = {}
}
EOF
terraform apply -auto-approve

echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
echo "@                                                                                       @"
echo "@  Please review and merge the LB hieradata MR listed in Terraform output hieradata_mr. @"
echo "@                                                                                       @"
echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
sleep 3
while (GITLAB_HOST=git.vshn.net GITLAB_TOKEN="${INPUT_gitlab_api_token}" glab mr list -R=appuio/appuio_hieradata | grep "${INPUT_commodore_cluster_id}")
do
  sleep 10
done
echo PR merged, waiting for CI to finish...
sleep 10
while (GITLAB_HOST=git.vshn.net GITLAB_TOKEN="${INPUT_gitlab_api_token}" glab mr list -R=appuio/appuio_hieradata | grep "running")
do
  sleep 10
done

ssh "${INPUT_lb_fqdn_1}" sudo puppetctl run
ssh "${INPUT_lb_fqdn_2}" sudo puppetctl run

echo -n "Waiting for Bootstrap API to become available .."
API_URL=$(yq e '.clusters[0].cluster.server' "${installer_dir}/auth/kubeconfig")
while ! curl --connect-timeout 1 "${API_URL}/healthz" -k &>/dev/null; do
  echo -n "."
  sleep 5
done && echo "✅ API is up"

env -i "kubeconfig_path=${installer_dir}/auth/kubeconfig" >> "$OUTPUT"


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I provision the control plane

This step provisions the control plane nodes with Terraform.

Inputs

  • exoscale_key

  • exoscale_secret

  • control_vshn_api_token

  • gitlab_user_name

  • gitlab_api_token

  • commodore_cluster_id

  • commodore_api_url

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_control_vshn_api_token=
# export INPUT_gitlab_user_name=
# export INPUT_gitlab_api_token=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_api_url=
# export INPUT_kubeconfig_path=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export CLUSTER_ID="${INPUT_commodore_cluster_id}"
export GITLAB_TOKEN="${INPUT_gitlab_api_token}"
export GITLAB_USER="${INPUT_gitlab_user_name}"

cat <<EOF > ./terraform.env
EXOSCALE_API_KEY=${INPUT_exoscale_key}
EXOSCALE_API_SECRET=${INPUT_exoscale_secret}
TF_VAR_control_vshn_net_token=${INPUT_control_vshn_api_token}
GIT_AUTHOR_NAME=$(git config --global user.name)
GIT_AUTHOR_EMAIL=$(git config --global user.email)
HIERADATA_REPO_TOKEN=${INPUT_gitlab_api_token}
EOF

# Set terraform image and tag to be used
tf_image=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.image" \
  dependencies/openshift4-terraform/class/defaults.yml)
tf_tag=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.tag" \
  dependencies/openshift4-terraform/class/defaults.yml)

# Generate the terraform alias
base_dir=$(pwd)
terraform() {
  touch .terraformrc
  docker run --rm -e REAL_UID="$(id -u)" -e TF_CLI_CONFIG_FILE=/tf/.terraformrc --env-file "${base_dir}/terraform.env" -w /tf -v "$(pwd):/tf" --ulimit memlock=-1 "${tf_image}:${tf_tag}" /tf/terraform.sh "${@}"
}

GITLAB_REPOSITORY_URL=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r '.gitRepo.url' | sed 's|ssh://||; s|/|:|')
GITLAB_REPOSITORY_NAME=${GITLAB_REPOSITORY_URL##*/}
GITLAB_CATALOG_PROJECT_ID=$(curl -sH "Authorization: Bearer ${GITLAB_TOKEN}" "https://git.vshn.net/api/v4/projects?simple=true&search=${GITLAB_REPOSITORY_NAME/.git}" | jq -r ".[] | select(.ssh_url_to_repo == \"${GITLAB_REPOSITORY_URL}\") | .id")
GITLAB_STATE_URL="https://git.vshn.net/api/v4/projects/${GITLAB_CATALOG_PROJECT_ID}/terraform/state/cluster"

pushd catalog/manifests/openshift4-terraform/

terraform init \
  "-backend-config=address=${GITLAB_STATE_URL}" \
  "-backend-config=lock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=unlock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=username=${GITLAB_USER}" \
  "-backend-config=password=${GITLAB_TOKEN}" \
  "-backend-config=lock_method=POST" \
  "-backend-config=unlock_method=DELETE" \
  "-backend-config=retry_wait_min=5"

cat > override.tf <<EOF
module "cluster" {
  bootstrap_count          = 1
  infra_count              = 0
  storage_count            = 0
  worker_count             = 0
  additional_worker_groups = {}
}
EOF

echo "Running Terraform ..."

terraform apply -auto-approve

export KUBECONFIG="${INPUT_kubeconfig_path}"

echo "Waiting for masters to become ready ..."
kubectl wait --for create --timeout=600s node -l node-role.kubernetes.io/master
kubectl wait --for condition=ready --timeout=600s node -l node-role.kubernetes.io/master
popd


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I deploy initial manifests

This step deploys some manifests required during bootstrap, including cert-manager, machine-api-provider, machinesets, loadbalancer controller, and ingress loadbalancer.

Inputs

  • commodore_api_url

  • vault_address

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_api_url=
# export INPUT_vault_address=
# export INPUT_kubeconfig_path=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export KUBECONFIG="${INPUT_kubeconfig_path}"

export VAULT_ADDR=${INPUT_vault_address}
vault login -method=oidc

echo '# Applying cert-manager ... #'
kubectl apply -f catalog/manifests/cert-manager/00_namespace.yaml
kubectl apply -Rf catalog/manifests/cert-manager/10_cert_manager
# shellcheck disable=2046
# we need word splitting here
kubectl -n syn-cert-manager patch --type=merge \
  $(kubectl -n syn-cert-manager get deploy -oname) \
  -p '{"spec":{"template":{"spec":{"tolerations":[{"operator":"Exists"}]}}}}'
echo '# Applied cert-manager. #'
echo


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I wait for bootstrap to complete

This step waits for OpenShift bootstrap to complete successfully.

Inputs

  • openshift_install_bin

Script

OUTPUT=$(mktemp)

# export INPUT_openshift_install_bin=

set -euo pipefail
openshift-install() {
  "${INPUT_openshift_install_bin}" "${@}"
}
installer_dir="$(pwd)/target"
openshift-install --dir "${installer_dir}" \
  wait-for bootstrap-complete --log-level debug


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I provision the remaining nodes

After successful bootstrapping, this step removes the bootstrap node again.

Inputs

  • exoscale_key

  • exoscale_secret

  • control_vshn_api_token

  • gitlab_user_name

  • gitlab_api_token

  • commodore_cluster_id

  • commodore_api_url

  • lb_fqdn_1

  • lb_fqdn_2

Script

OUTPUT=$(mktemp)

# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=
# export INPUT_control_vshn_api_token=
# export INPUT_gitlab_user_name=
# export INPUT_gitlab_api_token=
# export INPUT_commodore_cluster_id=
# export INPUT_commodore_api_url=
# export INPUT_lb_fqdn_1=
# export INPUT_lb_fqdn_2=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export CLUSTER_ID="${INPUT_commodore_cluster_id}"
export GITLAB_TOKEN="${INPUT_gitlab_api_token}"
export GITLAB_USER="${INPUT_gitlab_user_name}"

cat <<EOF > ./terraform.env
EXOSCALE_API_KEY=${INPUT_exoscale_key}
EXOSCALE_API_SECRET=${INPUT_exoscale_secret}
TF_VAR_control_vshn_net_token=${INPUT_control_vshn_api_token}
GIT_AUTHOR_NAME=$(git config --global user.name)
GIT_AUTHOR_EMAIL=$(git config --global user.email)
HIERADATA_REPO_TOKEN=${INPUT_gitlab_api_token}
EOF

# Set terraform image and tag to be used
tf_image=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.image" \
  dependencies/openshift4-terraform/class/defaults.yml)
tf_tag=$(\
  yq eval ".parameters.openshift4_terraform.images.terraform.tag" \
  dependencies/openshift4-terraform/class/defaults.yml)

# Generate the terraform alias
base_dir=$(pwd)
terraform() {
  touch .terraformrc
  docker run --rm -e REAL_UID="$(id -u)" -e TF_CLI_CONFIG_FILE=/tf/.terraformrc --env-file "${base_dir}/terraform.env" -w /tf -v "$(pwd):/tf" --ulimit memlock=-1 "${tf_image}:${tf_tag}" /tf/terraform.sh "${@}"
}

GITLAB_REPOSITORY_URL=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r '.gitRepo.url' | sed 's|ssh://||; s|/|:|')
GITLAB_REPOSITORY_NAME=${GITLAB_REPOSITORY_URL##*/}
GITLAB_CATALOG_PROJECT_ID=$(curl -sH "Authorization: Bearer ${GITLAB_TOKEN}" "https://git.vshn.net/api/v4/projects?simple=true&search=${GITLAB_REPOSITORY_NAME/.git}" | jq -r ".[] | select(.ssh_url_to_repo == \"${GITLAB_REPOSITORY_URL}\") | .id")
GITLAB_STATE_URL="https://git.vshn.net/api/v4/projects/${GITLAB_CATALOG_PROJECT_ID}/terraform/state/cluster"

pushd catalog/manifests/openshift4-terraform/

terraform init \
  "-backend-config=address=${GITLAB_STATE_URL}" \
  "-backend-config=lock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=unlock_address=${GITLAB_STATE_URL}/lock" \
  "-backend-config=username=${GITLAB_USER}" \
  "-backend-config=password=${GITLAB_TOKEN}" \
  "-backend-config=lock_method=POST" \
  "-backend-config=unlock_method=DELETE" \
  "-backend-config=retry_wait_min=5"

rm -f override.tf
terraform apply --auto-approve

echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
echo "@                                                                                       @"
echo "@  Please review and merge the LB hieradata MR listed in Terraform output hieradata_mr. @"
echo "@                                                                                       @"
echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
sleep 3
while (GITLAB_HOST=git.vshn.net GITLAB_TOKEN="${INPUT_gitlab_api_token}" glab mr list -R=appuio/appuio_hieradata | grep "${INPUT_commodore_cluster_id}")
do
  sleep 10
done
echo PR merged, waiting for CI to finish...
sleep 10
while (GITLAB_HOST=git.vshn.net GITLAB_TOKEN="${INPUT_gitlab_api_token}" glab mr list -R=appuio/appuio_hieradata | grep "running")
do
  sleep 10
done

ssh "${INPUT_lb_fqdn_1}" sudo puppetctl run
ssh "${INPUT_lb_fqdn_2}" sudo puppetctl run

popd


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I approve and label the new nodes

This step approves the new nodes that joined the cluster after provisioning and adds the "ready" label to them.

Inputs

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_kubeconfig_path=

set -euo pipefail
export KUBECONFIG="${INPUT_kubeconfig_path}"

echo '##### CAUTION: This step does not currently wait for all CSRs to be created. #####'
echo '#####    Please re-run this step until no more CSRs are getting signed.      #####'
sleep 3

# TODO: approve CSRs until all nodes are labelled and ready.
echo "Approving node certs..."
csrs=$(oc get csr -o go-template='{{range .items}}{{if not .status}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}')
if [ -n "$csrs" ]; then
  echo "$csrs" | xargs oc adm certificate approve
fi

echo "Label infra nodes..."
kubectl get node -ojson | \
  jq -r '.items[] | select(.metadata.name | test("infra-")).metadata.name' | \
  xargs -I {} kubectl label node {} node-role.kubernetes.io/infra=

echo "Label and taint storage nodes..."
kubectl get node -ojson | \
  jq -r '.items[] | select(.metadata.name | test("storage-")).metadata.name' | \
  xargs -I {} kubectl  label node {} node-role.kubernetes.io/storage=

kubectl taint node -lnode-role.kubernetes.io/storage storagenode=True:NoSchedule --overwrite

echo "Label worker nodes..."
kubectl get node -ojson | \
  jq -r '.items[] | select(.metadata.name | test("infra|master|storage-")|not).metadata.name' | \
  xargs -I {} kubectl label node {} node-role.kubernetes.io/app=

echo "✅ All new nodes approved and labeled successfully."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I configure initial deployments

This step configures some deployments that require manual changes after cluster bootstrap, such as reverting the Cilium patch from earlier, enabling proxy protocol on the Ingress controller, and scheduling the ingress controller on the infrastructure nodes.

Inputs

  • commodore_cluster_id

  • commodore_api_url

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_cluster_id=
# export INPUT_commodore_api_url=
# export INPUT_kubeconfig_path=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export KUBECONFIG="${INPUT_kubeconfig_path}"

echo '# Enabling proxy protocol ... #'
kubectl -n openshift-ingress-operator patch ingresscontroller default --type=json \
  -p '[{
    "op":"replace",
    "path":"/spec/endpointPublishingStrategy",
    "value": {"type": "HostNetwork", "hostNetwork": {"protocol": "PROXY"}}
  }]'
echo '# Enabled proxy protocol. #'
echo

distribution="$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${INPUT_commodore_cluster_id} | jq -r .facts.distribution)"
if [[ "$distribution" != "oke" ]]
then
  echo '# Scheduling ingress controller on infra nodes ... #'
  kubectl -n openshift-ingress-operator patch ingresscontroller default --type=json \
    -p '[{
      "op":"replace",
      "path":"/spec/nodePlacement",
      "value":{"nodeSelector":{"matchLabels":{"node-role.kubernetes.io/infra":""}}}
    }]'
  echo '# Scheduled ingress controller on infra nodes. #'
  echo
fi

echo '# Removing temporary cert-manager tolerations ... #'
# shellcheck disable=2046
# we need word splitting here
kubectl -n syn-cert-manager patch --type=json \
  $(kubectl -n syn-cert-manager get deploy -oname) \
  -p '[{"op":"remove","path":"/spec/template/spec/tolerations"}]'
echo '# Removed temporary cert-manager tolerations. #'

echo "✅ Initial deployments configured successfully."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I wait for installation to complete

This step waits for OpenShift installation to complete successfully.

Inputs

  • openshift_install_bin

Script

OUTPUT=$(mktemp)

# export INPUT_openshift_install_bin=

set -euo pipefail
openshift-install() {
  "${INPUT_openshift_install_bin}" "${@}"
}
installer_dir="$(pwd)/target"
openshift-install --dir "${installer_dir}" \
  wait-for install-complete --log-level debug


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I create the registry S3 secret

This step creates a secret with the S3 credentials for the internal registry.

Inputs

  • kubeconfig_path

  • s3_key

  • s3_secret

Script

OUTPUT=$(mktemp)

# export INPUT_kubeconfig_path=
# export INPUT_s3_key=
# export INPUT_s3_secret=

set -euo pipefail

export KUBECONFIG="${INPUT_kubeconfig_path}"
secret_name="image-registry-private-configuration-user"

if oc get secret "${secret_name}" -n openshift-image-registry &>/dev/null; then
  echo "Secret ${secret_name} already exists, skipping creation."
else
  oc create secret generic image-registry-private-configuration-user \
    --namespace openshift-image-registry \
    --from-literal=REGISTRY_STORAGE_S3_ACCESSKEY=${INPUT_s3_key} \
    --from-literal=REGISTRY_STORAGE_S3_SECRETKEY=${INPUT_s3_secret}
fi

echo "✅ Registry S3 secret created successfully."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I enable default instance pool annotation injector for LB services

This step enables default instance pool annotation injector for LoadBalancer services.

Inputs

  • commodore_cluster_id

  • commodore_tenant_id

  • commodore_api_url

  • gitlab_user_name

  • gitlab_api_token

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_cluster_id=
# export INPUT_commodore_tenant_id=
# export INPUT_commodore_api_url=
# export INPUT_gitlab_user_name=
# export INPUT_gitlab_api_token=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export CLUSTER_ID="${INPUT_commodore_cluster_id}"
export TENANT_ID="${INPUT_commodore_tenant_id}"
export GITLAB_USER="${INPUT_gitlab_user_name}"
export GITLAB_TOKEN="${INPUT_gitlab_api_token}"

GITLAB_REPOSITORY_URL=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r '.gitRepo.url' | sed 's|ssh://||; s|/|:|')
GITLAB_REPOSITORY_NAME=${GITLAB_REPOSITORY_URL##*/}
GITLAB_CATALOG_PROJECT_ID=$(curl -sH "Authorization: Bearer ${GITLAB_TOKEN}" "https://git.vshn.net/api/v4/projects?simple=true&search=${GITLAB_REPOSITORY_NAME/.git}" | jq -r ".[] | select(.ssh_url_to_repo == \"${GITLAB_REPOSITORY_URL}\") | .id")
GITLAB_STATE_URL="https://git.vshn.net/api/v4/projects/${GITLAB_CATALOG_PROJECT_ID}/terraform/state/cluster"

pushd "inventory/classes/${TENANT_ID}/"

curl -fsu "${GITLAB_USER}:${GITLAB_TOKEN}" "$GITLAB_STATE_URL" |\
  jq '[.resources[] | select(.module == "module.cluster.module.worker" and .type == "exoscale_instance_pool")][0].instances[0].attributes.id' |\
  yq ea -i 'select(fileIndex == 0) * (select(fileIndex == 1) | {"parameters": {"exoscale_cloud_controller_manager":{"serviceLoadBalancerDefaultAnnotations":{"service.beta.kubernetes.io/exoscale-loadbalancer-service-instancepool-id": .}}}}) ' \
  "$CLUSTER_ID.yml" -
git commit -a -m "${CLUSTER_ID}: Enable default instance pool annotation injector for LoadBalancer services"
git push
popd


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I synthesize the cluster

This step enables Project Syn on the cluster.

Inputs

  • commodore_api_url

  • commodore_cluster_id

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_api_url=
# export INPUT_commodore_cluster_id=
# export INPUT_kubeconfig_path=

set -euo pipefail

export COMMODORE_API_URL="${INPUT_commodore_api_url}"
export KUBECONFIG="${INPUT_kubeconfig_path}"
LIEUTENANT_AUTH="Authorization:Bearer $(commodore fetch-token)"

if ! kubectl get deploy -n syn steward > /dev/null; then
  INSTALL_URL=$(curl -H "${LIEUTENANT_AUTH}" "${COMMODORE_API_URL}/clusters/${INPUT_commodore_cluster_id}" | jq -r ".installURL")

  if [[ $INSTALL_URL == "null" ]]
  # TODO(aa): consider doing this programmatically - especially if, at a later point, we add the lieutenant kubeconfig to the inputs anyway
  then
      echo '###################################################################################'
      echo '#                                                                                 #'
      echo '#  Could not fetch install URL! Please reset the bootstrap token and try again.   #'
      echo '#                                                                                 #'
      echo '###################################################################################'
      echo
      echo 'See https://kb.vshn.ch/corp-tech/projectsyn/explanation/bootstrap-token.html#_resetting_the_bootstrap_token'
      exit 1
  fi

  echo "# Deploying steward ..."
  kubectl create -f "$INSTALL_URL"
fi

echo "# Waiting for ArgoCD resource to exist ..."
kubectl wait --for=create crds/argocds.argoproj.io --timeout=5m
kubectl api-resources --api-group=argoproj.io # to refresh local K8s API cache
sleep 1 # to prevent race conditions

echo "# Waiting for ArgoCD instance to exist ..."
kubectl wait --for=create argocd/syn-argocd -nsyn --timeout=90s

echo "# Waiting for ArgoCD instance to be ready ..."
kubectl wait --for=jsonpath='{.status.phase}'=Available argocd/syn-argocd -nsyn --timeout=5m

echo "Done."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I set acme-dns CNAME records

This step ensures CNAME records exist for ACME challenges once cert-manager is properly deployed. This step is optional, but required if you are using Let’s Encrypt as your certificate authority.

Inputs

  • kubeconfig_path

  • no_letsencrypt: y/n: Set no_letsencrypt to 'y' if you want to skip this step, because you are not using Let’s Encrypt as your certificate authority.

  • cluster_domain

  • exoscale_key

  • exoscale_secret

Script

OUTPUT=$(mktemp)

# export INPUT_kubeconfig_path=
# export INPUT_no_letsencrypt=
# export INPUT_cluster_domain=
# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=

set -euo pipefail

if [[ "${INPUT_no_letsencrypt}" == "y" ]]; then
  echo "no_letsencrypt is set to 'y', skipping ACME DNS record setup."
  exit 0
fi

export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"

export KUBECONFIG="${INPUT_kubeconfig_path}"

echo '# Waiting for cert-manager namespace ...'
kubectl wait --for=create ns/syn-cert-manager --timeout 5m
echo '# Waiting for cert-manager secret ...'
kubectl wait --for=create secret/acme-dns-client -nsyn-cert-manager --timeout 10m

fulldomain=""

while [[ -z "$fulldomain" ]]
do
  fulldomain=$(kubectl -n syn-cert-manager \
    get secret acme-dns-client \
    -o jsonpath='{.data.acmedns\.json}' | \
    base64 -d  | \
    jq -r '[.[]][0].fulldomain')
  echo "$fulldomain"
done

for cname in "api" "apps"; do
  exo dns add CNAME "${INPUT_cluster_domain}" -n "_acme-challenge.${cname}" -a "${fulldomain}." -t 600
done

echo
echo "✅ ACME DNS records created successfully."


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I verify emergency access

This step ensures the emergency credentials for the cluster can be retrieved.

Inputs

  • kubeconfig_path

  • cluster_domain

  • commodore_cluster_id

  • passbolt_passphrase: Your password for Passbolt.

This is required to access the encrypted emergency credentials.

Outputs

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_kubeconfig_path=
# export INPUT_cluster_domain=
# export INPUT_commodore_cluster_id=
# export INPUT_passbolt_passphrase=

set -euo pipefail
export KUBECONFIG="${INPUT_kubeconfig_path}"

echo '# Waiting for emergency-credentials-controller namespace ...'
kubectl wait --for=create ns/appuio-emergency-credentials-controller
echo '# Waiting for emergency-credentials-controller ...'
kubectl wait --for=create secret/acme-dns-client -nsyn-cert-manager

echo '# Waiting for emergency credential tokens ...'
until kubectl -n appuio-emergency-credentials-controller get emergencyaccounts.cluster.appuio.io -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.lastTokenCreationTimestamp}{"\n"}{end}' | grep "$( date '+%Y' )" >/dev/null
do
  echo -n .
done

export EMR_KUBERNETES_ENDPOINT=https://api.${INPUT_cluster_domain}:6443
export EMR_PASSPHRASE="${INPUT_passbolt_passphrase}"
emergency-credentials-receive "${INPUT_commodore_cluster_id}"

yq -i e '.clusters[0].cluster.insecure-skip-tls-verify = true' "em-${INPUT_commodore_cluster_id}"
export KUBECONFIG="em-${INPUT_commodore_cluster_id}"
kubectl get nodes
oc whoami | grep system:serviceaccount:appuio-emergency-credentials-controller: || exit 1

env -i "kubeconfig_path=$(pwd)/em-${INPUT_commodore_cluster_id}" >> "$OUTPUT"

echo "#  Invalidating 10-year admin kubeconfig ..."
kubectl -n openshift-config patch cm admin-kubeconfig-client-ca --type=merge -p '{"data": {"ca-bundle.crt": ""}}'


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I configure the cluster alerts

This step configures monitoring alerts on the cluster.

Inputs

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_kubeconfig_path=

set -euo pipefail
export KUBECONFIG="${INPUT_kubeconfig_path}"

kubectl wait --for create --timeout=10m -n openshift-monitoring cronjob silence
echo '# Installing default alert silence ...'
oc --as=system:admin -n openshift-monitoring get job silence-manual &>/dev/null || \
  oc --as=system:admin -n openshift-monitoring create job --from=cronjob/silence silence-manual
oc wait -n openshift-monitoring --for=condition=complete job/silence-manual
oc --as=system:admin -n openshift-monitoring delete job/silence-manual

echo '# Retrieving active alerts ...'
kubectl --as=system:admin -n openshift-monitoring exec sts/alertmanager-main -- \
  amtool --alertmanager.url=http://localhost:9093 alert --active

echo
echo '#######################################################'
echo '#                                                     #'
echo '#  Please review the list of open alerts above,       #'
echo '#  address any that require action before proceeding. #'
echo '#                                                     #'
echo '#######################################################'
sleep 2


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I enable Opsgenie alerting

This step enables Opsgenie alerting for the cluster via Project Syn.

Inputs

  • commodore_cluster_id

  • commodore_tenant_id

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_cluster_id=
# export INPUT_commodore_tenant_id=

set -euo pipefail
pushd "inventory/classes/${INPUT_commodore_tenant_id}/"
yq eval -i 'del(.classes[] | select(. == "*.no-opsgenie"))' ${INPUT_commodore_cluster_id}.yml
git commit -a -m "Enable opsgenie alerting on cluster ${INPUT_commodore_cluster_id}"
git push
popd


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I schedule the first maintenance

This step verifies that the UpgradeConfig object is present on the cluster, and schedules a first maintenance.

Inputs

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_kubeconfig_path=

set -euo pipefail
export KUBECONFIG="${INPUT_kubeconfig_path}"

numconfs="$( kubectl -n appuio-openshift-upgrade-controller get upgradeconfig -oyaml | yq '.items | length' )"

if (( numconfs < 1 ))
then
  kubectl -n appuio-openshift-upgrade-controller get upgradeconfig
  echo
  echo ERROR: did not find an upgradeconfig
  echo Please review the output above and ensure an upgradeconfig is present.
  echo
  echo "Double check the cluster's maintenance_window fact."
  exit 1
fi

echo '# Scheduling a first maintenance ...'

uc="$(yq .parameters.facts.maintenance_window inventory/classes/params/cluster.yml)"
kubectl -n appuio-openshift-upgrade-controller get upgradeconfig "$uc" -oyaml | \
  yq '
    .metadata.name = "first",
    .metadata.labels = {},
    .spec.jobTemplate.metadata.labels.upgradeconfig/name = "first",
    .spec.schedule.cron = ((now+"1m")| tz("Europe/Zurich") | format_datetime("4 15")) + " * * *",
    .spec.pinVersionWindow = "0m"
  ' | \
  kubectl create -f - --as=system:admin


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

Then I configure apt-dater groups for the LoadBalancers

This step configures the apt-dater groups for the LoadBalancers via puppet.

Inputs

  • lb_fqdn_1

  • lb_fqdn_2

  • gitlab_api_token

  • commodore_cluster_id

Script

OUTPUT=$(mktemp)

# export INPUT_lb_fqdn_1=
# export INPUT_lb_fqdn_2=
# export INPUT_gitlab_api_token=
# export INPUT_commodore_cluster_id=

set -euo pipefail

if [ -e nodes_hieradata ]
then
  rm -rf nodes_hieradata
fi
git clone git@git.vshn.net:vshn-puppet/nodes_hieradata.git
pushd nodes_hieradata

if ! grep "s_apt_dater::host::group" "${INPUT_lb_fqdn_1}"
then
# NOTE(aa): no indentation because here documents are ... something
cat >"${INPUT_lb_fqdn_1}.yaml" <<EOF
---
s_apt_dater::host::group: '2200_20_night_main'
EOF
fi

if ! grep "s_apt_dater::host::group" "${INPUT_lb_fqdn_2}"
then
# NOTE(aa): no indentation because here documents are ... something
cat >"${INPUT_lb_fqdn_2}.yaml" <<EOF
---
s_apt_dater::host::group: '2200_40_night_second'
EOF
fi

git add ./*.yaml

if git diff-index --quiet HEAD
then
  echo "No changes, skipping commit"
else
  git commit -m"Configure apt-dater groups for LBs for OCP4 cluster ${INPUT_commodore_cluster_id}"
  git push origin master

  echo Waiting for CI to finish...
  sleep 10
  while (GITLAB_HOST=git.vshn.net GITLAB_TOKEN="${INPUT_gitlab_api_token}" glab ci list -R=vshn-puppet/nodes_hieradata | grep "running")
  do
    sleep 10
  done

  echo Running puppet ...
  for fqdn in "${INPUT_lb_fqdn_1}" "${INPUT_lb_fqdn_2}"
  do
    ssh "${fqdn}" sudo puppetctl run
  done
fi || true
popd


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I remove the bootstrap bucket

This step deletes the S3 bucket with the bootstrap ignition config.

Inputs

  • commodore_cluster_id

  • exoscale_key

  • exoscale_secret

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_cluster_id=
# export INPUT_exoscale_key=
# export INPUT_exoscale_secret=

set -euo pipefail

export EXOSCALE_API_KEY="${INPUT_exoscale_key}"
export EXOSCALE_API_SECRET="${INPUT_exoscale_secret}"

exo storage rb -r -f "sos://${INPUT_commodore_cluster_id}-bootstrap"

# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I add the cluster to openshift4-clusters

This step adds the cluster to git.vshn.net/vshn/openshift4-clusters

Inputs

  • commodore_cluster_id

  • kubeconfig_path

  • jumphost_fqdn: FQDN of the jumphost used to connect to this cluster, if any.

If no jumphost is used, enter "NONE".

  • socks5_port: SOCKS5 port number to use for this cluster, of the form 120XX. If the cluster shares a proxy jumphost with another cluster, use the same port. If the cluster uses a brand new jumphost, choose a new unique port.

If the cluster does not use a proxy jumphost, enter "NONE".

Script

OUTPUT=$(mktemp)

# export INPUT_commodore_cluster_id=
# export INPUT_kubeconfig_path=
# export INPUT_jumphost_fqdn=
# export INPUT_socks5_port=

set -euo pipefail
if [ -e openshift4-clusters ]
then
  rm -rf openshift4-clusters
fi
git clone git@git.vshn.net:vshn/openshift4-clusters.git
pushd openshift4-clusters

if [[ -d "${INPUT_commodore_cluster_id}" ]]
then
  echo "Cluster entry already exists - not touching that!"
  exit 0
else
  API_URL=$(yq e '.clusters[0].cluster.server' "${INPUT_kubeconfig_path}")

  mkdir -p "${INPUT_commodore_cluster_id}"
  pushd "${INPUT_commodore_cluster_id}"
  ln -s ../base_envrc .envrc
  cat >.connection_facts <<EOF
API=${API_URL}
EOF
  popd

  port="$( echo "${INPUT_socks5_port}" | tr '[:upper:]' '[:lower:]' )"
  jumphost="$( echo "${INPUT_jumphost_fqdn}" | tr '[:upper:]' '[:lower:]' )"

  if [[ "$port" != "none" ]] && [[ "$jumphost" != "none" ]]
  then
    cat >> "${INPUT_commodore_cluster_id}/.connection_facts" <<EOF
JUMPHOST=${INPUT_jumphost_fqdn}
SOCKS5_PORT=${INPUT_socks5_port}
EOF
    python foxyproxy_generate.py
  fi

  git add --force "${INPUT_commodore_cluster_id}"
  git add .

  if git diff-index --quiet HEAD
  then
    echo "No changes, skipping commit"
  else
    git commit -am "Add cluster ${INPUT_commodore_cluster_id}"
  fi || true
fi
popd

echo
echo '#########################################################'
echo '#                                                       #'
echo '#  Please test the cluster connection, and if it works  #'
echo '#  as expected, push the commit to the repository.      #'
echo '#                                                       #'
echo '#########################################################'
echo
echo "Run the following:"
echo "cd $(pwd)/openshift4-clusters/${INPUT_commodore_cluster_id}"
echo "direnv allow ."
echo "direnv exec . oc whoami"
echo "git push origin main   # only if everything is OK"
sleep 2


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"

And I wait for maintenance to complete

This step waits for the first maintenance to complete, and then removes the initial UpgradeConfig.

Inputs

  • kubeconfig_path

Script

OUTPUT=$(mktemp)

# export INPUT_kubeconfig_path=

set -euo pipefail
export KUBECONFIG="${INPUT_kubeconfig_path}"

echo "#  Waiting for initial maintenance to complete ..."
oc get clusterversion
until kubectl wait --for=condition=Succeeded upgradejob -l "upgradeconfig/name=first" -n appuio-openshift-upgrade-controller 2>/dev/null
do
  oc get clusterversion | grep -v NAME
done

echo "#  Deleting initial UpgradeConfig ..."
kubectl --as=system:admin -n appuio-openshift-upgrade-controller \
  delete upgradeconfig first


# echo "# Outputs"
# cat "$OUTPUT"
# rm -f "$OUTPUT"