로드맵 milestone 상태를 archive로 이동하고 작업 메타데이터 추적을 정합성 있게 반영한다. core의 상태 저장 경로 변경 반영과 핸들러 파일 분리로 DTO 처리·라우트 의존을 정리했다.
194 lines
5.4 KiB
Go
194 lines
5.4 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/toki/oto/services/core/internal/cicdstate"
|
|
"github.com/toki/oto/services/core/internal/runnerregistry"
|
|
otopb "github.com/toki/oto/services/core/oto"
|
|
)
|
|
|
|
func jobToJSON(j *cicdstate.Job) map[string]interface{} {
|
|
out := map[string]interface{}{
|
|
"id": j.ID,
|
|
"name": j.Name,
|
|
"state": j.State,
|
|
"created_at": j.CreatedAt.Format(time.RFC3339),
|
|
"updated_at": j.UpdatedAt.Format(time.RFC3339),
|
|
"execution_id": j.ExecutionID,
|
|
}
|
|
if j.RunInput != nil {
|
|
out["run_request"] = runInputToJSON(j.RunInput)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// validateRunInput enforces the remote claim contract: exactly one of
|
|
// pipeline_yaml or pipeline_yaml_path must be provided.
|
|
func validateRunInput(in *cicdstate.RunInput) error {
|
|
hasYAML := strings.TrimSpace(in.PipelineYAML) != ""
|
|
hasPath := strings.TrimSpace(in.PipelineYAMLPath) != ""
|
|
if hasYAML == hasPath {
|
|
return fmt.Errorf("run request must set exactly one of pipeline_yaml or pipeline_yaml_path")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// runRequestFromJSON converts an incoming otopb.RunRequest decoded from the job
|
|
// create body into a store-level RunInput. It returns nil when no run request
|
|
// was supplied, keeping job creation backward compatible.
|
|
func runRequestFromJSON(rr *otopb.RunRequest) *cicdstate.RunInput {
|
|
if rr == nil {
|
|
return nil
|
|
}
|
|
in := &cicdstate.RunInput{
|
|
PipelineYAMLPath: rr.GetPipelineYamlPath(),
|
|
PipelineYAML: rr.GetPipelineYaml(),
|
|
CommandTypes: append([]string(nil), rr.GetCommandTypes()...),
|
|
}
|
|
if len(rr.GetVariables()) > 0 {
|
|
in.Variables = make(map[string]string, len(rr.GetVariables()))
|
|
for k, v := range rr.GetVariables() {
|
|
in.Variables[k] = v
|
|
}
|
|
}
|
|
return in
|
|
}
|
|
|
|
// runRequestToProto builds the claim response RunRequest from the stored input,
|
|
// filling in the runner/job/execution identifiers resolved at claim time.
|
|
func runRequestToProto(in *cicdstate.RunInput, runnerID, jobID, execID string) *otopb.RunRequest {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
rr := &otopb.RunRequest{
|
|
RunnerId: runnerID,
|
|
JobId: jobID,
|
|
ExecutionId: execID,
|
|
PipelineYamlPath: in.PipelineYAMLPath,
|
|
PipelineYaml: in.PipelineYAML,
|
|
CommandTypes: append([]string(nil), in.CommandTypes...),
|
|
}
|
|
if len(in.Variables) > 0 {
|
|
rr.Variables = make(map[string]string, len(in.Variables))
|
|
for k, v := range in.Variables {
|
|
rr.Variables[k] = v
|
|
}
|
|
}
|
|
return rr
|
|
}
|
|
|
|
func runInputToJSON(in *cicdstate.RunInput) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"pipeline_yaml_path": in.PipelineYAMLPath,
|
|
"pipeline_yaml": in.PipelineYAML,
|
|
"variables": in.Variables,
|
|
"command_types": in.CommandTypes,
|
|
}
|
|
}
|
|
|
|
func execToJSON(e *cicdstate.Execution) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"id": e.ID,
|
|
"job_id": e.JobID,
|
|
"state": e.State,
|
|
"created_at": e.CreatedAt.Format(time.RFC3339),
|
|
"updated_at": e.UpdatedAt.Format(time.RFC3339),
|
|
"execution_id": e.ID,
|
|
}
|
|
}
|
|
|
|
func logsToJSON(logs []cicdstate.LogEntry) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"logs": logs,
|
|
}
|
|
}
|
|
|
|
func artifactsToJSON(artifacts []cicdstate.ArtifactEntry) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"artifacts": artifacts,
|
|
}
|
|
}
|
|
|
|
func errorToJSON(message string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"error": message,
|
|
}
|
|
}
|
|
|
|
func successToJSON(success bool) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"success": success,
|
|
}
|
|
}
|
|
|
|
func runnerDisconnectToJSON(success bool, err string) map[string]interface{} {
|
|
out := successToJSON(success)
|
|
if err != "" {
|
|
out["error"] = err
|
|
}
|
|
return out
|
|
}
|
|
|
|
func runnerRecordToJSON(record runnerregistry.RunnerRecord) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"runner_id": record.RunnerID,
|
|
"alias": record.Alias,
|
|
"protocol_version": record.ProtocolVersion,
|
|
"status": record.Status,
|
|
"accepted_at": record.AcceptedAt.Format(time.RFC3339),
|
|
"first_heartbeat_at": record.FirstHeartbeatAt.Format(time.RFC3339),
|
|
"last_heartbeat_at": record.LastHeartbeatAt.Format(time.RFC3339),
|
|
"failure_reason": record.FailureReason,
|
|
}
|
|
}
|
|
|
|
func runnerStatusToJSON(record runnerregistry.RunnerRecord, runnerID string, lastExec *cicdstate.Execution) map[string]interface{} {
|
|
currentJobID := ""
|
|
currentExecID := ""
|
|
if lastExec != nil {
|
|
currentJobID = lastExec.JobID
|
|
currentExecID = lastExec.ID
|
|
}
|
|
return map[string]interface{}{
|
|
"accepted": true,
|
|
"runner_id": runnerID,
|
|
"status": record.Status,
|
|
"current_job_id": currentJobID,
|
|
"current_execution_id": currentExecID,
|
|
"message": record.FailureReason,
|
|
}
|
|
}
|
|
|
|
func runnerStatusRejectedToJSON(message string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"accepted": false,
|
|
"message": message,
|
|
}
|
|
}
|
|
|
|
func selfUpdateToJSON(success bool, accepted bool, deferred bool, targetVersion string, message string) map[string]interface{} {
|
|
out := map[string]interface{}{
|
|
"success": success,
|
|
"accepted": accepted,
|
|
"deferred": deferred,
|
|
"message": message,
|
|
}
|
|
if !success && !deferred {
|
|
out["error_message"] = message
|
|
}
|
|
if targetVersion != "" {
|
|
out["target_version"] = targetVersion
|
|
}
|
|
return out
|
|
}
|
|
|
|
func writeResponse(w http.ResponseWriter, status int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(data)
|
|
}
|