Skip to content

Commit 17dfc51

Browse files
committed
feat: Add '--follow' flag for start/restart
This allows for following logs on re/starting instances. Following logs prints everything from when the instance stopped. Signed-off-by: Cezar Craciunoiu <[email protected]>
1 parent 8fa8415 commit 17dfc51

7 files changed

Lines changed: 176 additions & 24 deletions

File tree

cmd/unikraft/integration/instance_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,59 @@ func TestInstances(t *testing.T) {
146146
r.Run(t, []string{"unikraft", "instance", "delete", "test-" + instName})
147147
})
148148

149+
t.Run("start-follow", func(t *testing.T) {
150+
r := runner(t, true)
151+
instName := uniq()
152+
153+
// Create a stopped instance.
154+
r.Run(t, []string{
155+
"unikraft", "instance", "create",
156+
"--output", "quiet",
157+
"--set", "name=test-" + instName,
158+
"--set", "metro=" + r.Config.MetroName,
159+
"--set", "image=nginx:latest",
160+
"--set", "autostart=false",
161+
"--set", "resources.memory=128",
162+
"--set", "resources.vcpus=1",
163+
})
164+
165+
// Start it and follow logs; killed after 5 s is treated as success.
166+
r.Run(t, []string{
167+
"unikraft", "instance", "start",
168+
"--follow",
169+
"test-" + instName,
170+
}, integ.WithTimeout(5*time.Second))
171+
172+
r.Run(t, []string{"unikraft", "instance", "delete", "test-" + instName})
173+
})
174+
175+
t.Run("restart-follow", func(t *testing.T) {
176+
r := runner(t, true)
177+
instName := uniq()
178+
179+
// Create and start an instance.
180+
r.Run(t, []string{
181+
"unikraft", "instance", "create",
182+
"--output", "quiet",
183+
"--set", "name=test-" + instName,
184+
"--set", "metro=" + r.Config.MetroName,
185+
"--set", "image=nginx:latest",
186+
"--set", "autostart=true",
187+
"--set", "resources.memory=128",
188+
"--set", "resources.vcpus=1",
189+
})
190+
r.Run(t, []string{"unikraft", "instance", "wait", "--until", "state==running", "--timeout", "30s", "test-" + instName})
191+
192+
// Restart it and follow logs; killed after 5 s is treated as success.
193+
r.Run(t, []string{
194+
"unikraft", "instance", "restart",
195+
"--follow",
196+
"test-" + instName,
197+
}, integ.WithTimeout(5*time.Second))
198+
199+
r.Run(t, []string{"unikraft", "instance", "delete", "test-" + instName})
200+
})
201+
149202
t.Run("edit", func(t *testing.T) {
150203
r := runner(t, true)
151204
instName := uniq()

cmd/unikraft/testdata/TestHelp/instances

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ require (
5757
unikraft.com/x/joinerrgroup v0.0.0-20260304162956-523940cab1de
5858
unikraft.com/x/kingkong v0.0.0-20260331102539-2c733927b46f
5959
unikraft.com/x/kraftfile v0.0.0-20260501125416-502eb6c9c39d
60-
unikraft.com/x/log v0.0.0-20260126171022-af62c17fcdf7
60+
unikraft.com/x/log v0.0.0-20260527070319-f904d21cd6ea
6161
unikraft.com/x/ptr v0.0.0-20260126094137-ab6e717e5679
6262
)
6363

64+
require go.opentelemetry.io/otel/log v0.19.0 // indirect
65+
6466
require (
6567
charm.land/fang/v2 v2.0.1 // indirect
6668
dario.cat/mergo v1.0.2 // indirect
@@ -92,7 +94,6 @@ require (
9294
github.com/docker/go-connections v0.7.0 // indirect
9395
github.com/felixge/httpsnoop v1.0.4 // indirect
9496
github.com/fsnotify/fsnotify v1.9.0 // indirect
95-
github.com/getsentry/sentry-go v0.41.0 // indirect
9697
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
9798
github.com/go-logr/logr v1.4.3 // indirect
9899
github.com/go-logr/stdr v1.2.2 // indirect

go.sum

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
130130
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
131131
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
132132
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
133-
github.com/getsentry/sentry-go v0.41.0 h1:q/dQZOlEIb4lhxQSjJhQqtRr3vwrJ6Ahe1C9zv+ryRo=
134-
github.com/getsentry/sentry-go v0.41.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
135-
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
136-
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
137133
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
138134
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
139135
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -273,8 +269,6 @@ github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/
273269
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
274270
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
275271
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
276-
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
277-
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
278272
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
279273
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
280274
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -394,6 +388,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
394388
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
395389
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
396390
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
391+
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
392+
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
397393
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
398394
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
399395
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
@@ -507,7 +503,7 @@ unikraft.com/x/kingkong v0.0.0-20260331102539-2c733927b46f h1:pT+qeeK2nWgSlRaIEK
507503
unikraft.com/x/kingkong v0.0.0-20260331102539-2c733927b46f/go.mod h1:x/sY2g/oA3GKDTZyOFC1xArmd6ZySwc4KZlp1MO8a2k=
508504
unikraft.com/x/kraftfile v0.0.0-20260501125416-502eb6c9c39d h1:x10/Kuf67r6UTYT0BuTbEkLdxHglWGY3eV+59WQFpWc=
509505
unikraft.com/x/kraftfile v0.0.0-20260501125416-502eb6c9c39d/go.mod h1:OYaIMOzV1IpbZCe2J3SuC5jvT4tAfR0XG9uOPPD+p50=
510-
unikraft.com/x/log v0.0.0-20260126171022-af62c17fcdf7 h1:kFOE7rmK33PiJ2GbWFd3v0WrE8DeAaQsPjrUQ+15CIU=
511-
unikraft.com/x/log v0.0.0-20260126171022-af62c17fcdf7/go.mod h1:f/+5628rnWTnRaRrCDD9mOMf7OV5QmkGIgARlTu4XGE=
506+
unikraft.com/x/log v0.0.0-20260527070319-f904d21cd6ea h1:nI+37MA1awnJXdM38jM5TjjYU8ATIZ79aICWcsl40Qc=
507+
unikraft.com/x/log v0.0.0-20260527070319-f904d21cd6ea/go.mod h1:Lq81MseYxHhX3SK7XSheX5QAZDOdKw1yhE84NTRZ5+g=
512508
unikraft.com/x/ptr v0.0.0-20260126094137-ab6e717e5679 h1:ohptI7loX492JCbbn6yk13hAxysZHyGIZWN8hTw3080=
513509
unikraft.com/x/ptr v0.0.0-20260126094137-ab6e717e5679/go.mod h1:une+KauhLNYkBFnT0R4bzVvD8bJ54xACjXMBPMnc5ig=

internal/cmd/instances.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,8 @@ func (cmd *InstancesLogsCmd) Run(ctx context.Context, stdio config.Stdio) error
12501250
type InstancesStartCmd struct {
12511251
Targets []string `arg:"" name:"target" completion-predictor:"resource-key-instance" help:"Target instances to start."`
12521252

1253+
Follow bool `help:"Follow log output after starting."`
1254+
12531255
cmd.FormatOpts
12541256
}
12551257

@@ -1261,6 +1263,12 @@ func (cmd InstancesStartCmd) Examples() []kingkong.Example {
12611263
"unikraft instance start demo-instance",
12621264
},
12631265
},
1266+
{
1267+
Description: "Start an instance and follow its logs",
1268+
Commands: []string{
1269+
"unikraft instance start demo-instance --follow",
1270+
},
1271+
},
12641272
}
12651273
}
12661274

@@ -1275,6 +1283,19 @@ func (c *InstancesStartCmd) Run(ctx context.Context, stdio config.Stdio) error {
12751283
return err
12761284
}
12771285

1286+
var (
1287+
mux *muxreader.Mux
1288+
cancel context.CancelFunc
1289+
)
1290+
if c.Follow {
1291+
mux, cancel, err = newInstanceLogMux(ctx, keys, new(0), true)
1292+
if err != nil {
1293+
return err
1294+
}
1295+
defer cancel()
1296+
defer mux.Close()
1297+
}
1298+
12781299
started, startErr := startInstances(ctx, g, keys)
12791300
opErr = errors.Join(opErr, startErr)
12801301
if len(started) == 0 {
@@ -1301,7 +1322,14 @@ func (c *InstancesStartCmd) Run(ctx context.Context, stdio config.Stdio) error {
13011322
})
13021323

13031324
diffErr := cmd.Diff(ctx, stdio.Stdout, c.FormatOpts, Instance{}, before, updated)
1304-
return errors.Join(opErr, diffErr)
1325+
if err := errors.Join(opErr, diffErr); err != nil {
1326+
return err
1327+
}
1328+
if !c.Follow || len(started) == 0 {
1329+
return nil
1330+
}
1331+
fmt.Fprintln(stdio.Stdout)
1332+
return drainInstanceLogs(stdio, mux, true)
13051333
}
13061334

