From 62973331921896b5cb550fcb85231b04b8154420 Mon Sep 17 00:00:00 2001 From: arbaazkhan1 Date: Mon, 8 Jun 2026 18:14:42 -0400 Subject: [PATCH 1/6] new envelope format for json output --- .../core/cli/CommandOutputEnvelope.java | 118 ++++++++++++++++++ .../accumulo/core/cli/CommandReport.java | 51 ++++++++ .../apache/accumulo/core/cli/ServerOpts.java | 4 + .../server/util/adminCommand/Fate.java | 8 +- .../util/adminCommand/ServiceStatus.java | 13 +- .../util/fateCommand/FateSummaryReport.java | 9 +- .../serviceStatus/ServiceStatusReport.java | 17 ++- .../test/fate/FateOpsCommandsITBase.java | 42 +++++-- 8 files changed, 234 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java create mode 100644 core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java diff --git a/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java b/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java new file mode 100644 index 00000000000..5c834c20feb --- /dev/null +++ b/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.accumulo.core.cli; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import com.google.gson.Gson; + +/** + * A stable, versioned outer wrapper for all admin command JSON output. + * + *

+ * Every command that supports --json output wraps its command-specific data in this + * envelope. This provides a consistent structure that scripts can rely on regardless of which + * command produced the output: + * + *

+ * {
+ *   "command": "accumulo admin fate --summary",
+ *   "version": "1",
+ *   "reportTime": "2026-06-04T12:00:00Z",
+ *   "status": "OK",
+ *   "message": null,
+ *   "data": { ...command-specific payload... }
+ * }
+ * 
+ * + *

+ * The {@link version} field is a stability contract. When a breaking change is made to the envelope + * structure, the version will be incremented. Scripts should check this field and handle the + * version they were written against. + * + */ +public class CommandOutputEnvelope { + + /** + * Current envelop schema version. Increment this if a breaking structural change is made to the + * envelope fields (not to the {@link data} field, data changes command specific). + */ + public static final String VERSION = "1.0"; + private final DateTimeFormatter ISO_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private static final Gson PRETTY_GSON = + new Gson().newBuilder().setPrettyPrinting().disableJdkUnsafe().create(); + + private final String command; + private final String version; + private final String reportTime; + private final String status; + private final String message; + private final Object data; + + private CommandOutputEnvelope(String command, String status, String message, Object data) { + this.command = command; + this.version = VERSION; + this.reportTime = ISO_FMT.format(ZonedDateTime.now(ZoneOffset.UTC)); + this.status = status; + this.message = message; + this.data = data; + } + + public static CommandOutputEnvelope of(String command, Object data) { + return new CommandOutputEnvelope(command, "OK", null, data); + } + + public static CommandOutputEnvelope error(String command, String message) { + return new CommandOutputEnvelope(command, "ERROR", message, null); + } + + public String toJson() { + return PRETTY_GSON.toJson(this); + } + + public static CommandOutputEnvelope fromJson(String json) { + return PRETTY_GSON.fromJson(json, CommandOutputEnvelope.class); + } + + public String getCommand() { + return command; + } + + public String getVersion() { + return version; + } + + public String getReportTime() { + return reportTime; + } + + public String getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public Object getData() { + return data; + } +} diff --git a/core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java b/core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java new file mode 100644 index 00000000000..f5ceaddb9a2 --- /dev/null +++ b/core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.accumulo.core.cli; + +import java.util.List; + +/** + * Implemented by all command report classes that support both human-readable and computer readable + * outputs. + *

+ * Json output is always wrapped in a {@link CommandOutputEnvelope} to provide a stable, versioned + * outer structure that scripts can depend on, regardless of which command is used. + * + *

+ * Usage pattern is a command's execute() method: + * + *

+ * CommandReport report = buildReport(context, options);
+ * if (options.json()) {
+ *   System.out.println(report.toEnvelopedJson("accumulo admin "));
+ * } else {
+ *   report.formatLines().forEach(System.out::println);
+ * }
+ * 
+ */ +public interface CommandReport { + List formatLines(); + + Object getData(); + + default String toEnvelopedJson(String commandName) { + return CommandOutputEnvelope.of(commandName, getData()).toJson(); + } + +} diff --git a/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java b/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java index 4d8b39e191a..97209736b13 100644 --- a/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java +++ b/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java @@ -53,6 +53,10 @@ public List split(String value) { + " Expected format: -o = [-o =]") private List overrides = new ArrayList<>(); + @Parameter(names = {"-j", "--json"}, + description = "Print output in JSON format. Output is warpped in standard envelope with command, version, reportTime, status and data fields.") + public boolean json = false; + private SiteConfiguration siteConfig = null; public synchronized SiteConfiguration getSiteConfiguration() { diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java b/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java index a3caf0f9757..f2d9da940b4 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java @@ -137,10 +137,6 @@ static class FateOpts extends ServerOpts { description = "[...] Print a summary of FaTE transactions. Print only the FateId's specified or print all transactions if empty. Use -s to only print those with certain states. Use -t to only print those with certain FateInstanceTypes. Use -j to print the transactions in json.") boolean summarize; - @Parameter(names = {"-j", "--json"}, - description = "Print transactions in json. Only useful for --summary command.") - boolean printJson; - @Parameter(names = {"-s", "--state"}, description = "... Print transactions in the state(s) {NEW, IN_PROGRESS, FAILED_IN_PROGRESS, FAILED, SUCCESSFUL}") List states = new ArrayList<>(); @@ -383,8 +379,8 @@ private void summarizeFateTx(ServerContext context, FateOpts cmd, AdminUtil static class ServiceStatusCmdOpts extends ServerOpts { - @Parameter(names = "--json", description = "provide output in json format") - boolean json = false; - @Parameter(names = "--showHosts", description = "provide a summary of service counts with host details") boolean showHosts = false; @@ -106,17 +103,15 @@ public void execute(JCommander cl, ServiceStatusCmdOpts options) throws Exceptio ServiceStatusReport report = new ServiceStatusReport(services, options.showHosts); if (options.json) { - System.out.println(report.toJson()); + System.out.println(report.toEnvelopedJson("accumulo admin service-status")); } else { - StringBuilder sb = new StringBuilder(8192); - report.report(sb); - System.out.println(sb); + report.formatLines().forEach(System.out::println); } } /** - * The manager paths in ZooKeeper are: {@code /accumulo/[IID]/managers/lock/zlock#[NUM]} with the - * lock data providing a service descriptor with host and port. + * op The manager paths in ZooKeeper are: {@code /accumulo/[IID]/managers/lock/zlock#[NUM]} with + * the lock data providing a service descriptor with host and port. */ @VisibleForTesting StatusSummary getManagerStatus(ServerContext context) { diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/fateCommand/FateSummaryReport.java b/server/base/src/main/java/org/apache/accumulo/server/util/fateCommand/FateSummaryReport.java index 704536cd955..e20b276fe3b 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/fateCommand/FateSummaryReport.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/fateCommand/FateSummaryReport.java @@ -32,6 +32,7 @@ import java.util.TreeMap; import java.util.TreeSet; +import org.apache.accumulo.core.cli.CommandReport; import org.apache.accumulo.core.fate.AdminUtil; import org.apache.accumulo.core.fate.Fate; import org.apache.accumulo.core.fate.FateId; @@ -41,7 +42,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -public class FateSummaryReport { +public class FateSummaryReport implements CommandReport { private Map statusCounts = new TreeMap<>(); private Map cmdCounts = new TreeMap<>(); @@ -154,6 +155,7 @@ public static FateSummaryReport fromJson(final String jsonString) { * * @return formatted report lines. */ + @Override public List formatLines() { List lines = new ArrayList<>(); @@ -185,4 +187,9 @@ public List formatLines() { return lines; } + + @Override + public Object getData() { + return this; + } } diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java index 0c628cd6046..8547e02d2f3 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java @@ -25,9 +25,12 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.accumulo.core.cli.CommandReport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +42,19 @@ /** * Wrapper for JSON formatted report. */ -public class ServiceStatusReport { +public class ServiceStatusReport implements CommandReport { + + @Override + public List formatLines() { + StringBuilder sb = new StringBuilder(8192); + report(sb); + return Arrays.asList(sb.toString().split("\n")); + } + + @Override + public Object getData() { + return this; + } private static class HostExclusionStrategy implements ExclusionStrategy { diff --git a/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java b/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java index 5fb90e60c86..53db47fc81d 100644 --- a/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java +++ b/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java @@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -47,6 +48,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.apache.accumulo.core.cli.CommandOutputEnvelope; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.IteratorSetting; @@ -88,6 +90,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + public abstract class FateOpsCommandsITBase extends SharedMiniClusterBase implements FateTestRunner { private static final Logger log = LoggerFactory.getLogger(FateOpsCommandsITBase.class); @@ -134,7 +139,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte String result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - FateSummaryReport report = FateSummaryReport.fromJson(result); + FateSummaryReport report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertTrue(report.getStatusCounts().isEmpty()); @@ -157,7 +162,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -179,7 +184,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -198,7 +203,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -218,7 +223,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -241,7 +246,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -259,7 +264,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -281,7 +286,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -303,7 +308,7 @@ protected void testFateSummaryCommand(FateStore store, ServerConte result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - report = FateSummaryReport.fromJson(result); + report = parseFateSummaryFromEnvelope(result); assertNotNull(report); assertNotEquals(0, report.getReportTime()); assertFalse(report.getStatusCounts().isEmpty()); @@ -516,7 +521,7 @@ protected void testTransactionNameAndStep(FateStore store, ServerC String result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - FateSummaryReport report = FateSummaryReport.fromJson(result); + FateSummaryReport report = parseFateSummaryFromEnvelope(result); // Validate transaction name and transaction step from summary command @@ -904,7 +909,7 @@ private Map getFateIdsFromSummary() throws Exception { String result = p.readStdOut(); result = result.lines().filter(line -> !line.matches(".*(INFO|DEBUG|WARN|ERROR).*")) .collect(Collectors.joining("\n")); - FateSummaryReport report = FateSummaryReport.fromJson(result); + FateSummaryReport report = parseFateSummaryFromEnvelope(result); assertNotNull(report); Map fateIdToStatus = new HashMap<>(); report.getFateDetails().forEach((d) -> { @@ -990,4 +995,19 @@ protected void cleanupFateOps() throws Exception { args.toArray(new String[0])); assertEquals(0, p.getProcess().waitFor()); } + + private FateSummaryReport parseFateSummaryFromEnvelope(String json) { + CommandOutputEnvelope envelope = CommandOutputEnvelope.fromJson(json); + assertNotNull(envelope); + assertEquals(CommandOutputEnvelope.VERSION, envelope.getVersion()); + assertEquals("OK", envelope.getStatus()); + assertNull(envelope.getMessage()); + assertNotNull(envelope.getReportTime()); + assertNotNull(envelope.getCommand()); + assertTrue(envelope.getCommand().contains("fate")); + // data is a JsonObject — re-serialize and deserialize as FateSummaryReport + Gson gson = new GsonBuilder().disableJdkUnsafe().create(); + String dataJson = gson.toJson(envelope.getData()); + return FateSummaryReport.fromJson(dataJson); + } } From 9e8d47758549a0621ba977823497f960b6c68b88 Mon Sep 17 00:00:00 2001 From: arbaazkhan1 Date: Tue, 9 Jun 2026 12:41:23 -0400 Subject: [PATCH 2/6] fixed typo --- .../org/apache/accumulo/core/cli/CommandOutputEnvelope.java | 6 +++--- .../main/java/org/apache/accumulo/core/cli/ServerOpts.java | 2 +- .../org/apache/accumulo/server/util/adminCommand/Fate.java | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java b/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java index 5c834c20feb..45fd923fa77 100644 --- a/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java +++ b/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java @@ -28,9 +28,9 @@ * A stable, versioned outer wrapper for all admin command JSON output. * *

- * Every command that supports --json output wraps its command-specific data in this - * envelope. This provides a consistent structure that scripts can rely on regardless of which - * command produced the output: + * Every command that supports --json output wraps its command-specific data in this envelope. This + * provides a consistent structure that scripts can rely on regardless of which command produced the + * output: * *

  * {
diff --git a/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java b/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java
index 97209736b13..d0a4c387143 100644
--- a/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java
+++ b/core/src/main/java/org/apache/accumulo/core/cli/ServerOpts.java
@@ -54,7 +54,7 @@ public List split(String value) {
   private List overrides = new ArrayList<>();
 
   @Parameter(names = {"-j", "--json"},
-      description = "Print output in JSON format. Output is warpped in standard envelope with command, version, reportTime, status and data fields.")
+      description = "Print output in JSON format. Output is wrapped in standard envelope with command, version, reportTime, status and data fields.")
   public boolean json = false;
 
   private SiteConfiguration siteConfig = null;
diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java b/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java
index f2d9da940b4..dc8671e7b2d 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/Fate.java
@@ -380,7 +380,8 @@ private void summarizeFateTx(ServerContext context, FateOpts cmd, AdminUtil
Date: Tue, 9 Jun 2026 14:47:25 -0400
Subject: [PATCH 3/6] fixed failing tests

---
 .../core/cli/CommandOutputEnvelope.java        | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java b/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java
index 45fd923fa77..7839b97b3ce 100644
--- a/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java
+++ b/core/src/main/java/org/apache/accumulo/core/cli/CommandOutputEnvelope.java
@@ -56,16 +56,20 @@ public class CommandOutputEnvelope {
    * envelope fields (not to the {@link data} field, data changes command specific).
    */
   public static final String VERSION = "1.0";
-  private final DateTimeFormatter ISO_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
+  private static final DateTimeFormatter ISO_FMT =
+      DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
   private static final Gson PRETTY_GSON =
       new Gson().newBuilder().setPrettyPrinting().disableJdkUnsafe().create();
 
-  private final String command;
-  private final String version;
-  private final String reportTime;
-  private final String status;
-  private final String message;
-  private final Object data;
+  private String command;
+  private String version;
+  private String reportTime;
+  private String status;
+  private String message;
+  private Object data;
+
+  @SuppressWarnings("unused")
+  private CommandOutputEnvelope() {}
 
   private CommandOutputEnvelope(String command, String status, String message, Object data) {
     this.command = command;

From 6e556f54c19d927da236f099b9594c563f0c898a Mon Sep 17 00:00:00 2001
From: arbaazkhan1 
Date: Tue, 9 Jun 2026 16:22:34 -0400
Subject: [PATCH 4/6] added error()

---
 .../accumulo/server/util/ServerKeywordExecutable.java     | 8 ++++++++
 .../apache/accumulo/test/fate/FateOpsCommandsITBase.java  | 1 -
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java b/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
index 04251f250df..8742c1b6398 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
@@ -19,6 +19,7 @@
 package org.apache.accumulo.server.util;
 
 import org.apache.accumulo.core.cli.BaseKeywordExecutable;
+import org.apache.accumulo.core.cli.CommandOutputEnvelope;
 import org.apache.accumulo.core.cli.ServerOpts;
 import org.apache.accumulo.core.conf.AccumuloConfiguration;
 import org.apache.accumulo.core.conf.Property;
@@ -58,6 +59,13 @@ public void doExecute(JCommander cl, OPTS options) throws Exception {
         SecurityUtil.serverLogin(conf);
       }
       execute(cl, options);
+    } catch (Exception e) {
+      if (options.json) {
+        String commandName = "accumulo"
+            + (commandGroup().key().isBlank() ? "" : " " + commandGroup().key()) + " " + keyword();
+        System.out.println(CommandOutputEnvelope.error(commandName, e.getMessage()).toJson());
+        System.exit(1);
+      }
     }
   }
 }
diff --git a/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java b/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java
index 53db47fc81d..1b16dcc4963 100644
--- a/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java
+++ b/test/src/main/java/org/apache/accumulo/test/fate/FateOpsCommandsITBase.java
@@ -1005,7 +1005,6 @@ private FateSummaryReport parseFateSummaryFromEnvelope(String json) {
     assertNotNull(envelope.getReportTime());
     assertNotNull(envelope.getCommand());
     assertTrue(envelope.getCommand().contains("fate"));
-    // data is a JsonObject — re-serialize and deserialize as FateSummaryReport
     Gson gson = new GsonBuilder().disableJdkUnsafe().create();
     String dataJson = gson.toJson(envelope.getData());
     return FateSummaryReport.fromJson(dataJson);

From 9d9f30181e900275fc24ef533103587b5a056860 Mon Sep 17 00:00:00 2001
From: arbaazkhan1 
Date: Tue, 9 Jun 2026 17:53:36 -0400
Subject: [PATCH 5/6] removed System.exit

---
 .../main/java/org/apache/accumulo/core/cli/CommandReport.java   | 2 +-
 .../apache/accumulo/server/util/ServerKeywordExecutable.java    | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java b/core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java
index f5ceaddb9a2..ba33faa578e 100644
--- a/core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java
+++ b/core/src/main/java/org/apache/accumulo/core/cli/CommandReport.java
@@ -33,7 +33,7 @@
  * 
  * CommandReport report = buildReport(context, options);
  * if (options.json()) {
- *   System.out.println(report.toEnvelopedJson("accumulo admin "));
+ *   System.out.println(report.toEnvelopedJson("accumulo admin 'my-command'"));
  * } else {
  *   report.formatLines().forEach(System.out::println);
  * }
diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java b/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
index 8742c1b6398..d254ce03a90 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
@@ -64,7 +64,6 @@ public void doExecute(JCommander cl, OPTS options) throws Exception {
         String commandName = "accumulo"
             + (commandGroup().key().isBlank() ? "" : " " + commandGroup().key()) + " " + keyword();
         System.out.println(CommandOutputEnvelope.error(commandName, e.getMessage()).toJson());
-        System.exit(1);
       }
     }
   }

From 0d10159ddbba5189e019384d49dab5ed8ac6f112 Mon Sep 17 00:00:00 2001
From: arbaazkhan1 
Date: Wed, 10 Jun 2026 15:13:26 -0400
Subject: [PATCH 6/6] added exception throw

---
 .../org/apache/accumulo/server/util/ServerKeywordExecutable.java | 1 +
 1 file changed, 1 insertion(+)

diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java b/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
index d254ce03a90..37bf274a958 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/ServerKeywordExecutable.java
@@ -65,6 +65,7 @@ public void doExecute(JCommander cl, OPTS options) throws Exception {
             + (commandGroup().key().isBlank() ? "" : " " + commandGroup().key()) + " " + keyword();
         System.out.println(CommandOutputEnvelope.error(commandName, e.getMessage()).toJson());
       }
+      throw e;
     }
   }
 }