feat: extend main with runRemoteUser and runRemoteTtl

This commit is contained in:
dualfroz committed 2026-09-07 16:06:54 +02:00
1 parent 422f7c1639
commit ef1d0f4c06
1 file changed
+173
+173
View File
@@ -3079,6 +3079,179 @@ fn runRemoteTxn(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype,
std.process.exit(1); std.process.exit(1);
}; };
defer client.close(); defer client.close();
try authenticateIfRequested(&client, stderr, authFlags.auth);
const applied = client.txnOp(entries.items) catch |err| {
try stderr.print("transaction failed, nothing applied: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
try stdout.print("OK: {d} operation(s) committed atomically\n", .{applied});
}
/// The network counterpart of `runUser`: dispatches `remote-user <op> ...`
/// onto `client.zig`'s `admin*` methods (which wire-encode the
/// `user_admin` opcode) instead of a local `auth.Store`. Bootstrapping the
/// very first admin account still has to happen locally via `user add`
/// (see the module doc on `runUser`), but every subsequent user management
/// operation can go over the wire once that first admin can authenticate.
fn runRemoteUser(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
if (rest.len < 1) return fail(stderr, "remote-user needs a subcommand (add/passwd/role/del/list)");
const address = try parseHostPort(stdout, addressSpec);
var client = cobalt.Client.connect(allocator, address) catch |err| {
try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
std.process.exit(1);
};
defer client.close();
const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
defer allocator.free(authFlags.rest);
try authenticateIfRequested(&client, stderr, authFlags.auth);
const op = rest[0];
const args = authFlags.rest;
if (std.mem.eql(u8, op, "add")) {
if (args.len < 3) return fail(stderr, "remote-user add needs <username> <password> readonly|readwrite|admin");
const role = cobalt.AuthRole.parse(args[2]) catch return fail(stderr, "role must be readonly, readwrite, or admin");
client.adminCreateUser(args[0], args[1], role, cobalt.auth.recommendedRounds) catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
try stdout.print("user '{s}' created with role {s}\n", .{ args[0], role.name() });
} else if (std.mem.eql(u8, op, "passwd")) {
if (args.len < 2) return fail(stderr, "remote-user passwd needs <username> <newpassword>");
client.adminSetPassword(args[0], args[1], cobalt.auth.recommendedRounds) catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
try stdout.print("password updated for '{s}'\n", .{args[0]});
} else if (std.mem.eql(u8, op, "role")) {
if (args.len < 2) return fail(stderr, "remote-user role needs <username> readonly|readwrite|admin");
const role = cobalt.AuthRole.parse(args[1]) catch return fail(stderr, "role must be readonly, readwrite, or admin");
client.adminSetRole(args[0], role) catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
try stdout.print("role for '{s}' set to {s}\n", .{ args[0], role.name() });
} else if (std.mem.eql(u8, op, "del")) {
if (args.len < 1) return fail(stderr, "remote-user del needs <username>");
client.adminDeleteUser(args[0]) catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
try stdout.print("user '{s}' deleted\n", .{args[0]});
} else if (std.mem.eql(u8, op, "list")) {
const users = client.adminListUsers(allocator) catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
defer cobalt.auth.freeUserList(allocator, users);
for (users) |u| try stdout.print("{s}\t{s}\n", .{ u.username, u.role.name() });
} else {
return fail(stderr, "unknown remote-user operation");
}
}
/// The network counterpart of `runTtl`-shaped local commands (`expire`,
/// `persist`, `ttl`, `expire-sweep`): dispatches `remote-ttl <op> ...` onto
/// `client.zig`'s `ttl*` methods (which wire-encode the `ttl_op` opcode)
/// instead of calling `ttl.zig` directly against a local Db, the same
/// "reuse client.zig, keep the arg parsing local" shape as `runRemoteUser`.
fn runRemoteTtl(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
if (rest.len < 1) return fail(stderr, "remote-ttl needs a subcommand (expire/persist/ttl/sweep)");
const address = try parseHostPort(stdout, addressSpec);
var client = cobalt.Client.connect(allocator, address) catch |err| {
try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
std.process.exit(1);
};
defer client.close();
const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
defer allocator.free(authFlags.rest);
try authenticateIfRequested(&client, stderr, authFlags.auth);
const op = rest[0];
const args = authFlags.rest;
if (std.mem.eql(u8, op, "expire")) {
if (args.len < 2) return fail(stderr, "remote-ttl expire needs <key> <seconds>");
const seconds = std.fmt.parseInt(u64, args[1], 10) catch return fail(stderr, "expire needs a whole number of seconds");
client.ttlExpire(args[0], seconds) catch |err| {
if (err == error.ServerKeyNotFound) return fail(stderr, "no such key");
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
} else if (std.mem.eql(u8, op, "persist")) {
if (args.len < 1) return fail(stderr, "remote-ttl persist needs <key>");
client.ttlPersist(args[0]) catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
} else if (std.mem.eql(u8, op, "ttl")) {
if (args.len < 1) return fail(stderr, "remote-ttl ttl needs <key>");
const result = client.ttlQuery(args[0]) catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
switch (result) {
.noKey => try stdout.writeAll("-2\n"),
.noExpiry => try stdout.writeAll("-1\n"),
.seconds => |s| try stdout.print("{d}\n", .{s}),
}
} else if (std.mem.eql(u8, op, "sweep")) {
const reaped = client.ttlSweep() catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
try stdout.print("reaped: {d}\n", .{reaped});
} else {
return fail(stderr, "unknown remote-ttl operation");
}
}
/// The network counterpart of `runCollTtl`-shaped local commands (`coll
/// <kind> expire|ttl|persist`, `coll expire-sweep`): dispatches
/// `remote-coll-ttl sweep` or `remote-coll-ttl <kind> <op> ...` onto
/// `client.zig`'s `collTtl*` methods (which wire-encode the `coll_ttl_op`
/// opcode), same "reuse client.zig, keep the arg parsing local" shape as
/// `runRemoteTtl`. `sweep` has no `<kind>` (it reaps every kind at once,
/// same as local `coll expire-sweep`), so it is checked before trying to
/// parse `rest[0]` as a kind.
fn runRemoteCollTtl(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
if (rest.len < 1) return fail(stderr, "remote-coll-ttl needs sweep or <kind> expire|ttl|persist <name>");
const address = try parseHostPort(stdout, addressSpec);
var client = cobalt.Client.connect(allocator, address) catch |err| {
try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
std.process.exit(1);
};
defer client.close();
if (std.mem.eql(u8, rest[0], "sweep")) {
const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
defer allocator.free(authFlags.rest);
try authenticateIfRequested(&client, stderr, authFlags.auth);
const reaped = client.collTtlSweep() catch |err| {
try stderr.print("error: {s}\n", .{@errorName(err)});
std.process.exit(1);
};
try stdout.print("reaped: {d}\n", .{reaped});
return;
}
const kind = cobalt.CollectionKind.parse(rest[0]) orelse
return fail(stderr, "unknown collection kind (expected counter/hash/set/list/zset)");
if (rest.len < 2) return fail(stderr, "remote-coll-ttl <kind> needs expire|ttl|persist <name>");
const authFlags = extractAuthFlags(allocator, rest[2..]) catch return fail(stderr, "invalid --user/--pass/--token value");
defer allocator.free(authFlags.rest);
try authenticateIfRequested(&client, stderr, authFlags.auth);
} }
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void { fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {