This repository has no description
1package db
2
3import (
4 "database/sql"
5 "encoding/json"
6 "slices"
7 "strings"
8 "time"
9)
10
11// the raw token is never stored, only its hash
12type ExecutorToken struct {
13 Name string
14 CreatedAt string
15 ExpiresAt *time.Time
16 Labels []string
17 QuarantineReason *string
18 QuarantinedAt *string
19}
20
21func (d *DB) AddExecutorToken(name, tokenHash string, expiresAt *time.Time, labels []string) error {
22 normalized := normalizeLabels(labels)
23 labelsBytes, err := json.Marshal(normalized)
24 if err != nil {
25 return err
26 }
27 _, err = d.Exec(
28 `insert into mill_executors (name, token_hash, expires_at, labels) values (?, ?, ?, ?)`,
29 name, tokenHash, expiryArg(expiresAt), string(labelsBytes),
30 )
31 return err
32}
33
34func (d *DB) ResolveExecutorToken(tokenHash string) (string, []string, bool, error) {
35 var name string
36 var expires sql.NullString
37 var labelsRaw, quarantineReason sql.NullString
38 err := d.QueryRow(
39 `select name, expires_at, labels, quarantine_reason from mill_executors where token_hash = ?`, tokenHash,
40 ).Scan(&name, &expires, &labelsRaw, &quarantineReason)
41 if err == sql.ErrNoRows {
42 return "", nil, false, nil
43 }
44 if err != nil {
45 return "", nil, false, err
46 }
47 if quarantineReason.Valid {
48 return "", nil, false, nil
49 }
50 exp, hasExpiry, err := parseExpiry(expires)
51 if err != nil {
52 return "", nil, false, err
53 }
54 if hasExpiry && time.Now().After(exp) {
55 return "", nil, false, nil
56 }
57 var labels []string
58 if labelsRaw.Valid && labelsRaw.String != "" {
59 if err := json.Unmarshal([]byte(labelsRaw.String), &labels); err != nil {
60 return "", nil, false, err
61 }
62 }
63 return name, labels, true, nil
64}
65
66func (d *DB) QuarantineExecutor(name, reason string) error {
67 _, err := d.Exec(
68 `update mill_executors
69 set quarantine_reason = ?, quarantined_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
70 where name = ?`,
71 reason, name,
72 )
73 return err
74}
75
76func (d *DB) ClearExecutorQuarantine(name string) (bool, error) {
77 res, err := d.Exec(
78 `update mill_executors set quarantine_reason = null, quarantined_at = null where name = ?`,
79 name,
80 )
81 if err != nil {
82 return false, err
83 }
84 n, err := res.RowsAffected()
85 return n > 0, err
86}
87
88func (d *DB) RevokeExecutorToken(name string) (bool, error) {
89 res, err := d.Exec(`delete from mill_executors where name = ?`, name)
90 if err != nil {
91 return false, err
92 }
93 n, err := res.RowsAffected()
94 return n > 0, err
95}
96
97func (d *DB) ListExecutorTokens() ([]ExecutorToken, error) {
98 rows, err := d.Query(`select name, created_at, expires_at, labels, quarantine_reason, quarantined_at from mill_executors order by name`)
99 if err != nil {
100 return nil, err
101 }
102 defer rows.Close()
103
104 var out []ExecutorToken
105 for rows.Next() {
106 var t ExecutorToken
107 var expires, labelsRaw, quarantineReason, quarantinedAt sql.NullString
108 if err := rows.Scan(&t.Name, &t.CreatedAt, &expires, &labelsRaw, &quarantineReason, &quarantinedAt); err != nil {
109 return nil, err
110 }
111 exp, hasExpiry, err := parseExpiry(expires)
112 if err != nil {
113 return nil, err
114 }
115 if hasExpiry {
116 t.ExpiresAt = &exp
117 }
118 if labelsRaw.Valid && labelsRaw.String != "" {
119 if err := json.Unmarshal([]byte(labelsRaw.String), &t.Labels); err != nil {
120 return nil, err
121 }
122 }
123 if quarantineReason.Valid {
124 t.QuarantineReason = &quarantineReason.String
125 }
126 if quarantinedAt.Valid {
127 t.QuarantinedAt = &quarantinedAt.String
128 }
129 out = append(out, t)
130 }
131 return out, rows.Err()
132}
133
134func normalizeLabels(labels []string) []string {
135 var out []string
136 seen := make(map[string]bool)
137 for _, l := range labels {
138 trimmed := strings.TrimSpace(l)
139 if trimmed == "" {
140 continue
141 }
142 if !seen[trimmed] {
143 seen[trimmed] = true
144 out = append(out, trimmed)
145 }
146 }
147 slices.Sort(out)
148 return out
149}
150
151func expiryArg(t *time.Time) any {
152 if t == nil {
153 return nil
154 }
155 return t.UTC().Format(time.RFC3339)
156}
157
158func parseExpiry(s sql.NullString) (time.Time, bool, error) {
159 if !s.Valid {
160 return time.Time{}, false, nil
161 }
162 t, err := time.Parse(time.RFC3339, s.String)
163 if err != nil {
164 return time.Time{}, false, err
165 }
166 return t, true, nil
167}