- Add runner-proto-socket-transport-hardening milestone and SDD docs - Add runnersocket package for Go service - Update agent config, runner, and job client (Dart) - Update Bootstrap scripts (PowerShell, shell) - Update Go service HTTP server handlers and routes - Add CICD state store updates - Update agent-ops domain rules and phase roadmap
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 dispatch 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 runner-facing RunRequest from the stored input,
|
|
// filling in the runner/job/execution identifiers resolved at dispatch 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)
|
|
}
|