Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/scripts/e2e-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ OP_CONNECT_TOKEN="$OP_TOKEN" \
OP_VAULT_NAME="$OP_VAULT" \
OP_SECRET_ITEM_NAME="$OP_SECRET_ITEM" \
go test -tags e2e -v -count=1 -timeout 30m \
-run 'TestLifecycle|TestIronClawLifecycle|TestOpenClawLifecycle|TestPicoClawLifecycle' \
-run 'TestLifecycle|TestIronClawLifecycle|TestOpenClawLifecycle|TestOpenClawExtraSoftwareInstallsClaude|TestPicoClawLifecycle' \
./e2e/

echo "✅ Lifecycle E2E tests passed!"
24 changes: 24 additions & 0 deletions control-plane/charts/ironclaw/templates/startup-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ data:
#!/bin/sh
set -e

warn() {
echo "WARNING: $*"
}

# Load secrets from volume files into environment
SECRETS_DIR="/etc/claw/secrets"
if [ -d "$SECRETS_DIR" ]; then
Expand All @@ -19,6 +23,26 @@ data:
done
fi

{{- if .Values.extraSoftware.toolVersions }}
WORKSPACE_DIR="${HOME:-/home/ironclaw}/.ironclaw/workspace"
TOOL_VERSIONS_FILE="$WORKSPACE_DIR/.tool-versions"
mkdir -p "$WORKSPACE_DIR"

echo "Applying extra software tools from $TOOL_VERSIONS_FILE"
cat >"$TOOL_VERSIONS_FILE" <<'CLAWMACHINE_TOOL_VERSIONS'
{{ .Values.extraSoftware.toolVersions | nindent 4 }}
CLAWMACHINE_TOOL_VERSIONS
sed -i 's/^ //' "$TOOL_VERSIONS_FILE"

export MISE_CACHE_DIR="$WORKSPACE_DIR/.cache/mise"
mkdir -p "$MISE_CACHE_DIR"
if ! command -v mise >/dev/null 2>&1; then
warn "mise is not available in the container; skipping extra software installation"
elif ! (cd "$WORKSPACE_DIR" && mise install); then
warn "mise install failed; continuing startup"
fi
{{- end }}

{{- if .Values.postgresql.enabled }}
# Wait for PostgreSQL to accept TCP connections.
# Keep this bounded so probes do not kill the pod before app start.
Expand Down
3 changes: 3 additions & 0 deletions control-plane/charts/ironclaw/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ workspace:

env: {}

extraSoftware:
toolVersions: ""

