mirror of
https://gitea.com/gitea/act_runner.git
synced 2025-06-07 23:30:40 +02:00

I used to be able to do something like `./act_runner register --instance https://gitea.com --token testdcff --name test` on GitHub Actions Runners, but act_runner always asked me to enter instance, token etc. again and requiring me to use `--no-interactive` including passing everything per cli. My idea was to extract the preset input of some stages to skip the prompt for this value if it is a non empty string. Labels is the only question that has been asked more than once if validation failed, in this case the error path have to unset the values of the input structure to not end in a non-interactive loop. _I have written this initially for my own gitea runner, might be useful to everyone using the official runner as well_ Reviewed-on: https://gitea.com/gitea/act_runner/pulls/682 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Christopher Homberger <christopher.homberger@web.de> Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
395 lines
10 KiB
Go
395 lines
10 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
goruntime "runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
|
"connectrpc.com/connect"
|
|
"github.com/mattn/go-isatty"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
|
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
|
)
|
|
|
|
// runRegister registers a runner to the server
|
|
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
|
return func(cmd *cobra.Command, args []string) error {
|
|
log.SetReportCaller(false)
|
|
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
|
log.SetFormatter(&log.TextFormatter{
|
|
DisableColors: !isTerm,
|
|
DisableTimestamp: true,
|
|
})
|
|
log.SetLevel(log.DebugLevel)
|
|
|
|
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
|
goruntime.GOARCH, goruntime.GOOS, ver.Version())
|
|
|
|
// runner always needs root permission
|
|
if os.Getuid() != 0 {
|
|
// TODO: use a better way to check root permission
|
|
log.Warnf("Runner in user-mode.")
|
|
}
|
|
|
|
if regArgs.NoInteractive {
|
|
if err := registerNoInteractive(ctx, *configFile, regArgs); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
go func() {
|
|
if err := registerInteractive(ctx, *configFile, regArgs); err != nil {
|
|
log.Fatal(err)
|
|
return
|
|
}
|
|
os.Exit(0)
|
|
}()
|
|
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt)
|
|
<-c
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// registerArgs represents the arguments for register command
|
|
type registerArgs struct {
|
|
NoInteractive bool
|
|
InstanceAddr string
|
|
Token string
|
|
RunnerName string
|
|
Labels string
|
|
Ephemeral bool
|
|
}
|
|
|
|
type registerStage int8
|
|
|
|
const (
|
|
StageUnknown registerStage = -1
|
|
StageOverwriteLocalConfig registerStage = iota + 1
|
|
StageInputInstance
|
|
StageInputToken
|
|
StageInputRunnerName
|
|
StageInputLabels
|
|
StageWaitingForRegistration
|
|
StageExit
|
|
)
|
|
|
|
var defaultLabels = []string{
|
|
"ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest",
|
|
"ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04",
|
|
"ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04",
|
|
}
|
|
|
|
type registerInputs struct {
|
|
InstanceAddr string
|
|
Token string
|
|
RunnerName string
|
|
Labels []string
|
|
Ephemeral bool
|
|
}
|
|
|
|
func (r *registerInputs) validate() error {
|
|
if r.InstanceAddr == "" {
|
|
return fmt.Errorf("instance address is empty")
|
|
}
|
|
if r.Token == "" {
|
|
return fmt.Errorf("token is empty")
|
|
}
|
|
if len(r.Labels) > 0 {
|
|
return validateLabels(r.Labels)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateLabels(ls []string) error {
|
|
for _, label := range ls {
|
|
if _, err := labels.Parse(label); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *registerInputs) stageValue(stage registerStage) string {
|
|
switch stage {
|
|
case StageInputInstance:
|
|
return r.InstanceAddr
|
|
case StageInputToken:
|
|
return r.Token
|
|
case StageInputRunnerName:
|
|
return r.RunnerName
|
|
case StageInputLabels:
|
|
if len(r.Labels) > 0 {
|
|
return strings.Join(r.Labels, ",")
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *config.Config) registerStage {
|
|
// must set instance address and token.
|
|
// if empty, keep current stage.
|
|
if stage == StageInputInstance || stage == StageInputToken {
|
|
if value == "" {
|
|
return stage
|
|
}
|
|
}
|
|
|
|
// set hostname for runner name if empty
|
|
if stage == StageInputRunnerName && value == "" {
|
|
value, _ = os.Hostname()
|
|
}
|
|
|
|
switch stage {
|
|
case StageOverwriteLocalConfig:
|
|
if value == "Y" || value == "y" {
|
|
return StageInputInstance
|
|
}
|
|
return StageExit
|
|
case StageInputInstance:
|
|
r.InstanceAddr = value
|
|
return StageInputToken
|
|
case StageInputToken:
|
|
r.Token = value
|
|
return StageInputRunnerName
|
|
case StageInputRunnerName:
|
|
r.RunnerName = value
|
|
// if there are some labels configured in config file, skip input labels stage
|
|
if len(cfg.Runner.Labels) > 0 {
|
|
ls := make([]string, 0, len(cfg.Runner.Labels))
|
|
for _, l := range cfg.Runner.Labels {
|
|
_, err := labels.Parse(l)
|
|
if err != nil {
|
|
log.WithError(err).Warnf("ignored invalid label %q", l)
|
|
continue
|
|
}
|
|
ls = append(ls, l)
|
|
}
|
|
if len(ls) == 0 {
|
|
log.Warn("no valid labels configured in config file, runner may not be able to pick up jobs")
|
|
}
|
|
r.Labels = ls
|
|
return StageWaitingForRegistration
|
|
}
|
|
return StageInputLabels
|
|
case StageInputLabels:
|
|
r.Labels = defaultLabels
|
|
if value != "" {
|
|
r.Labels = strings.Split(value, ",")
|
|
}
|
|
|
|
if validateLabels(r.Labels) != nil {
|
|
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest)")
|
|
r.Labels = nil
|
|
return StageInputLabels
|
|
}
|
|
return StageWaitingForRegistration
|
|
}
|
|
return StageUnknown
|
|
}
|
|
|
|
func initInputs(regArgs *registerArgs) *registerInputs {
|
|
inputs := ®isterInputs{
|
|
InstanceAddr: regArgs.InstanceAddr,
|
|
Token: regArgs.Token,
|
|
RunnerName: regArgs.RunnerName,
|
|
Ephemeral: regArgs.Ephemeral,
|
|
}
|
|
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
|
|
// command line flag.
|
|
if regArgs.Labels != "" {
|
|
inputs.Labels = strings.Split(regArgs.Labels, ",")
|
|
}
|
|
return inputs
|
|
}
|
|
|
|
func registerInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
|
var (
|
|
reader = bufio.NewReader(os.Stdin)
|
|
stage = StageInputInstance
|
|
)
|
|
|
|
cfg, err := config.LoadDefault(configFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load config: %v", err)
|
|
}
|
|
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
|
stage = StageOverwriteLocalConfig
|
|
}
|
|
inputs := initInputs(regArgs)
|
|
|
|
for {
|
|
cmdString := inputs.stageValue(stage)
|
|
if cmdString == "" {
|
|
printStageHelp(stage)
|
|
var err error
|
|
cmdString, err = reader.ReadString('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
|
|
|
|
if stage == StageWaitingForRegistration {
|
|
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
|
if err := doRegister(ctx, cfg, inputs); err != nil {
|
|
return fmt.Errorf("Failed to register runner: %w", err)
|
|
}
|
|
log.Infof("Runner registered successfully.")
|
|
return nil
|
|
}
|
|
|
|
if stage == StageExit {
|
|
return nil
|
|
}
|
|
|
|
if stage <= StageUnknown {
|
|
log.Errorf("Invalid input, please re-run act command.")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func printStageHelp(stage registerStage) {
|
|
switch stage {
|
|
case StageOverwriteLocalConfig:
|
|
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
|
|
case StageInputInstance:
|
|
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
|
case StageInputToken:
|
|
log.Infoln("Enter the runner token:")
|
|
case StageInputRunnerName:
|
|
hostname, _ := os.Hostname()
|
|
log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
|
|
case StageInputLabels:
|
|
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest):")
|
|
case StageWaitingForRegistration:
|
|
log.Infoln("Waiting for registration...")
|
|
}
|
|
}
|
|
|
|
func registerNoInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
|
cfg, err := config.LoadDefault(configFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inputs := initInputs(regArgs)
|
|
// specify labels in config file.
|
|
if len(cfg.Runner.Labels) > 0 {
|
|
if regArgs.Labels != "" {
|
|
log.Warn("Labels from command will be ignored, use labels defined in config file.")
|
|
}
|
|
inputs.Labels = cfg.Runner.Labels
|
|
}
|
|
if len(inputs.Labels) == 0 {
|
|
inputs.Labels = defaultLabels
|
|
}
|
|
|
|
if inputs.RunnerName == "" {
|
|
inputs.RunnerName, _ = os.Hostname()
|
|
log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
|
|
}
|
|
if err := inputs.validate(); err != nil {
|
|
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
|
return err
|
|
}
|
|
if err := doRegister(ctx, cfg, inputs); err != nil {
|
|
return fmt.Errorf("Failed to register runner: %w", err)
|
|
}
|
|
log.Infof("Runner registered successfully.")
|
|
return nil
|
|
}
|
|
|
|
func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
|
|
// initial http client
|
|
cli := client.New(
|
|
inputs.InstanceAddr,
|
|
cfg.Runner.Insecure,
|
|
"",
|
|
"",
|
|
ver.Version(),
|
|
)
|
|
|
|
for {
|
|
_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
|
|
Data: inputs.RunnerName,
|
|
}))
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
if err != nil {
|
|
log.WithError(err).
|
|
Errorln("Cannot ping the Gitea instance server")
|
|
// TODO: if ping failed, retry or exit
|
|
time.Sleep(time.Second)
|
|
} else {
|
|
log.Debugln("Successfully pinged the Gitea instance server")
|
|
break
|
|
}
|
|
}
|
|
|
|
reg := &config.Registration{
|
|
Name: inputs.RunnerName,
|
|
Token: inputs.Token,
|
|
Address: inputs.InstanceAddr,
|
|
Labels: inputs.Labels,
|
|
Ephemeral: inputs.Ephemeral,
|
|
}
|
|
|
|
ls := make([]string, len(reg.Labels))
|
|
for i, v := range reg.Labels {
|
|
l, _ := labels.Parse(v)
|
|
ls[i] = l.Name
|
|
}
|
|
// register new runner.
|
|
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
|
Name: reg.Name,
|
|
Token: reg.Token,
|
|
Version: ver.Version(),
|
|
AgentLabels: ls, // Could be removed after Gitea 1.20
|
|
Labels: ls,
|
|
Ephemeral: reg.Ephemeral,
|
|
}))
|
|
if err != nil {
|
|
log.WithError(err).Error("poller: cannot register new runner")
|
|
return err
|
|
}
|
|
|
|
reg.ID = resp.Msg.Runner.Id
|
|
reg.UUID = resp.Msg.Runner.Uuid
|
|
reg.Name = resp.Msg.Runner.Name
|
|
reg.Token = resp.Msg.Runner.Token
|
|
|
|
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
|
|
// TODO we cannot remove the configuration via runner api, if we return an error here we just fill the database
|
|
log.Error("poller: cannot register new runner as ephemeral upgrade Gitea to gain security, run-once will be used automatically")
|
|
}
|
|
|
|
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
|
return fmt.Errorf("failed to save runner config: %w", err)
|
|
}
|
|
return nil
|
|
}
|