feat(agent): report and refuse Compose services built from source; honour --env-file (#7)

This commit is contained in:
2026-10-12 19:32:05 +02:00
parent 69ef55837e
commit 27c4200dda
8 changed files with 534 additions and 24 deletions
@@ -1,9 +1,16 @@
package inventory
import (
"bytes"
"context"
"errors"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
@@ -209,3 +216,77 @@ func TestSnapshot_ComposeConfigFilesIsRawLabel(t *testing.T) {
require.Len(t, got, 1)
assert.Equal(t, "/s/a/compose.yaml,/s/a/compose.override.yaml", got[0].ConfigFiles)
}
type fakeBuildCheck struct {
built map[string]bool
err error
calls int
}
func (f *fakeBuildCheck) BuiltServices(context.Context, string, string, string, string) (map[string]bool, error) {
f.calls++
return f.built, f.err
}
func buildCollector(t *testing.T, check BuildCheck, files string) (*Collector, *fakeDocker) {
t.Helper()
ct := composeContainer("abc", "app", "web", "app-web", "sha256:img1", "running")
ct.Labels[labelConfigFiles] = files
other := composeContainer("def", "app", "db", "pg:16", "sha256:img2", "running")
other.Labels[labelConfigFiles] = files
cli := &fakeDocker{containers: []container.Summary{ct, other}}
return New(cli, "").WithBuildCheck(check, slog.New(slog.NewTextHandler(io.Discard, nil))), cli
}
func TestSnapshot_BuiltServiceIsMarked(t *testing.T) {
check := &fakeBuildCheck{built: map[string]bool{"web": true, "db": false}}
c, _ := buildCollector(t, check, "/s/app/compose.yaml")
got, err := c.Snapshot(context.Background())
require.NoError(t, err)
byName := map[string]protocol.Service{}
for _, s := range got {
byName[s.ComposeService] = s
}
assert.True(t, byName["web"].Built)
assert.False(t, byName["db"].Built)
}
func TestSnapshot_BuildCheckIsCachedUntilFilesChange(t *testing.T) {
file := filepath.Join(t.TempDir(), "compose.yaml")
require.NoError(t, os.WriteFile(file, []byte("services: {}\n"), 0o644))
check := &fakeBuildCheck{built: map[string]bool{"web": true}}
c, _ := buildCollector(t, check, file)
for range 3 {
_, err := c.Snapshot(context.Background())
require.NoError(t, err)
}
assert.Equal(t, 1, check.calls, "one check per project while files are unchanged")
require.NoError(t, os.WriteFile(file, []byte("services:\n web: {}\n"), 0o644))
_, err := c.Snapshot(context.Background())
require.NoError(t, err)
assert.Equal(t, 2, check.calls, "a changed file re-runs the check")
}
func TestSnapshot_BuildCheckFailureIsNotFatalAndRetriesLater(t *testing.T) {
check := &fakeBuildCheck{err: errors.New("no mounts")}
c, _ := buildCollector(t, check, "/s/app/compose.yaml")
now := time.Now()
c.now = func() time.Time { return now }
var logs bytes.Buffer
c.log = slog.New(slog.NewTextHandler(&logs, nil))
got, err := c.Snapshot(context.Background())
require.NoError(t, err)
for _, s := range got {
assert.False(t, s.Built)
}
_, _ = c.Snapshot(context.Background())
assert.Equal(t, 1, check.calls, "a failure is not retried on every snapshot")
now = now.Add(buildRetry + time.Second)
_, _ = c.Snapshot(context.Background())
assert.Equal(t, 2, check.calls)
assert.Equal(t, 1, strings.Count(logs.String(), "compose_build_check_failed"), "logged once per project")
}