package store import ( "context" "database/sql" "errors" "fmt" "time" ) // CertKind enumerates the kinds of certs orca tracks. 'ca' is the // cluster's internal CA; 'server' is a per-node server cert. type CertKind string const ( CertKindCA CertKind = "ca" CertKindServer CertKind = "server" ) // Cert is the in-memory representation of a row in the `certs` table. type Cert struct { ID string `json:"id"` Kind CertKind `json:"kind"` NodeID string `json:"node_id"` SerialHex string `json:"serial_hex"` SubjectCN string `json:"subject_cn"` IssuerCN string `json:"issuer_cn"` NotBefore time.Time `json:"not_before"` NotAfter time.Time `json:"not_after"` Fingerprint string `json:"fingerprint"` SourcePath string `json:"source_path,omitempty"` CreatedAt time.Time `json:"created_at"` } // CertRepo is a CRUD wrapper around the `certs` table. type CertRepo struct { db *sql.DB } func NewCertRepo(db *sql.DB) *CertRepo { return &CertRepo{db: db} } // Insert persists a new cert. Fills CreatedAt to now() if zero. The caller // is responsible for setting ID, SerialHex, Fingerprint, etc. func (r *CertRepo) Insert(ctx context.Context, c *Cert) error { if c == nil { return errors.New("CertRepo.Insert: nil cert") } if c.ID == "" { return errors.New("CertRepo.Insert: ID is required") } if c.Kind == "" { return errors.New("CertRepo.Insert: Kind is required") } if c.CreatedAt.IsZero() { c.CreatedAt = time.Now().UTC() } _, err := r.db.ExecContext(ctx, `INSERT INTO certs (id, kind, node_id, serial_hex, subject_cn, issuer_cn, not_before, not_after, fingerprint, source_path, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, c.ID, string(c.Kind), c.NodeID, c.SerialHex, c.SubjectCN, c.IssuerCN, c.NotBefore, c.NotAfter, c.Fingerprint, c.SourcePath, c.CreatedAt) if err != nil { return fmt.Errorf("CertRepo.Insert: %w", err) } return nil } // Get returns a single cert by ID. Returns ErrNotFound if absent. func (r *CertRepo) Get(ctx context.Context, id string) (*Cert, error) { row := r.db.QueryRowContext(ctx, `SELECT id, kind, node_id, serial_hex, subject_cn, issuer_cn, not_before, not_after, fingerprint, source_path, created_at FROM certs WHERE id = ?`, id) return scanCert(row) } // List returns all certs ordered by created_at DESC. Use ListByNode / // LatestForKind for filtered queries. func (r *CertRepo) List(ctx context.Context) ([]*Cert, error) { rows, err := r.db.QueryContext(ctx, `SELECT id, kind, node_id, serial_hex, subject_cn, issuer_cn, not_before, not_after, fingerprint, source_path, created_at FROM certs ORDER BY created_at DESC`) if err != nil { return nil, fmt.Errorf("CertRepo.List: %w", err) } defer rows.Close() var certs []*Cert for rows.Next() { c, err := scanCert(rows) if err != nil { return nil, err } certs = append(certs, c) } return certs, rows.Err() } // ListByNode returns certs belonging to a node (or matching node_id for the // CA — CA rows use node_id = ”). func (r *CertRepo) ListByNode(ctx context.Context, nodeID string) ([]*Cert, error) { rows, err := r.db.QueryContext(ctx, `SELECT id, kind, node_id, serial_hex, subject_cn, issuer_cn, not_before, not_after, fingerprint, source_path, created_at FROM certs WHERE node_id = ? ORDER BY created_at DESC`, nodeID) if err != nil { return nil, fmt.Errorf("CertRepo.ListByNode: %w", err) } defer rows.Close() var certs []*Cert for rows.Next() { c, err := scanCert(rows) if err != nil { return nil, err } certs = append(certs, c) } return certs, rows.Err() } // LatestForKind returns the most recent cert of the given kind for the given // node. Returns ErrNotFound if none exists. nodeID may be empty to query // the cluster-wide CA. func (r *CertRepo) LatestForKind(ctx context.Context, nodeID string, kind CertKind) (*Cert, error) { row := r.db.QueryRowContext(ctx, `SELECT id, kind, node_id, serial_hex, subject_cn, issuer_cn, not_before, not_after, fingerprint, source_path, created_at FROM certs WHERE node_id = ? AND kind = ? ORDER BY created_at DESC LIMIT 1`, nodeID, string(kind)) return scanCert(row) } // PruneOlderThan deletes certs beyond the most recent `keep` rows for // (nodeID, kind), ordered by created_at DESC. Returns the number of // rows deleted. `keep` must be > 0; values <= 0 are treated as 1. func (r *CertRepo) PruneOlderThan(ctx context.Context, nodeID, kind string, keep int) (int64, error) { if keep <= 0 { keep = 1 } // Two-step delete: first find the cutoff created_at, then delete // everything older. Done in a single transaction via ExecContext. // modernc/sqlite supports multiple statements in a single Exec only // via the "multi-statement" pragma; we use a subquery instead. res, err := r.db.ExecContext(ctx, `DELETE FROM certs WHERE node_id = ? AND kind = ? AND id NOT IN ( SELECT id FROM certs WHERE node_id = ? AND kind = ? ORDER BY created_at DESC LIMIT ? )`, nodeID, kind, nodeID, kind, keep) if err != nil { return 0, fmt.Errorf("CertRepo.PruneOlderThan: %w", err) } n, _ := res.RowsAffected() return n, nil } // Delete removes a cert by ID. Returns ErrNotFound if no rows affected. func (r *CertRepo) Delete(ctx context.Context, id string) error { res, err := r.db.ExecContext(ctx, `DELETE FROM certs WHERE id = ?`, id) if err != nil { return fmt.Errorf("CertRepo.Delete: %w", err) } rows, _ := res.RowsAffected() if rows == 0 { return ErrNotFound } return nil } func scanCert(s scanner) (*Cert, error) { var ( c Cert kindStr string ) err := s.Scan(&c.ID, &kindStr, &c.NodeID, &c.SerialHex, &c.SubjectCN, &c.IssuerCN, &c.NotBefore, &c.NotAfter, &c.Fingerprint, &c.SourcePath, &c.CreatedAt) if err == sql.ErrNoRows { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("scan cert: %w", err) } c.Kind = CertKind(kindStr) return &c, nil }