package store import ( "context" "database/sql" "errors" "fmt" "time" ) // Lease ist die aktuelle Lease eines Knotens. Document ist das signierte // JSON (license.Lease); die Zeiten liegen zusätzlich als Spalten vor, damit // der Server ohne Parsen entscheiden kann. type Lease struct { NodeID string IssuedAt time.Time RenewAfter time.Time ValidUntil time.Time Document string } // PutLease speichert bzw. ersetzt die Lease eines Knotens. func (s *Store) PutLease(ctx context.Context, l Lease) error { _, err := s.db.ExecContext(ctx, `INSERT INTO leases (node_id, issued_at, renew_after, valid_until, document) VALUES (?, ?, ?, ?, ?) ON CONFLICT(node_id) DO UPDATE SET issued_at = excluded.issued_at, renew_after = excluded.renew_after, valid_until = excluded.valid_until, document = excluded.document`, l.NodeID, formatTime(l.IssuedAt), formatTime(l.RenewAfter), formatTime(l.ValidUntil), l.Document) if sqliteCode(err) != 0 { return fmt.Errorf("Lease für %s: Knoten: %w", l.NodeID, ErrNotFound) } return err } // GetLease liest die Lease eines Knotens. func (s *Store) GetLease(ctx context.Context, nodeID string) (Lease, error) { l := Lease{NodeID: nodeID} err := s.db.QueryRowContext(ctx, `SELECT issued_at, renew_after, valid_until, document FROM leases WHERE node_id = ?`, nodeID). Scan(dbTime{&l.IssuedAt}, dbTime{&l.RenewAfter}, dbTime{&l.ValidUntil}, &l.Document) if errors.Is(err, sql.ErrNoRows) { return Lease{}, fmt.Errorf("Lease für %s: %w", nodeID, ErrNotFound) } return l, err } // NodeCommand ist ein Befehl in der Warteschlange für einen Knoten. Die CLI // (eigener Prozess) reiht ein, der Server stellt zu. type NodeCommand struct { ID int64 NodeID string Command string Reason string CreatedAt time.Time } // EnqueueNodeCommand reiht einen Befehl für nodeID ein. func (s *Store) EnqueueNodeCommand(ctx context.Context, nodeID, command, reason string) error { if blank(command) { return invalid("Befehl fehlt") } _, err := s.db.ExecContext(ctx, `INSERT INTO node_commands (node_id, command, reason, created_at) VALUES (?, ?, ?, ?)`, nodeID, command, reason, formatTime(s.now())) return mapConstraint(err, "Befehl für "+nodeID) } // PendingNodeCommands liefert die noch nicht zugestellten Befehle in Reihenfolge. func (s *Store) PendingNodeCommands(ctx context.Context, nodeID string) ([]NodeCommand, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, command, reason, created_at FROM node_commands WHERE node_id = ? AND sent_at IS NULL ORDER BY id`, nodeID) if err != nil { return nil, err } defer rows.Close() var out []NodeCommand for rows.Next() { c := NodeCommand{NodeID: nodeID} if err := rows.Scan(&c.ID, &c.Command, &c.Reason, dbTime{&c.CreatedAt}); err != nil { return nil, err } out = append(out, c) } return out, rows.Err() } // MarkNodeCommandSent vermerkt die Zustellung. func (s *Store) MarkNodeCommandSent(ctx context.Context, id int64) error { res, err := s.db.ExecContext(ctx, `UPDATE node_commands SET sent_at = ? WHERE id = ?`, formatTime(s.now()), id) return requireAffected(res, err, fmt.Sprintf("Befehl %d", id)) } // UpdateNodePublicKey tauscht den Schlüssel eines Knotens (rotate_key). func (s *Store) UpdateNodePublicKey(ctx context.Context, nodeID, publicKey string) error { if blank(publicKey) { return invalid("Public Key fehlt") } res, err := s.db.ExecContext(ctx, `UPDATE nodes SET public_key = ? WHERE id = ?`, publicKey, nodeID) if err != nil { return mapConstraint(err, "Knoten "+nodeID) } return requireAffected(res, nil, "Knoten "+nodeID) }