1// Copyright 2022 The Gitea Authors. All rights reserved.2// SPDX-License-Identifier: MIT34package cmd56import (7 "bufio"8 "context"9 "fmt"10 "os"11 "os/signal"12 goruntime "runtime"13 "strings"14 "time"1516 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"2223 "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)2829// runRegister registers a runner to the server30func 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)3940 log.Infof("Registering runner, arch=%s, os=%s, version=%s.",41 goruntime.GOARCH, goruntime.GOOS, ver.Version())4243 // runner always needs root permission44 if os.Getuid() != 0 {45 // TODO: use a better way to check root permission46 log.Warnf("Runner in user-mode.")47 }4849 if regArgs.NoInteractive {50 if err := registerNoInteractive(ctx, *configFile, regArgs); err != nil {51 return err52 }53 } else {54 go func() {55 if err := registerInteractive(ctx, *configFile); err != nil {56 log.Fatal(err)57 return58 }59 os.Exit(0)60 }()6162 c := make(chan os.Signal, 1)63 signal.Notify(c, os.Interrupt)64 <-c65 }6667 return nil68 }69}7071// registerArgs represents the arguments for register command72type registerArgs struct {73 NoInteractive bool74 InstanceAddr string75 Token string76 RunnerName string77 Labels string78}7980type registerStage int88182const (83 StageUnknown registerStage = -184 StageOverwriteLocalConfig registerStage = iota + 185 StageInputInstance86 StageInputToken87 StageInputRunnerName88 StageInputLabels89 StageWaitingForRegistration90 StageExit91)9293var defaultLabels = []string{94 "docker:docker://node:20-bullseye",95}9697type registerInputs struct {98 InstanceAddr string99 Token string100 RunnerName string101 Labels []string102}103104func (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 nil115}116117func validateLabels(ls []string) error {118 for _, label := range ls {119 if _, err := labels.Parse(label); err != nil {120 return err121 }122 }123 return nil124}125126func (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 stage132 }133 }134135 // set hostname for runner name if empty136 if stage == StageInputRunnerName && value == "" {137 value, _ = os.Hostname()138 }139140 switch stage {141 case StageOverwriteLocalConfig:142 if value == "Y" || value == "y" {143 return StageInputInstance144 }145 return StageExit146 case StageInputInstance:147 r.InstanceAddr = value148 return StageInputToken149 case StageInputToken:150 r.Token = value151 return StageInputRunnerName152 case StageInputRunnerName:153 r.RunnerName = value154 // if there are some labels configured in config file, skip input labels stage155 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 continue162 }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 = ls169 return StageWaitingForRegistration170 }171 return StageInputLabels172 case StageInputLabels:173 r.Labels = defaultLabels174 if value != "" {175 r.Labels = strings.Split(value, ",")176 }177178 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 StageInputLabels181 }182 return StageWaitingForRegistration183 }184 return StageUnknown185}186187func registerInteractive(ctx context.Context, configFile string) error {188 var (189 reader = bufio.NewReader(os.Stdin)190 stage = StageInputInstance191 inputs = new(registerInputs)192 )193194 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 = StageOverwriteLocalConfig200 }201202 for {203 printStageHelp(stage)204205 cmdString, err := reader.ReadString('\n')206 if err != nil {207 return err208 }209 stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)210211 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 nil218 }219220 if stage == StageExit {221 return nil222 }223224 if stage <= StageUnknown {225 log.Errorf("Invalid input, please re-run act command.")226 return nil227 }228 }229}230231func 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}248249func registerNoInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {250 cfg, err := config.LoadDefault(configFile)251 if err != nil {252 return err253 }254 inputs := ®isterInputs{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.Labels271 }272273 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 nil280 }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 nil286}287288func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {289 // initial http client290 cli := client.New(291 inputs.InstanceAddr,292 cfg.Runner.Insecure,293 "",294 "",295 ver.Version(),296 )297298 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 break309 }310 if err != nil {311 log.WithError(err).312 Errorln("Cannot ping the Forgejo instance server")313 // TODO: if ping failed, retry or exit314 time.Sleep(time.Second)315 } else {316 log.Debugln("Successfully pinged the Forgejo instance server")317 break318 }319 }320321 reg := &config.Registration{322 Name: inputs.RunnerName,323 Token: inputs.Token,324 Address: inputs.InstanceAddr,325 Labels: inputs.Labels,326 }327328 ls := make([]string, len(reg.Labels))329 for i, v := range reg.Labels {330 l, _ := labels.Parse(v)331 ls[i] = l.Name332 }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.20339 Labels: ls,340 }))341 if err != nil {342 log.WithError(err).Error("poller: cannot register new runner")343 return err344 }345346 reg.ID = resp.Msg.Runner.Id347 reg.UUID = resp.Msg.Runner.Uuid348 reg.Name = resp.Msg.Runner.Name349 reg.Token = resp.Msg.Runner.Token350351 if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {352 return fmt.Errorf("failed to save runner config: %w", err)353 }354 return nil355}