Skip to content

Commit ce14c76

Browse files
committed
add -o json output to lifecycle commands
create/run/clone/restore emit VM JSON; start/stop/rm emit {"succeeded": [ids...]}. Route log output to stderr so stdout stays parseable.
1 parent 4e3714b commit ce14c76

5 files changed

Lines changed: 100 additions & 13 deletions

File tree

cmd/core/helpers.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,29 @@ func AddFormatFlag(cmd *cobra.Command) {
436436
cmd.Flags().StringP("format", "o", "table", `output format: "table" or "json"`)
437437
}
438438

439+
// AddOutputFlag adds --output/-o for lifecycle commands. Empty default keeps
440+
// the human-readable log output; "json" emits a parseable result on stdout.
441+
func AddOutputFlag(cmd *cobra.Command) {
442+
cmd.Flags().StringP("output", "o", "", `emit "json" for machine-readable output`)
443+
}
444+
445+
// WantJSON reports whether --output=json was requested.
446+
func WantJSON(cmd *cobra.Command) bool {
447+
out, _ := cmd.Flags().GetString("output")
448+
return out == "json"
449+
}
450+
451+
// MaybeOutputJSON emits v as JSON iff --output=json is set. Returns
452+
// (true, encodeErr) when JSON was emitted — caller should return the error
453+
// without falling through to human-readable logging. Returns (false, nil)
454+
// otherwise so the caller proceeds with normal log output.
455+
func MaybeOutputJSON(cmd *cobra.Command, v any) (bool, error) {
456+
if !WantJSON(cmd) {
457+
return false, nil
458+
}
459+
return true, OutputJSON(v)
460+
}
461+
439462
// OutputFormatted outputs as JSON or table based on --format flag.
440463
func OutputFormatted(cmd *cobra.Command, data any, tableFn func(w *tabwriter.Writer)) error {
441464
format, _ := cmd.Flags().GetString("format")

cmd/root.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"os"
78
"os/signal"
89
"runtime"
910
"syscall"
@@ -108,5 +109,12 @@ func initConfig(ctx context.Context) error {
108109
return fmt.Errorf("config: %w", err)
109110
}
110111

111-
return log.SetupLog(ctx, conf.Log, "")
112+
// core/log.SetupLog hardcodes os.Stdout as the logger writer. Swap
113+
// os.Stdout→os.Stderr for the call so the captured writer is stderr,
114+
// preserving stdout for -o json output. Restore immediately after.
115+
origStdout := os.Stdout
116+
os.Stdout = os.Stderr
117+
setupErr := log.SetupLog(ctx, conf.Log, "")
118+
os.Stdout = origStdout
119+
return setupErr
112120
}

cmd/vm/commands.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func Command(h Actions) *cobra.Command {
3636
RunE: h.Create,
3737
}
3838
addVMFlags(createCmd)
39+
cmdcore.AddOutputFlag(createCmd)
3940

