Operator-facing Ansible bootstrap for CAPI on OpenStack using CAPO
  • Python 57%
  • Go 13.4%
  • Jinja 12%
  • Shell 10.3%
  • Go Template 3.6%
  • Other 3.7%
Find a file
2026-10-28 10:41:21 +00:00
charts im-provision: reuse only tokens with non-empty scope (ID returns no userId for empty-scope tokens) 2026-10-27 18:57:39 +00:00
config chore(demo): add exact HEX-55 native tenant namespace pair 2026-10-27 19:24:21 +00:00
docs fix(routing): bind routing revision independently of foundation addons 2026-10-25 01:51:08 +00:00
gitops govstore: deploy bb-joget to demo-apps (dashboard) 2026-10-28 08:02:51 +00:00
inventories feat(network): optional public ingress from the LoadBalancer gateway to Spokes 2026-10-08 16:33:05 +02:00
operator/cluster-bridge Bridge 0.2.14: 0 workers also require schedulable live control-plane Nodes; read KCP/child only for 0 workers 2026-10-05 12:42:42 +02:00
playbooks fix(network): allow scoped X-Road client TLS service ingress 2026-10-28 10:20:54 +00:00
scripts chore(demo): add exact HEX-55 native tenant namespace pair 2026-10-27 19:24:21 +00:00
templates Merge pull request 'Permit exact configured routing revision for the routing addon' (#188) from samuel/hex55-routing-revision-binding into main 2026-10-27 09:53:24 +00:00
tests fix(network): allow scoped X-Road client TLS service ingress 2026-10-28 10:20:54 +00:00
tofu fix(network): allow scoped X-Road client TLS service ingress 2026-10-28 10:20:54 +00:00
upstream chore(deps): pin rollout verified legacy kubelet metrics 2026-10-12 14:04:59 +02:00
.gitattributes chore(vendor): import SeaweedFS chart 4.48.0 2026-10-09 19:34:27 +02:00
.gitignore Streamline CAPO load balancer reconciliation 2026-08-05 13:46:28 +02:00
.gitmodules chore: initialize GovStack bootstrap repository 2026-08-04 09:27:54 +02:00
AGENTS.md Update documentation 2026-08-04 16:19:43 +02:00
ansible.cfg chore: initialize GovStack bootstrap repository 2026-08-04 09:27:54 +02:00
Makefile fix(demo-apps): bind repo placement to strict admission contract 2026-10-22 11:09:36 +00:00
README.md Lab capacity: KSM, zram, Ceph OSD memory cap (playbook 16) and OpenTofu-owned lab flavors (playbook 17) 2026-10-02 16:08:12 +02:00
requirements.txt Review: declare bcrypt; lock creation/verify/replace per environment; unique temp file 2026-10-05 14:41:01 +02:00
requirements.yml Update documentation 2026-08-04 16:19:43 +02:00
VERSIONS.yaml Merge pull request 'feat(spokes): verified kubelet serving certificates and metrics-server' (#127) from feat/metrics-server-verified-kubelet-tls into main 2026-10-10 11:09:37 +00:00

GovStack Bootstrap

Bootstrap and Management-GitOps foundation for on-demand CAPI-on-OpenStack Spokes using CAPO.

This repository is intentionally separate from the deployed GovStack workspace. It does not target or modify an existing cluster by default.

Repository https://git.vanillacore.net/govstack/govstack-bootstrap (git clone https://git.vanillacore.net/govstack/govstack-bootstrap.git)
Dashboard https://git.vanillacore.net/govstack-apps/govstack-dashboard
Spoke lifecycle guide docs/spoke-lifecycle.md
Cluster bridge operator operator/cluster-bridge/README.md
Playbooks playbooks/README.md
Architecture docs/architecture.md

Single source of truth: everything, including the Spoke declarations in gitops/spokes/<name>/, lives on main. There is no separate Spoke branch. Argo CD follows main for Spokes (gitops.spoke_revision: main); the Management manifests are pinned to an exact main commit (gitops.management_revision, applied by playbook 61).

Quick start: day-2 operations

Task How
List Spokes kubectl -n govstack-clusters get gss or the dashboard card "Spokes (GovStackSpoke)"
Scale workers / control plane Dashboard +/−, or scripts/spoke-lifecycle.py scale --spoke gitops/spokes/<name> --workers N, then commit to main
Replace Machines Dashboard menu, or scripts/spoke-lifecycle.py replace --spoke gitops/spokes/<name> --pool workers
New Spoke scripts/render-spoke-manifests.py, then commit gitops/spokes/<name>/ to main
Delete a Spoke Remove the directory (PR), scripts/delete-spoke-resources.sh --confirm <name>, playbook 95
Roll out a Management change Merge the PR, set gitops.management_revision to the merge SHA, run playbook 61 (and 62 for the dashboard)

Minimum target

  1. One OpenTofu-created public gateway VM, configured by Ansible for floating-IP termination, routing/NAT, and WireGuard.
  2. One OpenTofu-created single-node Kubernetes Management seed, configured by Ansible behind the gateway.
  3. CAPI, kubeadm providers, CAPO, and required CAPO dependencies on the seed.
  4. Argo CD on the Management seed as the declarative delivery layer.
  5. A LoadBalancer Kubernetes cluster created and lifecycle-managed by CAPO.
  6. Foundational services on every CAPO cluster: Calico, kube-vip, cert-manager, Contour/Envoy ingress, Longhorn storage, monitoring, and cloud object-storage integration.
  7. A digest-pinned GovStack HTTP/SNI Envoy routing stack on the LoadBalancer cluster, reconciled by Argo CD from an exact Git revision.
  8. Additional Kubernetes clusters created by the same CAPO cluster playbook and converged through the same foundation workflow.

The transitional seed bootstrap still uses Ansible for host preparation and initial Management control-plane installation. After that boundary, the intended operator interface is Git: CAPO owns child Kubernetes machines and lifecycle, while Argo CD discovers and reconciles on-demand Spokes and their foundations.

On-demand Spoke workflow

A Spoke is not a fixed inventory entry. Add a declarative cluster input and render its non-secret CAPI and foundation resources into gitops/spokes/<name>/:

cp config/clusters/spoke.example.yml config/clusters/<name>.yml
# Replace every placeholder, then render from immutable state:
python3 scripts/render-spoke-manifests.py \
  --cluster config/clusters/<name>.yml \
  --environment config/environments/<environment>.yml \
  --infrastructure-outputs state/<environment>/infrastructure-outputs.json \
  --image state/<environment>/capo-image.json \
  --force

Usage guide: docs/spoke-lifecycle.md (create, scale, replace, status, delete, troubleshooting).

Node lifecycle is CAPI-only and GitOps-driven; no tool deletes Nodes or VMs directly. Each Spoke has a GovStackSpoke (govstack.itu.int/v1alpha1, kubectl -n govstack-clusters get gss) committed in gitops/spokes/<name>/cluster.yaml: its spec is the lifecycle API (replica counts, rolloutAfter), and the cluster bridge applies it to the CAPI Cluster and reports phase, replicas, foundations and conditions in its status. Re-rendering keeps the committed lifecycle (--reset-lifecycle <current workers> destructively adopts the declaration instead). After a commit Argo CD syncs the GovStackSpoke, the bridge applies it and CAPI acts:

  • scripts/spoke-lifecycle.py scale --spoke gitops/spokes/<name> --workers N adds or removes workers (scale-down drains and deletes the oldest Machines first); --control-plane 1|3|5 changes the KubeadmControlPlane size one step per commit (1<->3, 3<->5).
  • scripts/spoke-lifecycle.py replace --spoke gitops/spokes/<name> --pool workers|control-plane|all sets topology rollout.after, so CAPI replaces every Machine one at a time (new VM, then drain and delete the old one). This is also the recovery for Nodes with obsolete provider IDs.
  • scripts/spoke-lifecycle.py upgrade --cluster ... --kubernetes-version vX.Y.Z --image ... rolls to a new release.

With gitops.spoke_revision: main (instead of a pinned SHA) the Spoke ApplicationSets follow it and a lifecycle commit applies without re-running playbook 61. scale and replace need no environment, state or credentials beyond Git write access, so the Dashboard, API and agents perform the identical operation: edit the GovStackSpoke in cluster.yaml, commit, push.

Install playbooks/61-bootstrap-spoke-management.yml once on the Management seed. It installs the CAPI-to-Argo bridge and separate Git directory ApplicationSets for CAPI resources, foundations, and bridge RBAC. Argo then discovers each gitops/spokes/* directory, applies the narrowly scoped resources, registers provisioned children, and removes stale registrations. Adding a Spoke directory creates or reconciles it; removing a Spoke directory alone is intentionally not a deletion request because the ApplicationSet preserves CAPI resources. Use the reviewed explicit teardown workflow in gitops/spokes/README.md and scripts/delete-spoke-resources.sh.

Each generated Spoke may declare a fixed gateway address and WireGuard contract. CAPO creates the tainted gateway MachineDeployment, Kubeadm installs WireGuard with a node-local generated private key, and foundation reconciliation publishes the declared external UDP port through the shared gateway. OpenStack network/subnet and credential references remain environment/state inputs; no backend-specific values or secrets are written to the Spoke directory.

Existing automation dependency

upstream/rollout/ pins the existing govstack-openstack-rollout repository. Bootstrap implementation must extract or reuse its existing OpenStack, gateway, WireGuard, kubeadm, state, and CAPI/CAPO tasks rather than creating a parallel framework.

Initialize only the top-level dependency:

git submodule update --init

Do not initialize submodules recursively.

Planned operator flow

ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/00-test-environment-access.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/01-preflight-readonly.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/20-bootstrap-infrastructure.yml -e bootstrap_apply=true
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/30-bootstrap-gateway.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/40-bootstrap-management-seed.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/50-bootstrap-capi-capo.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/15-ciab-service-endpoint-bridge.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/16-hypervisor-capacity.yml  # optional single-host capacity guard
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/17-lab-flavors.yml          # optional right-sized lab flavors (after 20)
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/60-bootstrap-argocd.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/61-bootstrap-spoke-management.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/70-build-capo-image.yml
# Transitional CIAB verification path only:
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/80-deploy-loadbalancer-cluster.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/90-reconcile-loadbalancer-foundations.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/91-configure-object-storage.yml
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/62-deploy-dashboard-management.yml  # optional dashboard
# Spoke retirement, after scripts/delete-spoke-resources.sh:
ansible-playbook -i inventories/oslab-ciab/hosts.yml playbooks/95-revoke-spoke-identity.yml -e cluster_config_file_override=$PWD/config/clusters/<name>.yml

The component entry points will be implemented incrementally. They must be independently rerunnable and fail clearly when a prerequisite component is absent.

Configuration

make config-init
# Then review the ignored config/site.yml, config/environments/oslab-ciab.yml,
# and config/clusters/loadbalancer.yml files before running components.

Never commit OpenStack credentials, WireGuard private keys, kubeconfigs, generated state, or rendered cloud-init containing secrets.

Development checks

make init
make config-init
make check

Checks are local/static only. They do not contact OpenStack or Kubernetes.