v0.34.0 7c1886f

cli_runner.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"context"
7	"errors"
8	"fmt"
9	"strings"
10
11	"connectrpc.com/connect"
12	"github.com/spf13/cobra"
13
14	"bnema.dev/ermite-api/gen/ermite"
15	"bnema.dev/ermite/internal/adapters/client"
16)
17
18type jsonRunner struct {
19	ID           string   `json:"id"`
20	Name         string   `json:"name"`
21	Labels       []string `json:"labels"`
22	Isolation    string   `json:"isolation"`
23	Version      string   `json:"version"`
24	Arch         string   `json:"arch"`
25	Os           string   `json:"os"`
26	RegisteredAt *string  `json:"registered_at"`
27	LastSeen     *string  `json:"last_seen"`
28	Busy         bool     `json:"busy"`
29	Online       bool     `json:"online"`
30}
31
32const jsonRunnerHelp = `
33JSON fields of a runner (--json): id, name, labels, isolation (vm, container-sysbox,
34container-privileged), version, arch, os, registered_at, last_seen (RFC 3339 UTC, null if never
35seen), busy and online (decided by the server: heard from within the last 2 minutes by its
36clock; a server older than the field reports false for every runner).`
37
38func newRunnerCmd(env *cliEnv) *cobra.Command {
39	runner := &cobra.Command{
40		Use:   "runner",
41		Short: "List and remove CI runners",
42		Long: "Manage the CI runners registered on a remote server. A runner registers itself with a token\n" +
43			"made by `ermite token create --preset runner`. Needs the runner:register scope, on a token that is\n" +
44			"not limited to some repositories.",
45	}
46	runner.AddCommand(newRunnerListCmd(env), newRunnerRemoveCmd(env))
47	return runner
48}
49
50func newRunnerListCmd(env *cliEnv) *cobra.Command {
51	cmd := &cobra.Command{
52		Use:   "list",
53		Short: "List the registered runners",
54		Long:  "List the registered runners ordered by name." + jsonRunnerHelp + "\n--json prints an array of runners.",
55		Args:  cobra.NoArgs,
56	}
57	asJSON := addJSONFlag(cmd)
58	cmd.RunE = func(cmd *cobra.Command, _ []string) error {
59		c, err := env.client(cmd)
60		if err != nil {
61			return err
62		}
63		ctx := env.ctx(cmd)
64		runners, err := collect(ctx, func(token string) ([]*ermite.Runner, string, error) {
65			res, err := c.Runs().ListRunners(ctx, connect.NewRequest(&ermite.ListRunnersRequest{PageToken: token}))
66			if err != nil {
67				return nil, "", err
68			}
69			return res.Msg.GetRunners(), res.Msg.GetNextPageToken(), nil
70		})
71		if err != nil {
72			return err
73		}
74		if *asJSON {
75			out := make([]jsonRunner, 0, len(runners))
76			for _, r := range runners {
77				labels := r.GetLabels()
78				if labels == nil {
79					labels = []string{}
80				}
81				out = append(out, jsonRunner{
82					ID: r.GetId(), Name: r.GetName(), Labels: labels, Isolation: isolationText(r.GetIsolation()), Version: r.GetVersion(),
83					Arch: r.GetArch(), Os: r.GetOs(), RegisteredAt: jsonTime(r.GetRegisteredAt()), LastSeen: jsonTime(r.GetLastSeen()), Busy: r.GetBusy(), Online: r.GetOnline(),
84				})
85			}
86			return writeJSON(cmd.OutOrStdout(), out)
87		}
88		if len(runners) == 0 {
89			noticef(cmd, "no runners: register one with `ermite-runner register`")
90			return nil
91		}
92		rows := make([][]string, 0, len(runners))
93		for _, r := range runners {
94			state := "offline"
95			if r.GetOnline() {
96				state = "online"
97				if r.GetBusy() {
98					state = "busy"
99				}
100			}
101			rows = append(rows, []string{
102				r.GetId(), r.GetName(), state, isolationText(r.GetIsolation()), r.GetVersion(),
103				strings.Join(r.GetLabels(), ","), shortTime(r.GetLastSeen(), "never"),
104			})
105		}
106		return table(cmd.OutOrStdout(), []string{"ID", "NAME", "STATE", "ISOLATION", "VERSION", "LABELS", "LAST SEEN"}, rows)
107	}
108	return cmd
109}
110
111func newRunnerRemoveCmd(env *cliEnv) *cobra.Command {
112	cmd := &cobra.Command{
113		Use:   "remove <runner id>",
114		Short: "Remove a runner",
115		Long: "Remove a runner: its token stops working at once and it gets no more jobs. A job it is running is not\n" +
116			"affected: it ends, or fails as a lost runner when its heartbeats stop.",
117		Args: cobra.ExactArgs(1),
118		ValidArgsFunction: func(cmd *cobra.Command, args []string, prefix string) ([]string, cobra.ShellCompDirective) {
119			if len(args) > 0 {
120				return nil, cobra.ShellCompDirectiveNoFileComp
121			}
122			out := complete(env, cmd, prefix, func(ctx context.Context, c *client.Client) ([]*ermite.Runner, error) {
123				res, err := c.Runs().ListRunners(ctx, connect.NewRequest(&ermite.ListRunnersRequest{}))
124				if err != nil {
125					return nil, err
126				}
127				return res.Msg.GetRunners(), nil
128			}, func(r *ermite.Runner) (string, string) { return r.GetId(), r.GetName() })
129			return out, cobra.ShellCompDirectiveNoFileComp
130		},
131	}
132	cmd.RunE = func(cmd *cobra.Command, args []string) error {
133		c, err := env.client(cmd)
134		if err != nil {
135			return err
136		}
137		if _, err := c.Runs().RemoveRunner(env.ctx(cmd), connect.NewRequest(&ermite.RemoveRunnerRequest{RunnerId: args[0]})); err != nil {
138			var ce *connect.Error
139			if errors.As(err, &ce) && ce.Code() == connect.CodeNotFound {
140				return fmt.Errorf("no runner has the id %q", args[0])
141			}
142			return err
143		}
144		noticef(cmd, "runner %s removed", args[0])
145		return nil
146	}
147	return cmd
148}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 44

↑↓ moveenter openesc close