Replace the entity slugs on organizations, workspaces, resource pools, and plan ladders with nullable `key` columns and add keys to products, prices, and entitlement sets. Rename `providers.slug` to `provider` and add partial unique indexes for system and org role names. Assign invoice numbers per billing account from a gapless transactional counter; Stripe's number moves to the invoice mapping as an external reference. Seeds, fixtures, and the operator lookup address rows by key, and the returning-login resync no longer blanks a display name when the IdP sends no `name` claim.
238 lines
7.9 KiB
Go
238 lines
7.9 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var keyPattern = regexp.MustCompile(`^[a-z0-9]+$`)
|
|
|
|
// statePattern is a hygiene format for lifecycle state names. It is deliberately
|
|
// permissive (allows underscores, e.g. a future `quota_locked`) because the state
|
|
// vocabulary is per-provider, not a closed global enum.
|
|
var statePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
|
|
|
var validProviderKinds = map[ProviderKind]bool{
|
|
KindPayment: true,
|
|
KindProvisioning: true,
|
|
KindNotification: true,
|
|
KindTax: true,
|
|
}
|
|
|
|
var validOperations = map[Operation]bool{
|
|
OpCreate: true,
|
|
OpSetStatus: true,
|
|
OpDelete: true,
|
|
OpList: true,
|
|
OpDescribe: true,
|
|
}
|
|
|
|
// validateManifest enforces the parts of the provider contract that are
|
|
// checkable from the manifest alone: key hygiene, a known kind, known verbs,
|
|
// and key-prefix ownership of every declared resource key.
|
|
func validateManifest(m Manifest) error {
|
|
if !keyPattern.MatchString(m.Key) {
|
|
return fmt.Errorf("provider key %q must match ^[a-z0-9]+$ (no underscores)", m.Key)
|
|
}
|
|
if !validProviderKinds[m.Kind] {
|
|
return fmt.Errorf("provider %q: unknown provider_kind %q", m.Key, m.Kind)
|
|
}
|
|
declaresSetStatus := false
|
|
for _, op := range m.Operations {
|
|
if !validOperations[op] {
|
|
return fmt.Errorf("provider %q: unknown operation %q", m.Key, op)
|
|
}
|
|
if op == OpSetStatus {
|
|
declaresSetStatus = true
|
|
}
|
|
}
|
|
// State-set agreement: a provider declaring set_status must declare the
|
|
// implicit `active` baseline plus at least one non-active state; a provider
|
|
// without set_status must declare no states. (See provider-registry spec.)
|
|
hasActive, hasNonActive := false, false
|
|
for _, s := range m.States {
|
|
if !statePattern.MatchString(string(s)) {
|
|
return fmt.Errorf("provider %q: state %q must match ^[a-z][a-z0-9_]*$", m.Key, s)
|
|
}
|
|
if s == StateActive {
|
|
hasActive = true
|
|
} else {
|
|
hasNonActive = true
|
|
}
|
|
}
|
|
if declaresSetStatus {
|
|
if !hasActive {
|
|
return fmt.Errorf("provider %q: declares set_status but does not declare the `active` state", m.Key)
|
|
}
|
|
if !hasNonActive {
|
|
return fmt.Errorf("provider %q: declares set_status but declares no non-active state", m.Key)
|
|
}
|
|
} else if len(m.States) > 0 {
|
|
return fmt.Errorf("provider %q: declares lifecycle states but does not declare the set_status operation", m.Key)
|
|
}
|
|
prefix := m.Key + "_"
|
|
for _, key := range m.ResourceKeys {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
return fmt.Errorf("provider %q: owned resource key %q must be prefixed %q", m.Key, key, prefix)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// leadingToken returns the substring before the first underscore (the whole
|
|
// string if there is none).
|
|
func leadingToken(key string) string {
|
|
if i := strings.IndexByte(key, '_'); i >= 0 {
|
|
return key[:i]
|
|
}
|
|
return key
|
|
}
|
|
|
|
// checkKeyNesting enforces that the provider-key namespace and the platform
|
|
// (unprefixed) resource-key namespace do not collide: no provider key may be
|
|
// the leading underscore-delimited token of a platform key, which would let one
|
|
// provider's "<key>_" prefix swallow a platform-owned key. (Provider-key vs
|
|
// provider-key nesting is already impossible because provider keys carry no
|
|
// underscore.)
|
|
func checkKeyNesting(providerKeys map[string]bool, platformKeys []string) error {
|
|
for _, key := range platformKeys {
|
|
tok := leadingToken(key)
|
|
if providerKeys[tok] {
|
|
return fmt.Errorf("provider key %q nests under platform resource key %q", tok, key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RegisterProviders reconciles the provider registry from code at boot,
|
|
// parallel to how migrations are assembled from MigrationSources. It validates
|
|
// every manifest, enforces key hygiene against existing platform resource
|
|
// keys, then upserts provider + operation rows and stamps
|
|
// core.resource_keys.provider for each owned key — all in one
|
|
// transaction. It is idempotent: re-running with the same sources only
|
|
// refreshes capability fields and reconciles the declared operation set.
|
|
// Operational state (provider status) is never clobbered.
|
|
//
|
|
// NOTE: the contract's "every declared operation resolves to an implemented
|
|
// activity" check is only partially realizable here. The manifest is the
|
|
// declaration and verb validity is enforced by validateManifest; binding each
|
|
// declared verb to its registered Temporal activity lands with the
|
|
// outbox→activity dispatch layer (M8c and later).
|
|
func RegisterProviders(ctx context.Context, db *sql.DB, sources []ProviderSource) error {
|
|
manifests := make([]Manifest, 0, len(sources))
|
|
providerKeys := make(map[string]bool, len(sources))
|
|
for _, src := range sources {
|
|
m := src.ProviderManifest()
|
|
if err := validateManifest(m); err != nil {
|
|
return err
|
|
}
|
|
if providerKeys[m.Key] {
|
|
return fmt.Errorf("duplicate provider key %q", m.Key)
|
|
}
|
|
providerKeys[m.Key] = true
|
|
manifests = append(manifests, m)
|
|
}
|
|
|
|
owned := make(map[string]bool)
|
|
for _, m := range manifests {
|
|
for _, k := range m.ResourceKeys {
|
|
owned[k] = true
|
|
}
|
|
}
|
|
|
|
q := New(db)
|
|
platformKeys, err := q.ListPlatformResourceKeys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("listing platform resource keys: %w", err)
|
|
}
|
|
// A provider's own keys may still be NULL-provider at this point (freshly
|
|
// renamed into place, or about to be stamped below), so they surface as
|
|
// platform-owned. Exclude declared-owned keys: only genuinely platform-owned
|
|
// keys participate in the key-nesting check.
|
|
platform := platformKeys[:0]
|
|
for _, k := range platformKeys {
|
|
if !owned[k] {
|
|
platform = append(platform, k)
|
|
}
|
|
}
|
|
if err := checkKeyNesting(providerKeys, platform); err != nil {
|
|
return err
|
|
}
|
|
|
|
tx, err := db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
qtx := q.WithTx(tx)
|
|
|
|
for _, m := range manifests {
|
|
if _, err := qtx.UpsertProvider(ctx, UpsertProviderParams{
|
|
// The registry column is `provider` (migration 00014, upstream's
|
|
// ratified name); the manifest field is the Go contract's Key.
|
|
Provider: m.Key,
|
|
ProviderKind: string(m.Kind),
|
|
DisplayName: m.DisplayName,
|
|
OperatorSurfacePath: sql.NullString{String: m.OperatorSurfacePath, Valid: m.OperatorSurfacePath != ""},
|
|
}); err != nil {
|
|
return fmt.Errorf("upsert provider %q: %w", m.Key, err)
|
|
}
|
|
|
|
ops := make([]string, 0, len(m.Operations))
|
|
for _, op := range m.Operations {
|
|
ops = append(ops, string(op))
|
|
if err := qtx.AddProviderOperation(ctx, AddProviderOperationParams{
|
|
Provider: m.Key,
|
|
Operation: string(op),
|
|
}); err != nil {
|
|
return fmt.Errorf("add operation %q for %q: %w", op, m.Key, err)
|
|
}
|
|
}
|
|
// Reconcile: drop any operation this provider no longer declares.
|
|
if err := qtx.DeleteProviderOperationsNotIn(ctx, DeleteProviderOperationsNotInParams{
|
|
Provider: m.Key,
|
|
Operations: ops,
|
|
}); err != nil {
|
|
return fmt.Errorf("reconcile operations for %q: %w", m.Key, err)
|
|
}
|
|
|
|
states := make([]string, 0, len(m.States))
|
|
for _, s := range m.States {
|
|
states = append(states, string(s))
|
|
if err := qtx.AddProviderState(ctx, AddProviderStateParams{
|
|
Provider: m.Key,
|
|
State: string(s),
|
|
}); err != nil {
|
|
return fmt.Errorf("add state %q for %q: %w", s, m.Key, err)
|
|
}
|
|
}
|
|
// Reconcile: drop any state this provider no longer declares (an empty
|
|
// declared set removes all rows — correct for a provider that dropped
|
|
// set_status).
|
|
if err := qtx.DeleteProviderStatesNotIn(ctx, DeleteProviderStatesNotInParams{
|
|
Provider: m.Key,
|
|
States: states,
|
|
}); err != nil {
|
|
return fmt.Errorf("reconcile states for %q: %w", m.Key, err)
|
|
}
|
|
|
|
for _, key := range m.ResourceKeys {
|
|
n, err := qtx.SetResourceKeyProvider(ctx, SetResourceKeyProviderParams{
|
|
ResourceKey: key,
|
|
Provider: sql.NullString{String: m.Key, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("stamp resource key %q for %q: %w", key, m.Key, err)
|
|
}
|
|
if n == 0 {
|
|
return fmt.Errorf("provider %q declares resource key %q, but no such row exists in core.resource_keys", m.Key, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|