From 5240b77308da4d04e074ecea643833f53cd7521e Mon Sep 17 00:00:00 2001 From: Rashika Rajaraman Date: Wed, 6 May 2026 23:22:30 -0700 Subject: [PATCH 01/11] Add logic to resolve reserved cpu policy --- cluster-agent/internal/state/cpuPolicy.go | 242 ++++++++++++++++++ .../internal/state/installInProgress.go | 8 +- 2 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 cluster-agent/internal/state/cpuPolicy.go diff --git a/cluster-agent/internal/state/cpuPolicy.go b/cluster-agent/internal/state/cpuPolicy.go new file mode 100644 index 00000000..bf270141 --- /dev/null +++ b/cluster-agent/internal/state/cpuPolicy.go @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +package state + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/open-edge-platform/edge-node-agents/common/pkg/utils" +) + +const bitSize = 32 + +var reservedCPUsPattern = regexp.MustCompile(`--reserved-cpus[=\s]+["']?([^"'\s]+)["']?`) + +type CPU struct { + Sockets uint32 + Topology *CPUTopology +} + +type CPUTopology struct { + Sockets []*Socket +} + +type Socket struct { + SocketID uint32 + CoreGroups []*CoreGroup +} + +type CoreGroup struct { + Type string + List []uint32 +} + +type cores struct { + CPU string + Socket string + MaxMhz string +} + +// ResolveReservedCPUPolicy processes the install command and resolves reserved CPU policy keywords. +// If reserved-cpus is a literal value (e.g., "0-1", "16-31"), it passes through unchanged. +// If it's a policy keyword (auto, auto:pcore, auto:ecore), it computes the actual CPU set +// based on local system topology. +func ResolveReservedCPUPolicy(installCmd string) (string, error) { + matches := reservedCPUsPattern.FindStringSubmatch(installCmd) + if len(matches) < 2 { + return installCmd, nil + } + + reservedValue := matches[1] + if !isCPUPolicy(reservedValue) { + log.Debugf("reserved-cpus value %q is literal, passing through unchanged", reservedValue) + return installCmd, nil + } + + cpu, err := getCPUList() + if err != nil { + return "", fmt.Errorf("failed to get CPU information: %w", err) + } + if cpu == nil || cpu.Topology == nil || len(cpu.Topology.Sockets) == 0 { + return "", fmt.Errorf("invalid or empty CPU topology detected") + } + + var firstPCoreList, eCoreList []uint32 + for _, socket := range cpu.Topology.Sockets { + for _, coreGroup := range socket.CoreGroups { + if coreGroup.Type == "P-Core" && len(firstPCoreList) == 0 { + firstPCoreList = coreGroup.List + } else if coreGroup.Type == "E-Core" { + eCoreList = append(eCoreList, coreGroup.List...) + } + } + } + + var cpuSet string + switch reservedValue { + case "auto", "auto:pcore": + if len(firstPCoreList) == 0 { + return "", fmt.Errorf("no P-cores detected in CPU topology") + } + cpuSet = formatCPUSet(firstPCoreList) + case "auto:ecore": + if len(eCoreList) > 0 { + cpuSet = formatCPUSet(eCoreList) + } else if len(firstPCoreList) > 0 { + log.Info("No E-cores detected, falling back to P-core reservation for auto:ecore policy") + cpuSet = formatCPUSet(firstPCoreList) + } else { + return "", fmt.Errorf("no P-cores detected in CPU topology") + } + } + + oldFlag := matches[0] + newFlag := fmt.Sprintf(`--reserved-cpus="%s"`, cpuSet) + modifiedCmd := strings.Replace(installCmd, oldFlag, newFlag, 1) + + log.Infof("Resolved reserved CPU policy %q to %q", reservedValue, cpuSet) + return modifiedCmd, nil +} + +// isCPUPolicy reports whether the given value is a recognized CPU policy keyword. +func isCPUPolicy(value string) bool { + return value == "auto" || value == "auto:pcore" || value == "auto:ecore" +} + +// formatCPUSet converts a list of CPU IDs to kernel-acceptable range format. +// E.g., [0, 1, 2, 3] -> "0-3", [5] -> "5" +func formatCPUSet(cpuList []uint32) string { + if len(cpuList) == 0 { + return "" + } + first, last := cpuList[0], cpuList[len(cpuList)-1] + if first == last { + return fmt.Sprintf("%d", first) + } + return fmt.Sprintf("%d-%d", first, last) +} + +// getCPUList collects CPU information from `lscpu` and processes the result to generate structured CPU data +// It returns with a CPU struct. +// This reuses the same logic as hardware-discovery-agent/internal/cpu/cpu.go +func getCPUList() (*CPU, error) { + dataBytes, err := utils.ReadFromCommand(nil, "lscpu") + if err != nil { + return &CPU{}, fmt.Errorf("failed to read data from command; error: %w", err) + } + + lscpu := strings.Split(string(dataBytes), "\n") + var cpu CPU + var maxMhz string + for _, attribute := range lscpu { + attr := strings.TrimSpace(attribute) + if strings.HasPrefix(attr, "Socket(s)") { + socketStr := strings.TrimSpace(strings.TrimPrefix(attr, "Socket(s):")) + sockets, err := strconv.ParseUint(socketStr, 10, bitSize) + if err != nil { + continue + } + cpu.Sockets = uint32(sockets) + } + if strings.HasPrefix(attr, "CPU max MHz:") { + maxMhz = strings.TrimSpace(strings.TrimPrefix(attr, "CPU max MHz:")) + } + } + + // If the number of sockets has been retrieved using the `lscpu` command + // above, we can determine how many P-Cores and E-Cores are enabled on the Edge Node. + if cpu.Sockets != 0 { + coreInfo := []*cores{} + coreDetails, err := utils.ReadFromCommand(nil, "lscpu", "--extended=CPU,SOCKET,MAXMHZ") + if err != nil { + return &cpu, fmt.Errorf("failed to read data from command; error: %w", err) + } + parseCoreDetails := strings.SplitAfter(string(coreDetails), "\n") + + for _, coreData := range parseCoreDetails { + if strings.Contains(coreData, "CPU") || coreData == "" { + continue + } + var core cores + coreValues := strings.Fields(coreData) + core.CPU = coreValues[0] + core.Socket = coreValues[1] + coreMaxMhz := strings.Split(coreValues[2], "\n") + core.MaxMhz = coreMaxMhz[0] + coreInfo = append(coreInfo, &core) + } + + cpuTopology := inferEPCores(cpu.Sockets, coreInfo, maxMhz) + cpu.Topology = cpuTopology + } + + return &cpu, nil +} + +// inferEPCores classifies cores as P-Core or E-Core based on frequency heuristic. +func inferEPCores(sockets uint32, coreInfo []*cores, coreMaxFreq string) *CPUTopology { + socketInfo := []*Socket{} + for socketID := uint32(0); socketID < sockets; socketID++ { + // Determine a target max frequency for E Core detection based on the max frequency from lscpu. + maxCoreFreq, err := strconv.ParseUint(strings.TrimSuffix(coreMaxFreq, ".0000"), 10, 64) + if err != nil { + // If max frequency cannot be retrieved from lscpu, default to 0 so that all cores are considered P Cores. + maxCoreFreq = 0 + } + eCoreTargetFreq := (3 * maxCoreFreq) / 4 + socketDetails := getCoreGroupsPerSocket(socketID, coreInfo, eCoreTargetFreq) + socketInfo = append(socketInfo, socketDetails) + } + return &CPUTopology{Sockets: socketInfo} +} + +// getCoreGroupsPerSocket groups cores by type (P-Core vs E-Core) for a single socket +func getCoreGroupsPerSocket(socketID uint32, coreInfo []*cores, coreMaxFreq uint64) *Socket { + pCoreList := make([]uint32, 0) + eCoreList := make([]uint32, 0) + + for _, core := range coreInfo { + socket, err := strconv.ParseUint(core.Socket, 10, bitSize) + if err != nil { + socket = 0 + } + if socket == uint64(socketID) { + cpu, err := strconv.ParseUint(core.CPU, 10, bitSize) + if err != nil { + continue + } + if core.MaxMhz == "-" { + // If max frequency is not found, default to P Core for cpu ID and continue + pCoreList = append(pCoreList, uint32(cpu)) + continue + } + coreFreq, err := strconv.ParseUint(strings.TrimSuffix(core.MaxMhz, ".0000"), 10, 64) + if err != nil { + continue + } + if coreFreq <= coreMaxFreq { + eCoreList = append(eCoreList, uint32(cpu)) + } else { + pCoreList = append(pCoreList, uint32(cpu)) + } + } + } + + coreGroups := []*CoreGroup{} + coreGroups = append(coreGroups, &CoreGroup{ + Type: "P-Core", + List: pCoreList, + }) + if len(eCoreList) > 0 { + coreGroups = append(coreGroups, &CoreGroup{ + Type: "E-Core", + List: eCoreList, + }) + } + + return &Socket{SocketID: socketID, CoreGroups: coreGroups} +} diff --git a/cluster-agent/internal/state/installInProgress.go b/cluster-agent/internal/state/installInProgress.go index 3997d84a..fa33eabd 100644 --- a/cluster-agent/internal/state/installInProgress.go +++ b/cluster-agent/internal/state/installInProgress.go @@ -13,7 +13,13 @@ type InstallInProgress struct { func (s *InstallInProgress) Register() error { log.Info("Start kubernetes engine installation script") - err := s.execute(s.sm.ctx, s.sm.installCmd) + resolvedCmd, err := ResolveReservedCPUPolicy(s.sm.installCmd) + if err != nil { + log.Warnf("Failed to resolve reserved CPU policy, proceeding with original command: %v", err) + resolvedCmd = s.sm.installCmd + } + + err = s.execute(s.sm.ctx, resolvedCmd) if err != nil { s.sm.set(s.sm.inactive) return err From 0d12d0353816475c4f948244348b3188541a15b7 Mon Sep 17 00:00:00 2001 From: Andrei Palade Date: Fri, 8 May 2026 08:53:41 +0100 Subject: [PATCH 02/11] Extend node-agent with cluster detection (#670) --- ena-manifest.yaml | 4 +- node-agent/CLUSTER_DETECTION.md | 62 ++ node-agent/README.md | 3 +- node-agent/VERSION | 2 +- node-agent/cmd/node-agent/node-agent.go | 91 +++ node-agent/cmd/node-agent/node-agent_test.go | 3 +- node-agent/configs/node-agent.yaml | 8 + node-agent/go.mod | 2 + node-agent/go.sum | 4 +- node-agent/internal/cluster/cluster.go | 196 ++++++ node-agent/internal/cluster/cluster_test.go | 274 +++++++++ .../internal/cluster/kubeconfig_manager.go | 163 +++++ .../cluster/kubeconfig_manager_test.go | 582 ++++++++++++++++++ node-agent/internal/config/config.go | 34 + node-agent/internal/config/config_test.go | 9 + .../internal/hostmgr_client/hostmgr_client.go | 37 +- 16 files changed, 1466 insertions(+), 8 deletions(-) create mode 100644 node-agent/CLUSTER_DETECTION.md create mode 100644 node-agent/internal/cluster/cluster.go create mode 100644 node-agent/internal/cluster/cluster_test.go create mode 100644 node-agent/internal/cluster/kubeconfig_manager.go create mode 100644 node-agent/internal/cluster/kubeconfig_manager_test.go diff --git a/ena-manifest.yaml b/ena-manifest.yaml index 2be8f9f9..460528d4 100644 --- a/ena-manifest.yaml +++ b/ena-manifest.yaml @@ -3,7 +3,7 @@ --- metadata: schemaVersion: 2.0.0 - release: 1.6.4 + release: 1.6.5 repository: codename: 2026.1 component: main @@ -15,7 +15,7 @@ packages: version: 1.10.1 ociArtifact: edge-orch/en/deb/hardware-discovery-agent - name: node-agent - version: 1.11.0 + version: 1.11.1 ociArtifact: edge-orch/en/deb/node-agent - name: platform-manageability-agent version: 0.5.0 diff --git a/node-agent/CLUSTER_DETECTION.md b/node-agent/CLUSTER_DETECTION.md new file mode 100644 index 00000000..bb1e8ef3 --- /dev/null +++ b/node-agent/CLUSTER_DETECTION.md @@ -0,0 +1,62 @@ + + +# Node Agent Cluster Detection and Kubeconfig Management + +This document describes the cluster detection and kubeconfig management functionality added to the node-agent. + +## Overview + +The node-agent now has the ability to: +1. **Detect running clusters** on the node (only k3s and RKE2 supported at the moment). +2. **Retrieve kubeconfig** from detected clusters. +3. **Send dedicated cluster status updates** to the host manager when kubeconfig changes. +4. **Manage kubeconfig lifecycle** independently of node status reporting. + +## Architecture + +### Components Added + +1. **`cluster.ClusterDetector`** - Detects running cluster installations +2. **`cluster.KubeconfigManager`** - Manages kubeconfig lifecycle and dedicated cluster status communication + +### Detection Logic + +The cluster detector supports multiple cluster types and checks for: +- **K3s clusters**: Configurable binary path (defaults to `/var/lib/rancher/k3s/bin/k3s`), checks systemd service status +- **RKE2 clusters**: Configurable binary path (defaults to `/usr/local/bin/rke2`), checks systemd service status + +When a cluster is detected, it: +- Checks if the cluster service is running +- Locates the kubeconfig file in standard locations +- Validates the kubeconfig content +- **Sends dedicated cluster status updates** to the host manager via `UpdateClusterStatus` API + +The system looks for kubeconfig files in these locations: +- **K3s**: `/etc/rancher/k3s/k3s.yaml` +- **RKE2**: `/etc/rancher/rke2/rke2.yaml` + +Add the following section to your `node-agent.yaml`: + +```yaml +cluster: + # Default configuration (both K3s and RKE2) + detectionEnabled: true # Enable/disable cluster detection + detectionInterval: 120s # How often to check for clusters (default: 2 minutes) + + # Generalized cluster configuration (recommended) + # Will automatically default to K3s + clusterTypes: + type: k3s + binaryPath: "/usr/local/bin/k3s" +``` + +### Kubeconfig Management + +The kubeconfig manager: +- Tracks kubeconfig changes using SHA256 hashing +- Only notifies the host manager when kubeconfig content changes +- Uses dedicated `UpdateClusterStatus` API call +- Provides methods to clear kubeconfig when clusters are removed diff --git a/node-agent/README.md b/node-agent/README.md index 79cc9e71..1c7f2c44 100644 --- a/node-agent/README.md +++ b/node-agent/README.md @@ -1,5 +1,5 @@ # Node Agent @@ -12,6 +12,7 @@ It: - Registers and authenticates Edge Node with Edge Infrastructure Manager service - Reports status of Edge Node to the Edge Infrastructure Manager as it onboards - Creates and refreshes tokens for other agents running on the Edge Node +- Provides cluster detection and kubeconfig management functionality. See [CLUSTER_DETECTION.md](CLUSTER_DETECTION.md) for more information. ## Develop diff --git a/node-agent/VERSION b/node-agent/VERSION index 095b6672..720c7384 100644 --- a/node-agent/VERSION +++ b/node-agent/VERSION @@ -1 +1 @@ -1.11.1-dev +1.11.1 diff --git a/node-agent/cmd/node-agent/node-agent.go b/node-agent/cmd/node-agent/node-agent.go index 1917aca3..1f8b98e5 100644 --- a/node-agent/cmd/node-agent/node-agent.go +++ b/node-agent/cmd/node-agent/node-agent.go @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// // SPDX-License-Identifier: Apache-2.0 package main @@ -23,6 +24,7 @@ import ( statusService "github.com/open-edge-platform/edge-node-agents/node-agent/cmd/status-service" "github.com/open-edge-platform/edge-node-agents/node-agent/info" "github.com/open-edge-platform/edge-node-agents/node-agent/internal/auth" + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/cluster" "github.com/open-edge-platform/edge-node-agents/node-agent/internal/comms" "github.com/open-edge-platform/edge-node-agents/node-agent/internal/config" "github.com/open-edge-platform/edge-node-agents/node-agent/internal/hostmgr_client" @@ -40,6 +42,7 @@ var initTimestamp = time.Now().Unix() const REFRESH_CHECK_INTERVAL = 600 * time.Second const TOKEN_REFRESH_CHECK_INTERVAL = 300 * time.Second const COMPONENTS_INIT_WAIT_INTERVAL = 300 * time.Second +const CLUSTER_DETECTION_INTERVAL = 120 * time.Second func main() { if len(os.Args) == 2 && os.Args[1] == "version" { @@ -230,6 +233,48 @@ func main() { } }() + wg.Add(1) + // Go-routine to detect clusters and manage kubeconfig + go func() { + defer wg.Done() + // Do not detect clusters if onboarding is not enabled or cluster detection is disabled + if !confs.Onboarding.Enabled || !confs.Cluster.DetectionEnabled { + if !confs.Cluster.DetectionEnabled { + log.Info("Cluster detection disabled in configuration") + } + return + } + + tlsConfig, err := utils.GetAuthConfig(ctx, nil) + if err != nil { + log.Errorf("failed to create TLS config for cluster detection : %v", err) + return + } + + hostmgrCli, err := hostmgr_client.ConnectToHostMgr(ctx, confs.GUID, confs.Onboarding.ServiceURL, tlsConfig) + if err != nil { + log.Errorf("failed to create Host Manager client for cluster detection updates : %v", err) + return + } + + clusterDetector := cluster.NewClusterDetector(confs.GUID, confs.Cluster.ClusterType) + kubeconfigMgr := cluster.NewKubeconfigManager(hostmgrCli, confs.GUID) + + ticker := time.NewTicker(1 * time.Nanosecond) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + log.Info("terminating cluster detection") + return + case <-ticker.C: + detectAndManageCluster(ctx, clusterDetector, kubeconfigMgr, confs) + log.Info("Cluster detection cycle completed") + } + ticker.Reset(confs.Cluster.DetectionInterval) + } + }() + // once booted (connected to orchestrator, report boot stats) instrument.ReportBootStats() @@ -353,3 +398,49 @@ func updateInstanceStatus(ctx context.Context, hostMgrCli *hostmgr_client.Client log.Errorf("not able to update node status to running : %v", err) } } + +// detects clusters on the node and manages kubeconfig lifecycle +func detectAndManageCluster(ctx context.Context, + detector *cluster.ClusterDetector, kubeconfigMgr *cluster.KubeconfigManager, confs *config.NodeAgentConfig) { + clusterInfo, err := detector.DetectCluster() + if err != nil { + log.Debugf("No cluster detected: %v", err) + if kubeconfigMgr.HasKubeconfig() { + log.Debug("No cluster detected, clearing kubeconfig") + // Done in case cluster was removed after detection or kubeconfig was left orphaned due to some error + // In both cases, kubeconfig needs to be cleared to avoid stale kubeconfig scenario + if clearErr := kubeconfigMgr.ClearKubeconfig(ctx, confs); clearErr != nil { + log.Errorf("Failed to clear kubeconfig: %v", clearErr) + } + } + return + } + + if clusterInfo.Status != "running" { + log.Debugf("Cluster detected but not running: %s", clusterInfo.Status) + return + } + + if clusterInfo.KubeconfigPath == "" { + log.Warnf("Cluster running but no kubeconfig found for %s", clusterInfo.Type) + return + } + + kubeconfigData, err := detector.GetKubeconfig(clusterInfo) + if err != nil { + log.Errorf("Failed to retrieve kubeconfig: %v", err) + return + } + + if err := detector.ValidateKubeconfig(kubeconfigData); err != nil { + log.Errorf("Invalid kubeconfig detected: %v", err) + return + } + + if err := kubeconfigMgr.NotifyKubeconfig(ctx, kubeconfigData, clusterInfo, confs); err != nil { + log.Errorf("Failed to notify host manager about kubeconfig: %v", err) + return + } + + log.Debugf("Cluster management completed for %s cluster", clusterInfo.Type) +} diff --git a/node-agent/cmd/node-agent/node-agent_test.go b/node-agent/cmd/node-agent/node-agent_test.go index cfa08691..8a4fb2fa 100644 --- a/node-agent/cmd/node-agent/node-agent_test.go +++ b/node-agent/cmd/node-agent/node-agent_test.go @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: (C) 2025 Intel Corporation +// SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// // SPDX-License-Identifier: Apache-2.0 // main_test package implements integration test for the Node Agent diff --git a/node-agent/configs/node-agent.yaml b/node-agent/configs/node-agent.yaml index c575e840..b898fa94 100644 --- a/node-agent/configs/node-agent.yaml +++ b/node-agent/configs/node-agent.yaml @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: (C) 2026 Intel Corporation +# # SPDX-License-Identifier: Apache-2.0 --- @@ -26,3 +27,10 @@ auth: accessTokenPath: /etc/intel_edge_node/tokens clientCredsPath: /etc/intel_edge_node/client-credentials tokenClients: [ node-agent, hd-agent, cluster-agent, platform-update-agent, platform-observability-agent, platform-telemetry-agent, prometheus, connect-agent, attestation-manager, platform-manageability-agent ] +cluster: + detectionEnabled: true + detectionInterval: 120s + # Cluster configuration (defaults to k3s with standard binary path if not specified) + # clusterType: + # type: k3s + # binaryPath: "/usr/local/bin/k3s" diff --git a/node-agent/go.mod b/node-agent/go.mod index 6729a4c0..156a907b 100644 --- a/node-agent/go.mod +++ b/node-agent/go.mod @@ -68,3 +68,5 @@ require ( golang.org/x/sys v0.43.0 // indirect google.golang.org/grpc v1.82.0-dev ) + +replace github.com/open-edge-platform/infra-managers/host => github.com/open-edge-platform/infra-managers/host v1.25.5-0.20260402155654-ed7b22250fe3 diff --git a/node-agent/go.sum b/node-agent/go.sum index db73bfb2..70c64fb9 100644 --- a/node-agent/go.sum +++ b/node-agent/go.sum @@ -83,8 +83,8 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/open-edge-platform/edge-node-agents/common v1.11.0 h1:90MFBov0zadLXLevlJduptp6LE2xTNcG1w8hOt5QR9U= github.com/open-edge-platform/edge-node-agents/common v1.11.0/go.mod h1:AdcCf9e7GfNA3eR/qFnYHYLa5t1WcQIlCush9s2pBNE= -github.com/open-edge-platform/infra-managers/host v1.25.4 h1:5Ulfpasc3y8F5TwpSZYMQAA58SkIwWItKHrd/lai3gE= -github.com/open-edge-platform/infra-managers/host v1.25.4/go.mod h1:1aEoXXhxW9OIBIK71u+SHg9vn60pemsyLZIzAkBZU9o= +github.com/open-edge-platform/infra-managers/host v1.25.5-0.20260402155654-ed7b22250fe3 h1:hlMnCGERzxSXcdfAkfCYn9m6TZa5nWREiT8xbHPhhjM= +github.com/open-edge-platform/infra-managers/host v1.25.5-0.20260402155654-ed7b22250fe3/go.mod h1:uN9Cz6MpJlQwGayspoofYysy/QPs9MqTi1VLh89jObU= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= diff --git a/node-agent/internal/cluster/cluster.go b/node-agent/internal/cluster/cluster.go new file mode 100644 index 00000000..dccdb0a5 --- /dev/null +++ b/node-agent/internal/cluster/cluster.go @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +package cluster + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/config" + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/logger" +) + +var clusterLog = logger.Logger + +// represents detected cluster information +type ClusterInfo struct { + Type string `json:"type"` // k3s + Status string `json:"status"` // running, stopped, error + Version string `json:"version"` // cluster version + KubeconfigPath string `json:"kubeconfigPath"` // path to kubeconfig file + DetectedAt time.Time `json:"detectedAt"` // when cluster was detected +} + +// handles detection of running clusters on the node +type ClusterDetector struct { + nodeID string + clusterType config.ClusterType +} + +// creates a new cluster detector +func NewClusterDetector(nodeID string, clusterType config.ClusterType) *ClusterDetector { + return &ClusterDetector{ + nodeID: nodeID, + clusterType: clusterType, + } +} + +// checks if there's a cluster running on the node +func (cd *ClusterDetector) DetectCluster() (*ClusterInfo, error) { + clusterLog.Debug("Detecting clusters on the node...") + + switch cd.clusterType.Type { + case "k3s": + if clusterInfo, err := cd.detectK3s(cd.clusterType.BinaryPath); err == nil { + clusterLog.Infof("Detected K3s cluster: version=%s, status=%s", clusterInfo.Version, clusterInfo.Status) + return clusterInfo, nil + } else { + return nil, fmt.Errorf("K3s detection failed: %v", err) + } + case "rke2": + if clusterInfo, err := cd.detectRKE2(cd.clusterType.BinaryPath); err == nil { + clusterLog.Infof("Detected RKE2 cluster: version=%s, status=%s", clusterInfo.Version, clusterInfo.Status) + return clusterInfo, nil + } else { + return nil, fmt.Errorf("RKE2 detection failed: %v", err) + } + default: + clusterLog.Warnf("Unsupported cluster type: %s", cd.clusterType.Type) + } + + return nil, fmt.Errorf("no cluster detected on node") +} + +// detects K3s cluster installation +func (cd *ClusterDetector) detectK3s(k3sBinaryPath string) (*ClusterInfo, error) { + // Check if k3s binary exists + if _, err := os.Stat(k3sBinaryPath); os.IsNotExist(err) { + return nil, fmt.Errorf("K3s binary not found at %s", k3sBinaryPath) + } + + // Get K3s version + cmd := exec.Command(k3sBinaryPath, "--version") + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to get K3s version: %v", err) + } + + versionParts := strings.Fields(string(output)) + version := "unknown" + if len(versionParts) >= 3 { + version = strings.TrimSpace(versionParts[2]) + } + + // Check if K3s service is running + status := "stopped" + if cd.isServiceActive("k3s") { + status = "running" + } + + // Using reporting-agent's default path for k3s kubeconfig + kubeconfigPath := "/etc/rancher/k3s/k3s.yaml" + if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { + kubeconfigPath = "" + } + + return &ClusterInfo{ + Type: "k3s", + Status: status, + Version: version, + KubeconfigPath: kubeconfigPath, + DetectedAt: time.Now(), + }, nil +} + +// detects RKE2 cluster installation +func (cd *ClusterDetector) detectRKE2(rke2BinaryPath string) (*ClusterInfo, error) { + // Check if rke2 binary exists + if _, err := os.Stat(rke2BinaryPath); os.IsNotExist(err) { + return nil, fmt.Errorf("RKE2 binary not found at %s", rke2BinaryPath) + } + + // Get RKE2 version + cmd := exec.Command(rke2BinaryPath, "--version") + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to get RKE2 version: %v", err) + } + + versionParts := strings.Fields(string(output)) + version := "unknown" + if len(versionParts) >= 3 { + version = strings.TrimSpace(versionParts[2]) + } + + // Check if RKE2 server or agent service is running + status := "stopped" + if cd.isServiceActive("rke2-server") || cd.isServiceActive("rke2-agent") { + status = "running" + } + + // Using reporting-agent's default path for rke2 kubeconfig + kubeconfigPath := "/etc/rancher/rke2/rke2.yaml" + if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { + kubeconfigPath = "" + } + + return &ClusterInfo{ + Type: "rke2", + Status: status, + Version: version, + KubeconfigPath: kubeconfigPath, + DetectedAt: time.Now(), + }, nil +} + +// checks if a systemd service is active +func (cd *ClusterDetector) isServiceActive(serviceName string) bool { + cmd := exec.Command("systemctl", "is-active", serviceName) + output, err := cmd.Output() + if err != nil { + return false + } + return strings.TrimSpace(string(output)) == "active" +} + +// retrieves the kubeconfig content from the detected cluster +func (cd *ClusterDetector) GetKubeconfig(clusterInfo *ClusterInfo) ([]byte, error) { + if clusterInfo == nil || clusterInfo.KubeconfigPath == "" { + return nil, fmt.Errorf("no kubeconfig available for cluster") + } + + clusterLog.Debugf("Reading kubeconfig from: %s", clusterInfo.KubeconfigPath) + + content, err := os.ReadFile(clusterInfo.KubeconfigPath) + if err != nil { + return nil, fmt.Errorf("failed to read kubeconfig from %s: %v", clusterInfo.KubeconfigPath, err) + } + + clusterLog.Infof("Successfully retrieved kubeconfig: %d bytes", len(content)) + return content, nil +} + +// performs basic validation of kubeconfig content +func (cd *ClusterDetector) ValidateKubeconfig(kubeconfigData []byte) error { + if len(kubeconfigData) == 0 { + return fmt.Errorf("kubeconfig is empty") + } + + content := string(kubeconfigData) + + // Basic validation - check for required fields + requiredFields := []string{"apiVersion", "kind: Config", "clusters:", "users:", "contexts:"} + for _, field := range requiredFields { + if !strings.Contains(content, field) { + return fmt.Errorf("kubeconfig missing required field: %s", field) + } + } + + clusterLog.Debug("Kubeconfig validation passed") + return nil +} diff --git a/node-agent/internal/cluster/cluster_test.go b/node-agent/internal/cluster/cluster_test.go new file mode 100644 index 00000000..79918ba4 --- /dev/null +++ b/node-agent/internal/cluster/cluster_test.go @@ -0,0 +1,274 @@ +// SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +package cluster + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/config" +) + +func TestNewClusterDetector(t *testing.T) { + nodeID := "test-node-123" + clusterType := config.ClusterType{ + Type: "k3s", BinaryPath: "/usr/local/bin/k3s", + } + detector := NewClusterDetector(nodeID, clusterType) + + if detector == nil { + t.Fatal("NewClusterDetector should not return nil") + } + + if detector.nodeID != nodeID { + t.Errorf("Expected nodeID %s, got %s", nodeID, detector.nodeID) + } + + if detector.clusterType.Type != "k3s" { + t.Errorf("Expected cluster type 'k3s', got %s", detector.clusterType.Type) + } +} + +func TestNewClusterDetectorWithCustomPath(t *testing.T) { + nodeID := "test-node-custom" + clusterTypes := config.ClusterType{ + Type: "rke2", BinaryPath: "/usr/local/bin/rke2", + } + detector := NewClusterDetector(nodeID, clusterTypes) + + if detector == nil { + t.Fatal("NewClusterDetector should not return nil") + } + + if detector.nodeID != nodeID { + t.Errorf("Expected nodeID %s, got %s", nodeID, detector.nodeID) + } + + if detector.clusterType.BinaryPath != "/usr/local/bin/rke2" { + t.Errorf("Expected k3s binary path '/usr/bin/k3s', got %s", detector.clusterType.BinaryPath) + } +} + +func TestValidateKubeconfig(t *testing.T) { + clusterType := config.ClusterType{ + Type: "k3s", BinaryPath: "/usr/local/bin/k3s", + } + detector := NewClusterDetector("test-node", clusterType) + + tests := []struct { + name string + kubeconfig string + shouldError bool + }{ + { + name: "empty kubeconfig", + kubeconfig: "", + shouldError: true, + }, + { + name: "valid kubeconfig", + kubeconfig: ` +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://kubernetes.example.com:6443 + name: example-cluster +contexts: +- context: + cluster: example-cluster + user: example-user + name: example-context +users: +- name: example-user + user: + token: example-token +`, + shouldError: false, + }, + { + name: "missing required field", + kubeconfig: ` +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://kubernetes.example.com:6443 + name: example-cluster +`, + shouldError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := detector.ValidateKubeconfig([]byte(tt.kubeconfig)) + if tt.shouldError && err == nil { + t.Error("Expected error but got none") + } + if !tt.shouldError && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + }) + } +} + +func TestKubeconfigManager(t *testing.T) { + // Create a kubeconfig manager with nil client for testing + mgr := NewKubeconfigManager(nil, "test-node") + + if mgr == nil { + t.Fatal("NewKubeconfigManager should not return nil") + } + + if mgr.HasKubeconfig() { + t.Error("New manager should not have kubeconfig initially") + } + + if mgr.KubeconfigSize() != 0 { + t.Error("New manager should have kubeconfig size 0") + } + + // Test cluster status formatting + clusterInfo := &ClusterInfo{ + Type: "k3s", + Status: "running", + Version: "v1.28.2+k3s1", + KubeconfigPath: "/etc/rancher/k3s/k3s.yaml", + DetectedAt: time.Now(), + } + + status := mgr.GetClusterStatus(clusterInfo) + expectedSubstrings := []string{"k3s", "v1.28.2+k3s1", "running"} + + for _, substring := range expectedSubstrings { + if !strings.Contains(status, substring) { + t.Errorf("Status should contain '%s', got: %s", substring, status) + } + } +} + +func TestClusterInfo(t *testing.T) { + now := time.Now() + clusterInfo := &ClusterInfo{ + Type: "k3s", + Status: "running", + Version: "v1.28.2+k3s1", + KubeconfigPath: "/etc/rancher/k3s/k3s.yaml", + DetectedAt: now, + } + + if clusterInfo.Type != "k3s" { + t.Errorf("Expected type 'k3s', got %s", clusterInfo.Type) + } + + if clusterInfo.Status != "running" { + t.Errorf("Expected status 'running', got %s", clusterInfo.Status) + } + + if clusterInfo.DetectedAt != now { + t.Error("DetectedAt timestamp should match") + } +} + +// This test will only run if K3s is actually installed on the system +func TestDetectK3sIntegration(t *testing.T) { + k3sPath := "/usr/local/bin/k3s" + // Skip this test if we're not in an environment with K3s + if _, err := os.Stat(k3sPath); os.IsNotExist(err) { + t.Skip("Skipping K3s detection test - K3s not installed") + } + + clusterType := config.ClusterType{ + Type: "k3s", BinaryPath: k3sPath, + } + detector := NewClusterDetector("test-node", clusterType) + clusterInfo, err := detector.detectK3s(k3sPath) + + if err != nil { + t.Logf("K3s detection failed (expected if K3s not running): %v", err) + return + } + + if clusterInfo.Type != "k3s" { + t.Errorf("Expected type 'k3s', got %s", clusterInfo.Type) + } + + if clusterInfo.Version == "" || clusterInfo.Version == "unknown" { + t.Error("Should have detected K3s version") + } + + t.Logf("Detected K3s: version=%s, status=%s, kubeconfig=%s", + clusterInfo.Version, clusterInfo.Status, clusterInfo.KubeconfigPath) +} + +func TestDetectCluster(t *testing.T) { + clusterType := config.ClusterType{ + Type: "k3s", BinaryPath: "/usr/local/bin/k3s", + } + detector := NewClusterDetector("test-node", clusterType) + + // This test may fail if no cluster is installed, which is expected + clusterInfo, err := detector.DetectCluster() + + if err != nil { + t.Logf("No cluster detected (expected): %v", err) + return + } + + // If a cluster is detected, validate the structure + if clusterInfo.Type == "" { + t.Error("Detected cluster should have a type") + } + + if clusterInfo.Status == "" { + t.Error("Detected cluster should have a status") + } + + t.Logf("Detected cluster: type=%s, status=%s, version=%s", + clusterInfo.Type, clusterInfo.Status, clusterInfo.Version) +} + +func TestDetectK3sWithInvalidPath(t *testing.T) { + // Test with a non-existent k3s binary path + clusterType := config.ClusterType{ + Type: "k3s", BinaryPath: "/nonexistent/path/k3s", + } + detector := NewClusterDetector("test-node", clusterType) + + clusterInfo, err := detector.DetectCluster() + + if err == nil { + t.Error("Expected error when k3s binary not found, but got none") + } + + if clusterInfo != nil { + t.Error("Expected nil clusterInfo when k3s binary not found") + } + + if !strings.Contains(err.Error(), "K3s detection failed: K3s binary not found at /nonexistent/path/k3s") { + t.Errorf("Error message should indicate no cluster detected, got: %v", err) + } +} + +func TestDetectRKE2WithInvalidPath(t *testing.T) { + // Test RKE2 detection with non-existent binary + clusterType := config.ClusterType{ + Type: "rke2", BinaryPath: "/nonexistent/path/rke2", + } + detector := NewClusterDetector("test-node", clusterType) + + clusterInfo, err := detector.DetectCluster() + + if err == nil { + t.Error("Expected error when rke2 binary not found, but got none") + } + + if clusterInfo != nil { + t.Error("Expected nil clusterInfo when rke2 binary not found") + } +} diff --git a/node-agent/internal/cluster/kubeconfig_manager.go b/node-agent/internal/cluster/kubeconfig_manager.go new file mode 100644 index 00000000..f3b409ac --- /dev/null +++ b/node-agent/internal/cluster/kubeconfig_manager.go @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +package cluster + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "path/filepath" + "sync" + + "github.com/open-edge-platform/edge-node-agents/common/pkg/utils" + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/config" + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/hostmgr_client" + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/logger" +) + +var managerLog = logger.Logger + +// manages kubeconfig lifecycle and communication with host manager +// tracks the last known kubeconfig and only notifies host manager +// on changes to avoid unnecessary updates +type KubeconfigManager struct { + hostmgrClient *hostmgr_client.Client + nodeID string + lastKubeconfig []byte + lastKubeconfigHash string + mu sync.RWMutex +} + +// creates a new kubeconfig manager +func NewKubeconfigManager(hostmgrClient *hostmgr_client.Client, nodeID string) *KubeconfigManager { + return &KubeconfigManager{ + hostmgrClient: hostmgrClient, + nodeID: nodeID, + } +} + +// sends kubeconfig data to the host manager +func (km *KubeconfigManager) NotifyKubeconfig(ctx context.Context, kubeconfigData []byte, clusterInfo *ClusterInfo, confs *config.NodeAgentConfig) error { + km.mu.Lock() + defer km.mu.Unlock() + + if clusterInfo == nil { + return fmt.Errorf("clusterInfo cannot be nil") + } + + // Calculate hash to detect changes + hashBytes := sha256.Sum256(kubeconfigData) + currentHash := fmt.Sprintf("%x", hashBytes) + + //Avoid unnecessary updates to host manager and DB when kubeconfig content is the same + if km.lastKubeconfigHash == currentHash { + managerLog.Debug("Kubeconfig unchanged, skipping notification") + return nil + } + + managerLog.Infof("Notifying host manager about kubeconfig update (cluster: %s, version: %s)", + clusterInfo.Type, clusterInfo.Version) + + // Encode kubeconfig in base64 before storing to avoid any formatting issues while transmitting over gRPC and storing in DB + kubeconfigBlob := base64.StdEncoding.EncodeToString(kubeconfigData) + + // Notify host manager about kubeconfig update + tokenFile := filepath.Join(confs.Auth.AccessTokenPath, "node-agent", config.AccessToken) + + // Only update cluster status if hostmgr client is available (skip for tests with nil client) + if km.hostmgrClient != nil { + err := km.hostmgrClient.UpdateClusterStatus(utils.GetAuthContext(ctx, tokenFile), kubeconfigBlob) + if err != nil { + managerLog.Errorf("not able to update node status to running : %v", err) + return fmt.Errorf("failed to update cluster status: %v", err) + } + } + + // Update tracking information + km.lastKubeconfig = make([]byte, len(kubeconfigData)) + copy(km.lastKubeconfig, kubeconfigData) + km.lastKubeconfigHash = currentHash + + managerLog.Infof("Successfully notified host manager about kubeconfig (%d bytes)", len(kubeconfigData)) + return nil +} + +// set the kubeconfig to nil and notify host manager to clear it +// avoid having stale kubeconfig data in host manager when cluster is deleted or becomes unreachable +func (km *KubeconfigManager) ClearKubeconfig(ctx context.Context, confs *config.NodeAgentConfig) error { + km.mu.Lock() + defer km.mu.Unlock() + + managerLog.Info("Clearing kubeconfig from host manager") + + if km.hostmgrClient != nil { + tokenFile := filepath.Join(confs.Auth.AccessTokenPath, "node-agent", config.AccessToken) + + // Empty string means "clear kubeconfig" on Host Manager side. + if err := km.hostmgrClient.UpdateClusterStatus(utils.GetAuthContext(ctx, tokenFile), ""); err != nil { + managerLog.Errorf("failed to clear kubeconfig in host manager: %v", err) + return fmt.Errorf("failed to clear kubeconfig in host manager: %w", err) + } + } + + km.lastKubeconfig = nil + km.lastKubeconfigHash = "" + + managerLog.Info("Kubeconfig cleared successfully") + return nil +} + +// returns the last known kubeconfig +func (km *KubeconfigManager) GetLastKubeconfig() []byte { + km.mu.RLock() + defer km.mu.RUnlock() + + if km.lastKubeconfig == nil { + return nil + } + + result := make([]byte, len(km.lastKubeconfig)) + copy(result, km.lastKubeconfig) + return result +} + +// returns true if a kubeconfig is currently tracked +func (km *KubeconfigManager) HasKubeconfig() bool { + km.mu.RLock() + defer km.mu.RUnlock() + + return len(km.lastKubeconfig) > 0 +} + +// returns the size of the current kubeconfig in bytes +func (km *KubeconfigManager) KubeconfigSize() int { + km.mu.RLock() + defer km.mu.RUnlock() + + if km.lastKubeconfig == nil { + return 0 + } + return len(km.lastKubeconfig) +} + +// returns a formatted string describing the current cluster status +func (km *KubeconfigManager) GetClusterStatus(clusterInfo *ClusterInfo) string { + km.mu.RLock() + defer km.mu.RUnlock() + + if clusterInfo == nil { + if len(km.lastKubeconfig) > 0 { + return "cluster: unknown (kubeconfig cached)" + } + return "cluster: none detected" + } + + status := fmt.Sprintf("cluster: %s %s (%s)", clusterInfo.Type, clusterInfo.Version, clusterInfo.Status) + if len(km.lastKubeconfig) > 0 { + status += fmt.Sprintf(", kubeconfig: %d bytes", len(km.lastKubeconfig)) + } + return status +} diff --git a/node-agent/internal/cluster/kubeconfig_manager_test.go b/node-agent/internal/cluster/kubeconfig_manager_test.go new file mode 100644 index 00000000..13afeaa0 --- /dev/null +++ b/node-agent/internal/cluster/kubeconfig_manager_test.go @@ -0,0 +1,582 @@ +// SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +package cluster + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/open-edge-platform/edge-node-agents/node-agent/internal/config" + "github.com/stretchr/testify/assert" +) + +// Test helpers for cluster package + +// MockHostmgrClient provides a simple mock implementation of the host manager client +// for testing purposes +type MockHostmgrClient struct { + UpdateCallCount int + LastUpdateMessage string + ShouldReturnError bool + ErrorToReturn error +} + +// UpdateInstanceStatus mocks the host manager client's update method +func (m *MockHostmgrClient) UpdateInstanceStatus(ctx context.Context, state interface{}, status interface{}, message string) error { + m.UpdateCallCount++ + m.LastUpdateMessage = message + + if m.ShouldReturnError { + if m.ErrorToReturn != nil { + return m.ErrorToReturn + } + return fmt.Errorf("mock error from hostmgr client") + } + + return nil +} + +// Reset clears the mock's tracking data +func (m *MockHostmgrClient) Reset() { + m.UpdateCallCount = 0 + m.LastUpdateMessage = "" + m.ShouldReturnError = false + m.ErrorToReturn = nil +} + +// TestClusterInfoBuilder provides a fluent interface for creating test cluster info +type TestClusterInfoBuilder struct { + info *ClusterInfo +} + +// NewTestClusterInfoBuilder creates a new builder with default values +func NewTestClusterInfoBuilder() *TestClusterInfoBuilder { + return &TestClusterInfoBuilder{ + info: &ClusterInfo{ + Type: "test-cluster", + Status: "running", + Version: "v1.0.0", + KubeconfigPath: "/tmp/test-kubeconfig", + DetectedAt: time.Now(), + }, + } +} + +// WithType sets the cluster type +func (b *TestClusterInfoBuilder) WithType(clusterType string) *TestClusterInfoBuilder { + b.info.Type = clusterType + return b +} + +// WithStatus sets the cluster status +func (b *TestClusterInfoBuilder) WithStatus(status string) *TestClusterInfoBuilder { + b.info.Status = status + return b +} + +// WithVersion sets the cluster version +func (b *TestClusterInfoBuilder) WithVersion(version string) *TestClusterInfoBuilder { + b.info.Version = version + return b +} + +// WithKubeconfigPath sets the kubeconfig path +func (b *TestClusterInfoBuilder) WithKubeconfigPath(path string) *TestClusterInfoBuilder { + b.info.KubeconfigPath = path + return b +} + +// WithDetectedAt sets the detected timestamp +func (b *TestClusterInfoBuilder) WithDetectedAt(timestamp time.Time) *TestClusterInfoBuilder { + b.info.DetectedAt = timestamp + return b +} + +// Build returns the constructed ClusterInfo +func (b *TestClusterInfoBuilder) Build() *ClusterInfo { + return b.info +} + +// TestKubeconfigGenerator provides utilities for generating test kubeconfig data +type TestKubeconfigGenerator struct{} + +// NewTestKubeconfigGenerator creates a new kubeconfig generator +func NewTestKubeconfigGenerator() *TestKubeconfigGenerator { + return &TestKubeconfigGenerator{} +} + +// GenerateBasicKubeconfig creates a basic valid kubeconfig for testing +func (g *TestKubeconfigGenerator) GenerateBasicKubeconfig(clusterName, serverURL, token string) []byte { + kubeconfig := fmt.Sprintf(` +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: %s + name: %s +contexts: +- context: + cluster: %s + user: test-user + name: test-context +current-context: test-context +users: +- name: test-user + user: + token: %s +`, serverURL, clusterName, clusterName, token) + + return []byte(kubeconfig) +} + +// GenerateInvalidKubeconfig creates an invalid kubeconfig for testing error cases +func (g *TestKubeconfigGenerator) GenerateInvalidKubeconfig() []byte { + return []byte(` +apiVersion: v1 +kind: Config +# Missing required fields +clusters: [] +`) +} + +// GenerateEmptyKubeconfig returns an empty kubeconfig +func (g *TestKubeconfigGenerator) GenerateEmptyKubeconfig() []byte { + return []byte("") +} + +// GenerateLargeKubeconfig creates a large kubeconfig for stress testing +func (g *TestKubeconfigGenerator) GenerateLargeKubeconfig(size int) []byte { + baseConfig := g.GenerateBasicKubeconfig("large-cluster", "https://kubernetes.example.com:6443", "large-token") + + // Pad with comments to reach desired size + padding := make([]byte, size-len(baseConfig)) + for i := range padding { + switch i % 80 { + case 0: + padding[i] = '\n' + case 1: + padding[i] = '#' + default: + padding[i] = ' ' + } + } + + return append(baseConfig, padding...) +} + +// Test helper functions +func createTestClusterInfo() *ClusterInfo { + return &ClusterInfo{ + Type: "k3s", + Status: "running", + Version: "v1.28.2+k3s1", + KubeconfigPath: "/etc/rancher/k3s/k3s.yaml", + DetectedAt: time.Now(), + } +} + +func createTestKubeconfig() []byte { + return []byte(` +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://kubernetes.example.com:6443 + name: test-cluster +contexts: +- context: + cluster: test-cluster + user: test-user + name: test-context +users: +- name: test-user + user: + token: test-token +`) +} + +func createTestConfig() *config.NodeAgentConfig { + return &config.NodeAgentConfig{ + Auth: config.ConfigAuth{ + AccessTokenPath: "/tmp/tokens", + }, + } +} + +func TestNewKubeconfigManager(t *testing.T) { + tests := []struct { + name string + nodeID string + expected string + }{ + { + name: "valid node ID", + nodeID: "test-node-123", + expected: "test-node-123", + }, + { + name: "empty node ID", + nodeID: "", + expected: "", + }, + { + name: "node ID with special characters", + nodeID: "node-with-special-chars_123", + expected: "node-with-special-chars_123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := NewKubeconfigManager(nil, tt.nodeID) + + assert.NotNil(t, manager) + assert.Equal(t, tt.expected, manager.nodeID) + assert.Nil(t, manager.lastKubeconfig) + assert.Empty(t, manager.lastKubeconfigHash) + }) + } +} + +func TestKubeconfigManager_NotifyKubeconfig(t *testing.T) { + tests := []struct { + name string + kubeconfig []byte + clusterInfo *ClusterInfo + expectError bool + expectHashSet bool + expectDataCopy bool + subsequentCall bool + }{ + { + name: "valid kubeconfig first time", + kubeconfig: createTestKubeconfig(), + clusterInfo: createTestClusterInfo(), + expectError: false, + expectHashSet: true, + expectDataCopy: true, + }, + { + name: "same kubeconfig twice (should skip)", + kubeconfig: createTestKubeconfig(), + clusterInfo: createTestClusterInfo(), + expectError: false, + expectHashSet: true, + expectDataCopy: true, + subsequentCall: true, + }, + { + name: "different kubeconfig", + kubeconfig: []byte("different kubeconfig content"), + clusterInfo: createTestClusterInfo(), + expectError: false, + expectHashSet: true, + expectDataCopy: true, + }, + { + name: "empty kubeconfig", + kubeconfig: []byte(""), + clusterInfo: createTestClusterInfo(), + expectError: false, + expectHashSet: true, + expectDataCopy: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + if tt.subsequentCall { + // Call twice with same data to test deduplication + err := manager.NotifyKubeconfig(ctx, tt.kubeconfig, tt.clusterInfo, createTestConfig()) + assert.NoError(t, err) + } + + err := manager.NotifyKubeconfig(ctx, tt.kubeconfig, tt.clusterInfo, createTestConfig()) + + if tt.expectError { + assert.NoError(t, err) + } + + if tt.expectHashSet { + assert.NotEmpty(t, manager.lastKubeconfigHash) + } + + if tt.expectDataCopy { + assert.Equal(t, tt.kubeconfig, manager.lastKubeconfig) + } + }) + } +} + +func TestKubeconfigManager_ClearKubeconfig(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + // First set some kubeconfig data + kubeconfig := createTestKubeconfig() + clusterInfo := createTestClusterInfo() + nodeAgentConfg := createTestConfig() + err := manager.NotifyKubeconfig(ctx, kubeconfig, clusterInfo, nodeAgentConfg) + assert.NoError(t, err) + + // Verify data is set + assert.True(t, manager.HasKubeconfig()) + assert.NotEmpty(t, manager.lastKubeconfigHash) + + // Clear the kubeconfig + err = manager.ClearKubeconfig(ctx, nodeAgentConfg) + assert.NoError(t, err) + + // Verify data is cleared + assert.False(t, manager.HasKubeconfig()) + assert.Empty(t, manager.lastKubeconfigHash) + assert.Nil(t, manager.lastKubeconfig) +} + +func TestKubeconfigManager_GetLastKubeconfig(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + // Initially should return nil + result := manager.GetLastKubeconfig() + assert.Nil(t, result) + + // Set kubeconfig + kubeconfig := createTestKubeconfig() + clusterInfo := createTestClusterInfo() + err := manager.NotifyKubeconfig(ctx, kubeconfig, clusterInfo, createTestConfig()) + assert.NoError(t, err) + + // Should return copy of kubeconfig + result = manager.GetLastKubeconfig() + assert.Equal(t, kubeconfig, result) + + // Verify it's a copy, not the same slice + result[0] = 'X' + originalResult := manager.GetLastKubeconfig() + assert.NotEqual(t, result[0], originalResult[0]) +} + +func TestKubeconfigManager_HasKubeconfig(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + // Initially should be false + assert.False(t, manager.HasKubeconfig()) + + // After setting kubeconfig should be true + kubeconfig := createTestKubeconfig() + clusterInfo := createTestClusterInfo() + nodeAgentConfg := createTestConfig() + err := manager.NotifyKubeconfig(ctx, kubeconfig, clusterInfo, nodeAgentConfg) + assert.NoError(t, err) + assert.True(t, manager.HasKubeconfig()) + + // After clearing should be false again + err = manager.ClearKubeconfig(ctx, nodeAgentConfg) + assert.NoError(t, err) + assert.False(t, manager.HasKubeconfig()) +} + +func TestKubeconfigManager_KubeconfigSize(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + // Initially should be 0 + assert.Equal(t, 0, manager.KubeconfigSize()) + + // After setting kubeconfig should return correct size + kubeconfig := createTestKubeconfig() + clusterInfo := createTestClusterInfo() + nodeAgentConfg := createTestConfig() + err := manager.NotifyKubeconfig(ctx, kubeconfig, clusterInfo, nodeAgentConfg) + assert.NoError(t, err) + assert.Equal(t, len(kubeconfig), manager.KubeconfigSize()) + + // After clearing should be 0 again + err = manager.ClearKubeconfig(ctx, nodeAgentConfg) + assert.NoError(t, err) + assert.Equal(t, 0, manager.KubeconfigSize()) +} + +func TestKubeconfigManager_GetClusterStatus(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + tests := []struct { + name string + clusterInfo *ClusterInfo + hasKubeconfig bool + expectedStrings []string + }{ + { + name: "nil cluster info, no kubeconfig", + clusterInfo: nil, + hasKubeconfig: false, + expectedStrings: []string{"none detected"}, + }, + { + name: "nil cluster info, has kubeconfig", + clusterInfo: nil, + hasKubeconfig: true, + expectedStrings: []string{"unknown", "kubeconfig cached"}, + }, + { + name: "valid cluster info, no kubeconfig", + clusterInfo: createTestClusterInfo(), + hasKubeconfig: false, + expectedStrings: []string{"k3s", "v1.28.2+k3s1", "running"}, + }, + { + name: "valid cluster info, has kubeconfig", + clusterInfo: createTestClusterInfo(), + hasKubeconfig: true, + expectedStrings: []string{"k3s", "v1.28.2+k3s1", "running", "kubeconfig:", "bytes"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset manager state + manager = NewKubeconfigManager(nil, "test-node") + + if tt.hasKubeconfig { + kubeconfig := createTestKubeconfig() + clusterInfo := createTestClusterInfo() + err := manager.NotifyKubeconfig(ctx, kubeconfig, clusterInfo, createTestConfig()) + assert.NoError(t, err) + } + + status := manager.GetClusterStatus(tt.clusterInfo) + + for _, expectedString := range tt.expectedStrings { + assert.Contains(t, status, expectedString, "Status should contain '%s', got: %s", expectedString, status) + } + }) + } +} + +func TestKubeconfigManager_ConcurrentAccess(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + var wg sync.WaitGroup + numGoroutines := 10 + + // Test concurrent reads and writes + wg.Add(numGoroutines * 3) + + // Concurrent writes + for i := 0; i < numGoroutines; i++ { + go func(i int) { + defer wg.Done() + kubeconfig := []byte("test-kubeconfig-" + string(rune(i))) + clusterInfo := createTestClusterInfo() + manager.NotifyKubeconfig(ctx, kubeconfig, clusterInfo, createTestConfig()) + }(i) + } + + // Concurrent reads + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + manager.GetLastKubeconfig() + manager.HasKubeconfig() + manager.KubeconfigSize() + }() + } + + // Concurrent clears + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + manager.ClearKubeconfig(ctx, createTestConfig()) + }() + } + + wg.Wait() + + // Verify final state is consistent (no panics or race conditions) + assert.NotNil(t, manager) +} + +func TestKubeconfigManager_HashGeneration(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + + kubeconfig1 := []byte("kubeconfig content 1") + kubeconfig2 := []byte("kubeconfig content 2") + clusterInfo := createTestClusterInfo() + + // Set first kubeconfig + err := manager.NotifyKubeconfig(ctx, kubeconfig1, clusterInfo, createTestConfig()) + assert.NoError(t, err) + hash1 := manager.lastKubeconfigHash + + // Set different kubeconfig + err = manager.NotifyKubeconfig(ctx, kubeconfig2, clusterInfo, createTestConfig()) + assert.NoError(t, err) + hash2 := manager.lastKubeconfigHash + + // Hashes should be different + assert.NotEqual(t, hash1, hash2) + assert.NotEmpty(t, hash1) + assert.NotEmpty(t, hash2) + + // Set same kubeconfig again + err = manager.NotifyKubeconfig(ctx, kubeconfig2, clusterInfo, createTestConfig()) + assert.NoError(t, err) + hash3 := manager.lastKubeconfigHash + + // Hash should remain the same + assert.Equal(t, hash2, hash3) +} + +func TestKubeconfigManager_EdgeCases(t *testing.T) { + t.Run("nil kubeconfig", func(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + clusterInfo := createTestClusterInfo() + + err := manager.NotifyKubeconfig(ctx, nil, clusterInfo, createTestConfig()) + assert.NoError(t, err) + + assert.Equal(t, 0, manager.KubeconfigSize()) + assert.False(t, manager.HasKubeconfig()) + }) + + t.Run("nil cluster info", func(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + kubeconfig := createTestKubeconfig() + + // Should handle nil cluster info gracefully with an error + err := manager.NotifyKubeconfig(ctx, kubeconfig, nil, createTestConfig()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "clusterInfo cannot be nil") + }) + + t.Run("very large kubeconfig", func(t *testing.T) { + manager := NewKubeconfigManager(nil, "test-node") + ctx := context.Background() + clusterInfo := createTestClusterInfo() + + // Create a large kubeconfig (1MB) + largeKubeconfig := make([]byte, 1024*1024) + for i := range largeKubeconfig { + largeKubeconfig[i] = 'a' + } + + err := manager.NotifyKubeconfig(ctx, largeKubeconfig, clusterInfo, createTestConfig()) + assert.NoError(t, err) + assert.Equal(t, len(largeKubeconfig), manager.KubeconfigSize()) + }) +} diff --git a/node-agent/internal/config/config.go b/node-agent/internal/config/config.go index 7b752f7c..d40f5bf1 100644 --- a/node-agent/internal/config/config.go +++ b/node-agent/internal/config/config.go @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// // SPDX-License-Identifier: Apache-2.0 // Package config contains Node Agent configuration management @@ -22,6 +23,10 @@ const NodeAgentKey = "node-agent-key.pem" const AccessToken = "access_token" const HEARTBEAT_DEFAULT = 10 +const CLUSTER_DETECTION_DEFAULT = 30 + +// Kubernetes cluster configuration +const K3S_DEFAULT_BINARY_PATH = "/var/lib/rancher/k3s/bin/k3s" var log = logger.Logger @@ -58,6 +63,17 @@ type ConfigMetrics struct { Interval time.Duration `yaml:"interval"` } +type ClusterType struct { + Type string `yaml:"type"` + BinaryPath string `yaml:"binaryPath"` +} + +type ConfigCluster struct { + DetectionEnabled bool `yaml:"detectionEnabled"` + DetectionInterval time.Duration `yaml:"detectionInterval"` + ClusterType ClusterType `yaml:"clusterType"` +} + type NodeAgentConfig struct { Version string `yaml:"version"` LogLevel string `yaml:"logLevel"` @@ -66,6 +82,7 @@ type NodeAgentConfig struct { Auth ConfigAuth `yaml:"auth"` Status ConfigStatus `yaml:"status"` Metrics ConfigMetrics `yaml:"metrics"` + Cluster ConfigCluster `yaml:"cluster"` } // Create a new Node agent configuration. @@ -131,6 +148,23 @@ func (cfg *NodeAgentConfig) setDefaults(cfgPath string) { interval = 60 * time.Second } cfg.Status.NetworkStatusInterval = interval + + if cfg.Cluster.DetectionInterval <= 0*time.Second { + log.Warnf("cluster detection interval not provided by %s, setting to default %d", cfgPath, CLUSTER_DETECTION_DEFAULT) + cfg.Cluster.DetectionInterval = CLUSTER_DETECTION_DEFAULT * time.Second + } + + // Set default cluster types if none configured + if cfg.Cluster.ClusterType.Type != "" { + log.Infof("Cluster type configured: %s", cfg.Cluster.ClusterType.Type) + } else { + log.Infof("No cluster type configured in %s, setting to default k3s with binary path %s", + cfgPath, K3S_DEFAULT_BINARY_PATH) + cfg.Cluster.ClusterType = ClusterType{ + Type: "k3s", + BinaryPath: K3S_DEFAULT_BINARY_PATH, + } + } } // readConfigFile loads yaml file into NodeAgentConfig type diff --git a/node-agent/internal/config/config_test.go b/node-agent/internal/config/config_test.go index ce492018..c636d40e 100644 --- a/node-agent/internal/config/config_test.go +++ b/node-agent/internal/config/config_test.go @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: (C) 2026 Intel Corporation +// // SPDX-License-Identifier: Apache-2.0 package config_test @@ -74,6 +75,7 @@ func createConfigFile(t *testing.T, testGUID string, onboardingServiceURL string Endpoint: testMetricsEndpoint, Interval: testMetricsHeartbeatInterval, }, + Cluster: config.ConfigCluster{}, } file, err := yaml.Marshal(c) @@ -116,6 +118,13 @@ func getExpectedConfig(logLevel string, heartbeatInterval time.Duration, network Endpoint: testMetricsEndpoint, Interval: testMetricsHeartbeatInterval, }, + Cluster: config.ConfigCluster{ + DetectionEnabled: false, + DetectionInterval: 30 * time.Second, + ClusterType: config.ClusterType{ + Type: "k3s", BinaryPath: config.K3S_DEFAULT_BINARY_PATH, + }, + }, } } diff --git a/node-agent/internal/hostmgr_client/hostmgr_client.go b/node-agent/internal/hostmgr_client/hostmgr_client.go index 4e8df052..03922e76 100644 --- a/node-agent/internal/hostmgr_client/hostmgr_client.go +++ b/node-agent/internal/hostmgr_client/hostmgr_client.go @@ -95,7 +95,7 @@ func ConnectToHostMgr(ctx context.Context, guid string, serverAddr string, tlsCo } -// UpdateInstanceStatus client method sends UpdateInstanceStateStatusByHostGUIDRequest message to the server & receives UpdateInstanceStateStatusByHostGUIDResponse message +// client method sends UpdateInstanceStateStatusByHostGUIDRequest message to the server & receives UpdateInstanceStateStatusByHostGUIDResponse message func (cli *Client) UpdateInstanceStatus(ctx context.Context, insState proto.InstanceState, insStatus proto.InstanceStatus, insDetails string) error { updateInstanceStatusRequest := proto.UpdateInstanceStateStatusByHostGUIDRequest{ HostGuid: cli.HostGUID, @@ -130,3 +130,38 @@ func (cli *Client) UpdateInstanceStatus(ctx context.Context, insState proto.Inst return nil } + +// client method sends UpdateHostSystemInfoByGUIDRequest message to the server & receives UpdateHostSystemInfoByGUIDResponse message +func (cli *Client) UpdateClusterStatus(ctx context.Context, kubeconfig string) error { + + updateHostSystemInfoRequest := proto.UpdateHostSystemInfoByGUIDRequest{ + HostGuid: cli.HostGUID, + SystemInfo: &proto.SystemInfo{KcInfo: &proto.ClusterInfo{Kubeconfig: kubeconfig}}, + } + + op := func() error { + _, err := cli.InfraSouthboundClient.UpdateHostSystemInfoByGUID(ctx, &updateHostSystemInfoRequest) + if err != nil { + log.Errorf("UpdateClusterStatus failed with error: %v", err) + return err + } + return nil + } + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.WithContext(backoff.NewExponentialBackOff(), ctx), NUM_RETRIES)) + if err != nil { + log.Errorf("will try to reconnect because of failure: %v", err) + conn_err := cli.GrpcConn.Close() + if conn_err != nil { + return conn_err + } + conn_err = cli.Connect(ctx) + if conn_err != nil { + return conn_err + } + return err + } + + log.Infof("UpdateClusterStatus sent successfully") + + return nil +} From c041d36d3a913b3621869b4f7efa2407f1760152 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 09:12:05 +0000 Subject: [PATCH 03/11] [gha] Bump aws-actions/amazon-ecr-login from 2.1.4 to 2.1.5 (#704) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christopher Nolan --- .github/workflows/post-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml index 41452a52..d44ad13b 100644 --- a/.github/workflows/post-merge.yml +++ b/.github/workflows/post-merge.yml @@ -121,7 +121,7 @@ jobs: aws-region: us-west-2 - name: Login to Amazon ECR - uses: aws-actions/amazon-ecr-login@19d944daaa35f0fa1d3f7f8af1d3f2e5de25c5b7 # v2.1.4 + uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2.1.5 with: registries: "080137407410" From d31c388aa40005c3da946ce8e10e77eaa3b0c650 Mon Sep 17 00:00:00 2001 From: SYS-EMF Date: Fri, 8 May 2026 12:29:30 +0300 Subject: [PATCH 04/11] Version bump to 1.11.2-dev for node-agent (#705) Co-authored-by: github-bot@intel.com --- node-agent/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-agent/VERSION b/node-agent/VERSION index 720c7384..a439b9b8 100644 --- a/node-agent/VERSION +++ b/node-agent/VERSION @@ -1 +1 @@ -1.11.1 +1.11.2-dev From 1012f2da1070642ff32d463dbe6a0d4282ddae8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 17:40:01 +0100 Subject: [PATCH 05/11] [gomod] Bump github.com/open-edge-platform/infra-managers/host from 1.25.4 to 1.26.1 in /hardware-discovery-agent in the internal-dependencies group (#710) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- hardware-discovery-agent/go.mod | 4 ++-- hardware-discovery-agent/go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hardware-discovery-agent/go.mod b/hardware-discovery-agent/go.mod index e4a1f590..821a3850 100644 --- a/hardware-discovery-agent/go.mod +++ b/hardware-discovery-agent/go.mod @@ -1,13 +1,13 @@ module github.com/open-edge-platform/edge-node-agents/hardware-discovery-agent -go 1.26.1 +go 1.26.3 require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.3.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/open-edge-platform/edge-node-agents/common v1.11.0 - github.com/open-edge-platform/infra-managers/host v1.25.4 + github.com/open-edge-platform/infra-managers/host v1.26.1 github.com/safchain/ethtool v0.7.0 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 diff --git a/hardware-discovery-agent/go.sum b/hardware-discovery-agent/go.sum index a53438d0..a03e2879 100644 --- a/hardware-discovery-agent/go.sum +++ b/hardware-discovery-agent/go.sum @@ -44,8 +44,8 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/open-edge-platform/edge-node-agents/common v1.11.0 h1:90MFBov0zadLXLevlJduptp6LE2xTNcG1w8hOt5QR9U= github.com/open-edge-platform/edge-node-agents/common v1.11.0/go.mod h1:AdcCf9e7GfNA3eR/qFnYHYLa5t1WcQIlCush9s2pBNE= -github.com/open-edge-platform/infra-managers/host v1.25.4 h1:5Ulfpasc3y8F5TwpSZYMQAA58SkIwWItKHrd/lai3gE= -github.com/open-edge-platform/infra-managers/host v1.25.4/go.mod h1:1aEoXXhxW9OIBIK71u+SHg9vn60pemsyLZIzAkBZU9o= +github.com/open-edge-platform/infra-managers/host v1.26.1 h1:4cQo+AHjMN32yq5XsWvzTWbtwpvM5bNZeXXGdy9N4p8= +github.com/open-edge-platform/infra-managers/host v1.26.1/go.mod h1:OqYecX1Y8Evzy9WGll+Z7S16CWMJ5TOBLPPkIUwBg3U= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= From a704c86fb2770a2da26317025df98f237faaf4bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 16:48:35 +0000 Subject: [PATCH 06/11] [gomod] Bump github.com/open-edge-platform/infra-managers/telemetry from 1.25.2 to 1.26.0 in /platform-telemetry-agent in the internal-dependencies group (#709) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- platform-telemetry-agent/go.mod | 8 ++++---- platform-telemetry-agent/go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/platform-telemetry-agent/go.mod b/platform-telemetry-agent/go.mod index b0ff8585..cd41c7b6 100644 --- a/platform-telemetry-agent/go.mod +++ b/platform-telemetry-agent/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/open-edge-platform/edge-node-agents/common v1.11.0 - github.com/open-edge-platform/infra-managers/telemetry v1.25.2 + github.com/open-edge-platform/infra-managers/telemetry v1.26.0 github.com/sirupsen/logrus v1.9.4 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 @@ -33,11 +33,11 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.51.0 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/platform-telemetry-agent/go.sum b/platform-telemetry-agent/go.sum index aa7cb5ac..19261774 100644 --- a/platform-telemetry-agent/go.sum +++ b/platform-telemetry-agent/go.sum @@ -41,8 +41,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/open-edge-platform/edge-node-agents/common v1.11.0 h1:90MFBov0zadLXLevlJduptp6LE2xTNcG1w8hOt5QR9U= github.com/open-edge-platform/edge-node-agents/common v1.11.0/go.mod h1:AdcCf9e7GfNA3eR/qFnYHYLa5t1WcQIlCush9s2pBNE= -github.com/open-edge-platform/infra-managers/telemetry v1.25.2 h1:yOQ+8qYBhzStL+Z+mlQveFexGou6MbEG6BuZTj+OkPc= -github.com/open-edge-platform/infra-managers/telemetry v1.25.2/go.mod h1:gVk9VG0q+k1l9kxfFaa2liPV4sMYYS+l+auXN6fFGGE= +github.com/open-edge-platform/infra-managers/telemetry v1.26.0 h1:1ZyOkGTw+mqPVGTSpTTyyunC7Rkpb1sM8vsMVqloum0= +github.com/open-edge-platform/infra-managers/telemetry v1.26.0/go.mod h1:rQz6l30Kf15ojDVYDTt9zpkvByuLDD8bh0f3vvEWK1w= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -85,18 +85,18 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From c9f5ace4e10e3aaea0257691c75602bf142e0035 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 17:03:50 +0000 Subject: [PATCH 07/11] [gomod] Bump github.com/open-edge-platform/infra-managers/maintenance from 1.25.2 to 1.26.0 in /platform-update-agent in the internal-dependencies group (#708) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- platform-update-agent/go.mod | 4 ++-- platform-update-agent/go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/platform-update-agent/go.mod b/platform-update-agent/go.mod index 540276fe..82cc9eb3 100644 --- a/platform-update-agent/go.mod +++ b/platform-update-agent/go.mod @@ -3,7 +3,7 @@ module github.com/open-edge-platform/edge-node-agents/platform-update-agent -go 1.26.1 +go 1.26.3 require ( github.com/cenkalti/backoff/v4 v4.3.0 @@ -11,7 +11,7 @@ require ( github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/open-edge-platform/edge-node-agents/common v1.11.0 - github.com/open-edge-platform/infra-managers/maintenance v1.25.2 + github.com/open-edge-platform/infra-managers/maintenance v1.26.0 github.com/sirupsen/logrus v1.9.4 github.com/spf13/afero v1.15.0 github.com/stretchr/testify v1.11.1 diff --git a/platform-update-agent/go.sum b/platform-update-agent/go.sum index 2b26bc62..bc52d99a 100644 --- a/platform-update-agent/go.sum +++ b/platform-update-agent/go.sum @@ -171,8 +171,8 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/open-edge-platform/edge-node-agents/common v1.11.0 h1:90MFBov0zadLXLevlJduptp6LE2xTNcG1w8hOt5QR9U= github.com/open-edge-platform/edge-node-agents/common v1.11.0/go.mod h1:AdcCf9e7GfNA3eR/qFnYHYLa5t1WcQIlCush9s2pBNE= -github.com/open-edge-platform/infra-managers/maintenance v1.25.2 h1:AhUYSDXFELkj2v6XxIfj+GhP9u73a/QSiKErcpc3gNc= -github.com/open-edge-platform/infra-managers/maintenance v1.25.2/go.mod h1:Vdsq4hD3a8+uLsoFD2VDW+XWDJOe/uHUXBw8WxCTfio= +github.com/open-edge-platform/infra-managers/maintenance v1.26.0 h1:z3Auky1LupGA1DrmruQCyilUmTPIoGI0rACVobdBaSQ= +github.com/open-edge-platform/infra-managers/maintenance v1.26.0/go.mod h1:YGOfjMOQEYJfdS3LmvrCq/ejKQTL6FJG8JHz+rDdFIg= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 308f4f6c39ad59dc4faf3643fa9be8138994e071 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 17:13:41 +0000 Subject: [PATCH 08/11] [gomod] Bump golang.org/x/sys from 0.43.0 to 0.44.0 in /in-band-manageability in the dependencies group (#707) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- in-band-manageability/go.mod | 2 +- in-band-manageability/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/in-band-manageability/go.mod b/in-band-manageability/go.mod index 95486723..94ca848f 100644 --- a/in-band-manageability/go.mod +++ b/in-band-manageability/go.mod @@ -26,7 +26,7 @@ require ( github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 golang.org/x/net v0.51.0 // indirect - golang.org/x/sys v0.43.0 + golang.org/x/sys v0.44.0 golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect ) diff --git a/in-band-manageability/go.sum b/in-band-manageability/go.sum index ae5cfbb5..ae5bc0a6 100644 --- a/in-band-manageability/go.sum +++ b/in-band-manageability/go.sum @@ -58,8 +58,8 @@ go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLh go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= From 243f1f23aa12e0fb78950caf11d4524ee6cd3a68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 17:30:57 +0000 Subject: [PATCH 09/11] [gomod] Bump golang.org/x/term from 0.42.0 to 0.43.0 in /device-discovery-agent in the dependencies group (#706) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: SYS-EMF --- device-discovery-agent/go.mod | 4 ++-- device-discovery-agent/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/device-discovery-agent/go.mod b/device-discovery-agent/go.mod index 86d0aeec..02ae2f21 100644 --- a/device-discovery-agent/go.mod +++ b/device-discovery-agent/go.mod @@ -9,7 +9,7 @@ require ( github.com/open-edge-platform/infra-onboarding/onboarding-manager v1.40.0 github.com/sirupsen/logrus v1.9.4 golang.org/x/oauth2 v0.36.0 - golang.org/x/term v0.42.0 + golang.org/x/term v0.43.0 google.golang.org/grpc v1.82.0-dev ) @@ -17,7 +17,7 @@ require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect golang.org/x/net v0.52.0 // indirect - golang.org/x/sys v0.43.0 // indirect + golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/device-discovery-agent/go.sum b/device-discovery-agent/go.sum index fa331f8f..1fb2ab4a 100644 --- a/device-discovery-agent/go.sum +++ b/device-discovery-agent/go.sum @@ -40,10 +40,10 @@ golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= From 54abf84b8617f598bc419eb1e4ae5015b754974a Mon Sep 17 00:00:00 2001 From: Andrei Palade Date: Mon, 11 May 2026 11:10:09 +0100 Subject: [PATCH 10/11] Bump host manager version from 1.25.5 to 1.26.2 (#712) Co-authored-by: Palash Goel --- ena-manifest.yaml | 4 ++-- node-agent/VERSION | 2 +- node-agent/go.mod | 6 ++---- node-agent/go.sum | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ena-manifest.yaml b/ena-manifest.yaml index 460528d4..0b6affdb 100644 --- a/ena-manifest.yaml +++ b/ena-manifest.yaml @@ -3,7 +3,7 @@ --- metadata: schemaVersion: 2.0.0 - release: 1.6.5 + release: 1.6.6 repository: codename: 2026.1 component: main @@ -15,7 +15,7 @@ packages: version: 1.10.1 ociArtifact: edge-orch/en/deb/hardware-discovery-agent - name: node-agent - version: 1.11.1 + version: 1.11.2 ociArtifact: edge-orch/en/deb/node-agent - name: platform-manageability-agent version: 0.5.0 diff --git a/node-agent/VERSION b/node-agent/VERSION index a439b9b8..ca717669 100644 --- a/node-agent/VERSION +++ b/node-agent/VERSION @@ -1 +1 @@ -1.11.2-dev +1.11.2 diff --git a/node-agent/go.mod b/node-agent/go.mod index 156a907b..5cd1b217 100644 --- a/node-agent/go.mod +++ b/node-agent/go.mod @@ -1,6 +1,6 @@ module github.com/open-edge-platform/edge-node-agents/node-agent -go 1.26.1 +go 1.26.3 require ( github.com/cenkalti/backoff/v4 v4.3.0 @@ -62,11 +62,9 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 - github.com/open-edge-platform/infra-managers/host v1.25.4 + github.com/open-edge-platform/infra-managers/host v1.26.2 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 golang.org/x/sys v0.43.0 // indirect google.golang.org/grpc v1.82.0-dev ) - -replace github.com/open-edge-platform/infra-managers/host => github.com/open-edge-platform/infra-managers/host v1.25.5-0.20260402155654-ed7b22250fe3 diff --git a/node-agent/go.sum b/node-agent/go.sum index 70c64fb9..7b0215c7 100644 --- a/node-agent/go.sum +++ b/node-agent/go.sum @@ -83,8 +83,8 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/open-edge-platform/edge-node-agents/common v1.11.0 h1:90MFBov0zadLXLevlJduptp6LE2xTNcG1w8hOt5QR9U= github.com/open-edge-platform/edge-node-agents/common v1.11.0/go.mod h1:AdcCf9e7GfNA3eR/qFnYHYLa5t1WcQIlCush9s2pBNE= -github.com/open-edge-platform/infra-managers/host v1.25.5-0.20260402155654-ed7b22250fe3 h1:hlMnCGERzxSXcdfAkfCYn9m6TZa5nWREiT8xbHPhhjM= -github.com/open-edge-platform/infra-managers/host v1.25.5-0.20260402155654-ed7b22250fe3/go.mod h1:uN9Cz6MpJlQwGayspoofYysy/QPs9MqTi1VLh89jObU= +github.com/open-edge-platform/infra-managers/host v1.26.2 h1:Kx83rXVN8aDdiBmR28zFt9H//ECoB1Tlce8V3zwQqZk= +github.com/open-edge-platform/infra-managers/host v1.26.2/go.mod h1:wNkMNr6I9T6Dm4N/tsbdTm/aX2Fv9npUJkI9f5WnOKQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= From 2b6f7db04f5bdcce4a6788a44f55f6cb83ffd2ca Mon Sep 17 00:00:00 2001 From: SYS-EMF Date: Mon, 11 May 2026 16:50:56 +0300 Subject: [PATCH 11/11] Version bump to 1.11.3-dev for node-agent (#713) Co-authored-by: github-bot@intel.com --- node-agent/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-agent/VERSION b/node-agent/VERSION index ca717669..6953c91a 100644 --- a/node-agent/VERSION +++ b/node-agent/VERSION @@ -1 +1 @@ -1.11.2 +1.11.3-dev