diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27b8c15..4029395 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,22 @@ on: types: [opened, synchronize, reopened] workflow_dispatch: inputs: + run_acceptance: + description: "Run acceptance tests" + type: boolean + default: false k8s_versions: description: "Optional explicit Kubernetes versions (comma-separated, e.g. v1.31.0,v1.30.0). Leave blank to use the K8S_MATRIX_VERSIONS repository variable." required: false default: "" + run_crc_acceptance: + description: "Run OpenShift acceptance tests against an external OCP/CRC cluster (requires OCP_SERVER and OCP_TOKEN secrets)" + type: boolean + default: false + ocp_storage_class: + description: "OpenShift storage class override for CRC/OCP cluster (e.g. crc-csi-hostpath-provisioner)" + type: string + required: false permissions: contents: read @@ -217,3 +229,100 @@ jobs: if: always() run: make acceptance-cleanup + # --------------------------------------------------------------------------- + # OpenShift acceptance test using MicroShift AIO — same pattern as + # acceptance-test uses KIND. A throwaway OCP cluster starts inside Docker on + # the GitHub runner. No external OCP cluster, CRC, or OCP_SERVER/OCP_TOKEN + # secrets are required. + # --------------------------------------------------------------------------- + openshift-acceptance-test: + name: OpenShift Acceptance Test (MicroShift) + runs-on: ubuntu-latest + needs: [unit-test, security] + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && + github.base_ref == 'main' && + github.event.pull_request.draft == false) + env: + BOUNDARY_ADDR: ${{ secrets.BOUNDARY_ADDR }} + BOUNDARY_AUTH_METHOD_ID: ${{ secrets.BOUNDARY_AUTH_METHOD_ID }} + BOUNDARY_CLUSTER_ID: ${{ secrets.BOUNDARY_CLUSTER_ID }} + BOUNDARY_LOGIN_NAME: ${{ secrets.BOUNDARY_LOGIN_NAME }} + BOUNDARY_PASSWORD: ${{ secrets.BOUNDARY_PASSWORD }} + BOUNDARY_TARGET_ID: ${{ secrets.BOUNDARY_TARGET_ID }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Helm + run: make setup-helm + + - name: Install Boundary CLI + run: | + wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + sudo apt update && sudo apt install -y boundary + boundary version + + - name: Setup MicroShift acceptance environment + run: make microshift-setup + + - name: Run OpenShift acceptance test + run: make microshift-full + + - name: Cleanup + if: always() + run: make microshift-cleanup + + # --------------------------------------------------------------------------- + # CRC / external OCP cluster acceptance test — manual trigger only. + # Requires OCP_SERVER and OCP_TOKEN repository secrets pointing to a running + # OCP or CRC cluster. Trigger via workflow_dispatch with run_crc_acceptance=true. + # --------------------------------------------------------------------------- + crc-acceptance-test: + name: OpenShift Acceptance Test (CRC/OCP) + runs-on: ubuntu-latest + needs: [unit-test, security] + if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_crc_acceptance == 'true' + env: + BOUNDARY_ADDR: ${{ secrets.BOUNDARY_ADDR }} + BOUNDARY_AUTH_METHOD_ID: ${{ secrets.BOUNDARY_AUTH_METHOD_ID }} + BOUNDARY_CLUSTER_ID: ${{ secrets.BOUNDARY_CLUSTER_ID }} + BOUNDARY_LOGIN_NAME: ${{ secrets.BOUNDARY_LOGIN_NAME }} + BOUNDARY_PASSWORD: ${{ secrets.BOUNDARY_PASSWORD }} + BOUNDARY_TARGET_ID: ${{ secrets.BOUNDARY_TARGET_ID }} + OCP_STORAGE_CLASS: ${{ github.event.inputs.ocp_storage_class }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install oc CLI + run: | + curl -Lo /tmp/oc.tar.gz https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable/openshift-client-linux.tar.gz + sudo tar -xzf /tmp/oc.tar.gz -C /usr/local/bin oc + oc version --client + + - name: Setup Helm + run: make setup-helm + + - name: Install Boundary CLI + run: | + wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + sudo apt update && sudo apt install -y boundary + boundary version + + - name: Login to OpenShift cluster + run: | + oc login "${{ secrets.OCP_SERVER }}" \ + --token="${{ secrets.OCP_TOKEN }}" \ + --insecure-skip-tls-verify=true + + - name: Run OpenShift acceptance tests + run: make openshift-acceptance-full + + - name: Cleanup + if: always() + run: make openshift-acceptance-cleanup + diff --git a/Makefile b/Makefile index 5ec65ec..1bde62d 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,11 @@ export K8S_MATRIX_VERSIONS # ================================ .PHONY: help format deps clean lint test unit-test worker-config .PHONY: setup-helm setup-kubeconform setup-trivy setup-kubescape setup-helm-unittest lint-helm-k8s trivy-scan kubescape-scan -.PHONY: acceptance-setup acceptance-cluster acceptance-helm acceptance-test acceptance-full acceptance-cleanup +.PHONY: acceptance-setup acceptance-cluster acceptance-helm acceptance-test acceptance-full acceptance-cleanup acceptance-all .PHONY: k8s-matrix-test k8s-matrix-cleanup +.PHONY: openshift-smoke-test openshift-helm openshift-acceptance-test openshift-acceptance-full openshift-acceptance-cleanup +.PHONY: crc-setup crc-helm crc-test crc-full crc-cleanup +.PHONY: microshift-setup microshift-helm microshift-test microshift-full microshift-cleanup .PHONY: eks-setup eks-helm eks-test eks-full eks-cleanup .PHONY: tf-setup tf-destroy tf-output tf-plan .PHONY: aks-setup aks-helm aks-test aks-full aks-cleanup @@ -57,9 +60,31 @@ help: @echo " make acceptance-test - Run acceptance tests" @echo " make acceptance-full - Run full acceptance workflow (setup + worker-config + helm + tests)" @echo " make acceptance-cleanup - Delete acceptance cluster" + @echo " make acceptance-all - Run BOTH Kubernetes (KIND) and OpenShift (CRC) acceptance tests" @echo " make k8s-matrix-test - Run tcp-target-conn-test.sh across kindest/node K8s versions (set K8S_MATRIX_VERSIONS or K8S_VERSIONS)" @echo " make k8s-matrix-cleanup - Delete the acceptance cluster and generated worker config" @echo "" + @echo "OpenShift Acceptance Testing targets:" + @echo " make openshift-smoke-test - Run OpenShift cluster + chart smoke test (standalone: installs+verifies+cleans up)" + @echo " make openshift-helm - Install Helm chart on an OpenShift cluster (values.openshift.yaml)" + @echo " make openshift-acceptance-test - Run full OpenShift acceptance test suite (chart must already be deployed)" + @echo " make openshift-acceptance-full - Full OpenShift workflow (worker-config + openshift-helm + tests)" + @echo " make openshift-acceptance-cleanup - Uninstall Helm release from OpenShift" + @echo "" + @echo "OpenShift Local (CRC) Acceptance Testing targets:" + @echo " make crc-setup - Start CRC cluster and configure oc context" + @echo " make crc-helm - Install Helm chart on CRC with values.openshift.yaml" + @echo " make crc-test - Run full OpenShift acceptance suite against CRC" + @echo " make crc-full - Full CRC workflow (crc-setup + worker-config + crc-helm + crc-test)" + @echo " make crc-cleanup - Uninstall Helm release and stop CRC cluster" + @echo "" + @echo "OpenShift MicroShift (CI) Acceptance Testing targets:" + @echo " make microshift-setup - Start MicroShift AIO cluster in Docker (no external cluster needed)" + @echo " make microshift-helm - Install Helm chart on MicroShift with values.openshift.yaml" + @echo " make microshift-test - Run full OpenShift acceptance suite against MicroShift" + @echo " make microshift-full - Full MicroShift workflow (setup + worker-config + helm + test)" + @echo " make microshift-cleanup - Remove MicroShift container and cleanup" + @echo "" @echo "AWS EKS Acceptance Testing targets (shell-based, legacy):" @echo " make eks-setup - Provision EKS cluster via Terraform (tf-setup)" @echo " make eks-helm - Install Helm chart with EKS values (gp3, NLB)" @@ -549,6 +574,415 @@ acceptance-test: @echo "✅ All acceptance tests passed!" @echo "" +# ================================ +# OpenShift Acceptance Testing Targets +# ================================ + +openshift-smoke-test: + @echo "================================" + @echo "OpenShift Worker Chart Smoke Test" + @echo "================================" + @echo "" + @command -v oc >/dev/null 2>&1 || (echo "❌ oc CLI not found. Run: oc login "; exit 1) + @if [ ! -f worker.hcl ]; then \ + echo "❌ worker.hcl not found. Run 'make worker-config' first"; \ + exit 1; \ + fi + @bash tests/acceptance/openshift-smoke-test.sh + +openshift-helm: + @echo "============================================" + @echo "Installing Helm Chart on OpenShift" + @echo "============================================" + @echo "" + @command -v helm >/dev/null 2>&1 || (echo "❌ Helm not found"; exit 1) + @[ -f worker.hcl ] || { echo "❌ worker.hcl not found. Run 'make worker-config' first"; exit 1; } + @echo "Installing boundary-worker chart with values.openshift.yaml..." + @EXTRA_ARGS=""; \ + if [ -n "$${OCP_STORAGE_CLASS:-}" ]; then \ + EXTRA_ARGS="$$EXTRA_ARGS --set worker.persistence.recording.storageClass=$${OCP_STORAGE_CLASS} --set worker.persistence.authStorage.storageClass=$${OCP_STORAGE_CLASS}"; \ + fi; \ + if [ -n "$${OCP_MEM_REQUEST:-}" ]; then \ + EXTRA_ARGS="$$EXTRA_ARGS --set worker.resources.requests.memory=$${OCP_MEM_REQUEST}"; \ + fi; \ + if [ -n "$${OCP_CPU_REQUEST:-}" ]; then \ + EXTRA_ARGS="$$EXTRA_ARGS --set worker.resources.requests.cpu=$${OCP_CPU_REQUEST}"; \ + fi; \ + if [ -n "$${OCP_MEM_LIMIT:-}" ]; then \ + EXTRA_ARGS="$$EXTRA_ARGS --set worker.resources.limits.memory=$${OCP_MEM_LIMIT}"; \ + fi; \ + if [ -n "$${OCP_CPU_LIMIT:-}" ]; then \ + EXTRA_ARGS="$$EXTRA_ARGS --set worker.resources.limits.cpu=$${OCP_CPU_LIMIT}"; \ + fi; \ + helm upgrade --install boundary-worker . \ + --namespace boundary \ + --create-namespace \ + -f values.openshift.yaml \ + --set-file worker.config=worker.hcl \ + --wait \ + --timeout 5m \ + $$EXTRA_ARGS + @echo "✅ Helm chart installed on OpenShift" + @echo "" + @oc get all -n boundary + +openshift-acceptance-test: + @echo "================================" + @echo "OpenShift Acceptance Test Suite" + @echo "================================" + @echo "" + @command -v oc >/dev/null 2>&1 || (echo "❌ oc CLI not found. Run: oc login "; exit 1) + @SKIP_HELM_INSTALL=true bash tests/acceptance/openshift-smoke-test.sh + @bash tests/acceptance/openshift-tcp-target-conn-test.sh + @bash tests/acceptance/cleanup-worker.sh + @echo "✅ All OpenShift acceptance tests passed!" + @echo "" + +openshift-acceptance-full: + @echo "================================" + @echo "Running Full OpenShift Acceptance Workflow" + @echo "================================" + @echo "" + @$(MAKE) worker-config + @$(MAKE) openshift-helm + @$(MAKE) openshift-acceptance-test + @echo "" + @echo "To cleanup, run: make openshift-acceptance-cleanup" + @echo "" + +openshift-acceptance-cleanup: + @echo "================================" + @echo "Cleaning up OpenShift Acceptance" + @echo "================================" + @echo "Cleaning up worker from Boundary cluster..." + @bash tests/acceptance/cleanup-worker.sh || true + @echo "" + @echo "Uninstalling Helm release..." + @helm uninstall boundary-worker --namespace boundary 2>/dev/null && echo "✅ Helm release uninstalled" || echo "⚠️ Helm release not found" + @rm -f worker.hcl + @rm -f /tmp/boundary-worker-id.txt + @echo "✅ OpenShift acceptance cleanup complete" + +# ================================ +# OpenShift Local (CRC) Targets +# ================================ + +crc-setup: + @echo "================================" + @echo "Setting up CRC (OpenShift Local)" + @echo "================================" + @echo "" + @command -v crc >/dev/null 2>&1 || (echo "❌ crc not installed. Download from: https://developers.redhat.com/products/openshift-local"; exit 1) + @command -v oc >/dev/null 2>&1 || (echo "❌ oc CLI not found. Run: eval $$(crc oc-env)"; exit 1) + @echo "✅ crc and oc are installed" + @echo "" + @echo "Starting CRC cluster (this may take several minutes)..." + @crc start + @echo "" + @echo "Configuring oc context..." + @eval $$(crc oc-env) && \ + KUBEADMIN_PASS=$$(crc console --credentials 2>/dev/null | grep 'kubeadmin' | sed 's/.*-p \([^ ]*\) .*/\1/') && \ + oc login -u kubeadmin -p "$$KUBEADMIN_PASS" \ + --insecure-skip-tls-verify=true \ + https://api.crc.testing:6443 + @oc cluster-info + @echo "✅ CRC cluster is ready" + @echo "" + @echo "Next steps:" + @echo " - Generate worker config: make worker-config" + @echo " - Install Helm chart: make crc-helm" + @echo " - Run tests: make crc-test" + @echo " - Full workflow: make crc-full" + +crc-helm: + @echo "============================================" + @echo "Installing Helm Chart on CRC" + @echo "============================================" + @echo "" + @command -v helm >/dev/null 2>&1 || (echo "❌ Helm not found"; exit 1) + @if [ ! -f worker.hcl ]; then \ + echo "❌ worker.hcl not found. Run 'make worker-config' first"; \ + exit 1; \ + fi + @echo "Installing boundary-worker chart with values.openshift.yaml..." + @helm upgrade --install boundary-worker . \ + --namespace boundary \ + --create-namespace \ + -f values.openshift.yaml \ + --set worker.persistence.recording.storageClass=crc-csi-hostpath-provisioner \ + --set worker.persistence.authStorage.storageClass=crc-csi-hostpath-provisioner \ + --set worker.resources.requests.memory=128Mi \ + --set worker.resources.requests.cpu=50m \ + --set worker.resources.limits.memory=512Mi \ + --set worker.resources.limits.cpu=200m \ + --set-file worker.config=worker.hcl \ + --wait \ + --timeout 5m + @echo "✅ Helm chart installed successfully" + @echo "" + @echo "Deployed resources:" + @oc get all -n boundary + @echo "" + @echo "Waiting for deployment to be ready..." + @oc wait --for=condition=available --timeout=5m \ + deployment/boundary-worker-deployment \ + -n boundary + @echo "✅ Deployment is ready" + +crc-test: + @echo "================================" + @echo "CRC OpenShift Acceptance Tests" + @echo "================================" + @echo "" + @command -v oc >/dev/null 2>&1 || (echo "❌ oc CLI not found. Run: eval $$(crc oc-env)"; exit 1) + @SKIP_HELM_INSTALL=true bash tests/acceptance/openshift-smoke-test.sh + @bash tests/acceptance/openshift-tcp-target-conn-test.sh + @bash tests/acceptance/cleanup-worker.sh + @echo "✅ All CRC acceptance tests passed!" + @echo "" + +crc-full: + @echo "================================" + @echo "Running Full CRC Workflow" + @echo "================================" + @echo "" + @$(MAKE) crc-setup + @$(MAKE) worker-config + @$(MAKE) crc-helm + @$(MAKE) crc-test + @echo "" + @echo "To cleanup, run: make crc-cleanup" + @echo "" + +crc-cleanup: + @echo "================================" + @echo "Cleaning up CRC" + @echo "================================" + @echo "Cleaning up worker from Boundary cluster..." + @bash tests/acceptance/cleanup-worker.sh || true + @echo "" + @echo "Uninstalling Helm release..." + @helm uninstall boundary-worker --namespace boundary 2>/dev/null && echo "✅ Helm release uninstalled" || echo "⚠️ Helm release not found" + @rm -f worker.hcl + @rm -f /tmp/boundary-worker-id.txt + @echo "" + @echo "Stopping CRC cluster..." + @crc stop && echo "✅ CRC cluster stopped" || echo "⚠️ CRC stop failed" + @echo "✅ CRC cleanup complete" + +# ================================ +# OpenShift MicroShift (CI) Targets +# ================================ +# Runs a throwaway MicroShift AIO cluster inside Docker — the same +# pattern KIND uses for Kubernetes acceptance tests. +# No external OCP cluster, CRC, or OCP_SERVER/OCP_TOKEN secrets are needed. + +microshift-setup: + @echo "================================" + @echo "Setting up MicroShift (OpenShift CI)" + @echo "================================" + @echo "" + @echo "Checking dependencies..." + @command -v docker >/dev/null 2>&1 || (echo "❌ docker is not installed"; exit 1) + @echo "✅ docker is installed" + @command -v helm >/dev/null 2>&1 || (echo "❌ Helm not found. Run 'make setup-helm' first"; exit 1) + @echo "✅ helm is installed" + @if ! command -v oc >/dev/null 2>&1; then \ + echo "Installing oc CLI..."; \ + curl -Lo /tmp/oc.tar.gz https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable/openshift-client-linux.tar.gz; \ + sudo tar -xzf /tmp/oc.tar.gz -C /usr/local/bin oc; \ + rm -f /tmp/oc.tar.gz; \ + fi + @echo "✅ oc CLI is installed ($$(oc version --client 2>/dev/null | head -n1))" + @echo "" + @echo "Creating storage loop device and LVM volume group for MicroShift (4 GB)..." + @sudo truncate -s 4G /tmp/microshift-disk.img + @LOOP=$$(sudo losetup --find --show /tmp/microshift-disk.img) && \ + echo "$$LOOP" > /tmp/microshift-loop-device && \ + echo "✅ Loop device: $$LOOP" + @sudo apt-get install -y --quiet lvm2 2>/dev/null || true + @LOOP_DEV=$$(cat /tmp/microshift-loop-device) && \ + sudo pvcreate "$$LOOP_DEV" && \ + sudo vgcreate rhel "$$LOOP_DEV" && \ + echo "✅ LVM volume group 'rhel' created on $$LOOP_DEV" + @echo "Configuring iptables-legacy (required for MicroShift networking on Ubuntu)..." + @sudo apt-get install -y --quiet iptables 2>/dev/null || true + @sudo update-alternatives --set iptables /usr/sbin/iptables-legacy 2>/dev/null || true + @sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy 2>/dev/null || true + @echo "✅ iptables-legacy configured" + @echo "" + @echo "Configuring CRI-O storage driver (vfs required for overlay-on-overlay CI environments)..." + @printf '[storage]\ndriver = "vfs"\ngraphroot = "/var/lib/containers/storage"\nrunroot = "/run/containers/storage"\n' > /tmp/microshift-storage.conf + @echo "✅ CRI-O storage config created (vfs)" + @echo "" + @# Pinned to MicroShift AIO latest as of 2025-07 — update digest when upgrading + @echo "Starting MicroShift AIO cluster (this may take 3-5 minutes)..." + @docker run -d \ + --name microshift \ + --privileged \ + --cgroupns=host \ + --network host \ + --tmpfs /run \ + --tmpfs /tmp \ + -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ + -v /lib/modules:/lib/modules:ro \ + -v /tmp/microshift-storage.conf:/etc/containers/storage.conf:ro \ + -v microshift-data:/var/lib/microshift \ + quay.io/microshift/microshift-aio@sha256:e5c53f97b43a042e4bdceeccf980bcdf8630ece81cb75f8fd11d7d8564edc832 + @echo "Waiting for MicroShift node to be Ready (up to 10 minutes)..." + @i=0; while [ $$i -lt 120 ]; do \ + if docker exec microshift kubectl \ + --kubeconfig /var/lib/microshift/resources/kubeadmin/kubeconfig \ + get nodes --no-headers 2>/dev/null | grep -q ' Ready'; then \ + echo "✅ MicroShift node is Ready"; break; \ + fi; \ + if [ $$(( $$i % 6 )) -eq 0 ]; then \ + echo " Container status: $$(docker inspect microshift --format '{{.State.Status}}' 2>/dev/null)"; \ + echo " Service status: microshift=$$(docker exec microshift systemctl is-active microshift 2>/dev/null) crio=$$(docker exec microshift systemctl is-active crio 2>/dev/null)"; \ + docker exec microshift journalctl -u microshift --no-pager --lines=3 2>/dev/null || true; \ + fi; \ + echo " waiting... ($$(( $$i * 5 ))s)"; \ + sleep 5; i=$$(( $$i + 1 )); \ + if [ $$i -eq 120 ]; then \ + echo "❌ MicroShift node never became Ready after 10 minutes."; \ + echo "--- systemctl status ---"; \ + docker exec microshift systemctl status microshift crio --no-pager 2>/dev/null || true; \ + echo "--- MicroShift journal ---"; \ + docker exec microshift journalctl -u microshift --no-pager --lines=50 2>/dev/null || true; \ + echo "--- CRI-O journal ---"; \ + docker exec microshift journalctl -u crio --no-pager --lines=30 2>/dev/null || true; \ + exit 1; \ + fi; \ + done + @echo "" + @echo "Configuring kubeconfig..." + @mkdir -p ~/.kube + @docker cp microshift:/var/lib/microshift/resources/kubeadmin/kubeconfig ~/.kube/config + @echo "✅ Kubeconfig configured" + @echo "Waiting for CNI config file in /etc/cni/net.d/..." + @timeout 300 bash -c \ + 'until docker exec microshift ls /etc/cni/net.d/ 2>/dev/null | grep -qE "\.conf|\.conflist"; do sleep 5; done' \ + || echo "⚠️ CNI config not found; pod networking may not work" + @echo "✅ CNI config file present" + @docker exec microshift ls -la /etc/cni/net.d/ 2>/dev/null || true + @echo "Ensuring pod egress NAT (10.42.0.0/16 → internet)..." + @sudo sysctl -w net.ipv4.ip_forward=1 2>/dev/null || true + @# On Ubuntu 22.04, Docker uses iptables-nft (nftables backend) which runs BEFORE + @# iptables-legacy in the kernel. Docker's FORWARD=DROP lives in nftables, so rules + @# added only to iptables-legacy are silently bypassed. Apply to BOTH backends. + @for ipt in iptables iptables-nft; do \ + sudo $$ipt -t nat -C POSTROUTING -s 10.42.0.0/16 ! -d 10.42.0.0/16 -j MASQUERADE 2>/dev/null \ + || sudo $$ipt -t nat -I POSTROUTING 1 -s 10.42.0.0/16 ! -d 10.42.0.0/16 -j MASQUERADE 2>/dev/null \ + || true; \ + sudo $$ipt -I FORWARD 1 -s 10.42.0.0/16 -j ACCEPT 2>/dev/null || true; \ + sudo $$ipt -I FORWARD 1 -d 10.42.0.0/16 -j ACCEPT 2>/dev/null || true; \ + sudo $$ipt -I FORWARD 1 -s 10.43.0.0/16 -j ACCEPT 2>/dev/null || true; \ + sudo $$ipt -I FORWARD 1 -d 10.43.0.0/16 -j ACCEPT 2>/dev/null || true; \ + done + @echo "✅ Pod egress NAT + FORWARD rules applied to both iptables-legacy and iptables-nft" + @echo "Waiting for CoreDNS pods to be Running..." + @timeout 180 bash -c \ + 'until docker exec microshift kubectl --kubeconfig /var/lib/microshift/resources/kubeadmin/kubeconfig \ + get pods -n openshift-dns --no-headers 2>/dev/null | grep "dns-default" | grep -q " Running "; do sleep 5; done' \ + || echo "⚠️ DNS pods not yet Running; continuing" + @echo "✅ DNS pods ready" + @echo "Waiting for StorageClass topolvm-provisioner..." + @timeout 120 bash -c \ + 'until kubectl get storageclass topolvm-provisioner >/dev/null 2>&1; do sleep 3; done' \ + || echo "⚠️ topolvm-provisioner not yet available; continuing" + @kubectl cluster-info || true + @echo "✅ MicroShift cluster is ready" + @echo "" + @echo "Next steps:" + @echo " - Generate worker config: make worker-config" + @echo " - Install Helm chart: make microshift-helm" + @echo " - Run tests: make microshift-test" + @echo " - Full workflow: make microshift-full" + +microshift-helm: + @echo "============================================" + @echo "Installing Helm Chart on MicroShift" + @echo "============================================" + @echo "" + @command -v helm >/dev/null 2>&1 || (echo "❌ Helm not found"; exit 1) + @[ -f worker.hcl ] || { echo "❌ worker.hcl not found. Run 'make worker-config' first"; exit 1; } + @echo "Installing boundary-worker chart with values.openshift.yaml..." + @helm upgrade --install boundary-worker . \ + --namespace boundary \ + --create-namespace \ + -f values.openshift.yaml \ + --set worker.persistence.recording.storageClass=topolvm-provisioner \ + --set worker.persistence.authStorage.storageClass=topolvm-provisioner \ + --set worker.resources.requests.memory=128Mi \ + --set worker.resources.requests.cpu=50m \ + --set worker.resources.limits.memory=512Mi \ + --set worker.resources.limits.cpu=200m \ + --set 'openshift.podSecurityContext.runAsUser=1001' \ + --set 'openshift.containerSecurityContext.runAsUser=1001' \ + --set 'hostNetwork=true' \ + --set-file worker.config=worker.hcl \ + --wait \ + --timeout 5m + @echo "✅ Helm chart installed on MicroShift" + @echo "" + @oc get all -n boundary + +microshift-test: + @echo "================================" + @echo "MicroShift OpenShift Acceptance Tests" + @echo "================================" + @echo "" + @command -v oc >/dev/null 2>&1 || (echo "❌ oc CLI not found"; exit 1) + @SKIP_HELM_INSTALL=true bash tests/acceptance/openshift-smoke-test.sh + @bash tests/acceptance/openshift-tcp-target-conn-test.sh + @bash tests/acceptance/cleanup-worker.sh + @echo "✅ All MicroShift acceptance tests passed!" + @echo "" + +microshift-full: + @echo "================================" + @echo "Running Full MicroShift Acceptance Workflow" + @echo "================================" + @echo "" + @if docker inspect microshift >/dev/null 2>&1; then \ + echo "⚠️ MicroShift container already exists — skipping microshift-setup"; \ + else \ + $(MAKE) microshift-setup; \ + fi + @$(MAKE) worker-config + @$(MAKE) microshift-helm + @$(MAKE) microshift-test + @echo "" + @echo "To cleanup, run: make microshift-cleanup" + @echo "" + +microshift-cleanup: + @echo "================================" + @echo "Cleaning up MicroShift" + @echo "================================" + @echo "Cleaning up worker from Boundary cluster..." + @bash tests/acceptance/cleanup-worker.sh || true + @echo "" + @echo "Uninstalling Helm release..." + @helm uninstall boundary-worker --namespace boundary 2>/dev/null && echo "✅ Helm release uninstalled" || echo "⚠️ Helm release not found" + @rm -f worker.hcl + @rm -f /tmp/boundary-worker-id.txt + @echo "" + @echo "Stopping and removing MicroShift container..." + @docker stop microshift 2>/dev/null && docker rm microshift 2>/dev/null \ + && echo "✅ MicroShift container removed" || echo "⚠️ MicroShift container not found" + @docker volume rm microshift-data 2>/dev/null || true + @echo "Removing loop device, LVM and disk image..." + @if [ -f /tmp/microshift-loop-device ]; then \ + LOOP_DEV=$$(cat /tmp/microshift-loop-device); \ + sudo vgremove -f rhel 2>/dev/null || true; \ + sudo pvremove -f "$$LOOP_DEV" 2>/dev/null || true; \ + sudo losetup -d "$$LOOP_DEV" 2>/dev/null || true; \ + rm -f /tmp/microshift-loop-device; \ + fi + @sudo rm -f /tmp/microshift-disk.img + @echo "✅ MicroShift cleanup complete" + # Note: acceptance-full does NOT run the Kubernetes version matrix test. # The matrix manages its own cluster lifecycle (it deletes/recreates the @@ -571,6 +1005,20 @@ acceptance-full: @echo "To cleanup, run: make acceptance-cleanup" @echo "" +acceptance-all: + @echo "==================================================" + @echo "Running Full Acceptance Suite (Kubernetes + OpenShift)" + @echo "==================================================" + @echo "" + @echo "--- Phase 1: Kubernetes (KIND) Acceptance Tests ---" + @$(MAKE) acceptance-full + @echo "" + @echo "--- Phase 2: OpenShift (MicroShift) Acceptance Tests ---" + @$(MAKE) microshift-full + @echo "" + @echo "✅ All acceptance tests passed (Kubernetes + OpenShift)!" + @echo "" + # ================================ # Kubernetes Version Matrix Testing # ================================ diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 57166b7..b8e4be2 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -170,6 +170,13 @@ limits: memory: 256Mi {{- end }} +{{/* +Get the OpenShift Route name for the worker proxy port +*/}} +{{- define "boundary.worker.route.name" -}} +{{- printf "%s-proxy-route" (include "boundary.fullname" .) }} +{{- end }} + {{/* Get the service account name for the worker */}} diff --git a/templates/worker-deployment.yaml b/templates/worker-deployment.yaml index 797bffe..5fa4dec 100644 --- a/templates/worker-deployment.yaml +++ b/templates/worker-deployment.yaml @@ -27,19 +27,31 @@ spec: spec: serviceAccountName: {{ include "boundary.worker.serviceAccountName" . }} automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + {{- if .Values.hostNetwork }} + hostNetwork: true + dnsPolicy: Default + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} terminationGracePeriodSeconds: {{ .Values.worker.terminationGracePeriodSeconds }} securityContext: + {{- if .Values.openshift.enabled }} + {{- toYaml .Values.openshift.podSecurityContext | nindent 8 }} + {{- else }} {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- end }} containers: - name: worker image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} securityContext: + {{- if .Values.openshift.enabled }} + {{- toYaml .Values.openshift.containerSecurityContext | nindent 12 }} + {{- else }} {{- toYaml .Values.containerSecurityContext | nindent 12 }} + {{- end }} env: - name: POD_NAME valueFrom: @@ -102,10 +114,8 @@ spec: mountPath: /processed-config - name: auth-storage mountPath: {{ .Values.worker.persistence.authStorage.path | default "/var/lib/boundary" }} - {{- if .Values.worker.persistence.recording.enabled }} - name: recording-storage mountPath: {{ .Values.worker.persistence.recording.path | default "/boundary/recording" }} - {{- end }} volumes: - name: config configMap: @@ -126,6 +136,9 @@ spec: - name: recording-storage persistentVolumeClaim: claimName: {{ include "boundary.worker.recordingPvcName" . }} + {{- else }} + - name: recording-storage + emptyDir: {} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: diff --git a/templates/worker-route.yaml b/templates/worker-route.yaml new file mode 100644 index 0000000..bea9e83 --- /dev/null +++ b/templates/worker-route.yaml @@ -0,0 +1,28 @@ +# Copyright IBM Corp. 2026 + +{{- if and .Values.openshift.enabled .Values.openshift.route.proxy.enabled }} +# Boundary Worker OpenShift Route +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: {{ include "boundary.worker.route.name" . }} + namespace: {{ include "boundary.namespace" . }} + labels: + {{- include "boundary.labels" . | nindent 4 }} + app.kubernetes.io/component: worker + {{- with .Values.openshift.route.proxy.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.openshift.route.proxy.host }} + host: {{ .Values.openshift.route.proxy.host }} + {{- end }} + to: + kind: Service + name: {{ include "boundary.worker.proxy.serviceName" . }} + port: + targetPort: proxy + tls: + termination: {{ .Values.openshift.route.proxy.tls.termination }} +{{- end }} diff --git a/templates/worker-service.yaml b/templates/worker-service.yaml index 1dfe8c3..3fa3dab 100644 --- a/templates/worker-service.yaml +++ b/templates/worker-service.yaml @@ -15,7 +15,7 @@ metadata: {{- . | nindent 4 }} {{- end }} spec: - type: {{ .Values.worker.service.proxy.type }} + type: {{ if .Values.openshift.enabled }}ClusterIP{{ else }}{{ .Values.worker.service.proxy.type }}{{ end }} ports: - port: {{ .Values.worker.service.proxy.port }} targetPort: {{ .Values.worker.service.proxy.targetPort }} diff --git a/tests/acceptance/openshift-smoke-test.sh b/tests/acceptance/openshift-smoke-test.sh new file mode 100755 index 0000000..c36c611 --- /dev/null +++ b/tests/acceptance/openshift-smoke-test.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Copyright IBM Corp. 2026 + +# OpenShift Worker Chart — Acceptance Smoke Test +# Validates the Helm chart deploys correctly on OpenShift using values.openshift.yaml. + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Test configuration +NAMESPACE="boundary" +HELM_RELEASE="boundary-worker" +WORKER_HCL="${WORKER_HCL:-./worker.hcl}" +TIMEOUT="${TIMEOUT:-300}" +DEPLOY="${HELM_RELEASE}-deployment" +# When SKIP_HELM_INSTALL=true the chart is already deployed (e.g. via crc-helm or +# openshift-helm). The smoke test then only verifies existing resources. +SKIP_HELM_INSTALL="${SKIP_HELM_INSTALL:-false}" + +echo "OpenShift Worker Chart — Acceptance Smoke Test" + +# Function to print test results +print_result() { + if [ $1 -eq 0 ]; then + echo -e "${GREEN}✅${NC} $2" + else + echo -e "${RED}❌ FAILED:${NC} $2" + exit 1 + fi +} + +# Test 1: Verify OpenShift cluster is accessible +echo "Test 1: Verifying OpenShift cluster accessibility..." +if oc cluster-info > /dev/null 2>&1; then + print_result 0 "OpenShift cluster accessible" +else + print_result 1 "OpenShift cluster is not accessible. Run: oc login " +fi +echo "" + +# Test 2: Install chart with values.openshift.yaml (skipped if SKIP_HELM_INSTALL=true) +if [ "${SKIP_HELM_INSTALL}" = "true" ]; then + echo "Test 2: Verifying existing Helm release (SKIP_HELM_INSTALL=true)..." + if helm status "${HELM_RELEASE}" -n "${NAMESPACE}" > /dev/null 2>&1; then + print_result 0 "Helm release '${HELM_RELEASE}' is deployed" + else + print_result 1 "Helm release '${HELM_RELEASE}' not found. Deploy it first (e.g. make crc-helm or make openshift-helm)" + fi +else + echo "Test 2: Installing chart with values.openshift.yaml..." + [ -f "${WORKER_HCL}" ] || { echo -e "${RED}❌ FAILED:${NC} worker.hcl not found at '${WORKER_HCL}'"; exit 1; } + if helm upgrade --install "${HELM_RELEASE}" . \ + --namespace "${NAMESPACE}" \ + --create-namespace \ + -f values.openshift.yaml \ + --set-file worker.config="${WORKER_HCL}" \ + --wait \ + --timeout "${TIMEOUT}s" \ + > /dev/null 2>&1; then + print_result 0 "Helm release '${HELM_RELEASE}' installed successfully" + else + print_result 1 "helm upgrade --install failed" + fi +fi +echo "" + +# Test 3: Verify deployment is available +echo "Test 3: Verifying deployment is available..." +if kubectl wait --for=condition=available \ + --timeout="${TIMEOUT}s" \ + deployment/"${DEPLOY}" \ + -n "${NAMESPACE}" > /dev/null 2>&1; then + print_result 0 "Deployment '${DEPLOY}' is available" +else + print_result 1 "Deployment '${DEPLOY}' did not become available" +fi +echo "" + +# Test 4: Verify OpenShift Route exists with passthrough TLS +echo "Test 4: Verifying OpenShift Route exists..." +ROUTE_NAME="${HELM_RELEASE}-proxy-route" +if oc get route "${ROUTE_NAME}" -n "${NAMESPACE}" > /dev/null 2>&1; then + print_result 0 "Route '${ROUTE_NAME}' exists" +else + print_result 1 "Route '${ROUTE_NAME}' not found" +fi +TLS_TERM=$(oc get route "${ROUTE_NAME}" -n "${NAMESPACE}" \ + -o jsonpath='{.spec.tls.termination}' 2>/dev/null || true) +if [ "${TLS_TERM}" = "passthrough" ]; then + print_result 0 "Route TLS termination is 'passthrough'" +else + print_result 1 "Expected TLS termination 'passthrough', got '${TLS_TERM:-empty}'" +fi +echo "" + +# Test 5: Verify proxy Service type is ClusterIP +echo "Test 5: Verifying proxy Service type is ClusterIP..." +PROXY_SVC="${HELM_RELEASE}-proxy" +SVC_TYPE=$(oc get service "${PROXY_SVC}" -n "${NAMESPACE}" \ + -o jsonpath='{.spec.type}' 2>/dev/null || true) +if [ "${SVC_TYPE}" = "ClusterIP" ]; then + print_result 0 "Proxy Service type is 'ClusterIP'" +else + print_result 1 "Expected 'ClusterIP', got '${SVC_TYPE:-empty}'" +fi +echo "" + +# Cleanup (only when this script managed the install) +if [ "${SKIP_HELM_INSTALL}" = "false" ]; then + echo "Cleaning up..." + if helm uninstall "${HELM_RELEASE}" -n "${NAMESPACE}" > /dev/null 2>&1; then + echo "✅ Helm release cleaned up" + else + echo -e "${YELLOW}⚠️ WARNING:${NC} Failed to uninstall Helm release" + fi + echo "" +fi +echo "✅ OpenShift Worker Chart Smoke Test passed!" diff --git a/tests/acceptance/openshift-tcp-target-conn-test.sh b/tests/acceptance/openshift-tcp-target-conn-test.sh new file mode 100755 index 0000000..ffb5ea3 --- /dev/null +++ b/tests/acceptance/openshift-tcp-target-conn-test.sh @@ -0,0 +1,305 @@ +#!/bin/bash +# Copyright IBM Corp. 2026 + +# OpenShift Worker Chart — TCP Target Connection Test +# Scenarios: +# 1. Worker running on OpenShift cluster +# 2. Worker registers with Boundary cluster +# 3. Session creation validated via authorize-session +# 4. TCP connection & session field validation + +set -euo pipefail + +# ── Helpers ──────────────────────────────────────────────────────────────────── +pass() { echo " ✅ $1"; } +fail() { echo "❌ FAILED: $1"; exit 1; } +info() { echo " $1"; } +warn() { echo "⚠️ WARN: $1"; } + +# ── Cleanup trap: cancel any open sessions and kill background processes ────── +CONN_PID="" +CONN_SESSION_ID="" +SESSION_ID="" +CONN_OUT="" +_cleanup() { + if [ -n "${CONN_SESSION_ID}" ]; then + boundary sessions cancel \ + -id "${CONN_SESSION_ID}" \ + -addr "${BOUNDARY_ADDR:-}" \ + -token env://BOUNDARY_TOKEN >/dev/null 2>&1 || true + fi + if [ -n "${SESSION_ID}" ]; then + boundary sessions cancel \ + -id "${SESSION_ID}" \ + -addr "${BOUNDARY_ADDR:-}" \ + -token env://BOUNDARY_TOKEN >/dev/null 2>&1 || true + fi + [ -n "${CONN_PID}" ] && kill "${CONN_PID}" 2>/dev/null || true + [ -n "${CONN_OUT}" ] && rm -f "${CONN_OUT}" || true +} +trap _cleanup EXIT + +# ── Config ───────────────────────────────────────────────────────────────────── +NAMESPACE="boundary" +DEPLOY="boundary-worker-deployment" +# Allow callers to override the timeout. +# Default is 300s for standalone runs. +TIMEOUT="${TIMEOUT:-300}" + +# ── Load .env ───────────────────────────────────────────────────────────────── +if [ -f .env ]; then + set -o allexport + # shellcheck disable=SC1091 + source .env + set +o allexport +fi + +echo "OpenShift Worker Chart — TCP Target Connection Test Suite" +echo "" + +# Test 1: Worker running on OpenShift cluster +echo "Validating Worker Running on OpenShift Cluster..." +info "Checking OpenShift cluster accessibility..." +oc cluster-info >/dev/null 2>&1 \ + || fail "OpenShift cluster is not accessible. Run: oc login " +pass "OpenShift cluster accessible" +echo "" + +info "Checking worker deployment..." +oc get deployment "${DEPLOY}" -n "${NAMESPACE}" >/dev/null 2>&1 \ + || fail "Deployment '${DEPLOY}' not found in namespace '${NAMESPACE}'. Run: make openshift-acceptance-helm" +pass "Worker deployment '${DEPLOY}' exists" +echo "" + +info "Waiting for deployment to be available (timeout: ${TIMEOUT}s...)" +oc wait --for=condition=available \ + --timeout="${TIMEOUT}s" \ + deployment/"${DEPLOY}" \ + -n "${NAMESPACE}" >/dev/null 2>&1 \ + || fail "Worker deployment did not become available within ${TIMEOUT}s" +pass "Worker deployment is available" +echo "" + +POD=$(oc get pods \ + -n "${NAMESPACE}" \ + -l app.kubernetes.io/name=boundary-worker \ + --field-selector=status.phase=Running \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + +[ -n "${POD}" ] || fail "No running worker pod found" +pass "Worker pod running: ${POD}" +echo "" + +# Confirm proxy Service is ClusterIP on OpenShift (Route handles external traffic) +SVC_TYPE=$(oc get service boundary-worker-proxy -n "${NAMESPACE}" \ + -o jsonpath='{.spec.type}' 2>/dev/null || true) +[ "${SVC_TYPE}" = "ClusterIP" ] \ + || fail "Proxy Service type is '${SVC_TYPE}', expected 'ClusterIP' on OpenShift" +pass "Proxy Service type is ClusterIP" +echo "" + +# Test 2: Validate Worker Registration with Boundary Cluster +echo "Validating Worker Registration with Boundary Cluster..." +# Check required env vars +for var in BOUNDARY_ADDR BOUNDARY_AUTH_METHOD_ID BOUNDARY_LOGIN_NAME BOUNDARY_PASSWORD; do + [ -n "${!var:-}" ] || fail "'${var}' is not set. Check your .env file." +done +pass "Required environment variables are set" +info "Boundary address: ${BOUNDARY_ADDR}" +echo "" + +# Authenticate with Boundary +info "Authenticating with Boundary cluster..." +AUTH_OUT=$(boundary authenticate password \ + -addr "${BOUNDARY_ADDR}" \ + -auth-method-id "${BOUNDARY_AUTH_METHOD_ID}" \ + -login-name "${BOUNDARY_LOGIN_NAME}" \ + -password env://BOUNDARY_PASSWORD \ + -keyring-type=none 2>&1) || fail "Boundary authentication failed:\n${AUTH_OUT}" + +BOUNDARY_TOKEN=$(printf '%s\n' "${AUTH_OUT}" \ + | awk '/The token is:/ { getline; gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit }') +[ -n "${BOUNDARY_TOKEN}" ] || fail "Failed to extract auth token from authentication output" +export BOUNDARY_TOKEN +pass "Authenticated with Boundary cluster" +echo "" + +# ── Ops health endpoint check (port-forward to ClusterIP ops service) ──────── +info "Checking worker ops health endpoint (port 9203)..." +pkill -f "port-forward.*9203" 2>/dev/null || true +sleep 1 +oc port-forward \ + -n "${NAMESPACE}" \ + "pod/${POD}" 9203:9203 >/dev/null 2>&1 & +PF_PID=$! + +OPS_STATUS="" +for ((i=1; i<=15; i++)); do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://localhost:9203/health 2>/dev/null || true) + if [ -n "${HTTP_CODE}" ] && [ "${HTTP_CODE}" != "000" ]; then + OPS_STATUS="${HTTP_CODE}" + break + fi + sleep 2 +done +kill "${PF_PID}" 2>/dev/null || true +wait "${PF_PID}" 2>/dev/null || true + +if [ -n "${OPS_STATUS}" ] && [ "${OPS_STATUS}" != "000" ]; then + info "Worker ops health endpoint responded with HTTP ${OPS_STATUS}" + pass "Worker ops health endpoint is reachable" +else + fail "Ops health endpoint /health on port 9203 did not respond — worker is not healthy" +fi +echo "" + +# ── Auth storage: confirm node enrollment was initiated ─────────────────────── +info "Checking worker auth storage (node enrollment)..." +AUTH_FILES=$(oc exec -n "${NAMESPACE}" "${POD}" \ + -- find /var/lib/boundary -type f 2>/dev/null | wc -l | tr -d ' ') || AUTH_FILES=0 + +if [ "${AUTH_FILES}" -gt 0 ]; then + pass "Worker auth storage populated (${AUTH_FILES} file(s)) — node enrollment initiated" +else + warn "Auth storage is empty; worker may not have started enrollment yet" +fi +echo "" + +# ── Boundary API: confirm worker record exists (activation token consumed) ──── +info "Verifying worker record exists in Boundary (waiting up to 3m for registration)..." +WORKER_ID="" +for i in $(seq 1 36); do + WORKERS_JSON=$(boundary workers list \ + -scope-id global \ + -addr "${BOUNDARY_ADDR}" \ + -token env://BOUNDARY_TOKEN \ + -format json 2>/dev/null || true) + WORKER_ID=$(printf '%s\n' "${WORKERS_JSON}" | python3 -c " +import json, sys +data = json.load(sys.stdin) +for w in data.get('items', []): + tags = w.get('canonical_tags', {}).get('type', []) + if 'worker' in tags and w.get('address'): + print(w.get('id', '')) + break +" 2>/dev/null || true) + [ -n "${WORKER_ID}" ] && break + info " Worker not registered yet (attempt ${i}/36, waited $(( (i-1)*5 ))s)..." + sleep 5 +done +[ -n "${WORKER_ID}" ] || fail "No worker with 'worker' tag found in Boundary after 3 minutes. Pod outbound connectivity may be broken." +pass "Worker record exists in Boundary: ${WORKER_ID}" + +# Save WORKER_ID for cleanup +echo "${WORKER_ID}" > /tmp/boundary-worker-id.txt +echo "" + +# ── Log: confirm worker is reaching upstream ────────────────────────────────── +info "Checking worker is attempting upstream connection..." +echo "" +if oc -n "${NAMESPACE}" logs "${POD}" 2>/dev/null \ + | grep -q "Setting HCP Boundary cluster address\|upstream.*address\|upstreamDialerFunc"; then + pass "Worker is actively attempting upstream connection to Boundary cluster" +fi +echo "" + +# Test 3: Session creation +echo "Validating Session Creation..." +[ -n "${BOUNDARY_TARGET_ID:-}" ] \ + || fail "'BOUNDARY_TARGET_ID' is not set. Add it to your .env file." +pass "Target configured: ${BOUNDARY_TARGET_ID}" +echo "" +info "Authorizing session to target..." + +SESSION_OUT=$(boundary targets authorize-session \ + -id "${BOUNDARY_TARGET_ID}" \ + -addr "${BOUNDARY_ADDR}" \ + -token env://BOUNDARY_TOKEN \ + -format json 2>&1) || fail "authorize-session failed:\n${SESSION_OUT}" + +SESSION_ID=$(printf '%s\n' "${SESSION_OUT}" \ + | grep -o '"session_id":"[^"]*"' \ + | head -1 \ + | cut -d'"' -f4) + +[ -n "${SESSION_ID}" ] || fail "Failed to extract session_id from authorize-session response" + +SESSION_STATUS=$(printf '%s\n' "${SESSION_OUT}" \ + | grep -o '"status":"[^"]*"' \ + | head -1 \ + | cut -d'"' -f4 || true) +[ -n "${SESSION_STATUS}" ] && info "Session status: ${SESSION_STATUS}" +pass "Session authorized and validated" + +# Cancel the authorize-session token immediately — it is not used for the +# TCP connect test below (boundary connect creates its own session). +boundary sessions cancel \ + -id "${SESSION_ID}" \ + -addr "${BOUNDARY_ADDR}" \ + -token env://BOUNDARY_TOKEN >/dev/null 2>&1 || true +SESSION_ID="" # cleared so the EXIT trap does not double-cancel +echo "" + +# Test 4: TCP connection & session field validation +echo "TCP connection & session field validation..." +info "Establishing proxy connection..." +echo "" + +CONN_OUT=$(mktemp) +boundary connect \ + -target-id "${BOUNDARY_TARGET_ID}" \ + -addr "${BOUNDARY_ADDR}" \ + -token env://BOUNDARY_TOKEN > "${CONN_OUT}" 2>&1 & +CONN_PID=$! + +for i in $(seq 1 30); do + if grep -q "Session ID:" "${CONN_OUT}" 2>/dev/null; then break; fi + sleep 1 +done + +CONN_SESSION_ID=$(grep "Session ID:" "${CONN_OUT}" | awk '{print $NF}') +CONN_PROXY_ADDR=$(grep "Address:" "${CONN_OUT}" | awk '{print $NF}') +CONN_PROXY_PORT=$(grep "Port:" "${CONN_OUT}" | awk '{print $NF}') +CONN_PROXY_PROTO=$(grep "Protocol:" "${CONN_OUT}" | awk '{print $NF}') +CONN_PROXY_EXPIRY=$(grep "Expiration:" "${CONN_OUT}" | sed 's/.*Expiration:[[:space:]]*//') +CONN_LIMIT=$(grep "Connection Limit:" "${CONN_OUT}" | awk '{print $NF}') + +echo "Session Details-" +echo "Session ID: ${CONN_SESSION_ID:-MISSING}" +echo "Address: ${CONN_PROXY_ADDR:-MISSING}" +echo "Port: ${CONN_PROXY_PORT:-MISSING}" +echo "Protocol: ${CONN_PROXY_PROTO:-MISSING}" +echo "Expiration: ${CONN_PROXY_EXPIRY:-MISSING}" +echo "Connection Limit: ${CONN_LIMIT:-MISSING}" +echo "" + +CONN_PASS=1 +[ -n "${CONN_SESSION_ID}" ] || CONN_PASS=0 +[ -n "${CONN_PROXY_ADDR}" ] || CONN_PASS=0 +[ -n "${CONN_PROXY_PORT}" ] || CONN_PASS=0 +[ -n "${CONN_PROXY_PROTO}" ] || CONN_PASS=0 +[ -n "${CONN_PROXY_EXPIRY}" ] || CONN_PASS=0 +[ -n "${CONN_LIMIT}" ] || CONN_PASS=0 + +if [ -n "${CONN_SESSION_ID}" ]; then + info "Waiting 15 seconds before cancelling session..." + sleep 15 + info "Cancelling session ${CONN_SESSION_ID}..." + boundary sessions cancel \ + -id "${CONN_SESSION_ID}" \ + -addr "${BOUNDARY_ADDR}" \ + -token env://BOUNDARY_TOKEN >/dev/null 2>&1 || true + CONN_SESSION_ID="" # cleared so the EXIT trap does not double-cancel + pass "Session cancelled Successfully" + echo "" +fi +kill "${CONN_PID}" 2>/dev/null || true +wait "${CONN_PID}" 2>/dev/null || true +CONN_PID="" +rm -f "${CONN_OUT}" +CONN_OUT="" + +[ "${CONN_PASS}" -eq 1 ] || fail "One or more session fields were missing" +echo "" + +echo "✅ OpenShift Worker Chart TCP Target Connection Test passed!" diff --git a/tests/unit/worker-deployment_test.yaml b/tests/unit/worker-deployment_test.yaml index ba76c8e..de8c438 100644 --- a/tests/unit/worker-deployment_test.yaml +++ b/tests/unit/worker-deployment_test.yaml @@ -96,13 +96,25 @@ tests: path: spec.template.spec.containers[0].volumeMounts[4].mountPath value: /custom/recording - - it: should not mount recording volume when disabled + - it: should mount recording volume with emptyDir when disabled template: worker-deployment.yaml set: worker.persistence.recording.enabled: false asserts: + - equal: + path: spec.template.spec.containers[0].volumeMounts[4].name + value: recording-storage + - equal: + path: spec.template.spec.containers[0].volumeMounts[4].mountPath + value: /boundary/recording + - equal: + path: spec.template.spec.volumes[4].name + value: recording-storage + - equal: + path: spec.template.spec.volumes[4].emptyDir + value: {} - isNull: - path: spec.template.spec.containers[0].volumeMounts[4] + path: spec.template.spec.volumes[4].persistentVolumeClaim # ============================================================================ # SECURITY CONTEXT TESTS @@ -379,8 +391,6 @@ tests: template: worker-deployment.yaml release: name: test-release - set: - worker.persistence.recording.enabled: true asserts: - equal: path: spec.template.spec.volumes[0].name @@ -403,8 +413,6 @@ tests: - it: should configure volume mounts correctly template: worker-deployment.yaml - set: - worker.persistence.recording.enabled: true asserts: - equal: path: spec.template.spec.containers[0].volumeMounts[0].name @@ -468,6 +476,28 @@ tests: path: spec.template.spec.volumes[3].persistentVolumeClaim.claimName value: test-release-auth-storage + # ============================================================================ + # HOST NETWORK TESTS + # ============================================================================ + + - it: should not set hostNetwork by default + template: worker-deployment.yaml + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should set hostNetwork and dnsPolicy when hostNetwork is true + template: worker-deployment.yaml + set: + hostNetwork: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + - equal: + path: spec.template.spec.dnsPolicy + value: Default + # ============================================================================ # DEPLOYMENT SPEC TESTS # ============================================================================ @@ -583,3 +613,68 @@ tests: - equal: path: spec.template.spec.containers[0].env[3].name value: EXTRA_VAR + + # ============================================================================ + # OPENSHIFT SECURITY CONTEXT TESTS + # ============================================================================ + + - it: should omit runAsUser and fsGroup from pod security context when openshift.enabled is true + template: worker-deployment.yaml + set: + openshift.enabled: true + asserts: + - isNull: + path: spec.template.spec.securityContext.runAsUser + - isNull: + path: spec.template.spec.securityContext.runAsGroup + - isNull: + path: spec.template.spec.securityContext.fsGroup + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + + - it: should retain runAsUser and fsGroup in pod security context when openshift.enabled is false + template: worker-deployment.yaml + set: + openshift.enabled: false + asserts: + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 100 + - equal: + path: spec.template.spec.securityContext.runAsGroup + value: 1000 + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 1000 + + - it: should omit runAsUser and runAsGroup from container security context when openshift.enabled is true + template: worker-deployment.yaml + set: + openshift.enabled: true + asserts: + - isNull: + path: spec.template.spec.containers[0].securityContext.runAsUser + - isNull: + path: spec.template.spec.containers[0].securityContext.runAsGroup + - equal: + path: spec.template.spec.containers[0].securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation + value: false + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: true + + - it: should retain runAsUser in container security context when openshift.enabled is false + template: worker-deployment.yaml + set: + openshift.enabled: false + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.runAsUser + value: 100 + - equal: + path: spec.template.spec.containers[0].securityContext.runAsGroup + value: 1000 diff --git a/tests/unit/worker-pvc_test.yaml b/tests/unit/worker-pvc_test.yaml index b99a4ce..325773d 100644 --- a/tests/unit/worker-pvc_test.yaml +++ b/tests/unit/worker-pvc_test.yaml @@ -227,6 +227,19 @@ tests: path: spec.storageClassName documentIndex: 0 + - it: should set storageClassName on recording PVC when storageClass is provided + set: + worker.persistence.recording.enabled: true + worker.persistence.authStorage.enabled: false + worker.persistence.recording.storageClass: gp3-csi + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.storageClassName + value: gp3-csi + documentIndex: 0 + - it: should omit storageClassName for auth PVC when storageClass is empty set: worker.persistence.recording.enabled: false @@ -239,6 +252,19 @@ tests: path: spec.storageClassName documentIndex: 0 + - it: should set storageClassName on auth PVC when storageClass is provided + set: + worker.persistence.recording.enabled: false + worker.persistence.authStorage.enabled: true + worker.persistence.authStorage.storageClass: thin-csi + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.storageClassName + value: thin-csi + documentIndex: 0 + # ============================================================================ # RETENTION POLICY TESTS # ============================================================================ diff --git a/tests/unit/worker-route_test.yaml b/tests/unit/worker-route_test.yaml new file mode 100644 index 0000000..4af4591 --- /dev/null +++ b/tests/unit/worker-route_test.yaml @@ -0,0 +1,151 @@ +# Copyright IBM Corp. 2026 + +suite: Test Worker OpenShift Route Configuration +templates: + - worker-route.yaml +tests: + # ============================================================================ + # ROUTE NOT RENDERED ON VANILLA KUBERNETES + # ============================================================================ + + - it: should not render a Route when openshift.enabled is false + set: + openshift.enabled: false + openshift.route.proxy.enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: should not render a Route when openshift.route.proxy.enabled is false + set: + openshift.enabled: true + openshift.route.proxy.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: should not render a Route when both flags are false + set: + openshift.enabled: false + openshift.route.proxy.enabled: false + asserts: + - hasDocuments: + count: 0 + + # ============================================================================ + # ROUTE RENDERED ON OPENSHIFT + # ============================================================================ + + - it: should render a Route when openshift.enabled and route.proxy.enabled are true + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + asserts: + - hasDocuments: + count: 1 + - isAPIVersion: + of: route.openshift.io/v1 + - isKind: + of: Route + + - it: should use passthrough TLS termination by default + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + asserts: + - equal: + path: spec.tls.termination + value: passthrough + + - it: should allow edge TLS termination override + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + openshift.route.proxy.tls.termination: edge + asserts: + - equal: + path: spec.tls.termination + value: edge + + - it: should not set spec.host when host is empty + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + openshift.route.proxy.host: "" + asserts: + - isNull: + path: spec.host + + - it: should set spec.host when a custom host is provided + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + openshift.route.proxy.host: "boundary.apps.cluster.example.com" + asserts: + - equal: + path: spec.host + value: "boundary.apps.cluster.example.com" + + - it: should target the proxy service + release: + name: my-worker + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + asserts: + - equal: + path: spec.to.kind + value: Service + - equal: + path: spec.to.name + value: my-worker-proxy + - equal: + path: spec.port.targetPort + value: proxy + + - it: should apply route annotations when provided + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + openshift.route.proxy.annotations: + haproxy.router.openshift.io/timeout: "2h" + asserts: + - equal: + path: metadata.annotations["haproxy.router.openshift.io/timeout"] + value: "2h" + + - it: should use the release namespace + release: + namespace: boundary-prod + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + asserts: + - equal: + path: metadata.namespace + value: boundary-prod + + - it: should carry standard labels + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/component"] + value: worker + - isNotNull: + path: metadata.labels["helm.sh/chart"] + - isNotNull: + path: metadata.labels["app.kubernetes.io/name"] + + - it: should use values.namespace over release namespace when set + release: + namespace: release-ns + set: + openshift.enabled: true + openshift.route.proxy.enabled: true + namespace: override-ns + asserts: + - equal: + path: metadata.namespace + value: override-ns diff --git a/tests/unit/worker-service_test.yaml b/tests/unit/worker-service_test.yaml index 83dbbf0..49fc7d8 100644 --- a/tests/unit/worker-service_test.yaml +++ b/tests/unit/worker-service_test.yaml @@ -181,3 +181,41 @@ tests: path: metadata.namespace value: custom-namespace documentIndex: 1 + + # ============================================================================ + # OPENSHIFT SERVICE TESTS + # ============================================================================ + + - it: should use ClusterIP for proxy service when openshift.enabled is true + set: + worker.service.proxy.enabled: true + worker.service.proxy.type: LoadBalancer + openshift.enabled: true + documentIndex: 0 + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: should use configured type for proxy service when openshift.enabled is false + set: + worker.service.proxy.enabled: true + worker.service.proxy.type: LoadBalancer + openshift.enabled: false + documentIndex: 0 + asserts: + - equal: + path: spec.type + value: LoadBalancer + + - it: should keep ops service type unchanged when openshift.enabled is true + set: + worker.service.proxy.enabled: true + worker.service.ops.enabled: true + worker.service.ops.type: ClusterIP + openshift.enabled: true + documentIndex: 1 + asserts: + - equal: + path: spec.type + value: ClusterIP diff --git a/values.openshift.yaml b/values.openshift.yaml new file mode 100644 index 0000000..5cc58f1 --- /dev/null +++ b/values.openshift.yaml @@ -0,0 +1,63 @@ +# Copyright IBM Corp. 2026 + +# These overrides are appropriate defaults for deploying this chart on OpenShift. +# Usage: +# helm install boundary-worker . \ +# --namespace boundary \ +# --create-namespace \ +# -f values.openshift.yaml \ +# --set-file worker.config=./worker.hcl + +# OpenShift-compatible UBI-based image (AMD64), publicly available on ECR. +image: + repository: public.ecr.aws/g0u5x5a3/boundary-enterprise + tag: "1.0.0-ent-ubi" + +openshift: + enabled: true + route: + proxy: + enabled: true + # Leave empty to let OpenShift auto-assign a hostname from the cluster's + # ingress domain (e.g. boundary-worker-proxy-boundary.apps.cluster.example.com). + host: "" + tls: + # passthrough keeps TLS end-to-end to the worker container. + # Use edge only if you terminate TLS at the OCP router and re-encrypt inward. + termination: passthrough + annotations: {} + +# ServiceAccount — create one so it can be bound to an SCC if needed. +# After installing, grant the restricted-v2 SCC if your namespace policy requires it: +# oc adm policy add-scc-to-user restricted-v2 -z boundary-worker -n +# +# SCC requirements for the boundary-worker pod: +# - runAsNonRoot: true (OCP assigns a UID from the namespace range) +# - allowPrivilegeEscalation: false +# - readOnlyRootFilesystem: true +# - All capabilities dropped +# The built-in "restricted-v2" SCC (OCP 4.11+) satisfies all of the above. +# On OCP 4.10 and earlier, use "restricted". +serviceAccount: + create: true + name: "boundary-worker" + annotations: {} + automountServiceAccountToken: false + +# Storage — set the OCP storage class appropriate for your platform. +# Examples: +# ROSA / AWS: gp3-csi +# OCP on vSphere: thin-csi +# OCP on Azure: managed-csi +# CRC (local): crc-csi-hostpath-provisioner +# Leave storageClass empty to use the cluster default StorageClass. +worker: + service: + proxy: + # On OpenShift the proxy port is exposed via the Route above; a LoadBalancer is not needed. + type: ClusterIP + persistence: + recording: + storageClass: "" + authStorage: + storageClass: "" diff --git a/values.schema.json b/values.schema.json index aaad826..851af3e 100644 --- a/values.schema.json +++ b/values.schema.json @@ -224,6 +224,48 @@ }, "namespace": { "type": "string" + }, + "openshift": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "podSecurityContext": { + "type": "object" + }, + "containerSecurityContext": { + "type": "object" + }, + "route": { + "type": "object", + "properties": { + "proxy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "tls": { + "type": "object", + "properties": { + "termination": { + "type": "string", + "enum": ["passthrough", "edge", "reencrypt"] + } + } + }, + "annotations": { + "type": "object" + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/values.yaml b/values.yaml index 430fb09..c73e127 100644 --- a/values.yaml +++ b/values.yaml @@ -86,6 +86,7 @@ worker: # - Using ClusterIP with an Ingress controller # - Setting scheme to "internal" in annotations # - Implementing network policies + # Note: ignored when openshift.enabled=true — the proxy port is exposed via a Route instead. type: LoadBalancer port: 9202 targetPort: 9202 @@ -170,8 +171,46 @@ serviceAccount: # iam.gke.io/gcp-service-account: boundary-worker@PROJECT.iam.gserviceaccount.com automountServiceAccountToken: false # Set to true when using IRSA or Workload Identity +# OpenShift configuration +# Set openshift.enabled=true when deploying on OpenShift. +# - Swaps the proxy Service type to ClusterIP (a Route handles external traffic instead). +# - Drops the fixed runAsUser/runAsGroup/fsGroup so OpenShift's SCC assigns a valid UID from the namespace range. +# - Renders an openshift.io/v1 Route for TCP-passthrough access to the proxy port. +openshift: + enabled: false + # Pod security context used instead of podSecurityContext when openshift.enabled=true. + # runAsUser/runAsGroup/fsGroup are intentionally absent so the namespace SCC assigns them. + podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + # Container security context used instead of containerSecurityContext when openshift.enabled=true. + containerSecurityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + route: + proxy: + enabled: true + # Leave empty to let OpenShift auto-assign a hostname. + host: "" + tls: + # passthrough keeps TLS end-to-end to the worker; use edge only if you terminate TLS at the router. + termination: passthrough + annotations: {} + # Optional configurations podAnnotations: {} nodeSelector: {} tolerations: [] affinity: {} + +# hostNetwork: run the pod in the host network namespace. +# Useful for CI environments (e.g. MicroShift in Docker) where pod egress +# routing is unavailable. Do NOT enable in production OpenShift deployments. +hostNetwork: false