mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
250 lines
6 KiB
Go
250 lines
6 KiB
Go
package system
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
)
|
|
|
|
const maxProcReadBytes int64 = 64 * 1024
|
|
|
|
var (
|
|
envGetFn = os.Getenv
|
|
statFn = os.Stat
|
|
readFileFn = boundedReadFile
|
|
hostnameFn = os.Hostname
|
|
)
|
|
|
|
var (
|
|
errFileTooLarge = errors.New("file exceeds size limit")
|
|
errInvalidReadMax = errors.New("max read limit must be positive")
|
|
)
|
|
|
|
var containerMarkers = []string{
|
|
"docker",
|
|
"lxc",
|
|
"containerd",
|
|
"kubepods",
|
|
"podman",
|
|
"crio",
|
|
"libpod",
|
|
"lxcfs",
|
|
}
|
|
|
|
var dockerRuntimeMarkers = []string{
|
|
"docker",
|
|
"containerd",
|
|
"kubepods",
|
|
"podman",
|
|
"crio",
|
|
"libpod",
|
|
}
|
|
|
|
// InContainer reports whether Pulse is running inside a containerised environment.
|
|
func InContainer() bool {
|
|
// Allow operators to force container behaviour when automatic detection falls short.
|
|
if isTruthy(envGetFn("PULSE_FORCE_CONTAINER")) {
|
|
return true
|
|
}
|
|
|
|
if _, err := statFn("/.dockerenv"); err == nil {
|
|
return true
|
|
}
|
|
if _, err := statFn("/run/.containerenv"); err == nil {
|
|
return true
|
|
}
|
|
|
|
// Check common environment hints provided by systemd/nspawn, LXC, etc.
|
|
if val := strings.ToLower(strings.TrimSpace(envGetFn("container"))); val != "" && val != "host" {
|
|
return true
|
|
}
|
|
|
|
// Some distros expose the container hint through PID 1's environment.
|
|
if data, err := readFileFn("/proc/1/environ"); err == nil {
|
|
lower := strings.ToLower(string(data))
|
|
if strings.Contains(lower, "container=") && !strings.Contains(lower, "container=host") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Fall back to cgroup inspection which covers older Docker/LXC setups.
|
|
if data, err := readFileFn("/proc/1/cgroup"); err == nil {
|
|
content := strings.ToLower(string(data))
|
|
if hasAnyMarker(content, containerMarkers) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// DetectDockerContainerName attempts to detect the Docker container name.
|
|
// Returns empty string if not in Docker or name cannot be determined.
|
|
func DetectDockerContainerName() string {
|
|
if !inDockerRuntime() {
|
|
return ""
|
|
}
|
|
|
|
// Method 1: Check hostname (Docker uses container ID or name as hostname)
|
|
if hostname, err := hostnameFn(); err == nil && hostname != "" {
|
|
// Docker hostnames are either short container ID (12 chars) or custom name
|
|
// If it looks like a container ID (hex), skip it - user needs to use name
|
|
if !utils.IsHexString(hostname) || len(hostname) > 12 {
|
|
return hostname
|
|
}
|
|
}
|
|
|
|
// Method 2: Try reading from /proc/self/cgroup
|
|
if data, err := readFileFn("/proc/self/cgroup"); err == nil {
|
|
// Look for patterns like: 0::/docker/<container-id>
|
|
// But we can't get name from cgroup, only ID
|
|
_ = data // placeholder for future enhancement
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func inDockerRuntime() bool {
|
|
if _, err := statFn("/.dockerenv"); err == nil {
|
|
return true
|
|
}
|
|
if _, err := statFn("/run/.containerenv"); err == nil {
|
|
return true
|
|
}
|
|
|
|
switch strings.ToLower(strings.TrimSpace(envGetFn("container"))) {
|
|
case "docker", "podman", "containerd", "crio", "cri-o", "libpod", "kubepods", "oci":
|
|
return true
|
|
}
|
|
|
|
for _, path := range []string{"/proc/1/environ", "/proc/self/environ"} {
|
|
if data, err := readFileFn(path); err == nil {
|
|
lower := strings.ToLower(string(data))
|
|
if strings.Contains(lower, "container=docker") ||
|
|
strings.Contains(lower, "container=podman") ||
|
|
strings.Contains(lower, "container=containerd") ||
|
|
strings.Contains(lower, "container=crio") ||
|
|
strings.Contains(lower, "container=cri-o") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, path := range []string{"/proc/1/cgroup", "/proc/self/cgroup"} {
|
|
if data, err := readFileFn(path); err == nil {
|
|
if hasAnyMarker(strings.ToLower(string(data)), dockerRuntimeMarkers) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func hasAnyMarker(content string, markers []string) bool {
|
|
for _, marker := range markers {
|
|
if strings.Contains(content, marker) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isHexString(s string) bool {
|
|
for _, c := range s {
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 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 := readFileFn("/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 := hostnameFn(); 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":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func boundedReadFile(path string) ([]byte, error) {
|
|
return readFileWithLimit(path, maxProcReadBytes)
|
|
}
|
|
|
|
func readFileWithLimit(path string, maxBytes int64) ([]byte, error) {
|
|
if maxBytes <= 0 {
|
|
return nil, errInvalidReadMax
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
data, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if int64(len(data)) > maxBytes {
|
|
return nil, errFileTooLarge
|
|
}
|
|
|
|
return data, nil
|
|
}
|