|
1 | 1 | package cmd |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "errors" |
5 | 4 | "fmt" |
6 | | - "net/http" |
7 | 5 | "os" |
8 | | - "path/filepath" |
9 | | - "sort" |
10 | | - "strings" |
11 | | - "syscall" |
12 | 6 |
|
13 | | - "github.com/charmbracelet/lipgloss" |
14 | | - "github.com/gorilla/websocket" |
15 | 7 | "github.com/render-oss/cli/pkg/client" |
16 | 8 | workflows "github.com/render-oss/cli/pkg/client/workflows" |
17 | 9 | "github.com/render-oss/cli/pkg/command" |
18 | 10 | "github.com/render-oss/cli/pkg/logs" |
19 | | - renderstyle "github.com/render-oss/cli/pkg/style" |
20 | 11 | "github.com/render-oss/cli/pkg/tasks" |
21 | 12 | "github.com/render-oss/cli/pkg/text" |
22 | 13 | "github.com/render-oss/cli/pkg/tui/flows" |
23 | 14 | "github.com/render-oss/cli/pkg/tui/views" |
24 | 15 | workflowviews "github.com/render-oss/cli/pkg/tui/views/workflows" |
25 | | - "github.com/render-oss/cli/pkg/workflows/apiserver" |
26 | | - logstore "github.com/render-oss/cli/pkg/workflows/logs" |
27 | | - "github.com/render-oss/cli/pkg/workflows/orchestrator" |
28 | | - "github.com/render-oss/cli/pkg/workflows/store" |
29 | | - "github.com/render-oss/cli/pkg/workflows/taskserver" |
30 | 16 | "github.com/spf13/cobra" |
31 | 17 | ) |
32 | 18 |
|
33 | | -const defaultTaskAPIPort = 8120 |
34 | | - |
35 | 19 | var taskCmd = &cobra.Command{ |
36 | 20 | Use: "tasks", |
37 | 21 | Short: "Manage tasks", |
38 | 22 | } |
39 | 23 |
|
40 | | -var taskDevCmd = &cobra.Command{ |
41 | | - Use: "dev -- <command to start a workflow service>", |
42 | | - Short: "Start a workflow service in development mode", |
43 | | - SilenceUsage: true, |
44 | | - Long: `Start a workflow service in development mode for local testing. |
45 | | -
|
46 | | -This command runs your workflow service locally on port 8120, allowing you to list and run |
47 | | -tasks without deploying to Render. Task runs and their logs are stored in memory, so you |
48 | | -can query them after tasks complete. |
49 | | -
|
50 | | -The command will spawn a new subprocess with your specified command whenever it needs to |
51 | | -run a task or list the defined tasks. |
52 | | -
|
53 | | -To interact with the local task server: |
54 | | - • Use the --local flag with other task commands (e.g., 'render tasks list --local') |
55 | | - • Or set RENDER_USE_LOCAL_DEV=true when using the workflow client SDK |
56 | | -
|
57 | | -To use a different port: |
58 | | - • Specify --port when starting the dev server |
59 | | - • Then use --port with other task commands, or set RENDER_LOCAL_DEV_URL in the SDK |
60 | | -
|
61 | | -Examples: |
62 | | - render workflows tasks dev -- "go run main.go" |
63 | | - render workflows tasks dev --port 9000 -- "npm start" |
64 | | - render workflows tasks list --local |
65 | | - render workflows taskruns start my-task --local --input='["arg1"]' |
66 | | - `, |
67 | | - RunE: func(cmd *cobra.Command, args []string) error { |
68 | | - ctx := cmd.Context() |
69 | | - var commandArgs []string |
70 | | - if cmd.ArgsLenAtDash() >= 0 { |
71 | | - commandArgs = args[cmd.ArgsLenAtDash():] |
72 | | - } |
73 | | - |
74 | | - if len(commandArgs) == 0 { |
75 | | - return errors.New("command is required") |
76 | | - } |
77 | | - |
78 | | - debugMode, err := cmd.Flags().GetBool("debug") |
79 | | - if err != nil { |
80 | | - return fmt.Errorf("failed to get debug flag: %w", err) |
81 | | - } |
82 | | - |
83 | | - port, err := cmd.Flags().GetInt("port") |
84 | | - if err != nil { |
85 | | - return fmt.Errorf("failed to get port flag: %w", err) |
86 | | - } |
87 | | - |
88 | | - socketTracker, err := orchestrator.NewSocketTracker(ctx) |
89 | | - if err != nil { |
90 | | - return err |
91 | | - } |
92 | | - |
93 | | - taskServerFactory := taskserver.NewTaskServerFactory() |
94 | | - |
95 | | - logs := logstore.NewLogStore() |
96 | | - store := store.NewTaskStore() |
97 | | - var ( |
98 | | - pending []string |
99 | | - ready bool |
100 | | - ) |
101 | | - |
102 | | - statusReporter := orchestrator.NewPrintStatusReporter( |
103 | | - func(format string, args ...any) { |
104 | | - message := fmt.Sprintf(format, args...) |
105 | | - if !ready { |
106 | | - pending = append(pending, message) |
107 | | - return |
108 | | - } |
109 | | - command.Println(cmd, "%s", message) |
110 | | - }, |
111 | | - orchestrator.WithStatusReporterTimestamps(debugMode), |
112 | | - orchestrator.WithStatusReporterTaskEnqueued(debugMode), |
113 | | - orchestrator.WithStatusReporterIncludeInputs(true), |
114 | | - ) |
115 | | - coordinator := orchestrator.NewCoordinator( |
116 | | - ctx, |
117 | | - store, |
118 | | - orchestrator.NewExec(logs, debugMode, commandArgs[0], commandArgs[1:]...), |
119 | | - socketTracker, |
120 | | - taskServerFactory, |
121 | | - statusReporter, |
122 | | - ) |
123 | | - |
124 | | - upgrader := &websocket.Upgrader{ |
125 | | - Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { |
126 | | - http.Error(w, "failed to upgrade http connection to websocket", http.StatusUpgradeRequired) |
127 | | - }, |
128 | | - } |
129 | | - |
130 | | - api := apiserver.NewHandler(coordinator, store, logs, upgrader) |
131 | | - apiSrv, err := apiserver.Start(api, port) |
132 | | - if err != nil { |
133 | | - if errors.Is(err, syscall.EADDRINUSE) { |
134 | | - return fmt.Errorf("port %d is already in use. Stop the other process or use --port to pick a different one", port) |
135 | | - } |
136 | | - return fmt.Errorf("failed to start server on port %d: %w", port, err) |
137 | | - } |
138 | | - |
139 | | - ok := lipgloss.NewStyle().Foreground(renderstyle.ColorOK) |
140 | | - info := lipgloss.NewStyle().Foreground(renderstyle.ColorInfo) |
141 | | - dim := lipgloss.NewStyle().Foreground(renderstyle.ColorDeprioritized) |
142 | | - |
143 | | - command.Println(cmd, "%s %s", |
144 | | - ok.Render("Workflow server listening on"), |
145 | | - renderstyle.Bold(fmt.Sprintf("http://localhost:%d", port)), |
146 | | - ) |
147 | | - |
148 | | - logs.Start(ctx) |
149 | | - |
150 | | - registeredTasks, err := coordinator.PopulateTasks(ctx) |
151 | | - if err != nil { |
152 | | - return fmt.Errorf("failed to load tasks: %w", err) |
153 | | - } |
154 | | - |
155 | | - command.Println(cmd, "%s", formatTaskSummary(info, registeredTasks, describeWorkflowSource(commandArgs))) |
156 | | - command.Println(cmd, "") |
157 | | - |
158 | | - portFlag := "" |
159 | | - if port != defaultTaskAPIPort { |
160 | | - portFlag = fmt.Sprintf(" --port %d", port) |
161 | | - } |
162 | | - command.Println(cmd, "%s", dim.Render("To browse and run tasks, open another terminal and run:")) |
163 | | - command.Println(cmd, " %s", renderstyle.Bold(fmt.Sprintf("render workflows tasks list --local%s", portFlag))) |
164 | | - command.Println(cmd, "") |
165 | | - command.Println(cmd, "%s", dim.Render("To trigger a specific task directly:")) |
166 | | - command.Println(cmd, " %s", renderstyle.Bold(fmt.Sprintf("render workflows taskruns start <task-name> --local%s --input='[\"arg1\"]'", portFlag))) |
167 | | - command.Println(cmd, "") |
168 | | - |
169 | | - ready = true |
170 | | - for _, line := range pending { |
171 | | - command.Println(cmd, "%s", line) |
172 | | - } |
173 | | - pending = nil |
174 | | - |
175 | | - <-ctx.Done() |
176 | | - |
177 | | - apiSrv.Shutdown(ctx) |
178 | | - |
179 | | - return nil |
180 | | - }, |
181 | | - Args: cobra.MinimumNArgs(1), |
182 | | -} |
183 | | - |
184 | 24 | func NewTaskRunStartCmd(deps flows.WorkflowDeps) *cobra.Command { |
185 | 25 | cmd := &cobra.Command{ |
186 | 26 | Use: "start [taskID] --input=<json>", |
@@ -264,11 +104,7 @@ func init() { |
264 | 104 | taskCmd.PersistentFlags().Bool("local", false, "Run against the server spawned by the task dev command") |
265 | 105 | taskCmd.PersistentFlags().Int("port", defaultTaskAPIPort, "Port of the local task server (8120 when not specified)") |
266 | 106 |
|
267 | | - taskDevCmd.Flags().Int("port", defaultTaskAPIPort, "Port of the local task server (8120 when not specified)") |
268 | | - taskDevCmd.Flags().Bool("debug", false, "Print detailed workflow task execution events") |
269 | | - |
270 | 107 | WorkflowsCmd.AddCommand(taskCmd) |
271 | | - taskCmd.AddCommand(taskDevCmd) |
272 | 108 | } |
273 | 109 |
|
274 | 110 | type localDeps struct { |
@@ -320,44 +156,3 @@ func getLocalDeps(cmd *cobra.Command, deps flows.WorkflowDeps) (flows.WorkflowDe |
320 | 156 | } |
321 | 157 | return deps, local, nil |
322 | 158 | } |
323 | | - |
324 | | -func describeWorkflowSource(commandArgs []string) string { |
325 | | - if len(commandArgs) == 0 { |
326 | | - return "" |
327 | | - } |
328 | | - |
329 | | - last := commandArgs[len(commandArgs)-1] |
330 | | - base := filepath.Base(last) |
331 | | - if strings.Contains(base, ".") { |
332 | | - return base |
333 | | - } |
334 | | - |
335 | | - return strings.Join(commandArgs, " ") |
336 | | -} |
337 | | - |
338 | | -func formatTaskSummary(info lipgloss.Style, tasks []*store.Task, source string) string { |
339 | | - if len(tasks) == 0 { |
340 | | - return fmt.Sprintf("0 tasks found in %s (waiting for registration)", renderstyle.Bold(source)) |
341 | | - } |
342 | | - |
343 | | - names := make([]string, 0, len(tasks)) |
344 | | - for _, task := range tasks { |
345 | | - names = append(names, task.Name) |
346 | | - } |
347 | | - sort.Strings(names) |
348 | | - |
349 | | - plural := "" |
350 | | - if len(names) > 1 { |
351 | | - plural = "s" |
352 | | - } |
353 | | - |
354 | | - header := fmt.Sprintf("%d task%s found in %s", len(names), plural, renderstyle.Bold(source)) |
355 | | - |
356 | | - var b strings.Builder |
357 | | - b.WriteString(header) |
358 | | - for _, name := range names { |
359 | | - b.WriteString("\n • ") |
360 | | - b.WriteString(info.Render(name)) |
361 | | - } |
362 | | - return b.String() |
363 | | -} |
0 commit comments