13071335
type InstancesStopCmd struct {
@@ -1381,6 +1409,8 @@ type InstancesRestartCmd struct {
13811409
Targets []string `arg:"" name:"target" completion-predictor:"resource-key-instance" help:"Target instances to restart."`
13821410
StopOpts
13831411

1412+
Follow bool `help:"Follow log output after restarting."`
1413+
13841414
cmd.FormatOpts
13851415
}
13861416

@@ -1398,6 +1428,12 @@ func (cmd InstancesRestartCmd) Examples() []kingkong.Example {
13981428
"unikraft instance restart demo-instance --force",
13991429
},
14001430
},
1431+
{
1432+
Description: "Restart an instance and follow its logs",
1433+
Commands: []string{
1434+
"unikraft instance restart demo-instance --follow",
1435+
},
1436+
},
14011437
}
14021438
}
14031439

@@ -1418,6 +1454,19 @@ func (c *InstancesRestartCmd) Run(ctx context.Context, stdio config.Stdio) error
14181454
return opErr
14191455
}
14201456

1457+
var (
1458+
mux *muxreader.Mux
1459+
cancel context.CancelFunc
1460+
)
1461+
if c.Follow {
1462+
mux, cancel, err = newInstanceLogMux(ctx, stopped, new(0), true)
1463+
if err != nil {
1464+
return err
1465+
}
1466+
defer cancel()
1467+
defer mux.Close()
1468+
}
1469+
14211470
started, startErr := startInstances(ctx, g, stopped)
14221471
if len(started) == 0 {
14231472
if _, ok := startErr.(group.ErrRefNotFound); ok {
@@ -1448,7 +1497,14 @@ func (c *InstancesRestartCmd) Run(ctx context.Context, stdio config.Stdio) error
14481497
})
14491498

