-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathvalidate.sh
More file actions
executable file
·340 lines (294 loc) · 11.9 KB
/
Copy pathvalidate.sh
File metadata and controls
executable file
·340 lines (294 loc) · 11.9 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
#!/usr/bin/env bash
# =============================================================================
# hack/e2e/lib/validate.sh - Node join verification and smoke tests
#
# Functions:
# validate_node_joined <vm_name> - Wait for a specific node to appear in kubectl
# validate_all_nodes - Verify MSI, token, and kubeadm nodes joined
# validate_npd_status <vm_name> <vm_ip> - Verify node-problem-detector is active
# validate_node_absent <vm_name> - Wait for a node to disappear from kubectl
# validate_all_nodes_absent - Verify all flex nodes are gone after unjoin
# smoke_test <vm_name> <label> - Schedule an nginx pod on a node
# smoke_test_all - Run smoke tests on all nodes
# =============================================================================
set -euo pipefail
[[ -n "${_E2E_VALIDATE_LOADED:-}" ]] && return 0
readonly _E2E_VALIDATE_LOADED=1
# shellcheck disable=SC1091
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
# ---------------------------------------------------------------------------
# validate_node_joined - Wait for a node to appear in the cluster
# ---------------------------------------------------------------------------
validate_node_joined() {
local vm_name="$1"
local timeout="${E2E_NODE_JOIN_TIMEOUT}"
local elapsed=0
log_info "Waiting for node '${vm_name}' to join cluster (timeout: ${timeout}s)..."
while [[ "${elapsed}" -lt "${timeout}" ]]; do
if kubectl get nodes 2>/dev/null | grep -q "${vm_name}"; then
log_success "Node '${vm_name}' joined the cluster"
kubectl get node "${vm_name}" -o wide
return 0
fi
sleep 10
elapsed=$((elapsed + 10))
log_debug "Waiting for ${vm_name}... (${elapsed}/${timeout}s)"
done
log_error "Node '${vm_name}' did not join cluster within ${timeout}s"
log_error "Current nodes:"
kubectl get nodes 2>&1 || true
echo ""
log_error "Certificate Signing Requests:"
kubectl get csr 2>&1 || true
return 1
}
# ---------------------------------------------------------------------------
# validate_node_ip - Ensure node InternalIP matches the expected provisioned IP
# ---------------------------------------------------------------------------
validate_node_ip() {
local vm_name="$1"
local expected_ip="$2"
if [[ -z "${expected_ip}" ]] || ! is_valid_ipv4 "${expected_ip}"; then
log_error "Expected node IP is empty or invalid for '${vm_name}': '${expected_ip}'"
return 1
fi
local internal_ips
internal_ips="$(kubectl get node "${vm_name}" -o jsonpath='{range .status.addresses[?(@.type=="InternalIP")]}{.address}{"\n"}{end}')"
if grep -Fxq "${expected_ip}" <<<"${internal_ips}"; then
log_success "Node '${vm_name}' InternalIP includes expected IP '${expected_ip}'"
return 0
fi
log_error "Node '${vm_name}' InternalIP mismatch; expected '${expected_ip}'"
log_error "Observed InternalIP values:"
echo "${internal_ips}"
return 1
}
# ---------------------------------------------------------------------------
# validate_npd_status - Ensure node-problem-detector is active and reporting
# ---------------------------------------------------------------------------
validate_npd_status() {
local vm_name="$1"
local vm_ip="$2"
local timeout="${E2E_NODE_JOIN_TIMEOUT}"
local elapsed=0
local npd_condition_jsonpath='{.status.conditions[?(@.type=="KernelDeadlock")].status}'
local condition_error="${E2E_WORK_DIR}/npd-condition-${vm_name}.err"
local quoted_timeout
log_info "Validating node-problem-detector on '${vm_name}'..."
if ! [[ "${timeout}" =~ ^[0-9]+$ ]]; then
log_error "E2E_NODE_JOIN_TIMEOUT must be numeric, got '${timeout}'"
return 1
fi
printf -v quoted_timeout "%q" "${timeout}"
remote_exec "${vm_ip}" "E2E_NODE_JOIN_TIMEOUT=${quoted_timeout} bash -s" <<'REMOTE'
set -euo pipefail
deadline=$((SECONDS + E2E_NODE_JOIN_TIMEOUT))
active_machine_error="/tmp/aks-flex-node-e2e-active-machine-$$.err"
status_error="/tmp/aks-flex-node-e2e-npd-status-$$.err"
while true; do
if [[ ! -f /etc/aks-flex-node/daemon-state.json ]]; then
active_machine=""
echo "/etc/aks-flex-node/daemon-state.json is missing" > "${active_machine_error}"
else
active_machine="$(sudo python3 - <<'PY' 2>"${active_machine_error}" || true
import json
with open("/etc/aks-flex-node/daemon-state.json", encoding="utf-8") as state:
print(json.load(state).get("activeMachine", ""))
PY
)"
fi
if [[ -n "${active_machine}" ]] && machinectl show "${active_machine}" &>/dev/null; then
status="$(sudo systemd-run --machine="${active_machine}" --quiet --pipe systemctl is-active node-problem-detector.service 2>"${status_error}" || true)"
if [[ "${status}" == "active" ]]; then
echo "node-problem-detector.service is active in ${active_machine}"
exit 0
fi
fi
if (( SECONDS >= deadline )); then
echo "node-problem-detector.service did not become active"
if [[ -s "${active_machine_error}" ]]; then
cat "${active_machine_error}"
fi
if [[ -s "${status_error}" ]]; then
cat "${status_error}"
fi
machinectl list --no-pager || true
if [[ -n "${active_machine:-}" ]]; then
sudo systemd-run --machine="${active_machine}" --quiet --pipe systemctl status node-problem-detector.service --no-pager -l || true
sudo systemd-run --machine="${active_machine}" --quiet --pipe journalctl -u node-problem-detector.service -n 50 --no-pager || true
fi
exit 1
fi
sleep 5
done
REMOTE
local kernel_deadlock
while [[ "${elapsed}" -lt "${timeout}" ]]; do
kernel_deadlock="$(kubectl get node "${vm_name}" -o jsonpath="${npd_condition_jsonpath}" 2>"${condition_error}" || true)"
if [[ "${kernel_deadlock}" == "False" ]]; then
log_success "node-problem-detector is active and reporting on '${vm_name}'"
return 0
fi
sleep 10
elapsed=$((elapsed + 10))
log_debug "Waiting for node-problem-detector condition on ${vm_name}... (${elapsed}/${timeout}s)"
done
log_error "node-problem-detector did not report KernelDeadlock=False on '${vm_name}' within ${timeout}s"
if [[ -s "${condition_error}" ]]; then
cat "${condition_error}" >&2
fi
kubectl describe node "${vm_name}" 2>&1 || true
return 1
}
# ---------------------------------------------------------------------------
# validate_all_nodes - Check all MSI, token, and kubeadm VMs joined
# ---------------------------------------------------------------------------
validate_all_nodes() {
log_section "Validating Node Join"
# Re-fetch kubeconfig to be safe
local cluster_name resource_group
cluster_name="$(state_get cluster_name)"
resource_group="$(state_get resource_group)"
az aks get-credentials \
--resource-group "${resource_group}" \
--name "${cluster_name}" \
--overwrite-existing \
--admin
local msi_vm_name token_vm_name kubeadm_vm_name
local msi_vm_ip token_vm_ip kubeadm_vm_ip
local token_vm_private_ip
msi_vm_name="$(state_get msi_vm_name)"
token_vm_name="$(state_get token_vm_name)"
kubeadm_vm_name="$(state_get kubeadm_vm_name)"
msi_vm_ip="$(state_get msi_vm_ip)"
token_vm_ip="$(state_get token_vm_ip)"
kubeadm_vm_ip="$(state_get kubeadm_vm_ip)"
token_vm_private_ip="$(state_get token_vm_private_ip)"
local failed=0
validate_node_joined "${msi_vm_name}" || failed=1
validate_node_joined "${token_vm_name}" || failed=1
validate_node_joined "${kubeadm_vm_name}" || failed=1
validate_node_ip "${token_vm_name}" "${token_vm_private_ip}" || failed=1
validate_npd_status "${msi_vm_name}" "${msi_vm_ip}" || failed=1
validate_npd_status "${token_vm_name}" "${token_vm_ip}" || failed=1
validate_npd_status "${kubeadm_vm_name}" "${kubeadm_vm_ip}" || failed=1
if [[ "${failed}" -eq 1 ]]; then
log_error "One or more nodes failed to join"
return 1
fi
echo ""
log_info "All cluster nodes:"
kubectl get nodes -o wide
log_success "All nodes verified in cluster"
}
# ---------------------------------------------------------------------------
# validate_node_absent - Wait for a node to disappear from the cluster
# ---------------------------------------------------------------------------
validate_node_absent() {
local vm_name="$1"
local timeout="${E2E_NODE_JOIN_TIMEOUT}"
local elapsed=0
log_info "Waiting for node '${vm_name}' to leave cluster (timeout: ${timeout}s)..."
while [[ "${elapsed}" -lt "${timeout}" ]]; do
if ! kubectl get node "${vm_name}" &>/dev/null; then
log_success "Node '${vm_name}' is no longer in the cluster"
return 0
fi
sleep 10
elapsed=$((elapsed + 10))
log_debug "Waiting for ${vm_name} to disappear... (${elapsed}/${timeout}s)"
done
log_error "Node '${vm_name}' still present in cluster after ${timeout}s"
log_error "Current nodes:"
kubectl get nodes 2>&1 || true
return 1
}
# ---------------------------------------------------------------------------
# validate_all_nodes_absent - Check all flex nodes are gone after unjoin
# ---------------------------------------------------------------------------
validate_all_nodes_absent() {
log_section "Validating Nodes Absent After Unjoin"
local msi_vm_name token_vm_name kubeadm_vm_name
msi_vm_name="$(state_get msi_vm_name)"
token_vm_name="$(state_get token_vm_name)"
kubeadm_vm_name="$(state_get kubeadm_vm_name)"
local failed=0
# TODO: MSI validation skipped until credential plugin auth is supported
log_info "Skipping MSI node absence validation (credential plugin auth not yet supported)"
validate_node_absent "${token_vm_name}" || failed=1
validate_node_absent "${kubeadm_vm_name}" || failed=1
if [[ "${failed}" -eq 1 ]]; then
log_error "One or more nodes still present after unjoin"
return 1
fi
echo ""
log_info "All cluster nodes:"
kubectl get nodes -o wide
log_success "All flex nodes confirmed absent"
}
# ---------------------------------------------------------------------------
# smoke_test - Schedule a pod on a specific node and wait for Ready
# ---------------------------------------------------------------------------
smoke_test() {
local vm_name="$1"
local label="$2"
local pod_name="e2e-smoke-${label}"
log_info "Smoke test: scheduling '${pod_name}' on node '${vm_name}'..."
# Create pod manifest
local manifest="${E2E_WORK_DIR}/${pod_name}.yaml"
cat > "${manifest}" <<EOF
apiVersion: v1
kind: Pod
metadata:
name: ${pod_name}
spec:
nodeSelector:
kubernetes.io/hostname: ${vm_name}
tolerations:
- effect: NoSchedule
operator: Exists
containers:
- name: nginx
image: nginx:alpine
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
EOF
kubectl apply -f "${manifest}"
if kubectl wait --for=condition=Ready "pod/${pod_name}" --timeout="${E2E_POD_READY_TIMEOUT}s"; then
log_success "Smoke test PASSED for '${pod_name}' on '${vm_name}'"
kubectl get pod "${pod_name}" -o wide
kubectl delete pod "${pod_name}" --wait=false
return 0
else
log_error "Smoke test FAILED for '${pod_name}' on '${vm_name}'"
kubectl describe pod "${pod_name}" 2>&1 || true
kubectl delete pod "${pod_name}" --wait=false 2>/dev/null || true
return 1
fi
}
# ---------------------------------------------------------------------------
# smoke_test_all - Run smoke tests on all nodes
# ---------------------------------------------------------------------------
smoke_test_all() {
log_section "Running Smoke Tests"
local msi_vm_name token_vm_name kubeadm_vm_name
msi_vm_name="$(state_get msi_vm_name)"
token_vm_name="$(state_get token_vm_name)"
kubeadm_vm_name="$(state_get kubeadm_vm_name)"
# A default bridge CNI config (99-bridge.conf) is written during bootstrap,
# making nodes Ready without requiring unbounded-net-node DaemonSet.
local failed=0
smoke_test "${msi_vm_name}" "msi" || failed=1
smoke_test "${token_vm_name}" "token" || failed=1
smoke_test "${kubeadm_vm_name}" "kubeadm" || failed=1
if [[ "${failed}" -eq 1 ]]; then
log_error "One or more smoke tests failed"
return 1
fi
log_success "All smoke tests passed"
}