erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
1 parent
9d5789c5d7
commit
c8df9d7651
41 files changed
+6455
-36
No files matched your search
@@ -0,0 +1,155 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NodeStatus ist der Verbindungsstatus eines Agenten.
|
||||
type NodeStatus string
|
||||
|
||||
const (
|
||||
NodeOnline NodeStatus = "online"
|
||||
NodeOffline NodeStatus = "offline"
|
||||
NodeDrained NodeStatus = "drained"
|
||||
)
|
||||
|
||||
func (s NodeStatus) valid() bool {
|
||||
return s == NodeOnline || s == NodeOffline || s == NodeDrained
|
||||
}
|
||||
|
||||
// Node ist ein registrierter Agent.
|
||||
type Node struct {
|
||||
ID string
|
||||
TenantID string
|
||||
Hostname string
|
||||
PublicKey string // Ed25519 Public Key, vom Aufrufer kodiert (z. B. Base64)
|
||||
LastSeen time.Time
|
||||
IPAddress string
|
||||
Status NodeStatus
|
||||
}
|
||||
|
||||
func (n *Node) normalize(now time.Time) error {
|
||||
if n.Status == "" {
|
||||
n.Status = NodeOnline
|
||||
}
|
||||
if n.LastSeen.IsZero() {
|
||||
n.LastSeen = now
|
||||
}
|
||||
n.LastSeen = normTime(n.LastSeen)
|
||||
|
||||
switch {
|
||||
case blank(n.ID):
|
||||
return invalid("Knoten-ID fehlt")
|
||||
case blank(n.TenantID):
|
||||
return invalid("Mandant fehlt")
|
||||
case blank(n.Hostname):
|
||||
return invalid("Hostname fehlt")
|
||||
case blank(n.PublicKey):
|
||||
return invalid("Public Key fehlt")
|
||||
case blank(n.IPAddress):
|
||||
return invalid("IP-Adresse fehlt")
|
||||
case !n.Status.valid():
|
||||
return invalid("unbekannter Status %q", n.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateNode legt einen Knoten an. Status ist ohne Angabe online, LastSeen
|
||||
// ohne Angabe jetzt. ID und Public Key müssen eindeutig sein (ErrExists), der
|
||||
// Mandant muss existieren (ErrNotFound).
|
||||
func (s *Store) CreateNode(ctx context.Context, n Node) (Node, error) {
|
||||
return s.insertNode(ctx, s.db, n)
|
||||
}
|
||||
|
||||
func (s *Store) insertNode(ctx context.Context, db execer, n Node) (Node, error) {
|
||||
if err := n.normalize(s.now()); err != nil {
|
||||
return Node{}, err
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `INSERT INTO nodes
|
||||
(id, tenant_id, hostname, public_key, last_seen, ip_address, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
n.ID, n.TenantID, n.Hostname, n.PublicKey, formatTime(n.LastSeen), n.IPAddress, n.Status)
|
||||
if err != nil {
|
||||
return Node{}, mapConstraint(err, "Knoten "+n.ID)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
const nodeColumns = `id, tenant_id, hostname, public_key, last_seen, ip_address, status`
|
||||
|
||||
func scanNode(row interface{ Scan(...any) error }) (Node, error) {
|
||||
var n Node
|
||||
err := row.Scan(&n.ID, &n.TenantID, &n.Hostname, &n.PublicKey, dbTime{&n.LastSeen}, &n.IPAddress, &n.Status)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *Store) getNode(ctx context.Context, where, arg, what string) (Node, error) {
|
||||
n, err := scanNode(s.db.QueryRowContext(ctx, `SELECT `+nodeColumns+` FROM nodes WHERE `+where+` = ?`, arg))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Node{}, fmt.Errorf("%s: %w", what, ErrNotFound)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// GetNode liest einen Knoten über seine ID.
|
||||
func (s *Store) GetNode(ctx context.Context, id string) (Node, error) {
|
||||
return s.getNode(ctx, "id", id, "Knoten "+id)
|
||||
}
|
||||
|
||||
// GetNodeByPublicKey liest einen Knoten über seinen Public Key (für die
|
||||
// Prüfung signierter Requests).
|
||||
func (s *Store) GetNodeByPublicKey(ctx context.Context, publicKey string) (Node, error) {
|
||||
return s.getNode(ctx, "public_key", publicKey, "Knoten mit diesem Public Key")
|
||||
}
|
||||
|
||||
// ListNodes liefert die Knoten eines Mandanten (tenantID "" = alle), sortiert nach ID.
|
||||
func (s *Store) ListNodes(ctx context.Context, tenantID string) ([]Node, error) {
|
||||
q, args := `SELECT `+nodeColumns+` FROM nodes ORDER BY id`, []any{}
|
||||
if tenantID != "" {
|
||||
q, args = `SELECT `+nodeColumns+` FROM nodes WHERE tenant_id = ? ORDER BY id`, []any{tenantID}
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
nodes := []Node{}
|
||||
for rows.Next() {
|
||||
n, err := scanNode(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
return nodes, rows.Err()
|
||||
}
|
||||
|
||||
// TouchNode vermerkt ein Lebenszeichen: last_seen = jetzt und die aktuelle
|
||||
// IP. Der Status bleibt unverändert (ein gedrainter Knoten bleibt gedraint).
|
||||
func (s *Store) TouchNode(ctx context.Context, id, ip string) error {
|
||||
if blank(ip) {
|
||||
return invalid("IP-Adresse fehlt")
|
||||
}
|
||||
res, err := s.db.ExecContext(ctx, `UPDATE nodes SET last_seen = ?, ip_address = ? WHERE id = ?`,
|
||||
formatTime(s.now()), ip, id)
|
||||
return requireAffected(res, err, "Knoten "+id)
|
||||
}
|
||||
|
||||
// SetNodeStatus ändert den Status eines Knotens.
|
||||
func (s *Store) SetNodeStatus(ctx context.Context, id string, status NodeStatus) error {
|
||||
if !status.valid() {
|
||||
return invalid("unbekannter Status %q", status)
|
||||
}
|
||||
res, err := s.db.ExecContext(ctx, `UPDATE nodes SET status = ? WHERE id = ?`, status, id)
|
||||
return requireAffected(res, err, "Knoten "+id)
|
||||
}
|
||||
|
||||
// DeleteNode löscht einen Knoten; sein Public Key wird damit wieder frei.
|
||||
func (s *Store) DeleteNode(ctx context.Context, id string) error {
|
||||
res, err := s.db.ExecContext(ctx, `DELETE FROM nodes WHERE id = ?`, id)
|
||||
return requireAffected(res, err, "Knoten "+id)
|
||||
}
|
||||
Reference in new issue
Block a user