93b6911b34
Every deploy across all four source kinds now writes a workload-scoped
event via a shared plugin.EmitDeployEvent helper (replacing the inline
emit duplicated in static/dockerfile, standardizing static's metadata
key site_id->workload_id, and adding emission to image+compose which
were silent). New indexed event_log.workload_id column, EventLogFilter
.WorkloadID, and GET /api/workloads/{id}/events (id pinned from path).
Frontend: a forge "Activity" panel on /apps/[id] reusing EventLogEntry,
live SSE prepend filtered by workload_id, load-more pagination, an
All/Errors severity filter, and a shared toEventLogEntry mapper. en/ru
i18n parity.
Security: compose's failure status emits a generic reason instead of raw
`docker compose up` output, which can echo app secrets and egresses to
operator webhooks (NotificationURL + event-trigger actions); full detail
stays only in the returned error. Rune-safe 256-rune status cap.
Reviewed: go + typescript APPROVE; security HIGH fixed.
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestInsertEvent_RoundTripsWorkloadID(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
in := EventLog{
|
|
Source: "image",
|
|
WorkloadID: "wl-abc",
|
|
Severity: "info",
|
|
Message: "my-app: deployed",
|
|
Metadata: `{"workload_id":"wl-abc"}`,
|
|
}
|
|
saved, err := s.InsertEvent(in)
|
|
if err != nil {
|
|
t.Fatalf("InsertEvent: %v", err)
|
|
}
|
|
if saved.ID == 0 {
|
|
t.Fatal("expected non-zero ID after insert")
|
|
}
|
|
if saved.WorkloadID != "wl-abc" {
|
|
t.Fatalf("returned WorkloadID = %q, want %q", saved.WorkloadID, "wl-abc")
|
|
}
|
|
|
|
rows, err := s.ListEvents(EventLogFilter{WorkloadID: "wl-abc"})
|
|
if err != nil {
|
|
t.Fatalf("ListEvents: %v", err)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("got %d rows, want 1", len(rows))
|
|
}
|
|
got := rows[0]
|
|
if got.WorkloadID != "wl-abc" {
|
|
t.Errorf("WorkloadID = %q, want %q", got.WorkloadID, "wl-abc")
|
|
}
|
|
if got.Source != "image" || got.Severity != "info" || got.Message != "my-app: deployed" {
|
|
t.Errorf("round-trip mismatch: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestInsertEvent_DefaultsWorkloadIDToEmpty(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
// Non-deploy callers leave WorkloadID at its zero value; the column
|
|
// must accept "" (NOT NULL DEFAULT '').
|
|
saved, err := s.InsertEvent(EventLog{Source: "stale", Severity: "warn", Message: "x"})
|
|
if err != nil {
|
|
t.Fatalf("InsertEvent: %v", err)
|
|
}
|
|
if saved.WorkloadID != "" {
|
|
t.Fatalf("WorkloadID = %q, want empty", saved.WorkloadID)
|
|
}
|
|
|
|
rows, err := s.ListEvents(EventLogFilter{Source: "stale"})
|
|
if err != nil {
|
|
t.Fatalf("ListEvents: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].WorkloadID != "" {
|
|
t.Fatalf("expected one unscoped row, got %+v", rows)
|
|
}
|
|
}
|
|
|
|
func TestListEvents_FilterByWorkloadID(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
for _, e := range []EventLog{
|
|
{Source: "image", WorkloadID: "wl-1", Severity: "info", Message: "a"},
|
|
{Source: "image", WorkloadID: "wl-1", Severity: "error", Message: "b"},
|
|
{Source: "compose", WorkloadID: "wl-2", Severity: "info", Message: "c"},
|
|
{Source: "stale", WorkloadID: "", Severity: "warn", Message: "d"},
|
|
} {
|
|
if _, err := s.InsertEvent(e); err != nil {
|
|
t.Fatalf("InsertEvent %q: %v", e.Message, err)
|
|
}
|
|
}
|
|
|
|
// Filtering by wl-1 returns only its two rows.
|
|
rows, err := s.ListEvents(EventLogFilter{WorkloadID: "wl-1"})
|
|
if err != nil {
|
|
t.Fatalf("ListEvents wl-1: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("wl-1: got %d rows, want 2", len(rows))
|
|
}
|
|
for _, r := range rows {
|
|
if r.WorkloadID != "wl-1" {
|
|
t.Errorf("wl-1 filter leaked row with workload_id %q", r.WorkloadID)
|
|
}
|
|
}
|
|
|
|
// wl-2 returns exactly one row.
|
|
rows, err = s.ListEvents(EventLogFilter{WorkloadID: "wl-2"})
|
|
if err != nil {
|
|
t.Fatalf("ListEvents wl-2: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Message != "c" {
|
|
t.Fatalf("wl-2: got %+v, want single row 'c'", rows)
|
|
}
|
|
|
|
// Combined workload + severity filter still narrows correctly.
|
|
rows, err = s.ListEvents(EventLogFilter{WorkloadID: "wl-1", Severity: "error"})
|
|
if err != nil {
|
|
t.Fatalf("ListEvents wl-1+error: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Message != "b" {
|
|
t.Fatalf("wl-1+error: got %+v, want single row 'b'", rows)
|
|
}
|
|
|
|
// No filter returns all four rows (back-compat: unscoped query intact).
|
|
rows, err = s.ListEvents(EventLogFilter{})
|
|
if err != nil {
|
|
t.Fatalf("ListEvents all: %v", err)
|
|
}
|
|
if len(rows) != 4 {
|
|
t.Fatalf("unfiltered: got %d rows, want 4", len(rows))
|
|
}
|
|
}
|