forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package models
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
9 "strings"
10)
11
12type WorkflowLogger struct {
13 file *os.File
14 encoder *json.Encoder
15}
16
17func NewWorkflowLogger(baseDir string, wid WorkflowId) (*WorkflowLogger, error) {
18 path := LogFilePath(baseDir, wid)
19
20 file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
21 if err != nil {
22 return nil, fmt.Errorf("creating log file: %w", err)
23 }
24
25 return &WorkflowLogger{
26 file: file,
27 encoder: json.NewEncoder(file),
28 }, nil
29}
30
31func LogFilePath(baseDir string, workflowID WorkflowId) string {
32 logFilePath := filepath.Join(baseDir, fmt.Sprintf("%s.log", workflowID.String()))
33 return logFilePath
34}
35
36func (l *WorkflowLogger) Close() error {
37 return l.file.Close()
38}
39
40// WriteSetupError writes a setup error (e.g., image pull failure) to the log.
41func (l *WorkflowLogger) WriteSetupError(msg string) error {
42 step := SimpleStep{N: "Setup", K: StepKindSystem}
43
44 // Start the step
45 if err := l.encoder.Encode(NewControlLogLine(0, step, StepStatusStart)); err != nil {
46 return err
47 }
48
49 // Write the error as data
50 if err := l.encoder.Encode(NewDataLogLine(0, msg, "stderr")); err != nil {
51 return err
52 }
53
54 // End the step
55 return l.encoder.Encode(NewControlLogLine(0, step, StepStatusEnd))
56}
57
58func (l *WorkflowLogger) DataWriter(idx int, stream string) io.Writer {
59 return &dataWriter{
60 logger: l,
61 idx: idx,
62 stream: stream,
63 }
64}
65
66func (l *WorkflowLogger) ControlWriter(idx int, step Step, stepStatus StepStatus) io.Writer {
67 return &controlWriter{
68 logger: l,
69 idx: idx,
70 step: step,
71 stepStatus: stepStatus,
72 }
73}
74
75type dataWriter struct {
76 logger *WorkflowLogger
77 idx int
78 stream string
79}
80
81func (w *dataWriter) Write(p []byte) (int, error) {
82 line := strings.TrimRight(string(p), "\r\n")
83 entry := NewDataLogLine(w.idx, line, w.stream)
84 if err := w.logger.encoder.Encode(entry); err != nil {
85 return 0, err
86 }
87 return len(p), nil
88}
89
90type controlWriter struct {
91 logger *WorkflowLogger
92 idx int
93 step Step
94 stepStatus StepStatus
95}
96
97func (w *controlWriter) Write(_ []byte) (int, error) {
98 entry := NewControlLogLine(w.idx, w.step, w.stepStatus)
99 if err := w.logger.encoder.Encode(entry); err != nil {
100 return 0, err
101 }
102 return len(w.step.Name()), nil
103}
104
105// registryError represents the error format returned by Docker registries
106type registryError struct {
107 Errors []struct {
108 Code string `json:"code"`
109 Message string `json:"message"`
110 } `json:"errors"`
111}
112
113// ParseSetupError extracts a user-friendly message from setup errors.
114// It handles Docker registry errors (like nixery) that contain JSON payloads.
115// Falls back to the original error message if parsing fails.
116func ParseSetupError(err error) string {
117 if err == nil {
118 return ""
119 }
120
121 errStr := err.Error()
122
123 // Look for JSON payload after "unknown: " (Docker registry error format)
124 idx := strings.Index(errStr, "unknown: ")
125 if idx == -1 {
126 return errStr
127 }
128
129 jsonStr := errStr[idx+len("unknown: "):]
130
131 var regErr registryError
132 if jsonErr := json.Unmarshal([]byte(jsonStr), ®Err); jsonErr != nil {
133 return errStr
134 }
135
136 if len(regErr.Errors) > 0 && regErr.Errors[0].Message != "" {
137 return regErr.Errors[0].Message
138 }
139
140 return errStr
141}