diff --git a/.gitignore b/.gitignore index 61dcdf4..1c0b9ba 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,8 @@ result # Local scripts monitor-deploy.sh +# Tool versions (asdf, mise) +.tool-versions + # Claude CLAUDE.md diff --git a/agent.go b/agent.go index d0349fe..7026ebb 100644 --- a/agent.go +++ b/agent.go @@ -196,7 +196,11 @@ func runSession(ctx context.Context, baseURL, token string) error { sessionCancel() return } - msgCh <- data + select { + case msgCh <- data: + case <-sessionCtx.Done(): + return + } } }() @@ -319,7 +323,7 @@ func runSession(ctx context.Context, baseURL, token string) error { case data := <-msgCh: var msg channelMsg if err := json.Unmarshal(data, &msg); err != nil { - slog.Warn("invalid message", "error", err) + slog.Debug("invalid message", "error", err) continue } shouldEnd, endErr := handleMessage(sessionCtx, msg, pools, snmpResultCh, mikrotikResultCh, credTestResultCh, monitoringCheckCh, checkResultCh, lldpTopologyResultCh) diff --git a/checks.go b/checks.go index 6f9975b..7e45b01 100644 --- a/checks.go +++ b/checks.go @@ -11,6 +11,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "codeberg.org/towerops-agent/towerops-agent/pb" @@ -28,7 +29,18 @@ var ( MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, } - sslRootCAs = x509.SystemCertPool + sslRootCAsOnce sync.Once + sslRootCAsPool *x509.CertPool + sslRootCAsErr error + + // sslRootCAs returns the system cert pool, cached after first load. + // Overridable for tests. + sslRootCAs = func() (*x509.CertPool, error) { + sslRootCAsOnce.Do(func() { + sslRootCAsPool, sslRootCAsErr = x509.SystemCertPool() + }) + return sslRootCAsPool, sslRootCAsErr + } ) // ExecuteCheck runs a service check and returns the result. @@ -146,17 +158,16 @@ func executeHTTPCheck(ctx context.Context, config *pb.HttpCheckConfig, timeoutMs // Check content regex if provided if config.Regex != "" { + re, err := regexp.Compile(config.Regex) + if err != nil { + return 2, fmt.Sprintf("Invalid regex: %v", err), responseTime + } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return 2, fmt.Sprintf("Failed to read body: %v", err), responseTime } - matched, err := regexp.MatchString(config.Regex, string(body)) - if err != nil { - return 2, fmt.Sprintf("Invalid regex: %v", err), responseTime - } - - if !matched { + if !re.Match(body) { return 2, fmt.Sprintf("Content does not match pattern: %s", config.Regex), responseTime } } diff --git a/lldp.go b/lldp.go index 457728c..1a324ae 100644 --- a/lldp.go +++ b/lldp.go @@ -106,7 +106,6 @@ func discoverLldpNeighbors(client *gosnmp.GoSNMP, deviceID, jobID string) (*pb.L // Walk remote port descriptions remotePorts := make(map[string]string) - var walkErrors []string if err := client.Walk(oidRemPortDesc, func(pdu gosnmp.SnmpPDU) error { key := parseRemoteKey(pdu.Name, oidRemPortDesc) if key != "" { @@ -114,7 +113,6 @@ func discoverLldpNeighbors(client *gosnmp.GoSNMP, deviceID, jobID string) (*pb.L } return nil }); err != nil { - walkErrors = append(walkErrors, fmt.Sprintf("walk remote port descriptions: %v", err)) slog.Warn("failed to walk remote port descriptions", "error", err) } @@ -127,7 +125,6 @@ func discoverLldpNeighbors(client *gosnmp.GoSNMP, deviceID, jobID string) (*pb.L } return nil }); err != nil { - walkErrors = append(walkErrors, fmt.Sprintf("walk remote port IDs: %v", err)) slog.Warn("failed to walk remote port IDs", "error", err) } @@ -140,7 +137,6 @@ func discoverLldpNeighbors(client *gosnmp.GoSNMP, deviceID, jobID string) (*pb.L } return nil }); err != nil { - walkErrors = append(walkErrors, fmt.Sprintf("walk management addresses: %v", err)) slog.Warn("failed to walk management addresses", "error", err) } @@ -172,9 +168,6 @@ func discoverLldpNeighbors(client *gosnmp.GoSNMP, deviceID, jobID string) (*pb.L result.Neighbors = append(result.Neighbors, neighbor) } - // Walk errors are already logged; return result with whatever data was collected - _ = walkErrors - return result, nil } diff --git a/ssh_test.go b/ssh_test.go index 0346a08..4accd2d 100644 --- a/ssh_test.go +++ b/ssh_test.go @@ -7,7 +7,9 @@ import ( "crypto/rand" "fmt" "net" + "path/filepath" "strings" + "sync" "testing" "time" @@ -382,6 +384,15 @@ func TestExecuteMikrotikBackupWithOutput(t *testing.T) { } func TestExecuteMikrotikBackupSessionError(t *testing.T) { + // Reset global host key store to avoid TOFU collisions from other tests + origStore := globalHostKeys + defer func() { + hostKeysOnce = sync.Once{} + globalHostKeys = origStore + }() + hostKeysOnce = sync.Once{} + t.Setenv("TOWEROPS_HOST_KEYS_FILE", filepath.Join(t.TempDir(), "hosts.json")) + // SSH server that accepts connection but rejects all channel requests key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil {