Improve bootstrap token UX with smart environment detection

- Added DetectLXCCTID() to internal/system/container.go to detect Proxmox container ID
- Extended /api/security/status to expose inContainer and lxcCtid fields
- Updated FirstRunSetup to show most relevant command based on detected environment:
  * LXC with CTID: Shows 'pct exec 171 -- cat /etc/pulse/.bootstrap_token'
  * Docker: Shows 'docker exec <container-name> cat /data/.bootstrap_token'
  * Bare metal: Shows 'cat /etc/pulse/.bootstrap_token'
- Collapsed alternative methods behind 'Show other retrieval methods' button

This addresses user feedback that showing all options was overwhelming.
Now users see the command most likely to work for their setup first,
with alternatives hidden but still accessible.
This commit is contained in:
rcourtman 2025-11-15 10:16:30 +00:00
parent dd9089b26c
commit ae0481c982
5 changed files with 131 additions and 16 deletions

8
copy_and_run.sh Executable file
View file

@ -0,0 +1,8 @@
cd /
if [ -d /tmp/pulse-copy ]; then
rm -rf /tmp/pulse-copy
fi
mkdir -p /tmp/pulse-copy
cp -R /opt/pulse /tmp/pulse-copy/repo
cd /tmp/pulse-copy/repo
GOCACHE=/tmp/go-cache HOME=/tmp/pulse-copy/repo go test ./...

View file

@ -26,6 +26,9 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool
const [isUnlocked, setIsUnlocked] = createSignal(false);
const [bootstrapTokenPath, setBootstrapTokenPath] = createSignal<string>('');
const [isDocker, setIsDocker] = createSignal<boolean>(false);
const [inContainer, setInContainer] = createSignal<boolean>(false);
const [lxcCtid, setLxcCtid] = createSignal<string>('');
const [showAlternatives, setShowAlternatives] = createSignal(false);
const applyTheme = (mode: 'system' | 'light' | 'dark') => {
if (mode === 'light') {
@ -72,6 +75,8 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool
if (data.bootstrapTokenPath) {
setBootstrapTokenPath(data.bootstrapTokenPath);
setIsDocker(data.isDocker || false);
setInContainer(data.inContainer || false);
setLxcCtid(data.lxcCtid || '');
}
}
} catch (error) {
@ -317,26 +322,68 @@ IMPORTANT: Keep these credentials secure!
</div>
}
>
{/* Show most relevant command based on detected environment */}
<div class="bg-white dark:bg-gray-800 rounded p-3 font-mono text-xs text-gray-800 dark:text-gray-200">
<Show when={isDocker()}>
<div class="text-blue-600 dark:text-blue-400 mb-1"># From Docker host:</div>
docker exec &lt;container-name&gt; cat {bootstrapTokenPath()}
<div class="text-gray-500 dark:text-gray-400 mt-2 mb-1"># For Kubernetes:</div>
kubectl exec &lt;pod-name&gt; -- cat {bootstrapTokenPath()}
<div class="text-gray-500 dark:text-gray-400 mt-2 mb-1"># For Proxmox LXC running Docker:</div>
pct exec &lt;ctid&gt; -- docker exec &lt;container-name&gt; cat {bootstrapTokenPath()}
<div class="text-gray-500 dark:text-gray-400 mt-2 mb-1"># Or enter the LXC container first:</div>
pct enter &lt;ctid&gt;
<div class="text-gray-500 dark:text-gray-400 mt-1 mb-1"># Then inside LXC:</div>
{/* LXC with detected CTID */}
<Show when={inContainer() && !isDocker() && lxcCtid()}>
<div class="text-blue-600 dark:text-blue-400 mb-1 font-semibold"># Run from Proxmox host:</div>
pct exec {lxcCtid()} -- cat {bootstrapTokenPath()}
</Show>
<Show when={!isDocker() && bootstrapTokenPath() === '/etc/pulse/.bootstrap_token'}>
<div class="text-blue-600 dark:text-blue-400 mb-1"># For Proxmox LXC, run from Proxmox host:</div>
pct enter &lt;ctid&gt;
<div class="text-gray-500 dark:text-gray-400 mt-2 mb-1"># Or directly retrieve:</div>
{/* LXC without CTID */}
<Show when={inContainer() && !isDocker() && !lxcCtid()}>
<div class="text-blue-600 dark:text-blue-400 mb-1 font-semibold"># Run from Proxmox host:</div>
pct exec &lt;ctid&gt; -- cat {bootstrapTokenPath()}
<div class="text-gray-500 dark:text-gray-400 mt-2 mb-1"># Or from inside the LXC container:</div>
</Show>
cat {bootstrapTokenPath()}
{/* Docker (generic) */}
<Show when={isDocker()}>
<div class="text-blue-600 dark:text-blue-400 mb-1 font-semibold"># From Docker host:</div>
docker exec &lt;container-name&gt; cat {bootstrapTokenPath()}
</Show>
{/* Bare metal / inside container */}
<Show when={!inContainer()}>
<div class="text-blue-600 dark:text-blue-400 mb-1 font-semibold"># On this host:</div>
cat {bootstrapTokenPath()}
</Show>
</div>
{/* Collapsible alternatives */}
<div class="mt-2">
<button
type="button"
onClick={() => setShowAlternatives(!showAlternatives())}
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
{showAlternatives() ? '▼' : '▶'} Show other retrieval methods
</button>
<Show when={showAlternatives()}>
<div class="mt-2 space-y-2 text-gray-600 dark:text-gray-400">
<Show when={isDocker()}>
<div class="bg-gray-50 dark:bg-gray-900 rounded p-3 font-mono text-xs">
<div class="mb-1"># For Kubernetes:</div>
kubectl exec &lt;pod-name&gt; -- cat {bootstrapTokenPath()}
</div>
<div class="bg-gray-50 dark:bg-gray-900 rounded p-3 font-mono text-xs">
<div class="mb-1"># For Proxmox LXC running Docker:</div>
pct exec &lt;ctid&gt; -- docker exec &lt;container-name&gt; cat {bootstrapTokenPath()}
</div>
</Show>
<Show when={inContainer() && !isDocker()}>
<div class="bg-gray-50 dark:bg-gray-900 rounded p-3 font-mono text-xs">
<div class="mb-1"># Enter container then run:</div>
pct enter {lxcCtid() || '<ctid>'}
<br />
cat {bootstrapTokenPath()}
</div>
</Show>
<div class="bg-gray-50 dark:bg-gray-900 rounded p-3 font-mono text-xs">
<div class="mb-1"># Direct file access:</div>
cat {bootstrapTokenPath()}
</div>
</div>
</Show>
</div>
</Show>
</div>

