@@ -2,13 +2,12 @@ package main
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
7- "log"
88 "log/slog"
99 "net/http"
1010 "os"
11- "path"
1211 "time"
1312
1413 dockerClient "github.com/docker/docker/client"
@@ -17,31 +16,10 @@ import (
1716
1817 "github.com/arduino/arduino-app-cli/internal/api"
1918 "github.com/arduino/arduino-app-cli/internal/orchestrator"
19+ "github.com/arduino/arduino-app-cli/pkg/httprecover"
2020 "github.com/arduino/arduino-app-cli/pkg/parser"
2121)
2222
23- const DockerRegistry = "ghcr.io/bcmi-labs/"
24- const DockerPythonImage = "arduino/appslab-python-apps-base:0.0.2"
25-
26- var pythonImage string
27-
28- func init () {
29- // Registry base: contains the registry and namespace, common to all Arduino docker images.
30- registryBase := os .Getenv ("DOCKER_REGISTRY_BASE" )
31- if registryBase == "" {
32- registryBase = DockerRegistry
33- }
34-
35- // Python image: image name (repository) and optionally a tag.
36- pythonImageAndTag := os .Getenv ("DOCKER_PYTHON_BASE_IMAGE" )
37- if pythonImageAndTag == "" {
38- pythonImageAndTag = DockerPythonImage
39- }
40-
41- pythonImage = path .Join (registryBase , pythonImageAndTag )
42- fmt .Println ("Using pythonImage:" , pythonImage )
43- }
44-
4523func main () {
4624 docker , err := dockerClient .NewClientWithOpts (
4725 dockerClient .FromEnv ,
@@ -52,117 +30,199 @@ func main() {
5230 }
5331 defer docker .Close ()
5432
55- var parsedApp parser.App
33+ var daemonPort string
34+ var completionNoDesc bool // Disable completion description for shells that support it
5635
5736 rootCmd := & cobra.Command {
58- Use : "app <APP_PATH> " ,
37+ Use : "arduino- app-cli " ,
5938 Short : "A CLI to manage the Python app" ,
6039 PersistentPreRun : func (cmd * cobra.Command , args []string ) {
61- if cmd .Name () == "daemon" {
62- return
63- }
64- if len (args ) != 1 {
65- _ = cmd .Help ()
66- os .Exit (1 )
67- }
68- app , err := parser .Load (args [0 ])
69- if err != nil {
70- log .Panic (err )
40+ },
41+ }
42+
43+ completionCommand := & cobra.Command {
44+ Use : "completion [bash|zsh|fish|powershell] [--no-descriptions]" ,
45+ ValidArgs : []string {"bash" , "zsh" , "fish" , "powershell" },
46+ Args : cobra .ExactArgs (1 ),
47+ Short : "Generates completion scripts" ,
48+ Long : "Generates completion scripts for various shells" ,
49+ Example : " " + os .Args [0 ] + " completion bash > completion.sh\n " +
50+ " " + "source completion.sh" ,
51+ RunE : func (cmd * cobra.Command , args []string ) error {
52+ switch args [0 ] {
53+ case "bash" :
54+ return cmd .Root ().GenBashCompletionV2 (cmd .OutOrStdout (), ! completionNoDesc )
55+ case "zsh" :
56+ if completionNoDesc {
57+ return cmd .Root ().GenZshCompletionNoDesc (cmd .OutOrStdout ())
58+ } else {
59+ return cmd .Root ().GenZshCompletion (cmd .OutOrStdout ())
60+ }
61+ case "fish" :
62+ return cmd .Root ().GenFishCompletion (cmd .OutOrStdout (), ! completionNoDesc )
63+ case "powershell" :
64+ return cmd .Root ().GenPowerShellCompletion (cmd .OutOrStdout ())
7165 }
72- parsedApp = app
66+ return nil
67+ },
68+ }
69+ completionCommand .Flags ().BoolVar (& completionNoDesc , "no-descriptions" , false , "Disable completion description for shells that support it" )
70+
71+ daemonCmd := & cobra.Command {
72+ Use : "daemon" ,
73+ Short : "Run an HTTP server to expose arduino-app-cli functionality thorough REST API" ,
74+ Run : func (cmd * cobra.Command , args []string ) {
75+ httpHandler (cmd .Context (), docker , daemonPort )
7376 },
7477 }
78+ daemonCmd .Flags ().StringVar (& daemonPort , "port" , "8080" , "The TCP port the daemon will listen to" )
7579
7680 rootCmd .AddCommand (
7781 & cobra.Command {
78- Use : "stop" ,
82+ Use : "stop app_path " ,
7983 Short : "Stop the Python app" ,
80- Run : func (cmd * cobra.Command , args []string ) {
81- stopHandler (cmd .Context (), parsedApp )
84+ Args : cobra .MaximumNArgs (1 ),
85+ RunE : func (cmd * cobra.Command , args []string ) error {
86+ if len (args ) == 0 {
87+ return cmd .Help ()
88+ }
89+ app , err := parser .Load (args [0 ])
90+ if err != nil {
91+ return err
92+ }
93+ return stopHandler (cmd .Context (), app )
8294 },
8395 },
8496 & cobra.Command {
85- Use : "start" ,
97+ Use : "start app_path " ,
8698 Short : "Start the Python app" ,
87- Run : func (cmd * cobra.Command , args []string ) {
88- if parsedApp .MainPythonFile != nil {
89- provisionHandler (cmd .Context (), docker , parsedApp )
99+ Args : cobra .MaximumNArgs (1 ),
100+ RunE : func (cmd * cobra.Command , args []string ) error {
101+ if len (args ) == 0 {
102+ return cmd .Help ()
90103 }
91-
92- startHandler (cmd .Context (), parsedApp )
104+ app , err := parser .Load (args [0 ])
105+ if err != nil {
106+ return err
107+ }
108+ return startHandler (cmd .Context (), docker , app )
93109 },
94110 },
95111 & cobra.Command {
96- Use : "logs" ,
112+ Use : "logs app_path " ,
97113 Short : "Show the logs of the Python app" ,
98- Run : func (cmd * cobra.Command , args []string ) {
99- logsHandler (cmd .Context (), parsedApp )
114+ Args : cobra .MaximumNArgs (1 ),
115+ RunE : func (cmd * cobra.Command , args []string ) error {
116+ if len (args ) == 0 {
117+ return cmd .Help ()
118+ }
119+ app , err := parser .Load (args [0 ])
120+ if err != nil {
121+ return err
122+ }
123+ return logsHandler (cmd .Context (), app )
100124 },
101125 },
102126 & cobra.Command {
103127 Use : "list" ,
104128 Short : "List all running Python apps" ,
105- Run : func (cmd * cobra.Command , args []string ) {
106- listHandler (cmd .Context ())
129+ RunE : func (cmd * cobra.Command , args []string ) error {
130+ return listHandler (cmd .Context ())
107131 },
108132 },
109133 & cobra.Command {
110- Use : "provision" ,
134+ Use : "provision app_path " ,
111135 Short : "Makes sure the Python app deps are downloaded and running" ,
112- Run : func (cmd * cobra.Command , args []string ) {
113- provisionHandler (cmd .Context (), docker , parsedApp )
114- },
115- },
116- & cobra.Command {
117- Use : "daemon" ,
118- Short : "Run an HTTP server to expose orchestrator functionality thorough REST API" ,
119- Run : func (cmd * cobra.Command , args []string ) {
120- httpHandler (cmd .Context (), docker )
136+ Args : cobra .MaximumNArgs (1 ),
137+ RunE : func (cmd * cobra.Command , args []string ) error {
138+ if len (args ) == 0 {
139+ return cmd .Help ()
140+ }
141+ app , err := parser .Load (args [0 ])
142+ if err != nil {
143+ return err
144+ }
145+ return provisionHandler (cmd .Context (), docker , app )
121146 },
122147 },
148+ completionCommand ,
149+ daemonCmd ,
123150 )
124151
125152 ctx := context .Background ()
126- ctx , cancel := cleanup .InterruptableContext (ctx )
127- defer cancel ()
153+ ctx , _ = cleanup .InterruptableContext (ctx )
128154 if err := rootCmd .ExecuteContext (ctx ); err != nil {
129- log . Panic (err )
155+ slog . Error (err . Error () )
130156 }
131157}
132158
133- func provisionHandler (ctx context.Context , docker * dockerClient.Client , app parser.App ) {
134- orchestrator .ProvisionApp (ctx , pythonImage , docker , app )
159+ func provisionHandler (ctx context.Context , docker * dockerClient.Client , app parser.App ) error {
160+ if err := orchestrator .ProvisionApp (ctx , docker , app ); err != nil {
161+ return err
162+ }
163+ return nil
135164}
136165
137- func startHandler (ctx context.Context , app parser.App ) {
138- orchestrator .StartApp (ctx , app )
166+ func startHandler (ctx context.Context , docker * dockerClient.Client , app parser.App ) error {
167+ for message := range orchestrator .StartApp (ctx , docker , app ) {
168+ switch message .GetType () {
169+ case orchestrator .ProgressType :
170+ slog .Info ("progress" , slog .Float64 ("progress" , float64 (message .GetProgress ().Progress )))
171+ case orchestrator .InfoType :
172+ slog .Info ("log" , slog .String ("message" , message .GetData ()))
173+ case orchestrator .ErrorType :
174+ return errors .New (message .GetError ().Error ())
175+ }
176+ }
177+ return nil
139178}
140179
141- func stopHandler (ctx context.Context , app parser.App ) {
142- orchestrator .StopApp (ctx , app )
180+ func stopHandler (ctx context.Context , app parser.App ) error {
181+ for message := range orchestrator .StopApp (ctx , app ) {
182+ switch message .GetType () {
183+ case orchestrator .ProgressType :
184+ slog .Info ("progress" , slog .Float64 ("progress" , float64 (message .GetProgress ().Progress )))
185+ case orchestrator .InfoType :
186+ slog .Info ("log" , slog .String ("message" , message .GetData ()))
187+ case orchestrator .ErrorType :
188+ return errors .New (message .GetError ().Error ())
189+ }
190+ }
191+ return nil
143192}
144193
145- func logsHandler (ctx context.Context , app parser.App ) {
146- orchestrator .AppLogs (ctx , app )
194+ func logsHandler (ctx context.Context , app parser.App ) error {
195+ logsIter , err := orchestrator .AppLogs (ctx , app , orchestrator.AppLogsRequest {ShowAppLogs : true , Follow : true })
196+ if err != nil {
197+ return err
198+ }
199+ for msg := range logsIter {
200+ fmt .Printf ("[%s] %s\n " , msg .Name , msg .Content )
201+ }
202+ return nil
147203}
148204
149- func listHandler (ctx context.Context ) {
205+ func listHandler (ctx context.Context ) error {
150206 res , err := orchestrator .ListApps (ctx )
151207 if err != nil {
152- slog .Error (err .Error ())
153- return
208+ return nil
154209 }
155- fmt .Println (string (res .Stdout ))
156- if len (res .Stderr ) > 0 {
157- fmt .Println (string (res .Stderr ))
210+
211+ resJSON , err := json .Marshal (res )
212+ if err != nil {
213+ return nil
158214 }
215+ fmt .Println (string (resJSON ))
216+ return nil
159217}
160218
161- func httpHandler (ctx context.Context , dockerClient * dockerClient.Client ) {
219+ func httpHandler (ctx context.Context , dockerClient * dockerClient.Client , daemonPort string ) {
220+ slog .Info ("Starting HTTP server" , slog .String ("address" , ":" + daemonPort ))
162221 apiSrv := api .NewHTTPRouter (dockerClient )
222+
163223 httpSrv := http.Server {
164- Addr : ":8080" ,
165- Handler : apiSrv ,
224+ Addr : ":" + daemonPort ,
225+ Handler : httprecover . RecoverPanic ( apiSrv ) ,
166226 ReadHeaderTimeout : 60 * time .Second ,
167227 }
168228 go func () {
@@ -172,8 +232,10 @@ func httpHandler(ctx context.Context, dockerClient *dockerClient.Client) {
172232 }()
173233
174234 <- ctx .Done ()
235+ slog .Info ("Shutting down HTTP server" , slog .String ("address" , ":" + daemonPort ))
175236
176237 ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
177238 _ = httpSrv .Shutdown (ctx )
178239 cancel ()
240+ slog .Info ("HTTP server shut down" , slog .String ("address" , ":" + daemonPort ))
179241}
0 commit comments