feat(xce-test-cli-ux): wire confirmation gate to the five destructive commands
Gates jobkill and shutdown unconditionally, qctrl only when action is QUEUE_CLOSE/QUEUE_INACTIVE (not ACTIVE/OPEN), hostctrl only when HOST_CLOSE (not OPEN), and upgradeconfig unconditionally -- exactly Requirement 9.5's enumeration, with jstart/jfinish/jfail (worker context) correctly untouched. Each gains a -y/--yes bypass flag via flagspec_t; cmd_set gains -y/+y calling confirm_set_skip(). A decline maps to a plain `return 0` (success-coded, distinct from a negative failure return) per confirm.h's own documented contract -- this means a declined destructive command inside a source'd file under `set -e` won't trip abort-on-failure, a real but narrow interaction only reachable when a human is actively declining; left for task 4.3 to test explicitly. 1321/1321 across the full controller suite (+3), 0 regressions. Task 3.3 of .kiro/specs/xce-test-cli-ux/tasks.md. Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
parent
45120af2cc
commit
a57c04716b
3 changed files with 635 additions and 5 deletions
|
|
@ -361,6 +361,7 @@ static const char USAGE_SET[] =
|
|||
"set\n"
|
||||
"set <key> <value>\n"
|
||||
"set -e | +e | continue-on-error\n"
|
||||
"set -y | +y\n"
|
||||
" (no args) list all settings in the current context\n"
|
||||
" (current value + default)\n"
|
||||
" <key> <value> set setting <key> (e.g. masters, secret,\n"
|
||||
|
|
@ -369,6 +370,12 @@ static const char USAGE_SET[] =
|
|||
" command (errexit)\n"
|
||||
" +e | continue-on-error keep the read loop running after a\n"
|
||||
" failing command\n"
|
||||
" -y skip the confirmation prompt before a\n"
|
||||
" destructive command (jobkill, shutdown,\n"
|
||||
" qctrl -i/-c, hostctrl -c, upgradeconfig)\n"
|
||||
" regardless of TTY state, until +y\n"
|
||||
" +y require confirmation before those\n"
|
||||
" commands again (default)\n"
|
||||
"example: set masters ctrl1=localhost:9991,ctrl2=localhost:9992\n";
|
||||
|
||||
static const char USAGE_JOIN_WORKER[] =
|
||||
|
|
@ -1897,10 +1904,59 @@ static int shutdown_thunk(void* ctxp)
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.3: confirmation bypass. shutdown
|
||||
* had no flags at all before this task (task 3.1's getopt migration
|
||||
* explicitly left it out of scope for exactly that reason) -- this is the
|
||||
* FIRST flagspec_t table it gets, added solely to carry --yes. Same
|
||||
* short/long spelling as the other four destructive commands
|
||||
* (Requirement 6.2). */
|
||||
static const flagspec_t shutdown_flags[] = {
|
||||
{ 'y', "yes", "yes", no_argument, 0, NULL },
|
||||
};
|
||||
#define SHUTDOWN_NFLAGS (sizeof(shutdown_flags) / sizeof(shutdown_flags[0]))
|
||||
|
||||
int cmd_shutdown(int argc, char* argv[])
|
||||
{
|
||||
int rc;
|
||||
shutdown_call_t* call = (shutdown_call_t*)calloc(1, sizeof(*call));
|
||||
int opt;
|
||||
int yes = 0;
|
||||
struct option long_opts[SHUTDOWN_NFLAGS + 1];
|
||||
char optstring[16];
|
||||
shutdown_call_t* call;
|
||||
|
||||
optind = 1;
|
||||
opterr = 0;
|
||||
|
||||
if (flagspec_build_opts(shutdown_flags, SHUTDOWN_NFLAGS, long_opts,
|
||||
optstring, sizeof(optstring))
|
||||
< 0) {
|
||||
fprintf(stderr,
|
||||
"xce_test: shutdown: internal error building flag table\n");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
while ((opt = getopt_long(argc, argv, optstring, long_opts, NULL))
|
||||
!= -1) {
|
||||
switch (opt) {
|
||||
case 'y':
|
||||
yes = 1;
|
||||
break;
|
||||
case '?':
|
||||
return flagspec_reject("shutdown", shutdown_flags,
|
||||
SHUTDOWN_NFLAGS, argv[optind - 1],
|
||||
"shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.5: shutdown is unconditionally
|
||||
* destructive. See cmd_jobkill's comment above for why this call site
|
||||
* only needs to check its own --yes flag. */
|
||||
if (!yes && !confirm_destructive("shut down the xce controller")) {
|
||||
fprintf(stderr, "shutdown: not run (declined)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
call = (shutdown_call_t*)calloc(1, sizeof(*call));
|
||||
|
||||
if (call == NULL) {
|
||||
return -ENOMEM;
|
||||
|
|
@ -3529,6 +3585,12 @@ int cmd_resval(int argc, char* argv[])
|
|||
static const flagspec_t jobkill_flags[] = {
|
||||
{ 's', "signal", "signal", required_argument, 0, "SIGTERM" },
|
||||
{ 'j', "jobid", "jobid", required_argument, 1, NULL },
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.3: per-command confirmation
|
||||
* bypass -- jobkill is unconditionally destructive (Requirement 9.5), so
|
||||
* this flag's only effect is to skip the confirm_destructive() prompt
|
||||
* below. Same short/long spelling used across all five destructive
|
||||
* commands (Requirement 6.2 cross-command consistency). */
|
||||
{ 'y', "yes", "yes", no_argument, 0, NULL },
|
||||
};
|
||||
#define JOBKILL_NFLAGS (sizeof(jobkill_flags) / sizeof(jobkill_flags[0]))
|
||||
|
||||
|
|
@ -3554,6 +3616,7 @@ int cmd_jobkill(int argc, char* argv[])
|
|||
uint64_t jobid = 0;
|
||||
int opt;
|
||||
int rc;
|
||||
int yes = 0;
|
||||
xce_jobctrl_t jobctrl;
|
||||
xce_signal_t siginfo;
|
||||
char jobidStr[64];
|
||||
|
|
@ -3581,6 +3644,9 @@ int cmd_jobkill(int argc, char* argv[])
|
|||
case 'j':
|
||||
parse_jobid(&jobid, optarg);
|
||||
break;
|
||||
case 'y':
|
||||
yes = 1;
|
||||
break;
|
||||
case '?':
|
||||
return flagspec_reject("jobkill", jobkill_flags, JOBKILL_NFLAGS,
|
||||
argv[optind - 1],
|
||||
|
|
@ -3593,6 +3659,28 @@ int cmd_jobkill(int argc, char* argv[])
|
|||
return -1;
|
||||
}
|
||||
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.5: jobkill is unconditionally
|
||||
* destructive -- no action-dependent check like qctrl/hostctrl below.
|
||||
* confirm_destructive() already owns the session-toggle and TTY checks
|
||||
* internally (confirm.h); this call site's only job is to short-circuit
|
||||
* around it when jobkill's OWN --yes flag was given (confirm_destructive
|
||||
* has no way to know about a per-command flag). A 0 return means the
|
||||
* operator declined (or Ctrl-C'd the prompt, Requirement 9.9) -- report
|
||||
* "not run" (a plain, non-negative return so the session verdict
|
||||
* machinery in preprocess_and_run treats this as success, not failure --
|
||||
* see confirm.h's return-contract note), distinct from a "failed" RPC. */
|
||||
fmt_jobid(jobidStr, sizeof(jobidStr) - 1, jobid);
|
||||
|
||||
if (!yes) {
|
||||
char desc[96];
|
||||
|
||||
snprintf(desc, sizeof(desc), "kill job %s", jobidStr);
|
||||
if (!confirm_destructive(desc)) {
|
||||
fprintf(stderr, "jobkill: not run (declined)\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
jobctrl.action = JobControl_u_siginfo;
|
||||
jobctrl.jobid = jobid;
|
||||
jobctrl.u.siginfo = &siginfo;
|
||||
|
|
@ -3624,7 +3712,6 @@ int cmd_jobkill(int argc, char* argv[])
|
|||
return rc;
|
||||
}
|
||||
|
||||
fmt_jobid(jobidStr, sizeof(jobidStr) - 1, jobid);
|
||||
printf("Job %s signaled\n", jobidStr);
|
||||
|
||||
return 0;
|
||||
|
|
@ -3755,6 +3842,12 @@ static const flagspec_t qctrl_flags[] = {
|
|||
{ 'o', "open", "open", no_argument, 0, NULL },
|
||||
{ 'c', "close", "close", no_argument, 0, NULL },
|
||||
{ 'q', "queue", "queue", required_argument, 1, NULL },
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.3: confirmation bypass.
|
||||
* qctrl is destructive ONLY for -i/--deactivate and -c/--close
|
||||
* (Requirement 9.5); -a/--activate and -o/--open are reversal/
|
||||
* reactivation actions and never prompt. Same short/long spelling as
|
||||
* the other four destructive commands (Requirement 6.2). */
|
||||
{ 'y', "yes", "yes", no_argument, 0, NULL },
|
||||
};
|
||||
#define QCTRL_NFLAGS (sizeof(qctrl_flags) / sizeof(qctrl_flags[0]))
|
||||
|
||||
|
|
@ -3786,6 +3879,7 @@ int cmd_qctrl(int argc, char* argv[])
|
|||
* indeterminate stack memory as the queue action. */
|
||||
int action = QUEUE_DO_NONE;
|
||||
char* queue = NULL;
|
||||
int yes = 0;
|
||||
struct option long_opts[QCTRL_NFLAGS + 1];
|
||||
char optstring[16];
|
||||
|
||||
|
|
@ -3818,6 +3912,9 @@ int cmd_qctrl(int argc, char* argv[])
|
|||
case 'q':
|
||||
queue = strdup(optarg);
|
||||
break;
|
||||
case 'y':
|
||||
yes = 1;
|
||||
break;
|
||||
case '?':
|
||||
FREEUP(queue);
|
||||
return flagspec_reject("qctrl", qctrl_flags, QCTRL_NFLAGS,
|
||||
|
|
@ -3841,6 +3938,23 @@ int cmd_qctrl(int argc, char* argv[])
|
|||
return -1;
|
||||
}
|
||||
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.5: qctrl is destructive ONLY
|
||||
* for -i/--deactivate and -c/--close -- -a/--activate and -o/--open are
|
||||
* reversal/reactivation actions and never prompt, regardless of short
|
||||
* or long spelling. See cmd_jobkill's comment above for why this call
|
||||
* site only needs to check its own --yes flag. */
|
||||
if (((action == QUEUE_INACTIVE) || (action == QUEUE_CLOSE)) && !yes) {
|
||||
char desc[160];
|
||||
const char* verb = (action == QUEUE_CLOSE) ? "close" : "deactivate";
|
||||
|
||||
snprintf(desc, sizeof(desc), "%s queue %s", verb, queue);
|
||||
if (!confirm_destructive(desc)) {
|
||||
fprintf(stderr, "qctrl: not run (declined)\n");
|
||||
free(queue);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
queuectrl.action = action;
|
||||
queuectrl.queue = queue;
|
||||
|
||||
|
|
@ -3890,6 +4004,11 @@ static const flagspec_t hostctrl_flags[] = {
|
|||
{ 'o', "open", "open", no_argument, 0, NULL },
|
||||
{ 'c', "close", "close", no_argument, 0, NULL },
|
||||
{ 'h', "host", "host", required_argument, 1, NULL },
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.3: confirmation bypass.
|
||||
* hostctrl is destructive ONLY for -c/--close (Requirement 9.5);
|
||||
* -o/--open is a reactivation action and never prompts. Same short/long
|
||||
* spelling as the other four destructive commands (Requirement 6.2). */
|
||||
{ 'y', "yes", "yes", no_argument, 0, NULL },
|
||||
};
|
||||
#define HOSTCTRL_NFLAGS (sizeof(hostctrl_flags) / sizeof(hostctrl_flags[0]))
|
||||
|
||||
|
|
@ -3915,6 +4034,7 @@ int cmd_hostctrl(int argc, char* argv[])
|
|||
xce_hostctrl_t hostctrl;
|
||||
int action = HOST_DO_NONE;
|
||||
char* hostname = NULL;
|
||||
int yes = 0;
|
||||
struct option long_opts[HOSTCTRL_NFLAGS + 1];
|
||||
char optstring[16];
|
||||
|
||||
|
|
@ -3941,6 +4061,9 @@ int cmd_hostctrl(int argc, char* argv[])
|
|||
case 'h':
|
||||
hostname = strdup(optarg);
|
||||
break;
|
||||
case 'y':
|
||||
yes = 1;
|
||||
break;
|
||||
case '?':
|
||||
FREEUP(hostname);
|
||||
return flagspec_reject("hostctrl", hostctrl_flags,
|
||||
|
|
@ -3960,6 +4083,22 @@ int cmd_hostctrl(int argc, char* argv[])
|
|||
return -1;
|
||||
}
|
||||
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.5: hostctrl is destructive
|
||||
* ONLY for -c/--close -- -o/--open is a reactivation action and never
|
||||
* prompts, regardless of short or long spelling. See cmd_jobkill's
|
||||
* comment above for why this call site only needs to check its own
|
||||
* --yes flag. */
|
||||
if ((action == HOST_CLOSE) && !yes) {
|
||||
char desc[160];
|
||||
|
||||
snprintf(desc, sizeof(desc), "close host %s", hostname);
|
||||
if (!confirm_destructive(desc)) {
|
||||
fprintf(stderr, "hostctrl: not run (declined)\n");
|
||||
free(hostname);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
hostctrl.action = action;
|
||||
hostctrl.hostname = hostname;
|
||||
|
||||
|
|
@ -4029,14 +4168,58 @@ static int upgradeconfig_thunk(void* ctxp)
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.3: confirmation bypass.
|
||||
* upgradeconfig had no flags at all before this task (task 3.1's getopt
|
||||
* migration explicitly left it out of scope for exactly that reason,
|
||||
* alongside shutdown) -- this is the FIRST flagspec_t table it gets, added
|
||||
* solely to carry --yes; the <cluster.bin> path stays a plain positional
|
||||
* argument (argv[optind] once getopt_long has consumed --yes). Same
|
||||
* short/long spelling as the other four destructive commands
|
||||
* (Requirement 6.2). */
|
||||
static const flagspec_t upgradeconfig_flags[] = {
|
||||
{ 'y', "yes", "yes", no_argument, 0, NULL },
|
||||
};
|
||||
#define UPGRADECONFIG_NFLAGS \
|
||||
(sizeof(upgradeconfig_flags) / sizeof(upgradeconfig_flags[0]))
|
||||
|
||||
int cmd_upgradeconfig(int argc, char* argv[])
|
||||
{
|
||||
if (argc != 2) {
|
||||
int opt;
|
||||
int yes = 0;
|
||||
struct option long_opts[UPGRADECONFIG_NFLAGS + 1];
|
||||
char optstring[16];
|
||||
|
||||
optind = 1;
|
||||
opterr = 0;
|
||||
|
||||
if (flagspec_build_opts(upgradeconfig_flags, UPGRADECONFIG_NFLAGS,
|
||||
long_opts, optstring, sizeof(optstring))
|
||||
< 0) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"xce_test: upgradeconfig: internal error building flag table\n");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
while ((opt = getopt_long(argc, argv, optstring, long_opts, NULL))
|
||||
!= -1) {
|
||||
switch (opt) {
|
||||
case 'y':
|
||||
yes = 1;
|
||||
break;
|
||||
case '?':
|
||||
return flagspec_reject("upgradeconfig", upgradeconfig_flags,
|
||||
UPGRADECONFIG_NFLAGS, argv[optind - 1],
|
||||
"upgradeconfig <cluster.bin>");
|
||||
}
|
||||
}
|
||||
|
||||
if ((argc - optind) != 1) {
|
||||
fprintf(stderr, "usage: upgradeconfig <cluster.bin>\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* path = argv[1];
|
||||
const char* path = argv[optind];
|
||||
FILE* fp = fopen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
fprintf(stderr, "upgradeconfig: cannot open %s: %s\n", path,
|
||||
|
|
@ -4078,6 +4261,23 @@ int cmd_upgradeconfig(int argc, char* argv[])
|
|||
uint8_t sha256[SHA256_DIGEST_LENGTH];
|
||||
SHA256(bytes, (size_t)fsz, sha256);
|
||||
|
||||
/* xce-test-cli-ux task 3.3, Requirement 9.5: upgradeconfig is
|
||||
* unconditionally destructive. See cmd_jobkill's comment above for why
|
||||
* this call site only needs to check its own --yes flag. Deliberately
|
||||
* placed after the file has already been read/hashed (a purely local,
|
||||
* read-only step) and right before the network call itself. */
|
||||
if (!yes) {
|
||||
char desc[PATH_MAX + 64];
|
||||
|
||||
snprintf(desc, sizeof(desc),
|
||||
"upgrade the cluster configuration from %s", path);
|
||||
if (!confirm_destructive(desc)) {
|
||||
fprintf(stderr, "upgradeconfig: not run (declined)\n");
|
||||
free(bytes);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
upgradeconfig_call_t* call
|
||||
= (upgradeconfig_call_t*)calloc(1, sizeof(*call));
|
||||
int rc;
|
||||
|
|
@ -5467,6 +5667,22 @@ int cmd_set(int argc, char* argv[])
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* Confirmation-skip toggle (xce-test-cli-ux task 3.3, Requirement
|
||||
* 9.6/9.7), same shell-style on/off pair as `-e`/`+e` immediately
|
||||
* above: `-y` enables skipping (assume yes to every destructive
|
||||
* command's prompt, regardless of TTY state), `+y` restores the default
|
||||
* (confirmation required). Wraps confirm_set_skip()/confirm_is_skip_set()
|
||||
* (confirm.h, task 2.1), which the five destructive commands' own
|
||||
* confirm_destructive() calls already consult internally. */
|
||||
if ((argc == 2) && (strcmp(argv[1], "-y") == 0)) {
|
||||
confirm_set_skip(1); /* -y: skip destructive-command confirmation */
|
||||
return 0;
|
||||
}
|
||||
if ((argc == 2) && (strcmp(argv[1], "+y") == 0)) {
|
||||
confirm_set_skip(0); /* +y: require confirmation again (default) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (argc != 3) {
|
||||
printf("set <key> <value>\n");
|
||||
return -EINVAL;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue