Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions Darling/Darling.Tests/ManagedConfMigrationRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,94 @@ public void VerifyStepB_NewErrorFromManagedFile_Fails()
Assert.Contains("work_mem", outcome.MismatchedKeys);
}

/// <summary>Pin: a store with a configured network endpoint always starts PostgreSQL with
/// <c>listen_addresses</c> forced onto the command line (<see cref="DarlingManagedPostgres.BuildServerRuntimeOptions"/>).
/// The rendered <c>darling-managed.conf</c> line stays loopback-only, so PostgreSQL reports THAT file
/// row with <c>error = "setting could not be applied"</c> once its command-line value differs —
/// confirmed against a live PostgreSQL 18 instance. <see cref="ManagedConfMigrationRunner.VerifyStepB"/>
/// must not treat that as a mismatch: <c>listen_addresses</c> is skipped, but a REAL mismatch on another
/// key in the same batch still fails.</summary>
[Fact]
public void VerifyStepB_ListenAddressesOverriddenByCommandLine_IsNotAMismatch_OtherKeyStillFails()
{
var rendered = "listen_addresses = '127.0.0.1'\nwork_mem = '16MB'\n";
WriteManaged(rendered);

var managedPath = Path.Combine(_dataDir, ManagedConfFile.FileName);
var rows = new List<FileSettingRow>
{
new(SourceFile: managedPath, SourceLine: 1, Name: "listen_addresses", Setting: "127.0.0.1", Applied: false, Error: "setting could not be applied"),
new(SourceFile: managedPath, SourceLine: 2, Name: "work_mem", Setting: "8MB", Applied: true, Error: null),
};

var outcome = ManagedConfMigrationRunner.VerifyStepB(_dataDir, rows, rendered, previousText: null);

Assert.Equal(ManagedConfVerificationStatus.Failed, outcome.Status);
Assert.DoesNotContain("listen_addresses", outcome.MismatchedKeys);
Assert.Contains("work_mem", outcome.MismatchedKeys);
}

/// <summary>Pin: when EVERY other key matches and the only rendered key that doesn't is
/// <c>listen_addresses</c> — overridden by the command line, same as above — Step B now completes
/// (<c>Verified</c>), writes the verified stamp against the rendered text, and leaves the managed file
/// in place (no restore of <paramref name="previousText"/>). Before this fix, this row alone drove Step
/// B to <c>Failed</c> on every start of any store with a configured network endpoint. The row's exact
/// shape (<c>setting</c> reads the file's rendered value, not the command line's; <c>error</c> reads
/// <c>"setting could not be applied"</c>; <c>applied</c> is <c>false</c>) was captured from a real
/// <c>pg_file_settings</c> row on a PostgreSQL 18 container started with a command-line
/// <c>listen_addresses</c> set to loopback plus the container's own address (<c>docker run
/// timescale/timescaledb:2.30.1-pg18 -c listen_addresses=127.0.0.1,&lt;container address&gt;</c>, with an
/// included conf file rendering the product's usual loopback-only <c>listen_addresses = '127.0.0.1'</c>
/// line), then querying <see cref="ManagedConfFileSettings.SnapshotSql"/> directly against it. The address
/// below (<c>192.0.2.10</c>) is the RFC 5737 documentation range, standing in for the container's own
/// address — the captured row shape does not depend on which address is used.</summary>
[Fact]
public void VerifyStepB_OnlyListenAddressesOverriddenByCommandLine_Verifies()
{
var rendered = "listen_addresses = '127.0.0.1'\nwork_mem = '16MB'\nmax_connections = '200'\n";
WriteManaged(rendered);

var managedPath = Path.Combine(_dataDir, ManagedConfFile.FileName);
var rows = new List<FileSettingRow>
{
new(SourceFile: managedPath, SourceLine: 1, Name: "listen_addresses", Setting: "127.0.0.1", Applied: false, Error: "setting could not be applied"),
Applied("work_mem", "16MB", file: managedPath, line: 2),
Applied("max_connections", "200", file: managedPath, line: 3),
};

var previousText = "work_mem = '8MB'\n";

var outcome = ManagedConfMigrationRunner.VerifyStepB(_dataDir, rows, rendered, previousText);

Assert.Equal(ManagedConfVerificationStatus.Verified, outcome.Status);
Assert.Equal(ManagedConfMigrationStep.B, outcome.Step);
Assert.True(ManagedConfMigrationSteps.IsVerified(_dataDir));
Assert.Equal(rendered, File.ReadAllText(managedPath));
}

