Skip to content

Commit 8afe523

Browse files
committed
feat(deploy): print last logs from failed pre-deploy hook
1 parent 5b5cd44 commit 8afe523

3 files changed

Lines changed: 78 additions & 7 deletions

File tree

‎cmd/uncloud/deploy.go‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@ import (
1010
composecli "github.com/compose-spec/compose-go/v2/cli"
1111
"github.com/docker/compose/v2/pkg/progress"
1212
"github.com/psviderski/uncloud/internal/cli"
13+
"github.com/psviderski/uncloud/internal/cli/logs"
1314
"github.com/psviderski/uncloud/internal/cli/tui"
15+
"github.com/psviderski/uncloud/pkg/api"
1416
"github.com/psviderski/uncloud/pkg/client"
1517
"github.com/psviderski/uncloud/pkg/client/compose"
1618
"github.com/psviderski/uncloud/pkg/client/deploy"
19+
"github.com/psviderski/uncloud/pkg/client/deploy/operation"
1720
"github.com/spf13/cobra"
1821
)
1922

23+
// failedContainerLogsTail is the number of recent log lines to print from a failed container to give the user immediate
24+
// context without requiring a follow-up 'uc logs' invocation.
25+
const failedContainerLogsTail = 10
26+
2027
type deployOptions struct {
2128
cli.BuildServicesOptions
2229

@@ -223,10 +230,48 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
223230
if deployTarget != "" {
224231
title += " to " + tui.NameStyle.Render(deployTarget)
225232
}
226-
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
233+
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
227234
if err := plan.Execute(ctx, clusterClient); err != nil {
228235
return fmt.Errorf("deploy services: %w", err)
229236
}
230237
return nil
231238
}, uncli.ProgressOut(), title)
239+
if err != nil {
240+
fmt.Println()
241+
if hookErr, ok := errors.AsType[*operation.PreDeployHookError](err); ok {
242+
printPreDeployHookLogs(ctx, clusterClient, hookErr)
243+
fmt.Println()
244+
}
245+
return err
246+
}
247+
return nil
248+
}
249+
250+
// printPreDeployHookLogs fetches the last log lines from the failed pre-deploy hook container and prints them using
251+
// the standard log formatter.
252+
func printPreDeployHookLogs(ctx context.Context, cli *client.Client, hookErr *operation.PreDeployHookError) {
253+
_, ch, err := cli.ServiceLogs(ctx, hookErr.ServiceName, api.ServiceLogsOptions{
254+
Containers: []string{hookErr.ContainerID},
255+
Machines: []string{hookErr.MachineName},
256+
Tail: failedContainerLogsTail,
257+
})
258+
if err != nil {
259+
fmt.Fprintf(os.Stderr, "Failed to fetch pre-deploy hook logs: %v\n", err)
260+
fmt.Fprintf(os.Stderr, "You can try manually with: uc logs %s\n", hookErr.ServiceName)
261+
return
262+
}
263+
264+
header := fmt.Sprintf("Last %d log lines from failed pre-deploy hook:", failedContainerLogsTail)
265+
fmt.Println(tui.BoldRed.Render(header))
266+
267+
logsEmpty := true
268+
formatter := logs.NewFormatter([]string{hookErr.MachineName}, []string{hookErr.ServiceName}, false)
269+
for entry := range ch {
270+
logsEmpty = false
271+
formatter.PrintEntry(entry)
272+
}
273+
274+
if logsEmpty {
275+
fmt.Println("<no logs available>")
276+
}
232277
}

‎pkg/api/client.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Client interface {
1212
ContainerClient
1313
DNSClient
1414
ImageClient
15+
LogsClient
1516
MachineClient
1617
ServiceClient
1718
VolumeClient
@@ -43,6 +44,13 @@ type ImageClient interface {
4344
InspectRemoteImage(ctx context.Context, id string) ([]MachineRemoteImage, error)
4445
}
4546

47+
type LogsClient interface {
48+
ServiceLogs(
49+
ctx context.Context, serviceNameOrID string, opts ServiceLogsOptions,
50+
) (Service, <-chan ServiceLogEntry, error)
51+
MachineLogs(ctx context.Context, unit string, opts ServiceLogsOptions) (<-chan ServiceLogEntry, error)
52+
}
53+
4654
type MachineClient interface {
4755
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
4856
ListMachines(ctx context.Context, filter *MachineFilter) (MachineMembersList, error)

‎pkg/client/deploy/operation/predeploy.go‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import (
1616
"github.com/psviderski/uncloud/pkg/api"
1717
)
1818

19+
// PreDeployHookError indicates that a pre-deploy hook container exited with a non-zero code or timed out.
20+
type PreDeployHookError struct {
21+
error
22+
ServiceName string
23+
ContainerID string
24+
MachineName string
25+
}
26+
1927
// DefaultPreDeployTimeout is the maximum duration to wait for a pre-deploy hook container to complete.
2028
const DefaultPreDeployTimeout = 5 * time.Minute
2129

@@ -128,9 +136,14 @@ func (o *RunPreDeployOperation) waitForExit(
128136
return fmt.Errorf("pre-deploy hook container '%s': %w", ctrID, ctx.Err())
129137
}
130138
pw.Event(progress.NewEvent(eventID, progress.Error, fmt.Sprintf("Timeout (%s)", timeout)))
131-
return fmt.Errorf("pre-deploy hook container '%s' timed out after %s. "+
132-
"It's stopped and available for inspection. Fetch logs with 'uc logs %s'",
133-
ctrID, timeout, o.Spec.Name)
139+
return &PreDeployHookError{
140+
ServiceName: o.Spec.Name,
141+
ContainerID: containerID,
142+
MachineName: o.MachineName,
143+
error: fmt.Errorf("pre-deploy hook container '%s' timed out after %s. "+
144+
"It's stopped and available for inspection. View logs with 'uc logs %s'",
145+
ctrID, timeout, o.Spec.Name),
146+
}
134147

135148
case <-ticker.C:
136149
mc, err := cli.InspectContainer(ctx, o.ServiceID, containerID)
@@ -157,9 +170,14 @@ func (o *RunPreDeployOperation) waitForExit(
157170

158171
pw.Event(progress.ErrorEvent(eventID))
159172
ctrID := fmt.Sprintf("%s/%s", o.Spec.Name, ctr.ShortID())
160-
return fmt.Errorf("pre-deploy hook container '%s' failed with exit code: %d. "+
161-
"It's stopped and available for inspection. Fetch logs with 'uc logs %s'",
162-
ctrID, ctr.State.ExitCode, o.Spec.Name)
173+
return &PreDeployHookError{
174+
ServiceName: o.Spec.Name,
175+
ContainerID: containerID,
176+
MachineName: o.MachineName,
177+
error: fmt.Errorf("pre-deploy hook container '%s' failed with exit code: %d. "+
178+
"It's stopped and available for inspection. View logs with 'uc logs %s'",
179+
ctrID, ctr.State.ExitCode, o.Spec.Name),
180+
}
163181
}
164182
}
165183
}

0 commit comments

Comments
 (0)