Newer
Older
EnvoyControlPlane / internal / pkg / storage / sqlite.go
package storage

import (
	"database/sql"
	"fmt"
)

// ---------------------------
// SQLite Strategy
// ---------------------------

type SQLiteStrategy struct{}

func (s *SQLiteStrategy) DriverName() string { return "sqlite" }

// Placeholder returns ?.
func (s *SQLiteStrategy) Placeholder(n int) string { return "?" }

// GetTimeNow returns the SQLite function for current timestamp.
func (s *SQLiteStrategy) GetTimeNow() string { return "CURRENT_TIMESTAMP" }

// GetTrueValue returns the integer representation.
func (s *SQLiteStrategy) GetTrueValue() string { return "1" }

// GetFalseValue returns the integer representation.
func (s *SQLiteStrategy) GetFalseValue() string { return "0" }

func (s *SQLiteStrategy) InitSchemaSQL() string {
	return `
		CREATE TABLE IF NOT EXISTS clusters (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT UNIQUE NOT NULL,
			data TEXT NOT NULL,
			enabled BOOLEAN DEFAULT 1,
			updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
		);
		CREATE TABLE IF NOT EXISTS listeners (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT UNIQUE NOT NULL,
			data TEXT NOT NULL,
			enabled BOOLEAN DEFAULT 1,
			updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
		);
		CREATE TABLE IF NOT EXISTS secrets (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT UNIQUE NOT NULL,
			data TEXT NOT NULL,
			enabled BOOLEAN DEFAULT 1,
			updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
			domain TEXT NULL
		);
		CREATE TABLE IF NOT EXISTS certificates (
			domain TEXT PRIMARY KEY,
			email TEXT NOT NULL,
			cert_pem BLOB NOT NULL,
			key_pem BLOB NOT NULL,
			full_chain_pem BLOB DEFAULT '', -- Added: full_chain_pem with a default empty blob for backward compatibility
			account_key BLOB NOT NULL,
			account_url TEXT NOT NULL,
			issuer_type TEXT DEFAULT '',
			secret_name TEXT DEFAULT '',
			updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
			enable_rotation BOOLEAN DEFAULT 0,
			renew_before INTEGER DEFAULT 0
		);
		-- Add ALTER TABLE statement for existing databases to add the full_chain_pem column
		-- This must be separate as ALTER TABLE IF NOT EXISTS ADD COLUMN is not universally supported in SQLite for BLOB.
		-- A simpler approach is to just try adding and ignore the error if it exists, as InitSchema does.
		-- However, for robustness, we check for column existence first.
		PRAGMA foreign_keys = OFF; -- Disable foreign key checks for schema modification
		INSERT OR IGNORE INTO sqlite_master (type, name, tbl_name, rootpage, sql) VALUES ('table', 'certificates', 'certificates', 0, 'CREATE TABLE certificates (domain TEXT PRIMARY KEY, email TEXT NOT NULL, cert_pem BLOB NOT NULL, key_pem BLOB NOT NULL, full_chain_pem BLOB DEFAULT '', account_key BLOB NOT NULL, account_url TEXT NOT NULL, issuer_type TEXT DEFAULT '', secret_name TEXT DEFAULT '', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, enable_rotation BOOLEAN DEFAULT 0, renew_before INTEGER DEFAULT 0)');
		ALTER TABLE certificates ADD COLUMN full_chain_pem BLOB DEFAULT '';
		PRAGMA foreign_keys = ON; -- Re-enable foreign key checks

		CREATE TABLE IF NOT EXISTS extension_configs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT UNIQUE NOT NULL,
            data TEXT NOT NULL,
            enabled BOOLEAN DEFAULT 1,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );`
}

func (s *SQLiteStrategy) SaveCertificateSQL(ph []string) string {
	// ph should now have 11 elements for all fields including full_chain_pem
	return `
		INSERT INTO certificates (domain, email, cert_pem, key_pem, full_chain_pem, account_key, account_url, issuer_type, secret_name, updated_at, enable_rotation, renew_before)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)
		ON CONFLICT(domain) DO UPDATE SET
			email = excluded.email,
			cert_pem = excluded.cert_pem,
			key_pem = excluded.key_pem,
			full_chain_pem = excluded.full_chain_pem, -- Added: full_chain_pem update
			account_key = excluded.account_key,
			account_url = excluded.account_url,
			issuer_type = excluded.issuer_type,
			secret_name = excluded.secret_name,
			updated_at = CURRENT_TIMESTAMP,
			enable_rotation = excluded.enable_rotation,
			renew_before = excluded.renew_before`
}

func (s *SQLiteStrategy) SaveSecretSQL(ph []string) string {
	return `
		INSERT INTO secrets (name, data, enabled, updated_at, domain)
		VALUES (?, ?, 1, CURRENT_TIMESTAMP, ?)
		ON CONFLICT(name) DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP, domain=excluded.domain`
}

func (s *SQLiteStrategy) SaveClusterSQL(ph []string) string {
	return `
		INSERT INTO clusters (name, data, enabled, updated_at)
		VALUES (?, ?, 1, CURRENT_TIMESTAMP)
		ON CONFLICT(name) DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP`
}

func (s *SQLiteStrategy) SaveListenerSQL(ph []string) string {
	return `
		INSERT INTO listeners (name, data, enabled, updated_at)
		VALUES (?, ?, 1, CURRENT_TIMESTAMP)
		ON CONFLICT(name) DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP`
}

func (s *SQLiteStrategy) SaveExtensionConfigSQL(ph []string) string {
	// ph[0] = name, ph[1] = data
	return `
        INSERT INTO extension_configs (name, data, enabled, updated_at)
        VALUES (?, ?, 1, CURRENT_TIMESTAMP)
        ON CONFLICT(name) DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP`
}

func (s *SQLiteStrategy) DumpSelectFields(table string) string {
	if table == "secrets" {
		return "name, data, domain" // MODIFIED: Include domain for secrets
	}
	return "name, data"
}

func (s *SQLiteStrategy) ScanRawRow(rows *sql.Rows, row *RawRow, table string) error {
	var dataStr string
	if table == "secrets" {
		// SQLite: 3 fields (name, TEXT data, domain)
		if err := rows.Scan(&row.Name, &dataStr, &row.Domain); err != nil {
			return err
		}
	} else {
		// SQLite: 2 fields (name, TEXT data)
		if err := rows.Scan(&row.Name, &dataStr); err != nil {
			return err
		}
	}
	row.Data = []byte(dataStr)
	return nil
}

func (s *SQLiteStrategy) RestoreRawRowSQL(table string) string {
	if table == "secrets" {
		return fmt.Sprintf(`
			INSERT INTO secrets (name, data, enabled, updated_at, domain)
			VALUES (?, ?, 1, CURRENT_TIMESTAMP, ?)
			ON CONFLICT(name) DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP, domain=excluded.domain`,
		) // MODIFIED: Handle domain for secrets
	}
	// clusters or listeners
	return fmt.Sprintf(`
		INSERT INTO %s (name, data, enabled, updated_at)
		VALUES (?, ?, 1, CURRENT_TIMESTAMP)
		ON CONFLICT(name)
		DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP`, table)
}

func (s *SQLiteStrategy) ClearTableSQL(table string) string {
	// SQLite safe way to clear
	return fmt.Sprintf("DELETE FROM %s", table)
}