Skip to content

Commit 55755c1

Browse files
authored
cmd-app-deploy-subcommands-added (#7)
<!-- mesa-description-start --> ## TL;DR This PR enhances the `deploy` command by adding `logs` and `history` subcommands, providing users with better tools to monitor and debug deployments. ## Why we made these changes The `deploy` command previously lacked built-in ways to easily access deployment logs or view past deployment history. This change makes the CLI more powerful and user-friendly by providing direct access to critical deployment information, improving the overall developer experience. ## What changed? - **`cmd/deploy.go`**: - Added a `deploy logs` subcommand to stream logs, with flags for following (`-f`), specifying a time range (`--since`), and showing timestamps. - Added a `deploy history` subcommand to display a table of past deployments. - Improved error messages to include the Deployment ID and a direct command to view logs on failure. - **`cmd/root.go`**: - Refactored the authentication exemption logic to correctly exempt the root command when it's called without any subcommands. <sup>_Description generated by Mesa. [Update settings](https://app.mesa.dev/onkernel/settings/pull-requests)_</sup> <!-- mesa-description-end -->
1 parent 06e946e commit 55755c1

File tree

1 file changed

+165
-2
lines changed

1 file changed

+165
-2
lines changed

cmd/deploy.go

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ import (
1515
"github.com/spf13/cobra"
1616
)
1717

18+
var deployLogsCmd = &cobra.Command{
19+
Use: "logs <deployment_id>",
20+
Short: "Stream logs for a deployment",
21+
Args: cobra.ExactArgs(1),
22+
RunE: runDeployLogs,
23+
}
24+
25+
var deployHistoryCmd = &cobra.Command{
26+
Use: "history [app_name]",
27+
Short: "Show deployment history",
28+
Args: cobra.RangeArgs(0, 1),
29+
RunE: runDeployHistory,
30+
}
31+
1832
var deployCmd = &cobra.Command{
1933
Use: "deploy <entrypoint>",
2034
Short: "Deploy a Kernel application",
@@ -27,6 +41,15 @@ func init() {
2741
deployCmd.Flags().Bool("force", false, "Allow overwrite of an existing version with the same name")
2842
deployCmd.Flags().StringArrayP("env", "e", []string{}, "Set environment variables (e.g., KEY=value). May be specified multiple times")
2943
deployCmd.Flags().StringArray("env-file", []string{}, "Read environment variables from a file (.env format). May be specified multiple times")
44+
45+
// Subcommands under deploy
46+
deployLogsCmd.Flags().BoolP("follow", "f", false, "Follow logs in real-time (stream continuously)")
47+
deployLogsCmd.Flags().StringP("since", "s", "", "How far back to retrieve logs. Supports duration formats: ns, us, ms, s, m, h (e.g., 5m, 2h, 1h30m). Note: 'd' not supported; use hours instead. Can also specify timestamps: 2006-01-02, 2006-01-02T15:04, 2006-01-02T15:04:05, 2006-01-02T15:04:05.000. Max lookback ~167h.")
48+
deployLogsCmd.Flags().BoolP("with-timestamps", "t", false, "Include timestamps in each log line")
49+
deployCmd.AddCommand(deployLogsCmd)
50+
51+
deployHistoryCmd.Flags().Bool("all", false, "Show deployment history for all applications")
52+
deployCmd.AddCommand(deployHistoryCmd)
3053
}
3154

3255
func runDeploy(cmd *cobra.Command, args []string) (err error) {
@@ -54,7 +77,7 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) {
5477
spinner.Fail("Failed to compress files")
5578
return err
5679
}
57-
spinner.Info("Compressed files")
80+
spinner.Success("Compressed files")
5881
defer os.Remove(tmpFile)
5982

6083
// make io.Reader from tmpFile
@@ -119,6 +142,8 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) {
119142
if status == string(kernel.DeploymentGetResponseStatusFailed) ||
120143
status == string(kernel.DeploymentGetResponseStatusStopped) {
121144
pterm.Error.Println("✖ Deployment failed")
145+
pterm.Error.Printf("Deployment ID: %s\n", resp.ID)
146+
pterm.Info.Printf("View logs: kernel deploy logs %s --since 1h\n", resp.ID)
122147
err = fmt.Errorf("deployment %s: %s", status, deploymentState.Deployment.StatusReason)
123148
return err
124149
}
@@ -136,21 +161,159 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) {
136161
}
137162
case "error":
138163
errorEv := data.AsErrorEvent()
164+
pterm.Error.Printf("Deployment ID: %s\n", resp.ID)
165+
pterm.Info.Printf("View logs: kernel deploy logs %s --since 1h\n", resp.ID)
139166
err = fmt.Errorf("%s: %s", errorEv.Error.Code, errorEv.Error.Message)
140167
return err
141168
}
142169
}
143170

144171
if serr := stream.Err(); serr != nil {
145172
pterm.Error.Println("✖ Stream error")
173+
pterm.Error.Printf("Deployment ID: %s\n", resp.ID)
174+
pterm.Info.Printf("View logs: kernel deploy logs %s --since 1h\n", resp.ID)
146175
return fmt.Errorf("stream error: %w", serr)
147176
}
148177
return nil
149178
}
150179

