diff --git a/.gitignore b/.gitignore index 90d4015..2ee6d97 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ /swagger-ui /.husky/_ +.idea diff --git a/server/api.go b/server/api.go index 9a6776e..60770e2 100644 --- a/server/api.go +++ b/server/api.go @@ -9,6 +9,8 @@ import ( "os" "sort" + "github.com/launchrctl/launchr/pkg/log" + "github.com/launchrctl/launchr" "github.com/launchrctl/launchr/pkg/action" "gopkg.in/yaml.v3" @@ -35,27 +37,27 @@ func (l *launchrServer) GetOneRunningActionByID(w http.ResponseWriter, _ *http.R }) } -func (l *launchrServer) GetRunningActionStreams(w http.ResponseWriter, _ *http.Request, id ActionId, runID ActionRunInfoId, _ GetRunningActionStreamsParams) { - _, ok := l.actionMngr.RunInfoByID(runID) +func (l *launchrServer) GetRunningActionStreams(w http.ResponseWriter, _ *http.Request, id ActionId, runID ActionRunInfoId, params GetRunningActionStreamsParams) { + ri, ok := l.actionMngr.RunInfoByID(runID) if !ok { sendError(w, http.StatusNotFound, fmt.Sprintf("action run info with id %q is not found", id)) return } - outputFile, err := os.ReadFile(fmt.Sprintf("%s-out.txt", id)) + streams := ri.Action.GetInput().IO + fStreams, ok := streams.(fileStreams) + if !ok { + panic("not supported") + } + sd, err := fStreams.GetStreamData(params) if err != nil { - if os.IsNotExist(err) { - sendError(w, http.StatusNotFound, fmt.Sprintf("Output file associated with actionId %q not found", id)) - } - sendError(w, http.StatusInternalServerError, "Error accessing file") + log.Debug(err.Error()) + sendError(w, http.StatusInternalServerError, "Error reading streams") } - // @todo: care about error file aswell. + // @todo: care about error file as well w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(ActionRunStreamData{ - Type: "stdOut", - Content: string(outputFile), - }) + _ = json.NewEncoder(w).Encode(sd[0]) } func (l *launchrServer) basePath() string { @@ -88,8 +90,14 @@ func (l *launchrServer) GetActionByID(w http.ResponseWriter, _ *http.Request, id return } + afull, err := apiActionFull(l.basePath(), a) + if err != nil { + sendError(w, http.StatusInternalServerError, fmt.Sprintf("error on building actionFull %q", id)) + return + } + w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(apiActionFull(l.basePath(), a)) + _ = json.NewEncoder(w).Encode(afull) } func (l *launchrServer) GetActionJSONSchema(w http.ResponseWriter, _ *http.Request, id string) { @@ -102,7 +110,13 @@ func (l *launchrServer) GetActionJSONSchema(w http.ResponseWriter, _ *http.Reque sendError(w, http.StatusInternalServerError, fmt.Sprintf("error on loading action %q", id)) return } - afull := apiActionFull(l.basePath(), a) + + afull, err := apiActionFull(l.basePath(), a) + if err != nil { + sendError(w, http.StatusInternalServerError, fmt.Sprintf("error on building actionFull %q", id)) + return + } + w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(afull.JSONSchema) } @@ -110,14 +124,13 @@ func (l *launchrServer) GetActionJSONSchema(w http.ResponseWriter, _ *http.Reque func (l *launchrServer) GetRunningActionsByID(w http.ResponseWriter, _ *http.Request, id string) { runningActions := l.actionMngr.RunInfoByAction(id) - sortFunc := func(i, j int) bool { - if runningActions[i].Status != runningActions[j].Status { - return runningActions[i].Status < runningActions[j].Status + sort.Slice(runningActions, func(i, j int) bool { + if runningActions[i].Status == runningActions[j].Status { + return runningActions[i].ID < runningActions[j].ID } - return runningActions[i].ID < runningActions[j].ID - } - sort.Slice(runningActions, sortFunc) + return runningActions[i].Status < runningActions[j].Status + }) var result = make([]ActionRunInfo, 0, len(runningActions)) for _, ri := range runningActions { @@ -147,9 +160,10 @@ func (l *launchrServer) RunAction(w http.ResponseWriter, r *http.Request, id str // Prepare action for run. // Can we fetch directly json? - streams, err := fileStreams(id) + streams, err := createFileStreams(id) if err != nil { - sendError(w, http.StatusBadRequest, "Error creation files") + log.Debug(err.Error()) + sendError(w, http.StatusInternalServerError, "Error preparing streams") } defer func() { @@ -182,28 +196,26 @@ func (l *launchrServer) RunAction(w http.ResponseWriter, r *http.Request, id str }) } -func apiActionFull(baseURL string, a *action.Action) ActionFull { +func apiActionFull(baseURL string, a *action.Action) (ActionFull, error) { jsonschema := a.JSONSchema() jsonschema.ID = fmt.Sprintf("%s/actions/%s/schema.json", baseURL, url.QueryEscape(a.ID)) def := a.ActionDef() - var resultMap map[string]interface{} + var uiSchema map[string]interface{} yamlData, err := os.ReadFile(fmt.Sprintf("%s/ui-schema.yaml", a.Dir())) if err != nil { - if os.IsNotExist(err) { - fmt.Println("Info: ui-schema.yaml not found, using empty UISchema") - resultMap = map[string]interface{}{} - } else { - panic(err) + if !os.IsNotExist(err) { + return ActionFull{}, err } + + fmt.Println("Info: ui-schema.yaml not found, using empty UISchema") + uiSchema = map[string]interface{}{} } else { - var data interface{} - err = yaml.Unmarshal(yamlData, &data) + err = yaml.Unmarshal(yamlData, &uiSchema) if err != nil { - panic(err) + return ActionFull{}, err } - resultMap, _ = data.(map[string]interface{}) } return ActionFull{ @@ -211,8 +223,8 @@ func apiActionFull(baseURL string, a *action.Action) ActionFull { Title: def.Title, Description: def.Description, JSONSchema: jsonschema, - UISchema: resultMap, - } + UISchema: uiSchema, + }, nil } func apiActionShort(a *action.Action) (ActionShort, error) { diff --git a/server/server.go b/server/server.go index ac9aaec..d2a872b 100644 --- a/server/server.go +++ b/server/server.go @@ -74,27 +74,7 @@ func Run(ctx context.Context, app launchr.App, opts *RunOptions) error { } // Serve frontend files. - if opts.ProxyClient != "" { - target, _ := url.Parse(opts.ProxyClient) - proxy := httputil.NewSingleHostReverseProxy(target) - - // @todo: Add same SPA serving for proxy aswell. - r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { - proxy.ServeHTTP(w, r) - }) - } else { - r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { - f, err := opts.ClientFS.Open(strings.TrimPrefix(path.Clean(r.URL.Path), "/")) - if err == nil { - defer f.Close() - } - if os.IsNotExist(err) { - r.URL.Path = "/" - } - http.FileServer(http.FS(opts.ClientFS)).ServeHTTP(w, r) - }) - - } + r.HandleFunc("/*", spaHandler(opts)) // Use the validation middleware to check all requests against the OpenAPI schema on Api subroutes. r.Route(opts.APIPrefix, func(r chi.Router) { @@ -123,6 +103,28 @@ func Run(ctx context.Context, app launchr.App, opts *RunOptions) error { return s.ListenAndServe() } +func spaHandler(opts *RunOptions) http.HandlerFunc { + if opts.ProxyClient != "" { + target, _ := url.Parse(opts.ProxyClient) + proxy := httputil.NewSingleHostReverseProxy(target) + + return func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + } + } + + return func(w http.ResponseWriter, r *http.Request) { + f, err := opts.ClientFS.Open(strings.TrimPrefix(path.Clean(r.URL.Path), "/")) + if err == nil { + defer f.Close() + } + if os.IsNotExist(err) { + r.URL.Path = "/" + } + http.FileServer(http.FS(opts.ClientFS)).ServeHTTP(w, r) + } +} + func serveSwaggerUI(swagger *openapi3.T, r chi.Router, opts *RunOptions) { pathUI := opts.APIPrefix + swaggerUIPath r.Route(pathUI, func(r chi.Router) { diff --git a/server/streams.go b/server/streams.go index 41e50cb..09b3243 100644 --- a/server/streams.go +++ b/server/streams.go @@ -1,6 +1,7 @@ package server import ( + "bufio" "fmt" "io" "os" @@ -9,6 +10,10 @@ import ( "github.com/launchrctl/launchr/pkg/cli" ) +type fileStreams interface { + GetStreamData(GetRunningActionStreamsParams) ([]*ActionRunStreamData, error) +} + // webCli implements Streams interface. // @todo Maybe refactor original streams. type webCli struct { @@ -41,6 +46,43 @@ func (cli *webCli) Close() (err error) { return nil } +// GetStreamData implements fileStreams. +func (cli *webCli) GetStreamData(_ GetRunningActionStreamsParams) ([]*ActionRunStreamData, error) { + // @todo include GetRunningActionStreamsParams + _, err := cli.files[0].Seek(0, 0) + if err != nil { + return nil, err + } + reader := bufio.NewReader(cli.files[0]) + outData, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + outSd := &ActionRunStreamData{ + Type: StdOut, + Content: string(outData), + } + + _, err = cli.files[0].Seek(0, 0) + if err != nil { + return nil, err + } + reader = bufio.NewReader(cli.files[1]) + errData, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + errSd := &ActionRunStreamData{ + Type: StdErr, + Content: string(errData), + } + + result := []*ActionRunStreamData{outSd, errSd} + return result, nil +} + type wrappedWriter struct { p ActionRunStreamDataType w io.Writer @@ -50,7 +92,7 @@ func (w *wrappedWriter) Write(p []byte) (int, error) { return w.w.Write(p) } -func fileStreams(actionId ActionId) (*webCli, error) { +func createFileStreams(actionId ActionId) (*webCli, error) { outfile, err := os.Create(fmt.Sprintf("%s-out.txt", actionId)) if err != nil { return nil, fmt.Errorf("error creating output file: %w", err)