diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 6a30af193..e8f0f5231 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -4308,6 +4308,66 @@ resolve_setup_auth_token() { fi } +extract_json_string_field() { + local json_input="$1" + local field_name="$2" + + printf '%%s\n' "$json_input" | sed -n 's/.*"'$field_name'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 +} + +extract_pve_token_value() { + local token_output="$1" + local token_value="" + + token_value=$(extract_json_string_field "$token_output" "value") + if [ -n "$token_value" ]; then + printf '%%s\n' "$token_value" + return 0 + fi + + printf '%%s\n' "$token_output" | awk -F'[|│]' ' + function trim(value) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + return value + } + + { + key = trim($2) + value = trim($3) + if (key == "value" && value != "") { + print value + exit + } + } + ' +} + +create_pve_token() { + if TOKEN_OUTPUT=$(pveum user token add pulse-monitor@pam "$TOKEN_NAME" --privsep 0 --output-format json 2>&1); then + TOKEN_CREATE_RC=0 + else + TOKEN_CREATE_RC=$? + fi + + if [ "$TOKEN_CREATE_RC" -eq 0 ]; then + TOKEN_VALUE=$(extract_pve_token_value "$TOKEN_OUTPUT") + return 0 + fi + + if echo "$TOKEN_OUTPUT" | grep -Eqi 'unknown option|unknown command|no such option|unable to parse option|output-format'; then + if TOKEN_OUTPUT=$(pveum user token add pulse-monitor@pam "$TOKEN_NAME" --privsep 0 2>&1); then + TOKEN_CREATE_RC=0 + else + TOKEN_CREATE_RC=$? + fi + if [ "$TOKEN_CREATE_RC" -eq 0 ]; then + TOKEN_VALUE=$(extract_pve_token_value "$TOKEN_OUTPUT") + fi + fi + + return "$TOKEN_CREATE_RC" +} + attempt_auto_registration() { resolve_setup_auth_token @@ -4407,8 +4467,7 @@ fi if [ "$TOKEN_ROTATION_SKIPPED" != true ]; then # Create token and capture value (shown once by Proxmox) - TOKEN_OUTPUT=$(pveum user token add pulse-monitor@pam "$TOKEN_NAME" --privsep 0 2>&1) - TOKEN_CREATE_RC=$? + create_pve_token if [ "$TOKEN_CREATE_RC" -ne 0 ]; then echo "❌ Failed to create token '$TOKEN_NAME'" echo "$TOKEN_OUTPUT" @@ -4416,8 +4475,6 @@ if [ "$TOKEN_ROTATION_SKIPPED" != true ]; then echo "Manual registration may be required." echo "" else - TOKEN_VALUE=$(echo "$TOKEN_OUTPUT" | grep "│ value" | awk -F'│' '{print $3}' | tr -d ' ' | tail -1) - if [ -z "$TOKEN_VALUE" ]; then echo "" echo "================================================================" diff --git a/internal/api/config_handlers_setup_script_integration_test.go b/internal/api/config_handlers_setup_script_integration_test.go index 4d6901110..296023841 100644 --- a/internal/api/config_handlers_setup_script_integration_test.go +++ b/internal/api/config_handlers_setup_script_integration_test.go @@ -67,6 +67,33 @@ echo "STATE AUTO_REG_SUCCESS=${AUTO_REG_SUCCESS} TOKEN_ROTATION_SKIPPED=${TOKEN_ assertContains(t, trace, "pveum user token add pulse-monitor@pam pulse-sentinel-url --privsep 0") assertContains(t, trace, "curl -s -X POST") }) + + t.Run("json_output_auto_registers_without_legacy_fallback", func(t *testing.T) { + output, trace := runSetupHarness(t, harness, mocks, map[string]string{ + "MOCK_PVE_TOKEN_FORMAT": "json", + }) + + assertContains(t, output, "API token generated successfully") + assertContains(t, output, "Node registered successfully") + assertContains(t, output, "STATE AUTO_REG_SUCCESS=true TOKEN_ROTATION_SKIPPED=false TOKEN_VALUE=mocked-pve-secret") + + assertContains(t, trace, "pveum user token add pulse-monitor@pam pulse-sentinel-url --privsep 0 --output-format json") + assertNotContains(t, trace, "pveum user token add pulse-monitor@pam pulse-sentinel-url --privsep 0\n") + }) + + t.Run("legacy_output_falls_back_when_json_format_is_unsupported", func(t *testing.T) { + output, trace := runSetupHarness(t, harness, mocks, map[string]string{ + "MOCK_PVE_JSON_UNSUPPORTED": "1", + }) + + assertContains(t, output, "API token generated successfully") + assertContains(t, output, "Node registered successfully") + assertContains(t, output, "STATE AUTO_REG_SUCCESS=true TOKEN_ROTATION_SKIPPED=false TOKEN_VALUE=mocked-pve-secret") + + assertContains(t, trace, "pveum user token add pulse-monitor@pam pulse-sentinel-url --privsep 0 --output-format json") + assertContains(t, trace, "pveum user token add pulse-monitor@pam pulse-sentinel-url --privsep 0") + assertContains(t, trace, "curl -s -X POST") + }) } func TestSetupScriptTokenLifecycleIntegration_PBS(t *testing.T) { @@ -228,6 +255,18 @@ case "$*" in ;; "user token remove pulse-monitor@pam pulse-sentinel-url") ;; + "user token add pulse-monitor@pam pulse-sentinel-url --privsep 0 --output-format json") + if [ "${MOCK_PVE_JSON_UNSUPPORTED:-0}" = "1" ]; then + echo "unknown option: output-format" >&2 + exit 1 + fi + if [ "${MOCK_PVE_TOKEN_FORMAT:-table}" = "json" ]; then + echo '{"tokenid":"pulse-monitor@pam!pulse-sentinel-url","value":"mocked-pve-secret"}' + exit 0 + fi + # Some versions support the flag but still keep older text rendering in wrappers. + printf '\342\224\202 value \342\224\202 mocked-pve-secret \342\224\202\n' + ;; "user token add pulse-monitor@pam pulse-sentinel-url --privsep 0") # Emit the box-drawing format parsed by the setup script (│ value │ secret │) printf '\342\224\202 value \342\224\202 mocked-pve-secret \342\224\202\n'