// Package store is how a context reaches the database it exclusively owns. // // novox/hq ADR 0008: a context is granted only what it exclusively owns — no shared writes, no // read-only role on another context's store. That is a rule about credentials, so this package // makes it a rule about credentials rather than a rule about intentions. // // There is no mesh-wide connection string and no way to ask for one. A store is opened by naming // a context, and the settings for that context come from an environment variable named after it. // A control plane process that has been granted `inventory` holds MESH_STORE_INVENTORY and // nothing else, so reaching another context's store is not a matter of restraint — the process // has no address for it and no credential to present. // // Which is also how the rule is *checked*: what a context can reach is visible in the // declaration that runs it, as the list of variables it was given. package store import ( "context" "errors" "fmt" "os" "regexp" "strings" "time" "github.com/jackc/pgx/v5/pgxpool" ) // contextName is what a context may be called. // // Constrained because the name becomes part of an environment variable and part of a database // name, and a name that is valid in one and not the other is a fault discovered at bootstrap on // a machine with no mesh on it. var contextName = regexp.MustCompile(`^[a-z][a-z0-9]*$`) // Store is one context's database. type Store struct { context string pool *pgxpool.Pool } // Variable is the environment variable holding a context's connection settings. // // Exported because the bootstrap has to set it and the declaration has to name it, and both // should read it from here rather than spell it out again. func Variable(context string) string { return "MESH_STORE_" + strings.ToUpper(context) } // Database is what a context's database is called. // // Named after the context, so that a person looking at a PostgreSQL server can see which // contexts exist without a map. There is deliberately no database named for the mesh as a whole: // novox/hq ADR 0006 records that *the mesh database* names a thing that will not exist. func Database(context string) string { return context } // Open connects to the database a context owns. // // The settings are read from the environment rather than passed in, which is not indirection for // its own sake: it means no caller anywhere can hand a context a connection to something else. func Open(ctx context.Context, name string) (*Store, error) { if !contextName.MatchString(name) { return nil, fmt.Errorf( "%q is not a usable context name: it becomes an environment variable and a database "+ "name, so it must be lower-case letters and digits, starting with a letter", name) } dsn := os.Getenv(Variable(name)) if strings.TrimSpace(dsn) == "" { return nil, fmt.Errorf( "this process has no %s, so it was not granted the %s store. A context reaches only "+ "the store it exclusively owns (novox/hq ADR 0008), so this is either the wrong "+ "context or a missing grant — it is never something to work around by reusing "+ "another context's connection", Variable(name), name) } config, err := pgxpool.ParseConfig(dsn) if err != nil { // Deliberately not wrapping the driver's error verbatim into a message that gets logged: // a malformed DSN often *is* the password, and the value is the one thing here that must // not be quoted back. return nil, fmt.Errorf( "the connection settings in %s could not be read; the value is not quoted here "+ "because it carries a password", Variable(name)) } pool, err := pgxpool.NewWithConfig(ctx, config) if err != nil { return nil, fmt.Errorf("cannot open the %s store: %w", name, err) } return &Store{context: name, pool: pool}, nil } // Context is which context this store belongs to. func (s *Store) Context() string { return s.context } // Pool is the connection pool, for the context that owns it. func (s *Store) Pool() *pgxpool.Pool { return s.pool } // Close releases the connections. func (s *Store) Close() { if s.pool != nil { s.pool.Close() } } // Ready waits until the database answers, or gives up. // // A read-back rather than a connect: pgxpool connects lazily, so a Store that was opened without // error proves only that a string parsed. The bootstrap raises PostgreSQL and the control plane // moments later, and "the container is running" is not "the database will answer" — that // distinction has already cost a debugging session on this project once. func (s *Store) Ready(ctx context.Context, within time.Duration) error { deadline := time.Now().Add(within) var last error for { err := s.pool.Ping(ctx) if err == nil { return nil } last = err if ctx.Err() != nil { return errors.Join(ctx.Err(), last) } if time.Now().After(deadline) { return fmt.Errorf( "the %s store did not answer within %s: %w", s.context, within, last) } select { case <-ctx.Done(): return errors.Join(ctx.Err(), last) case <-time.After(250 * time.Millisecond): } } }