14501499
diffErr := cmd.Diff(ctx, stdio.Stdout, c.FormatOpts, Instance{}, before, updated)
1451-
return errors.Join(opErr, diffErr)
1500+
if err := errors.Join(opErr, diffErr); err != nil {
1501+
return err
1502+
}
1503+
if !c.Follow || len(started) == 0 {
1504+
return nil
1505+
}
1506+
fmt.Fprintln(stdio.Stdout)
1507+
return drainInstanceLogs(stdio, mux, true)
14521508
}
14531509

14541510
type StopOpts struct {

internal/cmd/run.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,40 +128,53 @@ func (c *RunCmd) Run(ctx context.Context, stdio config.Stdio, sandbox *resource.
128128
keys = append(keys, instance.key)
129129
}
130130

131-
return streamInstanceLogs(ctx, stdio, keys, 0, true)
131+
return streamInstanceLogs(ctx, stdio, keys, nil, c.Follow)
132132
}
133133

134-
func streamInstanceLogs(ctx context.Context, stdio config.Stdio, keys multimetro.Keys, tail int, follow bool) error {
134+
func newInstanceLogMux(ctx context.Context, keys multimetro.Keys, tail *int, follow bool) (*muxreader.Mux, context.CancelFunc, error) {
135135
g, err := multimetro.NewClient(ctx)
136136
if err != nil {
137-
return err
137+
return nil, nil, err
138138
}
139139

140140
mux := muxreader.New()
141-
defer mux.Close()
142141

143142
ctx, cancel := context.WithCancel(ctx)
144-
defer cancel()
145143

146144
err = group.DoRefs(ctx, g, keys.Refs(), func(_ context.Context, c multimetro.MetroClient, refs group.Refs) (group.Refs, error) {
147145
for _, ref := range refs {
148-
key := multimetro.Key(ref)
149-
r, err := logs.InstanceLogs(ctx, c).Reader(ref.NameOrUUID(), &tail, follow)
146+
r, err := logs.InstanceLogs(ctx, c).Reader(ref.NameOrUUID(), tail, follow)
150147
if err != nil {
151148
return nil, err
152149
}
153-
mux.With(key.String(), r)
150+
mux.With(multimetro.Key(ref).String(), r)
154151
}
155152
return refs, nil
156153
})
157154
if err != nil {
158-
return err
155+
cancel()
156+
mux.Close()
157+
return nil, nil, err
159158
}
160159
mux.Seal()
161160

162-
_, err = io.Copy(stdio.Stdout, mux)
161+
return mux, cancel, nil
162+
}
163+
164+
func drainInstanceLogs(stdio config.Stdio, mux *muxreader.Mux, follow bool) error {
165+
_, err := io.Copy(stdio.Stdout, mux)
163166
if follow && errors.Is(err, context.Canceled) {
164167
return nil
165168
}
166169
return err
167170
}
171+
172+
func streamInstanceLogs(ctx context.Context, stdio config.Stdio, keys multimetro.Keys, tail *int, follow bool) error {
173+
mux, cancel, err := newInstanceLogMux(ctx, keys, tail, follow)
174+
if err != nil {
175+
return err
176+
}
177+
defer cancel()
178+
defer mux.Close()
179+
return drainInstanceLogs(stdio, mux, follow)
180+
}

0 commit comments

Comments
 (0)