mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
166f6d8aae
Reuse the complete-download and preflight private-token entry boundary for both monitoring modes. Keep issued credentials separate and no-store, use the installer-owned token path in the diagnostic service reference, and reject structural unit injection before minting. Add executable root/sudo, history, transport, failure cleanup and systemd grammar regressions alongside the subsystem contracts. Change-source: pulse-maintainer
178 lines
6.6 KiB
Go
178 lines
6.6 KiB
Go
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)
|
|
}
|