Kcli allows to quickly interact with different virtualization platforms to build machines with some specific configurations, and via the use of plans it allows to automate most of the setup required to have an environment ready.

In our case, let’s setup an environment to practice with Open Cluster Management but instead of using kind clusters, let’s use VM’s.


We’ll require to setup an openshift_pull.json file for Kcli to consume when accessing the required resources for this to work. That file, contains the credentials for accessing several container registries used for the deployment.


The script described below can be downloaded from kcli.sh.

Let’s first cover the prerequisites for the different pieces we’re going to use:

# Install tmux
dnf -y install tmux

# Upgrade packages
dnf -y upgrade

# Enable epel
dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm

# Install a proper editor
dnf -y install joe

# Install container engine
dnf -y install podman

# Install go
dnf -y install go

# Extend path
echo 'PATH=$PATH:~/go/bin' >~/.bashrc

At this point we’ve our system ready to use go, and some other utilities available.

Let’s now continue with clusteradm and some other utilities like kubectl:

# Install clusteradm
curl -L https://raw.githubusercontent.com/open-cluster-management-io/clusteradm/main/install.sh | bash

# Install kubectl
curl -L https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl >/usr/bin/kubectl
chmod +x /usr/bin/kubectl

Now, let’s go to install Kcli requirements from its documentation:

# Prepare Kcli installation

sudo yum -y install libvirt libvirt-daemon-driver-qemu qemu-kvm
sudo usermod -aG qemu,libvirt $(id -un)
sudo newgrp libvirt
sudo systemctl enable --now libvirtd

# Optional if using docker daemon
sudo groupadd docker
sudo usermod -aG docker $(id -un)
sudo systemctl restart docker

sudo dnf -y copr enable karmab/kcli
sudo dnf -y install kcli

At this step, we should make sure that our host, has the default virt-pool configured so that we can continue with the creation of the cluster.

For doing so, we’ll use the following plan, defined with a Jinja template.

As you can see, we first define some parameters for the whole cluster, specially the number of machines to create, the Kcli network to use, addressing, etc.

  cluster: cluster
  domain: karmalabs.corp
  number: 2
  network: default

{{ network }}:
  type: network
  cidr: {{ cidr }}

Then, we define the network and define the first stanza for the hub cluster.

{% set num = 0 %}

{% set api_ip = cidr|network_ip(200 + num ) %}
{% set cluster = 'hub' %}
  type: cluster
  kubetype: openshift
  domain: {{ domain }}
  ctlplanes: 1
  api_ip: {{ api_ip }}
  numcpus: 16
  memory: 32768

 type: dns
 net: {{ network }}
 ip: {{ api_ip }}
 - api.{{ cluster }}.{{ domain }}
 - api-int.{{ cluster }}.{{ domain }}

{% if num == 0 %}
 type: dns
 net: {{ network }}
 ip: {{ api_ip }}
 - console-openshift-console.apps.{{ cluster }}.{{ domain }}
 - oauth-openshift.apps.{{ cluster }}.{{ domain }}
 - prometheus-k8s-openshift-monitoring.apps.{{ cluster }}.{{ domain }}
 - canary-openshift-ingress-canary.apps.{{ cluster }}.{{ domain }}
 - multicloud-console.apps.{{ cluster }}.{{ domain }}
{% endif %}

And now, we’ll iterate to generate the stanzas for the spoke clusters:

{% for num in range(1, number) %}
{% set api_ip = cidr|network_ip(200 + num ) %}
{% set cluster = "cluster" %}

cluster{{ num }}:
  type: cluster
  kubetype: openshift
  domain: {{ domain }}
  ctlplanes: 1
  api_ip: {{ api_ip }}
  numcpus: 16
  memory: 32768

api-cluster{{ num}}:
 type: dns
 net: {{ network }}
 ip: {{ api_ip }}
 - api.{{ cluster }}{{ num }}.{{ domain }}
 - api-int.{{ cluster }}{{ num }}.{{ domain }}

{% endfor %}

This definition uses a new feature provided by Kcli which allows to start the deployment in parallel, so let’s get ready for it:

# Download openshift-install to avoid bug when downloading in parallel during plan creation
for command in oc openshift-install; do
  kcli download ${command}
  mv ${command} /usr/bin/

# Create the plan
kcli create plan -f kcli-plan-hub-spoke.yml

Here, Kcli will have created the different VM’s, kubeconfig files, etc to get access to the environment, so that we can continue with the Open Cluster Management part:

# Prepare clusteradm on HUB
export KUBECONFIG=/root/.kcli/clusters/hub/auth/kubeconfig
clusteradm init --wait
kubectl -n open-cluster-management get

# Add the Policy framework
clusteradm install hub-addon --names governance-policy-framework

# Get values we'll need for adding spokes
apiserver=$(clusteradm get token | grep -v token= | tr " " "\n" | grep apiserver -A1 | tail -1)


# Join the spokes to the cluster
for spoke in $(seq 1 ${MAXSPOKE}); do
  export KUBECONFIG=/root/.kcli/clusters/hub/auth/kubeconfig
  token=$(clusteradm get token } | grep token= | cut -d "=" -f 2-)
  export KUBECONFIG=/root/.kcli/clusters/cluster${spoke}/auth/kubeconfig
  clusteradm join --hub-token ${token} --hub-apiserver ${apiserver} --wait --cluster-name "cluster${spoke}" # --force-internal-endpoint-lookup

Each host (spoke) will connect to the hub and reach it to request a signed certificate and being accepted as spoke, we can perform some diagnosis when checking the klusterlet status:

# Check clusterlet status
for spoke in $(seq 1 ${MAXSPOKE}); do
  export KUBECONFIG=/root/.kcli/clusters/cluster${spoke}/auth/kubeconfig
  kubectl get klusterlet

And the pending Certificate Signing Requests (CSR):

# Check pending CSR
export KUBECONFIG=/root/.kcli/clusters/hub/auth/kubeconfig
kubectl get csr

Last step, is to accept, from the HUB, all the requests received from the spoke clusters.

# Accept joins from HUB
for spoke in $(seq 1 ${MAXSPOKE}); do
  export KUBECONFIG=/root/.kcli/clusters/hub/auth/kubeconfig
  clusteradm accept --clusters cluster${spoke}

With this, we’ve an environment with several spoke clusters and one hub, that we can use to test the scenarios described at https://open-cluster-management.io/scenarios/.

Enjoy! (and if you do, you can Buy Me a Coffee )