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 ¶
- Variables
- func CompareGlibc(a, b string) int
- func DetectHostGlibc() (string, error)
- func DetectRunMode(runEngine string) string
- func DotenvLoaded(name string) bool
- func EnrichNoProxy(envs []string, containerNames []string) []string
- func ExpandHostHome(path string) string
- func FormatForDistroID(id string) string
- func LoadProcessDotenv(dir string) error
- func LoadWorkspaceEnv(workspace string) ([]string, error)
- func ParseEnvBytes(data []byte) ([]string, error)
- func ParseEnvFile(path string) ([]string, error)
- func ParseGlibcVersion(out string) string
- func ReexecOverSSH(host, identityFile string, options []string, controllerBin, version string, ...) int
- func ResolveAutoEnable(envVal string, cfgVal *bool) bool
- func ResolveEncryptedStoragePath(envVal, cfgVal string) string
- func ResolveEnvVars(globalEnv []string, deployEnv []string, deployEnvFile string, envDir string, ...) ([]string, error)
- func ResolveValue(envVal, cfgVal, defaultVal string) string
- func ResolveVolumesPath(envVal, cfgVal string) string
- func SaveRuntimeConfig(cfg *RuntimeConfig) error
- func SplitOsReleaseLine(line string) (key, val string, ok bool)
- func SystemdUserAvailable() bool
- func ValidateBindAddress(value string) error
- func ValidateEngine(value, field string) error
- func ValidateRunMode(value string) error
- type EngineConfig
- type HostDistro
- type ResolvedRuntime
- type RuntimeConfig
- type RuntimeVmConfig
Constants ¶
This section is empty.
Variables ¶
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).
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:
- `systemctl --user start virtqemud.service` — preferred when the unit is installed (Debian/Ubuntu mostly).
- `systemctl --user start libvirtd.service` — legacy monolithic libvirt.
- `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).
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 ¶
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 ¶
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 ¶
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 ¶
DotenvLoaded reports whether a given env var name was loaded from the project .env file.
func EnrichNoProxy ¶
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 ¶
ExpandHostHome expands ~ and $HOME in a path using the actual user's home directory.
func FormatForDistroID ¶
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 ¶
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 ¶
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 ¶
ParseEnvBytes parses KEY=VALUE entries from raw bytes. Skips comments (#), blank lines, and strips surrounding quotes from values.
func ParseEnvFile ¶
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 ¶
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 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 ¶
ResolveValue returns the first non-empty value from the chain.
func ResolveVolumesPath ¶
func SaveRuntimeConfig ¶
func SaveRuntimeConfig(cfg *RuntimeConfig) error
SaveRuntimeConfig writes the runtime config file, creating directories as needed.
func SplitOsReleaseLine ¶
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 ValidateEngine ¶
func ValidateRunMode ¶
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