package main import ( "context" "crypto/subtle" "encoding/json" "errors" "net/http" "strings" "git.cin.su/m/Assistant/adapters/postgres" commitcontract "git.cin.su/m/Assistant/contracts/commit" commitdomain "git.cin.su/m/Assistant/modules/commit/domain" "git.cin.su/m/Assistant/modules/commit/ports" ) type executionRepository interface { LoadAuthorized(context.Context, commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error) LoadLatestAttempt(context.Context, commitcontract.AuthorizedActionID) (commitcontract.ActionAttempt, error) BeginAttempt(context.Context, commitcontract.ActionAttempt) error FinishAttempt(context.Context, commitcontract.ActionAttempt) error } type attemptRequest struct { AttemptID commitcontract.ActionAttemptID } func registerExecutionRoutes(mux *http.ServeMux, repository executionRepository, executor ports.Executor, reconciler ports.Reconciler, token string) { mux.HandleFunc("POST /v1/internal/authorized-actions/{id}/execute", requireCommitToken(token, func(w http.ResponseWriter, r *http.Request) { action, err := repository.LoadAuthorized(r.Context(), commitcontract.AuthorizedActionID(r.PathValue("id"))) if errors.Is(err, postgres.ErrAuthorizedActionNotFound) { http.Error(w, "authorized action not found", http.StatusNotFound) return } if err != nil { http.Error(w, "authorized action unavailable", http.StatusInternalServerError) return } if _, err = repository.LoadLatestAttempt(r.Context(), action.ID); err == nil { http.Error(w, "action already has an execution attempt", http.StatusConflict) return } else if !errors.Is(err, postgres.ErrActionAttemptNotFound) { http.Error(w, "action attempt unavailable", http.StatusInternalServerError) return } request, ok := decodeAttemptRequest(w, r) if !ok { return } begun := commitcontract.ActionAttempt{ID: request.AttemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting} if err = repository.BeginAttempt(r.Context(), begun); err != nil { http.Error(w, "begin execution attempt", http.StatusConflict) return } attempt, executeErr := commitdomain.Execute(action, request.AttemptID, executor) if err = repository.FinishAttempt(r.Context(), attempt); err != nil { http.Error(w, "finish execution attempt", http.StatusInternalServerError) return } writeAttempt(w, attempt, executeErr) })) mux.HandleFunc("POST /v1/internal/authorized-actions/{id}/reconcile", requireCommitToken(token, func(w http.ResponseWriter, r *http.Request) { action, err := repository.LoadAuthorized(r.Context(), commitcontract.AuthorizedActionID(r.PathValue("id"))) if errors.Is(err, postgres.ErrAuthorizedActionNotFound) { http.Error(w, "authorized action not found", http.StatusNotFound) return } if err != nil { http.Error(w, "authorized action unavailable", http.StatusInternalServerError) return } previous, err := repository.LoadLatestAttempt(r.Context(), action.ID) if err != nil { http.Error(w, "unknown execution attempt not found", http.StatusConflict) return } if previous.State == commitcontract.ActionExecuting { previous.State = commitcontract.ActionUnknownOutcome if err = repository.FinishAttempt(r.Context(), previous); err != nil { http.Error(w, "recover interrupted attempt", http.StatusInternalServerError) return } } if previous.State != commitcontract.ActionUnknownOutcome { http.Error(w, "action does not require reconciliation", http.StatusConflict) return } request, ok := decodeAttemptRequest(w, r) if !ok { return } begun := commitcontract.ActionAttempt{ID: request.AttemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting} if err = repository.BeginAttempt(r.Context(), begun); err != nil { http.Error(w, "begin reconciliation attempt", http.StatusConflict) return } attempt, reconcileErr := commitdomain.Reconcile(action, previous, request.AttemptID, reconciler) if err = repository.FinishAttempt(r.Context(), attempt); err != nil { http.Error(w, "finish reconciliation attempt", http.StatusInternalServerError) return } writeAttempt(w, attempt, reconcileErr) })) } func decodeAttemptRequest(w http.ResponseWriter, r *http.Request) (attemptRequest, bool) { var request attemptRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10)).Decode(&request); err != nil || request.AttemptID == "" { http.Error(w, "invalid attempt request", http.StatusBadRequest) return attemptRequest{}, false } return request, true } func writeAttempt(w http.ResponseWriter, attempt commitcontract.ActionAttempt, operationErr error) { w.Header().Set("Content-Type", "application/json") if errors.Is(operationErr, commitdomain.ErrUnknownOutcome) { w.WriteHeader(http.StatusAccepted) } _ = json.NewEncoder(w).Encode(attempt) } func requireCommitToken(expected string, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { provided := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if expected == "" || len(provided) != len(expected) || subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) != 1 { http.Error(w, "commit executor denied", http.StatusUnauthorized) return } next(w, r) } }