4041
runCmd := &cobra.Command{
4142
Use: "run [flags] IMAGE",
@@ -44,6 +45,7 @@ func Command(h Actions) *cobra.Command {
4445
RunE: h.Run,
4546
}
4647
addVMFlags(runCmd)
48+
cmdcore.AddOutputFlag(runCmd)
4749

4850
cloneCmd := &cobra.Command{
4951
Use: "clone [flags] SNAPSHOT",
@@ -52,13 +54,15 @@ func Command(h Actions) *cobra.Command {
5254
RunE: h.Clone,
5355
}
5456
addCloneFlags(cloneCmd)
57+
cmdcore.AddOutputFlag(cloneCmd)
5558

5659
startCmd := &cobra.Command{
5760
Use: "start VM [VM...]",
5861
Short: "Start created/stopped VM(s)",
5962
Args: cobra.MinimumNArgs(1),
6063
RunE: h.Start,
6164
}
65+
cmdcore.AddOutputFlag(startCmd)
6266

6367
stopCmd := &cobra.Command{
6468
Use: "stop VM [VM...]",
@@ -68,6 +72,7 @@ func Command(h Actions) *cobra.Command {
6872
}
6973
stopCmd.Flags().Bool("force", false, "force stop (skip graceful shutdown, immediate SIGTERM/SIGKILL)")
7074
stopCmd.Flags().Int("timeout", 0, "ACPI shutdown timeout in seconds (0 = use config default)")
75+
cmdcore.AddOutputFlag(stopCmd)
7176

7277
listCmd := &cobra.Command{
7378
Use: "list",
@@ -99,6 +104,7 @@ func Command(h Actions) *cobra.Command {
99104
RunE: h.RM,
100105
}
101106
rmCmd.Flags().Bool("force", false, "force delete running VMs")
107+
cmdcore.AddOutputFlag(rmCmd)
102108

103109
restoreCmd := &cobra.Command{
104110
Use: "restore [flags] VM SNAPSHOT",
@@ -110,6 +116,7 @@ func Command(h Actions) *cobra.Command {
110116
restoreCmd.Flags().String("memory", "", "memory size (empty = keep current)")
111117
restoreCmd.Flags().String("storage", "", "COW disk size (empty = keep current)")
112118
restoreCmd.Flags().Bool("on-demand", false, "use UFFD on-demand memory loading for faster restore (CH only; snapshot file must remain on disk)")
119+
cmdcore.AddOutputFlag(restoreCmd)
113120

114121
debugCmd := &cobra.Command{
115122
Use: "debug [flags] IMAGE",

cmd/vm/lifecycle.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (h Handler) Start(cmd *cobra.Command, args []string) error {
3939
h.recoverNetwork(ctx, conf, hyper, refs)
4040
}
4141

42-
return batchRoutedCmd(ctx, "start", "started", routed, func(hyper hypervisor.Hypervisor, refs []string) ([]string, error) {
42+
return batchRoutedCmd(ctx, cmd, "start", "started", routed, func(hyper hypervisor.Hypervisor, refs []string) ([]string, error) {
4343
return hyper.Start(ctx, refs)
4444
})
4545
}
@@ -67,7 +67,7 @@ func (h Handler) Stop(cmd *cobra.Command, args []string) error {
6767
if err != nil {
6868
return err
6969
}
70-
return batchRoutedCmd(ctx, "stop", "stopped", routed, func(hyper hypervisor.Hypervisor, refs []string) ([]string, error) {
70+
return batchRoutedCmd(ctx, cmd, "stop", "stopped", routed, func(hyper hypervisor.Hypervisor, refs []string) ([]string, error) {
7171
return hyper.Stop(ctx, refs)
7272
})
7373
}
@@ -167,12 +167,15 @@ func (h Handler) RM(cmd *cobra.Command, args []string) error {
167167
return err
168168
}
169169

170+
wantJSON := cmdcore.WantJSON(cmd)
170171
var allDeleted []string
171172
var lastErr error
172173
for hyper, refs := range routed {
173174
deleted, deleteErr := hyper.Delete(ctx, refs, force)
174-
for _, id := range deleted {
175-
logger.Infof(ctx, "deleted VM: %s", id)
175+
if !wantJSON {
176+
for _, id := range deleted {
177+
logger.Infof(ctx, "deleted VM: %s", id)
178+
}
176179
}
177180
allDeleted = append(allDeleted, deleted...)
178181
if deleteErr != nil {
@@ -193,6 +196,9 @@ func (h Handler) RM(cmd *cobra.Command, args []string) error {
193196
if lastErr != nil {
194197
return fmt.Errorf("rm: %w", lastErr)
195198
}
199+
if done, jsonErr := cmdcore.MaybeOutputJSON(cmd, map[string][]string{"succeeded": allDeleted}); done {
200+
return jsonErr
201+
}
196202
if len(allDeleted) == 0 {
197203
logger.Info(ctx, "no VMs deleted")
198204
}
@@ -260,14 +266,17 @@ func providerForVM(conf *config.Config, cniProvider network.Network, bridgeCache
260266
return cniProvider, nil
261267
}
262268

263-
func batchRoutedCmd(ctx context.Context, name, pastTense string, routed map[hypervisor.Hypervisor][]string, fn func(hypervisor.Hypervisor, []string) ([]string, error)) error {
269+
func batchRoutedCmd(ctx context.Context, cmd *cobra.Command, name, pastTense string, routed map[hypervisor.Hypervisor][]string, fn func(hypervisor.Hypervisor, []string) ([]string, error)) error {
264270
logger := log.WithFunc("cmd." + name)
271+
wantJSON := cmdcore.WantJSON(cmd)
265272
var allDone []string
266273
var lastErr error
267274
for hyper, refs := range routed {
268275
done, err := fn(hyper, refs)
269-
for _, id := range done {
270-
logger.Infof(ctx, "%s: %s", pastTense, id)
276+
if !wantJSON {
277+
for _, id := range done {
278+
logger.Infof(ctx, "%s: %s", pastTense, id)
279+
}
271280
}
272281
allDone = append(allDone, done...)
273282
if err != nil {
@@ -277,6 +286,9 @@ func batchRoutedCmd(ctx context.Context, name, pastTense string, routed map[hype
277286
if lastErr != nil {
278287
return fmt.Errorf("%s: %w", name, lastErr)
279288
}
289+
if done, jsonErr := cmdcore.MaybeOutputJSON(cmd, map[string][]string{"succeeded": allDone}); done {
290+
return jsonErr
291+
}
280292
if len(allDone) == 0 {
281293
logger.Infof(ctx, "no VMs %s", pastTense)
282294
}

cmd/vm/run.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ func (h Handler) Create(cmd *cobra.Command, args []string) error {
2828
if err != nil {
2929
return err
3030
}
31+
if done, jsonErr := cmdcore.MaybeOutputJSON(cmd, vm); done {
32+
return jsonErr
33+
}
3134
logger := log.WithFunc("cmd.vm.create")
3235
logger.Infof(ctx, "VM created: %s (name: %s, state: %s)", vm.ID, vm.Config.Name, vm.State)
3336
logger.Infof(ctx, "start with: cocoon vm start %s", vm.ID)
@@ -40,12 +43,28 @@ func (h Handler) Run(cmd *cobra.Command, args []string) error {
4043
return err
4144
}
4245
logger := log.WithFunc("cmd.vm.run")
43-
logger.Infof(ctx, "VM created: %s (name: %s)", vm.ID, vm.Config.Name)
46+
wantJSON := cmdcore.WantJSON(cmd)
47+
if !wantJSON {
48+
logger.Infof(ctx, "VM created: %s (name: %s)", vm.ID, vm.Config.Name)
49+
}
4450

4551
started, err := hyper.Start(ctx, []string{vm.ID})
4652
if err != nil {
4753
return fmt.Errorf("start VM %s: %w", vm.ID, err)
4854
}
55+
if wantJSON {
56+
// Re-inspect to capture post-start state (running, PID, IP).
57+
// On failure, fall back to pre-start vm and surface the reason so the
58+
// caller doesn't silently consume stale JSON.
59+
info, inspectErr := hyper.Inspect(ctx, vm.ID)
60+
switch {
61+
case inspectErr != nil:
62+
logger.Warnf(ctx, "inspect after start failed: %v (json payload may be stale)", inspectErr)
63+
case info != nil:
64+
vm = info
65+
}
66+
return cmdcore.OutputJSON(vm)
67+
}
4968
for _, id := range started {
5069
logger.Infof(ctx, "started: %s", id)
5170
}
@@ -110,6 +129,9 @@ func (h Handler) Clone(cmd *cobra.Command, args []string) error {
110129
return fmt.Errorf("clone VM: %w", cloneErr)
111130
}
112131

132+
if done, jsonErr := cmdcore.MaybeOutputJSON(cmd, vm); done {
133+
return jsonErr
134+
}
113135
logger.Infof(ctx, "VM cloned: %s (name: %s)", vm.ID, vm.Config.Name)
114136
printPostCloneHints(vm, networkConfigs)
115137
return nil
@@ -156,7 +178,7 @@ func (h Handler) Restore(cmd *cobra.Command, args []string) error {
156178
return err
157179
}
158180

159-
done, directErr := h.restoreDirect(ctx, snapRef, vmRef, vmCfg, snapBackend, hyper, logger)
181+
done, directErr := h.restoreDirect(ctx, cmd, snapRef, vmRef, vmCfg, snapBackend, hyper, logger)
160182
if done {
161183
return directErr
162184
}
@@ -179,6 +201,9 @@ func (h Handler) Restore(cmd *cobra.Command, args []string) error {
179201
return fmt.Errorf("restore: %w", err)
180202
}
181203

204+
if done, jsonErr := cmdcore.MaybeOutputJSON(cmd, result); done {
205+
return jsonErr
206+
}
182207
logger.Infof(ctx, "VM %s restored (state: %s)", result.ID, result.State)
183208
return nil
184209
}
@@ -194,14 +219,20 @@ func (h Handler) cloneDirect(ctx context.Context, cmd *cobra.Command, conf *conf
194219
return err
195220
}
196221

197-
logger.Infof(ctx, "cloning VM from snapshot %s (direct) ...", snapRef)
222+
wantJSON := cmdcore.WantJSON(cmd)
223+
if !wantJSON {
224+
logger.Infof(ctx, "cloning VM from snapshot %s (direct) ...", snapRef)
225+
}
198226

199227
vm, cloneErr := dcr.DirectClone(ctx, vmID, vmCfg, networkConfigs, cfg, dataDir)
200228
if cloneErr != nil {
201229
rollbackNetwork(ctx, netProvider, vmID)
202230
return fmt.Errorf("clone VM: %w", cloneErr)
203231
}
204232

233+
if wantJSON {
234+
return cmdcore.OutputJSON(vm)
235+
}
205236
logger.Infof(ctx, "VM cloned: %s (name: %s)", vm.ID, vm.Config.Name)
206237
printPostCloneHints(vm, networkConfigs)
207238
return nil
@@ -251,7 +282,7 @@ func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *con
251282
return vmCfg, vmID, netProvider, networkConfigs, nil
252283
}
253284

254-
func (h Handler) restoreDirect(ctx context.Context, snapRef, vmRef string, vmCfg *types.VMConfig, snapBackend snapshot.Snapshot, hyper hypervisor.Hypervisor, logger *log.Fields) (bool, error) {
285+
func (h Handler) restoreDirect(ctx context.Context, cmd *cobra.Command, snapRef, vmRef string, vmCfg *types.VMConfig, snapBackend snapshot.Snapshot, hyper hypervisor.Hypervisor, logger *log.Fields) (bool, error) {
255286
da, ok := snapBackend.(snapshot.Direct)
256287
if !ok {
257288
return false, nil
@@ -266,12 +297,18 @@ func (h Handler) restoreDirect(ctx context.Context, snapRef, vmRef string, vmCfg
266297
return true, fmt.Errorf("open snapshot: %w", err)
267298
}
268299

269-
logger.Infof(ctx, "restoring VM %s from snapshot %s (direct) ...", vmRef, snapRef)
300+
wantJSON := cmdcore.WantJSON(cmd)
301+
if !wantJSON {
302+
logger.Infof(ctx, "restoring VM %s from snapshot %s (direct) ...", vmRef, snapRef)
303+
}
270304
result, err := dcr.DirectRestore(ctx, vmRef, vmCfg, dataDir)
271305
if err != nil {
272306
return true, fmt.Errorf("restore: %w", err)
273307
}
274308

309+
if wantJSON {
310+
return true, cmdcore.OutputJSON(result)
311+
}
275312
logger.Infof(ctx, "VM %s restored (state: %s)", result.ID, result.State)
276313
return true, nil
277314
}

0 commit comments

Comments
 (0)