From ec96d5687d911f49391f2465ddb542b53ad1f406 Mon Sep 17 00:00:00 2001 From: Samuel Enocsson Date: Mon, 12 Oct 2026 19:45:29 +0200 Subject: [PATCH] fix(agent): reset per-service warnings after a successful build check (#7) --- internal/agent/inventory/collector.go | 15 ++++++++---- internal/agent/inventory/collector_test.go | 27 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/internal/agent/inventory/collector.go b/internal/agent/inventory/collector.go index 832599a..57a98bd 100644 --- a/internal/agent/inventory/collector.go +++ b/internal/agent/inventory/collector.go @@ -62,7 +62,7 @@ type Collector struct { mu sync.Mutex cache map[string]buildEntry // by Compose project - warned map[string]bool // projects whose check failure was logged + warned map[string]bool // logged warnings: "" (check failed) and "/" (service not in config) } // buildEntry is the cached outcome of a build check. It is valid as long as @@ -313,15 +313,22 @@ func (c *Collector) builtServices(ctx context.Context, project string, l stackLa built, err := c.check.BuiltServices(ctx, project, l.workingDir, l.configFiles, l.envFile) c.cache[project] = buildEntry{key: key, built: built, err: err, at: c.now()} if err == nil { - delete(c.warned, project) + // Forget the project's warnings so they are logged again if the + // problem returns. Project names cannot contain "/". + for k := range c.warned { + if k == project || strings.HasPrefix(k, project+"/") { + delete(c.warned, k) + } + } } else { c.warnOnceLocked(project, "compose_build_check_failed", "project", project, "error", err) } return built } -// warnOnce logs msg the first time it is called for key (until the next -// successful check of the project resets it). +// warnOnce logs msg the first time it is called for key. key is a project +// name (its build check failed) or "/" (the service is not +// in the project's config); a successful check of the project forgets both. func (c *Collector) warnOnce(key, msg string, args ...any) { c.mu.Lock() defer c.mu.Unlock() diff --git a/internal/agent/inventory/collector_test.go b/internal/agent/inventory/collector_test.go index 2feb70d..0fc8429 100644 --- a/internal/agent/inventory/collector_test.go +++ b/internal/agent/inventory/collector_test.go @@ -309,3 +309,30 @@ func TestSnapshot_ServiceAbsentFromConfigFailsClosed(t *testing.T) { } assert.Equal(t, 1, strings.Count(logs.String(), "compose_service_not_in_config"), "logged once per project") } + +func TestSnapshot_MissingServiceIsLoggedAgainAfterItCameBack(t *testing.T) { + file := filepath.Join(t.TempDir(), "compose.yaml") + write := func(body string) { + require.NoError(t, os.WriteFile(file, []byte(body), 0o644)) + } + write("v1") + check := &fakeBuildCheck{built: map[string]bool{"db": false}} // "web" missing + c, _ := buildCollector(t, check, file) + var logs bytes.Buffer + c.log = slog.New(slog.NewTextHandler(&logs, nil)) + snap := func() { + _, err := c.Snapshot(context.Background()) + require.NoError(t, err) + } + + snap() + write("v2 changed") + check.built = map[string]bool{"db": false, "web": false} // back + snap() + assert.Empty(t, c.warned, "warnings are forgotten once the project checks out") + write("v3 changed again") + check.built = map[string]bool{"db": false} // missing again + snap() + + assert.Equal(t, 2, strings.Count(logs.String(), "compose_service_not_in_config")) +}