This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new dfad2701c fix(rmqctl): propagate configuration output failures (#6310)
dfad2701c is described below
commit dfad2701cb2667d4791c93a86b326c1e77732b82
Author: pilichoumao <[email protected]>
AuthorDate: Mon Oct 19 18:54:54 2026 +0800
fix(rmqctl): propagate configuration output failures (#6310)
Verified against `rocketmq-studio` at f81e73a49 before merging:
缺陷仍在(`rmqctl/cmd/config.go:69-70` set-context、`:101-102` use-context、`:133-134`
delete-context、`:175-176` current-context,四处仍写完 `return nil`;主干未动该文件,行
merge-tree clean against the current trunk. Merged as-is (squash) per the
maintainer review of 2026-10-19.
---
rmqctl/cmd/config.go | 18 +++---
rmqctl/cmd/config_output_test.go | 122 +++++++++++++++++++++++++++++++++++++++
2 files changed, 132 insertions(+), 8 deletions(-)
diff --git a/rmqctl/cmd/config.go b/rmqctl/cmd/config.go
index ab1bf8d0a..0f264fec5 100644
--- a/rmqctl/cmd/config.go
+++ b/rmqctl/cmd/config.go
@@ -16,6 +16,8 @@ import (
"github.com/spf13/pflag"
)
+// Configuration changes are saved before confirmation output. Report output
errors without
+// rolling back or replaying the successful change.
func newConfigCommand(runtime commandRuntime) *cobra.Command {
root := &cobra.Command{Use: "config", Short: "Manage local rmqctl
contexts"}
root.AddCommand(newConfigSetContextCommand(runtime))
@@ -66,8 +68,8 @@ func newConfigSetContextCommand(runtime commandRuntime)
*cobra.Command {
if output.IsStructured(runtime.options.output) {
return output.Structured(out,
runtime.options.output, map[string]any{"context": name, "created": !exists})
}
- fmt.Fprintf(out, "updated context %s\n", name)
- return nil
+ _, err = fmt.Fprintf(out, "updated context %s\n", name)
+ return err
},
}
cmd.Flags().StringVar(&server, "server", "", "Studio Server URL")
@@ -98,8 +100,8 @@ func newConfigUseContextCommand(runtime commandRuntime)
*cobra.Command {
if output.IsStructured(runtime.options.output) {
return output.Structured(out,
runtime.options.output, map[string]string{"currentContext": args[0]})
}
- fmt.Fprintf(out, "current context: %s\n", args[0])
- return nil
+ _, err = fmt.Fprintf(out, "current context: %s\n",
args[0])
+ return err
},
}
}
@@ -130,8 +132,8 @@ func newConfigDeleteContextCommand(runtime commandRuntime)
*cobra.Command {
if output.IsStructured(runtime.options.output) {
return output.Structured(out,
runtime.options.output, map[string]any{"deleted": name})
}
- fmt.Fprintf(out, "deleted context %s\n", name)
- return nil
+ _, err = fmt.Fprintf(out, "deleted context %s\n", name)
+ return err
},
}
}
@@ -172,8 +174,8 @@ func newConfigCurrentContextCommand(runtime commandRuntime)
*cobra.Command {
if output.IsStructured(runtime.options.output) {
return output.Structured(out,
runtime.options.output, map[string]string{"currentContext": cfg.CurrentContext})
}
- fmt.Fprintln(out, cfg.CurrentContext)
- return nil
+ _, err = fmt.Fprintln(out, cfg.CurrentContext)
+ return err
},
}
}
diff --git a/rmqctl/cmd/config_output_test.go b/rmqctl/cmd/config_output_test.go
new file mode 100644
index 000000000..55d3873c6
--- /dev/null
+++ b/rmqctl/cmd/config_output_test.go
@@ -0,0 +1,122 @@
+/*
+ * 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
+ *
+ * http://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 cmd
+
+import (
+ "bytes"
+ "errors"
+ "strings"
+ "testing"
+
+ "github.com/apache/rocketmq-dashboard/rmqctl/internal/config"
+)
+
+type configResultWriter struct {
+ buffer bytes.Buffer
+ failure error
+}
+
+func (w *configResultWriter) Write(p []byte) (int, error) {
+ if w.failure != nil {
+ return 0, w.failure
+ }
+ return w.buffer.Write(p)
+}
+
+func TestConfigCommandOutput(t *testing.T) {
+ commands := []struct {
+ name string
+ args []string
+ text string
+ }{
+ {"set-context", []string{"set-context", "prod", "--server",
"https://updated.example.com"}, "updated context prod\n"},
+ {"use-context", []string{"use-context", "dev"}, "current
context: dev\n"},
+ {"delete-context", []string{"delete-context", "prod"}, "deleted
context prod\n"},
+ {"current-context", []string{"current-context"}, "prod\n"},
+ }
+ for _, command := range commands {
+ for _, format := range []string{"table", "json", "yaml"} {
+ for _, fail := range []bool{false, true} {
+ mode := "success"
+ if fail {
+ mode = "writer failure"
+ }
+ t.Run(command.name+"/"+format+"/"+mode, func(t
*testing.T) {
+ out := &configResultWriter{}
+ if fail {
+ out.failure =
errors.New("synthetic output unavailable")
+ }
+ stderr := &bytes.Buffer{}
+ app := NewApp(out, stderr)
+ app.Store.Getenv = emptyEnv
+ path := newTestConfigPath(t)
+ context := config.Context{Server:
"https://studio.example.com", Credential: config.CredentialRef{
+ AccessKeyRef:
"env:SYNTHETIC_AK", SecretKeyRef: "env:SYNTHETIC_SK",
+ }}
+ if err := app.Store.Save(path,
config.Config{CurrentContext: "prod", Contexts: map[string]config.Context{
+ "prod": context, "dev": context,
+ }}); err != nil {
+ t.Fatal(err)
+ }
+ args := append([]string{"--config",
path, "--output", format, "config"}, command.args...)
+ code := app.Execute(args)
+ // An output failure must not roll back
or replay an already saved mutation.
+ saved, err := app.Store.Load(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ switch command.name {
+ case "set-context":
+ if
saved.Contexts["prod"].Server != "https://updated.example.com" {
+ t.Fatalf("saved
context: %#v", saved)
+ }
+ case "use-context":
+ if saved.CurrentContext !=
"dev" {
+ t.Fatalf("saved
context: %#v", saved)
+ }
+ case "delete-context":
+ if _, exists :=
saved.Contexts["prod"]; exists || saved.CurrentContext != "" {
+ t.Fatalf("saved
context: %#v", saved)
+ }
+ case "current-context":
+ if saved.CurrentContext !=
"prod" {
+ t.Fatalf("saved
context: %#v", saved)
+ }
+ }
+ if fail {
+ if code == 0 {
+ t.Fatal("command
reported success despite failed output")
+ }
+ if
!strings.Contains(stderr.String(), "synthetic output unavailable") {
+ t.Fatalf("lost writer
error: %s", stderr)
+ }
+ } else {
+ if code != 0 {
+ t.Fatalf("exit %d: %s",
code, stderr)
+ }
+ if format == "table" &&
out.buffer.String() != command.text {
+ t.Fatalf("unexpected
output: %q", out.buffer.String())
+ }
+ if out.buffer.Len() == 0 {
+ t.Fatal("missing
output")
+ }
+ }
+ })
+ }
+ }
+ }
+}