SeedAIProvidersFromEnv reconciles the deployment's environment- derived AI provider configuration with rows in the ai_providers table at server startup. Concurrent server starts are serialized via a Postgres advisory lock; rows that already exist with a matching canonical hash are left alone, missin
( ctx context.Context, db database.Store, cfg codersdk.AIBridgeConfig, logger slog.Logger, )
| 41 | // |
| 42 | // Audit entries are recorded via the system actor for any inserts. |
| 43 | func SeedAIProvidersFromEnv( |
| 44 | ctx context.Context, |
| 45 | db database.Store, |
| 46 | cfg codersdk.AIBridgeConfig, |
| 47 | logger slog.Logger, |
| 48 | ) error { |
| 49 | desired, err := providersFromEnv(ctx, cfg, logger) |
| 50 | if err != nil { |
| 51 | return xerrors.Errorf("compute providers from env: %w", err) |
| 52 | } |
| 53 | if len(desired) == 0 { |
| 54 | return nil |
| 55 | } |
| 56 | |
| 57 | // Audit entries are attributed to the deployment rather than a user. |
| 58 | //nolint:gocritic // server startup, no user actor available |
| 59 | sysCtx := dbauthz.AsSystemRestricted(ctx) |
| 60 | |
| 61 | // Collect inserted rows inside the transaction and emit audit |
| 62 | // entries only after the transaction commits. The auditor writes |
| 63 | // through the outer db handle, so emitting inside InTx would leave |
| 64 | // phantom audit rows if the transaction later rolls back. |
| 65 | var ( |
| 66 | insertedProviders []database.AIProvider |
| 67 | insertedKeys []database.AIProviderKey |
| 68 | ) |
| 69 | |
| 70 | err = db.InTx(func(tx database.Store) error { |
| 71 | insertedProviders = insertedProviders[:0] |
| 72 | insertedKeys = insertedKeys[:0] |
| 73 | |
| 74 | // Acquire the advisory lock. The lock is released when the |
| 75 | // transaction ends. |
| 76 | if err := tx.AcquireLock(sysCtx, database.LockIDAIProvidersEnvSeed); err != nil { |
| 77 | return xerrors.Errorf("acquire ai providers env seed lock: %w", err) |
| 78 | } |
| 79 | |
| 80 | // Load every provider (including soft-deleted and disabled rows) |
| 81 | // once so we can decide insert vs. skip vs. drift per desired |
| 82 | // row without a query per name. |
| 83 | all, err := tx.GetAIProviders(sysCtx, database.GetAIProvidersParams{ |
| 84 | IncludeDeleted: true, |
| 85 | IncludeDisabled: true, |
| 86 | }) |
| 87 | if err != nil { |
| 88 | return xerrors.Errorf("load ai providers: %w", err) |
| 89 | } |
| 90 | // Prefer the live row when a soft-deleted row shares its name. |
| 91 | byName := make(map[string]database.AIProvider, len(all)) |
| 92 | for _, row := range all { |
| 93 | if existing, ok := byName[row.Name]; ok && !existing.Deleted && row.Deleted { |
| 94 | continue |
| 95 | } |
| 96 | byName[row.Name] = row |
| 97 | } |
| 98 | |
| 99 | for _, dp := range desired { |
| 100 | settings, err := encodeAIProviderSettings(codersdk.AIProviderSettings{Bedrock: dp.Bedrock}) |