github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/client/jobs.go (about) 1 package client 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/url" 7 "time" 8 9 "github.com/cozy/cozy-stack/client/request" 10 ) 11 12 type jobOptions struct { 13 MaxExecCount int `json:"max_exec_count,omitempty"` 14 Timeout *time.Duration `json:"timeout,omitempty"` 15 } 16 17 // JobOptions is the options to run a job. 18 type JobOptions struct { 19 Worker string 20 Arguments interface{} 21 MaxExecCount int 22 Timeout *time.Duration 23 Logs chan *JobLog 24 } 25 26 // JobLog is a log being outputted by the running job. 27 type JobLog struct { 28 Time time.Time `json:"time"` 29 Message string `json:"message"` 30 Level string `json:"level"` 31 Data map[string]interface{} `json:"data"` 32 } 33 34 // Job is a struct representing a job 35 type Job struct { 36 ID string `json:"id"` 37 Rev string `json:"rev"` 38 Attrs struct { 39 Domain string `json:"domain"` 40 TriggerID string `json:"trigger_id"` 41 Message json.RawMessage `json:"message"` 42 Debounced bool `json:"debounced"` 43 Event struct { 44 Domain string `json:"domain"` 45 Verb string `json:"verb"` 46 Doc json.RawMessage `json:"doc"` 47 OldDoc json.RawMessage `json:"old,omitempty"` 48 } `json:"event"` 49 Options *jobOptions `json:"options"` 50 QueuedAt time.Time `json:"queued_at"` 51 StartedAt time.Time `json:"started_at"` 52 State string `json:"state"` 53 Worker string `json:"worker"` 54 } `json:"attributes"` 55 } 56 57 // Trigger is a struct representing a trigger 58 type Trigger struct { 59 ID string `json:"id"` 60 Rev string `json:"rev"` 61 Attrs struct { 62 Domain string `json:"domain"` 63 Type string `json:"type"` 64 WorkerType string `json:"worker"` 65 Arguments string `json:"arguments"` 66 Debounce string `json:"debounce"` 67 Message json.RawMessage `json:"message"` 68 Options *struct { 69 MaxExecCount int `json:"max_exec_count"` 70 Timeout time.Duration `json:"timeout"` 71 } `json:"options"` 72 } `json:"attributes"` 73 } 74 75 // JobPush is used to push a new job into the job queue. 76 func (c *Client) JobPush(r *JobOptions) (*Job, error) { 77 args, err := json.Marshal(r.Arguments) 78 if err != nil { 79 return nil, err 80 } 81 82 type jobAttrs struct { 83 Arguments json.RawMessage `json:"arguments"` 84 ForwardLogs bool `json:"forward_logs"` 85 Options *jobOptions `json:"options"` 86 } 87 88 opt := &jobOptions{} 89 if r.MaxExecCount > 0 { 90 opt.MaxExecCount = r.MaxExecCount 91 } 92 if r.Timeout != nil { 93 opt.Timeout = r.Timeout 94 } 95 96 withLogs := r.Logs != nil 97 var channel *RealtimeChannel 98 if withLogs { 99 channel, err = c.RealtimeClient(RealtimeOptions{ 100 DocTypes: []string{"io.cozy.jobs", "io.cozy.jobs.logs"}, 101 }) 102 if err != nil { 103 return nil, err 104 } 105 } 106 107 job := struct { 108 Attrs jobAttrs `json:"attributes"` 109 }{ 110 Attrs: jobAttrs{ 111 Arguments: args, 112 ForwardLogs: withLogs, 113 Options: opt, 114 }, 115 } 116 body, err := writeJSONAPI(job) 117 if err != nil { 118 return nil, err 119 } 120 res, err := c.Req(&request.Options{ 121 Method: "POST", 122 Path: "/jobs/queue/" + url.PathEscape(r.Worker), 123 Body: body, 124 }) 125 if err != nil { 126 return nil, err 127 } 128 129 var j *Job 130 if err = readJSONAPI(res.Body, &j); err != nil { 131 return nil, err 132 } 133 134 defer func() { 135 if withLogs { 136 close(r.Logs) 137 } 138 }() 139 140 if withLogs { 141 for evt := range channel.Channel() { 142 if evt.Event == "error" { 143 return nil, fmt.Errorf("realtime: %s", evt.Payload.Title) 144 } 145 switch evt.Payload.Type { 146 case "io.cozy.jobs": 147 var doc struct { 148 ID string `json:"_id"` 149 } 150 if err = json.Unmarshal(evt.Payload.Doc, &doc); err != nil { 151 return nil, err 152 } 153 if doc.ID != j.ID { 154 continue 155 } 156 if err = json.Unmarshal(evt.Payload.Doc, &j.Attrs); err != nil { 157 return nil, err 158 } 159 if j.Attrs.State == "done" || j.Attrs.State == "errored" { 160 return j, nil 161 } 162 case "io.cozy.jobs.logs": 163 var log JobLog 164 if err = json.Unmarshal(evt.Payload.Doc, &log); err != nil { 165 return nil, err 166 } 167 r.Logs <- &log 168 } 169 } 170 } 171 172 return j, nil 173 } 174 175 // GetTrigger return the trigger with the specified ID. 176 func (c *Client) GetTrigger(triggerID string) (*Trigger, error) { 177 res, err := c.Req(&request.Options{ 178 Method: "GET", 179 Path: fmt.Sprintf("/jobs/triggers/%s", url.PathEscape(triggerID)), 180 }) 181 if err != nil { 182 return nil, err 183 } 184 var t *Trigger 185 if err := readJSONAPI(res.Body, &t); err != nil { 186 return nil, err 187 } 188 return t, nil 189 } 190 191 // GetTriggers returns the list of all triggers with the specified worker type. 192 func (c *Client) GetTriggers(worker string) ([]*Trigger, error) { 193 res, err := c.Req(&request.Options{ 194 Method: "GET", 195 Path: "/jobs/triggers", 196 Queries: url.Values{"Worker": {worker}}, 197 }) 198 if err != nil { 199 return nil, err 200 } 201 var t []*Trigger 202 if err := readJSONAPI(res.Body, &t); err != nil { 203 return nil, err 204 } 205 return t, nil 206 } 207 208 // TriggerLaunch launches manually the trigger with the specified ID. 209 func (c *Client) TriggerLaunch(triggerID string) (*Job, error) { 210 res, err := c.Req(&request.Options{ 211 Method: "POST", 212 Path: fmt.Sprintf("/jobs/triggers/%s/launch", url.PathEscape(triggerID)), 213 }) 214 if err != nil { 215 return nil, err 216 } 217 var j *Job 218 if err := readJSONAPI(res.Body, &j); err != nil { 219 return nil, err 220 } 221 return j, nil 222 } 223 224 // ListTriggers returns the list of all triggers for an instance. 225 func (c *Client) ListTriggers() ([]*Trigger, error) { 226 res, err := c.Req(&request.Options{ 227 Method: "GET", 228 Path: "/jobs/triggers", 229 }) 230 if err != nil { 231 return nil, err 232 } 233 var list []*Trigger 234 if err := readJSONAPI(res.Body, &list); err != nil { 235 return nil, err 236 } 237 return list, nil 238 }