This commit is contained in:
parent
d060a36d57
commit
37a0381b67
@ -24,6 +24,9 @@ func (m *ConfigManager) SwitchDatabase(ctx context.Context, expectedGeneration u
|
||||
if err := m.authorize(ctx, actor, ConfigOperationMigration, SectionRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.writeMu.Lock()
|
||||
defer m.writeMu.Unlock()
|
||||
@ -62,9 +65,15 @@ func (m *ConfigManager) SwitchDatabase(ctx context.Context, expectedGeneration u
|
||||
if err := m.snapshots.Validate(candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.transactions.Commit(current, candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -69,3 +69,20 @@ func TestConfigManagerSwitchDatabaseRequiresAuthorizedActor(t *testing.T) {
|
||||
t.Fatalf("missing actor error = %v, want authorization required", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigManagerSwitchDatabaseHonorsCancellationBeforePersistence(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeRuntimeFixture(t, root, validRuntimeConfig())
|
||||
manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := manager.Snapshot().Root.Database
|
||||
target.LocalRef = "next.db"
|
||||
if err := manager.SwitchDatabase(newCancelAfterFirstContext(), 1, target, authorizedConfigActor()); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled database switch error = %v, want context canceled", err)
|
||||
}
|
||||
if manager.Snapshot().Generation != 1 || manager.Snapshot().Root.Database.LocalRef != "smail.db" {
|
||||
t.Fatalf("canceled database switch changed snapshot: %+v", manager.Snapshot())
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,10 +301,16 @@ func (m *ConfigManager) Update(ctx context.Context, req UpdateRequest) (*UpdateR
|
||||
if err := m.authorize(ctx, req.Actor, ConfigOperationUpdate, req.Section); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidate, err := applySectionPayload(current, req.Section, req.Payload, req.SecretChanges, m.vault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCandidateGeneration(&candidate, current.Generation+1)
|
||||
if err := ValidateRuntimeConfig(candidate, m.options); err != nil {
|
||||
return nil, err
|
||||
@ -315,6 +321,9 @@ func (m *ConfigManager) Update(ctx context.Context, req UpdateRequest) (*UpdateR
|
||||
if err := m.confirm(ctx, req.Actor, req.Confirmation, ConfigOperationUpdate, req.Section); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.pending = &pendingConfig{Candidate: candidate, Section: req.Section, Actor: req.Actor, Confirmation: req.Confirmation}
|
||||
m.status.PendingRestart = servicesForSection(req.Section)
|
||||
@ -352,6 +361,9 @@ func (m *ConfigManager) apply(ctx context.Context, generation uint64, actor Audi
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
candidate := pending.Candidate
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if actor.UserID != "" && (pending.Actor.UserID != actor.UserID || pending.Actor.SessionID != actor.SessionID) {
|
||||
return nil, ErrAuthorizationRequired
|
||||
}
|
||||
@ -370,9 +382,15 @@ func (m *ConfigManager) apply(ctx context.Context, generation uint64, actor Audi
|
||||
if err := m.snapshots.Validate(candidate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.transactions.Commit(current, candidate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -436,6 +454,9 @@ func (m *ConfigManager) PreviewRollback(ctx context.Context, generation uint64,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateRuntimeSecrets(m.vault, backup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -471,6 +492,9 @@ func (m *ConfigManager) IssueRollbackTicket(ctx context.Context, generation uint
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ValidateRuntimeSecrets(m.vault, backup); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -523,6 +547,9 @@ func (m *ConfigManager) rollback(ctx context.Context, generation uint64, actor A
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.writeMu.Lock()
|
||||
defer m.writeMu.Unlock()
|
||||
m.mu.RLock()
|
||||
@ -536,6 +563,9 @@ func (m *ConfigManager) rollback(ctx context.Context, generation uint64, actor A
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateRuntimeSecrets(m.vault, candidate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -547,9 +577,15 @@ func (m *ConfigManager) rollback(ctx context.Context, generation uint64, actor A
|
||||
if err := m.snapshots.Validate(candidate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.transactions.Commit(current, candidate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -826,6 +862,9 @@ func (m *ConfigManager) MigrateLegacyPluginSecrets(ctx context.Context, importer
|
||||
|
||||
values := make([]LegacyPluginSecret, 0, len(current.Plugins.Plugins))
|
||||
for logicalID, instance := range current.Plugins.Plugins {
|
||||
if err := contextError(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !storedSecretHasMaterial(instance.Secret) {
|
||||
continue
|
||||
}
|
||||
@ -845,6 +884,9 @@ func (m *ConfigManager) MigrateLegacyPluginSecrets(ctx context.Context, importer
|
||||
if err := importer(ctx, values); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
candidate := cloneRuntimeConfig(current)
|
||||
for logicalID, instance := range candidate.Plugins.Plugins {
|
||||
@ -858,9 +900,15 @@ func (m *ConfigManager) MigrateLegacyPluginSecrets(ctx context.Context, importer
|
||||
if err := m.snapshots.Validate(candidate); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := contextError(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := m.transactions.Commit(current, candidate); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@ -247,6 +247,60 @@ func TestConfigManagerLoadHonorsCancellationAfterReadingConfiguration(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigManagerUpdateHonorsCancellationBeforeCreatingPendingState(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeRuntimeFixture(t, root, validRuntimeConfig())
|
||||
manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := json.Marshal(manager.Snapshot().Email)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := manager.Update(newCancelAfterFirstContext(), UpdateRequest{
|
||||
Section: SectionEmail, ExpectedGeneration: 1, Payload: payload, Actor: authorizedConfigActor(),
|
||||
}); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled update error = %v, want context canceled", err)
|
||||
}
|
||||
status := manager.Status()
|
||||
if len(status.PendingRestart) != 0 {
|
||||
t.Fatalf("canceled update left pending restart state: %+v", status.PendingRestart)
|
||||
}
|
||||
if manager.Snapshot().Generation != 1 {
|
||||
t.Fatalf("canceled update changed active generation: %d", manager.Snapshot().Generation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigManagerApplyHonorsCancellationBeforePersistence(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeRuntimeFixture(t, root, validRuntimeConfig())
|
||||
manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := json.Marshal(manager.Snapshot().Email)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updated, err := manager.Update(context.Background(), UpdateRequest{
|
||||
Section: SectionEmail, ExpectedGeneration: 1, Payload: payload, Actor: authorizedConfigActor(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := manager.Apply(newCancelAfterFirstContext(), updated.Generation); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled apply error = %v, want context canceled", err)
|
||||
}
|
||||
if manager.Snapshot().Generation != 1 {
|
||||
t.Fatalf("canceled apply changed active generation: %d", manager.Snapshot().Generation)
|
||||
}
|
||||
status := manager.Status()
|
||||
if len(status.PendingRestart) != 1 || status.PendingRestart[0] != ServiceEmail {
|
||||
t.Fatalf("canceled apply lost pending state: %+v", status.PendingRestart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigManagerApplyPreflightsSnapshotBeforePersisting(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeRuntimeFixture(t, root, validRuntimeConfig())
|
||||
@ -847,6 +901,38 @@ func TestConfigManagerRetainsLegacySecretWhenImportFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigManagerLegacySecretMigrationHonorsCancellationAfterImport(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
runtime := validRuntimeConfig()
|
||||
vault, err := credential.NewVault(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := ApplySecretChange(vault, StoredSecret{}, SecretChange{Operation: SecretReplace, Value: &SecretInput{Plaintext: "legacy-token"}}, SecretPolicy{Required: true, MaxLength: 4096}, time.Now().UTC())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin := runtime.Plugins.Plugins["telegram_push"]
|
||||
plugin.Secret = stored
|
||||
runtime.Plugins.Plugins["telegram_push"] = plugin
|
||||
writeRuntimeFixture(t, root, runtime)
|
||||
manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if migrated, err := manager.MigrateLegacyPluginSecrets(ctx, func(context.Context, []LegacyPluginSecret) error {
|
||||
cancel()
|
||||
return nil
|
||||
}); !errors.Is(err, context.Canceled) || migrated {
|
||||
t.Fatalf("canceled legacy migration = %v, %v", migrated, err)
|
||||
}
|
||||
if manager.Snapshot().Generation != 1 || !manager.Snapshot().Plugins.Plugins["telegram_push"].Secret.Configured() {
|
||||
t.Fatal("canceled legacy migration altered the active configuration")
|
||||
}
|
||||
}
|
||||
|
||||
func containsAny(value string, terms ...string) bool {
|
||||
for _, term := range terms {
|
||||
if strings.Contains(value, term) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user