{"id":19204411,"url":"https://github.com/mmul-it/kubelab","last_synced_at":"2025-05-12T15:45:38.613Z","repository":{"id":156445868,"uuid":"605010450","full_name":"mmul-it/kubelab","owner":"mmul-it","description":"Kubelab Ansible Role - Kubernetes installation and management","archived":false,"fork":false,"pushed_at":"2024-05-31T10:42:30.000Z","size":883,"stargazers_count":21,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-20T14:42:50.672Z","etag":null,"topics":["ansible","ansible-playbook","cluster","kubernetes"],"latest_commit_sha":null,"homepage":"","language":"Jinja","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mmul-it.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-02-22T08:53:49.000Z","updated_at":"2025-03-25T12:35:44.000Z","dependencies_parsed_at":"2024-01-11T13:43:26.410Z","dependency_job_id":"e8e0ee3e-fd88-4054-a8a1-a67754a19589","html_url":"https://github.com/mmul-it/kubelab","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmul-it%2Fkubelab","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmul-it%2Fkubelab/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmul-it%2Fkubelab/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmul-it%2Fkubelab/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mmul-it","download_url":"https://codeload.github.com/mmul-it/kubelab/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253767631,"owners_count":21961145,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ansible","ansible-playbook","cluster","kubernetes"],"created_at":"2024-11-09T13:07:51.003Z","updated_at":"2025-05-12T15:45:38.563Z","avatar_url":"https://github.com/mmul-it.png","language":"Jinja","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ![Kubelab Ansible Role](./images/kubelab-github-header.png)\n\nThis role can be used to deploy a Kubernetes cluster with a fully automated and\nidempotent implementation of several components.\n\n[![Lint the project](https://github.com/mmul-it/kubelab/actions/workflows/main.yml/badge.svg)](https://github.com/mmul-it/kubelab/actions/workflows/main.yml)\n[![Ansible Galaxy](https://img.shields.io/badge/ansible--galaxy-kubelab-blue.svg)](https://galaxy.ansible.com/mmul/kubelab)\n\n## Features\n\nThis role can be configured to enable all of these features:\n\n- **Single or multi control plane cluster implementation** with HAProxy and Keepalived\n  for High Availability.\n\n- **Multi network add-ons** Flannel and Calico.\n\n- Kubernetes **Dashboard**.\n\n- **Users management** with certificate generation and `kubeconfig` file update.\n\n- **Ceph-CSI** StorageClass for block devices.\n\n- **MetalLB** load balancer for baremetal environments.\n\n- **Ingress NGINX** for service exposition.\n\n- **Cert Manager** for automated certificate management.\n\n## Install the cluster with the Ansible Playbook\n\nThe best way to prepare the environment is to use a Python VirtualEnv,\ninstalling ansible using `pip3`:\n\n```console\nuser@lab ~ # python3 -m venv ansible\nuser@lab ~ # source ansible/bin/activate\n(ansible) user@lab ~ # pip3 install ansible\nCollecting ansible\n  Using cached ansible-7.5.0-py3-none-any.whl (43.6 MB)\n...\n...\nInstalling collected packages: resolvelib, PyYAML, pycparser, packaging, MarkupSafe, jinja2, cffi, cryptography, ansible-core, ansible\nSuccessfully installed MarkupSafe-2.1.2 PyYAML-6.0 ansible-7.5.0 ansible-core-2.14.5 cffi-1.15.1 cryptography-40.0.2 jinja2-3.1.2 packaging-23.1 pycparser-2.21 resolvelib-0.8.1\n```\n\nThen you will need this role, and in this case using `ansible-galaxy` is a good\nchoice to make it all automatic:\n\n```console\n(ansible) user@lab ~ # ansible-galaxy install mmul.kubelab -p ansible/roles/\nStarting galaxy role install process\n- downloading role 'kubelab', owned by mmul\n- downloading role from https://github.com/mmul-it/kubelab/archive/main.tar.gz\n- extracting mmul.kubelab to /home/rasca/ansible/roles/mmul.kubelab\n- mmul.kubelab (main) was installed successfully\n```\n\nWith the role in place you can complete the requirements, again by using `pip3`:\n\n```console\n(ansible) user@lab ~ # pip3 install -r ansible/roles/mmul.kubelab/requirements.txt\n...\n...\nSuccessfully installed ansible-vault-2.1.0 cachetools-5.3.0 certifi-2023.5.7 charset-normalizer-3.1.0 google-auth-2.18.0 idna-3.4 kubernetes-26.1.0 oauthlib-3.2.2 pyasn1-0.5.0 pyasn1-modules-0.3.0 python-dateutil-2.8.2 requests-2.30.0 requests-oauthlib-1.3.1 rsa-4.9 six-1.16.0 urllib3-1.26.15 websocket-client-1.5.1\n```\n\nOnce requirements are available, you'll typically use the role by launching the\n`tests/kubelab.yml` playbook, like this:\n\n```console\n(ansible) user@lab ~ # ansible-playbook -i tests/inventory/kubelab tests/kubelab.yml\n```\n\n\u003cu\u003e\u003cstrong\u003eNOTE\u003c/strong\u003e\u003c/u\u003e: date \u0026 time of the involved systems are important!\nHaving a clock skew between the machine you're executing Ansible playbooks and\nthe destination machines could cause certificate verification to fail.\n\n\u003cu\u003e\u003cstrong\u003eNOTE\u003c/strong\u003e\u003c/u\u003e: you can chose anytime to reset everything by\npassing `k8s_reset` as `true`. This will reset your entire cluster, so use it\nwith caution:\n\n```console\n(ansible) user@lab ~ # ansible-playbook -i tests/inventory/kubelab tests/kubelab.yml -e k8s_reset=true\n```\n\n## Interact with the cluster after the installation\n\nOnce the playbook completes its execution the best way to interact with the\ncluster is by using the `kubectl` command that can be installed as follows:\n\n```console\nuser@lab ~ # curl -s -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"\n\nuser@lab ~ # chmod +x kubectl\n\nuser@lab ~ # sudo mv kubectl /usr/local/bin\n```\n\nThe Kubernetes role produces a local directory which contains the main\nkubeconfig file, named admin.conf. The easiest way to use it is by exporting the\nKUBECONFIG variable, like this:\n\n```console\nuser@lab ~ # export KUBECONFIG=~/kubernetes/admin.conf\n```\n\nFrom now until the end of the session, every time you'll `kubectl` it will rely\non the credentials contained in that file:\n\n```console\nuser@lab ~ # kubectl cluster-info\nKubernetes control plane is running at https://192.168.122.199:8443\nCoreDNS is running at https://192.168.122.199:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy\n\nTo further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.\n\nuser@lab ~ # kubectl get nodes\nNAME           STATUS   ROLES           AGE   VERSION\nkubernetes-1   Ready    control-plane   26h   v1.25.3\nkubernetes-2   Ready    control-plane   26h   v1.25.3\nkubernetes-3   Ready    control-plane   26h   v1.25.3\nkubernetes-4   Ready    \u003cnone\u003e          26h   v1.25.3\n```\n\nIt is also possible to use different users to log into the cluster, check the\n[Users](#Users) section for details.\n\n## Configuration\n\n### Inventory\n\nA typical inventory depends on what you want to deploy, looking at the example\n`kubelab`  you can declare inside the hosts file (see [tests/inventory/kubelab/hosts](https://github.com/mmul-it/ansible/blob/master/tests/inventory/kubelab/hosts))\nall the nodes:\n\n```ini\n# Kubernetes hosts\n[kubelab]\nkubernetes-1 k8s_role=control-plane run_non_infra_pods=true\nkubernetes-2 k8s_role=control-plane run_non_infra_pods=true\nkubernetes-3 k8s_role=control-plane run_non_infra_pods=true\nkubernetes-4 k8s_role=worker\n```\n\nYou'll set which nodes will act as control plane and also whether or not those\nwill run non infrastructure pods (so to make the control plane also a worker).\n\nThen you can define, inside group file (i.e.\n[inventory/kubelab/group_vars/kubelab.yml](https://github.com/mmul-it/kubelab/blob/master/inventory/kubelab/group_vars/kubelab.yml)),\nall the additional configurations, depending on what do you want to implement.\n\nThe name of the host group for the Kubernetes host is by default `kubelab` but\ncan be overridden by declaring the `k8s_host_group` variable.\n\n### Kubernetes cluster\n\nIf you want to implement a multi-control-plane, high availability cluster\nyou'll need to specify these variables:\n\n```yaml\nk8s_cluster_name: kubelab\n\nk8s_control_plane_node: kubernetes-1\nk8s_control_plane_port: 6443\nk8s_control_plane_cert_key: \"91bded725a628a081d74888df8745172ed842fe30c7a3898b3c63ca98c7226fd\"\n\nk8s_multi_control_plane: true\nk8s_balancer_VIP: 192.168.122.199\nk8s_balancer_interface: eth0\nk8s_balancer_port: 8443\nk8s_balancer_password: \"d6e284576158b1\"\n\nk8s_wait_timeout: 1200\n\nk8s_control_plane_ports:\n  - 2379-2380/tcp\n  - 6443/tcp\n  - 8443/tcp\n  - 10250/tcp\n  - 10257/tcp\n  - 10259/tcp\n```\n\nThis will bring up a cluster starting from node `kubernetes-1` enabling multi\ncontrol plane via `k8s_multi_control_plane` and setting the VIP address and the\ninterface.\n\n**\u003cu\u003eNote\u003c/u\u003e**: you'll want to change both `k8s_control_plane_cert_key` and\n`k8s_balancer_password` for better security.\n\n**\u003cu\u003eNote\u003c/u\u003e**: it is possible to have a more atomic way to configure pods\nnetwork cidr, worker ports , nodeports ranges and firewall management, you can\ncheck [the defaults file](https://github.com/mmul-it/ansible/blob/master/roles/kubernetes/defaults/main.yml#L50-L67).\n\n### Network Add-on\n\nThe Kubernetes role supports Flannel and Calico network add-ons. The\nconfiguration depends on which add-on you want to implement.\n\nFor Flannel you'll need something like:\n\n```yaml\n# Flannel addon\nk8s_network_addon: flannel\nk8s_network_addon_ports:\n  - 8285/udp\n  - 8472/udp\n```\n\nTo check how to implement Calico have a look at [the defaults file](https://github.com/mmul-it/ansible/blob/master/roles/kubernetes/defaults/main.yml#L69-L75).\n\n### Dashboard\n\nThe Kubernetes dashboard can be implemented by adding this to the configuration:\n\n```yaml\nk8s_dashboard_enable: true\n```\n\nOnce the installation completes the easiest way to access the dashboard is by\nusing the `kubectl proxy` and then access the related url\n[http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/](http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/).\n\nA login prompt will be presented, and you can login by passing a token. By\ndefault the Kubernetes role creates a user named `dashboard-user` (you can\noverride it).\n\nTo retrieve the token you'll need use `kubectl`, like this:\n\n```console\nuser@lab ~ # kubectl -n kubernetes-dashboard create token dashboard-user\n\u003cYOUR TOKEN\u003e\n```\n\nCopy and paste the output of the above command inside the prompt and you'll\ncomplete the login.\n\n### Users\n\nIt is possible to add users to your cluster, by declaring something like this:\n\n```yaml\nk8s_users:\n  - name: pod-viewer\n    namespace: default\n    role_name: pod-viewer-role\n    role_rules_apigroups: '\"\"'\n    role_rules_resources: '\"pods\",\"pods/exec\",\"pods/log\"'\n    role_rules_verbs: '\"*\"'\n    rolebinding_name: pod-viewer-rolebinding\n    cert_expire_days: 3650\n    update_kube_config: true\n```\n\nThis will create a local directory containing these files:\n\n```console\nuser@lab ~ # ls -1 kubernetes/users/\npod-viewer.crt\npod-viewer.csr\npod-viewer.key\nusers.conf\nusers-rolebindings.yaml\nusers-roles.yaml\n```\n\nThe `users.conf` file can then be used to access the cluster with this user,\nlike this:\n\n```console\nuser@lab ~ # export KUBECONFIG=~/kubernetes/users/users.conf\n\nrasca@catastrofe [~]\u003e kubectl config get-contexts\nCURRENT   NAME                       CLUSTER   AUTHINFO           NAMESPACE\n*         kubernetes-admin@kubelab   kubelab   kubernetes-admin\n          pod-viewer@kubelab         kubelab   pod-viewer         default\n\nuser@lab ~ # kubectl config use-context pod-viewer@kubelab\nSwitched to context \"pod-viewer@kubelab\".\n\nuser@lab ~ # kubectl config get-contexts\nCURRENT   NAME                       CLUSTER   AUTHINFO           NAMESPACE\n          kubernetes-admin@kubelab   kubelab   kubernetes-admin\n*         pod-viewer@kubelab         kubelab   pod-viewer         default\n\nuser@lab ~ # kubectl get pods\nNo resources found in default namespace.\n```\n\n### Ceph CSI\n\nThe Kubernetes role actually supports the implementation of the Ceph CSI\nStorageClass. It can be defined as follows:\n\n```yaml\nk8s_ceph_csi_enable: true\nk8s_ceph_csi_id: lab-ceph\nk8s_ceph_csi_secret_userid: kubernetes\nk8s_ceph_csi_secret_userkey: AQAWvU5jjBHSGhAAuAXtHFt0h05B5J/VHERGOA==\nk8s_ceph_csi_clusterid: d046bbb0-4ee4-11ed-8f6f-525400f292ff\nk8s_ceph_csi_pool: kubepool\nk8s_ceph_csi_monitors:\n  - 192.168.122.11:6789\n  - 192.168.122.12:6789\n  - 192.168.122.13:6789\n```\n\nThen it will be possible to declare new PVC:\n\n```yaml\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: rbd-pvc\n  namespace: rasca\nspec:\n  accessModes:\n    - ReadWriteOnce\n  volumeMode: Filesystem\n  resources:\n    requests:\n      storage: 1Gi\n  storageClassName: csi-rbd-sc\n```\n\nAnd related Pod:\n\n```yaml\n---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: csi-rbd-demo-pod\n  namespace: rasca\nspec:\n  containers:\n    - name: web-server\n      image: nginx\n      volumeMounts:\n        - name: mypvc\n          mountPath: /var/lib/www/html\n  volumes:\n    - name: mypvc\n      persistentVolumeClaim:\n        claimName: rbd-pvc\n        readOnly: false\n```\n\n**\u003cu\u003eNOTE\u003c/u\u003e**: at the moment only `rbd` provisioning is supported.\n\n### MetalLB\n\nTo enable [MetalLB](https://metallb.universe.tf/) a load-balancer implementation\nfor baremetal Kubernetes clusters, using standard routing protocols, it is\nsufficient to declare:\n\n```yaml\nk8s_metallb_enable: true\nk8s_metallb_pools:\n  - name: 'first-pool'\n    addresses: '192.168.122.100-192.168.122.130'\n```\n\nThen it will be possible to use this LoadBalancer to create IPs inside the\ndeclared pool range (check next `ingress-nginx` example to understand how).\n\n### Ingress NGINX\n\nTo enable [Ingress NGINX](https://github.com/kubernetes/ingress-nginx), an\nIngress controller for Kubernetes using NGINX as a reverse proxy and load\nbalancer, it is sufficient to declare:\n\n```yaml\nk8s_ingress_nginx_enable: true\n```\n\nThis will install the Ingress NGINX controller that can be used for different\npurposes.\n\n#### Ingress NGINX on control-planes\n\nFor example it is possible to use Ingress NGINX by exposing the `80` and `443`\nports on the balanced IP managed by haproxy, by declaring this:\n\n```yaml\nk8s_ingress_nginx_enable: true\nk8s_ingress_nginx_haproxy_conf: true\nk8s_ingress_nginx_services:\n  - name: ingress-nginx-externalip\n    spec:\n      externalIPs:\n      - 192.168.122.199\n      ports:\n      - name: port-1\n        port: 80\n        protocol: TCP\n      - name: port-2\n        port: 443\n        protocol: TCP\n      selector:\n        app.kubernetes.io/component: controller\n        app.kubernetes.io/instance: ingress-nginx\n        app.kubernetes.io/name: ingress-nginx\n```\n\nThis will expose both ports on the balanced IP (in this case `192.168.122.199`\nand will make the service responding there.\n\nTo test it just try this:\n\n```console\n$ kubectl create deployment demo --image=httpd --port=80\ndeployment.apps/demo created\n\n$ kubectl expose deployment demo\nservice/demo exposed\n\n$ kubectl create ingress demo --class=nginx \\\n    --rule=\"demo.192.168.122.199.nip.io/*=demo:80\" \\\n    --annotation=\"nginx.ingress.kubernetes.io/service-upstream=true\"\ningress.networking.k8s.io/demo created\n\n$ curl http://demo.192.168.122.199.nip.io\n\u003chtml\u003e\u003cbody\u003e\u003ch1\u003eIt works!\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e\n```\n\nOr to test TLS:\n\n```console\n$ kubectl create deployment demo --image=httpd --port=80\ndeployment.apps/demo created\n\n$ kubectl expose deployment demo\nservice/demo exposed\n\n$ openssl genrsa -out cert.key 2048\n(no output)\n\n$ openssl req -new -key cert.key -out cert.csr -subj \"/CN=demo.192.168.122.199.nip.io\"\n(no output)\n\n$ openssl x509 -req -days 366 -in cert.csr -signkey cert.key -out cert.crt\nCertificate request self-signature ok\nsubject=CN = demo.192.168.122.199.nip.io\n\n$ kubectl create secret tls tls-secret --cert=./cert.crt --key=./cert.key\nsecret/tls-secret created\n\n$ kubectl create ingress demo --class=nginx \\\n    --rule=\"demo.192.168.122.199.nip.io/*=demo:80,tls=tls-secret\" \\\n    --annotation=\"nginx.ingress.kubernetes.io/service-upstream=true\"\ningress.networking.k8s.io/demo created\n\n$ curl -k https://demo.192.168.122.199.nip.io\n\u003chtml\u003e\u003cbody\u003e\u003ch1\u003eIt works!\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e\n```\n\nThe reason why the `--annotation=\"nginx.ingress.kubernetes.io/service-upstream=true\"`\nis needed in explained in this [ingress-nginx issue](https://github.com/kubernetes/ingress-nginx/issues/8081).\n\n#### Ingress NGINX with MetalLB\n\nAnother way is to use it in combination with MetalLB, by declaring a\n`LoadBalancer` service, as follows:\n\n```yaml\nk8s_ingress_nginx_enable: true\nk8s_ingress_nginx_services:\n  - name: ingress-nginx-lb\n    spec:\n      type: LoadBalancer\n      loadBalancerIP: 192.168.122.100\n      ports:\n      - name: port-1\n        port: 80\n        protocol: TCP\n      - name: port-2\n        port: 443\n        protocol: TCP\n```\n\nThis will install everything related to the controller, and assign the\n`loadBalancerIP` that is part of the range supplied by MetalLB, by exposing both\n`80` and `443` ports.\n\n### Cert Manager\n\nTo enable [Cert Manager](https://cert-manager.io/), a controller to automate\ncertificate management in Kubernetes, it is sufficient to declare:\n\n```yaml\nk8s_cert_manager_enable: true\nk8s_cert_manager_issuers:\n  - name: letsencrypt\n    cluster: true\n    acme:\n      server: https://acme-v02.api.letsencrypt.org/directory\n      email: rasca@mmul.it\n      privateKeySecretRef:\n        name: letsencrypt\n      solvers:\n      - http01:\n          ingress:\n            class: nginx\n```\n\nThis will install everything related to the controller and create a cluster\nissuer that will use `letsencrypt` with `http01` challenge resolution, via NGINX\ningress class.\n\nOnce everything is installed and you want to expose an application, you can test\neverything by using something like this yaml:\n\n```yaml\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: rasca\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: index-html\n  namespace: rasca\ndata:\n  index.html: |\n    This is my faboulous Webserver!\n---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: nginx\n  namespace: rasca\n  labels:\n    app: nginx\nspec:\n  containers:\n    - name: web-server\n      image: nginx\n      volumeMounts:\n      - name: docroot\n        mountPath: /usr/share/nginx/html\n  volumes:\n    - name: docroot\n      configMap:\n        name: index-html\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-service\n  namespace: rasca\nspec:\n  ports:\n  - port: 80\n    protocol: TCP\n    targetPort: 80\n  selector:\n    app: nginx\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  annotations:\n    cert-manager.io/cluster-issuer: letsencrypt\n  name: nginx-ingress\n  namespace: rasca\nspec:\n  ingressClassName: nginx\n  rules:\n  - host: nginx.apps.kubelab.mmul.it\n    http:\n      paths:\n      - backend:\n          service:\n            name: nginx-service\n            port:\n              number: 80\n        path: /\n        pathType: Prefix\n  tls:\n  - hosts:\n    - nginx.apps.kubelab.mmul.it\n    secretName: nginx.apps.kubelab.mmul.it\n```\n\nIf you look specifically on the last resource, the `Ingress` named\n`nginx-ingress` you will see two important sections:\n\n- Under `metadata` -\u003e `annotations` the annotation\n  `cert-manager.io/cluster-issuer: letsencrypt`\n\n- Under `spec:` -\u003e `tls` the host declaration.\n\nWith this in place, after some time, you'll have your cert served for the\nexposed service.\n\n## License\n\nMIT\n\n## Author Information\n\nRaoul Scarazzini ([rascasoft](https://github.com/rascasoft))\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmul-it%2Fkubelab","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmmul-it%2Fkubelab","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmul-it%2Fkubelab/lists"}