hub v0.51.0: DR-tier-by-default — per-customer dr_tier flag (default ON, legacy backfill from reality), cascade stages, WG-registration auto-provision hook, offsite-requires-DR guard (F-6 policy), host-page capability chips (inactive=neutral)
Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
This commit is contained in:
+72
-11
@@ -142,6 +142,15 @@ func (s *Store) migrate() error {
|
||||
// at 1, so an already-running box records that as its baseline on its next report without restarting.
|
||||
s.db.Exec("ALTER TABLE customer_configs ADD COLUMN config_version INTEGER NOT NULL DEFAULT 1")
|
||||
|
||||
// v0.51.0 (DR-tier-by-default): per-customer DR-tier flag. NEW customers default ON (the
|
||||
// create handler sets it); the column default 0 is the LEGACY initialization — an existing
|
||||
// customer is only flipped ON by the one-time backfill (initialize from reality: its host
|
||||
// already carries an ENABLED pbs_dr descriptor; never auto-cascade a legacy box). The
|
||||
// backfill runs exactly once — only when the ALTER actually added the column — and is
|
||||
// DEFERRED to the end of migrate(): the hosts table it reads is created further down (a
|
||||
// fresh DB reaches the ALTER before the CREATE).
|
||||
_, drTierAlterErr := s.db.Exec("ALTER TABLE customer_configs ADD COLUMN dr_tier INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
// v0.15.0: hub_settings — a tiny key/value table for operator-set globals that must survive
|
||||
// restarts (currently only the global controller-version floor). The config/env DEFAULT_MIN_
|
||||
// CONTROLLER_VERSION is the FALLBACK; a row here (set via the operator UI) overrides it.
|
||||
@@ -546,6 +555,14 @@ func (s *Store) migrate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// v0.51.0 dr_tier one-time legacy backfill — see the ALTER above; runs last so every table
|
||||
// it touches (hosts, customer_configs) exists on a fresh DB too (where it finds nothing).
|
||||
if drTierAlterErr == nil {
|
||||
if err := s.backfillDRTierFromDescriptors(); err != nil {
|
||||
return fmt.Errorf("dr_tier backfill: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -892,6 +909,43 @@ func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
// backfillDRTierFromDescriptors is the ONE-TIME legacy initialization for the v0.51.0 dr_tier
|
||||
// column (called only when the ALTER just added it): a customer whose host already carries an
|
||||
// ENABLED pbs_dr descriptor in desired_json gets dr_tier=1 — initialize from reality; everyone
|
||||
// else stays 0 (never auto-cascade a legacy box). JSON is inspected in Go, not by LIKE, so field
|
||||
// order/whitespace can't fool it.
|
||||
func (s *Store) backfillDRTierFromDescriptors() error {
|
||||
rows, err := s.db.Query(`SELECT customer_id, desired_json FROM hosts WHERE desired_json LIKE '%pbs_dr%'`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
enabled := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var customerID, desired string
|
||||
if err := rows.Scan(&customerID, &desired); err != nil {
|
||||
return err
|
||||
}
|
||||
var doc struct {
|
||||
PBSDR *struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"pbs_dr"`
|
||||
}
|
||||
if json.Unmarshal([]byte(desired), &doc) == nil && doc.PBSDR != nil && doc.PBSDR.Enabled {
|
||||
enabled[customerID] = true
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for customerID := range enabled {
|
||||
if _, err := s.db.Exec(`UPDATE customer_configs SET dr_tier = 1 WHERE customer_id = ?`, customerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CustomerConfig holds a pre-provisioned customer configuration.
|
||||
type CustomerConfig struct {
|
||||
CustomerID string
|
||||
@@ -908,8 +962,14 @@ type CustomerConfig struct {
|
||||
// ConfigVersion is the monotonic config counter (bumped on every SaveCustomerConfig). The report
|
||||
// ACK advertises it; the controller re-pulls + self-restarts when it changes. Never a YAML hash.
|
||||
ConfigVersion int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
// DRTier (v0.51.0, DR-tier-by-default) is the per-customer DR-tier flag — the operator INTENT
|
||||
// the pbsdr cascade converges toward. Default ON for NEW customers (create handler); legacy
|
||||
// rows were initialized from reality by the one-time backfill (enabled descriptor → ON).
|
||||
// OFF = zero Felhom-side cost (no ep0 namespace is provisioned) and offsite provisioning is
|
||||
// refused (the escrow ceremony depends on the PBS key — drill F-6, closed by policy).
|
||||
DRTier bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// SaveCustomerConfig creates or updates a customer configuration. Every save BUMPS config_version
|
||||
@@ -921,8 +981,8 @@ type CustomerConfig struct {
|
||||
func (s *Store) SaveCustomerConfig(cfg *CustomerConfig) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO customer_configs (customer_id, customer_name, domain, email,
|
||||
retrieval_password, api_key, config_json, min_controller_version, config_version, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'))
|
||||
retrieval_password, api_key, config_json, min_controller_version, dr_tier, config_version, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'))
|
||||
ON CONFLICT(customer_id) DO UPDATE SET
|
||||
customer_name = excluded.customer_name,
|
||||
domain = excluded.domain,
|
||||
@@ -931,10 +991,11 @@ func (s *Store) SaveCustomerConfig(cfg *CustomerConfig) error {
|
||||
api_key = excluded.api_key,
|
||||
config_json = excluded.config_json,
|
||||
min_controller_version = excluded.min_controller_version,
|
||||
dr_tier = excluded.dr_tier,
|
||||
config_version = customer_configs.config_version + 1,
|
||||
updated_at = datetime('now')`,
|
||||
cfg.CustomerID, cfg.CustomerName, cfg.Domain, cfg.Email,
|
||||
cfg.RetrievalPassword, cfg.APIKey, cfg.ConfigJSON, cfg.MinControllerVersion,
|
||||
cfg.RetrievalPassword, cfg.APIKey, cfg.ConfigJSON, cfg.MinControllerVersion, cfg.DRTier,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -945,12 +1006,12 @@ func (s *Store) GetCustomerConfig(customerID string) (*CustomerConfig, error) {
|
||||
var createdAt, updatedAt string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT customer_id, customer_name, domain, email,
|
||||
retrieval_password, api_key, config_json, status, min_controller_version, config_version, created_at, updated_at
|
||||
retrieval_password, api_key, config_json, status, min_controller_version, dr_tier, config_version, created_at, updated_at
|
||||
FROM customer_configs WHERE customer_id = ?`,
|
||||
customerID,
|
||||
).Scan(&cfg.CustomerID, &cfg.CustomerName, &cfg.Domain, &cfg.Email,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON, &cfg.Status, &cfg.MinControllerVersion,
|
||||
&cfg.ConfigVersion, &createdAt, &updatedAt)
|
||||
&cfg.DRTier, &cfg.ConfigVersion, &createdAt, &updatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1000,7 +1061,7 @@ func (s *Store) ConsumeOneTimeSecret(customerID string) (string, error) {
|
||||
func (s *Store) ListCustomerConfigs() ([]CustomerConfig, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT customer_id, customer_name, domain, email,
|
||||
retrieval_password, api_key, config_json, status, min_controller_version, config_version, created_at, updated_at
|
||||
retrieval_password, api_key, config_json, status, min_controller_version, dr_tier, config_version, created_at, updated_at
|
||||
FROM customer_configs ORDER BY customer_id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1013,7 +1074,7 @@ func (s *Store) ListCustomerConfigs() ([]CustomerConfig, error) {
|
||||
var createdAt, updatedAt string
|
||||
if err := rows.Scan(&cfg.CustomerID, &cfg.CustomerName, &cfg.Domain, &cfg.Email,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON, &cfg.Status, &cfg.MinControllerVersion,
|
||||
&cfg.ConfigVersion, &createdAt, &updatedAt); err != nil {
|
||||
&cfg.DRTier, &cfg.ConfigVersion, &createdAt, &updatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.CreatedAt = parseSQLiteTime(createdAt)
|
||||
@@ -1036,12 +1097,12 @@ func (s *Store) GetCustomerConfigByAPIKey(apiKey string) (*CustomerConfig, error
|
||||
var createdAt, updatedAt string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT customer_id, customer_name, domain, email,
|
||||
retrieval_password, api_key, config_json, status, min_controller_version, config_version, created_at, updated_at
|
||||
retrieval_password, api_key, config_json, status, min_controller_version, dr_tier, config_version, created_at, updated_at
|
||||
FROM customer_configs WHERE api_key = ?`,
|
||||
apiKey,
|
||||
).Scan(&cfg.CustomerID, &cfg.CustomerName, &cfg.Domain, &cfg.Email,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON, &cfg.Status, &cfg.MinControllerVersion,
|
||||
&cfg.ConfigVersion, &createdAt, &updatedAt)
|
||||
&cfg.DRTier, &cfg.ConfigVersion, &createdAt, &updatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user