Add admin console access and platform pages (trust, users, keys, workloads, settings)

This commit is contained in:
Emil
2026-08-03 00:11:47 +03:00
parent 7fb1059401
commit e923627ce0
36 changed files with 1562 additions and 86 deletions
+189 -7
View File
@@ -2,6 +2,7 @@ package usecase
import (
"context"
"log/slog"
"sort"
"strings"
"time"
@@ -9,6 +10,7 @@ import (
"github.com/google/uuid"
"github.com/emil28092005/SciMesh/coordinator/internal/domain"
"github.com/emil28092005/SciMesh/coordinator/internal/workloads"
)
// AdminReadRepository is the bounded read projection behind the coordinator
@@ -49,6 +51,9 @@ type AdminNodeInfo struct {
DBEngine string
PublicURL string
Userservice string // base URL; empty when the UI runs without user auth
// WorkerToken reads the shared worker token for the Settings page. It is a
// func so serve mode can read the token file lazily after provisioning.
WorkerToken func() string
}
type AdminStorageView struct {
@@ -135,18 +140,40 @@ type AdminMetricsView struct {
// Admin answers the coordinator admin console from the bounded read model
// plus process info supplied at startup.
type Admin struct {
read AdminReadRepository
uiRead UIReadRepository
node AdminNodeInfo
ready func(context.Context) error
now func() time.Time
read AdminReadRepository
uiRead UIReadRepository
workers WorkerRepository
settings WorkloadSettingsRepository
catalog *workloads.Catalog
node AdminNodeInfo
ready func(context.Context) error
now func() time.Time
log *slog.Logger
audit func(ctx context.Context, action, detail string)
}
func NewAdmin(read AdminReadRepository, uiRead UIReadRepository, node AdminNodeInfo, ready func(context.Context) error, now func() time.Time) *Admin {
func NewAdmin(read AdminReadRepository, uiRead UIReadRepository, workers WorkerRepository,
settings WorkloadSettingsRepository, catalog *workloads.Catalog, node AdminNodeInfo,
ready func(context.Context) error, now func() time.Time) *Admin {
if now == nil {
now = time.Now
}
return &Admin{read: read, uiRead: uiRead, node: node, ready: ready, now: now}
return &Admin{read: read, uiRead: uiRead, workers: workers, settings: settings, catalog: catalog, node: node, ready: ready, now: now}
}
// WithAuditLog attaches an audit sink for sensitive actions (token reveal).
// Without it the admin usecase stays silent about them.
func (a *Admin) WithAuditLog(log *slog.Logger, audit func(ctx context.Context, action, detail string)) *Admin {
a.log = log
a.audit = audit
return a
}
func (a *Admin) revealToken(ctx context.Context, actor string) string {
if a.node.WorkerToken != nil {
return a.node.WorkerToken()
}
return ""
}
func (a *Admin) System(ctx context.Context) (AdminSystemView, error) {
@@ -339,3 +366,158 @@ func (a *Admin) Metrics(ctx context.Context) (AdminMetricsView, error) {
})
return out, nil
}
// AdminWorkerCard is one row of the admin workers table.
type AdminWorkerCard struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capabilities []string `json:"capabilities"`
Trust string `json:"trust"`
OwnerID string `json:"owner_id,omitempty"`
Owner string `json:"owner"`
Completed int `json:"completed"`
LastHeartbeatAt time.Time `json:"last_heartbeat_at"`
}
type AdminWorkersView struct {
Workers []AdminWorkerCard `json:"workers"`
}
// Workers lists the whole fleet for the admin console. Owner emails are
// resolved through the same map as the jobs table (userservice-backed).
func (a *Admin) Workers(ctx context.Context, ownerEmails map[uuid.UUID]string) (AdminWorkersView, error) {
workers, err := a.uiRead.ListWorkers(ctx, 100)
if err != nil {
return AdminWorkersView{}, err
}
out := AdminWorkersView{Workers: make([]AdminWorkerCard, 0, len(workers))}
for _, w := range workers {
card := AdminWorkerCard{
ID: w.ID.String(),
Name: w.Name,
Status: string(w.Status),
Capabilities: w.Capabilities,
Trust: string(w.TrustLevel),
LastHeartbeatAt: w.LastHeartbeatAt,
Owner: "cluster token",
}
if w.OwnerID != nil {
card.OwnerID = w.OwnerID.String()
card.Owner = "user " + shortID(w.OwnerID.String())
if email, ok := ownerEmails[*w.OwnerID]; ok && email != "" {
card.Owner = email
}
}
out.Workers = append(out.Workers, card)
}
return out, nil
}
// SetTrust reclassifies one worker (trusted/untrusted).
func (a *Admin) SetTrust(ctx context.Context, id uuid.UUID, trusted bool) error {
trust := domain.WorkerUntrusted
if trusted {
trust = domain.WorkerTrusted
}
return a.workers.SetTrust(ctx, id, trust)
}
// AdminWorkloadView is the catalog plus the persisted enable flag.
type AdminWorkloadView struct {
Name string `json:"name"`
Description string `json:"description"`
Reduction string `json:"reduction"`
Parameters int `json:"parameters"`
UploadReady bool `json:"upload_ready"`
Enabled bool `json:"enabled"`
DefaultOn bool `json:"default_on"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
type AdminWorkloadsView struct {
Workloads []AdminWorkloadView `json:"workloads"`
}
// Workloads lists the catalog with persisted enable/disable overrides.
func (a *Admin) Workloads(ctx context.Context) (AdminWorkloadsView, error) {
if a.catalog == nil {
return AdminWorkloadsView{}, domain.ErrInvalidInput
}
items := a.catalog.Items()
overrides, err := a.settings.List(ctx)
if err != nil {
return AdminWorkloadsView{}, err
}
enabled := make(map[string]WorkloadSetting, len(overrides))
for _, s := range overrides {
enabled[s.Workload] = s
}
out := AdminWorkloadsView{Workloads: make([]AdminWorkloadView, 0, len(items))}
for _, item := range items {
params := 0
if properties, ok := item.Parameters["properties"].(map[string]any); ok {
params = len(properties)
}
view := AdminWorkloadView{
Name: item.Name,
Description: item.Description,
Reduction: item.Reduction,
Parameters: params,
UploadReady: item.UploadReady,
Enabled: true,
DefaultOn: true,
}
if s, ok := enabled[item.Name]; ok {
view.Enabled = s.Enabled
view.DefaultOn = false
view.UpdatedAt = &s.UpdatedAt
}
out.Workloads = append(out.Workloads, view)
}
return out, nil
}
// SetWorkloadEnabled flips the persisted enable flag. An unknown workload is
// rejected: the admin console must not invent catalog entries.
func (a *Admin) SetWorkloadEnabled(ctx context.Context, name string, enabled bool) error {
if a.catalog == nil || a.catalog.ByName(name) == nil {
return domain.ErrInvalidInput
}
return a.settings.SetEnabled(ctx, name, enabled, a.now())
}
// AdminSettingsView is the read-only cluster configuration the Settings page
// shows. The token is never included; it is revealed only through
// RevealWorkerToken, which audits.
type AdminSettingsView struct {
PublicURL string `json:"public_url"`
Addr string `json:"addr"`
DataDir string `json:"data_dir"`
DBEngine string `json:"db_engine"`
Binary string `json:"binary"`
}
func (a *Admin) Settings() AdminSettingsView {
return AdminSettingsView{
PublicURL: a.node.PublicURL,
Addr: a.node.Addr,
DataDir: a.node.DataDir,
DBEngine: a.node.DBEngine,
Binary: a.node.Binary,
}
}
// RevealWorkerToken returns the shared worker token for the Settings page and
// records the reveal in the audit log. It must only be called for an admin
// session.
func (a *Admin) RevealWorkerToken(ctx context.Context, actor string) string {
token := a.revealToken(ctx, actor)
if a.audit != nil {
a.audit(ctx, "worker token revealed", "by "+actor)
}
if a.log != nil {
a.log.Warn("admin console revealed the worker token", "actor", actor)
}
return token
}
+115
View File
@@ -9,6 +9,7 @@ import (
"github.com/google/uuid"
"github.com/emil28092005/SciMesh/coordinator/internal/domain"
"github.com/emil28092005/SciMesh/coordinator/internal/workloads"
)
type fakeAdminRead struct {
@@ -73,6 +74,34 @@ type fakeUIRead struct {
workers []domain.Worker
}
type fakeSettings struct {
WorkloadSettingsRepository // embedded: only the methods below are exercised
overrides map[string]bool
}
func (f *fakeSettings) GetEnabled(ctx context.Context, workload string) (bool, error) {
if enabled, ok := f.overrides[workload]; ok {
return enabled, nil
}
return true, nil
}
func (f *fakeSettings) List(ctx context.Context) ([]WorkloadSetting, error) {
out := make([]WorkloadSetting, 0, len(f.overrides))
for name, enabled := range f.overrides {
out = append(out, WorkloadSetting{Workload: name, Enabled: enabled, UpdatedAt: time.Now()})
}
return out, nil
}
func (f *fakeSettings) SetEnabled(ctx context.Context, workload string, enabled bool, now time.Time) error {
if f.overrides == nil {
f.overrides = map[string]bool{}
}
f.overrides[workload] = enabled
return nil
}
func (f *fakeUIRead) ListWorkers(ctx context.Context, limit int) ([]domain.Worker, error) {
return f.workers, nil
}
@@ -81,6 +110,9 @@ func adminFixture() *Admin {
return NewAdmin(
&fakeAdminRead{},
&fakeUIRead{},
nil, // workers repo
&fakeSettings{},
nil, // catalog
AdminNodeInfo{
Version: "1.1.0-alpha.1", StartedAt: time.Unix(1_000_000, 0).UTC(),
Binary: "/usr/local/bin/coordinator", Addr: ":8080", DataDir: "/var/lib/scimesh",
@@ -221,3 +253,86 @@ func TestAdminMetricsBuckets(t *testing.T) {
t.Errorf("rate=%.4f avg=%.2f", v.FailureRate, v.AvgShardSeconds)
}
}
type fakeWorkerRepo struct {
WorkerRepository // embedded: only SetTrust is exercised
}
func (f *fakeWorkerRepo) SetTrust(ctx context.Context, id uuid.UUID, trust domain.WorkerTrust) error {
return nil
}
func TestAdminWorkersAndTrust(t *testing.T) {
owner := uuid.New()
a := adminFixture()
a.uiRead = &fakeUIRead{workers: []domain.Worker{
{ID: uuid.New(), Name: "lab-node-01", Status: domain.WorkerBusy, TrustLevel: domain.WorkerTrusted, Capabilities: []string{"similarity-search"}},
{ID: uuid.New(), Name: "emil-laptop", Status: domain.WorkerOnline, TrustLevel: domain.WorkerUntrusted, OwnerID: &owner},
}}
view, err := a.Workers(context.Background(), map[uuid.UUID]string{owner: "alice@lab.org"})
if err != nil {
t.Fatal(err)
}
if len(view.Workers) != 2 {
t.Fatalf("workers = %d, want 2", len(view.Workers))
}
if view.Workers[0].Trust != "trusted" || view.Workers[1].Trust != "untrusted" {
t.Errorf("trust flags wrong: %+v", view.Workers)
}
if view.Workers[1].Owner != "alice@lab.org" {
t.Errorf("owner = %q, want alice@lab.org", view.Workers[1].Owner)
}
}
func TestAdminSetTrust(t *testing.T) {
called := false
a := adminFixture()
a.workers = &fakeWorkerRepo{}
_ = called
if err := a.SetTrust(context.Background(), uuid.New(), false); err != nil {
t.Fatal(err)
}
}
func TestAdminWorkloadsWithOverrides(t *testing.T) {
catalog, err := workloads.Load()
if err != nil {
t.Fatal(err)
}
a := adminFixture()
a.catalog = catalog
a.settings = &fakeSettings{overrides: map[string]bool{"molwt-filter": false}}
view, err := a.Workloads(context.Background())
if err != nil {
t.Fatal(err)
}
found := false
for _, w := range view.Workloads {
if w.Name == "molwt-filter" {
found = true
if w.Enabled || w.DefaultOn {
t.Errorf("molwt-filter: enabled=%v default_on=%v, want disabled override", w.Enabled, w.DefaultOn)
}
}
if w.Name == "similarity-search" && !w.Enabled {
t.Error("similarity-search must stay enabled (no override)")
}
}
if !found {
t.Error("molwt-filter missing from the catalog view")
}
if err := a.SetWorkloadEnabled(context.Background(), "similarity-search", false); err != nil {
t.Fatal(err)
}
if err := a.SetWorkloadEnabled(context.Background(), "nope", false); err == nil {
t.Error("unknown workload must be rejected")
}
}
func TestAdminRevealToken(t *testing.T) {
a := adminFixture()
a.node.WorkerToken = func() string { return "sm_live_secret" }
if got := a.RevealWorkerToken(context.Background(), "admin:user"); got != "sm_live_secret" {
t.Errorf("token = %q", got)
}
}
+23
View File
@@ -94,6 +94,9 @@ type WorkerRepository interface {
// MarkStaleOffline flips every worker last seen before cutoff to offline and
// reports how many changed.
MarkStaleOffline(ctx context.Context, cutoff time.Time) (int64, error)
// SetTrust reclassifies a worker's trust level (trusted/untrusted). Returns
// ErrNotFound when the id is unknown.
SetTrust(ctx context.Context, id uuid.UUID, trust domain.WorkerTrust) error
}
// ArtifactRepository persists artifact metadata. The bytes live in a BlobStore;
@@ -131,6 +134,26 @@ type Clock interface {
Now() time.Time
}
// WorkloadSetting is one persisted enable/disable override from the admin
// console. A workload with no row in the store is enabled by default.
type WorkloadSetting struct {
Workload string `json:"workload"`
Enabled bool `json:"enabled"`
UpdatedAt time.Time `json:"updated_at"`
}
// WorkloadSettingsRepository persists the admin enable/disable overrides on
// top of the embedded workload catalog.
type WorkloadSettingsRepository interface {
// GetEnabled reports whether the workload is enabled. True when the
// workload has no override row (catalog default).
GetEnabled(ctx context.Context, workload string) (bool, error)
// List returns every override row, newest update first.
List(ctx context.Context) ([]WorkloadSetting, error)
// SetEnabled upserts the override.
SetEnabled(ctx context.Context, workload string, enabled bool, now time.Time) error
}
// ErrNotImplemented marks scaffold code with no body yet. Unlike the errors in
// domain, it describes the state of this codebase, not a business rule.
var ErrNotImplemented = errors.New("not implemented")
+12 -2
View File
@@ -24,17 +24,27 @@ type SubmitDataset struct {
clk Clock
maxAttempts int
catalog *workloads.Catalog
settings WorkloadSettingsRepository
}
func NewSubmitDataset(blobs BlobStore, artifacts ArtifactRepository, jobs JobRepository,
tasks TaskRepository, tx TxManager, clk Clock, maxAttempts int, catalog *workloads.Catalog) *SubmitDataset {
return &SubmitDataset{blobs: blobs, artifacts: artifacts, jobs: jobs, tasks: tasks, tx: tx, clk: clk, maxAttempts: maxAttempts, catalog: catalog}
tasks TaskRepository, tx TxManager, clk Clock, maxAttempts int, catalog *workloads.Catalog, settings WorkloadSettingsRepository) *SubmitDataset {
return &SubmitDataset{blobs: blobs, artifacts: artifacts, jobs: jobs, tasks: tasks, tx: tx, clk: clk, maxAttempts: maxAttempts, catalog: catalog, settings: settings}
}
func (uc *SubmitDataset) Execute(ctx context.Context, in SubmitDatasetInput) (SubmitDatasetResult, error) {
if err := validateUploadedWorkload(uc.catalog, in.Workload, in.Parameters); err != nil {
return SubmitDatasetResult{}, err
}
if uc.settings != nil {
enabled, err := uc.settings.GetEnabled(ctx, in.Workload)
if err != nil {
return SubmitDatasetResult{}, err
}
if !enabled {
return SubmitDatasetResult{}, domain.ErrWorkloadDisabled
}
}
if uc.maxAttempts < 1 {
return SubmitDatasetResult{}, domain.ErrInvalidInput
}
+46 -1
View File
@@ -42,6 +42,7 @@ type harness struct {
blobs *memstore.BlobStore
clk *memstore.Clock
taskResults *memstore.TaskResultRepo
settings *memSettings
createJob *usecase.CreateJob
submit *usecase.SubmitDataset
@@ -70,10 +71,11 @@ func newHarness() *harness {
blobs: memstore.NewBlobStore(),
clk: memstore.NewClock(time.Date(2026, 7, 21, 12, 0, 0, 0, time.UTC)),
taskResults: memstore.NewTaskResultRepo(),
settings: newMemSettings(),
}
tx := memstore.Tx{}
h.createJob = usecase.NewCreateJob(h.jobs, h.tasks, tx, h.clk)
h.submit = usecase.NewSubmitDataset(h.blobs, h.arts, h.jobs, h.tasks, tx, h.clk, 3, testCatalog())
h.submit = usecase.NewSubmitDataset(h.blobs, h.arts, h.jobs, h.tasks, tx, h.clk, 3, testCatalog(), h.settings)
h.claim = usecase.NewClaimTask(h.tasks, h.jobs, h.work, tx, h.clk, lease, testCatalog())
h.renew = usecase.NewRenewLease(h.tasks, h.work, tx, h.clk, lease)
h.complete = usecase.NewCompleteTask(h.tasks, h.jobs, h.arts, h.work, h.taskResults, tx, h.clk, 2, testCatalog())
@@ -935,3 +937,46 @@ func TestFinalLeaseExpiryPersistsFailedJobAndCannotBeCancelled(t *testing.T) {
t.Errorf("cancel terminal lease failure = %v, want ErrJobNotCancellable", err)
}
}
// memSettings is an in-memory WorkloadSettingsRepository for tests.
type memSettings struct {
overrides map[string]bool
}
func newMemSettings() *memSettings { return &memSettings{overrides: map[string]bool{}} }
func (m *memSettings) GetEnabled(ctx context.Context, workload string) (bool, error) {
if enabled, ok := m.overrides[workload]; ok {
return enabled, nil
}
return true, nil
}
func (m *memSettings) List(ctx context.Context) ([]usecase.WorkloadSetting, error) { return nil, nil }
func (m *memSettings) SetEnabled(ctx context.Context, workload string, enabled bool, now time.Time) error {
m.overrides[workload] = enabled
return nil
}
func TestSubmitDatasetRejectsDisabledWorkload(t *testing.T) {
h := newHarness()
h.settings.SetEnabled(ctx, "molwt-filter", false, h.clk.Now())
_, err := h.submit.Execute(ctx, usecase.SubmitDatasetInput{
Workload: "molwt-filter", Parameters: map[string]any{"min_molwt": 100, "max_molwt": 600},
RowsPerShard: 2, Filename: "m.tsv", ContentType: "text/tab-separated-values",
Body: strings.NewReader("smiles\nCC\n"),
})
if !errors.Is(err, domain.ErrWorkloadDisabled) {
t.Fatalf("err = %v, want ErrWorkloadDisabled", err)
}
// Re-enabling accepts the same submit.
h.settings.SetEnabled(ctx, "molwt-filter", true, h.clk.Now())
if _, err := h.submit.Execute(ctx, usecase.SubmitDatasetInput{
Workload: "molwt-filter", Parameters: map[string]any{"min_molwt": 100, "max_molwt": 600},
RowsPerShard: 2, Filename: "m.tsv", ContentType: "text/tab-separated-values",
Body: strings.NewReader("chembl_id\tcanonical_smiles\nA\tCC\n"),
}); err != nil {
t.Fatalf("submit after re-enable: %v", err)
}
}