backup:
enabled: false
restoreOnStartup: false
Expand Down
3 changes: 3 additions & 0 deletions control-plane/charts/openclaw/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ spec:
metadata:
labels:
{{- include "openclaw.labels" . | nindent 8 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/startup: {{ include (print $.Template.BasePath "/startup-configmap.yaml") . | sha256sum }}
spec:
{{- if and .Values.backup.enabled .Values.backup.restoreOnStartup }}
initContainers:
Expand Down
22 changes: 21 additions & 1 deletion control-plane/charts/openclaw/templates/startup-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,33 @@ data:
echo "Applying managed configFile.content to $CONFIG_FILE"
cat >"$CONFIG_FILE" <<'CLAWMACHINE_OPENCLAW_CONFIG'
{{ .Values.configFile.content | nindent 4 }}
CLAWMACHINE_OPENCLAW_CONFIG
CLAWMACHINE_OPENCLAW_CONFIG
{{- end }}

warn() {
echo "WARNING: $*"
}

{{- if .Values.extraSoftware.toolVersions }}
TOOL_VERSIONS_DIR="{{ .Values.openclawConfigDir }}/workspace"
TOOL_VERSIONS_FILE="$TOOL_VERSIONS_DIR/.tool-versions"
mkdir -p "$TOOL_VERSIONS_DIR"

echo "Applying extra software tools from $TOOL_VERSIONS_FILE"
cat >"$TOOL_VERSIONS_FILE" <<'CLAWMACHINE_TOOL_VERSIONS'
{{ .Values.extraSoftware.toolVersions | nindent 4 }}
CLAWMACHINE_TOOL_VERSIONS
sed -i 's/^ //' "$TOOL_VERSIONS_FILE"

export MISE_CACHE_DIR="$TOOL_VERSIONS_DIR/.cache/mise"
mkdir -p "$MISE_CACHE_DIR"
if ! command -v mise >/dev/null 2>&1; then
warn "mise is not available in the container; skipping extra software installation"
elif ! (cd "$TOOL_VERSIONS_DIR" && mise install); then
warn "mise install failed; continuing startup"
fi
{{- end }}

set_cfg_json() {
local path="$1"
local value="$2"
Expand Down
3 changes: 3 additions & 0 deletions control-plane/charts/openclaw/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ networkPolicy:

env: {}

extraSoftware:
toolVersions: ""

backup:
enabled: false
restoreOnStartup: false
Expand Down
17 changes: 17 additions & 0 deletions control-plane/charts/picoclaw/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ spec:
- name: picoclaw
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["/bin/sh", "/scripts/start-picoclaw.sh"]
args: ["gateway"]
ports:
- name: http
containerPort: {{ .Values.service.port }}
Expand Down Expand Up @@ -125,12 +127,27 @@ spec:
port: http
initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.periodSeconds }}
{{- if .Values.healthCheck.startupProbe.enabled }}
startupProbe:
httpGet:
path: {{ .Values.healthCheck.path }}
port: http
periodSeconds: {{ .Values.healthCheck.startupProbe.periodSeconds }}
timeoutSeconds: {{ .Values.healthCheck.startupProbe.timeoutSeconds }}
failureThreshold: {{ .Values.healthCheck.startupProbe.failureThreshold }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: startup-script
mountPath: /scripts
- name: workspace
mountPath: {{ .Values.configFile.mountPath }}
volumes:
- name: startup-script
configMap:
name: {{ include "picoclaw.fullname" . }}-startup
defaultMode: 0755
- name: workspace
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
Expand Down
28 changes: 28 additions & 0 deletions control-plane/charts/picoclaw/templates/startup-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ data:
#!/bin/sh
set -e

warn() {
echo "WARNING: $*"
}

# Load secrets from volume files into environment
SECRETS_DIR="/etc/claw/secrets"
if [ -d "$SECRETS_DIR" ]; then
Expand All @@ -19,5 +23,29 @@ data:
done
fi

{{- if .Values.extraSoftware.toolVersions }}
WORKSPACE_DIR="${HOME:-/home/picoclaw}/.picoclaw/workspace"
TOOL_VERSIONS_FILE="$WORKSPACE_DIR/.tool-versions"
mkdir -p "$WORKSPACE_DIR"

echo "Applying extra software tools from $TOOL_VERSIONS_FILE"
cat >"$TOOL_VERSIONS_FILE" <<'CLAWMACHINE_TOOL_VERSIONS'
{{ .Values.extraSoftware.toolVersions | nindent 4 }}
CLAWMACHINE_TOOL_VERSIONS
sed -i 's/^ //' "$TOOL_VERSIONS_FILE"

export MISE_CACHE_DIR="$WORKSPACE_DIR/.cache/mise"
mkdir -p "$MISE_CACHE_DIR"
if ! command -v mise >/dev/null 2>&1; then
warn "mise is not available in the container; skipping extra software installation"
elif ! (cd "$WORKSPACE_DIR" && mise install); then
warn "mise install failed; continuing startup"
fi
{{- end }}

# Bind gateway on all interfaces for Kubernetes probes/services unless overridden.
export PICOCLAW_GATEWAY_HOST="${PICOCLAW_GATEWAY_HOST:-0.0.0.0}"
export PICOCLAW_GATEWAY_PORT="${PICOCLAW_GATEWAY_PORT:-{{ .Values.service.port }}}"

# Exec the picoclaw binary
exec /usr/local/bin/picoclaw "$@"
8 changes: 8 additions & 0 deletions control-plane/charts/picoclaw/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ healthCheck:
port: 18790
initialDelaySeconds: 10
periodSeconds: 15
startupProbe:
enabled: true
failureThreshold: 60
periodSeconds: 5
timeoutSeconds: 1

networkPolicy:
ingress: false
Expand All @@ -47,6 +52,9 @@ networkPolicy:

env: {}

extraSoftware:
toolVersions: ""

# Config file mount — generates a Secret with bot config and mounts it as a file.
configFile:
enabled: false
Expand Down
102 changes: 102 additions & 0 deletions control-plane/e2e/openclaw_extra_software_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//go:build e2e

package e2e

import (
"context"
"fmt"
"io"
"strings"
"testing"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func waitForPodReadyBySelector(t *testing.T, namespace, labelSelector string, timeout time.Duration) string {
t.Helper()
clientset, _ := getK8sClients(t)

deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
pods, err := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
LabelSelector: labelSelector,
})
if err == nil && len(pods.Items) > 0 {
for _, pod := range pods.Items {
for _, cond := range pod.Status.Conditions {
if cond.Type == "Ready" && cond.Status == "True" {
return pod.Name
}
}
}
}
time.Sleep(3 * time.Second)
}

t.Fatalf("timed out waiting for ready pod with selector %q", labelSelector)
return ""
}

