feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
159
internal/webapp/inventory_test.go
Обычный файл
159
internal/webapp/inventory_test.go
Обычный файл
@@ -0,0 +1,159 @@
|
||||
package webapp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// makeFakeProc builds a minimal /proc tree suitable for
|
||||
// ScanProcApps. It writes:
|
||||
//
|
||||
// - a "comm" file for each pid
|
||||
// - a "cmdline" file (NUL-separated)
|
||||
// - a few sockets under fd/ so the listener scan can match
|
||||
//
|
||||
// We deliberately skip the cwd symlink (root-only) and accept that
|
||||
// the readlink call returns an error; ScanProcApps must tolerate
|
||||
// EACCES / ENOENT for permission-denied fds.
|
||||
func makeFakeProc(t *testing.T, pids []fakeProcEntry) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
for _, e := range pids {
|
||||
pdir := filepath.Join(root, strconv.Itoa(e.pid))
|
||||
require.NoError(t, os.MkdirAll(pdir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(pdir, "comm"), []byte(e.comm+"\n"), 0o644))
|
||||
if e.cmdline != "" {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(pdir, "cmdline"), []byte(e.cmdline), 0o644))
|
||||
}
|
||||
if len(e.sockets) > 0 {
|
||||
fdDir := filepath.Join(pdir, "fd")
|
||||
require.NoError(t, os.MkdirAll(fdDir, 0o755))
|
||||
for i, sock := range e.sockets {
|
||||
// fake inode numbers are arbitrary, but must match the
|
||||
// /proc/net/tcp "inode" column for ScanProcApps to
|
||||
// resolve them. Use a stable mapping.
|
||||
target := "socket:[" + strconv.FormatInt(sock, 10) + "]"
|
||||
require.NoError(t, os.Symlink(target, filepath.Join(fdDir, strconv.Itoa(i))))
|
||||
}
|
||||
}
|
||||
}
|
||||
// /proc/net/tcp with state 0A (LISTEN) entries pointing at the
|
||||
// fake inodes. We do this last so test setup is sequential.
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, "net"), 0o755))
|
||||
var lines []string
|
||||
lines = append(lines, " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ")
|
||||
for _, e := range pids {
|
||||
for _, ino := range e.sockets {
|
||||
// "0100007F:1E61" -> 127.0.0.1:7777 in little-endian hex.
|
||||
lines = append(lines, fakeTCPLine(ino, "0100007F:1E61"))
|
||||
}
|
||||
}
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(root, "net", "tcp"),
|
||||
[]byte(joinLines(lines)),
|
||||
0o644))
|
||||
return root
|
||||
}
|
||||
|
||||
type fakeProcEntry struct {
|
||||
pid int
|
||||
comm string
|
||||
cmdline string
|
||||
sockets []int64 // fake inode numbers
|
||||
}
|
||||
|
||||
func fakeTCPLine(inode int64, local string) string {
|
||||
// 4 hex chars for tx/rx queue (always 0), 8 hex for tr/tm->when,
|
||||
// 8 hex for retrnsmt, 1 hex for uid (0), 1 hex for timeout (0),
|
||||
// then inode (10 hex zero-padded). The trailing fields are zero-
|
||||
// filled so the scanner skips them.
|
||||
inodeHex := strconv.FormatInt(inode, 16)
|
||||
for len(inodeHex) < 8 {
|
||||
inodeHex = "0" + inodeHex
|
||||
}
|
||||
return " 0: " + local + " 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 " + inodeHex + " 0 0 0 0 0"
|
||||
}
|
||||
|
||||
func joinLines(ls []string) string {
|
||||
out := ""
|
||||
for i, l := range ls {
|
||||
if i > 0 {
|
||||
out += "\n"
|
||||
}
|
||||
out += l
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestScanProcAppsEmpty(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
apps, err := ScanProcApps(root)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, apps)
|
||||
}
|
||||
|
||||
func TestScanProcAppsSingleProcess(t *testing.T) {
|
||||
root := makeFakeProc(t, []fakeProcEntry{
|
||||
{pid: 42, comm: "rsmon-worker", cmdline: "rsmon-worker --foo\x00bar", sockets: []int64{1001}},
|
||||
})
|
||||
apps, err := ScanProcApps(root)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 1)
|
||||
assert.Equal(t, "rsmon-worker", apps[0].Name)
|
||||
assert.Equal(t, 42, apps[0].PID)
|
||||
assert.Equal(t, "rsmon-worker --foo bar", apps[0].Cmdline)
|
||||
// ports slice should have the resolved address.
|
||||
assert.Contains(t, apps[0].Ports, "127.0.0.1:7777/tcp")
|
||||
}
|
||||
|
||||
func TestScanProcAppsSkipsKernelThreads(t *testing.T) {
|
||||
// A pid directory with no comm file is treated as a vanished
|
||||
// process; ScanProcApps must skip it rather than panic.
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, "1"), 0o755))
|
||||
apps, err := ScanProcApps(root)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, apps)
|
||||
}
|
||||
|
||||
func TestInventoryStoreReplace(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := OpenStore(filepath.Join(dir, "webapp.db"))
|
||||
require.NoError(t, err)
|
||||
defer store.Close() //nolint:errcheck
|
||||
|
||||
inv := NewInventory(store, nil)
|
||||
_ = inv // currently no public method to inject scanned rows;
|
||||
// we exercise ReplaceApps directly via the store.
|
||||
now := mustParseTime(t)
|
||||
rows := []App{
|
||||
{Name: "rsmon-worker", Source: "process", PID: 1, Ports: "7401/tcp", LastSeen: now},
|
||||
{Name: "postgres", Source: "process", PID: 2, Ports: "5432/tcp", LastSeen: now},
|
||||
}
|
||||
ctx := context.Background()
|
||||
require.NoError(t, store.ReplaceApps(ctx, rows))
|
||||
got, err := store.ListApps(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 2)
|
||||
assert.Equal(t, "rsmon-worker", got[0].Name)
|
||||
|
||||
require.NoError(t, store.ReplaceApps(ctx, []App{
|
||||
{Name: "redis", Source: "process", PID: 3, Ports: "6379/tcp", LastSeen: now},
|
||||
}))
|
||||
got, err = store.ListApps(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, 1)
|
||||
assert.Equal(t, "redis", got[0].Name)
|
||||
}
|
||||
|
||||
func mustParseTime(t *testing.T) time.Time {
|
||||
t.Helper()
|
||||
return time.Now().UTC()
|
||||
}
|
||||
Ссылка в новой задаче
Block a user