hostenv

package
v0.2026219.1057 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package hostenv holds kind-agnostic HOST-ENVIRONMENT probes — pure host inspection (os-release distro identity, glibc version) + best-effort host setup actions (libvirt user-session spawn) with no charly-config / kind / registry coupling. Relocated from sdk/vmshared (#55 vmshared Bucket C) so kernel-floor charly files reach these host facts without importing the sdk/vmshared mechanism kit; vmshared keeps re-export forwarders for its plugin/test callers. stdlib + os/exec only — no heavy external dependency.

Index

Constants

This section is empty.

Variables

View Source
var RuntimeConfigPath = defaultRuntimeConfigPath

RuntimeConfigPath returns the path to the user's runtime config file. A test-injection SEAM var (tests redirect it to a t.TempDir()); it lives here ONLY and every reader/writer references it directly — kit does NOT re-export it (a value-copy alias would break write-through).

View Source
var StartLibvirtUserSession = func() {

	for _, unit := range []string{"virtqemud.service", "libvirtd.service"} {

		_ = exec.Command("systemctl", "--user", "start", unit).Run()
	}

	if _, err := exec.LookPath("virsh"); err == nil {
		_ = exec.Command("virsh", "-c", "qemu:///session", "list").Run()
	}
}

StartLibvirtUserSession ensures the libvirt user-session daemon is running. Modular libvirt's `virtqemud --timeout=120` auto-exits after 120 s of idle, so consecutive `charly check libvirt …` calls spaced wider than that find the socket gone.

Three start mechanisms tried in order, all best-effort:

  1. `systemctl --user start virtqemud.service` — preferred when the unit is installed (Debian/Ubuntu mostly).
  2. `systemctl --user start libvirtd.service` — legacy monolithic libvirt.
  3. `virsh -c qemu:///session list` — works on Arch and any host where libvirt installs WITHOUT systemd user units. virsh dispatches to `virt-ssh-helper` / `virtqemud` directly, which spawns the daemon and creates `/run/user/$UID/libvirt/ virtqemud-sock` on first connect.

The function silently ignores all failures. Two outcomes:

  • Daemon now running → caller's subsequent socket dial succeeds.
  • Daemon not installable (no libvirt on this host) → caller's downstream socket dial returns "no such file or directory", which surfaces the real error.

Reason for best-effort: don't block legitimate non-libvirt users.

Package-level var (not a plain func) so a caller's test can stub it to a no-op when needed (e.g. candy/plugin-vm's resolveVmBackendPlugin coverage, vm_backend_resolve_test.go's stubNoLibvirtSpawn, which stubs the sdk/vmshared re-export forwarder it also calls — self-consistent within that package).

View Source
var SystemdUserRuntimeDir = func() string {
	return filepath.Join("/run/user", strconv.Itoa(os.Geteuid()), "systemd")
}

SystemdUserRuntimeDir returns the path the directory check probes — `/run/user/<uid>/systemd`. Exposed as a package-level SEAM var so tests can redirect to a t.TempDir(). Lives here ONLY (kit does not re-export it).

Functions

func CompareGlibc

func CompareGlibc(a, b string) int

CompareGlibc returns -1 / 0 / 1 for a vs b, where each is "MAJOR.MINOR". Empty strings compare as equal (unknown vs unknown). A single-empty comparison returns 0 (treat unknown as compatible).

func DetectHostGlibc

func DetectHostGlibc() (string, error)

DetectHostGlibc runs `ldd --version` and extracts the version. Returns "" with no error when glibc can't be detected (e.g. musl hosts) — callers should treat an empty string as "unknown, skip the preflight check".

func DetectRunMode

func DetectRunMode(runEngine string) string

DetectRunMode returns "quadlet" when podman is present AND a functional systemd-user session is reachable (systemctl binary + XDG_RUNTIME_DIR + /run/user/<uid>/systemd directory). Otherwise returns "direct".

The functional-systemd-user check (added 2026-04-27) catches nested environments — harness sandbox pods, supervisord-only containers, sysvinit hosts — that have the systemctl binary present but no running `systemd --user` session. Without this check, `charly fleet add <name> <ref>` would silently pick run_mode=quadlet, write the .container file, and fail at `systemctl --user daemon-reload` time. With the check, run_mode=direct is auto-selected on those hosts and `runConfigDirect()` (in config_image.go) emits a `podman run -d` invocation instead.

func DotenvLoaded

func DotenvLoaded(name string) bool

DotenvLoaded reports whether a given env var name was loaded from the project .env file.

func EnrichNoProxy

func EnrichNoProxy(envs []string, containerNames []string) []string

EnrichNoProxy appends container hostnames to NO_PROXY when a proxy is configured. Chrome does not support CIDR ranges in NO_PROXY — only exact hostnames and domain suffixes. This ensures container-to-container traffic bypasses the proxy.

func ExpandHostHome

func ExpandHostHome(path string) string

ExpandHostHome expands ~ and $HOME in a path using the actual user's home directory.

func FormatForDistroID

func FormatForDistroID(id string) string

FormatForDistroID maps an /etc/os-release-style distro ID (or an embedded distro: vocabulary key) to its package format via the single distroIDToFormat table. Returns "" for an unknown ID.

func LoadProcessDotenv

func LoadProcessDotenv(dir string) error

LoadProcessDotenv loads .env from dir into the process environment. Variables already set in the environment are NOT overwritten (real env wins). Silently returns nil if .env does not exist.

func LoadWorkspaceEnv

func LoadWorkspaceEnv(workspace string) ([]string, error)

LoadWorkspaceEnv loads env vars from a workspace .env file (if it exists). Does NOT run direnv — direnv modifies the host env before charly runs. Returns nil, nil if no .env file found.

func ParseEnvBytes

func ParseEnvBytes(data []byte) ([]string, error)

ParseEnvBytes parses KEY=VALUE entries from raw bytes. Skips comments (#), blank lines, and strips surrounding quotes from values.

func ParseEnvFile

func ParseEnvFile(path string) ([]string, error)

ParseEnvFile reads a .env file and returns KEY=VALUE strings. Skips comments (#), blank lines, and supports KEY=VALUE and KEY="VALUE" (strips quotes). Compatible with docker --env-file format.

func ParseGlibcVersion

func ParseGlibcVersion(out string) string

ParseGlibcVersion extracts "MAJOR.MINOR" from ldd output. Broken out for unit-testing against stable output strings.

func ReexecOverSSH

func ReexecOverSSH(host, identityFile string, options []string, controllerBin, version string, wantTTY bool) int

ReexecOverSSH rewrites os.Args by stripping --host and the client- local path flags (--dir/-C, --repo), resolves the remote charly endpoint (the venue's own PATH charly when it is at least as new as the local controller; otherwise a version-gated replica of the local binary delivered by execc.EnsureCharlyInDeployVenue), then invokes `ssh <resolved-target> <endpoint> <rest of argv>`. Stdin/stdout/stderr are piped straight through. The returned exit code is whatever `ssh` exits with (which propagates the remote `charly` exit code). The happy path prints nothing — a diagnostic appears only when the local binary is actually replicated or the bootstrap fails.

host/identityFile/options are the caller's --host/--host-identity-file/ --host-option flag values; controllerBin is the caller's OWN resolved active binary path (charly-core's activeCharlyBinary()); version is the caller's OWN CalVer identity (charly-core's CharlyVersion()); wantTTY is whether stdin is a terminal (charly-core's term.IsTerminal(stdin)) — all charly-core-only concerns the caller resolves and threads in, so this function stays pure stdlib+spec.

func ResolveAutoEnable

func ResolveAutoEnable(envVal string, cfgVal *bool) bool

func ResolveEncryptedStoragePath

func ResolveEncryptedStoragePath(envVal, cfgVal string) string

func ResolveEnvVars

func ResolveEnvVars(globalEnv []string, deployEnv []string, deployEnvFile string, envDir string, cliEnvFile string, cliEnv []string) ([]string, error)

ResolveEnvVars merges env vars from multiple sources. Priority (last wins for duplicate keys): global env < deploy config < workspace .env < CLI --env-file < CLI -e flags.

func ResolveValue

func ResolveValue(envVal, cfgVal, defaultVal string) string

ResolveValue returns the first non-empty value from the chain.

func ResolveVolumesPath

func ResolveVolumesPath(envVal, cfgVal string) string

func SaveRuntimeConfig

func SaveRuntimeConfig(cfg *RuntimeConfig) error

SaveRuntimeConfig writes the runtime config file, creating directories as needed.

func SplitOsReleaseLine

func SplitOsReleaseLine(line string) (key, val string, ok bool)

SplitOsReleaseLine parses a single line of /etc/os-release into (key, value). Values may be unquoted, single-quoted, or double-quoted. Comments (# ...) and blank lines return ok=false.

func SystemdUserAvailable

func SystemdUserAvailable() bool

SystemdUserAvailable reports whether a functional `systemd --user` session is reachable for the current process. Both signals must hold:

  • $XDG_RUNTIME_DIR is set (the bus address resolves against it)
  • /run/user/<uid>/systemd exists as a directory (systemd-user has populated its runtime dir, i.e. the user-instance has actually started)

Either alone is insufficient: $XDG_RUNTIME_DIR can be set in stale environments where systemd-user never came up, and the runtime dir can exist on systems where the env var got dropped (sudo without -E, container entrypoints).

func ValidateBindAddress

func ValidateBindAddress(value string) error

func ValidateEngine

func ValidateEngine(value, field string) error

func ValidateRunMode

func ValidateRunMode(value string) error

Types

type EngineConfig

type EngineConfig struct {
	Build   string `yaml:"build,omitempty" json:"build,omitempty"`
	Run     string `yaml:"run,omitempty" json:"run,omitempty"`
	Rootful string `yaml:"rootful,omitempty" json:"rootful,omitempty"` // "auto", "machine", "sudo", "native"
}

EngineConfig specifies which container engine to use

type HostDistro

type HostDistro struct {
	// ID is the primary identifier, e.g. "fedora", "arch", "ubuntu",
	// "debian". Matches /etc/os-release's ID= field.
	ID string

	// VersionID is the release identifier, e.g. "43" for Fedora 43,
	// "24.04" for Ubuntu 24.04. Empty for rolling-release distros
	// (arch).
	VersionID string

	// IDLike is the list of distros this system claims compatibility
	// with, in order. Populated from ID_LIKE=; enables fallback when a
	// candy only has a parent-distro section (e.g. an ubuntu host
	// picking up a debian: section).
	IDLike []string

	// Tags is the ordered list of distro tags to use for format-section
	// matching: [exact ID+Version, ID, ID_LIKE entries]. Matches the
	// img.Distro list structure used by candy tag-section resolution.
	Tags []string
}

HostDistro identifies the host's distro for BuildDeployPlan.

func DetectHostDistro

func DetectHostDistro() (*HostDistro, error)

DetectHostDistro reads /etc/os-release and derives the structured distro identity. Errors only when /etc/os-release is unreadable.

func (*HostDistro) FormatHint

func (hd *HostDistro) FormatHint() string

FormatHint returns the best-guess format name (rpm/deb/pac) based on the host distro's ID / ID_LIKE, via the single distroIDToFormat table. Used when the caller has no DistroDef in hand (e.g. the synthetic host-adhoc image). For a resolved DistroDef, prefer DistroDef.PrimaryFormat.

func (*HostDistro) PopulateTags

func (hd *HostDistro) PopulateTags()

PopulateTags derives HostDistro.Tags from the other fields. The resulting list includes both the os-release ID (exact match for candy tag sections like `arch:`) and the embedded vocabulary's (charly/charly.yml) canonical name (for DistroConfig.ResolveDistro to find the format definitions).

func (*HostDistro) PrimaryTag

func (hd *HostDistro) PrimaryTag() string

PrimaryTag returns the first tag (most specific). Convenience for callers that want a single "best match" string.

type ResolvedRuntime

type ResolvedRuntime struct {
	BuildEngine          string // "docker" or "podman"
	RunEngine            string // "docker" or "podman"
	Rootful              string // "auto", "machine", "sudo", "native"
	RunMode              string // "direct" or "quadlet"
	AutoEnable           bool   // auto-enable quadlet on first start
	BindAddress          string // "127.0.0.1" or "0.0.0.0"
	EncryptedStoragePath string // path for gocryptfs encrypted storage
	VolumesPath          string // base path for bind mount volume data
	ForwardGpgAgent      bool   // forward host GPG agent socket into containers
	ForwardSshAgent      bool   // forward host SSH agent socket into containers
	VmBackend            string // "auto", "libvirt", or "qemu"
}

ResolvedRuntime holds the fully resolved runtime configuration

func ResolveRuntime

func ResolveRuntime() (*ResolvedRuntime, error)

ResolveRuntime resolves the runtime configuration: env vars > config file > defaults.

type RuntimeConfig

type RuntimeConfig struct {
	Engine                 EngineConfig      `yaml:"engine" json:"engine"`
	RunMode                string            `yaml:"run_mode,omitempty" json:"run_mode,omitempty"`
	AutoEnable             *bool             `yaml:"auto_enable,omitempty" json:"auto_enable,omitempty"`
	BindAddress            string            `yaml:"bind_address,omitempty" json:"bind_address,omitempty"`
	EncryptedStoragePath   string            `yaml:"encrypted_storage_path,omitempty" json:"encrypted_storage_path,omitempty"`
	VolumesPath            string            `yaml:"volumes_path,omitempty" json:"volumes_path,omitempty"`
	SecretBackend          string            `yaml:"secret_backend,omitempty" json:"secret_backend,omitempty"`       // "auto", "keyring", "config"
	ForwardGpgAgent        *bool             `yaml:"forward_gpg_agent,omitempty" json:"forward_gpg_agent,omitempty"` // Forward host GPG agent socket into containers (default: true)
	ForwardSshAgent        *bool             `yaml:"forward_ssh_agent,omitempty" json:"forward_ssh_agent,omitempty"` // Forward host SSH agent socket into containers (default: true)
	Vm                     RuntimeVmConfig   `yaml:"vm,omitempty" json:"vm,omitempty"`
	VncPasswords           map[string]string `yaml:"vnc_passwords,omitempty" json:"vnc_passwords,omitempty"`                       // VNC passwords keyed by image[-instance]
	KeyringKeys            []string          `yaml:"keyring_keys,omitempty" json:"keyring_keys,omitempty"`                         // Shadow index: names of keys stored in keyring (no values)
	KeyringCollectionLabel string            `yaml:"keyring_collection_label,omitempty" json:"keyring_collection_label,omitempty"` // Preferred Secret Service collection label; empty means use default alias then iterate.
	// HostAliases maps short names (e.g. "o") to SSH targets (e.g.
	// "user@o.example.org"). Consulted by charly's --host flag when
	// re-execing commands on remote machines. Set via
	// `charly settings set hosts.<alias> <ssh-target>`.
	HostAliases map[string]string `yaml:"host_aliases,omitempty" json:"host_aliases,omitempty"`
}

RuntimeConfig represents the user-level runtime configuration (~/.config/charly/config.yml)

func LoadRuntimeConfig

func LoadRuntimeConfig() (*RuntimeConfig, error)

LoadRuntimeConfig reads the runtime config file. Returns zero-value config if missing.

type RuntimeVmConfig

type RuntimeVmConfig struct {
	Backend   string `yaml:"backend,omitempty" json:"backend,omitempty"`     // "auto", "libvirt", "qemu"
	DiskSize  string `yaml:"disk_size,omitempty" json:"disk_size,omitempty"` // default disk size
	RootSize  string `yaml:"root_size,omitempty" json:"root_size,omitempty"` // root partition size
	Ram       string `yaml:"ram,omitempty" json:"ram,omitempty"`             // default RAM
	Cpus      int    `yaml:"cpus,omitempty" json:"cpus,omitempty"`           // default CPU count
	Rootfs    string `yaml:"rootfs,omitempty" json:"rootfs,omitempty"`       // root filesystem type
	Transport string `yaml:"transport,omitempty" json:"transport,omitempty"` // image transport (registry, containers-storage)
}

RuntimeVmConfig holds user-level VM defaults

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL