@openhands/extensions 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/check-extensions.yml +4 -0
- package/.github/workflows/pr-artifacts.yml +294 -0
- package/.github/workflows/pr-description-check.yml +4 -2
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +28 -4
- package/README.md +5 -2
- package/automations/README.md +52 -36
- package/automations/bundle-index.js +13 -3
- package/automations/catalog/custom-automation/manifest.json +229 -0
- package/automations/catalog/github-agents-md-maintainer/manifest.json +2 -1
- package/automations/catalog/github-issue-to-pr/manifest.json +3 -2
- package/automations/catalog/github-issue-triage/manifest.json +72 -0
- package/automations/catalog/github-pr-reviewer/manifest.json +3 -2
- package/automations/catalog/gitlab-issue-to-mr/manifest.json +124 -0
- package/automations/catalog/qa-changes/manifest.json +1 -1
- package/automations/catalog-index.js +23 -17
- package/automations/catalog.schema.json +81 -7
- package/automations/index.d.ts +52 -2
- package/automations/interface.json +2 -1
- package/integrations/catalog/gitlab.json +27 -5
- package/integrations/catalog/reportportal.json +76 -0
- package/integrations/catalog-index.js +6 -4
- package/marketplaces/openhands-extensions.json +36 -0
- package/package.json +1 -1
- package/pyproject.toml +2 -2
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/github/scripts/github_client.py +204 -0
- package/skills/github-agents-md-maintainer/SKILL.md +7 -1
- package/skills/github-agents-md-maintainer/scripts/main.py +4 -40
- package/skills/github-issue-to-pr/SKILL.md +8 -1
- package/skills/github-issue-to-pr/scripts/main.py +3 -40
- package/skills/github-issue-triage/.plugin/plugin.json +9 -0
- package/skills/github-issue-triage/README.md +9 -0
- package/skills/github-issue-triage/SKILL.md +37 -0
- package/skills/github-issue-triage/commands/github-issue-triage.md +8 -0
- package/skills/github-issue-triage/scripts/worker.py +123 -0
- package/skills/github-pr-reviewer/SKILL.md +8 -1
- package/skills/github-pr-reviewer/scripts/main.py +3 -41
- package/skills/github-repo-monitor/SKILL.md +7 -1
- package/skills/github-repo-monitor/scripts/main.py +4 -46
- package/skills/gitlab-issue-to-mr/.plugin/plugin.json +19 -0
- package/skills/gitlab-issue-to-mr/README.md +92 -0
- package/skills/gitlab-issue-to-mr/SKILL.md +408 -0
- package/skills/gitlab-issue-to-mr/commands/issue-to-mr-setup.md +8 -0
- package/skills/gitlab-issue-to-mr/references/state-schema.md +140 -0
- package/skills/gitlab-issue-to-mr/scripts/main.py +1448 -0
- package/skills/index.js +43 -6
- package/skills/openhands-automation/README.md +1 -1
- package/skills/openhands-automation/SKILL.md +18 -8
- package/skills/openhands-enterprise-troubleshooting/.plugin/plugin.json +20 -0
- package/skills/openhands-enterprise-troubleshooting/README.md +79 -0
- package/skills/openhands-enterprise-troubleshooting/SKILL.md +339 -0
- package/skills/openhands-enterprise-troubleshooting/references/diagnostics.md +698 -0
- package/skills/openhands-enterprise-troubleshooting/references/support-bundle-analysis.md +570 -0
- package/skills/openhands-enterprise-troubleshooting/scripts/bundle_triage.py +532 -0
- package/skills/openhands-sdk/SKILL.md +2 -0
- package/tests/fixtures/automations/custom-automation.json +398 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/github_automation_helpers.py +25 -0
- package/tests/test_automation_setup.py +340 -92
- package/tests/test_github_automation_foundation.py +73 -0
- package/tests/test_github_skill_installation.py +63 -0
- package/tests/test_github_triage_delivery.py +53 -0
- package/tests/test_gitlab_issue_to_mr.py +823 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
# OHE Diagnostics Reference
|
|
2
|
+
|
|
3
|
+
Detailed diagnostic procedures for each OpenHands Enterprise failure mode. Run these commands on the
|
|
4
|
+
VM via SSH.
|
|
5
|
+
|
|
6
|
+
**`kubectl` is not on the PATH by default**, so nothing below works until you get a working client.
|
|
7
|
+
`sudo ./openhands shell` exports the kubeconfig and puts `kubectl` on your PATH — but it needs a TTY,
|
|
8
|
+
so it will not work from a non-interactive session or an agent without one. In that case use the
|
|
9
|
+
bundled k0s client instead, which needs no TTY:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
sudo k0s kubectl get pods -A
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Note that `kubectl support-bundle` resolves only inside `openhands shell`; the plugin is not on the
|
|
16
|
+
PATH for `sudo k0s kubectl`.
|
|
17
|
+
|
|
18
|
+
**Working from a support bundle instead of a live cluster?** Every command here has an offline
|
|
19
|
+
equivalent. See [`support-bundle-analysis.md`](support-bundle-analysis.md) for the full mapping; the
|
|
20
|
+
short version:
|
|
21
|
+
|
|
22
|
+
| Live command | In the bundle |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `kubectl get pods -n <ns>` | `cluster-resources/pods/<ns>.json` |
|
|
25
|
+
| `kubectl logs <pod> -c <container>` | `cluster-resources/pods/logs/<ns>/<pod>/<container>.log` |
|
|
26
|
+
| `kubectl describe pod` | Reconstruct from `cluster-resources/pods/<ns>.json` (no `describe` is stored) |
|
|
27
|
+
| `kubectl get events -n <ns>` | `cluster-resources/events/<ns>.json` (short TTL window) |
|
|
28
|
+
| `kubectl top pods` / `top nodes` | `node-metrics/<node>.json` |
|
|
29
|
+
| `kubectl get ingress -n <ns>` | `cluster-resources/ingress/<ns>.json` |
|
|
30
|
+
| `kubectl get configmap/secret` | `cluster-resources/configmaps/<ns>.json`, `secrets/<ns>/` (keys only) |
|
|
31
|
+
|
|
32
|
+
Start with `python3 scripts/bundle_triage.py <bundle>` and `analysis.json` before hand-writing `jq`.
|
|
33
|
+
|
|
34
|
+
## Sandbox Startup
|
|
35
|
+
|
|
36
|
+
Sandboxes are created on demand by the **`runtime-api`** service; each one is its own
|
|
37
|
+
`runtime-*` pod. There is no long-lived "sandbox" deployment, so start with `runtime-api` when
|
|
38
|
+
sandboxes fail to start at all, and with the individual `runtime-*` pod when one specific
|
|
39
|
+
conversation fails.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# The service that creates sandboxes
|
|
43
|
+
kubectl get pods -n openhands -l app.kubernetes.io/name=runtime-api
|
|
44
|
+
kubectl logs -n openhands -l app.kubernetes.io/name=runtime-api --tail=100
|
|
45
|
+
|
|
46
|
+
# The sandboxes themselves
|
|
47
|
+
kubectl get pods -n openhands | grep '^runtime-'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Look for: `Running` status, multiple restarts, `ImagePullBackOff`, `CrashLoopBackOff`
|
|
51
|
+
|
|
52
|
+
Zero `runtime-` pods is not itself a fault. These are per-conversation sandboxes plus any warm pool,
|
|
53
|
+
so an idle install with the pool at zero legitimately has none, and a pod that is present may be a
|
|
54
|
+
warm spare rather than anyone's session. Judge sandbox startup by whether a *new* conversation gets
|
|
55
|
+
one, and by `runtime-api` logs — not by the count here.
|
|
56
|
+
|
|
57
|
+
### Check Sandbox Logs
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# A specific sandbox pod
|
|
61
|
+
kubectl logs -n openhands <runtime-pod> --tail=100
|
|
62
|
+
|
|
63
|
+
# Previous log, if it restarted
|
|
64
|
+
kubectl logs -n openhands <runtime-pod> --previous
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Common Sandbox Startup Errors
|
|
68
|
+
|
|
69
|
+
| Error Pattern | Likely Cause | Check |
|
|
70
|
+
|---------------|--------------|-------|
|
|
71
|
+
| `ImagePullBackOff` | Registry auth, network | `kubectl describe pod` for image pull error |
|
|
72
|
+
| `CrashLoopBackOff` | Config error, missing secret | `kubectl logs --previous` |
|
|
73
|
+
| `Init:Error` | Init container failed | `kubectl describe pod` for init container status |
|
|
74
|
+
| `Timeout` | Resource exhaustion, runtime issue | `kubectl top pods` |
|
|
75
|
+
|
|
76
|
+
### Sandbox Runtime Check
|
|
77
|
+
|
|
78
|
+
Sandboxes run under the **`sysbox-runc`** RuntimeClass, which maps to a sysbox containerd runtime
|
|
79
|
+
registered on each node by a DaemonSet. If that registration failed, every sandbox stays Pending or
|
|
80
|
+
fails to create while the rest of the platform looks healthy.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# The RuntimeClass sandboxes depend on
|
|
84
|
+
kubectl get runtimeclass sysbox-runc
|
|
85
|
+
|
|
86
|
+
# The installer that registers it on each node
|
|
87
|
+
kubectl get pods -A -l app.kubernetes.io/name=sysbox-installer
|
|
88
|
+
|
|
89
|
+
# Disk space inside a running sandbox
|
|
90
|
+
kubectl exec -n openhands <runtime-pod> -- df -h
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A missing runtime is not the only reason a sandbox stays Pending. If `runtime-api` is configured
|
|
94
|
+
with a `RUNTIME_NODE_SELECTOR`, sandboxes can only land on nodes carrying the labels it names, and
|
|
95
|
+
capacity on any other node is unreachable. The scheduler says which it was:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# The scheduler's own reason — names the selector, the affinity, or the missing resource
|
|
99
|
+
kubectl describe pod -n openhands <runtime-pod> | grep -A5 Events
|
|
100
|
+
|
|
101
|
+
# What the nodes actually offer
|
|
102
|
+
kubectl get nodes --show-labels
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Git Provider Auth
|
|
108
|
+
|
|
109
|
+
Four providers are supported, each configured independently: **GitHub**, **GitLab**,
|
|
110
|
+
**Bitbucket Data Center**, and **Azure DevOps**. There is no `git-provider-secret` and no
|
|
111
|
+
per-provider pod — each provider is one Secret, consumed as environment variables by the main
|
|
112
|
+
`openhands` deployment. So provider auth failures show up in the app's own logs, not in a
|
|
113
|
+
dedicated workload.
|
|
114
|
+
|
|
115
|
+
| Provider | Secret | Keys |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| GitHub | `github-app` | `app-id`, `app-slug`, `client-id`, `client-secret`, `private-key`, `webhook-secret` |
|
|
118
|
+
| GitLab | `gitlab-app` | `client-id`, `client-secret` |
|
|
119
|
+
| Bitbucket Data Center | `bitbucket-data-center-app` | `host`, `client-id`, `client-secret`, `bot-token` |
|
|
120
|
+
| Azure DevOps | `azure-devops-app` | `client-id`, `client-secret`, `webhook-secret` |
|
|
121
|
+
|
|
122
|
+
### Which providers are configured
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Only configured providers have a secret -- absence is the usual "auth broken" cause
|
|
126
|
+
kubectl get secret -n openhands github-app gitlab-app bitbucket-data-center-app azure-devops-app 2>&1
|
|
127
|
+
|
|
128
|
+
# Confirm the app was actually told to enable it (secret present but disabled is a common trap).
|
|
129
|
+
# Provider variables are not all bare-prefixed — expect OPENHANDS_*_SERVICE_CLS,
|
|
130
|
+
# OH_WEB_CLIENT_PROVIDERS_CONFIGURED and *_APP_CLIENT_* alongside GITHUB_/GITLAB_.
|
|
131
|
+
kubectl set env deploy/openhands -n openhands --list \
|
|
132
|
+
| grep -iE 'github|gitlab|bitbucket|azure|providers_configured'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`OH_WEB_CLIENT_PROVIDERS_CONFIGURED` is the most direct answer to "which provider does the app think
|
|
136
|
+
it has": it lists them.
|
|
137
|
+
|
|
138
|
+
### Check a provider secret has the keys it needs
|
|
139
|
+
|
|
140
|
+
Drive this off whichever secret actually exists rather than assuming GitHub — most installs have one
|
|
141
|
+
provider, and the other three will be `NotFound`. This prints key names and byte lengths, never the
|
|
142
|
+
values:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
for s in github-app gitlab-app bitbucket-data-center-app azure-devops-app; do
|
|
146
|
+
kubectl get secret -n openhands "$s" >/dev/null 2>&1 || continue
|
|
147
|
+
echo "== $s"
|
|
148
|
+
kubectl get secret -n openhands "$s" \
|
|
149
|
+
-o go-template='{{range $k,$v := .data}}{{$k}}={{len $v}} bytes{{"\n"}}{{end}}'
|
|
150
|
+
done
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Compare the keys against the row for that provider in the table above.
|
|
154
|
+
|
|
155
|
+
A key present but zero-length is the failure worth looking for — Helm renders empty values into a
|
|
156
|
+
valid Secret, so the object exists and looks correct while auth fails.
|
|
157
|
+
|
|
158
|
+
### Validate credentials against the provider
|
|
159
|
+
|
|
160
|
+
Each provider uses a different auth scheme and endpoint. Bitbucket Data Center and Azure DevOps are
|
|
161
|
+
self-hosted, so the host comes from your config, not a fixed domain.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# GitHub App -- requires a signed JWT, so a plain token check is not meaningful.
|
|
165
|
+
# Verify the app can see its installations (run from a pod with the credentials):
|
|
166
|
+
curl -s -H "Authorization: Bearer $GITHUB_JWT" https://api.github.com/app/installations
|
|
167
|
+
|
|
168
|
+
# GitLab
|
|
169
|
+
curl -s -H "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab.com/api/v4/user"
|
|
170
|
+
|
|
171
|
+
# Bitbucket Data Center -- self-hosted, REST API 1.0, bot token
|
|
172
|
+
curl -s -H "Authorization: Bearer $BITBUCKET_DATA_CENTER_BOT_TOKEN" \
|
|
173
|
+
"https://$BITBUCKET_DATA_CENTER_HOST/rest/api/1.0/users"
|
|
174
|
+
|
|
175
|
+
# Azure DevOps -- PAT, basic auth with an empty username
|
|
176
|
+
curl -s -u ":$AZURE_DEVOPS_TOKEN" \
|
|
177
|
+
"https://dev.azure.com/$AZURE_DEVOPS_ORG/_apis/projects?api-version=7.0"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
For a self-hosted provider, an auth failure is often really a **network or TLS** failure: the
|
|
181
|
+
cluster may not be able to reach the Bitbucket or Azure DevOps host at all, or may reject its
|
|
182
|
+
certificate. Test reachability from inside a pod before assuming the credentials are wrong, and see
|
|
183
|
+
the Certificate Issues section below for private CAs.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Certificate Issues
|
|
188
|
+
|
|
189
|
+
### Check Certificate Expiry
|
|
190
|
+
|
|
191
|
+
Read the port off the ingress controller Service. Do not go looking for a listening socket: a
|
|
192
|
+
NodePort is served by kube-proxy DNAT rules, so `ss -lntp` shows nothing even though the port works.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
kubectl get svc -A | grep -iE 'traefik|ingress-nginx' # e.g. 80:80/TCP,443:443/TCP
|
|
196
|
+
|
|
197
|
+
# HOST must be a bare hostname, not a URL — a leading https:// makes the
|
|
198
|
+
# hostname check below fail against a perfectly good certificate.
|
|
199
|
+
HOST="your-openhands-domain.com"
|
|
200
|
+
|
|
201
|
+
# The public name generally does not resolve from the VM itself, so connect to
|
|
202
|
+
# the local listener and pass the name as SNI.
|
|
203
|
+
echo | openssl s_client -connect 127.0.0.1:443 -servername $HOST 2>/dev/null \
|
|
204
|
+
| openssl x509 -noout -dates
|
|
205
|
+
|
|
206
|
+
# s_client does NOT check the hostname unless you ask it to: a certificate for
|
|
207
|
+
# the wrong name still reports "Verify return code: 0 (ok)". Add -verify_hostname
|
|
208
|
+
# to actually test the name, or a mismatch will read as healthy.
|
|
209
|
+
# Expect 0 (ok) for the right name, 62 (hostname mismatch) for the wrong one.
|
|
210
|
+
echo | openssl s_client -connect 127.0.0.1:443 -servername $HOST \
|
|
211
|
+
-verify_hostname $HOST 2>/dev/null | grep -E "Verify return code"
|
|
212
|
+
|
|
213
|
+
# Find the TLS secret the ingress actually references, then read its certificate.
|
|
214
|
+
# There is no `app=ingress-tls` label — the secret is named in the ingress spec.
|
|
215
|
+
kubectl get ingress -n openhands \
|
|
216
|
+
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.tls[*].secretName}{"\n"}{end}'
|
|
217
|
+
|
|
218
|
+
kubectl get secret -n openhands <tls-secret> -o jsonpath='{.data.tls\.crt}' \
|
|
219
|
+
| base64 -d | openssl x509 -noout -dates -subject -issuer
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A blank `secretName` against a `runtime-<id>` ingress is normal, not a fault: sandbox routing is
|
|
223
|
+
HTTP-only and TLS for it is handled by the ingress controller's default certificate.
|
|
224
|
+
|
|
225
|
+
### Check Certificate Chain
|
|
226
|
+
|
|
227
|
+
From outside the VM, use `-connect $HOST:443` instead.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Get full certificate chain
|
|
231
|
+
echo | openssl s_client -connect 127.0.0.1:443 -servername $HOST -showcerts 2>/dev/null
|
|
232
|
+
|
|
233
|
+
# Check chain completeness
|
|
234
|
+
echo | openssl s_client -connect 127.0.0.1:443 -servername $HOST 2>/dev/null | grep -A2 "Certificate chain"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Common Certificate Errors
|
|
238
|
+
|
|
239
|
+
| Error | Cause | Fix |
|
|
240
|
+
|-------|-------|-----|
|
|
241
|
+
| `CERT_HAS_EXPIRED` | Certificate expired | Renew certificate |
|
|
242
|
+
| `self signed certificate` | Self-signed in chain | Install proper chain |
|
|
243
|
+
| `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | Intermediate missing | Ensure full chain in ingress |
|
|
244
|
+
| `hostname mismatch` (openssl, verify code 62); `no alternative certificate subject name matches target host name` (curl) | Wrong CN/SAN | Reissue with correct hostname |
|
|
245
|
+
|
|
246
|
+
### Ingress TLS Check
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
kubectl get ingress -n openhands -o yaml | grep -A5 "tls:"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## LLM Connectivity
|
|
255
|
+
|
|
256
|
+
### Check LLM Configuration
|
|
257
|
+
|
|
258
|
+
LLM wiring lives in the LiteLLM config map and the env secrets, not in a `llm-config` /
|
|
259
|
+
`llm-credentials` pair:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# Model routing: which models are defined and where they point
|
|
263
|
+
kubectl get configmap -n openhands openhands-litellm-config \
|
|
264
|
+
-o jsonpath='{.data.config\.yaml}'
|
|
265
|
+
|
|
266
|
+
# Credential wiring — key names only, never values
|
|
267
|
+
kubectl get secret -n openhands litellm-env-secrets \
|
|
268
|
+
-o go-template='{{range $k,$v := .data}}{{$k}}={{len $v}} bytes{{"\n"}}{{end}}'
|
|
269
|
+
kubectl get secret -n openhands openhands-env-secrets \
|
|
270
|
+
-o go-template='{{range $k,$v := .data}}{{$k}}={{len $v}} bytes{{"\n"}}{{end}}'
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Expect keys along the lines of `LLM_API_KEY`, `LLM_BASE_URL`, `LLM_MODEL`. A key that is present but
|
|
274
|
+
zero bytes is the failure worth finding: Helm renders an empty value into a valid Secret, so the
|
|
275
|
+
object looks correct while every request fails to authenticate.
|
|
276
|
+
|
|
277
|
+
### Test LLM Endpoint
|
|
278
|
+
|
|
279
|
+
> **Do not decode the API key into your shell.** It is a live credential, and on a real install that
|
|
280
|
+
> puts it into your terminal history and any agent transcript. Run the request *inside* the pod that
|
|
281
|
+
> already holds the key, so the value never leaves the container.
|
|
282
|
+
|
|
283
|
+
**The secret key names are not the pod's variable names.** The keys above are what the Secret
|
|
284
|
+
contains; the app pod exposes the endpoint as `LITE_LLM_API_URL` and `LITE_LLM_API_KEY`. Always list
|
|
285
|
+
what the pod actually has before building a request against it:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
kubectl exec -n openhands deploy/openhands -- printenv \
|
|
289
|
+
| grep -iE 'LLM|LITE_LLM' | sed 's/=.*/=<set>/'
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Then use the names it reports:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
kubectl exec -n openhands deploy/openhands -- sh -c '
|
|
296
|
+
curl -s -o /dev/null -w "HTTP_CODE:%{http_code}\n" \
|
|
297
|
+
-H "Authorization: Bearer $LITE_LLM_API_KEY" \
|
|
298
|
+
"$LITE_LLM_API_URL/v1/models"'
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A 200 confirms the credential and the network path. A 401 or 403 means the credential is wrong; a
|
|
302
|
+
timeout or `connection refused` means the endpoint is unreachable from the cluster, which is a
|
|
303
|
+
network problem rather than a credential one. An empty variable in the request — producing a URL like
|
|
304
|
+
`/v1/models` with no host — means you used a secret key name rather than the pod's variable name.
|
|
305
|
+
If `curl` is absent from the image, read the failure out of the application log instead: it reports
|
|
306
|
+
the upstream status on every failed call.
|
|
307
|
+
|
|
308
|
+
### Network Policy Check
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Check if pods have network policies
|
|
312
|
+
kubectl get networkpolicy -n openhands
|
|
313
|
+
|
|
314
|
+
# Test DNS resolution from pod. The app image ships no `nslookup` or `dig`,
|
|
315
|
+
# so use getent, which is part of libc and always present.
|
|
316
|
+
kubectl exec -n openhands deploy/openhands -- getent hosts api.openai.com
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
No output from `getent` means the name did not resolve. If you need to prove the whole path rather
|
|
320
|
+
than just DNS, `curl -sS -o /dev/null -w '%{http_code}\n' https://api.openai.com` distinguishes a
|
|
321
|
+
resolution failure from a routing or TLS one by the error it reports.
|
|
322
|
+
|
|
323
|
+
### Common LLM Errors
|
|
324
|
+
|
|
325
|
+
| Error Pattern | Cause | Fix |
|
|
326
|
+
|---------------|-------|-----|
|
|
327
|
+
| `connection refused` | Wrong endpoint | Verify LLM endpoint URL |
|
|
328
|
+
| `401 Unauthorized` | Bad API key | Re-create/rotate API key |
|
|
329
|
+
| `403 Forbidden` | Insufficient permissions | Check model access |
|
|
330
|
+
| `timed out`, `Connection timed out`, `context deadline exceeded` | Network policy/firewall | Check network policies |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Keycloak
|
|
335
|
+
|
|
336
|
+
Keycloak runs **in the `openhands` namespace**, as a StatefulSet rather than a Deployment — so the
|
|
337
|
+
pod is `keycloak-0` and the selector is `app.kubernetes.io/name=keycloak`. There is no `keycloak`
|
|
338
|
+
namespace and no `app=keycloak` label. Addressing it as a Deployment, or in its own namespace, is
|
|
339
|
+
the most common way these commands silently return nothing.
|
|
340
|
+
|
|
341
|
+
### Check Keycloak Pods
|
|
342
|
+
|
|
343
|
+
**Start with the StatefulSet, not the pod list.** A pod query returns `No resources found` and exits
|
|
344
|
+
0 whether Keycloak is scaled to zero or your selector is simply wrong — the two are byte-identical,
|
|
345
|
+
and you cannot tell a dead component from a bad query. The workload object always exists, so its
|
|
346
|
+
READY count answers the question directly: `0/0` is scaled down, `0/1` is failing to start, `1/1` is
|
|
347
|
+
up.
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
kubectl get statefulset -n openhands keycloak
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Then, once READY tells you what you are looking at:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
kubectl get pods -n openhands -l app.kubernetes.io/name=keycloak
|
|
357
|
+
kubectl logs -n openhands keycloak-0 --tail=200
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Check Keycloak Database Connectivity
|
|
361
|
+
|
|
362
|
+
Bitnami images wire the database through `KEYCLOAK_DATABASE_*` rather than upstream's `KC_DB_*`, so
|
|
363
|
+
grepping only for `KC_DB` comes back empty on these installs and looks like nothing is configured.
|
|
364
|
+
Match both:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Ask the pod what database it is configured against, rather than assuming
|
|
368
|
+
kubectl exec -n openhands keycloak-0 -- printenv \
|
|
369
|
+
| grep -iE 'KC_DB|KEYCLOAK_DATABASE|DB_ADDR|DB_URL|JDBC|DATABASE' | sort
|
|
370
|
+
|
|
371
|
+
# The database error itself is usually in the log, and needs no in-pod tooling
|
|
372
|
+
kubectl logs -n openhands keycloak-0 --tail=200 \
|
|
373
|
+
| grep -iE 'connection refused|unknown host|timeout|FATAL|could not connect'
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Check Keycloak Realm Configuration
|
|
377
|
+
|
|
378
|
+
> **Do not decode the admin credential into your shell.** On a live install that writes a working
|
|
379
|
+
> password into your terminal history and into any agent transcript.
|
|
380
|
+
|
|
381
|
+
The admin credential is wired differently across versions, so check rather than assume. Some installs
|
|
382
|
+
carry a `keycloak-admin` secret mounted via `KC_BOOTSTRAP_ADMIN_PASSWORD_FILE`; others expose only a
|
|
383
|
+
`keycloak-realm` secret and no `-admin` secret at all. Read the wiring, never the value:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
# Which keycloak secrets exist here? Names only.
|
|
387
|
+
kubectl get secret -n openhands -o name | grep -i keycloak
|
|
388
|
+
|
|
389
|
+
# Which bootstrap mechanism is in use? Values masked.
|
|
390
|
+
kubectl exec -n openhands keycloak-0 -- printenv | grep -i 'KC_BOOTSTRAP_ADMIN' | sed 's/=.*/=<set>/'
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
To confirm Keycloak is serving its realm, use the public endpoint, which needs no credential:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# Select by the host it serves rather than by resource name — the ingress is
|
|
397
|
+
# named `keycloak` on current builds, but the auth host is the stable property.
|
|
398
|
+
KEYCLOAK_HOST=$(kubectl get ingress -n openhands \
|
|
399
|
+
-o jsonpath='{range .items[*].spec.rules[*]}{.host}{"\n"}{end}' | grep -i '^auth\.' | head -1)
|
|
400
|
+
|
|
401
|
+
# The scheme matters — without it curl defaults to http:// and a TLS-fronted
|
|
402
|
+
# Keycloak just redirects.
|
|
403
|
+
curl -fsS -o /dev/null -w '%{http_code}\n' \
|
|
404
|
+
"https://$KEYCLOAK_HOST/realms/master/.well-known/openid-configuration"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Keycloak Health Check
|
|
408
|
+
|
|
409
|
+
The realm check above is the one that answers the question that actually matters — whether users can
|
|
410
|
+
reach Keycloak — because it goes through the ingress and so exercises DNS, TLS, and routing. Prefer
|
|
411
|
+
it, and read a failure there as a fault in the path rather than in the server.
|
|
412
|
+
|
|
413
|
+
**Do not rely on `/health` here.** These installs ship Bitnami's Keycloak image, which does not
|
|
414
|
+
expose the management interface that upstream Keycloak 25+ serves on port 9000. On a verified
|
|
415
|
+
v0.58.0 install running Bitnami Keycloak 26.3.0, only `8080` and `7800` are exposed, `:9000` refuses
|
|
416
|
+
the connection outright, `:8080/health*` returns 404, and the realm endpoint is the only thing that
|
|
417
|
+
answers 200. A health check against this pod tells you nothing about Keycloak's state.
|
|
418
|
+
|
|
419
|
+
Confirm what the pod actually exposes before assuming any health port exists:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
kubectl get pod -n openhands keycloak-0 \
|
|
423
|
+
-o jsonpath='{range .spec.containers[*].ports[*]}{.name}:{.containerPort}{"\n"}{end}'
|
|
424
|
+
kubectl get pod -n openhands keycloak-0 -o jsonpath='{.spec.containers[*].image}{"\n"}'
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
If the image is `bitnami/keycloak` or `bitnamilegacy/keycloak`, use the realm check above as the
|
|
428
|
+
liveness signal and stop there. Only on an upstream `quay.io/keycloak/keycloak` image at 25 or newer
|
|
429
|
+
is `:9000/health/ready` meaningful, and even then a 404 means health is disabled on the server —
|
|
430
|
+
`health-enabled` is off, or `--legacy-observability-interface=true` has kept the endpoints on the
|
|
431
|
+
main port — rather than Keycloak being unhealthy.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Replicated Admin Console
|
|
436
|
+
|
|
437
|
+
The Admin Console (KOTS) runs in the `kotsadm` namespace. The Embedded Cluster operator runs in
|
|
438
|
+
`embedded-cluster`. Start a cluster shell first — it exports the kubeconfig and puts `kubectl` on
|
|
439
|
+
your PATH:
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
sudo ./openhands shell
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Check Admin Console Pods
|
|
446
|
+
|
|
447
|
+
The admin console chart uses bare `app=` labels — `app=kotsadm`,
|
|
448
|
+
`app=kotsadm-rqlite`, `app=kurl-proxy-kotsadm` — unlike the rest of the platform. Its
|
|
449
|
+
`app.kubernetes.io/name` is `admin-console`, so "correcting" these to
|
|
450
|
+
`app.kubernetes.io/name=kotsadm` breaks them.
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
kubectl get pods -n kotsadm
|
|
454
|
+
|
|
455
|
+
# Admin console logs
|
|
456
|
+
kubectl logs -n kotsadm -l app=kotsadm --tail=100
|
|
457
|
+
|
|
458
|
+
# rqlite is the admin console's datastore; kotsadm will not start without it
|
|
459
|
+
kubectl get pods -n kotsadm -l app=kotsadm-rqlite
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Check Embedded Cluster Operator
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
kubectl get pods -n embedded-cluster
|
|
466
|
+
kubectl logs -n embedded-cluster -l app.kubernetes.io/name=embedded-cluster-operator --tail=100
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Check Services and Ingress
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
# The console is served by kurl-proxy-kotsadm, a NodePort on 8800:30000. The
|
|
473
|
+
# name is a kURL leftover but the service is still there on Embedded Cluster.
|
|
474
|
+
kubectl get svc -n kotsadm -o wide
|
|
475
|
+
|
|
476
|
+
# From the VM itself, bypassing any external networking
|
|
477
|
+
curl -sk -o /dev/null -w "%{http_code}\n" https://localhost:30000
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Common Admin Console Issues
|
|
481
|
+
|
|
482
|
+
| Symptom | Check | Fix |
|
|
483
|
+
|---------|-------|-----|
|
|
484
|
+
| "Connection refused" on admin console | `kubectl get pods -n kotsadm` | Restart the kotsadm pod |
|
|
485
|
+
| Reachable on localhost:30000 but not externally | Host firewall / security group | Open port 30000 to the client |
|
|
486
|
+
| Admin console shows blank page | kotsadm logs | Check for migration errors |
|
|
487
|
+
| kotsadm stuck in `Init` | rqlite pod status | Fix rqlite first — kotsadm waits on it |
|
|
488
|
+
|
|
489
|
+
### Cluster Status from the Host
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
# Version and install state
|
|
493
|
+
sudo ./openhands version
|
|
494
|
+
|
|
495
|
+
# The embedded cluster runs on k0s; check the host service if the API is unreachable
|
|
496
|
+
sudo systemctl status k0scontroller
|
|
497
|
+
sudo journalctl -u k0scontroller --since "1 hour ago" --no-pager | tail -50
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
> The `replicated` CLI is a **vendor-side** tool and is not present on a customer VM. Use the
|
|
501
|
+
> application binary (`sudo ./openhands …`) and `kubectl` from within `sudo ./openhands shell`.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Upgrade Issues
|
|
506
|
+
|
|
507
|
+
> **Upgrades and rollbacks belong in the Admin Console.** KOTS owns the deployment state, so the
|
|
508
|
+
> Admin Console is the only place that can change it safely. Everything in this section is read-only
|
|
509
|
+
> diagnosis for an upgrade that has already gone wrong: use it to work out *what* failed, then act
|
|
510
|
+
> in the Admin Console. Confirm what you find there, and escalate if the two disagree.
|
|
511
|
+
|
|
512
|
+
### Check Failed Upgrade Jobs
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
kubectl get jobs -n openhands | grep -E "upgrade|migrate"
|
|
516
|
+
|
|
517
|
+
# Check failed job logs
|
|
518
|
+
UPGRADE_JOB=$(kubectl get jobs -n openhands -o jsonpath='{.items[?(@.status.failed)].metadata.name}' | awk '{print $1}')
|
|
519
|
+
kubectl logs -n openhands job/$UPGRADE_JOB
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Check Pre-flight Status
|
|
523
|
+
|
|
524
|
+
Preflight results for the current version are shown in the Admin Console under the version history
|
|
525
|
+
entry. From the CLI:
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
# Preflight state for the deployed version. Preflight runs once at deploy, so on
|
|
529
|
+
# anything but a just-installed cluster those lines have scrolled out of a short
|
|
530
|
+
# tail — search the whole log, not the last N lines.
|
|
531
|
+
kubectl get pods -n kotsadm -l app=kotsadm -o name \
|
|
532
|
+
| head -1 | xargs -I{} kubectl logs -n kotsadm {} | grep -i preflight
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Silence here means the log no longer reaches back to the deploy, not that preflight passed. The
|
|
536
|
+
Admin Console version history is the reliable source; treat the log as a shortcut that expires.
|
|
537
|
+
|
|
538
|
+
The last preflight run also leaves a full cluster snapshot inside any support bundle at
|
|
539
|
+
`kots/admin_console/kotsadm/*/kotsadm/tmp/last-preflight-result/` — useful as a second point in time
|
|
540
|
+
to diff against. See `support-bundle-analysis.md`.
|
|
541
|
+
|
|
542
|
+
### Rollback
|
|
543
|
+
|
|
544
|
+
Rollback is only possible if the application enables it (`allowRollback` in the KOTS Application
|
|
545
|
+
spec). It is driven from the **Admin Console** version history:
|
|
546
|
+
|
|
547
|
+
1. Open the Admin Console (`https://<vm-host>:30000`) → **Version history**
|
|
548
|
+
2. Find the previously deployed version
|
|
549
|
+
3. Click **Deploy** on that version
|
|
550
|
+
|
|
551
|
+
> If the install is broken, collect a support bundle and escalate. The Admin Console owns the
|
|
552
|
+
> deployment state, so restoring a working version is a job for the Console or for the OpenHands
|
|
553
|
+
> team.
|
|
554
|
+
|
|
555
|
+
### Common Upgrade Failures
|
|
556
|
+
|
|
557
|
+
| Error | Cause | Fix |
|
|
558
|
+
|-------|-------|-----|
|
|
559
|
+
| Migration job failed | Database schema change | Check job logs, retry |
|
|
560
|
+
| Pods crash on new version | Config incompatibility | Review changelog, adjust config |
|
|
561
|
+
| Pre-flight failed | Resource insufficient | Add resources, retry |
|
|
562
|
+
| Helm error | Values incompatible | Review helm values diff |
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Resource Exhaustion
|
|
567
|
+
|
|
568
|
+
### Check Node Resources
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
# Node CPU/memory
|
|
572
|
+
kubectl top nodes
|
|
573
|
+
|
|
574
|
+
# Node disk usage. This is a single-node install, so `df -h /` in an SSH session
|
|
575
|
+
# on the VM answers the same question without a debug pod. `kubectl debug` also
|
|
576
|
+
# needs a TTY, which an agent shell may not have; the node filesystem appears
|
|
577
|
+
# under /host inside it.
|
|
578
|
+
df -h /
|
|
579
|
+
|
|
580
|
+
# Check if OOMKilled
|
|
581
|
+
kubectl get events -n openhands | grep -i "oom\|killed"
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Check Pod Resource Usage
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
# Per-pod resource usage
|
|
588
|
+
kubectl top pods -n openhands
|
|
589
|
+
|
|
590
|
+
# Check pod resource limits
|
|
591
|
+
kubectl get pods -n openhands -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].resources.limits.memory}{"\n"}'
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Check File Descriptor Usage
|
|
595
|
+
|
|
596
|
+
Ask the **server process** (PID 1 in the container), not your own shell and not the `ls` you just
|
|
597
|
+
spawned. `ulimit -n` reports the limit of whatever shell you are sitting in, `/proc/sys/fs/file-max`
|
|
598
|
+
is a host-wide ceiling, and `ls /proc/self/fd` counts the handles of `ls` itself — all three return
|
|
599
|
+
a confident, plausible number that says nothing about the app:
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
# The app's actual fd limit and current usage
|
|
603
|
+
kubectl exec -n openhands deploy/openhands -- sh -c \
|
|
604
|
+
'grep -i "open files" /proc/1/limits; ls /proc/1/fd | wc -l'
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Compare the count against the limit. If they are not close, fd exhaustion is not your fault — look
|
|
608
|
+
elsewhere before raising limits.
|
|
609
|
+
|
|
610
|
+
### Check Disk Space
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
# Node disk pressure
|
|
614
|
+
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.conditions[?(@.type=="DiskPressure")].status}{"\n"}'
|
|
615
|
+
|
|
616
|
+
# Find large directories. The container runs as non-root, so du reports
|
|
617
|
+
# "Permission denied" on paths it cannot read and exits 1 even when it
|
|
618
|
+
# succeeded elsewhere — read the sizes, not the exit code.
|
|
619
|
+
kubectl exec -n openhands deploy/openhands -- du -sh /var/* 2>/dev/null
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Common Resource Exhaustion Fixes
|
|
623
|
+
|
|
624
|
+
| Resource | Check | Fix |
|
|
625
|
+
|----------|-------|-----|
|
|
626
|
+
| Memory OOM | `kubectl top pods` | Increase pod memory limits |
|
|
627
|
+
| Disk full | `du -sh` | Clean up logs, increase PV size |
|
|
628
|
+
| FD exhaustion | `/proc/1/limits` vs `ls /proc/1/fd \| wc -l` in the pod | Increase the container's limit |
|
|
629
|
+
| CPU throttling | `kubectl top pods` | Adjust CPU limits |
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## Log Pattern Quick Reference
|
|
634
|
+
|
|
635
|
+
### Search for Common Error Patterns
|
|
636
|
+
|
|
637
|
+
App logs are structured JSON with a `severity` key, and the app is Python/uvicorn — whose top tier is
|
|
638
|
+
`CRITICAL`, not `FATAL`, so grepping for `FATAL` never matches. Filter on the field:
|
|
639
|
+
|
|
640
|
+
```bash
|
|
641
|
+
kubectl logs -n openhands deploy/openhands --tail=2000 \
|
|
642
|
+
| grep -E '"severity": ?"(ERROR|CRITICAL)"'
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
If that returns nothing on an install you know has errors, print one line first and match the field
|
|
646
|
+
as it is really formatted — quoting and spacing vary by log library:
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
kubectl logs -n openhands deploy/openhands --tail=1
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
```bash
|
|
653
|
+
# In pod logs, search for these patterns:
|
|
654
|
+
grep -E "ERROR|CRITICAL|Exception|Traceback" /path/to/logs
|
|
655
|
+
|
|
656
|
+
# Search for timeout patterns
|
|
657
|
+
grep -E "timeout|timed out|deadline" /path/to/logs
|
|
658
|
+
|
|
659
|
+
# Search for connection errors
|
|
660
|
+
grep -E "connection refused|connection reset|dial tcp" /path/to/logs
|
|
661
|
+
|
|
662
|
+
# Search for auth errors
|
|
663
|
+
grep -E "unauthorized|forbidden|authentication" /path/to/logs
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Kubernetes Events
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
# Get recent events in namespace
|
|
670
|
+
kubectl get events -n openhands --sort-by='.lastTimestamp' | tail -50
|
|
671
|
+
|
|
672
|
+
# Filter events by type
|
|
673
|
+
kubectl get events -n openhands --field-selector type=Warning
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Useful One-Liners
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
# Get all pod statuses at once
|
|
682
|
+
kubectl get pods -n openhands -o wide
|
|
683
|
+
|
|
684
|
+
# Tail logs from all pods with a label
|
|
685
|
+
kubectl logs -n openhands -l app.kubernetes.io/name=runtime-api --tail=50 -f
|
|
686
|
+
|
|
687
|
+
# Get pod restart count
|
|
688
|
+
kubectl get pods -n openhands -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].restartCount}{"\n"}'
|
|
689
|
+
|
|
690
|
+
# Check pod age and status
|
|
691
|
+
kubectl get pods -n openhands -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.phase}{"\t"}{.metadata.creationTimestamp}{"\n"}'
|
|
692
|
+
|
|
693
|
+
# Extract error messages from all pods
|
|
694
|
+
for pod in $(kubectl get pods -n openhands -o name); do
|
|
695
|
+
echo "=== $pod ===";
|
|
696
|
+
kubectl logs -n openhands $pod --tail=20 2>&1 | grep -iE "error|fatal" | head -5;
|
|
697
|
+
done
|
|
698
|
+
```
|