/// <summary>Pin: the same completion for <c>port</c> — also command-line-owned
/// (<see cref="DarlingStoreHostProfile.CommandLineOnlyKeys"/>) — whose file row is overridden the same
/// way when the command line pins a different port than the rendered file line.</summary>
[Fact]
public void VerifyStepB_OnlyPortOverriddenByCommandLine_Verifies()
{
var rendered = "port = '5555'\nwork_mem = '16MB'\n";
WriteManaged(rendered);

var managedPath = Path.Combine(_dataDir, ManagedConfFile.FileName);
var rows = new List<FileSettingRow>
{
new(SourceFile: managedPath, SourceLine: 1, Name: "port", Setting: "5555", Applied: false, Error: "setting could not be applied"),
Applied("work_mem", "16MB", file: managedPath, line: 2),
};

var outcome = ManagedConfMigrationRunner.VerifyStepB(_dataDir, rows, rendered, previousText: null);

Assert.Equal(ManagedConfVerificationStatus.Verified, outcome.Status);
Assert.Equal(ManagedConfMigrationStep.B, outcome.Step);
Assert.True(ManagedConfMigrationSteps.IsVerified(_dataDir));
}

/// <summary>Pin (#4336): the exact change-log line for two changed keys.</summary>
[Fact]
public void FormatStepBChangeLog_TwoKeys_ExactLine()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,19 @@ private static ManagedConfMigrationOutcome CompareAndFinish(
/// (#4336; <see cref="Path.GetFileName(string)"/>, not a suffix match — a stray <c>old-darling-managed.conf</c>
/// in an include directory is a different file). A row's
/// <c>applied</c> may be false — an operator line below the include can legitimately override it; only
/// the value and the absence of an error matter here. All keys match: stamp the new text as verified. Any
/// key fails: restore <paramref name="previousText"/> (when there was one) so the old stamp matches
/// again, and report <c>Failed</c>.
/// the value and the absence of an error matter here.
///
/// <para><see cref="DarlingStoreHostProfile.CommandLineOnlyKeys"/> (<c>port</c>, <c>listen_addresses</c>) are
/// skipped entirely here — never looked up in <paramref name="renderedText"/>'s keys, never compared —
/// because <c>BuildServerRuntimeOptions</c> always starts PostgreSQL with both on the <c>pg_ctl -o "-c
/// ..."</c> command line, which outranks the conf file unconditionally (confirmed live). An exposed
/// store's running <c>listen_addresses</c> carries the network IP the rendered file line never does
/// (the file always renders loopback-only, #4215/BuildConfAppend); PostgreSQL's own <c>pg_file_settings</c>
/// then reports that file row with <c>error = 'setting could not be applied'</c> — not merely
/// <c>applied = false</c> — because the value differs from what's actually in force, same as any
/// operator line the command line has already overridden. Comparing this key here would fail Step B on
/// every start for any store with a configured network endpoint, even though the store's listener is
/// exactly what it should be; there is nothing this file could ever own for either key.</para>
/// </summary>
internal static ManagedConfMigrationOutcome VerifyStepB(
string dataDir,
Expand Down Expand Up @@ -335,6 +345,11 @@ internal static ManagedConfMigrationOutcome VerifyStepB(
var mismatchedKeys = new List<string>();
foreach (var (_, name, value) in DarlingManagedPostgres.ParseConfText(renderedText))
{
if (Array.IndexOf(DarlingStoreHostProfile.CommandLineOnlyKeys, name) >= 0)
{
continue;
}

var ok = byKey.TryGetValue(name, out var candidates)
&& candidates.Exists(r => r.Error is null && string.Equals(r.Setting, value, StringComparison.Ordinal));
if (!ok)
Expand Down
Loading