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  }