Files
member-console/cmd/bootstate.go
T
cgalo5758 23d5bad024 Add integration delivery core: provider connections, desired-state
records, outbox, lifecycle requests, relay, push, read-back, and FedWiki
lifecycle workflows

- Add core.provider_connections table and unique provider connection per
  provider
- Add core.target_mappings table linking connections to resource keys
  and targets
- Add core.desired_state_records table storing desired entitlements per
  connection and recipient with generation and content hash
- Add core.observed_projections table for application-reported state per
  connection and recipient
- Add core.lifecycle_requests table recording lifecycle requests by
  instance and generation
- Add core.person_cut_offs table and setter function to record persons
  cut off everywhere
- Add core.desired_state_outbox table with triggers on entitlement,
  membership, organization, connection, mapping, and cut-off tables to
  write change notices
- Enforce constraints on recipients, generations, and statuses across
  tables
- Implement desired_state_content SQL function computing record content
  per recipient rules
- Implement relay workflow to claim outbox rows, expand to records,
  recompute records, and dispatch pushes
- Implement push workflow to push records, handle transport answers, and
  record push results
- Implement read-back workflow to page through application state, update
  projections, and raise records as needed
- Add lifecycle request writers with idempotent recording, finishing,
  and acknowledgment
- Add FedWiki integration lifecycle workflows for create, set status,
  swap, delete, and reconcile
- Add FedWiki web handlers and forms for lifecycle requests, including
  create form with instance and generation
- Add tests for schema, triggers, relay, push, read-back, lifecycle
  requests, FedWiki workflows, and operator pages
- Add documentation updates for provider integration, entitlements,
  identity, organizations, lifecycle, and FedWiki integration
- Add benchmarks for delivery and push workflows
- Ensure schema-per-provider for in-tree integrations; connector state
  lives in generic core tables keyed by connection
- Secure functions and triggers with SECURITY DEFINER and restrict
  core_writer role privileges accordingly
- Handle Temporal client integration for workflow execution and
  scheduling
- Provide a recording transport for integration tests
- Close Issues 32 and 33 by implementing provider connections and
  desired-state records with lifecycle requests and relay mechanisms
2026-10-06 03:46:15 -05:00

116 lines
4.8 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package cmd
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
)
// ensureBootDataState performs the data-state work `start` does after
// migrations and before serving, and `seed-demo` does before seeding, so a
// database that was reset, migrated, and reseeded (test/reset-app-db.sh,
// the screen-capture utility's first step) holds exactly what a booted
// deployment holds:
//
// - every installed integration's provider record, after its key is
// checked against its manifest (integration-registration);
// - the System tenant (system-tenant);
// - expired pending domain claims swept (claim-expiry-and-carve-guards
// D1/D3): core registry work with no integration in it, run BEFORE the
// reconciliation below because a stranded pending external claim at a
// configured farm domain is exactly what makes EnsureOperatorRoot fail;
// logged, not fatal, since an unswept claim holds one name where an
// unensurable root leaves every hosted name unallocatable;
// - monthly RANGE partitions for core.webhook_events for the months ahead
// (webhook-partition-maintenance); logged, not fatal;
// - each integration's domains reconciliation (design D8): operator roots
// ensured from deployment configuration and placements backfilled for
// resources that predate the registry. Unconditional, not an
// integration's Startup hook, which runs only when Temporal is
// configured. Fatal: a registry that does not fit the deployment's own
// namespace configuration is a misconfiguration.
func ensureBootDataState(ctx context.Context, database *sql.DB, logger *slog.Logger, integs []integrations.Integration) error {
providerSources := make([]integration.ProviderSource, 0, len(integs))
for _, integ := range integs {
ps := integ.Provider()
if err := integration.AssertKeyMatch(integ.Key(), ps.ProviderManifest()); err != nil {
return fmt.Errorf("integration key mismatch: %w", err)
}
providerSources = append(providerSources, ps)
}
if err := integration.RegisterProviders(ctx, database, providerSources); err != nil {
return fmt.Errorf("register providers: %w", err)
}
if _, err := systemtenant.Ensure(ctx, database); err != nil {
return fmt.Errorf("ensure system tenant: %w", err)
}
if result, err := domains.NewRegistry(database).SweepExpiredClaims(ctx); err != nil {
logger.Error("domain claim expiry sweep failed",
slog.Int("candidates", result.Candidates),
slog.Int("expired", result.Expired),
slog.Int("failed", result.Failed),
slog.Any("error", err))
} else if result.Candidates > 0 {
logger.Info("domain claim expiry sweep completed",
slog.Int("candidates", result.Candidates),
slog.Int("expired", result.Expired))
}
if err := db.EnsureWebhookEventPartitions(ctx, database, time.Now(), db.DefaultWebhookPartitionMonthsAhead); err != nil {
logger.Warn("webhook_events partition ensure failed", slog.Any("error", err))
}
for _, integ := range integs {
dr, ok := integ.(domainsReconciler)
if !ok {
continue
}
if err := dr.ReconcileDomains(ctx, database, logger); err != nil {
return fmt.Errorf("reconcile domains registry for %s: %w", integ.Key(), err)
}
}
return nil
}
// bindIntegrationConnections runs after ensureBootDataState has registered
// providers (provider-connections, design D28): it looks up, once, the
// connection boot registration ensured for every integration whose manifest
// owns resource keys, fails when one is missing, and hands the identifier to
// an integration that caches it (integrations.ConnectionBinder) before its
// routes and workflows are registered. A missing row means registration and
// the database disagree, which no request should discover later.
func bindIntegrationConnections(ctx context.Context, database *sql.DB, integs []integrations.Integration) error {
q := integration.New(database)
for _, integ := range integs {
manifest := integ.Provider().ProviderManifest()
if len(manifest.ResourceKeys) == 0 {
continue
}
conn, err := q.GetConnectionByProvider(ctx, manifest.Key)
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("provider %q owns resource keys but has no connection", manifest.Key)
}
if err != nil {
return fmt.Errorf("look up the connection of provider %q: %w", manifest.Key, err)
}
if binder, ok := integ.(integrations.ConnectionBinder); ok {
binder.BindConnection(conn.ConnectionID)
}
}
return nil
}