This repository has no description
1package main
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "os"
8 "strings"
9 "text/tabwriter"
10 "time"
11
12 "github.com/urfave/cli/v3"
13 tlog "tangled.org/core/log"
14 "tangled.org/core/spindle"
15 "tangled.org/core/spindle/db"
16 "tangled.org/core/spindle/mill"
17)
18
19func main() {
20 cmd := &cli.Command{
21 Name: "spindle",
22 Usage: "spindle continuous integration runner",
23 Commands: []*cli.Command{
24 Command(),
25 millCommand(),
26 },
27 DefaultCommand: "run",
28 }
29
30 logger := tlog.New("spindle")
31 slog.SetDefault(logger)
32
33 ctx := context.Background()
34 ctx = tlog.IntoContext(ctx, logger)
35
36 if err := cmd.Run(ctx, os.Args); err != nil {
37 logger.Error(err.Error())
38 os.Exit(-1)
39 }
40}
41
42func Command() *cli.Command {
43 return &cli.Command{
44 Name: "run",
45 Usage: "run the spindle server",
46 Action: func(ctx context.Context, cmd *cli.Command) error {
47 return spindle.Run(ctx)
48 },
49 }
50}
51
52// for mill host administration and executor management
53func millCommand() *cli.Command {
54 dbFlag := &cli.StringFlag{
55 Name: "db",
56 Usage: "path to the spindle sqlite db",
57 Value: "spindle.db",
58 Sources: cli.EnvVars("SPINDLE_SERVER_DB_PATH"),
59 }
60 openDB := func(ctx context.Context, cmd *cli.Command) (*db.DB, error) {
61 return db.Make(ctx, cmd.String("db"))
62 }
63 return &cli.Command{
64 Name: "mill",
65 Usage: "mill host administration",
66 Commands: []*cli.Command{
67 {
68 Name: "executor",
69 Usage: "manage executors allowed to join this mill",
70 Commands: []*cli.Command{
71 {
72 Name: "add",
73 Usage: "register an executor and print its token",
74 ArgsUsage: "<name>",
75 Flags: []cli.Flag{
76 dbFlag,
77 &cli.DurationFlag{
78 Name: "ttl",
79 Usage: "token lifetime (e.g. 720h); omit for no expiry",
80 },
81 &cli.StringSliceFlag{
82 Name: "label",
83 Usage: "authorized labels for this executor",
84 },
85 },
86 Action: func(ctx context.Context, cmd *cli.Command) error {
87 name := cmd.Args().First()
88 if name == "" {
89 return fmt.Errorf("usage: spindle mill executor add <name>")
90 }
91 d, err := openDB(ctx, cmd)
92 if err != nil {
93 return err
94 }
95 token, err := mill.GenerateToken()
96 if err != nil {
97 return err
98 }
99 var expiresAt *time.Time
100 if ttl := cmd.Duration("ttl"); ttl > 0 {
101 exp := time.Now().UTC().Add(ttl)
102 expiresAt = &exp
103 }
104 labels := cmd.StringSlice("label")
105 if err := d.AddExecutorToken(name, mill.HashToken(token), expiresAt, labels); err != nil {
106 return fmt.Errorf("registering executor %q: %w", name, err)
107 }
108 fmt.Println(token)
109 return nil
110 },
111 },
112 {
113 Name: "list",
114 Usage: "list registered executors",
115 Flags: []cli.Flag{dbFlag},
116 Action: func(ctx context.Context, cmd *cli.Command) error {
117 d, err := openDB(ctx, cmd)
118 if err != nil {
119 return err
120 }
121 tokens, err := d.ListExecutorTokens()
122 if err != nil {
123 return err
124 }
125 w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
126 fmt.Fprintln(w, "NAME\tCREATED\tEXPIRES\tLABELS\tQUARANTINE")
127 for _, t := range tokens {
128 expires := "never"
129 if t.ExpiresAt != nil {
130 expires = t.ExpiresAt.Format(time.RFC3339)
131 if time.Now().After(*t.ExpiresAt) {
132 expires += " (expired)"
133 }
134 }
135 labels := strings.Join(t.Labels, ",")
136 if labels == "" {
137 labels = "-"
138 }
139 quarantine := "-"
140 if t.QuarantineReason != nil {
141 quarantine = *t.QuarantineReason
142 if t.QuarantinedAt != nil {
143 quarantine = *t.QuarantinedAt + ": " + quarantine
144 }
145 }
146 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", t.Name, t.CreatedAt, expires, labels, quarantine)
147 }
148 return w.Flush()
149 },
150 },
151 {
152 Name: "unquarantine",
153 Usage: "allow a quarantined executor to reconnect",
154 ArgsUsage: "<name>",
155 Flags: []cli.Flag{dbFlag},
156 Action: func(ctx context.Context, cmd *cli.Command) error {
157 name := cmd.Args().First()
158 if name == "" {
159 return fmt.Errorf("usage: spindle mill executor unquarantine <name>")
160 }
161 d, err := openDB(ctx, cmd)
162 if err != nil {
163 return err
164 }
165 ok, err := d.ClearExecutorQuarantine(name)
166 if err != nil {
167 return err
168 }
169 if !ok {
170 return fmt.Errorf("no such executor identity %q", name)
171 }
172 return nil
173 },
174 },
175 {
176 Name: "revoke",
177 Usage: "revoke an executor's token",
178 ArgsUsage: "<name>",
179 Flags: []cli.Flag{dbFlag},
180 Action: func(ctx context.Context, cmd *cli.Command) error {
181 name := cmd.Args().First()
182 if name == "" {
183 return fmt.Errorf("usage: spindle mill executor revoke <name>")
184 }
185 d, err := openDB(ctx, cmd)
186 if err != nil {
187 return err
188 }
189 ok, err := d.RevokeExecutorToken(name)
190 if err != nil {
191 return err
192 }
193 if !ok {
194 return fmt.Errorf("no such executor identity %q", name)
195 }
196 return nil
197 },
198 },
199 },
200 },
201 },
202 }
203}