151180
func quoteIfNeeded(s string) string {
152-
if len(s) > 0 && (s[0] == ' ' || s[len(s)-1] == ' ') {
181+
if strings.ContainsRune(s, ' ') {
153182
return fmt.Sprintf("\"%s\"", s)
154183
}
155184
return s
156185
}
186+
187+
func runDeployLogs(cmd *cobra.Command, args []string) error {
188+
client := getKernelClient(cmd)
189+
190+
deploymentID := args[0]
191+
pterm.Info.Printf("Streaming logs for deployment %s...\n", deploymentID)
192+
193+
since, _ := cmd.Flags().GetString("since")
194+
follow, _ := cmd.Flags().GetBool("follow")
195+
ts, _ := cmd.Flags().GetBool("with-timestamps")
196+
197+
stream := client.Deployments.FollowStreaming(cmd.Context(), deploymentID, kernel.DeploymentFollowParams{Since: kernel.Opt(since)}, option.WithMaxRetries(0))
198+
defer func() { _ = stream.Close() }()
199+
if stream.Err() != nil {
200+
return fmt.Errorf("failed to open log stream: %w", stream.Err())
201+
}
202+
203+
if follow {
204+
for stream.Next() {
205+
data := stream.Current()
206+
switch data.Event {
207+
case "log":
208+
logEntry := data.AsLog()
209+
if ts {
210+
fmt.Printf("%s %s\n", logEntry.Timestamp.Format(time.RFC3339Nano), strings.TrimSuffix(logEntry.Message, "\n"))
211+
} else {
212+
fmt.Println(strings.TrimSuffix(logEntry.Message, "\n"))
213+
}
214+
case "error":
215+
errEvt := data.AsErrorEvent()
216+
return fmt.Errorf("%s: %s", errEvt.Error.Code, errEvt.Error.Message)
217+
}
218+
}
219+
} else {
220+
// Non-follow: exit after brief inactivity window (3s) like app logs
221+
timeout := time.NewTimer(3 * time.Second)
222+
defer timeout.Stop()
223+
for {
224+
nextCh := make(chan bool, 1)
225+
go func() { nextCh <- stream.Next() }()
226+
select {
227+
case hasNext := <-nextCh:
228+
if !hasNext {
229+
return nil
230+
}
231+
data := stream.Current()
232+
switch data.Event {
233+
case "log":
234+
logEntry := data.AsLog()
235+
if ts {
236+
fmt.Printf("%s %s\n", logEntry.Timestamp.Format(time.RFC3339Nano), strings.TrimSuffix(logEntry.Message, "\n"))
237+
} else {
238+
fmt.Println(strings.TrimSuffix(logEntry.Message, "\n"))
239+
}
240+
case "error":
241+
errEvt := data.AsErrorEvent()
242+
return fmt.Errorf("%s: %s", errEvt.Error.Code, errEvt.Error.Message)
243+
}
244+
timeout.Reset(3 * time.Second)
245+
case <-timeout.C:
246+
_ = stream.Close()
247+
return nil
248+
}
249+
}
250+
}
251+
252+
if stream.Err() != nil {
253+
return fmt.Errorf("failed while streaming logs: %w", stream.Err())
254+
}
255+
return nil
256+
}
257+
258+
func runDeployHistory(cmd *cobra.Command, args []string) error {
259+
client := getKernelClient(cmd)
260+
261+
all, _ := cmd.Flags().GetBool("all")
262+
263+
var appNames []string
264+
if len(args) == 1 {
265+
appNames = []string{args[0]}
266+
} else if all {
267+
apps, err := client.Apps.List(cmd.Context(), kernel.AppListParams{})
268+
if err != nil {
269+
pterm.Error.Printf("Failed to list applications: %v\n", err)
270+
return nil
271+
}
272+
for _, a := range *apps {
273+
appNames = append(appNames, a.AppName)
274+
}
275+
// de-duplicate app names
276+
seen := map[string]struct{}{}
277+
uniq := make([]string, 0, len(appNames))
278+
for _, n := range appNames {
279+
if _, ok := seen[n]; ok {
280+
continue
281+
}
282+
seen[n] = struct{}{}
283+
uniq = append(uniq, n)
284+
}
285+
appNames = uniq
286+
} else {
287+
pterm.Error.Println("Either provide an app name or use --all")
288+
return nil
289+
}
290+
291+
table := pterm.TableData{{"Deployment ID", "Created At", "Region", "Status", "Entrypoint", "Reason"}}
292+
for _, appName := range appNames {
293+
params := kernel.DeploymentListParams{AppName: kernel.Opt(appName)}
294+
pterm.Debug.Printf("Listing deployments for app '%s'...\n", appName)
295+
deployments, err := client.Deployments.List(cmd.Context(), params)
296+
if err != nil {
297+
pterm.Error.Printf("Failed to list deployments for '%s': %v\n", appName, err)
298+
continue
299+
}
300+
for _, dep := range *deployments {
301+
created := dep.CreatedAt.Format(time.RFC3339)
302+
status := string(dep.Status)
303+
table = append(table, []string{
304+
dep.ID,
305+
created,
306+
string(dep.Region),
307+
status,
308+
dep.EntrypointRelPath,
309+
dep.StatusReason,
310+
})
311+
}
312+
}
313+
if len(table) == 1 {
314+
pterm.Info.Println("No deployments found")
315+
return nil
316+
}
317+
pterm.DefaultTable.WithHasHeader().WithData(table).Render()
318+
return nil
319+
}

0 commit comments

Comments
 (0)