diff --git a/api/handlers.go b/api/handlers.go index 727a210..80870bb 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -3,6 +3,8 @@ package api import ( "encoding/json" "fmt" + "github.com/rs/zerolog" + "io" "net/http" "path" "strings" @@ -16,74 +18,74 @@ type Response struct { Output string `json:"output"` } -// ParseDockerCommand parses a Docker command and returns the equivalent Docker Compose YAML. -func (server *Server) ParseDockerCommand(w http.ResponseWriter, r *http.Request) { - type DockerCommand struct { - Command string `json:"command"` +// ParseDockerCommands ParseDockerCommand parses a Docker command and returns the equivalent Docker Compose YAML. +func (server *Server) ParseDockerCommands(w http.ResponseWriter, r *http.Request) { + type DockerCommands struct { + Commands []string `json:"commands"` } + var dockerCmds DockerCommands - var dockerCmd DockerCommand - - logger := server.logger.With().Str("handler", "ParseDockerCommand").Str("remoteAddr", r.RemoteAddr).Logger() + logger := server.logger.With().Str("handler", "ParseDockerCommands").Str("remoteAddr", r.RemoteAddr).Logger() logger.Info().Msgf("%s %s %s", r.Method, r.URL.Path, r.Proto) - start := time.Now() - code := http.StatusOK - errorMsg := "" - defer func() { - log := logger.Info() - if errorMsg != "" { - log = logger.Error() - http.Error(w, errorMsg, code) + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + logger.Err(err).Msg("Error closing request body") } - log.Msgf("Returned %d in %v", code, time.Since(start)) - }() - err := json.NewDecoder(r.Body).Decode(&dockerCmd) + }(r.Body) + err := json.NewDecoder(r.Body).Decode(&dockerCmds) if err != nil { - errorMsg = fmt.Sprintf("Error decoding request body: %v", err) - code = http.StatusBadRequest + writeError(w, logger, "Error decoding request body", err, http.StatusBadRequest) return } - // Validate the command. - if dockerCmd.Command == "" { - errorMsg = "Docker command cannot be empty" - code = http.StatusBadRequest - return - } + start := time.Now() + defer logDuration(logger, start) - // Create a new Parser - p, err := parser.New(dockerCmd.Command) - if err != nil { - errorMsg = fmt.Sprintf("Error creating parser: %v", err) - code = http.StatusBadRequest - return - } + var p *parser.Parser + for _, cmd := range dockerCmds.Commands { + if cmd == "" { + writeError(w, logger, "Docker command cannot be empty", nil, http.StatusBadRequest) + return + } - // Parse the Docker command - err = p.Parse() - if err != nil { - errorMsg = fmt.Sprintf("Error parsing Docker command: %v", err) - code = http.StatusBadRequest - return - } + if p == nil { + p, err = parser.New(cmd) + } else { + p, err = parser.AppendToYAML([]byte(p.String()), cmd) + } - dockerComposeYaml := p.String() + if err != nil { + writeError(w, logger, "Error parsing Docker command", err, http.StatusBadRequest) + return + } - // Create the response - resp := Response{ - Output: dockerComposeYaml, + if err := p.Parse(); err != nil { + writeError(w, logger, "Error parsing Docker command", err, http.StatusBadRequest) + return + } } + resp := Response{ + Output: p.String(), + } w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(resp) - if err != nil { + if err := json.NewEncoder(w).Encode(resp); err != nil { logger.Err(err).Msg("Unable to write response") - return } } +func writeError(w http.ResponseWriter, logger zerolog.Logger, msg string, err error, code int) { + logger.Err(err).Msg(msg) + http.Error(w, fmt.Sprintf("%s: %v", msg, err), code) +} + +func logDuration(logger zerolog.Logger, start time.Time) { + logger.Info().Msgf("Returned in %v", time.Since(start)) +} + // appHandler is web app http handler function. func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) { staticServer := http.FileServer(http.FS(server.assets)) diff --git a/api/server.go b/api/server.go index cbab691..0f64640 100644 --- a/api/server.go +++ b/api/server.go @@ -29,7 +29,7 @@ func NewServer(logger *zerolog.Logger, listener net.Listener, assets fs.FS) *Ser } r := mux.NewRouter() - r.HandleFunc("/api/parse", server.ParseDockerCommand).Methods("POST") + r.HandleFunc("/api/parse", server.ParseDockerCommands).Methods("POST") r.PathPrefix("/").HandlerFunc(server.appHandler) server.http = http.Server{ diff --git a/api/server_test.go b/api/server_test.go new file mode 100644 index 0000000..0b56ec7 --- /dev/null +++ b/api/server_test.go @@ -0,0 +1,51 @@ +package api + +import ( + "bytes" + "context" + "fmt" + "net" + "testing" + "time" + + "github.com/rs/zerolog" +) + +func TestServer(t *testing.T) { + var logBuffer bytes.Buffer + logger := zerolog.New(&logBuffer).With().Timestamp().Logger() + listener, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("Failed to create listener: %v", err) + } + defer func() { + if err := listener.Close(); err != nil { + logger.Error().Err(err).Msg("Failed to close listener") + } + }() + + addr := listener.Addr().(*net.TCPAddr) + port := addr.Port + endpoint := fmt.Sprintf("http://localhost:%d/api/parse", port) + logger.Info().Msgf("Endpoint: %s", endpoint) + + server := NewServer(&logger, listener, nil) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + + go func() { + if err := server.Run(ctx); err != nil && err != context.Canceled { + logger.Error().Err(err).Msg("Server error") + } + }() + + // Wait for the server to start + time.Sleep(1 * time.Second) + + fmt.Printf("Server is running on %s. You have 2 minutes to test manually.\n", endpoint) + + // Wait for 2 minutes to allow manual testing + time.Sleep(2 * time.Minute) + +}