milestone 20: declarative configuration for iac
This commit is contained in:
@@ -22,7 +22,6 @@ const build_options = @import("build_options");
|
||||
const Writer = std.Io.Writer;
|
||||
|
||||
const cli = @import("../cli.zig");
|
||||
const bootstrap = @import("../config/bootstrap.zig");
|
||||
const config_export = @import("../config/export.zig");
|
||||
const import = @import("../config/import.zig");
|
||||
const model = @import("../config/model.zig");
|
||||
@@ -127,7 +126,7 @@ fn openMigrated(f: *const Fixture, name: []const u8) !Data {
|
||||
return .{ .dir = dir, .database = database };
|
||||
}
|
||||
|
||||
fn importInto(f: *const Fixture, data: *Data, file: []const u8, force: bool) !void {
|
||||
fn importInto(f: *const Fixture, data: *Data, file: []const u8, allow_delete: bool) !void {
|
||||
var diags: validate.Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
return import.importFile(
|
||||
@@ -136,7 +135,7 @@ fn importInto(f: *const Fixture, data: *Data, file: []const u8, force: bool) !vo
|
||||
&data.database,
|
||||
f.tmp.dir,
|
||||
file,
|
||||
.{ .force = force },
|
||||
.{ .allow_delete = allow_delete },
|
||||
&diags,
|
||||
);
|
||||
}
|
||||
@@ -655,7 +654,7 @@ test "S7 case 10: a config.db stamped one version ahead is refused and left alon
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// case 11-19: export, import and bootstrap on real files
|
||||
// case 11-19: export and import on real files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test "S7 case 11: an exported file is mode 0600 and starts with the header comment" {
|
||||
@@ -704,7 +703,7 @@ test "S7 case 12: export, import and export again are byte-identical files" {
|
||||
try testing.expectEqualStrings(a, b);
|
||||
}
|
||||
|
||||
test "S7 case 13: import refuses a configured database unless --force is given" {
|
||||
test "S7 case 13: import refuses a diff that deletes rows unless --allow-delete is given" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
var f: Fixture = .init();
|
||||
@@ -716,7 +715,10 @@ test "S7 case 13: import refuses a configured database unless --force is given"
|
||||
defer data.deinit();
|
||||
try importInto(&f, &data, "first.zon", false);
|
||||
|
||||
try testing.expectError(error.DatabaseNotEmpty, importInto(&f, &data, "second.zon", false));
|
||||
// The two files name different upstream urls, and a url is the upstream's
|
||||
// identity: applying the second deletes the first's row, which is what the
|
||||
// gate exists to stop.
|
||||
try testing.expectError(error.DestructiveImport, importInto(&f, &data, "second.zon", false));
|
||||
try testing.expectEqual(
|
||||
@as(i64, 1),
|
||||
try data.database.queryInt(
|
||||
@@ -753,14 +755,15 @@ test "S7 case 14: an invalid import reports every problem and writes nothing" {
|
||||
&data.database,
|
||||
f.tmp.dir,
|
||||
"config.zon",
|
||||
.{ .force = false },
|
||||
.{ .allow_delete = false },
|
||||
&diags,
|
||||
)) |_| {
|
||||
return error.TestUnexpectedResult;
|
||||
} else |_| {}
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), diags.problems.items.len);
|
||||
try testing.expect(try import.isEmpty(&data.database));
|
||||
try testing.expectEqual(@as(i64, 0), try data.database.queryInt("SELECT count(*) FROM upstreams"));
|
||||
try testing.expectEqual(@as(i64, 0), try data.database.queryInt("SELECT count(*) FROM settings"));
|
||||
|
||||
// Nothing beyond the database and its sidecars was created.
|
||||
var dir = try f.tmp.dir.openDir(io, "data", .{ .iterate = true });
|
||||
@@ -771,129 +774,6 @@ test "S7 case 14: an invalid import reports every problem and writes nothing" {
|
||||
}
|
||||
}
|
||||
|
||||
test "S7 case 15: bootstrap with no configuration file leaves the database empty" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
var f: Fixture = .init();
|
||||
defer f.deinit();
|
||||
|
||||
var data = try openMigrated(&f, "data");
|
||||
defer data.deinit();
|
||||
|
||||
var diags: validate.Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
|
||||
const outcome = try bootstrap.bootstrap(
|
||||
io,
|
||||
testing.allocator,
|
||||
&data.database,
|
||||
f.tmp.dir,
|
||||
"config.zon",
|
||||
&diags,
|
||||
);
|
||||
try testing.expectEqual(bootstrap.Outcome.no_config_file, outcome);
|
||||
try testing.expect(try import.isEmpty(&data.database));
|
||||
}
|
||||
|
||||
test "S7 case 16: bootstrap seeds an empty database from the configuration file" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
var f: Fixture = .init();
|
||||
defer f.deinit();
|
||||
try f.write("config.zon", rich_config);
|
||||
|
||||
var data = try openMigrated(&f, "data");
|
||||
defer data.deinit();
|
||||
|
||||
var diags: validate.Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
|
||||
const outcome = try bootstrap.bootstrap(
|
||||
io,
|
||||
testing.allocator,
|
||||
&data.database,
|
||||
f.tmp.dir,
|
||||
"config.zon",
|
||||
&diags,
|
||||
);
|
||||
try testing.expectEqual(bootstrap.Outcome.seeded, outcome);
|
||||
try testing.expectEqual(@as(i64, 2), try data.database.queryInt("SELECT count(*) FROM groups"));
|
||||
try testing.expectEqual(@as(i64, 2), try data.database.queryInt("SELECT count(*) FROM upstreams"));
|
||||
try testing.expectEqual(
|
||||
@as(i64, 1),
|
||||
try data.database.queryInt("SELECT count(*) FROM clients WHERE ip = 'fd00::1'"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@as(i64, 5353),
|
||||
try data.database.queryInt("SELECT CAST(value AS INTEGER) FROM settings WHERE key = 'dns.port'"),
|
||||
);
|
||||
}
|
||||
|
||||
test "S7 case 17: bootstrap on a configured database never reads the file" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
var f: Fixture = .init();
|
||||
defer f.deinit();
|
||||
try f.write("seed.zon", minimal_config);
|
||||
|
||||
var data = try openMigrated(&f, "data");
|
||||
defer data.deinit();
|
||||
try importInto(&f, &data, "seed.zon", false);
|
||||
|
||||
// Unparseable on purpose: the call can only succeed if the file is never
|
||||
// opened.
|
||||
try f.write("config.zon", broken_zon);
|
||||
|
||||
var diags: validate.Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
|
||||
const outcome = try bootstrap.bootstrap(
|
||||
io,
|
||||
testing.allocator,
|
||||
&data.database,
|
||||
f.tmp.dir,
|
||||
"config.zon",
|
||||
&diags,
|
||||
);
|
||||
try testing.expectEqual(bootstrap.Outcome.db_already_configured, outcome);
|
||||
try testing.expectEqual(@as(usize, 0), diags.problems.items.len);
|
||||
try testing.expectEqual(
|
||||
@as(i64, 1),
|
||||
try data.database.queryInt(
|
||||
"SELECT count(*) FROM upstreams WHERE url = 'https://dns.example/dns-query'",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
test "S7 case 18: bootstrap with an invalid configuration file fails and writes nothing" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
var f: Fixture = .init();
|
||||
defer f.deinit();
|
||||
try f.write("config.zon", two_problem_config);
|
||||
|
||||
var data = try openMigrated(&f, "data");
|
||||
defer data.deinit();
|
||||
|
||||
var diags: validate.Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
|
||||
if (bootstrap.bootstrap(
|
||||
io,
|
||||
testing.allocator,
|
||||
&data.database,
|
||||
f.tmp.dir,
|
||||
"config.zon",
|
||||
&diags,
|
||||
)) |outcome| {
|
||||
std.debug.print("bootstrap unexpectedly returned .{s}\n", .{@tagName(outcome)});
|
||||
return error.TestUnexpectedResult;
|
||||
} else |_| {}
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), diags.problems.items.len);
|
||||
try testing.expect(try import.isEmpty(&data.database));
|
||||
}
|
||||
|
||||
test "S7 case 19: writeToFile replaces an existing file and restores mode 0600" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
@@ -988,11 +868,13 @@ test "S7 case 21: runCheck passes a seeded database and reports two stored probl
|
||||
try importInto(&f, &data, "config.zon", false);
|
||||
}
|
||||
{
|
||||
// `applyToDb` rather than an import: the validator would refuse this
|
||||
// `apply` rather than an import: the validator would refuse this
|
||||
// configuration, and the case needs the problems to reach the database.
|
||||
var data = try openMigrated(&f, "bad");
|
||||
defer data.deinit();
|
||||
try import.applyToDb(io, testing.allocator, &data.database, two_problem_model, 42, .{});
|
||||
var diags: validate.Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
try import.apply(io, testing.allocator, &data.database, two_problem_model, 42, .{}, &diags);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1045,13 +927,16 @@ test "S7 case 22: runCheck probes a real upstream and prints an OK line" {
|
||||
|
||||
const code = cli.runCheck(
|
||||
captured.runner(),
|
||||
.{ .paths = .{ .config = config_path }, .config_explicit = true },
|
||||
.{ .config = config_path },
|
||||
true,
|
||||
);
|
||||
try testing.expectEqual(cli.exit_ok, code);
|
||||
// Milestone 13 changed the probe line to the redacted `OK upstreams[i]`
|
||||
// form; this expectation went stale unnoticed because nothing ran -Dlive
|
||||
// between then and milestone 20.
|
||||
try testing.expect(std.mem.count(
|
||||
u8,
|
||||
captured.out.written(),
|
||||
"OK https://cloudflare-dns.com/dns-query\n",
|
||||
"OK upstreams[0] https://cloudflare-dns.com\n",
|
||||
) == 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user