feat(fleet): add service type enum and infra scope

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>
This commit is contained in:
2026-08-27 08:06:50 +02:00
co-authored by Claude Opus 5
parent 5f94ae3b68
commit ecb1ba2bbe
11 changed files with 618 additions and 11 deletions
+11 -6
View File
@@ -21,6 +21,7 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/ClusterCockpit/cc-backend/internal/repository"
@@ -36,7 +37,7 @@ var ErrUnknownInstance = errors.New("fleet: unknown or deregistered instance id"
type Service struct {
Cluster string
Hostname string
ServiceType string
ServiceType ServiceType
InstanceID string
State string
RegisteredAt time.Time
@@ -50,7 +51,7 @@ type Service struct {
type RegistrationRequest struct {
Cluster string
Hostname string
ServiceType string
ServiceType ServiceType
MetaData map[string]string
}
@@ -85,8 +86,11 @@ func NewRegistry(staleAfter time.Duration) *Registry {
// instance_id plus the config_revision it currently has on record (0 for a
// never-before-seen service, so the caller knows to pull its initial config).
func (r *Registry) Register(req RegistrationRequest) (*Registration, error) {
if req.Cluster == "" || req.Hostname == "" || req.ServiceType == "" {
return nil, errors.New("fleet: cluster, hostname and service_type are required")
if req.Cluster == "" || req.Hostname == "" {
return nil, errors.New("fleet: cluster and hostname are required")
}
if !req.ServiceType.Valid() {
return nil, fmt.Errorf("fleet: unknown service_type %q", req.ServiceType)
}
instanceID, err := generateInstanceID()
@@ -102,8 +106,9 @@ func (r *Registry) Register(req RegistrationRequest) (*Registration, error) {
svc := &repository.ServiceDB{
Cluster: req.Cluster,
Hostname: req.Hostname,
ServiceType: req.ServiceType,
ServiceType: string(req.ServiceType),
InstanceID: instanceID,
Scope: ScopeCluster,
RegisteredAt: time.Now().Unix(),
MetaData: metaJSON,
}
@@ -239,7 +244,7 @@ func toService(row *repository.ServiceDB) (*Service, error) {
svc := &Service{
Cluster: row.Cluster,
Hostname: row.Hostname,
ServiceType: row.ServiceType,
ServiceType: ServiceType(row.ServiceType),
InstanceID: row.InstanceID,
State: row.State,
RegisteredAt: time.Unix(row.RegisteredAt, 0),