func TestOpenClawExtraSoftwareInstallsClaude(t *testing.T) {
skipIfNoServer(t)

clientset, config := getK8sClients(t)
const namespace = "claw-machine"
const testBotName = "e2e-openclaw-mise"

doDelete(t, baseURL+"/bots/"+testBotName)
time.Sleep(2 * time.Second)

t.Cleanup(func() {
doDelete(t, baseURL+"/bots/"+testBotName)
})

body := map[string]any{
"releaseName": testBotName,
"botType": "openclaw",
"values": map[string]any{
"persistence": map[string]any{
"enabled": true,
"size": "1Gi",
},
"networkPolicy": map[string]any{
"ingress": false,
"egress": true,
},
"extraSoftware": map[string]any{
"toolVersions": "claude latest",
},
},
}

resp := doPost(t, baseURL+"/bots", body)
if resp.StatusCode != 201 && resp.StatusCode != 200 && resp.StatusCode != 204 && resp.StatusCode != 202 {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("create bot failed: %d — %s", resp.StatusCode, string(b))
}
resp.Body.Close()

selector := fmt.Sprintf("app.kubernetes.io/instance=%s", testBotName)
waitForPodRunning(t, clientset, namespace, selector, 4*time.Minute)
podName := waitForPodReadyBySelector(t, namespace, selector, 4*time.Minute)
t.Logf("openclaw pod ready: %s", podName)

stdout, stderr, err := execInPod(t, clientset, config, namespace, podName, []string{
"sh", "-lc", "cat /root/.openclaw/workspace/.tool-versions",
})
if err != nil {
t.Fatalf("read .tool-versions failed: %v\nstderr=%s", err, stderr)
}
if !strings.Contains(stdout, "claude latest") {
t.Fatalf(".tool-versions missing expected entry; got:\n%s", stdout)
}

stdout, stderr, err = execInPod(t, clientset, config, namespace, podName, []string{
"sh", "-lc", "cd /root/.openclaw/workspace && CLAUDE_BIN=\"$(mise which claude 2>/dev/null)\" && [ -n \"$CLAUDE_BIN\" ] && [ -x \"$CLAUDE_BIN\" ] && echo \"$CLAUDE_BIN\"",
})
if err != nil {
t.Fatalf("claude binary lookup failed: %v\nstderr=%s\nstdout=%s", err, stderr, stdout)
}
t.Logf("claude binary found: %s", strings.TrimSpace(stdout))
}
4 changes: 2 additions & 2 deletions control-plane/e2e/openclaw_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func TestOpenClawLifecycle(t *testing.T) {
"anthropicApiKey": "1p:e2e-anthropic-key",
"discordBotToken": "1p:e2e-discord-token",
"discordEnabled": "true",
"defaultModel": "anthropic/claude-haiku-4-5-20251001", // Changed model
"defaultModel": "anthropic/claude-haiku-4-5", // Changed model
},
}
resp := doPut(t, baseURL+"/bots/"+testBotName+"/config", body)
Expand Down Expand Up @@ -409,7 +409,7 @@ func TestOpenClawLifecycle(t *testing.T) {
})

t.Run("Step10_VerifyUpdatedRuntimeConfig", func(t *testing.T) {
assertRuntimeConfig(t, "anthropic/claude-haiku-4-5-20251001")
assertRuntimeConfig(t, "anthropic/claude-haiku-4-5")
})

// --- Step 11: Delete bot ---
Expand Down
Loading