mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-08-31 08:57:14 +02:00
Replace the free-form service_type string with a ServiceType enum (ccms, ccmc, ccb, cces, ccsa, ccnc, ccem). The short code is the canonical value stored in the DB, the config-tree directory name and a NATS discovery subject token; Description() gives the human-readable name. Add a second scope alongside per-node agents: ScopeInfra covers cluster-independent monitoring infrastructure services, which are identified by (hostname, service_type) and carry an empty cluster. InfraRegistry is the ScopeInfra sibling of Registry and shares the same table, state machine and config-revision handshake. It deliberately exposes no StartSweep: MarkStale ages rows by last_heartbeat regardless of scope, so a single sweep goroutine per process covers both scopes. On the repository side ServiceDB gains a Scope column and the two queries the discovery publisher and infra enumeration need, ListActive() and ListByScope(). ResetConnection() now also resets the fleet repository singleton so tests get a fresh handle. Migration 13 is edited in place rather than superseded by a new migration: the service table is unreleased, so no existing deployment has it yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
3.8 KiB
Go
128 lines
3.8 KiB
Go
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
|
// All rights reserved. This file is part of cc-backend.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package fleet
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// setupDB migrates a fresh temp database to the current schema and wires the
|
|
// repository singletons to it, isolated per test.
|
|
func setupDB(t *testing.T) {
|
|
t.Helper()
|
|
cclog.Init("warn", true)
|
|
|
|
dbfile := filepath.Join(t.TempDir(), "fleet.db")
|
|
if err := repository.ResetConnection(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repository.MigrateDB(dbfile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repository.Connect(dbfile)
|
|
t.Cleanup(func() { repository.ResetConnection() })
|
|
}
|
|
|
|
func TestInfraRegistry(t *testing.T) {
|
|
setupDB(t)
|
|
reg := NewInfraRegistry()
|
|
|
|
t.Run("register requires hostname and a valid service_type", func(t *testing.T) {
|
|
if _, err := reg.Register(InfraRegistrationRequest{ServiceType: ServiceTypeMetricStore}); err == nil {
|
|
t.Fatal("expected error for missing hostname")
|
|
}
|
|
if _, err := reg.Register(InfraRegistrationRequest{Hostname: "ms01"}); err == nil {
|
|
t.Fatal("expected error for missing service_type")
|
|
}
|
|
if _, err := reg.Register(InfraRegistrationRequest{Hostname: "ms01", ServiceType: "bogus"}); err == nil {
|
|
t.Fatal("expected error for unknown service_type")
|
|
}
|
|
})
|
|
|
|
var instanceID string
|
|
t.Run("register issues identity with revision 0", func(t *testing.T) {
|
|
r, err := reg.Register(InfraRegistrationRequest{
|
|
Hostname: "ms01", ServiceType: ServiceTypeMetricStore,
|
|
MetaData: map[string]string{"version": "1.2.3"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.InstanceID == "" || r.ConfigRevision != 0 {
|
|
t.Fatalf("unexpected registration: %+v", r)
|
|
}
|
|
instanceID = r.InstanceID
|
|
})
|
|
|
|
t.Run("get returns infra scope and empty cluster", func(t *testing.T) {
|
|
svc, err := reg.Get(instanceID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if svc.Cluster != "" || svc.ServiceType != ServiceTypeMetricStore || svc.State != "pending" {
|
|
t.Fatalf("unexpected service: %+v", svc)
|
|
}
|
|
if svc.MetaData["version"] != "1.2.3" {
|
|
t.Fatalf("metadata not round-tripped: %+v", svc.MetaData)
|
|
}
|
|
})
|
|
|
|
t.Run("heartbeat activates known, rejects unknown", func(t *testing.T) {
|
|
if err := reg.Heartbeat(instanceID, time.Unix(5000, 0)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc, _ := reg.Get(instanceID)
|
|
if svc.State != "active" || svc.LastHeartbeat == nil {
|
|
t.Fatalf("heartbeat did not activate: %+v", svc)
|
|
}
|
|
if err := reg.Heartbeat("unknown", time.Unix(5000, 0)); !errors.Is(err, ErrUnknownInstance) {
|
|
t.Fatalf("want ErrUnknownInstance, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("list returns only infra services", func(t *testing.T) {
|
|
// A cluster-scope agent must not appear in the infra listing.
|
|
cReg := NewRegistry(time.Hour)
|
|
if _, err := cReg.Register(RegistrationRequest{
|
|
Cluster: "fritz", Hostname: "node01", ServiceType: ServiceTypeCollector,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
list, err := reg.List()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(list) != 1 || list[0].ServiceType != ServiceTypeMetricStore {
|
|
t.Fatalf("unexpected infra list: %+v", list)
|
|
}
|
|
})
|
|
|
|
t.Run("ack config records revision, deregister is terminal", func(t *testing.T) {
|
|
if err := reg.AckConfig(instanceID, 99); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc, _ := reg.Get(instanceID)
|
|
if svc.ConfigRevision != 99 {
|
|
t.Fatalf("want revision 99, got %d", svc.ConfigRevision)
|
|
}
|
|
|
|
if err := reg.Deregister(instanceID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := reg.Heartbeat(instanceID, time.Unix(6000, 0)); !errors.Is(err, ErrUnknownInstance) {
|
|
t.Fatalf("heartbeat after deregister should fail: %v", err)
|
|
}
|
|
})
|
|
}
|