oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendar
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "io"
8 "net"
9 "net/http"
10 "os"
11
12 "amadan.net/rastrillo/rastrillo/carlos"
13)
14
15const scheduleUnsupportedMessage = "calendar: this platform does not support safe scheduled work; update the platform before starting Calendar"
16
17type unsupportedScheduling struct{}
18
19func (unsupportedScheduling) Error() string { return scheduleUnsupportedMessage }
20
21func scheduleControl(ctx context.Context, path string, value any) error {
22 socket := os.Getenv("CARLOS_CONTROL_SOCKET")
23 if socket == "" {
24 return carlos.ErrNotOnCarlos
25 }
26 token := os.Getenv("CARLOS_ADMIN_TOKEN")
27 if token == "" {
28 return carlos.ErrUnauthorized
29 }
30 transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
31 return (&net.Dialer{}).DialContext(ctx, "unix", socket)
32 }}
33 defer transport.CloseIdleConnections()
34 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://carlos"+path, nil)
35 if err != nil {
36 return err
37 }
38 req.Header.Set("Authorization", "Bearer "+token)
39 response, err := (&http.Client{Transport: transport}).Do(req)
40 if err != nil {
41 return err
42 }
43 defer response.Body.Close()
44 if response.StatusCode == http.StatusUnauthorized {
45 return carlos.ErrUnauthorized
46 }
47 if path == "/schedule-capabilities" && response.StatusCode == http.StatusNotFound {
48 return unsupportedScheduling{}
49 }
50 if response.StatusCode != http.StatusOK {
51 return &carlos.StatusError{Status: response.StatusCode, Name: scheduledWorkName}
52 }
53 return json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(value)
54}
55
56func scheduleCapability(ctx context.Context) error {
57 var capability struct {
58 RegistrationFence bool `json:"registration_fence"`
59 }
60 if err := scheduleControl(ctx, "/schedule-capabilities", &capability); err != nil {
61 return err
62 }
63 if !capability.RegistrationFence {
64 return unsupportedScheduling{}
65 }
66 return nil
67}
68
69type remoteSchedule struct {
70 Name string `json:"name"`
71 Kind string `json:"kind"`
72 Path string `json:"path"`
73 NextAt int64 `json:"next_at"`
74 DueAt int64 `json:"due_at"`
75 Attempts int `json:"attempts"`
76}
77
78func scheduleArmed(ctx context.Context) (remoteSchedule, error) {
79 var response struct {
80 Timers []remoteSchedule `json:"timers"`
81 }
82 if err := scheduleControl(ctx, "/schedule", &response); err != nil {
83 return remoteSchedule{}, err
84 }
85 for _, timer := range response.Timers {
86 if timer.Name == scheduledWorkName {
87 return timer, nil
88 }
89 }
90 return remoteSchedule{}, nil
91}
92
93func permanentScheduleError(err error) bool {
94 var unsupported unsupportedScheduling
95 var status *carlos.StatusError
96 return errors.As(err, &unsupported) || errors.Is(err, carlos.ErrNotOnCarlos) || errors.Is(err, carlos.ErrUnauthorized) ||
97 errors.Is(err, carlos.ErrDeclaredSchedule) || (errors.As(err, &status) && status.Status >= 400 && status.Status < 500 && status.Status != 408 && status.Status != 429)
98}
99