2022-11-15 20:46:29 +08:00
package cmd
import (
2022-11-15 21:28:07 +08:00
"bufio"
2022-11-15 20:46:29 +08:00
"context"
2022-11-17 19:43:26 +08:00
"fmt"
2022-11-15 21:28:07 +08:00
"os"
"os/signal"
"runtime"
"strings"
2022-11-15 22:42:41 +08:00
"time"
"gitea.com/gitea/act_runner/client"
"gitea.com/gitea/act_runner/config"
"gitea.com/gitea/act_runner/register"
pingv1 "gitea.com/gitea/proto-go/ping/v1"
"github.com/appleboy/com/file"
"github.com/bufbuild/connect-go"
"github.com/joho/godotenv"
2022-11-15 21:28:07 +08:00
"github.com/mattn/go-isatty"
2022-11-15 20:46:29 +08:00
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// runRegister registers a runner to the server
2022-11-15 22:42:41 +08:00
func runRegister ( ctx context . Context , regArgs * registerArgs , envFile string ) func ( * cobra . Command , [ ] string ) error {
2022-11-15 20:46:29 +08:00
return func ( cmd * cobra . Command , args [ ] string ) error {
2022-11-15 21:28:07 +08:00
log . SetReportCaller ( false )
isTerm := isatty . IsTerminal ( os . Stdout . Fd ( ) )
log . SetFormatter ( & log . TextFormatter {
DisableColors : ! isTerm ,
DisableTimestamp : true ,
} )
2022-11-15 22:42:41 +08:00
log . SetLevel ( log . DebugLevel )
2022-11-15 21:28:07 +08:00
log . Infof ( "Registering runner, arch=%s, os=%s, version=%s." ,
runtime . GOARCH , runtime . GOOS , 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." )
}
2022-11-17 19:43:26 +08:00
if regArgs . NoInteractive {
if err := registerNoInteractive ( envFile , regArgs ) ; err != nil {
return err
2022-11-15 21:28:07 +08:00
}
2022-11-17 19:43:26 +08:00
} else {
go func ( ) {
if err := registerInteractive ( envFile ) ; err != nil {
// log.Errorln(err)
os . Exit ( 2 )
return
}
os . Exit ( 0 )
} ( )
c := make ( chan os . Signal , 1 )
signal . Notify ( c , os . Interrupt )
<- c
}
2022-11-15 21:28:07 +08:00
2022-11-15 20:46:29 +08:00
return nil
}
}
// registerArgs represents the arguments for register command
type registerArgs struct {
NoInteractive bool
InstanceAddr string
Token string
2022-11-17 19:43:26 +08:00
RunnerName string
Labels string
2022-11-15 20:46:29 +08:00
}
2022-11-15 21:28:07 +08:00
type registerStage int8
const (
2022-11-15 22:42:41 +08:00
StageUnknown registerStage = - 1
StageOverwriteLocalConfig registerStage = iota + 1
StageInputInstance
2022-11-15 21:28:07 +08:00
StageInputToken
StageInputRunnerName
StageInputCustomLabels
StageWaitingForRegistration
2022-11-15 22:42:41 +08:00
StageExit
2022-11-15 21:28:07 +08:00
)
type registerInputs struct {
InstanceAddr string
Token string
RunnerName string
CustomLabels [ ] string
}
2022-11-17 19:43:26 +08:00
func ( r * registerInputs ) validate ( ) error {
if r . InstanceAddr == "" {
return fmt . Errorf ( "instance address is empty" )
}
if r . Token == "" {
return fmt . Errorf ( "token is empty" )
}
2022-11-21 20:12:25 +08:00
if len ( r . CustomLabels ) > 0 {
return validateLabels ( r . CustomLabels )
}
return nil
}
func validateLabels ( labels [ ] string ) error {
for _ , label := range labels {
values := strings . SplitN ( label , ":" , 2 )
if len ( values ) != 2 {
return fmt . Errorf ( "Invalid label: %s" , label )
}
// TODO: validate value format, like docker://node:16-buster
}
2022-11-17 19:43:26 +08:00
return nil
}
2022-11-15 21:28:07 +08:00
func ( r * registerInputs ) assignToNext ( stage registerStage , value string ) 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 {
2022-11-15 22:42:41 +08:00
case StageOverwriteLocalConfig :
if value == "Y" || value == "y" {
return StageInputInstance
}
return StageExit
2022-11-15 21:28:07 +08:00
case StageInputInstance :
r . InstanceAddr = value
return StageInputToken
case StageInputToken :
r . Token = value
return StageInputRunnerName
case StageInputRunnerName :
r . RunnerName = value
return StageInputCustomLabels
case StageInputCustomLabels :
2022-11-21 20:12:25 +08:00
if value == "" {
return StageWaitingForRegistration
}
2022-11-15 21:28:07 +08:00
r . CustomLabels = strings . Split ( value , "," )
2022-11-21 20:12:25 +08:00
if validateLabels ( r . CustomLabels ) != nil {
log . Infoln ( "Invalid labels, please input again (for example, ubuntu-latest:docker://node:16-buster)" )
return StageInputCustomLabels
}
2022-11-15 21:28:07 +08:00
return StageWaitingForRegistration
}
return StageUnknown
}
2022-11-15 22:42:41 +08:00
func registerInteractive ( envFile string ) error {
2022-11-15 21:28:07 +08:00
var (
reader = bufio . NewReader ( os . Stdin )
stage = StageInputInstance
inputs = new ( registerInputs )
)
2022-11-15 22:42:41 +08:00
// check if overwrite local config
_ = godotenv . Load ( envFile )
cfg , _ := config . FromEnviron ( )
if file . IsFile ( cfg . Runner . File ) {
stage = StageOverwriteLocalConfig
}
2022-11-15 21:28:07 +08:00
for {
printStageHelp ( stage )
2022-11-15 22:42:41 +08:00
2022-11-15 21:28:07 +08:00
cmdString , err := reader . ReadString ( '\n' )
if err != nil {
return err
}
stage = inputs . assignToNext ( stage , strings . TrimSpace ( cmdString ) )
if stage == StageWaitingForRegistration {
log . Infof ( "Registering runner, name=%s, instance=%s, labels=%v." , inputs . RunnerName , inputs . InstanceAddr , inputs . CustomLabels )
2022-11-15 22:42:41 +08:00
if err := doRegister ( & cfg , inputs ) ; err != nil {
log . Errorf ( "Failed to register runner: %v" , err )
} else {
log . Infof ( "Runner registered successfully." )
}
return nil
}
if stage == StageExit {
2022-11-15 21:28:07 +08:00
return nil
}
if stage <= StageUnknown {
log . Errorf ( "Invalid input, please re-run act command." )
return nil
}
}
}
func printStageHelp ( stage registerStage ) {
switch stage {
2022-11-15 22:42:41 +08:00
case StageOverwriteLocalConfig :
log . Infoln ( "Runner is already registered, overwrite local config? [Y/n]" )
2022-11-15 21:28:07 +08:00
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 StageInputCustomLabels :
2022-11-21 20:12:25 +08:00
log . Infoln ( "Enter the runner custom labels (comma-separated, for example, ubuntu-latest:docker://node:16-buster,ubuntu-2204:docker://node:18-buster):" )
2022-11-15 21:28:07 +08:00
case StageWaitingForRegistration :
log . Infoln ( "Waiting for registration..." )
}
}
2022-11-15 22:42:41 +08:00
2022-11-17 19:43:26 +08:00
func registerNoInteractive ( envFile string , regArgs * registerArgs ) error {
_ = godotenv . Load ( envFile )
cfg , _ := config . FromEnviron ( )
inputs := & registerInputs {
InstanceAddr : regArgs . InstanceAddr ,
Token : regArgs . Token ,
RunnerName : regArgs . RunnerName ,
2022-11-21 20:12:25 +08:00
}
regArgs . Labels = strings . TrimSpace ( regArgs . Labels )
if regArgs . Labels != "" {
inputs . CustomLabels = strings . Split ( regArgs . Labels , "," )
2022-11-17 19:43:26 +08:00
}
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 nil
}
if err := doRegister ( & cfg , inputs ) ; err != nil {
log . Errorf ( "Failed to register runner: %v" , err )
return err
}
log . Infof ( "Runner registered successfully." )
return nil
}
2022-11-15 22:42:41 +08:00
func doRegister ( cfg * config . Config , inputs * registerInputs ) error {
ctx := context . Background ( )
// initial http client
cli := client . New (
inputs . InstanceAddr ,
client . WithSkipVerify ( cfg . Client . SkipVerify ) ,
client . WithGRPC ( cfg . Client . GRPC ) ,
client . WithGRPCWeb ( cfg . Client . GRPCWeb ) ,
)
for {
_ , err := cli . Ping ( ctx , connect . NewRequest ( & pingv1 . PingRequest {
Data : inputs . RunnerName ,
} ) )
select {
case <- ctx . Done ( ) :
return nil
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
}
}
register := register . New (
cli ,
& client . Filter {
OS : cfg . Platform . OS ,
Arch : cfg . Platform . Arch ,
Labels : inputs . CustomLabels ,
} ,
)
cfg . Runner . Name = inputs . RunnerName
cfg . Runner . Token = inputs . Token
_ , err := register . Register ( ctx , cfg . Runner )
if err != nil {
log . WithError ( err ) . Errorln ( "Cannot register the runner" )
return nil
}
return nil
}