View file

@ -28,6 +28,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/dockeragent"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/system"
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
@ -485,6 +486,10 @@ func (r *Router) setupRoutes() {
if r.bootstrapTokenHash != "" {
status["bootstrapTokenPath"] = r.bootstrapTokenPath
status["isDocker"] = os.Getenv("PULSE_DOCKER") == "true"
status["inContainer"] = system.InContainer()
if ctid := system.DetectLXCCTID(); ctid != "" {
status["lxcCtid"] = ctid
}
}
if r.config.DisableAuthEnvDetected {

View file

@ -56,6 +56,58 @@ func InContainer() bool {
return false
}
// DetectLXCCTID attempts to detect the Proxmox LXC container ID.
// Returns empty string if not in an LXC container or CTID cannot be determined.
func DetectLXCCTID() string {
// Method 1: Parse /proc/1/cgroup for LXC container ID
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
// Look for patterns like: 0::/lxc/123 or 0::/lxc.payload.123
if strings.Contains(line, "/lxc") || strings.Contains(line, "lxc.payload") {
// Extract digits after lxc/ or lxc.payload.
parts := strings.Split(line, "/")
for _, part := range parts {
part = strings.TrimPrefix(part, "lxc.payload.")
part = strings.TrimPrefix(part, "lxc.")
if isNumeric(part) {
return part
}
}
}
// Also check for machine-lxc-NNN pattern
if strings.Contains(line, "machine-lxc") {
fields := strings.Split(line, "-")
for _, field := range fields {
if isNumeric(field) {
return field
}
}
}
}
}
// Method 2: Check hostname (some LXC containers use CTID as hostname)
if hostname, err := os.Hostname(); err == nil && isNumeric(hostname) {
return hostname
}
return ""
}
func isNumeric(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return false
}
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return true
}
func isTruthy(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "t", "yes", "y", "on":

3
work.sh Normal file
View file

@ -0,0 +1,3 @@
#!/bin/bash
cd /opt/pulse
git checkout cleanup-auto-uninstall