forgejo-runner

git clone git://git.lin.moe/forgejo-runner.git

  1// Copyright 2022 The Gitea Authors. All rights reserved.
  2// SPDX-License-Identifier: MIT
  3
  4package cmd
  5
  6import (
  7	"bufio"
  8	"context"
  9	"fmt"
 10	"os"
 11	"os/signal"
 12	goruntime "runtime"
 13	"strings"
 14	"time"
 15
 16	pingv1 "code.gitea.io/actions-proto-go/ping/v1"
 17	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
 18	"connectrpc.com/connect"
 19	"github.com/mattn/go-isatty"
 20	log "github.com/sirupsen/logrus"
 21	"github.com/spf13/cobra"
 22
 23	"gitea.com/gitea/act_runner/internal/pkg/client"
 24	"gitea.com/gitea/act_runner/internal/pkg/config"
 25	"gitea.com/gitea/act_runner/internal/pkg/labels"
 26	"gitea.com/gitea/act_runner/internal/pkg/ver"
 27)
 28
 29// runRegister registers a runner to the server
 30func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
 31	return func(cmd *cobra.Command, args []string) error {
 32		log.SetReportCaller(false)
 33		isTerm := isatty.IsTerminal(os.Stdout.Fd())
 34		log.SetFormatter(&log.TextFormatter{
 35			DisableColors:    !isTerm,
 36			DisableTimestamp: true,
 37		})
 38		log.SetLevel(log.DebugLevel)
 39
 40		log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
 41			goruntime.GOARCH, goruntime.GOOS, ver.Version())
 42
 43		// runner always needs root permission
 44		if os.Getuid() != 0 {
 45			// TODO: use a better way to check root permission
 46			log.Warnf("Runner in user-mode.")
 47		}
 48
 49		if regArgs.NoInteractive {
 50			if err := registerNoInteractive(ctx, *configFile, regArgs); err != nil {
 51				return err
 52			}
 53		} else {
 54			go func() {
 55				if err := registerInteractive(ctx, *configFile); err != nil {
 56					log.Fatal(err)
 57					return
 58				}
 59				os.Exit(0)
 60			}()
 61
 62			c := make(chan os.Signal, 1)
 63			signal.Notify(c, os.Interrupt)
 64			<-c
 65		}
 66
 67		return nil
 68	}
 69}
 70
 71// registerArgs represents the arguments for register command
 72type registerArgs struct {
 73	NoInteractive bool
 74	InstanceAddr  string
 75	Token         string
 76	RunnerName    string
 77	Labels        string
 78}
 79
 80type registerStage int8
 81
 82const (
 83	StageUnknown              registerStage = -1
 84	StageOverwriteLocalConfig registerStage = iota + 1
 85	StageInputInstance
 86	StageInputToken
 87	StageInputRunnerName
 88	StageInputLabels
 89	StageWaitingForRegistration
 90	StageExit
 91)
 92
 93var defaultLabels = []string{
 94	"docker:docker://node:20-bullseye",
 95}
 96
 97type registerInputs struct {
 98	InstanceAddr string
 99	Token        string
100	RunnerName   string
101	Labels       []string
102}
103
104func (r *registerInputs) validate() error {
105	if r.InstanceAddr == "" {
106		return fmt.Errorf("instance address is empty")
107	}
108	if r.Token == "" {
109		return fmt.Errorf("token is empty")
110	}
111	if len(r.Labels) > 0 {
112		return validateLabels(r.Labels)
113	}
114	return nil
115}
116
117func validateLabels(ls []string) error {
118	for _, label := range ls {
119		if _, err := labels.Parse(label); err != nil {
120			return err
121		}
122	}
123	return nil
124}
125
126func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *config.Config) registerStage {
127	// must set instance address and token.
128	// if empty, keep current stage.
129	if stage == StageInputInstance || stage == StageInputToken {
130		if value == "" {
131			return stage
132		}
133	}
134
135	// set hostname for runner name if empty
136	if stage == StageInputRunnerName && value == "" {
137		value, _ = os.Hostname()
138	}
139
140	switch stage {
141	case StageOverwriteLocalConfig:
142		if value == "Y" || value == "y" {
143			return StageInputInstance
144		}
145		return StageExit
146	case StageInputInstance:
147		r.InstanceAddr = value
148		return StageInputToken
149	case StageInputToken:
150		r.Token = value
151		return StageInputRunnerName
152	case StageInputRunnerName:
153		r.RunnerName = value
154		// if there are some labels configured in config file, skip input labels stage
155		if len(cfg.Runner.Labels) > 0 {
156			ls := make([]string, 0, len(cfg.Runner.Labels))
157			for _, l := range cfg.Runner.Labels {
158				_, err := labels.Parse(l)
159				if err != nil {
160					log.WithError(err).Warnf("ignored invalid label %q", l)
161					continue
162				}
163				ls = append(ls, l)
164			}
165			if len(ls) == 0 {
166				log.Warn("no valid labels configured in config file, runner may not be able to pick up jobs")
167			}
168			r.Labels = ls
169			return StageWaitingForRegistration
170		}
171		return StageInputLabels
172	case StageInputLabels:
173		r.Labels = defaultLabels
174		if value != "" {
175			r.Labels = strings.Split(value, ",")
176		}
177
178		if validateLabels(r.Labels) != nil {
179			log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:20-bookworm,ubuntu-18.04:docker://node:20-bookworm)")
180			return StageInputLabels
181		}
182		return StageWaitingForRegistration
183	}
184	return StageUnknown
185}
186
187func registerInteractive(ctx context.Context, configFile string) error {
188	var (
189		reader = bufio.NewReader(os.Stdin)
190		stage  = StageInputInstance
191		inputs = new(registerInputs)
192	)
193
194	cfg, err := config.LoadDefault(configFile)
195	if err != nil {
196		return fmt.Errorf("failed to load config: %v", err)
197	}
198	if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
199		stage = StageOverwriteLocalConfig
200	}
201
202	for {
203		printStageHelp(stage)
204
205		cmdString, err := reader.ReadString('\n')
206		if err != nil {
207			return err
208		}
209		stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
210
211		if stage == StageWaitingForRegistration {
212			log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
213			if err := doRegister(ctx, cfg, inputs); err != nil {
214				return fmt.Errorf("Failed to register runner: %w", err)
215			}
216			log.Infof("Runner registered successfully.")
217			return nil
218		}
219
220		if stage == StageExit {
221			return nil
222		}
223
224		if stage <= StageUnknown {
225			log.Errorf("Invalid input, please re-run act command.")
226			return nil
227		}
228	}
229}
230
231func printStageHelp(stage registerStage) {
232	switch stage {
233	case StageOverwriteLocalConfig:
234		log.Infoln("Runner is already registered, overwrite local config? [y/N]")
235	case StageInputInstance:
236		log.Infoln("Enter the Forgejo instance URL (for example, https://next.forgejo.org/):")
237	case StageInputToken:
238		log.Infoln("Enter the runner token:")
239	case StageInputRunnerName:
240		hostname, _ := os.Hostname()
241		log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
242	case StageInputLabels:
243		log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:20-bookworm,ubuntu-18.04:docker://node:20-bookworm):")
244	case StageWaitingForRegistration:
245		log.Infoln("Waiting for registration...")
246	}
247}
248
249func registerNoInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
250	cfg, err := config.LoadDefault(configFile)
251	if err != nil {
252		return err
253	}
254	inputs := &registerInputs{
255		InstanceAddr: regArgs.InstanceAddr,
256		Token:        regArgs.Token,
257		RunnerName:   regArgs.RunnerName,
258		Labels:       defaultLabels,
259	}
260	regArgs.Labels = strings.TrimSpace(regArgs.Labels)
261	// command line flag.
262	if regArgs.Labels != "" {
263		inputs.Labels = strings.Split(regArgs.Labels, ",")
264	}
265	// specify labels in config file.
266	if len(cfg.Runner.Labels) > 0 {
267		if regArgs.Labels != "" {
268			log.Warn("Labels from command will be ignored, use labels defined in config file.")
269		}
270		inputs.Labels = cfg.Runner.Labels
271	}
272
273	if inputs.RunnerName == "" {
274		inputs.RunnerName, _ = os.Hostname()
275		log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
276	}
277	if err := inputs.validate(); err != nil {
278		log.WithError(err).Errorf("Invalid input, please re-run act command.")
279		return nil
280	}
281	if err := doRegister(ctx, cfg, inputs); err != nil {
282		return fmt.Errorf("Failed to register runner: %w", err)
283	}
284	log.Infof("Runner registered successfully.")
285	return nil
286}
287
288func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
289	// initial http client
290	cli := client.New(
291		inputs.InstanceAddr,
292		cfg.Runner.Insecure,
293		"",
294		"",
295		ver.Version(),
296	)
297
298	for {
299		_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
300			Data: inputs.RunnerName,
301		}))
302		select {
303		case <-ctx.Done():
304			return ctx.Err()
305		default:
306		}
307		if ctx.Err() != nil {
308			break
309		}
310		if err != nil {
311			log.WithError(err).
312				Errorln("Cannot ping the Forgejo instance server")
313			// TODO: if ping failed, retry or exit
314			time.Sleep(time.Second)
315		} else {
316			log.Debugln("Successfully pinged the Forgejo instance server")
317			break
318		}
319	}
320
321	reg := &config.Registration{
322		Name:    inputs.RunnerName,
323		Token:   inputs.Token,
324		Address: inputs.InstanceAddr,
325		Labels:  inputs.Labels,
326	}
327
328	ls := make([]string, len(reg.Labels))
329	for i, v := range reg.Labels {
330		l, _ := labels.Parse(v)
331		ls[i] = l.Name
332	}
333	// register new runner.
334	resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
335		Name:        reg.Name,
336		Token:       reg.Token,
337		Version:     ver.Version(),
338		AgentLabels: ls, // Could be removed after Gitea 1.20
339		Labels:      ls,
340	}))
341	if err != nil {
342		log.WithError(err).Error("poller: cannot register new runner")
343		return err
344	}
345
346	reg.ID = resp.Msg.Runner.Id
347	reg.UUID = resp.Msg.Runner.Uuid
348	reg.Name = resp.Msg.Runner.Name
349	reg.Token = resp.Msg.Runner.Token
350
351	if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
352		return fmt.Errorf("failed to save runner config: %w", err)
353	}
354	return nil
355}