package main import ( "context" "encoding/json" "log/slog" "net/http" "os" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/google/uuid" "github.com/lab/automation-jobs-core/temporal/workflows" "go.temporal.io/sdk/client" ) // The task queue name for our sample workflow. const SampleTaskQueue = "sample-task-queue" // application holds the dependencies for our API handlers. type application struct { temporalClient client.Client } // runJobRequest defines the expected JSON body for the POST /jobs/run endpoint. type runJobRequest struct { Name string `json:"name"` } // runJobResponse defines the JSON response for a successful job submission. type runJobResponse struct { WorkflowID string `json:"workflow_id"` RunID string `json:"run_id"` } // jobStatusResponse defines the JSON response for the job status endpoint. type jobStatusResponse struct { WorkflowID string `json:"workflow_id"` RunID string `json:"run_id"` Status string `json:"status"` } func main() { slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) temporalAddress := os.Getenv("TEMPORAL_ADDRESS") if temporalAddress == "" { slog.Warn("TEMPORAL_ADDRESS not set, defaulting to localhost:7233") temporalAddress = "localhost:7233" } c, err := client.Dial(client.Options{ HostPort: temporalAddress, Logger: slog.Default(), }) if err != nil { slog.Error("Unable to create Temporal client", "error", err) os.Exit(1) } defer c.Close() app := &application{ temporalClient: c, } r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) // Chi's default logger r.Use(middleware.Recoverer) r.Post("/jobs/run", app.runJobHandler) r.Get("/jobs/{workflowID}/status", app.getJobStatusHandler) slog.Info("Starting API server", "port", "8080") if err := http.ListenAndServe(":8080", r); err != nil { slog.Error("Failed to start server", "error", err) } } // runJobHandler starts a new SampleWorkflow execution. func (app *application) runJobHandler(w http.ResponseWriter, r *http.Request) { var req runJobRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" { http.Error(w, "Name field is required", http.StatusBadRequest) return } options := client.StartWorkflowOptions{ ID: "sample-workflow-" + uuid.NewString(), TaskQueue: SampleTaskQueue, } we, err := app.temporalClient.ExecuteWorkflow(context.Background(), options, workflows.SampleWorkflow, req.Name) if err != nil { slog.Error("Unable to start workflow", "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } slog.Info("Started workflow", "workflow_id", we.GetID(), "run_id", we.GetRunID()) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(runJobResponse{ WorkflowID: we.GetID(), RunID: we.GetRunID(), }) } // getJobStatusHandler retrieves the status of a specific workflow execution. func (app *application) getJobStatusHandler(w http.ResponseWriter, r *http.Request) { workflowID := chi.URLParam(r, "workflowID") // Note: RunID can be empty to get the latest run. resp, err := app.temporalClient.DescribeWorkflowExecution(context.Background(), workflowID, "") if err != nil { slog.Error("Unable to describe workflow", "error", err, "workflow_id", workflowID) http.Error(w, "Workflow not found", http.StatusNotFound) return } status := resp.GetWorkflowExecutionInfo().GetStatus().String() slog.Info("Described workflow", "workflow_id", workflowID, "status", status) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(jobStatusResponse{ WorkflowID: resp.GetWorkflowExecutionInfo().GetExecution().GetWorkflowId(), RunID: resp.GetWorkflowExecutionInfo().GetExecution().GetRunId(), Status: status, }) }