OTO Core가 job, execution, log, artifact 상태를 서버 내부에서 소유하고 조회할 수 있어야 runner dispatch와 통합 smoke 단계로 이어질 수 있다. in-memory 상태 저장소와 HTTP API, 검증 테스트, 관련 agent-task 산출물을 함께 반영한다.
323 lines
6.2 KiB
Go
323 lines
6.2 KiB
Go
package cicdstate
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
StateQueued = "queued"
|
|
StateRunning = "running"
|
|
StateSucceeded = "succeeded"
|
|
StateFailed = "failed"
|
|
StateCanceled = "canceled"
|
|
)
|
|
|
|
var validTransitions = map[string][]string{
|
|
StateQueued: {StateRunning, StateFailed, StateCanceled},
|
|
StateRunning: {StateSucceeded, StateFailed},
|
|
StateSucceeded: nil,
|
|
StateFailed: nil,
|
|
StateCanceled: nil,
|
|
}
|
|
|
|
type Job struct {
|
|
ID string
|
|
Name string
|
|
State string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
ExecutionID string
|
|
}
|
|
|
|
func (j *Job) TransitionTo(newState string) error {
|
|
allowed, ok := validTransitions[j.State]
|
|
if !ok {
|
|
return fmt.Errorf("unknown state: %s", j.State)
|
|
}
|
|
for _, s := range allowed {
|
|
if s == newState {
|
|
j.State = newState
|
|
j.UpdatedAt = time.Now()
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid transition from %s to %s", j.State, newState)
|
|
}
|
|
|
|
type Execution struct {
|
|
ID string
|
|
JobID string
|
|
State string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
Logs []LogEntry
|
|
Artifacts []ArtifactEntry
|
|
}
|
|
|
|
func (e *Execution) TransitionTo(newState string) error {
|
|
allowed, ok := validTransitions[e.State]
|
|
if !ok {
|
|
return fmt.Errorf("unknown state: %s", e.State)
|
|
}
|
|
for _, s := range allowed {
|
|
if s == newState {
|
|
e.State = newState
|
|
e.UpdatedAt = time.Now()
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid transition from %s to %s", e.State, newState)
|
|
}
|
|
|
|
type LogEntry struct {
|
|
Timestamp time.Time
|
|
Line string
|
|
}
|
|
|
|
type ArtifactEntry struct {
|
|
Name string
|
|
Path string
|
|
}
|
|
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
jobs map[string]*Job
|
|
executions map[string]*Execution
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewStore() *Store {
|
|
return &Store{
|
|
now: time.Now,
|
|
jobs: make(map[string]*Job),
|
|
executions: make(map[string]*Execution),
|
|
}
|
|
}
|
|
|
|
func (s *Store) CreateJob(id, name string) (*Job, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if _, exists := s.jobs[id]; exists {
|
|
return nil, fmt.Errorf("job %s already exists", id)
|
|
}
|
|
|
|
now := s.now()
|
|
job := &Job{
|
|
ID: id,
|
|
Name: name,
|
|
State: StateQueued,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
s.jobs[id] = job
|
|
return job, nil
|
|
}
|
|
|
|
func (s *Store) GetJob(id string) (*Job, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
job, ok := s.jobs[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("job not found: %s", id)
|
|
}
|
|
|
|
return copyJob(job), nil
|
|
}
|
|
|
|
func (s *Store) CreateExecution(jobID, execID string) (*Execution, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
job, ok := s.jobs[jobID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("job not found: %s", jobID)
|
|
}
|
|
|
|
if job.ExecutionID != "" {
|
|
return nil, fmt.Errorf("job %s already has execution %s", jobID, job.ExecutionID)
|
|
}
|
|
|
|
now := s.now()
|
|
exec := &Execution{
|
|
ID: execID,
|
|
JobID: jobID,
|
|
State: StateQueued,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Logs: make([]LogEntry, 0),
|
|
Artifacts: make([]ArtifactEntry, 0),
|
|
}
|
|
s.executions[execID] = exec
|
|
job.ExecutionID = execID
|
|
job.UpdatedAt = now
|
|
|
|
return exec, nil
|
|
}
|
|
|
|
func (s *Store) GetExecution(id string) (*Execution, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
exec, ok := s.executions[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("execution not found: %s", id)
|
|
}
|
|
|
|
return copyExecution(exec), nil
|
|
}
|
|
|
|
func (s *Store) AppendLog(execID string, line string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
exec, ok := s.executions[execID]
|
|
if !ok {
|
|
return fmt.Errorf("execution not found: %s", execID)
|
|
}
|
|
|
|
exec.Logs = append(exec.Logs, LogEntry{
|
|
Timestamp: s.now(),
|
|
Line: line,
|
|
})
|
|
exec.UpdatedAt = s.now()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) GetLogs(execID string) ([]LogEntry, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
exec, ok := s.executions[execID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("execution not found: %s", execID)
|
|
}
|
|
|
|
result := make([]LogEntry, len(exec.Logs))
|
|
copy(result, exec.Logs)
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Store) AppendArtifact(execID string, name string, path string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
exec, ok := s.executions[execID]
|
|
if !ok {
|
|
return fmt.Errorf("execution not found: %s", execID)
|
|
}
|
|
|
|
exec.Artifacts = append(exec.Artifacts, ArtifactEntry{
|
|
Name: name,
|
|
Path: path,
|
|
})
|
|
exec.UpdatedAt = s.now()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) GetArtifacts(execID string) ([]ArtifactEntry, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
exec, ok := s.executions[execID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("execution not found: %s", execID)
|
|
}
|
|
|
|
result := make([]ArtifactEntry, len(exec.Artifacts))
|
|
copy(result, exec.Artifacts)
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Store) TransitionJob(id, newState string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
job, ok := s.jobs[id]
|
|
if !ok {
|
|
return fmt.Errorf("job not found: %s", id)
|
|
}
|
|
|
|
allowed, ok := validTransitions[job.State]
|
|
if !ok {
|
|
return fmt.Errorf("unknown state: %s", job.State)
|
|
}
|
|
for _, target := range allowed {
|
|
if target == newState {
|
|
job.State = newState
|
|
job.UpdatedAt = s.now()
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid transition from %s to %s", job.State, newState)
|
|
}
|
|
|
|
func (s *Store) TransitionExecution(id, newState string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
exec, ok := s.executions[id]
|
|
if !ok {
|
|
return fmt.Errorf("execution not found: %s", id)
|
|
}
|
|
|
|
allowed, ok := validTransitions[exec.State]
|
|
if !ok {
|
|
return fmt.Errorf("unknown state: %s", exec.State)
|
|
}
|
|
for _, target := range allowed {
|
|
if target == newState {
|
|
exec.State = newState
|
|
exec.UpdatedAt = s.now()
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid transition from %s to %s", exec.State, newState)
|
|
}
|
|
|
|
func (s *Store) SnapshotJobs() []Job {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
result := make([]Job, 0, len(s.jobs))
|
|
for _, j := range s.jobs {
|
|
result = append(result, *copyJob(j))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Store) SnapshotExecutions() []Execution {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
result := make([]Execution, 0, len(s.executions))
|
|
for _, e := range s.executions {
|
|
result = append(result, *copyExecution(e))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func copyJob(j *Job) *Job {
|
|
if j == nil {
|
|
return nil
|
|
}
|
|
cp := *j
|
|
return &cp
|
|
}
|
|
|
|
func copyExecution(e *Execution) *Execution {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
cp := *e
|
|
cp.Logs = make([]LogEntry, len(e.Logs))
|
|
copy(cp.Logs, e.Logs)
|
|
cp.Artifacts = make([]ArtifactEntry, len(e.Artifacts))
|
|
copy(cp.Artifacts, e.Artifacts)
|
|
return &cp
|
|
}
|