milestone 13 discrepancies: redact credentials from urls in logs, metrics and cli output
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
//! One definition of "the operator's configuration is wrong".
|
||||
//!
|
||||
//! `run`, `check` and `import` all sort a failure into two buckets: the
|
||||
//! configuration is wrong and the operator can fix it (exit 2, `nxdns check`
|
||||
//! is the next step), or something else broke (exit 1). Each subcommand used to
|
||||
//! carry its own list of which errors meant which, and the lists disagreed —
|
||||
//! the same seed file exited 1 from `run` and 2 from `check`. There is one list
|
||||
//! now, and it is this file. Nothing else may keep a second one.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const validate = @import("validate.zig");
|
||||
|
||||
/// `ValidateError` enters as a whole set rather than variant by variant, so a
|
||||
/// variant added to the validator cannot silently fall through to exit 1. The
|
||||
/// four extras are the configuration faults raised outside the validator: the
|
||||
/// ZON reader (`ParseZon`), the seed-file size limit (`ConfigTooLarge`), the
|
||||
/// composition root's upstream build (`NoUsableUpstreams`) and its certificate
|
||||
/// load (`BadCertificate`).
|
||||
///
|
||||
/// Not here on purpose: `error.DatabaseNotEmpty`, which reports the state of
|
||||
/// the database rather than the content of a file, and is the one config-shaped
|
||||
/// exit 2 `cli` decides for itself.
|
||||
const ConfigFault = validate.ValidateError || error{
|
||||
ParseZon,
|
||||
ConfigTooLarge,
|
||||
NoUsableUpstreams,
|
||||
BadCertificate,
|
||||
};
|
||||
|
||||
const faults: []const anyerror = blk: {
|
||||
const set = @typeInfo(ConfigFault).error_set.?;
|
||||
var list: [set.len]anyerror = undefined;
|
||||
for (set, 0..) |member, i| list[i] = @field(anyerror, member.name);
|
||||
const frozen = list;
|
||||
break :blk &frozen;
|
||||
};
|
||||
|
||||
/// True when `err` means the configuration the operator supplied is wrong.
|
||||
/// Linear over a set of about forty errors, on failure paths only.
|
||||
pub fn isConfigFault(err: anyerror) bool {
|
||||
for (faults) |fault| {
|
||||
if (err == fault) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "every ValidateError variant is a configuration fault, with no exceptions" {
|
||||
// The guard on the derivation: a variant added to `ValidateError` and left
|
||||
// out of the classification fails here rather than exiting 1 in the field.
|
||||
//
|
||||
// No member is excused. This file used to subtract `error.OutOfMemory`
|
||||
// here, which made the rule "every ValidateError is a fault, except one" —
|
||||
// a private exclusion list of exactly the kind this file exists to abolish.
|
||||
// `validate.ValidateError` no longer carries an allocation failure, so the
|
||||
// rule is literal again.
|
||||
inline for (@typeInfo(validate.ValidateError).error_set.?) |member| {
|
||||
const err = @field(anyerror, member.name);
|
||||
if (!isConfigFault(err)) {
|
||||
std.debug.print("isConfigFault(error.{s}) is false, expected true\n", .{member.name});
|
||||
return error.TestUnexpectedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "an allocation failure is not a member of the validator's verdict" {
|
||||
// The root of it: `faults.zig` can only be exception-free while
|
||||
// `ValidateError` holds nothing that is not a verdict on the file.
|
||||
inline for (@typeInfo(validate.ValidateError).error_set.?) |member| {
|
||||
if (std.mem.eql(u8, member.name, "OutOfMemory")) {
|
||||
std.debug.print("ValidateError carries error.OutOfMemory\n", .{});
|
||||
return error.TestUnexpectedResult;
|
||||
}
|
||||
}
|
||||
// It is still reachable from `validate`, just not as a finding: the
|
||||
// allocator can fail and the caller has to handle it.
|
||||
comptime var reachable = false;
|
||||
inline for (@typeInfo(validate.Error).error_set.?) |member| {
|
||||
if (comptime std.mem.eql(u8, member.name, "OutOfMemory")) reachable = true;
|
||||
}
|
||||
try testing.expect(reachable);
|
||||
}
|
||||
|
||||
test "the faults raised outside the validator are configuration faults" {
|
||||
try testing.expect(isConfigFault(error.ParseZon));
|
||||
try testing.expect(isConfigFault(error.ConfigTooLarge));
|
||||
try testing.expect(isConfigFault(error.NoUsableUpstreams));
|
||||
try testing.expect(isConfigFault(error.BadCertificate));
|
||||
}
|
||||
|
||||
test "the seed-file errors that used to exit 1 from run are configuration faults" {
|
||||
// D1 verbatim: these three reached `run` from a rejected seed file and were
|
||||
// classified as runtime failures.
|
||||
try testing.expect(isConfigFault(error.ParseZon));
|
||||
try testing.expect(isConfigFault(error.MissingDefaultGroup));
|
||||
try testing.expect(isConfigFault(error.NoUpstreams));
|
||||
}
|
||||
|
||||
test "a runtime failure is not a configuration fault" {
|
||||
try testing.expect(!isConfigFault(error.OutOfMemory));
|
||||
try testing.expect(!isConfigFault(error.AccessDenied));
|
||||
try testing.expect(!isConfigFault(error.FileNotFound));
|
||||
try testing.expect(!isConfigFault(error.AddressInUse));
|
||||
// A state conflict, not a bad file: `import` refuses to overwrite a
|
||||
// configured database and decides that exit code itself.
|
||||
try testing.expect(!isConfigFault(error.DatabaseNotEmpty));
|
||||
// Only ever a warning, so it never reaches an exit code by this route.
|
||||
try testing.expect(!isConfigFault(error.SourceInNoGroup));
|
||||
}
|
||||
Reference in New Issue
Block a user