package api import ( "fmt" "net/http" "strings" "github.com/rcourtman/pulse-go-rewrite/internal/api/agenttokens" "github.com/rcourtman/pulse-go-rewrite/internal/api/configapi" "github.com/rcourtman/pulse-go-rewrite/internal/config" ) const ( proxmoxInstallTypePVE = "pve" proxmoxInstallTypePBS = "pbs" // agentInstallTypeHost marks install tokens minted for the generic unified // host agent flow (Settings > Infrastructure > Add Pulse Agent), as opposed // to the Proxmox-specific pve/pbs installer. agentInstallTypeHost = "host" ) var ( errAgentInstallTokenGeneration = agenttokens.ErrGeneration errAgentInstallTokenRecord = agenttokens.ErrRecord errAgentInstallTokenPersist = agenttokens.ErrPersist ) func normalizeProxmoxInstallType(raw string) (string, error) { installType := strings.ToLower(strings.TrimSpace(raw)) if installType != proxmoxInstallTypePVE && installType != proxmoxInstallTypePBS { return "", fmt.Errorf("Type must be 'pve' or 'pbs'") } return installType, nil } func proxmoxAgentInstallScopes(enableCommands bool) []string { return agenttokens.ProxmoxScopes(enableCommands) } // hostAgentInstallScopes returns the scopes for a generic unified host agent // install token. The exec scope is included only when the operator asked for // command execution, because the token is minted before the agent enrols and // scopes cannot be upgraded on an existing token. func hostAgentInstallScopes(enableCommands bool) []string { return agenttokens.HostScopes(enableCommands) } type issueAgentInstallTokenOptions = agenttokens.IssueOptions func issueAndPersistAgentInstallToken(cfg *config.Config, persistence *config.ConfigPersistence, opts issueAgentInstallTokenOptions) (string, *config.APITokenRecord, error) { return agenttokens.IssueAndPersist(cfg, persistence, opts) } type agentInstallCommandOptions = configapi.AgentInstallCommandOptions type setupScriptInstallArtifact = configapi.SetupScriptInstallArtifact func normalizeAgentInstallBaseURL(raw string) string { return strings.TrimRight(strings.TrimSpace(raw), "/") } func posixShellQuote(value string) string { escaped := strings.ReplaceAll(value, "'", `'"'"'`) return "'" + escaped + "'" } func installBaseURLRequiresInsecure(raw string) bool { baseURL := strings.ToLower(strings.TrimSpace(raw)) return strings.HasPrefix(baseURL, "http://") } func authConfiguredForAgentLifecycle(cfg *config.Config) bool { if cfg == nil { return false } return (strings.TrimSpace(cfg.AuthUser) != "" && strings.TrimSpace(cfg.AuthPass) != "") || cfg.HasAPITokens() || strings.TrimSpace(cfg.ProxyAuthSecret) != "" || hasEnabledSSOProvidersForAuth(cfg) } func withPrivilegeEscalation(command string) string { const installPipe = "| bash -s --" idx := strings.Index(command, installPipe) if idx == -1 { return command } args := command[idx+len(installPipe):] return command[:idx] + `| { if [ "$(id -u)" -eq 0 ]; then bash -s --` + args + `; elif command -v sudo >/dev/null 2>&1; then sudo bash -s --` + args + `; else echo "Root privileges required. Run as root (su -) and retry." >&2; exit 1; fi; }` } func buildProxmoxAgentInstallCommand(opts agentInstallCommandOptions) string { return configapi.BuildProxmoxAgentInstallCommand(opts) } func containerRuntimeAgentScopes(enableHost bool) []string { scopes := []string{config.ScopeDockerReport} if enableHost { scopes = append(scopes, config.ScopeAgentReport, config.ScopeAgentConfigRead, ) } return scopes } func containerRuntimeAgentHostFlag(enableHost bool) string { if enableHost { return "--enable-host" } return "--enable-host=false" } func buildContainerRuntimeAgentInstallCommand(baseURL string, token string, enableHost bool) string { return configapi.BuildContainerRuntimeAgentInstallCommand(baseURL, token, enableHost) } // The diagnostic reference must not compete with the installer's complete // service renderer or expose the issued token. Its default Linux state path // matches scripts/install.sh; custom/least-privilege profiles keep their // installer-generated unit instead of replacing it with this reference. func buildContainerRuntimeAgentServiceSnippet(baseURL string, enableHost bool) (string, error) { baseURL = normalizeAgentInstallBaseURL(baseURL) // Environment= does not expand shell variables, but systemd does expand % // specifiers. Escape those as well as the unit's quoting/control boundary. urlLine, err := quoteSystemdEnvironment("PULSE_URL", strings.ReplaceAll(baseURL, "%", "%%")) if err != nil { return "", err } trustArgs := "" if installBaseURLRequiresInsecure(baseURL) { trustArgs = " --insecure" } return fmt.Sprintf("# Default Linux reference only. Run installCommand to create the private token file and complete service.\n# Keep the installer-generated unit for custom state directories or privilege profiles.\n[Service]\nType=simple\n%s\nExecStart=/usr/local/bin/pulse-agent --url ${PULSE_URL} --token-file /var/lib/pulse-agent/token --enable-docker %s --interval 30s%s\nRestart=always\nRestartSec=5s\nUser=root", urlLine, containerRuntimeAgentHostFlag(enableHost), trustArgs), nil } func buildSetupScriptCommand(scriptURL string, token string) string { return configapi.BuildSetupScriptCommand(scriptURL, token) } func buildSetupScriptTokenHint(token string) string { trimmed := strings.TrimSpace(token) if len(trimmed) <= 6 { return trimmed } return fmt.Sprintf("%s…%s", trimmed[:3], trimmed[len(trimmed)-3:]) } func buildSetupScriptURL(baseURL string, installType string, host string, pulseURL string, backupPerms bool) string { return configapi.BuildSetupScriptURL(baseURL, installType, host, pulseURL, backupPerms) } func buildSetupScriptDownloadURL(baseURL string, installType string, host string, pulseURL string, backupPerms bool, setupToken string) string { return configapi.BuildSetupScriptDownloadURL(baseURL, installType, host, pulseURL, backupPerms, setupToken) } func buildSetupScriptFileName(installType string) string { return fmt.Sprintf("pulse-setup-%s.sh", strings.TrimSpace(installType)) } func buildSetupScriptInstallArtifact(baseURL string, installType string, host string, pulseURL string, backupPerms bool, setupToken string, expiresAt int64) setupScriptInstallArtifact { return configapi.BuildSetupScriptInstallArtifact(baseURL, installType, host, pulseURL, backupPerms, setupToken, expiresAt) } func resolveConfigAgentInstallBaseURL(req *http.Request, cfg *config.Config, hostedMode bool) string { return resolveConfiguredPublicBaseURL(req, cfg, hostedMode) } func writeConfigAgentInstallBaseURLUnavailable(w http.ResponseWriter) { http.Error(w, "A valid external Pulse URL is required", http.StatusServiceUnavailable) }