diff --git a/CHANGELOG.md b/CHANGELOG.md index bdddf6b..a622bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,7 +100,7 @@ Release candidate after provision lifecycle hardening, runtime observability fix ### Changed - **Live reprovision:** `edgelet system provision` on a running daemon reloads registries, volume mounts, microservices, and config without restarting background workers. -- **Split log basenames:** `edgelet.service` writes `edgelet.*.log`; `edgelet-containerd.service` writes `edgelet-containerd.*.log` under the same `logDiskDirectory` (60/40 rotation budget when runtime split is enabled). See `docs/edgelet/logging.md`. +- **Split log basenames:** `edgelet.service` writes `edgelet.*.log`; `edgelet-containerd.service` writes `edgelet-containerd.*.log` under the same `logDirectory` (60/40 rotation budget when runtime split is enabled). See `docs/edgelet/logging.md`. - **Disk usage metric:** `edgelet system status` `diskUsage` walks the full configured `diskDirectory` instead of legacy `messages/archive` + `volumes/` partial sums; automatic archive prune removed (greenfield). - **Image drift (embedded engine):** reconcile compares images with `imageref.Match` so controller short names (e.g. `user/repo:tag`) match runtime `docker.io/` prefixes without endless `config_drift` UPDATE loops. - **Deprovision cleanup:** volume-mount index cleanup no longer holds locks across slow filesystem work that blocked the process-manager monitor during background deprovision. diff --git a/Makefile b/Makefile index 582af79..5b0281b 100644 --- a/Makefile +++ b/Makefile @@ -363,12 +363,12 @@ install-dev: build-edgelet-local ## Install edgelet and setup local dev environm ' networkInterface: "dynamic"' \ ' containerEngine: "docker"' \ ' containerEngineUrl: "unix:///var/run/docker.sock"' \ - ' diskConsumptionLimit: "10"' \ + ' diskLimit: "10"' \ " diskDirectory: \"$(DEV_VAR_LIB)/\"" \ - ' memoryConsumptionLimit: "4096"' \ - ' processorConsumptionLimit: "80.0"' \ - ' logDiskConsumptionLimit: "10.0"' \ - " logDiskDirectory: \"$(DEV_VAR_LOG)/\"" \ + ' memoryLimit: "4096"' \ + ' cpuLimit: "80.0"' \ + ' logLimit: "10.0"' \ + " logDirectory: \"$(DEV_VAR_LOG)/\"" \ ' logFileCount: "10"' \ ' logLevel: "DEBUG"' \ ' statusFrequency: "30"' \ diff --git a/cmd/edgelet-server/daemon.go b/cmd/edgelet-server/daemon.go index fad57ad..f3c1616 100644 --- a/cmd/edgelet-server/daemon.go +++ b/cmd/edgelet-server/daemon.go @@ -199,7 +199,7 @@ func startLoggingService(basename ...string) { cfg := config.GetInstance() logo += fmt.Sprintf(" Log Level: %s\n", cfg.LogLevel) - logo += fmt.Sprintf(" Log Directory: %s\n", cfg.LogDiskDirectory) + logo += fmt.Sprintf(" Log Directory: %s\n", cfg.LogDirectory) logo += fmt.Sprintf(" Log Basename: %s\n", logBasename) _, _ = fmt.Print(logo) @@ -209,8 +209,8 @@ func startLoggingService(basename ...string) { role = logging.SeriesDataPlane runtimeSplit = true } - logDiskLimitMB := logging.DaemonLogBudgetMB(cfg.LogDiskLimit, role, runtimeSplit) - if err := logging.SetupLogger(cfg.LogDiskDirectory, logDiskLimitMB, cfg.LogFileCount, cfg.LogLevel, logBasename); err != nil { + logLimitMB := logging.DaemonLogBudgetMB(cfg.LogLimit, role, runtimeSplit) + if err := logging.SetupLogger(cfg.LogDirectory, logLimitMB, cfg.LogFileCount, cfg.LogLevel, logBasename); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Failed to setup logger: %v\n", err) exitDaemon(1) } diff --git a/cmd/edgelet-server/runtime_bootstrap.go b/cmd/edgelet-server/runtime_bootstrap.go index dd7b785..4fc2621 100644 --- a/cmd/edgelet-server/runtime_bootstrap.go +++ b/cmd/edgelet-server/runtime_bootstrap.go @@ -89,8 +89,8 @@ func reloadRuntimeBootstrapConfig() { logging.LogError("RUNTIME_BOOTSTRAP", "Configuration validation failed after reload", err) return } - budgetMB := logging.DaemonLogBudgetMB(cfg.LogDiskLimit, logging.SeriesDataPlane, true) - if err := logging.InstanceConfigUpdated(cfg.LogDiskDirectory, budgetMB, cfg.LogFileCount, cfg.LogLevel, logging.BasenameDataPlane); err != nil { + budgetMB := logging.DaemonLogBudgetMB(cfg.LogLimit, logging.SeriesDataPlane, true) + if err := logging.InstanceConfigUpdated(cfg.LogDirectory, budgetMB, cfg.LogFileCount, cfg.LogLevel, logging.BasenameDataPlane); err != nil { logging.LogError("RUNTIME_BOOTSTRAP", "Failed to update logger configuration", err) return } diff --git a/cmd/edgelet/daemon_desktop.go b/cmd/edgelet/daemon_desktop.go index a862828..ba39d98 100644 --- a/cmd/edgelet/daemon_desktop.go +++ b/cmd/edgelet/daemon_desktop.go @@ -157,11 +157,11 @@ func startLoggingService() { cfg := config.GetInstance() logo += fmt.Sprintf(" Log Level: %s\n", cfg.LogLevel) - logo += fmt.Sprintf(" Log Directory: %s\n", cfg.LogDiskDirectory) + logo += fmt.Sprintf(" Log Directory: %s\n", cfg.LogDirectory) _, _ = fmt.Print(logo) - logDiskLimitMB := logging.DaemonLogBudgetMB(cfg.LogDiskLimit, logging.SeriesControlPlane, false) - if err := logging.SetupLogger(cfg.LogDiskDirectory, logDiskLimitMB, cfg.LogFileCount, cfg.LogLevel); err != nil { + logLimitMB := logging.DaemonLogBudgetMB(cfg.LogLimit, logging.SeriesControlPlane, false) + if err := logging.SetupLogger(cfg.LogDirectory, logLimitMB, cfg.LogFileCount, cfg.LogLevel); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Failed to setup logger: %v\n", err) exitDaemon(1) } diff --git a/docs/cli/generated/edgelet_config.md b/docs/cli/generated/edgelet_config.md index f1f5c4a..aff7a37 100644 --- a/docs/cli/generated/edgelet_config.md +++ b/docs/cli/generated/edgelet_config.md @@ -55,8 +55,8 @@ edgelet config [flags] --gps-mode string GPS mode (auto|dynamic|manual|off). Alias: --gps --gps-scan-frequency int GPS scan frequency. Alias: --gpsf -h, --help help for config - --log-disk-directory string log directory. Alias: --ld - --log-disk-limit-gib float log disk limit (GiB). Alias: --l + --log-directory string log directory. Alias: --ld + --log-limit-gib float log limit (GiB). Alias: --l --log-file-count int log file count. Alias: --lc --log-level string log level (DEBUG|INFO|WARN|ERROR). Alias: --ll --memory-limit-mib float memory limit (MiB). Alias: --m diff --git a/docs/edgelet/cgroups.md b/docs/edgelet/cgroups.md index 9c0e802..d9b172c 100644 --- a/docs/edgelet/cgroups.md +++ b/docs/edgelet/cgroups.md @@ -75,7 +75,7 @@ Per-microservice fields enforced on the edgelet engine (CRI/crun): - `memoryLimit` (bytes) - `cpuSetCpus` -Node configuration `memoryConsumptionLimit` / `processorConsumptionLimit` remain **monitor-only** (not cgroup-enforced). +Node configuration `memoryLimit` / `cpuLimit` remain **monitor-only** (not cgroup-enforced). Missing **hugetlb** / **rdma** controllers on edge hardware are tolerated. diff --git a/docs/edgelet/init-systems.md b/docs/edgelet/init-systems.md index 86ef3dd..fbc5b8c 100644 --- a/docs/edgelet/init-systems.md +++ b/docs/edgelet/init-systems.md @@ -105,7 +105,7 @@ plane uses `supervise-daemon` respawn (`respawn_max=0`, parity with systemd `Res | Log | Source | |-----|--------| | `/var/log/edgelet/daemon.log` | OpenRC `output_log` / `error_log` — thin wrapper and pre-exec errors | -| `/var/log/edgelet/edgelet.0.log` (rotated) | Fat daemon after `edgelet daemon` starts (`logDiskDirectory`) | +| `/var/log/edgelet/edgelet.0.log` (rotated) | Fat daemon after `edgelet daemon` starts (`logDirectory`) | --- diff --git a/docs/edgelet/logging.md b/docs/edgelet/logging.md index c51f9c1..35a08c3 100644 --- a/docs/edgelet/logging.md +++ b/docs/edgelet/logging.md @@ -64,7 +64,7 @@ Set via config or environment (`LOG_LEVEL=info`). ## Log files (rotated) -When `logDiskDirectory` is configured (default `/var/log/edgelet/`), control plane and data plane write **separate file series** in the same directory: +When `logDirectory` is configured (default `/var/log/edgelet/`), control plane and data plane write **separate file series** in the same directory: | systemd unit | Basename | Active file | |--------------|----------|-------------| @@ -78,9 +78,9 @@ Each series rotates independently using `logFileCount` and a share of **`logLimi | Runtime split (`EDGELET_RUNTIME_SPLIT=1`, embedded engine) | 60% | 40% | | Monolithic embedded, docker, podman, desktop | 100% | (unit not used) | -**Hot reload:** `logLevel`, `logLimit`, and `logFileCount` apply on config reload without rotating the active log file. Control plane reloads via SIGHUP / `edgelet system reload`; data plane reloads via `systemctl kill -s HUP edgelet-containerd`. Changing `logDiskDirectory` still requires a process restart. +**Hot reload:** `logLevel`, `logLimit`, and `logFileCount` apply on config reload without rotating the active log file. Control plane reloads via SIGHUP / `edgelet system reload`; data plane reloads via `systemctl kill -s HUP edgelet-containerd`. Changing `logDirectory` still requires a process restart. -Microservice logs under `logDiskDirectory/containers/` use separate per-UUID series (not included in the daemon 60/40 split). +Microservice logs under `logDirectory/containers/` use separate per-UUID series (not included in the daemon 60/40 split). ## journald diff --git a/docs/edgelet/modules/engines.md b/docs/edgelet/modules/engines.md index 6dd98cd..2abcca2 100644 --- a/docs/edgelet/modules/engines.md +++ b/docs/edgelet/modules/engines.md @@ -72,7 +72,7 @@ Supervisor retries docker/podman init; if socket unavailable: | `containerEngine` | Factory selector | | `containerEngineUrl` | Socket/endpoint for external engines | | `dockerApiVersion` | Docker API version string | -| `logDiskDirectory` | Container log root passed to edgelet engine | +| `logDirectory` | Container log root passed to edgelet engine | ## Interface surface diff --git a/install.sh b/install.sh index 3d20698..75281e0 100755 --- a/install.sh +++ b/install.sh @@ -2656,7 +2656,7 @@ profiles: containerEngine: ${_eng} containerEngineUrl: ${_du} diskDirectory: ${_disk} - logDiskDirectory: ${_log_disk} + logDirectory: ${_log_disk} logLevel: INFO YAML chmod 640 "$CONFIG_FILE" @@ -3291,7 +3291,7 @@ profiles: containerEngine: ${_eng} containerEngineUrl: ${_du} diskDirectory: ${_disk} - logDiskDirectory: ${_log_disk} + logDirectory: ${_log_disk} logLevel: INFO YAML chmod 640 "$CONFIG_FILE" diff --git a/internal/cli/domain/config/flags.go b/internal/cli/domain/config/flags.go index 13ae07d..e50efa1 100644 --- a/internal/cli/domain/config/flags.go +++ b/internal/cli/domain/config/flags.go @@ -21,8 +21,8 @@ var longFlagNames = map[string]string{ "diskDirectory": "disk-directory", "memoryLimitMiB": "memory-limit-mib", "cpuLimitPercent": "cpu-limit-percent", - "logDiskLimitGiB": "log-disk-limit-gib", - "logDiskDirectory": "log-disk-directory", + "logLimitGiB": "log-limit-gib", + "logDirectory": "log-directory", "logFileCount": "log-file-count", "logLevel": "log-level", "statusFrequencySeconds": "status-frequency-seconds", diff --git a/internal/cli/domain/config/flags_test.go b/internal/cli/domain/config/flags_test.go index 2ee518d..a49df6b 100644 --- a/internal/cli/domain/config/flags_test.go +++ b/internal/cli/domain/config/flags_test.go @@ -130,8 +130,8 @@ func TestLongFlagName_ExplicitGiBAndMiB(t *testing.T) { if got := longFlagName("memoryLimitMiB"); got != "memory-limit-mib" { t.Fatalf("memoryLimitMiB=%q", got) } - if got := longFlagName("logDiskLimitGiB"); got != "log-disk-limit-gib" { - t.Fatalf("logDiskLimitGiB=%q", got) + if got := longFlagName("logLimitGiB"); got != "log-limit-gib" { + t.Fatalf("logLimitGiB=%q", got) } } diff --git a/internal/cli/domain/config/rules.go b/internal/cli/domain/config/rules.go index 6e1b0ac..ea7535f 100644 --- a/internal/cli/domain/config/rules.go +++ b/internal/cli/domain/config/rules.go @@ -40,8 +40,8 @@ var configKeyRules = map[string]configKeyRule{ "diskDirectory": {Key: "diskDirectory", Aliases: []string{"dl"}, Type: configValueString, Help: "disk directory"}, "memoryLimitMiB": {Key: "memoryLimitMiB", Aliases: []string{"m"}, Type: configValueFloat, Help: "memory limit (MiB)"}, "cpuLimitPercent": {Key: "cpuLimitPercent", Aliases: []string{"p"}, Type: configValueFloat, Help: "CPU limit (%)"}, - "logDiskLimitGiB": {Key: "logDiskLimitGiB", Aliases: []string{"l"}, Type: configValueFloat, Help: "log disk limit (GiB)"}, - "logDiskDirectory": {Key: "logDiskDirectory", Aliases: []string{"ld"}, Type: configValueString, Help: "log directory"}, + "logLimitGiB": {Key: "logLimitGiB", Aliases: []string{"l"}, Type: configValueFloat, Help: "log limit (GiB)"}, + "logDirectory": {Key: "logDirectory", Aliases: []string{"ld"}, Type: configValueString, Help: "log directory"}, "logFileCount": {Key: "logFileCount", Aliases: []string{"lc"}, Type: configValueInt, Help: "log file count"}, "logLevel": {Key: "logLevel", Aliases: []string{"ll"}, Type: configValueString, Enums: []string{"DEBUG", "INFO", "WARN", "ERROR"}, Help: "log level"}, "statusFrequencySeconds": {Key: "statusFrequencySeconds", Aliases: []string{"sf"}, Type: configValueInt, Help: "status frequency (seconds)"}, diff --git a/internal/cli/output/edgeletapi_human.go b/internal/cli/output/edgeletapi_human.go index c67b120..e50eafa 100644 --- a/internal/cli/output/edgeletapi_human.go +++ b/internal/cli/output/edgeletapi_human.go @@ -50,7 +50,7 @@ var infoOutputOrder = []string{ "diskLimit", "diskDirectory", "logLimit", - "logFileDirectory", + "logDirectory", "logFilesCount", "logLevel", "upgradeScanFrequency", @@ -73,7 +73,6 @@ var infoAliasToCanonical = map[string]string{ "cpuLimit": "cpuUsageLimit", "memoryLimit": "memoryRamLimit", "diskLimit": "diskUsageLimit", - "logLimit": "logDiskLimit", "logLevel": "logFilesLevel", "upgradeScanFrequency": "readyToUpgradeScanFrequency", "deviceScanFrequency": "scanDevicesFrequency", diff --git a/internal/config/config.go b/internal/config/config.go index 2454fb0..c3a4d65 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,8 +31,8 @@ type Config struct { MemoryLimit float64 DiskDirectory string CPULimit float64 - LogDiskLimit float64 - LogDiskDirectory string + LogLimit float64 + LogDirectory string LogFileCount int LogLevel string StatusFrequency int @@ -362,11 +362,11 @@ func (c *Config) GetConfigReportWithIP(ipAddress string) string { // CPU limit buildLine("CPU Usage Limit", fmt.Sprintf("%.2f%%", c.CPULimit)) - // Log disk limit - buildLine("Log Disk Limit", fmt.Sprintf("%.2f GiB", c.LogDiskLimit)) + // Log limit + buildLine("Log Limit", fmt.Sprintf("%.2f GiB", c.LogLimit)) // Log directory - buildLine("Log File Directory", c.LogDiskDirectory) + buildLine("Log Directory", c.LogDirectory) // Log file count buildLine("Log Files Count", fmt.Sprintf("%d", c.LogFileCount)) diff --git a/internal/config/config_diff.go b/internal/config/config_diff.go new file mode 100644 index 0000000..ccb77f4 --- /dev/null +++ b/internal/config/config_diff.go @@ -0,0 +1,148 @@ +package config + +import ( + "fmt" + "strconv" + "strings" + + "github.com/eclipse-iofog/edgelet/internal/constants" +) + +// FilterChangedConfigKeys returns only entries whose normalized value differs from +// the current in-memory config. Used when applying full controller config snapshots +// so unchanged keys are not re-written or validated as mutations. +func (c *Config) FilterChangedConfigKeys(configMap map[string]any) map[string]any { + if len(configMap) == 0 { + return nil + } + + c.mu.RLock() + defer c.mu.RUnlock() + + effectiveEngine := c.effectiveEngineForBatchLocked(configMap) + changed := make(map[string]any, len(configMap)) + for option, value := range configMap { + if _, ok := ConfigParamMap[option]; !ok { + continue + } + if c.configKeyUnchangedLocked(option, value, effectiveEngine) { + continue + } + changed[option] = value + } + return changed +} + +func (c *Config) effectiveEngineForBatchLocked(configMap map[string]any) string { + if raw, ok := configMap["ce"]; ok { + return normalizeIncomingValue(raw) + } + return c.ContainerEngine +} + +func (c *Config) configKeyUnchangedLocked(option string, value any, effectiveEngine string) bool { + incoming := normalizeIncomingValue(value) + fieldName := ConfigParamMap[option] + + switch fieldName { + case "diskLimit": + v, err := strconv.ParseFloat(incoming, 64) + return err == nil && v == c.DiskLimit + case "diskDirectory": + return incoming == c.DiskDirectory + case "memoryLimit": + v, err := strconv.ParseFloat(incoming, 64) + return err == nil && v == c.MemoryLimit + case "cpuLimit": + v, err := strconv.ParseFloat(incoming, 64) + return err == nil && v == c.CPULimit + case "controllerURL": + return incoming == c.ControllerURL + case "controllerCert": + return incoming == c.ControllerCert + case "containerEngineURL": + if strings.EqualFold(strings.TrimSpace(effectiveEngine), constants.EngineEdgelet) { + return strings.TrimSpace(incoming) == constants.EdgeletEngineSocketURL() + } + return strings.TrimSpace(incoming) == strings.TrimSpace(c.ContainerEngineURL) + case "containerEngine": + return strings.EqualFold(incoming, c.ContainerEngine) + case "networkInterface": + return incoming == c.NetworkInterface + case "logLimit": + v, err := strconv.ParseFloat(incoming, 64) + return err == nil && v == c.LogLimit + case "logDirectory": + return incoming == c.LogDirectory + case "logFileCount": + v, err := strconv.Atoi(incoming) + return err == nil && v == c.LogFileCount + case "logLevel": + return strings.EqualFold(incoming, c.LogLevel) + case "statusFrequency": + v, err := strconv.Atoi(incoming) + return err == nil && v == c.StatusFrequency + case "changeFrequency": + v, err := strconv.Atoi(incoming) + return err == nil && v == c.ChangeFrequency + case "deviceScanFrequency": + v, err := strconv.Atoi(incoming) + return err == nil && v == c.DeviceScanFrequency + case "watchdogEnabled": + return watchdogEnabledEqual(incoming, c.WatchdogEnabled) + case "edgeGuardFrequency": + v, err := strconv.ParseInt(incoming, 10, 64) + return err == nil && v == c.EdgeGuardFrequency + case "gpsMode": + return strings.EqualFold(incoming, c.GPSMode) + case "gpsCoordinates": + current, err := normalizeGPSCoordinates(c.GPSCoordinates) + if err != nil { + return false + } + incomingNorm, err := normalizeGPSCoordinates(incoming) + return err == nil && incomingNorm == current + case "gpsDevice": + return incoming == c.GPSDevice + case "gpsScanFrequency": + v, err := strconv.ParseInt(incoming, 10, 64) + return err == nil && v == c.GPSScanFrequency + case "arch": + return strings.EqualFold(incoming, c.Arch) + case "secureMode": + return ParseSecureMode(incoming) == c.SecureMode + case "pruningFrequency": + v, err := strconv.ParseInt(incoming, 10, 64) + return err == nil && v == c.PruningFrequency + case "availableDiskThreshold": + v, err := strconv.ParseInt(incoming, 10, 64) + return err == nil && v == c.AvailableDiskThreshold + case "upgradeScanFrequency": + v, err := strconv.Atoi(incoming) + return err == nil && v == c.UpgradeScanFrequency + case "devMode": + return devModeEqual(incoming, c.DevMode) + case "timeZone": + return incoming == c.TimeZone + default: + return false + } +} + +func normalizeIncomingValue(value any) string { + return strings.TrimPrefix(strings.TrimSpace(fmt.Sprintf("%v", value)), "+") +} + +func watchdogEnabledEqual(incoming string, current bool) bool { + normalized := strings.ToLower(strings.TrimSpace(incoming)) + switch normalized { + case "off", "false", "0", "no": + return !current + default: + return current + } +} + +func devModeEqual(incoming string, current bool) bool { + return (strings.ToLower(strings.TrimSpace(incoming)) != "off") == current +} diff --git a/internal/config/config_diff_test.go b/internal/config/config_diff_test.go new file mode 100644 index 0000000..807aa3e --- /dev/null +++ b/internal/config/config_diff_test.go @@ -0,0 +1,111 @@ +package config + +import ( + "os" + "testing" + + "github.com/eclipse-iofog/edgelet/internal/constants" +) + +func TestFilterChangedConfigKeysSkipsUnchanged(t *testing.T) { + tmpFile, err := os.CreateTemp("", "config-diff-*.yaml") + if err != nil { + t.Fatalf("create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + testConfig := `currentProfile: default +profiles: + default: + diskLimit: "500" + memoryLimit: "8192" + logLevel: "INFO" +` + if _, err := tmpFile.WriteString(testConfig); err != nil { + t.Fatalf("write temp config: %v", err) + } + _ = tmpFile.Close() + + if err := LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + cfg := GetInstance() + changed := cfg.FilterChangedConfigKeys(map[string]any{ + "d": 500, + "m": 8192, + "ll": "INFO", + }) + if len(changed) != 0 { + t.Fatalf("expected no changed keys, got: %+v", changed) + } +} + +func TestFilterChangedConfigKeysDetectsChange(t *testing.T) { + tmpFile, err := os.CreateTemp("", "config-diff-change-*.yaml") + if err != nil { + t.Fatalf("create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + testConfig := `currentProfile: default +profiles: + default: + diskLimit: "500" + changeFrequency: "20" +` + if _, err := tmpFile.WriteString(testConfig); err != nil { + t.Fatalf("write temp config: %v", err) + } + _ = tmpFile.Close() + + if err := LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + cfg := GetInstance() + changed := cfg.FilterChangedConfigKeys(map[string]any{ + "d": 500, + "cf": 30, + }) + if len(changed) != 1 { + t.Fatalf("expected one changed key, got: %+v", changed) + } + if _, ok := changed["cf"]; !ok { + t.Fatalf("expected cf to be changed, got: %+v", changed) + } +} + +func TestFilterChangedConfigKeysSkipsEdgeletContainerEngineURL(t *testing.T) { + tmpFile, err := os.CreateTemp("", "config-diff-edgelet-cu-*.yaml") + if err != nil { + t.Fatalf("create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + want := constants.EdgeletEngineSocketURL() + testConfig := `currentProfile: default +profiles: + default: + containerEngine: edgelet + containerEngineUrl: "` + want + `" + changeFrequency: "20" +` + if _, err := tmpFile.WriteString(testConfig); err != nil { + t.Fatalf("write temp config: %v", err) + } + _ = tmpFile.Close() + + if err := LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + cfg := GetInstance() + changed := cfg.FilterChangedConfigKeys(map[string]any{ + "cu": want, + "cf": 20, + }) + if len(changed) != 0 { + t.Fatalf("expected edgelet cu echo to be filtered, got: %+v", changed) + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2475264..0f4dd79 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -54,7 +54,7 @@ func TestValidateConfig(t *testing.T) { cfg.DiskLimit = 10.0 cfg.MemoryLimit = 4096.0 cfg.CPULimit = 80.0 - cfg.LogDiskLimit = 10.0 + cfg.LogLimit = 10.0 cfg.LogFileCount = 10 cfg.LogLevel = "INFO" cfg.StatusFrequency = 10 @@ -77,8 +77,13 @@ func TestValidateConfig(t *testing.T) { t.Errorf("Expected valid config, got error: %v", err) } + cfg.DiskLimit = 1024.0 + if err := ValidateConfig(cfg); err != nil { + t.Errorf("Expected 1024 GiB disk limit to be valid, got error: %v", err) + } + // Test invalid config - cfg.DiskLimit = 200.0 // Invalid: exceeds max + cfg.DiskLimit = 200000.0 // Invalid: exceeds max if err := ValidateConfig(cfg); err == nil { t.Error("Expected validation error for invalid disk limit") } @@ -89,7 +94,7 @@ func TestSnakeToCamel(t *testing.T) { input string expected string }{ - {"disk_consumption_limit", "diskConsumptionLimit"}, + {"disk_limit", "diskLimit"}, {"log_file_count", "logFileCount"}, {"gps_scan_frequency", "gpsScanFrequency"}, {"", ""}, diff --git a/internal/config/default_config.yaml b/internal/config/default_config.yaml index 7b1cfd2..b6560be 100644 --- a/internal/config/default_config.yaml +++ b/internal/config/default_config.yaml @@ -9,5 +9,5 @@ profiles: containerEngine: edgelet containerEngineUrl: unix:///run/edgelet/containerd.sock diskDirectory: /var/lib/edgelet/ - logDiskDirectory: /var/log/edgelet/ + logDirectory: /var/log/edgelet/ logLevel: INFO diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 6505083..4641041 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -6,7 +6,7 @@ import ( ) // snakeToCamel converts snake_case to camelCase -// Example: "disk_consumption_limit" -> "diskConsumptionLimit" +// Example: "disk_limit" -> "diskLimit" func snakeToCamel(s string) string { if s == "" { return s diff --git a/internal/config/loader.go b/internal/config/loader.go index 8f09308..92b205f 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -134,7 +134,11 @@ func loadConfigValues(cfg *Config) { cfg.ContainerEngine = getProp("containerEngine", "edgelet") cfg.ContainerEngineURL = getProp("containerEngineUrl", "unix:///run/edgelet/containerd.sock") cfg.DiskDirectory = getProp("diskDirectory", "/var/lib/edgelet/") - cfg.LogDiskDirectory = getProp("logDiskDirectory", "/var/log/edgelet/") + logDirectory := getProp("logDirectory", "") + if logDirectory == "" { + logDirectory = getProp("logDiskDirectory", "/var/log/edgelet/") + } + cfg.LogDirectory = logDirectory cfg.LogLevel = strings.ToUpper(getProp("logLevel", "INFO")) cfg.GPSDevice = getProp("gpsDevice", "/dev/ttyUSB0") cfg.GPSMode = strings.ToLower(strings.TrimSpace(getProp("gpsMode", "auto"))) @@ -177,7 +181,15 @@ func loadConfigValues(cfg *Config) { cfg.DiskLimit = parseFloat("diskLimit", "10") cfg.MemoryLimit = parseFloat("memoryLimit", "4096") cfg.CPULimit = parseFloat("cpuLimit", "80") - cfg.LogDiskLimit = parseFloat("logLimit", "10") + logLimitRaw := getProp("logLimit", "") + if logLimitRaw == "" { + logLimitRaw = getProp("logDiskLimit", "10") + } + var logLimitVal float64 + if _, err := fmt.Sscanf(logLimitRaw, "%f", &logLimitVal); err != nil { + logging.LogWarn(configLoaderModuleName, fmt.Sprintf("Failed to parse config value for logLimit: %v", err)) + } + cfg.LogLimit = logLimitVal cfg.LogFileCount = parseInt("logFileCount", "10") cfg.StatusFrequency = parseInt("statusFrequency", "10") cfg.ChangeFrequency = parseInt("changeFrequency", "20") diff --git a/internal/config/reload.go b/internal/config/reload.go index ee29281..3ea9dd1 100644 --- a/internal/config/reload.go +++ b/internal/config/reload.go @@ -44,8 +44,8 @@ func FullReload(hooks ReloadHooks) error { } SetLastReloadSuccessful(true) - logDiskLimitMB := logging.DaemonLogBudgetMB(cfg.LogDiskLimit, logging.SeriesControlPlane, logging.RuntimeSplitFromEnv()) - if err := logging.InstanceConfigUpdated(cfg.LogDiskDirectory, logDiskLimitMB, cfg.LogFileCount, cfg.LogLevel); err != nil { + logLimitMB := logging.DaemonLogBudgetMB(cfg.LogLimit, logging.SeriesControlPlane, logging.RuntimeSplitFromEnv()) + if err := logging.InstanceConfigUpdated(cfg.LogDirectory, logLimitMB, cfg.LogFileCount, cfg.LogLevel); err != nil { logging.LogError(reloadModuleName, "Failed to update logger configuration", err) } diff --git a/internal/config/setconfig.go b/internal/config/setconfig.go index 6853f32..67190af 100644 --- a/internal/config/setconfig.go +++ b/internal/config/setconfig.go @@ -27,8 +27,8 @@ var ConfigParamMap = map[string]string{ "cu": "containerEngineURL", "ce": "containerEngine", "n": "networkInterface", - "l": "logDiskLimit", - "ld": "logDiskDirectory", + "l": "logLimit", + "ld": "logDirectory", "lc": "logFileCount", "ll": "logLevel", "sf": "statusFrequency", @@ -49,6 +49,10 @@ var ConfigParamMap = map[string]string{ "tz": "timeZone", } +// setConfigApplyOrder lists short codes applied before other keys in the same batch. +// containerEngine must precede containerEngineURL because URL validation depends on engine. +var setConfigApplyOrder = []string{"ce", "cu"} + // SetConfig sets configuration values from a map of command line parameters // Returns a map of errors keyed by parameter name func (c *Config) SetConfig(configMap map[string]any) map[string]string { @@ -56,35 +60,43 @@ func (c *Config) SetConfig(configMap map[string]any) map[string]string { defer c.mu.Unlock() errorMap := make(map[string]string) + applied := make(map[string]struct{}, len(configMap)) + + applyOne := func(option string, value any) { + if _, done := applied[option]; done { + return + } + applied[option] = struct{}{} - for option, value := range configMap { - // Get config field name fieldName, exists := ConfigParamMap[option] if !exists { errorMap[option] = "Invalid parameter" - continue + return } - // Convert value to string valueStr := fmt.Sprintf("%v", value) - - // Remove leading "+" if present valueStr = strings.TrimPrefix(valueStr, "+") - // Validate if strings.TrimSpace(valueStr) == "" && option != "ac" { errorMap[option] = "Command or value is invalid" - continue + return } - // Set the config value based on field name - err := c.setConfigField(fieldName, valueStr, option) - if err != nil { + if err := c.setConfigField(fieldName, valueStr, option); err != nil { errorMap[option] = err.Error() logging.LogError(setConfigModuleName, fmt.Sprintf("Error setting %s: %v", option, err), nil) } } + for _, option := range setConfigApplyOrder { + if value, ok := configMap[option]; ok { + applyOne(option, value) + } + } + for option, value := range configMap { + applyOne(option, value) + } + // Save config updates (while holding lock) if len(errorMap) == 0 { // Save config (don't acquire lock - we already hold it) @@ -181,7 +193,11 @@ func (c *Config) setConfigField(fieldName, value, _ string) error { case "containerEngineURL": if strings.EqualFold(strings.TrimSpace(c.ContainerEngine), constants.EngineEdgelet) { - return fmt.Errorf("containerEngineUrl is fixed for containerEngine edgelet (%s)", constants.EdgeletEngineSocketURL()) + want := constants.EdgeletEngineSocketURL() + if strings.TrimSpace(value) == want { + return nil + } + return fmt.Errorf("containerEngineUrl is fixed for containerEngine edgelet (%s)", want) } c.ContainerEngineURL = value if err := c.setYamlProperty("containerEngineUrl", value); err != nil { @@ -213,19 +229,19 @@ func (c *Config) setConfigField(fieldName, value, _ string) error { logging.LogWarn(setConfigModuleName, fmt.Sprintf("Failed to persist config property: %v", err)) } - case "logDiskLimit": + case "logLimit": val, err := strconv.ParseFloat(value, 64) if err != nil { - return fmt.Errorf("invalid log disk limit: %w", err) + return fmt.Errorf("invalid log limit: %w", err) } - c.LogDiskLimit = val - if err := c.setYamlProperty("logDiskLimit", value); err != nil { + c.LogLimit = val + if err := c.setYamlProperty("logLimit", value); err != nil { logging.LogWarn(setConfigModuleName, fmt.Sprintf("Failed to persist config property: %v", err)) } - case "logDiskDirectory": - c.LogDiskDirectory = value - if err := c.setYamlProperty("logDiskDirectory", value); err != nil { + case "logDirectory": + c.LogDirectory = value + if err := c.setYamlProperty("logDirectory", value); err != nil { logging.LogWarn(setConfigModuleName, fmt.Sprintf("Failed to persist config property: %v", err)) } diff --git a/internal/config/setconfig_edgelet_test.go b/internal/config/setconfig_edgelet_test.go new file mode 100644 index 0000000..0d008c9 --- /dev/null +++ b/internal/config/setconfig_edgelet_test.go @@ -0,0 +1,118 @@ +package config + +import ( + "os" + "testing" + + "github.com/eclipse-iofog/edgelet/internal/constants" +) + +func TestSetConfigContainerEngineURLNoOpForEdgeletEngine(t *testing.T) { + tmpFile, err := os.CreateTemp("", "setconfig-edgelet-cu-*.yaml") + if err != nil { + t.Fatalf("create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + want := constants.EdgeletEngineSocketURL() + testConfig := `currentProfile: default +profiles: + default: + containerEngine: edgelet + containerEngineUrl: "` + want + `" +` + if _, err := tmpFile.WriteString(testConfig); err != nil { + t.Fatalf("write temp config: %v", err) + } + _ = tmpFile.Close() + + if err := LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + cfg := GetInstance() + errors := cfg.SetConfig(map[string]any{"cu": want}) + if len(errors) > 0 { + t.Fatalf("expected no error echoing fixed edgelet socket, got: %+v", errors) + } + if cfg.ContainerEngineURL != want { + t.Fatalf("ContainerEngineURL=%q want %q", cfg.ContainerEngineURL, want) + } +} + +func TestSetConfigContainerEngineURLRejectsWrongValueForEdgeletEngine(t *testing.T) { + tmpFile, err := os.CreateTemp("", "setconfig-edgelet-cu-reject-*.yaml") + if err != nil { + t.Fatalf("create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + want := constants.EdgeletEngineSocketURL() + testConfig := `currentProfile: default +profiles: + default: + containerEngine: edgelet + containerEngineUrl: "` + want + `" +` + if _, err := tmpFile.WriteString(testConfig); err != nil { + t.Fatalf("write temp config: %v", err) + } + _ = tmpFile.Close() + + if err := LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + cfg := GetInstance() + errors := cfg.SetConfig(map[string]any{"cu": "unix:///var/run/docker.sock"}) + if len(errors) == 0 { + t.Fatal("expected error when changing containerEngineUrl on edgelet engine") + } + if got := errors["cu"]; got == "" { + t.Fatalf("expected cu error, got: %+v", errors) + } + if cfg.ContainerEngineURL != want { + t.Fatalf("ContainerEngineURL=%q want unchanged %q", cfg.ContainerEngineURL, want) + } +} + +func TestSetConfigAppliesContainerEngineBeforeURL(t *testing.T) { + tmpFile, err := os.CreateTemp("", "setconfig-ce-before-cu-*.yaml") + if err != nil { + t.Fatalf("create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + edgeletURL := constants.EdgeletEngineSocketURL() + dockerURL := "unix:///var/run/docker.sock" + testConfig := `currentProfile: default +profiles: + default: + containerEngine: edgelet + containerEngineUrl: "` + edgeletURL + `" +` + if _, err := tmpFile.WriteString(testConfig); err != nil { + t.Fatalf("write temp config: %v", err) + } + _ = tmpFile.Close() + + if err := LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + cfg := GetInstance() + // Map iteration order is random; ce must still be applied before cu. + errors := cfg.SetConfig(map[string]any{ + "cu": dockerURL, + "ce": constants.EngineDocker, + }) + if len(errors) > 0 { + t.Fatalf("expected engine+url batch to succeed, got: %+v", errors) + } + if cfg.ContainerEngine != constants.EngineDocker { + t.Fatalf("ContainerEngine=%q want docker", cfg.ContainerEngine) + } + if cfg.ContainerEngineURL != dockerURL { + t.Fatalf("ContainerEngineURL=%q want %q", cfg.ContainerEngineURL, dockerURL) + } +} diff --git a/internal/config/validator.go b/internal/config/validator.go index 68ac4e3..605eaec 100644 --- a/internal/config/validator.go +++ b/internal/config/validator.go @@ -31,7 +31,7 @@ func ValidateConfig(cfg *Config) error { } // Validate log disk limit - if cfg.LogDiskLimit < 0.5 || cfg.LogDiskLimit > utils.MaxDiskConsumptionLimit { + if cfg.LogLimit < 0.5 || cfg.LogLimit > utils.MaxDiskConsumptionLimit { errors = append(errors, fmt.Sprintf("log disk limit must be between 0.5 and %.1f GB", utils.MaxDiskConsumptionLimit)) } diff --git a/internal/constants/engine.go b/internal/constants/engine.go index 4e38db0..3c39722 100644 --- a/internal/constants/engine.go +++ b/internal/constants/engine.go @@ -11,7 +11,7 @@ const ( // // /var/lib/edgelet/ — user data (diskDirectory config field) // -// diskConsumptionLimit applies only here +// diskLimit applies only here // // /var/lib/edgelet-containerd/ — containerd images, snapshots, layers // @@ -24,7 +24,7 @@ const ( EdgeletDataDir = "/var/lib/edgelet" // Containerd persistent data root — separate from EdgeletDataDir so that - // image layers and snapshots are not counted by diskConsumptionLimit. + // image layers and snapshots are not counted by diskLimit. EdgeletContainerdLibDir = "/var/lib/edgelet-containerd" EdgeletContainerdRootDir = "/var/lib/edgelet-containerd/root" EdgeletContainerdStateDir = "/var/lib/edgelet-containerd/state" diff --git a/internal/edgeletapi/handlers/api.go b/internal/edgeletapi/handlers/api.go index a936873..df3b7e1 100644 --- a/internal/edgeletapi/handlers/api.go +++ b/internal/edgeletapi/handlers/api.go @@ -336,7 +336,7 @@ func (h *EdgeletAPIHandler) HandleSystemLogs(w http.ResponseWriter, r *http.Requ reader := utils.NewLocalLogReader( fmt.Sprintf("edgeletapi-system-logs-%d", time.Now().UnixNano()), iofogUUID, - cfg.LogDiskDirectory, + cfg.LogDirectory, &utils.TailConfig{ Follow: false, Lines: tailLines, @@ -739,8 +739,8 @@ func (h *EdgeletAPIHandler) HandleConfig(w http.ResponseWriter, r *http.Request) "diskDirectory": cfg.DiskDirectory, "memoryLimitMiB": cfg.MemoryLimit, "cpuLimitPercent": cfg.CPULimit, - "logDiskLimitGiB": cfg.LogDiskLimit, - "logDiskDirectory": cfg.LogDiskDirectory, + "logLimitGiB": cfg.LogLimit, + "logDirectory": cfg.LogDirectory, "logFileCount": cfg.LogFileCount, "logLevel": cfg.LogLevel, "statusFrequencySeconds": cfg.StatusFrequency, @@ -2319,7 +2319,7 @@ func (h *EdgeletAPIHandler) handleSystemLogsStreamWS(w http.ResponseWriter, r *h reader := utils.NewLocalLogReader( fmt.Sprintf("edgeletapi-system-log-stream-%d", time.Now().UnixNano()), iofogUUID, - cfg.LogDiskDirectory, + cfg.LogDirectory, &utils.TailConfig{ Follow: true, Lines: tailLines, @@ -2488,9 +2488,9 @@ func configKeyToShortCode(key string) (string, bool) { return "m", true case "p", "cpuLimitPercent": return "p", true - case "l", "logDiskLimitGiB": + case "l", "logLimitGiB": return "l", true - case "ld", "logDiskDirectory": + case "ld", "logDirectory": return "ld", true case "lc", "logFileCount": return "lc", true diff --git a/internal/edgeletapi/handlers/api_cli_endpoints_test.go b/internal/edgeletapi/handlers/api_cli_endpoints_test.go index 6970dba..0d78f57 100644 --- a/internal/edgeletapi/handlers/api_cli_endpoints_test.go +++ b/internal/edgeletapi/handlers/api_cli_endpoints_test.go @@ -181,7 +181,7 @@ func TestHandleSystemPrune_RejectsInvalidMode(t *testing.T) { func TestHandleSystemLogs_RejectsInvalidTail(t *testing.T) { cfg := setupConfigForGPSTests(t) - cfg.LogDiskDirectory = t.TempDir() + string(os.PathSeparator) + cfg.LogDirectory = t.TempDir() + string(os.PathSeparator) handler := NewEdgeletAPIHandler() req := httptest.NewRequest(http.MethodGet, "/v1/system/logs?tailLines=bad", nil) @@ -195,8 +195,8 @@ func TestHandleSystemLogs_RejectsInvalidTail(t *testing.T) { func TestHandleSystemLogs_BoundedReturnsEntries(t *testing.T) { cfg := setupConfigForGPSTests(t) - cfg.LogDiskDirectory = t.TempDir() + string(os.PathSeparator) - logFile := filepath.Join(cfg.LogDiskDirectory, "edgelet.0.log") + cfg.LogDirectory = t.TempDir() + string(os.PathSeparator) + logFile := filepath.Join(cfg.LogDirectory, "edgelet.0.log") logLines := strings.Join([]string{ "2026-05-17 00:00:01.000 [info] boot", "2026-05-17 00:00:02.000 [info] ready", diff --git a/internal/edgeletapi/handlers/api_logs_stream_test.go b/internal/edgeletapi/handlers/api_logs_stream_test.go index db1e4d7..1f636e2 100644 --- a/internal/edgeletapi/handlers/api_logs_stream_test.go +++ b/internal/edgeletapi/handlers/api_logs_stream_test.go @@ -143,8 +143,8 @@ func TestHandleLogsStreamWS_StreamingErrorReturnsErrorEvent(t *testing.T) { func TestHandleSystemLogsStreamWS_StreamsDaemonLogs(t *testing.T) { cfg := setupConfigForGPSTests(t) - cfg.LogDiskDirectory = t.TempDir() + string(os.PathSeparator) - logFile := filepath.Join(cfg.LogDiskDirectory, "edgelet.0.log") + cfg.LogDirectory = t.TempDir() + string(os.PathSeparator) + logFile := filepath.Join(cfg.LogDirectory, "edgelet.0.log") if err := os.WriteFile(logFile, []byte("2026-05-17 00:00:01.000 [info] boot\n"), 0o600); err != nil { t.Fatalf("failed to write log file: %v", err) } diff --git a/internal/fieldagent/changes.go b/internal/fieldagent/changes.go index 72febe6..88ba29a 100644 --- a/internal/fieldagent/changes.go +++ b/internal/fieldagent/changes.go @@ -356,36 +356,36 @@ func (fa *FieldAgent) getFogConfig() error { // Map controller config keys to agent config keys (short codes) configMap := make(map[string]any) - // Mapping from controller JSON keys to internal short codes + // Mapping from controller JSON keys to internal short codes (Pot agent-service.js). keyMapping := map[string]string{ - "diskConsumptionLimit": "d", - "diskDirectory": "dl", - "memoryConsumptionLimit": "m", - "processorConsumptionLimit": "p", - "controllerUrl": "a", - "controllerCert": "ac", - "containerEngineUrl": "cu", - "containerEngine": "ce", - "networkInterface": "n", - "logDiskConsumptionLimit": "l", - "logDiskDirectory": "ld", - "logFileCount": "lc", - "logLevel": "ll", - "statusFrequency": "sf", - "changeFrequency": "cf", - "deviceScanFrequency": "sd", - "watchdogEnabled": "wd", - "edgeGuardFrequency": "egf", - "gpsMode": "gps", - "gpsDevice": "gpsd", - "gpsScanFrequency": "gpsf", - "arch": "ft", - "secureMode": "sec", - "pruningFrequency": "pf", - "availableDiskThreshold": "dt", - "upgradeScanFrequency": "uf", - "devMode": "dev", - "timeZone": "tz", + "diskLimit": "d", + "diskDirectory": "dl", + "memoryLimit": "m", + "cpuLimit": "p", + "controllerUrl": "a", + "controllerCert": "ac", + "containerEngineUrl": "cu", + "containerEngine": "ce", + "networkInterface": "n", + "logLimit": "l", + "logDirectory": "ld", + "logFileCount": "lc", + "logLevel": "ll", + "statusFrequency": "sf", + "changeFrequency": "cf", + "deviceScanFrequency": "sd", + "watchdogEnabled": "wd", + "edgeGuardFrequency": "egf", + "gpsMode": "gps", + "gpsDevice": "gpsd", + "gpsScanFrequency": "gpsf", + "arch": "ft", + "secureMode": "sec", + "pruningFrequency": "pf", + "availableDiskThreshold": "dt", + "upgradeScanFrequency": "uf", + "devMode": "dev", + "timeZone": "tz", } for k, v := range configs { @@ -394,17 +394,20 @@ func (fa *FieldAgent) getFogConfig() error { } } - // Apply configuration - if len(configMap) > 0 { - logging.LogDebug(moduleName, fmt.Sprintf("Applying config changes: %+v", configMap)) - cfg := config.GetInstance() - errorMap := cfg.SetConfig(configMap) + // Apply only keys that differ from local config (controller sends full snapshot). + cfg := config.GetInstance() + changedMap := cfg.FilterChangedConfigKeys(configMap) + if len(changedMap) > 0 { + logging.LogDebug(moduleName, fmt.Sprintf("Applying config changes: %+v", changedMap)) + errorMap := cfg.SetConfig(changedMap) if len(errorMap) > 0 { logging.LogError(moduleName, fmt.Sprintf("Errors applying config: %+v", errorMap), nil) } else { logging.LogInfo(moduleName, "Configuration applied successfully") } + } else if len(configMap) > 0 { + logging.LogDebug(moduleName, "Controller config unchanged; skipping apply") } else { logging.LogDebug(moduleName, "No matching config fields found to apply") } @@ -449,29 +452,29 @@ func (fa *FieldAgent) postFogConfig() error { // Build config data configData := map[string]any{ - "networkInterface": networkInterfaceName, - "containerEngineUrl": cfg.ContainerEngineURL, - "diskConsumptionLimit": cfg.DiskLimit, - "diskDirectory": cfg.DiskDirectory, - "memoryConsumptionLimit": cfg.MemoryLimit, - "processorConsumptionLimit": cfg.CPULimit, - "logDiskConsumptionLimit": cfg.LogDiskLimit, - "logDiskDirectory": cfg.LogDiskDirectory, - "logFileCount": cfg.LogFileCount, - "statusFrequency": cfg.StatusFrequency, - "changeFrequency": cfg.ChangeFrequency, - "deviceScanFrequency": cfg.DeviceScanFrequency, - "watchdogEnabled": cfg.WatchdogEnabled, - "edgeGuardFrequency": cfg.EdgeGuardFrequency, - "gpsDevice": cfg.GPSDevice, - "gpsScanFrequency": cfg.GPSScanFrequency, - "gpsMode": strings.ToLower(cfg.GPSMode), - "latitude": latitude, - "longitude": longitude, - "logLevel": strings.ToUpper(cfg.LogLevel), - "availableDiskThreshold": cfg.AvailableDiskThreshold, - "pruningFrequency": cfg.PruningFrequency, - "upgradeScanFrequency": cfg.UpgradeScanFrequency, + "networkInterface": networkInterfaceName, + "containerEngineUrl": cfg.ContainerEngineURL, + "diskLimit": cfg.DiskLimit, + "diskDirectory": cfg.DiskDirectory, + "memoryLimit": cfg.MemoryLimit, + "cpuLimit": cfg.CPULimit, + "logLimit": cfg.LogLimit, + "logDirectory": cfg.LogDirectory, + "logFileCount": cfg.LogFileCount, + "statusFrequency": cfg.StatusFrequency, + "changeFrequency": cfg.ChangeFrequency, + "deviceScanFrequency": cfg.DeviceScanFrequency, + "watchdogEnabled": cfg.WatchdogEnabled, + "edgeGuardFrequency": cfg.EdgeGuardFrequency, + "gpsDevice": cfg.GPSDevice, + "gpsScanFrequency": cfg.GPSScanFrequency, + "gpsMode": strings.ToLower(cfg.GPSMode), + "latitude": latitude, + "longitude": longitude, + "logLevel": strings.ToUpper(cfg.LogLevel), + "availableDiskThreshold": cfg.AvailableDiskThreshold, + "pruningFrequency": cfg.PruningFrequency, + "upgradeScanFrequency": cfg.UpgradeScanFrequency, } // Use context from FieldAgent (daemon mode) or create new one (CLI mode) diff --git a/internal/fieldagent/changes_config_wire_test.go b/internal/fieldagent/changes_config_wire_test.go new file mode 100644 index 0000000..1d87664 --- /dev/null +++ b/internal/fieldagent/changes_config_wire_test.go @@ -0,0 +1,286 @@ +package fieldagent + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "strings" + "sync/atomic" + "testing" + + "github.com/eclipse-iofog/edgelet/internal/auth" + "github.com/eclipse-iofog/edgelet/internal/config" + "github.com/eclipse-iofog/edgelet/internal/models" +) + +func TestPostFogConfig_UsesControllerConfigKeys(t *testing.T) { + cfg := config.GetInstance() + cfg.IOFogUUID = "agent-1" + cfg.NetworkInterface = "eth0" + cfg.ContainerEngineURL = "unix:///var/run/docker.sock" + cfg.DiskLimit = 500 + cfg.DiskDirectory = "/var/lib/edgelet/" + cfg.MemoryLimit = 8192 + cfg.CPULimit = 90 + cfg.LogLimit = 25 + cfg.LogDirectory = "/var/log/edgelet/" + cfg.LogFileCount = 10 + cfg.StatusFrequency = 10 + cfg.ChangeFrequency = 20 + cfg.DeviceScanFrequency = 60 + cfg.LogLevel = "INFO" + + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch || r.URL.Path != "/agent/config" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode body: %v", err) + } + w.WriteHeader(http.StatusNoContent) + })) + t.Cleanup(server.Close) + + fa := &FieldAgent{ + config: cfg, + state: NewState(), + ctx: context.Background(), + apiClient: &APIClient{ + baseURL: server.URL, + httpClient: server.Client(), + jwtManager: auth.GetJWTManager(), + }, + } + fa.state.SetControllerStatus(models.ControllerStatusOK) + fa.state.SetControllerVerified(true) + + if err := fa.postFogConfig(); err != nil { + t.Fatalf("postFogConfig: %v", err) + } + + assertConfigKeyFloat(t, body, "diskLimit", 500) + assertConfigKeyFloat(t, body, "memoryLimit", 8192) + assertConfigKeyFloat(t, body, "cpuLimit", 90) + assertConfigKeyFloat(t, body, "logLimit", 25) + if got, ok := body["logDirectory"].(string); !ok || got != "/var/log/edgelet/" { + t.Fatalf("logDirectory=%v want /var/log/edgelet/", body["logDirectory"]) + } + for _, legacy := range []string{ + "diskConsumptionLimit", + "memoryConsumptionLimit", + "processorConsumptionLimit", + "logDiskConsumptionLimit", + "logDiskDirectory", + } { + if _, ok := body[legacy]; ok { + t.Fatalf("legacy key %q should not be sent", legacy) + } + } +} + +func TestGetFogConfig_AppliesControllerConfigKeys(t *testing.T) { + tmpFile, err := os.CreateTemp("", "edgelet-config-wire-*.yaml") + if err != nil { + t.Fatalf("create temp config: %v", err) + } + t.Cleanup(func() { _ = os.Remove(tmpFile.Name()) }) + + content := `currentProfile: default +profiles: + default: + diskLimit: "10" + memoryLimit: "4096" + cpuLimit: "80" + logLevel: "INFO" +` + if _, err := tmpFile.WriteString(content); err != nil { + t.Fatalf("write temp config: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("close temp config: %v", err) + } + if err := config.LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + var getCount int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, "/agent/config") { + http.NotFound(w, r) + return + } + if r.Method == http.MethodGet { + atomic.AddInt32(&getCount, 1) + _ = json.NewEncoder(w).Encode(map[string]any{ + "diskLimit": 500, + "memoryLimit": 8192, + "cpuLimit": 90, + "logLimit": 25, + "logDirectory": "/var/log/custom/", + }) + return + } + http.NotFound(w, r) + })) + t.Cleanup(server.Close) + + cfg := config.GetInstance() + origURL := cfg.ControllerURL + origUUID := cfg.IOFogUUID + cfg.ControllerURL = server.URL + cfg.IOFogUUID = "agent-wire-test" + t.Cleanup(func() { + cfg.ControllerURL = origURL + cfg.IOFogUUID = origUUID + }) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + fa := &FieldAgent{ + config: cfg, + state: NewState(), + ctx: ctx, + apiClient: &APIClient{ + baseURL: server.URL, + httpClient: server.Client(), + jwtManager: auth.GetJWTManager(), + }, + } + fa.state.SetControllerStatus(models.ControllerStatusOK) + fa.state.SetControllerVerified(true) + fa.state.SetInitialization(false) + + if err := fa.getFogConfig(); err != nil { + t.Fatalf("getFogConfig: %v", err) + } + if got := atomic.LoadInt32(&getCount); got != 1 { + t.Fatalf("expected one GET config, got %d", got) + } + if cfg.DiskLimit != 500 { + t.Fatalf("DiskLimit=%v want 500", cfg.DiskLimit) + } + if cfg.MemoryLimit != 8192 { + t.Fatalf("MemoryLimit=%v want 8192", cfg.MemoryLimit) + } + if cfg.CPULimit != 90 { + t.Fatalf("CPULimit=%v want 90", cfg.CPULimit) + } + if cfg.LogLimit != 25 { + t.Fatalf("LogLimit=%v want 25", cfg.LogLimit) + } + if cfg.LogDirectory != "/var/log/custom/" { + t.Fatalf("LogDirectory=%q want /var/log/custom/", cfg.LogDirectory) + } +} + +func TestGetFogConfig_SkipsUnchangedControllerConfig(t *testing.T) { + tmpFile, err := os.CreateTemp("", "edgelet-config-unchanged-*.yaml") + if err != nil { + t.Fatalf("create temp config: %v", err) + } + t.Cleanup(func() { _ = os.Remove(tmpFile.Name()) }) + + content := `currentProfile: default +profiles: + default: + diskLimit: "500" + memoryLimit: "8192" + cpuLimit: "90" + logLimit: "25" + logDirectory: "/var/log/custom/" + containerEngine: edgelet + containerEngineUrl: "` + config.DefaultContainerEngineURLForEngine("edgelet") + `" +` + if _, err := tmpFile.WriteString(content); err != nil { + t.Fatalf("write temp config: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("close temp config: %v", err) + } + if err := config.LoadConfig(tmpFile.Name()); err != nil { + t.Fatalf("load config: %v", err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, "/agent/config") { + http.NotFound(w, r) + return + } + if r.Method == http.MethodGet { + _ = json.NewEncoder(w).Encode(map[string]any{ + "diskLimit": 500, + "memoryLimit": 8192, + "cpuLimit": 90, + "logLimit": 25, + "logDirectory": "/var/log/custom/", + "containerEngineUrl": config.DefaultContainerEngineURLForEngine("edgelet"), + "changeFrequency": 20, + }) + return + } + http.NotFound(w, r) + })) + t.Cleanup(server.Close) + + cfg := config.GetInstance() + origURL := cfg.ControllerURL + origUUID := cfg.IOFogUUID + cfg.ControllerURL = server.URL + cfg.IOFogUUID = "agent-wire-unchanged" + t.Cleanup(func() { + cfg.ControllerURL = origURL + cfg.IOFogUUID = origUUID + }) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + fa := &FieldAgent{ + config: cfg, + state: NewState(), + ctx: ctx, + apiClient: &APIClient{ + baseURL: server.URL, + httpClient: server.Client(), + jwtManager: auth.GetJWTManager(), + }, + } + fa.state.SetControllerStatus(models.ControllerStatusOK) + fa.state.SetControllerVerified(true) + fa.state.SetInitialization(false) + + if err := fa.getFogConfig(); err != nil { + t.Fatalf("getFogConfig: %v", err) + } + if cfg.ChangeFrequency != 20 { + t.Fatalf("ChangeFrequency=%d want unchanged 20", cfg.ChangeFrequency) + } +} + +func assertConfigKeyFloat(t *testing.T, body map[string]any, key string, want float64) { + t.Helper() + raw, ok := body[key] + if !ok { + t.Fatalf("missing key %q in body: %#v", key, body) + } + switch v := raw.(type) { + case float64: + if v != want { + t.Fatalf("%s=%v want %v", key, v, want) + } + case json.Number: + f, err := v.Float64() + if err != nil { + t.Fatalf("%s number parse: %v", key, err) + } + if f != want { + t.Fatalf("%s=%v want %v", key, f, want) + } + default: + t.Fatalf("%s has unexpected type %T", key, raw) + } +} diff --git a/internal/fieldagent/log_session_manager.go b/internal/fieldagent/log_session_manager.go index 328043e..0f688db 100644 --- a/internal/fieldagent/log_session_manager.go +++ b/internal/fieldagent/log_session_manager.go @@ -523,7 +523,7 @@ func (lsm *LogSessionManager) startFogLogStreamingLocked(sessionID, iofogUUID st // Get log directory from config cfg := config.GetInstance() - logDir := cfg.LogDiskDirectory + logDir := cfg.LogDirectory // Create and start LocalLogReader reader := utils.NewLocalLogReader(sessionID, iofogUUID, logDir, tailConfig, handler) diff --git a/internal/supervisor/engine_lifecycle.go b/internal/supervisor/engine_lifecycle.go index 9b6e73e..7fbf761 100644 --- a/internal/supervisor/engine_lifecycle.go +++ b/internal/supervisor/engine_lifecycle.go @@ -91,7 +91,7 @@ func (s *Supervisor) handleWarmContainerEngineURLReload(reloadCtx *reloadEngineC engConfig := engine.EngineConfig{ SocketURL: cfg.ContainerEngineURL, APIVersion: cfg.DockerAPIVersion, - LogDir: cfg.LogDiskDirectory + "containers", + LogDir: cfg.LogDirectory + "containers", } newEng, err := initExternalEngineAttempt(cfg.ContainerEngine, engConfig) @@ -163,7 +163,7 @@ func (s *Supervisor) liveExternalEngineConfig() engine.EngineConfig { return engine.EngineConfig{ SocketURL: cfg.ContainerEngineURL, APIVersion: cfg.DockerAPIVersion, - LogDir: cfg.LogDiskDirectory + "containers", + LogDir: cfg.LogDirectory + "containers", } } diff --git a/internal/supervisor/supervisor.go b/internal/supervisor/supervisor.go index 8ba0488..f9e53aa 100644 --- a/internal/supervisor/supervisor.go +++ b/internal/supervisor/supervisor.go @@ -186,7 +186,7 @@ func (s *Supervisor) Start() error { engConfig := engine.EngineConfig{ SocketURL: cfg.ContainerEngineURL, APIVersion: cfg.DockerAPIVersion, - LogDir: cfg.LogDiskDirectory + "containers", + LogDir: cfg.LogDirectory + "containers", } var eng engine.ContainerEngine diff --git a/internal/utils/constants.go b/internal/utils/constants.go index 266ba2b..62a74b5 100644 --- a/internal/utils/constants.go +++ b/internal/utils/constants.go @@ -132,7 +132,7 @@ const ( // Limits const ( - MaxDiskConsumptionLimit = 100.0 + MaxDiskConsumptionLimit = 100000.0 PercentageCompletion = 100.0 ) diff --git a/packaging/edgelet/etc/edgelet/config.default.yaml b/packaging/edgelet/etc/edgelet/config.default.yaml index 502f05d..f9ebded 100644 --- a/packaging/edgelet/etc/edgelet/config.default.yaml +++ b/packaging/edgelet/etc/edgelet/config.default.yaml @@ -11,5 +11,5 @@ profiles: watchdogEnabled: "off" pruningFrequency: "0" diskDirectory: /var/lib/edgelet/ - logDiskDirectory: /var/log/edgelet/ + logDirectory: /var/log/edgelet/ logLevel: INFO diff --git a/packaging/edgelet/etc/edgelet/config_new.yaml b/packaging/edgelet/etc/edgelet/config_new.yaml index cb8860a..917f230 100644 --- a/packaging/edgelet/etc/edgelet/config_new.yaml +++ b/packaging/edgelet/etc/edgelet/config_new.yaml @@ -33,15 +33,15 @@ profiles: # containerEngineUrl is ignored; the daemon always connects to its private # socket at /run/edgelet/containerd.sock. # Persistent data lives in /var/lib/edgelet-containerd/ and - # is NOT counted against diskDirectory/diskConsumptionLimit. + # is NOT counted against diskDirectory/diskLimit. containerEngine: "edgelet" containerEngineUrl: "unix:///run/edgelet/containerd.sock" - diskConsumptionLimit: "10" + diskLimit: "10" diskDirectory: "/var/lib/edgelet/" - memoryConsumptionLimit: "4096" - processorConsumptionLimit: "80.0" - logDiskConsumptionLimit: "10.0" - logDiskDirectory: "/var/log/edgelet/" + memoryLimit: "4096" + cpuLimit: "80.0" + logLimit: "10.0" + logDirectory: "/var/log/edgelet/" logFileCount: "10" logLevel: "INFO" statusFrequency: "10" @@ -52,7 +52,7 @@ profiles: gpsDevice: "" gpsScanFrequency: "60" watchdogEnabled: "off" - edgeGuardFreq: "0" + edgeGuardFrequency: "0" pruningFrequency: "0" availableDiskThreshold: "20" upgradeScanFrequency: "24" @@ -78,15 +78,15 @@ profiles: # containerEngineUrl is ignored; the daemon always connects to its private # socket at /run/edgelet/containerd.sock. # Persistent data lives in /var/lib/edgelet-containerd/ and - # is NOT counted against diskDirectory/diskConsumptionLimit. + # is NOT counted against diskDirectory/diskLimit. containerEngine: "edgelet" containerEngineUrl: "unix:///run/edgelet/containerd.sock" - diskConsumptionLimit: "10" + diskLimit: "10" diskDirectory: "/var/lib/edgelet/" - memoryConsumptionLimit: "4096" - processorConsumptionLimit: "80.0" - logDiskConsumptionLimit: "10.0" - logDiskDirectory: "/var/log/edgelet/" + memoryLimit: "4096" + cpuLimit: "80.0" + logLimit: "10.0" + logDirectory: "/var/log/edgelet/" logFileCount: "10" logLevel: "INFO" statusFrequency: "10" @@ -97,7 +97,7 @@ profiles: gpsDevice: "" gpsScanFrequency: "60" watchdogEnabled: "off" - edgeGuardFreq: "0" + edgeGuardFrequency: "0" pruningFrequency: "0" availableDiskThreshold: "20" upgradeScanFrequency: "24" @@ -123,15 +123,15 @@ profiles: # containerEngineUrl is ignored; the daemon always connects to its private # socket at /run/edgelet/containerd.sock. # Persistent data lives in /var/lib/edgelet-containerd/ and - # is NOT counted against diskDirectory/diskConsumptionLimit. + # is NOT counted against diskDirectory/diskLimit. containerEngine: "edgelet" containerEngineUrl: "unix:///run/edgelet/containerd.sock" - diskConsumptionLimit: "10" + diskLimit: "10" diskDirectory: "/var/lib/edgelet/" - memoryConsumptionLimit: "4096" - processorConsumptionLimit: "80.0" - logDiskConsumptionLimit: "10.0" - logDiskDirectory: "/var/log/edgelet/" + memoryLimit: "4096" + cpuLimit: "80.0" + logLimit: "10.0" + logDirectory: "/var/log/edgelet/" logFileCount: "10" logLevel: "INFO" statusFrequency: "10" @@ -142,7 +142,7 @@ profiles: gpsDevice: "" gpsScanFrequency: "60" watchdogEnabled: "off" - edgeGuardFreq: "0" + edgeGuardFrequency: "0" pruningFrequency: "0" availableDiskThreshold: "20" upgradeScanFrequency: "24"