-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathk8s-run
More file actions
executable file
·201 lines (184 loc) · 6.98 KB
/
Copy pathk8s-run
File metadata and controls
executable file
·201 lines (184 loc) · 6.98 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
#!/usr/bin/env bash
# Generic k8s pod runner — mounts workspace PVC and runs a command in any image.
# Usage: k8s-run --image IMAGE --cmd CMD [--dir RELDIR] [--ns NS] [--pvc PVC] [--keep] [--timeout SEC]
set -euo pipefail
IMAGE=""
RELDIR=""
CMD=""
NAMESPACE="${KRUN_NS:-code-server}"
PVC="${KRUN_PVC:-code-server-workspace}"
KEEP=false
TIMEOUT=600
usage() {
cat <<'EOF'
k8s-run: run a command in a disposable k8s pod with workspace access
--image IMAGE container image to use (required)
--cmd CMD shell command to run (required)
--dir RELDIR path relative to /data/workspace (default: workspace root)
--ns NS kubernetes namespace (default: code-server)
--pvc PVC workspace PVC to mount at /data/workspace (default: code-server-workspace)
--keep keep pod after completion for inspection
--timeout SEC max seconds to wait for completion (default: 600)
Examples:
k8s-run --image ghcr.io/cirruslabs/flutter:stable --dir myapp --cmd "flutter test"
k8s-run --image node:20 --dir webapp --cmd "npm ci && npm test"
k8s-run --image golang:1.22 --dir myservice --cmd "go test ./..."
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case $1 in
--image) IMAGE="$2"; shift 2;;
--dir) RELDIR="$2"; shift 2;;
--cmd) CMD="$2"; shift 2;;
--ns) NAMESPACE="$2"; shift 2;;
--pvc) PVC="$2"; shift 2;;
--keep) KEEP=true; shift;;
--timeout) TIMEOUT="$2"; shift 2;;
-h|--help) usage;;
*) echo "Unknown arg: $1"; usage;;
esac
done
[[ -z "$IMAGE" ]] && { echo "Error: --image required"; usage; }
[[ -z "$CMD" ]] && { echo "Error: --cmd required"; usage; }
# Clean up stale krun pods (finished, older than 1 day) before each run
STALE=$(kubectl get pod -n "$NAMESPACE" -l app=krun \
-o json 2>/dev/null | \
jq -r --argjson cutoff "$(date -d '1 day ago' +%s)" \
'.items[] | select((.metadata.creationTimestamp | fromdateiso8601) < $cutoff) | .metadata.name' \
2>/dev/null || echo "")
if [[ -n "$STALE" ]]; then
echo "==> Cleaning stale pods: $(echo "$STALE" | tr '\n' ' ')"
echo "$STALE" | xargs kubectl delete pod -n "$NAMESPACE" --wait=false 2>/dev/null || true
fi
WORKDIR="/data/workspace${RELDIR:+/$RELDIR}"
IMAGE_SHORT=$(basename "${IMAGE}" | sed 's/:.*$//' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | cut -c1-24)
# k8s label values can't contain '/' (or other invalid chars) — sanitize. The
# workspace path (WORKDIR) keeps the real RELDIR; only this label is cleaned.
DIR_LABEL=$(echo "${RELDIR:-root}" | sed 's#[^A-Za-z0-9._-]#-#g; s#^[._-]*##; s#[._-]*$##' | cut -c1-63)
DIR_LABEL="${DIR_LABEL:-root}"
POD_NAME="krun-${IMAGE_SHORT}-$(date +%s)"
echo "==> Pod: $POD_NAME"
echo " Image: $IMAGE"
echo " Workdir: $WORKDIR"
echo " Cmd: $CMD"
echo ""
POD_JSON=$(jq -n \
--arg name "$POD_NAME" \
--arg ns "$NAMESPACE" \
--arg image "$IMAGE" \
--arg workdir "$WORKDIR" \
--arg cmd "$CMD" \
--arg pvc "$PVC" \
--arg ishort "$IMAGE_SHORT" \
--arg dir "$DIR_LABEL" \
--arg httpproxy "${HTTP_PROXY:-}" \
--arg noproxy "${NO_PROXY:-}" \
'{
apiVersion: "v1",
kind: "Pod",
metadata: {
name: $name,
namespace: $ns,
labels: {app: "krun", "krun-image": $ishort, "krun-dir": $dir}
},
spec: {
restartPolicy: "Never",
affinity: {
podAffinity: {
requiredDuringSchedulingIgnoredDuringExecution: [{
labelSelector: {
matchLabels: {"app.kubernetes.io/name": "code-server"}
},
topologyKey: "kubernetes.io/hostname"
}]
}
},
volumes: [{
name: "workspace",
persistentVolumeClaim: {claimName: $pvc}
}],
containers: [{
name: "runner",
image: $image,
workingDir: $workdir,
command: ["/bin/sh", "-c", $cmd],
# Inherit the gateway egress proxy so build pods are allowlisted too
# (fail-closed: with no proxy set they simply cannot reach the internet).
env: ([
{name: "HTTP_PROXY", value: $httpproxy}, {name: "HTTPS_PROXY", value: $httpproxy},
{name: "http_proxy", value: $httpproxy}, {name: "https_proxy", value: $httpproxy},
{name: "NO_PROXY", value: $noproxy}, {name: "no_proxy", value: $noproxy}
] | map(select(.value != ""))),
securityContext: {
allowPrivilegeEscalation: false,
capabilities: {drop: ["ALL"]},
seccompProfile: {type: "RuntimeDefault"}
},
volumeMounts: [{
name: "workspace",
mountPath: "/data/workspace",
mountPropagation: "HostToContainer"
}]
}]
}
}')
echo "$POD_JSON" | kubectl apply -f - >/dev/null
# Wait for pod to leave Pending (image pull can take time)
echo "==> Waiting for pod to start..."
for i in $(seq 1 90); do
PHASE=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" \
-o jsonpath='{.status.phase}' 2>/dev/null || echo "")
[[ "$PHASE" != "Pending" && -n "$PHASE" ]] && break
if (( i % 5 == 0 )); then
EVENT=$(kubectl get events -n "$NAMESPACE" \
--field-selector "involvedObject.name=$POD_NAME" \
--sort-by='.lastTimestamp' \
-o jsonpath='{.items[-1].message}' 2>/dev/null || echo "")
echo " phase=Pending ${EVENT:+(${EVENT:0:70})}"
fi
sleep 2
done
PHASE=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
if [[ "$PHASE" == "Pending" || -z "$PHASE" ]]; then
echo "Error: pod stuck in Pending"
kubectl get events -n "$NAMESPACE" --field-selector "involvedObject.name=$POD_NAME" --sort-by='.lastTimestamp' || true
[[ "$KEEP" == false ]] && kubectl delete pod "$POD_NAME" -n "$NAMESPACE" --wait=false 2>/dev/null || true
exit 1
fi
echo ""
echo "--- OUTPUT ---"
kubectl logs -n "$NAMESPACE" -f "$POD_NAME" 2>/dev/null || true
echo "--- END ---"
echo ""
# When `kubectl logs -f` returns, the container's terminated state (and exitCode)
# may not be committed to the API yet, so reading it immediately yields an empty
# string and we'd report "FAILED (exit=)" on a successful run. Poll until the
# exitCode is available, falling back to the pod phase.
EXIT_CODE=""
for i in $(seq 1 30); do
EXIT_CODE=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" \
-o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}' 2>/dev/null || echo "")
[[ -n "$EXIT_CODE" ]] && break
sleep 1
done
if [[ -z "$EXIT_CODE" ]]; then
PHASE=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
case "$PHASE" in
Succeeded) EXIT_CODE=0;;
*) EXIT_CODE=1;;
esac
fi
if [[ "$KEEP" == true ]]; then
echo "==> Kept (exit=$EXIT_CODE). Inspect: kubectl logs -n $NAMESPACE $POD_NAME"
echo " Delete: kubectl delete pod $POD_NAME -n $NAMESPACE"
else
kubectl delete pod "$POD_NAME" -n "$NAMESPACE" --wait=false 2>/dev/null || true
fi
if [[ "${EXIT_CODE}" == "0" ]]; then
echo "==> SUCCESS"
exit 0
else
echo "==> FAILED (exit=$EXIT_CODE)"
exit 1
fi