package main import ( "context" "fmt" "log/slog" "os" "strings" "text/tabwriter" "time" "github.com/urfave/cli/v3" tlog "tangled.org/core/log" "tangled.org/core/spindle" "tangled.org/core/spindle/db" "tangled.org/core/spindle/mill" ) func main() { cmd := &cli.Command{ Name: "spindle", Usage: "spindle continuous integration runner", Commands: []*cli.Command{ Command(), millCommand(), }, DefaultCommand: "run", } logger := tlog.New("spindle") slog.SetDefault(logger) ctx := context.Background() ctx = tlog.IntoContext(ctx, logger) if err := cmd.Run(ctx, os.Args); err != nil { logger.Error(err.Error()) os.Exit(-1) } } func Command() *cli.Command { return &cli.Command{ Name: "run", Usage: "run the spindle server", Action: func(ctx context.Context, cmd *cli.Command) error { return spindle.Run(ctx) }, } } // for mill host administration and executor management func millCommand() *cli.Command { dbFlag := &cli.StringFlag{ Name: "db", Usage: "path to the spindle sqlite db", Value: "spindle.db", Sources: cli.EnvVars("SPINDLE_SERVER_DB_PATH"), } openDB := func(ctx context.Context, cmd *cli.Command) (*db.DB, error) { return db.Make(ctx, cmd.String("db")) } return &cli.Command{ Name: "mill", Usage: "mill host administration", Commands: []*cli.Command{ { Name: "executor", Usage: "manage executors allowed to join this mill", Commands: []*cli.Command{ { Name: "add", Usage: "register an executor and print its token", ArgsUsage: "", Flags: []cli.Flag{ dbFlag, &cli.DurationFlag{ Name: "ttl", Usage: "token lifetime (e.g. 720h); omit for no expiry", }, &cli.StringSliceFlag{ Name: "label", Usage: "authorized labels for this executor", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { name := cmd.Args().First() if name == "" { return fmt.Errorf("usage: spindle mill executor add ") } d, err := openDB(ctx, cmd) if err != nil { return err } token, err := mill.GenerateToken() if err != nil { return err } var expiresAt *time.Time if ttl := cmd.Duration("ttl"); ttl > 0 { exp := time.Now().UTC().Add(ttl) expiresAt = &exp } labels := cmd.StringSlice("label") if err := d.AddExecutorToken(name, mill.HashToken(token), expiresAt, labels); err != nil { return fmt.Errorf("registering executor %q: %w", name, err) } fmt.Println(token) return nil }, }, { Name: "list", Usage: "list registered executors", Flags: []cli.Flag{dbFlag}, Action: func(ctx context.Context, cmd *cli.Command) error { d, err := openDB(ctx, cmd) if err != nil { return err } tokens, err := d.ListExecutorTokens() if err != nil { return err } w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) fmt.Fprintln(w, "NAME\tCREATED\tEXPIRES\tLABELS\tQUARANTINE") for _, t := range tokens { expires := "never" if t.ExpiresAt != nil { expires = t.ExpiresAt.Format(time.RFC3339) if time.Now().After(*t.ExpiresAt) { expires += " (expired)" } } labels := strings.Join(t.Labels, ",") if labels == "" { labels = "-" } quarantine := "-" if t.QuarantineReason != nil { quarantine = *t.QuarantineReason if t.QuarantinedAt != nil { quarantine = *t.QuarantinedAt + ": " + quarantine } } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", t.Name, t.CreatedAt, expires, labels, quarantine) } return w.Flush() }, }, { Name: "unquarantine", Usage: "allow a quarantined executor to reconnect", ArgsUsage: "", Flags: []cli.Flag{dbFlag}, Action: func(ctx context.Context, cmd *cli.Command) error { name := cmd.Args().First() if name == "" { return fmt.Errorf("usage: spindle mill executor unquarantine ") } d, err := openDB(ctx, cmd) if err != nil { return err } ok, err := d.ClearExecutorQuarantine(name) if err != nil { return err } if !ok { return fmt.Errorf("no such executor identity %q", name) } return nil }, }, { Name: "revoke", Usage: "revoke an executor's token", ArgsUsage: "", Flags: []cli.Flag{dbFlag}, Action: func(ctx context.Context, cmd *cli.Command) error { name := cmd.Args().First() if name == "" { return fmt.Errorf("usage: spindle mill executor revoke ") } d, err := openDB(ctx, cmd) if err != nil { return err } ok, err := d.RevokeExecutorToken(name) if err != nil { return err } if !ok { return fmt.Errorf("no such executor identity %q", name) } return nil }, }, }, }, }, } }