Repository navigation
2048 lines (1873 loc) · 89.2 KB
/
Copy pathk8s-native-e2e-add-node.yaml
File metadata and controls
2048 lines (1873 loc) · 89.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: "K8s Native E2E - Add Node (use topology-suite-k8s-add-node.yml for multi-test runs)"
run-name: "K8s Native E2E - Add Node | ${{ inputs.cluster_environment || 'local' }} | ${{ inputs.testname || 'K8sNativeAddNodeTest' }} | ${{ inputs.cluster_params || 'ndcs=2,npcs=2,bs=4096,chunk_bs=4096,partitions=1,jm_count=4' }}"
on:
workflow_call:
inputs:
testname:
type: string
default: 'K8sNativeAddNodeTest'
description: 'Test class name to run (default: K8sNativeAddNodeTest)'
operator_repo_branch:
type: string
default: 'main'
simplyblock_image:
type: string
default: 'main'
simplyblock_repository:
type: string
default: 'public.ecr.aws/simply-block/simplyblock'
operator_repository:
type: string
default: 'simplyblock/simplyblock-operator'
operator_tag:
type: string
default: 'main'
csi_repository:
type: string
default: 'simplyblock/spdkcsi'
csi_tag:
type: string
default: 'latest'
spdk_image:
type: string
default: 'simplyblock/spdk:main-latest'
worker_nodes:
type: string
required: true
default: 'worker-0.ocp.simplyblock.ai,worker-1.ocp.simplyblock.ai,worker-2.ocp.simplyblock.ai,worker-3.ocp.simplyblock.ai,worker-4.ocp.simplyblock.ai,worker-5.ocp.simplyblock.ai'
new_worker_nodes:
type: string
required: true
ifc_names:
type: string
default: 'br-ex:enp2s0f0'
max_subsys:
type: string
default: '30'
ssh_user:
type: string
default: 'root'
key_path:
type: string
default: '/home/ec2-user/.ssh/simplyblock-us-east-2.pem'
send_slack_notification:
type: boolean
default: true
cluster_params:
type: string
default: 'ndcs=2,npcs=2,bs=4096,chunk_bs=4096,partitions=1,jm_count=4'
cluster_environment:
type: string
default: 'openshift-baremetal'
skip_nfs:
type: string
default: 'false'
use_existing_cluster:
type: string
default: 'false'
tls_enabled:
type: boolean
default: true
client_ips:
type: string
default: ''
nfs_mountpoint:
type: string
default: '/mnt/nfs_share/'
cluster_security:
type: string
default: 'none'
description: "Cluster setup mode. 'backup' deploys MinIO and enables backup on StorageCluster."
preserve_resources_on_failure:
type: boolean
default: true
description: 'Preserve PVCs, FIO pods, snapshots on test failure for debugging'
workflow_dispatch:
inputs:
testname:
description: 'Test class name to run'
required: false
default: 'K8sNativeAddNodeTest'
operator_repo_branch:
description: 'Branch for simplyblock-operator repo'
required: true
default: 'main'
simplyblock_image:
description: 'Simplyblock image tag'
required: true
default: 'main'
simplyblock_repository:
description: 'Simplyblock image repository'
required: false
default: 'public.ecr.aws/simply-block/simplyblock'
operator_repository:
description: 'Operator image repository'
required: false
default: 'simplyblock/simplyblock-operator'
operator_tag:
description: 'Operator image tag'
required: false
default: 'main'
csi_repository:
description: 'CSI driver image repository (leave empty for chart default)'
required: false
default: 'simplyblock/spdkcsi'
csi_tag:
description: 'CSI driver image tag (leave empty for chart default)'
required: false
default: 'latest'
spdk_image:
description: 'SPDK container image'
required: true
default: 'simplyblock/spdk:main-latest'
worker_nodes:
description: 'Comma-separated initial K8s worker node names'
required: true
default: 'worker-0.ocp.simplyblock.ai,worker-1.ocp.simplyblock.ai,worker-2.ocp.simplyblock.ai,worker-3.ocp.simplyblock.ai,worker-4.ocp.simplyblock.ai,worker-5.ocp.simplyblock.ai'
new_worker_nodes:
description: 'Comma-separated new K8s worker node names to add during test (at least 1)'
required: true
ifc_names:
description: 'Network interfaces (mgmt_ifc:data_nics)'
required: false
default: 'br-ex:enp2s0f0'
max_subsys:
description: 'Max logical volume count per storage node'
required: false
default: '30'
ssh_user:
description: 'SSH user for client nodes'
required: false
default: 'root'
key_path:
description: "SSH private key path on runner (default: /home/ec2-user/.ssh/simplyblock-us-east-2.pem)"
required: true
default: "/home/ec2-user/.ssh/simplyblock-us-east-2.pem"
send_slack_notification:
description: 'Send Slack notification?'
required: false
default: true
type: boolean
cluster_params:
description: 'Cluster config: ndcs=2,npcs=2,bs=4096,chunk_bs=4096,partitions=1,jm_count=4'
required: false
default: 'ndcs=2,npcs=2,bs=4096,chunk_bs=4096,partitions=1,jm_count=4'
cluster_environment:
description: 'Target cluster environment'
required: true
default: 'openshift-baremetal'
type: choice
options:
- local
- aws-openshift
- openshift-local
- openshift-baremetal
- gcp
skip_nfs:
description: 'Skip NFS mounting (use local logs directory instead)'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
use_existing_cluster:
description: 'Skip cluster cleanup and setup, reuse existing cluster'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
tls_enabled:
description: 'Enable TLS (installs cert-manager, sets tls.enabled + tls.mutual_enabled)'
required: false
default: true
type: boolean
client_ips:
description: 'Comma-separated client IPs for external FIO (empty = FIO runs as K8s Jobs)'
required: false
default: ''
nfs_mountpoint:
description: 'NFS mountpoint on client nodes'
required: false
default: '/mnt/nfs_share/'
cluster_security:
description: "Cluster setup mode. 'backup' deploys MinIO and enables backup on StorageCluster. Auto-enabled for backup/backup-topology test names."
required: false
type: choice
default: "none"
options:
- none
- backup
preserve_resources_on_failure:
description: 'Preserve PVCs, FIO pods, snapshots on test failure for debugging'
required: false
default: true
type: boolean
jobs:
e2e-add-node:
runs-on: ${{ inputs.cluster_environment == 'aws-openshift' && 'vm-runner-43' || 'self-hosted' }}
timeout-minutes: 4320
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
env:
TESTNAME: ${{ inputs.testname || 'K8sNativeAddNodeTest' }}
KEY_PATH: ${{ inputs.key_path || '/home/ec2-user/.ssh/simplyblock-us-east-2.pem' }}
SSH_USER: ${{ inputs.ssh_user || 'root' }}
SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
steps:
- name: Fix workspace permissions
run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@master
name: Checkout simplyblock-operator
with:
repository: simplyblock/simplyblock-operator
ref: ${{ inputs.operator_repo_branch || 'main' }}
path: 'simplyblock-operator'
- name: Resolve KEY_PATH and validate key exists
shell: bash
run: |
set -euxo pipefail
kp="${KEY_PATH}"
# Strip wrapping quotes
kp="${kp%\"}"; kp="${kp#\"}"
kp="${kp%\'}"; kp="${kp#\'}"
# Normalize ".ssh/..." -> "$HOME/.ssh/..."
if [[ "$kp" == .ssh/* ]]; then
kp="${HOME}/${kp}"
fi
# Normalize "~/" -> "$HOME/"
if [[ "$kp" == ~/* ]]; then
kp="${HOME}/${kp#~/}"
fi
echo "Resolved KEY_PATH=$kp"
echo "KEY_PATH=$kp" >> "$GITHUB_ENV"
test -f "$kp" || (echo "ERROR: SSH key not found at $kp" && exit 1)
chmod 600 "$kp" || true
- name: Export KEY_NAME from KEY_PATH
shell: bash
run: |
set -euxo pipefail
key_name="$(basename "${KEY_PATH}")"
echo "KEY_NAME=${key_name}" >> "$GITHUB_ENV"
echo "Exported KEY_NAME=${key_name}"
- name: Setup kubeconfig
run: |
mkdir -p $HOME/.kube
KUBECONFIG_FILE="$HOME/.kube/config-${{ github.run_id }}"
echo "${KUBECONFIG_DATA}" > "$KUBECONFIG_FILE"
chmod 600 "$KUBECONFIG_FILE"
echo "KUBECONFIG=$KUBECONFIG_FILE" >> $GITHUB_ENV
env:
KUBECONFIG_DATA: ${{
inputs.cluster_environment == 'aws-openshift' && secrets.KUBECONFIG_AWS_OPENSHIFT ||
inputs.cluster_environment == 'openshift-local' && secrets.KUBECONFIG_OPENSHIFT_LOCAL ||
inputs.cluster_environment == 'openshift-baremetal' && secrets.KUBECONFIG_OPENSHIFT_BM ||
inputs.cluster_environment == 'gcp' && secrets.KUBECONFIG_GCP ||
secrets.KUBECONFIG_LOCAL || secrets.KUBECONFIG_CONTENT
}}
- name: Install Helm v3.15.4
run: |
if ! helm version 2>/dev/null | grep -q 'v3.15'; then
curl -L https://get.helm.sh/helm-v3.15.4-linux-amd64.tar.gz -o helm-v3.15.4-linux-amd64.tar.gz
tar -zxvf helm-v3.15.4-linux-amd64.tar.gz
sudo mv linux-amd64/helm /usr/local/bin/helm
rm -rf linux-amd64 helm-v3.15.4-linux-amd64.tar.gz
fi
helm version
- name: Install kubectl v1.31.0
run: |
if ! kubectl version --client 2>/dev/null | grep -q 'v1.31'; then
curl -LO "https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
fi
kubectl version --client
- name: Set parameters
run: |
PARAMS="${{ inputs.cluster_params || 'ndcs=2,npcs=2,bs=4096,chunk_bs=4096,partitions=1,jm_count=4' }}"
for pair in $(echo "$PARAMS" | tr ',' ' '); do
key=$(echo "$pair" | cut -d= -f1)
val=$(echo "$pair" | cut -d= -f2)
case "$key" in
ndcs) echo "NDCS=$val" >> $GITHUB_ENV ;;
npcs) echo "NPCS=$val" >> $GITHUB_ENV ;;
bs) echo "BS=$val" >> $GITHUB_ENV ;;
chunk_bs) echo "CHUNK_BS=$val" >> $GITHUB_ENV ;;
partitions) echo "PARTITIONS=$val" >> $GITHUB_ENV ;;
jm_count) echo "JM_COUNT=$val" >> $GITHUB_ENV ;;
esac
done
- name: Verify kubectl connectivity
run: kubectl get nodes
- name: Ensure OVN-Kubernetes routingViaHost is enabled
if: ${{ inputs.cluster_environment == 'aws-openshift' || inputs.cluster_environment == 'openshift-local' }}
run: |
CURRENT=$(oc get network.operator.openshift.io cluster \
-o jsonpath='{.spec.defaultNetwork.ovnKubernetesConfig.gatewayConfig.routingViaHost}' 2>/dev/null || echo "")
if [ "$CURRENT" = "true" ]; then
echo "routingViaHost is already enabled, skipping patch"
else
echo "routingViaHost is not enabled (current: '$CURRENT'), applying patch..."
oc patch network.operator.openshift.io cluster --type=merge \
-p '{"spec":{"defaultNetwork":{"ovnKubernetesConfig":{"gatewayConfig":{"routingViaHost":true}}}}}'
echo "Waiting 30s for OVN-Kubernetes to reconcile..."
sleep 30
fi
- name: Pre-clean clients (kill fio, NVMe disconnect, unmount NFS, remove mount dirs)
if: ${{ inputs.client_ips != '' }}
shell: bash
run: |
set +e
NFS_MOUNTPOINT="${{ inputs.nfs_mountpoint || '/mnt/nfs_share/' }}"
run_remote_with_retry() {
local ip="$1"; shift
local cmd="$*"
local max=5
for attempt in $(seq 1 $max); do
echo " [attempt $attempt/$max] ssh ${SSH_USER}@${ip} ..."
if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i "${KEY_PATH}" \
"${SSH_USER}@${ip}" "${cmd}"; then
return 0
fi
echo " SSH failed, retrying in 5s..."
sleep 5
done
echo "All $max attempts failed for $ip, continuing..."
return 0
}
normalized_targets="$(echo "${{ inputs.client_ips }}" | tr ',' ' ' | xargs -n1 | sort -u | xargs || true)"
if [ -z "${normalized_targets}" ]; then
echo "No client IPs provided, skipping client cleanup."
exit 0
fi
for ip in ${normalized_targets}; do
echo "---- ${ip}: kill fio/tmux + disconnect NVMe + umount ${NFS_MOUNTPOINT} + clean mount dirs ----"
run_remote_with_retry "${ip}" "set -euxo pipefail;
pkill -9 fio || true;
pkill -9 tmux || true;
# Disconnect stale NVMe-oF connections from previous runs
for nqn in \$(nvme list-subsys -o json 2>/dev/null | jq -r '.Subsystems[]?.NQN // empty' | grep lvol || true); do
nvme disconnect -n \"\$nqn\" || true;
done;
# Unmount NFS
mp='${NFS_MOUNTPOINT}';
timeout 10 umount -f \"\$mp\" 2>/dev/null || umount -l \"\$mp\" 2>/dev/null || true;
# Unmount and remove test mount dirs.
# Try umount unconditionally — if not mounted it just fails silently.
for mp in /mnt/test_location/*; do
[ -e \"\$mp\" ] || continue;
timeout 10 umount -f \"\$mp\" 2>/dev/null || umount -l \"\$mp\" 2>/dev/null || true;
done;
rm -rf /mnt/test_location/* 2>/dev/null || true"
done
# ── Cleanup old deployment ──────────────────────────────────────────────
- name: Cleanup old deployment (full 12-phase cleanup)
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
set +e
# Run the operator's own cleanup script first if available
if [ -x "$GITHUB_WORKSPACE/simplyblock-operator/helm-charts/scripts/cleanup-simplyblock.sh" ]; then
bash $GITHUB_WORKSPACE/simplyblock-operator/helm-charts/scripts/cleanup-simplyblock.sh -f simplyblock || true
fi
# Combine worker_nodes and new_worker_nodes for comprehensive cleanup
ALL_WORKERS="${{ inputs.worker_nodes }}"
NEW_WORKERS="${{ inputs.new_worker_nodes }}"
if [ -n "$NEW_WORKERS" ]; then
ALL_WORKERS="${ALL_WORKERS},${NEW_WORKERS}"
fi
# Run full 12-phase cleanup (handles Helm R25+R26, CRs, CRDs,
# cert-manager, kube-system, snapshots, PVCs/PVs, node cleanup,
# labels, CSI hostpath data, namespace finalization).
# Auto-detects Talos and skips node debug operations.
CLUSTER_ENV="${{ inputs.cluster_environment || 'local' }}"
bash $GITHUB_WORKSPACE/e2e/scripts/cleanup_upgrade_test.sh \
-n simplyblock \
-e "$CLUSTER_ENV" \
-w "$ALL_WORKERS" \
-c "$GITHUB_WORKSPACE/simplyblock-operator/helm-charts/charts/simplyblock-operator/crds/"
# Remove simplyblock labels from new worker nodes specifically
if [ -n "$NEW_WORKERS" ]; then
IFS=',' read -ra NEW_NODES <<< "$NEW_WORKERS"
for NODE in "${NEW_NODES[@]}"; do
NODE=$(echo "$NODE" | xargs)
[[ -z "$NODE" ]] && continue
kubectl label node "$NODE" simplyblock.io/role- 2>/dev/null || true
done
fi
- name: Cleanup old KMS deployment
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
set +e
echo "=== Uninstalling KMS (openbao) ==="
helm uninstall openbao -n vault 2>/dev/null || true
# Remove Helm release secrets in case uninstall left a broken/pending state
kubectl delete secret -n vault -l name=openbao,owner=helm 2>/dev/null || true
kubectl delete namespace vault --wait=false 2>/dev/null || true
echo "Waiting for vault namespace to be deleted..."
kubectl wait --for=delete namespace/vault --timeout=120s 2>/dev/null || true
echo "=== KMS cleanup complete ==="
# ── Label nodes ─────────────────────────────────────────────────────────
- name: Label initial worker nodes
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
CLUSTER_ENV="${{ inputs.cluster_environment || 'local' }}"
IFS=',' read -ra NODES <<< "${{ inputs.worker_nodes }}"
for NODE in "${NODES[@]}"; do
if [ "$CLUSTER_ENV" = "local" ] || [ "$CLUSTER_ENV" = "openshift-local" ]; then
echo "Labeling node $NODE with zone=default (local cluster)"
kubectl label node "$NODE" topology.kubernetes.io/zone=default --overwrite
else
echo "Skipping zone label for $NODE (cloud cluster: $CLUSTER_ENV)"
fi
kubectl label node "$NODE" simplyblock.io/role=mgmt-plane --overwrite
done
# NOTE: new_worker_nodes are NOT labeled here. They must remain
# unlabeled so the DaemonSet does not schedule storage-node pods on them
# before the test adds them to the cluster via StorageNodeSet CR update.
# ── Prepare namespace ───────────────────────────────────────────────────
- name: Create namespace and set pod-security labels
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
kubectl create namespace simplyblock --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace simplyblock \
pod-security.kubernetes.io/enforce=privileged \
pod-security.kubernetes.io/warn=privileged \
pod-security.kubernetes.io/audit=privileged \
--overwrite
- name: Create Docker registry secret
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
kubectl create secret docker-registry regcred \
--docker-server=https://index.docker.io/v1/ \
--docker-username=${DOCKER_USER} \
--docker-password=${DOCKER_PASSWORD} \
--docker-email=a@b.com \
-n simplyblock || true
env:
DOCKER_USER: ${{ secrets.DOCKER_USER }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Configure OpenShift SCC policies
if: ${{ inputs.use_existing_cluster != 'true' && (inputs.cluster_environment == 'aws-openshift' || inputs.cluster_environment == 'openshift-local') }}
run: |
oc adm policy add-scc-to-user privileged -z default -n simplyblock
oc adm policy add-scc-to-user anyuid -z default -n simplyblock
oc label namespace simplyblock \
pod-security.kubernetes.io/enforce=privileged \
pod-security.kubernetes.io/audit=privileged \
pod-security.kubernetes.io/warn=privileged \
--overwrite
# ── Deploy MinIO for backup tests (conditional) ────────────────────────
- name: Setup MinIO for backup tests
if: >-
${{
inputs.use_existing_cluster != 'true' &&
(inputs.cluster_security == 'backup' ||
inputs.testname == 'backup-topology' ||
contains(inputs.testname, 'Backup'))
}}
run: |
set -euxo pipefail
NAMESPACE=simplyblock
EXTERNAL_MINIO="http://192.168.10.164:9000"
MINIO_ACCESS_KEY="minioadmin"
MINIO_SECRET_KEY="minioadmin"
# Check if external MinIO is reachable FROM INSIDE the cluster
# (runner can always reach it, but cluster nodes/pods may not)
if kubectl run minio-check --rm -i --restart=Never --image=busybox:latest \
--timeout=30s -- wget -q -O /dev/null -T 5 "${EXTERNAL_MINIO}/minio/health/live" >/dev/null 2>&1; then
echo "=== External MinIO at ${EXTERNAL_MINIO} is reachable from cluster ==="
MINIO_ENDPOINT="${EXTERNAL_MINIO}"
echo "MINIO_SOURCE=external" >> "$GITHUB_ENV"
else
echo "=== External MinIO unreachable, deploying in-cluster MinIO ==="
kubectl create namespace minio --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace minio \
pod-security.kubernetes.io/enforce=privileged \
pod-security.kubernetes.io/warn=privileged \
pod-security.kubernetes.io/audit=privileged \
--overwrite
# On OpenShift, grant the default SA the anyuid SCC so MinIO can run
if kubectl api-resources | grep -q securitycontextconstraints; then
oc adm policy add-scc-to-user anyuid -z default -n minio 2>/dev/null || true
fi
cat <<'MINIOYAML' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio
namespace: minio
spec:
replicas: 1
selector:
matchLabels:
app: minio
template:
metadata:
labels:
app: minio
spec:
containers:
- name: minio
image: minio/minio:latest
args: ["server", "/data"]
ports:
- containerPort: 9000
env:
- name: MINIO_ROOT_USER
value: minioadmin
- name: MINIO_ROOT_PASSWORD
value: minioadmin
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: minio
spec:
selector:
app: minio
ports:
- port: 9000
targetPort: 9000
MINIOYAML
echo "Waiting for MinIO pod to appear..."
for i in $(seq 1 30); do
if kubectl get pod -l app=minio -n minio --no-headers 2>/dev/null | grep -q .; then
echo "MinIO pod found"
break
fi
if [ "$i" -eq 30 ]; then
echo "ERROR: MinIO pod never appeared. Deployment status:"
kubectl -n minio describe deployment minio
kubectl -n minio get events --sort-by='.lastTimestamp' | tail -20
exit 1
fi
echo " Waiting for pod ($i/30)..."
sleep 5
done
echo "Waiting for MinIO pod to be ready..."
kubectl wait --for=condition=ready pod -l app=minio -n minio --timeout=120s
# Port-forward to make MinIO accessible from the runner
kubectl port-forward -n minio svc/minio 9000:9000 &
echo "MC_PORT_FWD_PID=$!" >> "$GITHUB_ENV"
sleep 3
MINIO_ENDPOINT="http://minio.minio.svc:9000"
echo "MINIO_SOURCE=in-cluster" >> "$GITHUB_ENV"
fi
echo "MINIO_ENDPOINT=${MINIO_ENDPOINT}" >> "$GITHUB_ENV"
# Create backup-credentials K8s Secret in cluster namespace
cat <<CREDEOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: backup-credentials
namespace: ${NAMESPACE}
type: Opaque
stringData:
access_key_id: ${MINIO_ACCESS_KEY}
secret_access_key: ${MINIO_SECRET_KEY}
CREDEOF
echo "=== MinIO setup complete (source=${MINIO_SOURCE:-external}, endpoint=${MINIO_ENDPOINT}) ==="
- name: Start MinIO trace logging
if: >-
${{
inputs.use_existing_cluster != 'true' &&
(inputs.cluster_security == 'backup' ||
inputs.testname == 'backup-topology' ||
contains(inputs.testname, 'Backup'))
}}
run: |
set -euxo pipefail
# This VM hosts multiple parallel runners, so every path/alias/
# process used here MUST be scoped to this run — a shared
# /tmp/mc or a pattern-matching pkill would race with, corrupt,
# or kill another job's in-flight mc install/trace.
MC_BIN="/tmp/mc-${GITHUB_RUN_ID}"
MC_ALIAS="myminio-${GITHUB_RUN_ID}"
echo "MC_BIN=${MC_BIN}" >> "$GITHUB_ENV"
echo "MC_ALIAS=${MC_ALIAS}" >> "$GITHUB_ENV"
# 1. Install mc (MinIO Client) to our own per-run path
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o "${MC_BIN}"
chmod +x "${MC_BIN}"
# 2. Configure our own mc alias — for in-cluster MinIO use localhost (port-forward)
if [ "${MINIO_SOURCE:-external}" = "in-cluster" ]; then
"${MC_BIN}" alias set "${MC_ALIAS}" http://localhost:9000 minioadmin minioadmin
else
"${MC_BIN}" alias set "${MC_ALIAS}" http://192.168.10.164:9000 minioadmin minioadmin
fi
# 3. Start admin trace in background
MINIO_TRACE_LOG="/tmp/minio-trace-${GITHUB_RUN_ID}.log"
echo "MINIO_TRACE_LOG=${MINIO_TRACE_LOG}" >> "$GITHUB_ENV"
nohup "${MC_BIN}" admin trace -v --all "${MC_ALIAS}" > "${MINIO_TRACE_LOG}" 2>&1 &
echo "MC_TRACE_PID=$!" >> "$GITHUB_ENV"
echo "=== MinIO trace logging started (PID=$!, log=${MINIO_TRACE_LOG}) ==="
- name: Wait before helm install
if: ${{ inputs.use_existing_cluster != 'true' }}
run: sleep 30
# ── Deploy CSI stack ────────────────────────────────────────────────────
- name: Install cert-manager (TLS prerequisite)
if: ${{ inputs.use_existing_cluster != 'true' && inputs.tls_enabled }}
run: |
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.13.0 --set installCRDs=true
echo "Waiting for cert-manager pods to be ready..."
kubectl wait --for=condition=Ready pods --all -n cert-manager --timeout=120s
echo "cert-manager is ready."
- name: Install Helm Chart for simplyblock-operator
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
set -euxo pipefail
cd $GITHUB_WORKSPACE/simplyblock-operator/helm-charts/charts/simplyblock-operator/
TLS_FLAGS=""
if [ "${{ inputs.tls_enabled }}" = "true" ]; then
TLS_FLAGS="--set tls.enabled=true --set tls.mutual_enabled=true"
fi
CSI_FLAGS=""
if [ -n "${{ inputs.csi_repository }}" ]; then
CSI_FLAGS="$CSI_FLAGS --set image.csi.repository=${{ inputs.csi_repository }}"
fi
if [ -n "${{ inputs.csi_tag }}" ]; then
CSI_FLAGS="$CSI_FLAGS --set image.csi.tag=${{ inputs.csi_tag }}"
fi
helm upgrade --install spdk-csi ./ \
--namespace simplyblock \
--create-namespace \
--debug \
--timeout 10m \
--set controlplane.enabled=true \
--set operator.enabled=true \
--set controlplane.csiHostpathDriver.enabled=true \
--set controlplane.storageclass.name=local-hostpath \
--set image.simplyblock.repository="${{ inputs.simplyblock_repository || 'public.ecr.aws/simply-block/simplyblock' }}" \
--set image.simplyblock.tag="${{ inputs.simplyblock_image }}" \
--set image.operator.repository="${{ inputs.operator_repository || 'simplyblock/simplyblock-operator' }}" \
--set image.operator.tag="${{ inputs.operator_tag || 'main' }}" \
--set csiConfig.simplybk.ip="http://simplyblock-webappapi.simplyblock:5000" \
--set prometheus.server.persistentVolume.storageClass=local-hostpath \
--set controlplane.observability.enabled=true \
--set opensearch.persistence.storageClass=local-hostpath \
--set controlplane.observability.minio.storageClass=local-hostpath \
--set image.simplyblock.pullPolicy=IfNotPresent \
--set image.operator.pullPolicy=IfNotPresent \
$TLS_FLAGS $CSI_FLAGS
- name: Grant OpenShift SCC to all service accounts (post-helm)
if: ${{ inputs.use_existing_cluster != 'true' && (inputs.cluster_environment == 'aws-openshift' || inputs.cluster_environment == 'openshift-local') }}
run: |
for sa in $(oc get sa -n simplyblock -o name | cut -d/ -f2); do
oc adm policy add-scc-to-user privileged -z $sa -n simplyblock
echo "Granted privileged SCC to $sa"
done
- name: Patch fluent-bit daemonset (post-helm)
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
NAMESPACE=simplyblock
echo "=== Waiting for simplyblock-fluent-bit daemonset (up to 5 min) ==="
PATCHED=false
for i in $(seq 1 30); do
if kubectl get daemonset simplyblock-fluent-bit -n $NAMESPACE &>/dev/null; then
echo "fluent-bit daemonset found, patching affinity..."
kubectl patch daemonset simplyblock-fluent-bit -n $NAMESPACE \
--type=merge -p '{"spec":{"template":{"spec":{"affinity":null}}}}'
echo "fluent-bit daemonset patched successfully"
# Also patch CSI node daemonset with client toleration
if kubectl get daemonset simplyblock-csi-node -n $NAMESPACE &>/dev/null; then
kubectl patch daemonset simplyblock-csi-node -n $NAMESPACE \
--type=merge -p '{"spec":{"template":{"spec":{"tolerations":[{"key":"node-role","operator":"Equal","value":"client","effect":"NoSchedule"}]}}}}'
echo "csi-node daemonset patched with client toleration"
fi
PATCHED=true
break
fi
echo "fluent-bit daemonset not found yet ($i/30), waiting 10s..."
sleep 10
done
echo "FLUENTBIT_PATCHED=$PATCHED" >> $GITHUB_ENV
- name: Patch service accounts with imagePullSecrets
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
for sa in $(kubectl get serviceaccounts -n simplyblock --no-headers | awk '{print $1}'); do
kubectl patch serviceaccount "$sa" -n simplyblock \
--patch '{"imagePullSecrets": [{"name": "regcred"}]}'
done
for deploy in $(kubectl get deployments -n simplyblock --no-headers -o custom-columns=:metadata.name 2>/dev/null); do
kubectl patch deployment "$deploy" -n simplyblock \
--type=merge -p '{"spec":{"template":{"spec":{"imagePullSecrets":[{"name":"regcred"}]}}}}' || true
done
- name: Delete ImagePullBackOff pods and wait for Running (pre-CRD)
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
NAMESPACE=simplyblock
for pod in $(kubectl get pods -n $NAMESPACE --no-headers 2>/dev/null | grep ImagePullBackOff | awk '{print $1}'); do
kubectl delete pod -n $NAMESPACE "$pod" || true
done
echo "=== Waiting for all pods to be Running ==="
for i in $(seq 1 10); do
BAD_PODS=$(kubectl get pods -n $NAMESPACE --no-headers 2>/dev/null | grep -vE 'Running|Completed' | wc -l)
if [ "$BAD_PODS" -eq 0 ]; then
echo "All pods are Running"
break
fi
echo "Still $BAD_PODS pod(s) not ready ($i/10)..."
kubectl get pods -n $NAMESPACE --no-headers 2>/dev/null | grep -vE 'Running|Completed' || true
sleep 10
done
# ── Apply custom resources (initial nodes only) ────────────────────────
- name: Wait for operator pod to be ready
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
NAMESPACE=simplyblock
echo "=== Waiting for operator pod ==="
for i in $(seq 1 60); do
POD=$(kubectl -n $NAMESPACE get pods \
-l app=simplyblock-admin-control \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null) || true
if [ -n "$POD" ]; then
PHASE=$(kubectl -n $NAMESPACE get pod "$POD" \
-o jsonpath='{.status.phase}' 2>/dev/null) || true
if [ "$PHASE" = "Running" ]; then
echo "Operator pod $POD is Running"
break
fi
fi
echo "Waiting for operator pod ($i/60)..."
sleep 10
done
- name: Setup KMS (vault) for encryption
if: ${{ inputs.use_existing_cluster != 'true' && inputs.tls_enabled }}
run: |
# Ensure vault namespace from previous run is fully terminated before installing.
echo "Checking if vault namespace is still terminating..."
for i in $(seq 1 60); do
NS_STATUS=$(kubectl get namespace vault -o jsonpath='{.status.phase}' 2>/dev/null || echo "gone")
if [ "$NS_STATUS" = "gone" ]; then
echo "vault namespace does not exist — ready to install."
break
fi
if [ "$NS_STATUS" = "Terminating" ]; then
echo "vault namespace still Terminating ($i/60), clearing stuck finalizers..."
kubectl get all -n vault --no-headers -o name 2>/dev/null | while read -r res; do
kubectl patch "$res" -n vault --type=merge -p '{"metadata":{"finalizers":null}}' 2>/dev/null || true
done
kubectl patch namespace vault --type=merge -p '{"spec":{"finalizers":[]}}' 2>/dev/null || true
else
echo "vault namespace in phase '$NS_STATUS' ($i/60), waiting..."
fi
sleep 5
done
if kubectl get namespace vault &>/dev/null; then
echo "WARNING: vault namespace still exists after 300s, force-deleting resources..."
kubectl delete all --all -n vault --force --grace-period=0 2>/dev/null || true
kubectl delete namespace vault --force --grace-period=0 2>/dev/null || true
sleep 10
fi
# Auto-detect default StorageClass for vault PVCs
STORAGE_CLASS=$(kubectl get sc -o jsonpath='{.items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")].metadata.name}' | awk '{print $1}')
if [ -z "$STORAGE_CLASS" ]; then
STORAGE_CLASS=$(kubectl get sc -o jsonpath='{.items[0].metadata.name}')
fi
echo "Using StorageClass for KMS: $STORAGE_CLASS"
export STORAGE_CLASS
UNSEAL_KEYS_FILE=./openbao-init.txt bash $GITHUB_WORKSPACE/simplyblock-operator/helm-charts/scripts/setup-kms.sh
echo "Waiting for vault pods to be ready..."
kubectl wait --for=condition=Ready pods -l app.kubernetes.io/name=openbao -n vault --timeout=300s || true
echo "KMS setup complete."
- name: Apply operator custom resources
if: ${{ inputs.use_existing_cluster != 'true' }}
run: |
NAMESPACE=simplyblock
IFC_NAMES="${{ inputs.ifc_names || 'br-ex:enp2s0f0' }}"
MGMT_IFC="${IFC_NAMES%%:*}"
DATA_NICS="${IFC_NAMES#*:}"
MAX_SUBSYS="${{ inputs.max_subsys || '30' }}"
SB_REPO="${{ inputs.simplyblock_repository || 'public.ecr.aws/simply-block/simplyblock' }}"
SB_TAG="${{ inputs.simplyblock_image }}"
SPDK_IMAGE="${{ inputs.spdk_image }}"
PARTITIONS="${PARTITIONS:-0}"
JM_COUNT="${JM_COUNT:-3}"
OPENSHIFT_CLUSTER="${{ (inputs.cluster_environment == 'aws-openshift' || inputs.cluster_environment == 'openshift-local' || inputs.cluster_environment == 'openshift-baremetal') && 'true' || 'false' }}"
CORE_PERCENTAGE="${{ inputs.cluster_environment == 'aws-openshift' && '50' || inputs.cluster_environment == 'openshift-local' && '50' || inputs.cluster_environment == 'openshift-baremetal' && '50' || inputs.cluster_environment == 'local' && '35' || '65' }}"
SKIP_KUBELET_CONFIG="${{ (inputs.cluster_environment == 'aws-openshift' || inputs.cluster_environment == 'openshift-local' || inputs.cluster_environment == 'openshift-baremetal') && 'false' || 'true' }}"
FORCE_FORMAT_4K="${{ (inputs.cluster_environment == 'aws-openshift' || inputs.cluster_environment == 'openshift-local' || inputs.cluster_environment == 'openshift-baremetal') && 'true' || 'false' }}"
DRIVE_SIZE_RANGE="${{ inputs.cluster_environment == 'openshift-baremetal' && '1500G-2000G' || '1.7T-2T' }}"
ENABLE_CPU_TOPOLOGY="true"
CLUSTER_ENV="${{ inputs.cluster_environment || 'local' }}"
if [ "$CLUSTER_ENV" = "local" ]; then
ENABLE_CPU_TOPOLOGY="false"
fi
# Convert partitions to enableJournalDevice (inverted: 0→true, 1→false)
if [ "${PARTITIONS}" = "0" ]; then
ENABLE_JOURNAL_DEVICE="true"
else
ENABLE_JOURNAL_DEVICE="false"
fi
# Calculate vcpuCount from CORE_PERCENTAGE by querying actual CPU count on a worker node
FIRST_WORKER="${{ inputs.worker_nodes }}"
FIRST_WORKER="${FIRST_WORKER%%,*}"
if [ "$OPENSHIFT_CLUSTER" = "true" ]; then
TOTAL_CPUS=$(oc debug node/"${FIRST_WORKER}" -- chroot /host nproc 2>/dev/null || echo "0")
else
TOTAL_CPUS=$(kubectl get node "${FIRST_WORKER}" -o jsonpath='{.status.capacity.cpu}' 2>/dev/null || echo "0")
fi
TOTAL_CPUS=$(echo "${TOTAL_CPUS}" | tr -d '[:space:]')
if [ "${TOTAL_CPUS}" -gt 0 ] 2>/dev/null; then
VCPU_COUNT=$(( TOTAL_CPUS * CORE_PERCENTAGE / 100 ))
[ "${VCPU_COUNT}" -lt 1 ] && VCPU_COUNT=1
else
echo "WARNING: Could not determine CPU count, defaulting vcpuCount to 4"
VCPU_COUNT=4
fi
echo "Calculated vcpuCount=${VCPU_COUNT} (${TOTAL_CPUS} CPUs * ${CORE_PERCENTAGE}%)"
RESERVED_CPU_YAML=""
if [ "${{ inputs.cluster_environment }}" = "openshift-baremetal" ]; then
RESERVED_CPU_YAML=' reservedSystemCPU: "0,1,10,11,16,17,26,27"'
fi
PCIE_MODEL_YAML=""
CLUSTER_ENV="${{ inputs.cluster_environment }}"
if [ "$CLUSTER_ENV" != "openshift-baremetal" ] && [ "$CLUSTER_ENV" != "openshift-local" ]; then
PCIE_MODEL_YAML=' pcieModel: "SAMSUNG MZQLB1T9HAJR-00007"'
fi
# Build workerNodes YAML list from initial worker nodes only
WORKER_YAML=""
IFS=',' read -ra NODES <<< "${{ inputs.worker_nodes }}"
for NODE in "${NODES[@]}"; do
WORKER_YAML="${WORKER_YAML} - ${NODE}"$'\n'
done
# Conditionally build backup spec block
BACKUP_SPEC=""
CLUSTER_SEC="${{ inputs.cluster_security || 'none' }}"
TESTNAME_INPUT="${{ inputs.testname || '' }}"
if [ "$CLUSTER_SEC" = "backup" ] || [ "$TESTNAME_INPUT" = "backup-topology" ] || echo "$TESTNAME_INPUT" | grep -qi "Backup"; then
BACKUP_SPEC=" backup:
credentialsSecretRef:
name: backup-credentials
localEndpoint: ${MINIO_ENDPOINT}
localTesting: true
secondaryTarget: 0
snapshotBackups: true
withCompression: false"
fi
# Build vault settings and encryption pool blocks (conditional on TLS)
VAULT_SETTINGS=""
ENCRYPTION_POOL_YAML=""
if [ "${{ inputs.tls_enabled }}" = "true" ]; then
VAULT_SETTINGS=" hashicorpVaultSettings:
baseURL: \"https://openbao.vault:8200\""
ENCRYPTION_POOL_YAML="---
apiVersion: storage.simplyblock.io/v1alpha1
kind: StoragePool
metadata:
name: encryption-pool
namespace: ${NAMESPACE}
spec:
clusterName: simplyblock-cluster
storageClassParameters:
encryption: true"
fi
echo "=== Applying custom resources (initial ${#NODES[@]} workers) ==="
cat <<EOF | kubectl apply -f -
apiVersion: storage.simplyblock.io/v1alpha1
kind: StorageCluster
metadata:
name: simplyblock-cluster
namespace: ${NAMESPACE}
spec:
fabricType: tcp
enableNodeAffinity: true
stripe:
dataChunks: ${NDCS}
parityChunks: ${NPCS}
warningThreshold:
capacity: 95
provisionedCapacity: 97
criticalThreshold:
capacity: 96
provisionedCapacity: 98
${BACKUP_SPEC}
${VAULT_SETTINGS}
maxSubsystemCount: ${MAX_SUBSYS}
vcpuCount: ${VCPU_COUNT}
---
apiVersion: storage.simplyblock.io/v1alpha1
kind: StoragePool
metadata:
name: simplyblock-pool
namespace: ${NAMESPACE}
spec:
clusterName: simplyblock-cluster
${ENCRYPTION_POOL_YAML}
---
apiVersion: storage.simplyblock.io/v1alpha1
kind: StorageNodeSet
metadata:
name: simplyblock-node
namespace: ${NAMESPACE}