- Use Google Distroless images for all services (Go & Node.js). - Standardize documentation with [PROJECT-NAME].md. - Add .dockerignore and .gitignore to all projects. - Remove docker-compose.yml in favor of docker run instructions. - Fix Go version and dependency issues in observability, repo-integrations, and security-governance. - Add Podman support (fully qualified image names). - Update Dashboard to use Node.js static server for Distroless compatibility.
136 lines
3.9 KiB
Go
136 lines
3.9 KiB
Go
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,
|
|
})
|
|
}
|