From 7e7096e60b34cba3081ac4603c1e19dbc8d2d4a8 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Sun, 2 Apr 2023 22:41:48 +0800 Subject: [PATCH] Refactor environment variables to configuration and registration (#90) Close #21. Refactor environment variables to configuration file (config.yaml) and registration file (.runner). The old environment variables are still supported, but warning logs will be printed. Like: ```text $ GITEA_DEBUG=true ./act_runner -c config.yaml daemon INFO[0000] Starting runner daemon WARN[0000] env GITEA_DEBUG has been ignored because config file is used $ GITEA_DEBUG=true ./act_runner daemon INFO[0000] Starting runner daemon WARN[0000] env GITEA_DEBUG will be deprecated, please use config file instead ``` Reviewed-on: https://gitea.com/gitea/act_runner/pulls/90 Reviewed-by: Lunny Xiao --- .gitignore | 1 + .goreleaser.yaml | 2 +- README.md | 17 ++++ artifactcache/handler.go | 20 +++-- client/header.go | 10 +++ client/http.go | 7 +- cmd/cmd.go | 29 ++++--- cmd/daemon.go | 71 +++++++++------- cmd/exec.go | 2 +- cmd/register.go | 75 ++++++++++------ config/config.example.yaml | 38 +++++++++ config/config.go | 169 +++++++++++++++---------------------- config/deprecated.go | 62 ++++++++++++++ config/embed.go | 9 ++ config/registration.go | 54 ++++++++++++ core/runner.go | 21 ----- go.mod | 3 +- go.sum | 2 - poller/poller.go | 15 ++-- register/register.go | 66 --------------- 20 files changed, 393 insertions(+), 280 deletions(-) create mode 100644 client/header.go create mode 100644 config/config.example.yaml create mode 100644 config/deprecated.go create mode 100644 config/embed.go create mode 100644 config/registration.go delete mode 100644 core/runner.go delete mode 100644 register/register.go diff --git a/.gitignore b/.gitignore index 4e43895..d9d87a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ act_runner .runner coverage.txt /gitea-vet +/config.yaml # MS VSCode .vscode diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2533b2f..59eb868 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -58,7 +58,7 @@ builds: flags: - -trimpath ldflags: - - -s -w -X gitea.com/gitea/act_runner/cmd.version={{ .Version }} + - -s -w -X gitea.com/gitea/act_runner/cmd.version={{ .Summary }} binary: >- {{ .ProjectName }}- {{- .Version }}- diff --git a/README.md b/README.md index 4670ff3..bc4578a 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,20 @@ If the registry succeed, it will run immediately. Next time, you could run the r ```bash ./act_runner daemon ``` + + +### Configuration + +You can also configure the runner with a configuration file. +The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`. + +```bash +./act_runner generate-config > config.yaml +``` + +You can specify the configuration file path with `-c`/`--config` argument. + +```bash +./act_runner -c config.yaml register # register with config file +./act_runner -c config.yaml deamon # run with config file +``` diff --git a/artifactcache/handler.go b/artifactcache/handler.go index d29a7ce..86e7aa5 100644 --- a/artifactcache/handler.go +++ b/artifactcache/handler.go @@ -42,14 +42,15 @@ type Handler struct { outboundIP string } -func NewHandler() (*Handler, error) { +func NewHandler(dir, outboundIP string, port uint16) (*Handler, error) { h := &Handler{} - dir := "" // TODO: make the dir configurable if necessary - if home, err := os.UserHomeDir(); err != nil { - return nil, err - } else { - dir = filepath.Join(home, ".cache/actcache") + if dir == "" { + if home, err := os.UserHomeDir(); err != nil { + return nil, err + } else { + dir = filepath.Join(home, ".cache", "actcache") + } } if err := os.MkdirAll(dir, 0o755); err != nil { return nil, err @@ -70,7 +71,9 @@ func NewHandler() (*Handler, error) { } h.storage = storage - if ip, err := getOutboundIP(); err != nil { + if outboundIP != "" { + h.outboundIP = outboundIP + } else if ip, err := getOutboundIP(); err != nil { return nil, err } else { h.outboundIP = ip.String() @@ -102,8 +105,7 @@ func NewHandler() (*Handler, error) { h.gcCache() - // TODO: make the port configurable if necessary - listener, err := net.Listen("tcp", ":0") // random available port + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces if err != nil { return nil, err } diff --git a/client/header.go b/client/header.go new file mode 100644 index 0000000..df8627a --- /dev/null +++ b/client/header.go @@ -0,0 +1,10 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package client + +const ( + UUIDHeader = "x-runner-uuid" + TokenHeader = "x-runner-token" + VersionHeader = "x-runner-version" +) diff --git a/client/http.go b/client/http.go index b67c1de..fc374f4 100644 --- a/client/http.go +++ b/client/http.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" - "gitea.com/gitea/act_runner/core" "github.com/bufbuild/connect-go" ) @@ -35,13 +34,13 @@ func New(endpoint string, insecure bool, uuid, token, runnerVersion string, opts opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { if uuid != "" { - req.Header().Set(core.UUIDHeader, uuid) + req.Header().Set(UUIDHeader, uuid) } if token != "" { - req.Header().Set(core.TokenHeader, token) + req.Header().Set(TokenHeader, token) } if runnerVersion != "" { - req.Header().Set(core.VersionHeader, runnerVersion) + req.Header().Set(VersionHeader, runnerVersion) } return next(ctx, req) } diff --git a/cmd/cmd.go b/cmd/cmd.go index 5c870e7..375fc32 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -5,23 +5,18 @@ package cmd import ( "context" + "fmt" "os" "github.com/spf13/cobra" + + "gitea.com/gitea/act_runner/config" ) // the version of act_runner var version = "develop" -type globalArgs struct { - EnvFile string -} - func Execute(ctx context.Context) { - // task := runtime.NewTask("gitea", 0, nil, nil) - - var gArgs globalArgs - // ./act_runner rootCmd := &cobra.Command{ Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"", @@ -30,7 +25,8 @@ func Execute(ctx context.Context) { Version: version, SilenceUsage: true, } - rootCmd.PersistentFlags().StringVarP(&gArgs.EnvFile, "env-file", "", ".env", "Read in a file of environment variables.") + configFile := "" + rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path") // ./act_runner register var regArgs registerArgs @@ -38,11 +34,10 @@ func Execute(ctx context.Context) { Use: "register", Short: "Register a runner to the server", Args: cobra.MaximumNArgs(0), - RunE: runRegister(ctx, ®Args, gArgs.EnvFile), // must use a pointer to regArgs + RunE: runRegister(ctx, ®Args, &configFile), // must use a pointer to regArgs } registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode") registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address") - registerCmd.Flags().BoolVar(®Args.Insecure, "insecure", false, "If check server's certificate if it's https protocol") registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token") registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name") registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated") @@ -53,13 +48,23 @@ func Execute(ctx context.Context) { Use: "daemon", Short: "Run as a runner daemon", Args: cobra.MaximumNArgs(1), - RunE: runDaemon(ctx, gArgs.EnvFile), + RunE: runDaemon(ctx, &configFile), } rootCmd.AddCommand(daemonCmd) // ./act_runner exec rootCmd.AddCommand(loadExecCmd(ctx)) + // ./act_runner config + rootCmd.AddCommand(&cobra.Command{ + Use: "generate-config", + Short: "Generate an example config file", + Args: cobra.MaximumNArgs(0), + Run: func(_ *cobra.Command, _ []string) { + fmt.Printf("%s", config.Example) + }, + }) + // hide completion command rootCmd.CompletionOptions.HiddenDefaultCmd = true diff --git a/cmd/daemon.go b/cmd/daemon.go index f24cd6c..b0cf3e7 100644 --- a/cmd/daemon.go +++ b/cmd/daemon.go @@ -5,9 +5,9 @@ package cmd import ( "context" + "fmt" "os" - "github.com/joho/godotenv" "github.com/mattn/go-isatty" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -21,22 +21,28 @@ import ( "gitea.com/gitea/act_runner/runtime" ) -func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, args []string) error { +func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { log.Infoln("Starting runner daemon") - _ = godotenv.Load(envFile) - cfg, err := config.FromEnviron() + cfg, err := config.LoadDefault(*configFile) if err != nil { - log.WithError(err). - Fatalln("invalid configuration") + return fmt.Errorf("invalid configuration: %w", err) } initLogging(cfg) + reg, err := config.LoadRegistration(cfg.Runner.File) + if os.IsNotExist(err) { + log.Error("registration file not found, please register the runner first") + return err + } else if err != nil { + return fmt.Errorf("failed to load registration file: %w", err) + } + // require docker if a runner label uses a docker backend needsDocker := false - for _, l := range cfg.Runner.Labels { + for _, l := range reg.Labels { _, schema, _, _ := runtime.ParseLabel(l) if schema == "docker" { needsDocker = true @@ -55,40 +61,40 @@ func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, arg var g errgroup.Group cli := client.New( - cfg.Client.Address, - cfg.Client.Insecure, - cfg.Runner.UUID, - cfg.Runner.Token, + reg.Address, + cfg.Runner.Insecure, + reg.UUID, + reg.Token, version, ) runner := &runtime.Runner{ Client: cli, - Machine: cfg.Runner.Name, - ForgeInstance: cfg.Client.Address, - Environ: cfg.Runner.Environ, - Labels: cfg.Runner.Labels, + Machine: reg.Name, + ForgeInstance: reg.Address, + Environ: cfg.Runner.Envs, + Labels: reg.Labels, Version: version, } - if handler, err := artifactcache.NewHandler(); err != nil { - log.Errorf("cannot init cache server, it will be disabled: %v", err) - } else { - log.Infof("cache handler listens on: %v", handler.ExternalURL()) - runner.CacheHandler = handler + if *cfg.Cache.Enabled { + if handler, err := artifactcache.NewHandler(cfg.Cache.Dir, cfg.Cache.Host, cfg.Cache.Port); err != nil { + log.Errorf("cannot init cache server, it will be disabled: %v", err) + } else { + log.Infof("cache handler listens on: %v", handler.ExternalURL()) + runner.CacheHandler = handler + } } poller := poller.New( cli, runner.Run, - cfg.Runner.Capacity, + cfg, ) g.Go(func() error { l := log.WithField("capacity", cfg.Runner.Capacity). - WithField("endpoint", cfg.Client.Address). - WithField("os", cfg.Platform.OS). - WithField("arch", cfg.Platform.Arch) + WithField("endpoint", reg.Address) l.Infoln("polling the remote server") if err := poller.Poll(ctx); err != nil { @@ -108,17 +114,22 @@ func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, arg } // initLogging setup the global logrus logger. -func initLogging(cfg config.Config) { +func initLogging(cfg *config.Config) { isTerm := isatty.IsTerminal(os.Stdout.Fd()) log.SetFormatter(&log.TextFormatter{ DisableColors: !isTerm, FullTimestamp: true, }) - if cfg.Debug { - log.SetLevel(log.DebugLevel) - } - if cfg.Trace { - log.SetLevel(log.TraceLevel) + if l := cfg.Log.Level; l != "" { + level, err := log.ParseLevel(l) + if err != nil { + log.WithError(err). + Errorf("invalid log level: %q", l) + } + if log.GetLevel() != level { + log.Infof("log level changed to %v", level) + log.SetLevel(level) + } } } diff --git a/cmd/exec.go b/cmd/exec.go index c4c3eb5..a3b5b00 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -348,7 +348,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command } // init a cache server - handler, err := artifactcache.NewHandler() + handler, err := artifactcache.NewHandler("", "", 0) if err != nil { return err } diff --git a/cmd/register.go b/cmd/register.go index 5d0b4e2..ef06a31 100644 --- a/cmd/register.go +++ b/cmd/register.go @@ -14,20 +14,19 @@ import ( "time" pingv1 "code.gitea.io/actions-proto-go/ping/v1" + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "github.com/bufbuild/connect-go" - "github.com/joho/godotenv" "github.com/mattn/go-isatty" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "gitea.com/gitea/act_runner/client" "gitea.com/gitea/act_runner/config" - "gitea.com/gitea/act_runner/register" "gitea.com/gitea/act_runner/runtime" ) // runRegister registers a runner to the server -func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) func(*cobra.Command, []string) error { +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()) @@ -47,14 +46,13 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun } if regArgs.NoInteractive { - if err := registerNoInteractive(envFile, regArgs); err != nil { + if err := registerNoInteractive(*configFile, regArgs); err != nil { return err } } else { go func() { - if err := registerInteractive(envFile); err != nil { - // log.Errorln(err) - os.Exit(2) + if err := registerInteractive(*configFile); err != nil { + log.Fatal(err) return } os.Exit(0) @@ -73,7 +71,6 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun type registerArgs struct { NoInteractive bool InstanceAddr string - Insecure bool Token string RunnerName string Labels string @@ -101,7 +98,6 @@ var defaultLabels = []string{ type registerInputs struct { InstanceAddr string - Insecure bool Token string RunnerName string CustomLabels []string @@ -173,16 +169,17 @@ func (r *registerInputs) assignToNext(stage registerStage, value string) registe return StageUnknown } -func registerInteractive(envFile string) error { +func registerInteractive(configFile string) error { var ( reader = bufio.NewReader(os.Stdin) stage = StageInputInstance inputs = new(registerInputs) ) - // check if overwrite local config - _ = godotenv.Load(envFile) - cfg, _ := config.FromEnviron() + 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 } @@ -198,7 +195,7 @@ func registerInteractive(envFile string) error { if stage == StageWaitingForRegistration { log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels) - if err := doRegister(&cfg, inputs); err != nil { + if err := doRegister(cfg, inputs); err != nil { log.Errorf("Failed to register runner: %v", err) } else { log.Infof("Runner registered successfully.") @@ -235,12 +232,13 @@ func printStageHelp(stage registerStage) { } } -func registerNoInteractive(envFile string, regArgs *registerArgs) error { - _ = godotenv.Load(envFile) - cfg, _ := config.FromEnviron() +func registerNoInteractive(configFile string, regArgs *registerArgs) error { + cfg, err := config.LoadDefault(configFile) + if err != nil { + return err + } inputs := ®isterInputs{ InstanceAddr: regArgs.InstanceAddr, - Insecure: regArgs.Insecure, Token: regArgs.Token, RunnerName: regArgs.RunnerName, CustomLabels: defaultLabels, @@ -257,7 +255,7 @@ func registerNoInteractive(envFile string, regArgs *registerArgs) error { log.WithError(err).Errorf("Invalid input, please re-run act command.") return nil } - if err := doRegister(&cfg, inputs); err != nil { + if err := doRegister(cfg, inputs); err != nil { log.Errorf("Failed to register runner: %v", err) return nil } @@ -271,7 +269,7 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error { // initial http client cli := client.New( inputs.InstanceAddr, - inputs.Insecure, + cfg.Runner.Insecure, "", "", version, @@ -300,9 +298,36 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error { } } - cfg.Runner.Name = inputs.RunnerName - cfg.Runner.Token = inputs.Token - cfg.Runner.Labels = inputs.CustomLabels - _, err := register.New(cli).Register(ctx, cfg.Runner) - return err + reg := &config.Registration{ + Name: inputs.RunnerName, + Token: inputs.Token, + Address: inputs.InstanceAddr, + Labels: inputs.CustomLabels, + } + + labels := make([]string, len(reg.Labels)) + for i, v := range reg.Labels { + l, _, _, _ := runtime.ParseLabel(v) + labels[i] = l + } + // register new runner. + resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{ + Name: reg.Name, + Token: reg.Token, + AgentLabels: labels, + })) + 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 err := config.SaveRegistration(cfg.Runner.File, reg); err != nil { + return fmt.Errorf("failed to save runner config: %w", err) + } + return nil } diff --git a/config/config.example.yaml b/config/config.example.yaml new file mode 100644 index 0000000..10f3e47 --- /dev/null +++ b/config/config.example.yaml @@ -0,0 +1,38 @@ +# Example configuration file, it's safe to copy this as the default config file without any modification. + +log: + # The level of logging, can be trace, debug, info, warn, error, fatal + level: info + +runner: + # Where to store the registration result. + file: .runner + # Execute how many tasks concurrently at the same time. + capacity: 1 + # Extra environment variables to run jobs. + envs: + A_TEST_ENV_NAME_1: a_test_env_value_1 + A_TEST_ENV_NAME_2: a_test_env_value_2 + # Extra environment variables to run jobs from a file. + # It will be ignored if it's empty or the file doesn't exist. + env_file: .env + # The timeout for a job to be finished. + # Please note that the Gitea instance also has a timeout (3h by default) for the job. + # So the job could be stopped by the Gitea instance if it's timeout is shorter than this. + timeout: 3h + # Whether skip verifying the TLS certificate of the Gitea instance. + insecure: false + +cache: + # Enable cache server to use actions/cache. + enabled: true + # The directory to store the cache data. + # If it's empty, the cache data will be stored in $HOME/.cache/actcache. + dir: "" + # The host of the cache server. + # It's not for the address to listen, but the address to connect from job containers. + # So 0.0.0.0 is a bad choice, leave it empty to detect automatically. + host: "" + # The port of the cache server. + # 0 means to use a random available port. + port: 0 diff --git a/config/config.go b/config/config.go index 61d5e50..2679b03 100644 --- a/config/config.go +++ b/config/config.go @@ -4,115 +4,84 @@ package config import ( - "encoding/json" - "io" + "fmt" "os" - "runtime" - "strconv" - - "gitea.com/gitea/act_runner/core" + "path/filepath" + "time" "github.com/joho/godotenv" - "github.com/kelseyhightower/envconfig" + "gopkg.in/yaml.v3" ) -type ( - // Config provides the system configuration. - Config struct { - Debug bool `envconfig:"GITEA_DEBUG"` - Trace bool `envconfig:"GITEA_TRACE"` - Client Client - Runner Runner - Platform Platform - } - - Client struct { - Address string `ignored:"true"` - Insecure bool - } - +type Config struct { + Log struct { + Level string `yaml:"level"` + } `yaml:"log"` Runner struct { - UUID string `ignored:"true"` - Name string `envconfig:"GITEA_RUNNER_NAME"` - Token string `ignored:"true"` - Capacity int `envconfig:"GITEA_RUNNER_CAPACITY" default:"1"` - File string `envconfig:"GITEA_RUNNER_FILE" default:".runner"` - Environ map[string]string `envconfig:"GITEA_RUNNER_ENVIRON"` - EnvFile string `envconfig:"GITEA_RUNNER_ENV_FILE"` - Labels []string `envconfig:"GITEA_RUNNER_LABELS"` - } + File string `yaml:"file"` + Capacity int `yaml:"capacity"` + Envs map[string]string `yaml:"envs"` + EnvFile string `yaml:"env_file"` + Timeout time.Duration `yaml:"timeout"` + Insecure bool `yaml:"insecure"` + } `yaml:"runner"` + Cache struct { + Enabled *bool `yaml:"enabled"` // pointer to distinguish between false and not set, and it will be true if not set + Dir string `yaml:"dir"` + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + } `yaml:"cache"` +} - Platform struct { - OS string `envconfig:"GITEA_PLATFORM_OS"` - Arch string `envconfig:"GITEA_PLATFORM_ARCH"` - } -) - -// FromEnviron returns the settings from the environment. -func FromEnviron() (Config, error) { - cfg := Config{} - if err := envconfig.Process("", &cfg); err != nil { - return cfg, err - } - - // check runner config exist - f, err := os.Stat(cfg.Runner.File) - if err == nil && !f.IsDir() { - jsonFile, _ := os.Open(cfg.Runner.File) - defer jsonFile.Close() - byteValue, _ := io.ReadAll(jsonFile) - var runner core.Runner - if err := json.Unmarshal(byteValue, &runner); err != nil { - return cfg, err - } - if runner.UUID != "" { - cfg.Runner.UUID = runner.UUID - } - if runner.Name != "" { - cfg.Runner.Name = runner.Name - } - if runner.Token != "" { - cfg.Runner.Token = runner.Token - } - if len(runner.Labels) != 0 { - cfg.Runner.Labels = runner.Labels - } - if runner.Address != "" { - cfg.Client.Address = runner.Address - } - if runner.Insecure != "" { - cfg.Client.Insecure, _ = strconv.ParseBool(runner.Insecure) - } - } else if err != nil { - return cfg, err - } - - // runner config - if cfg.Runner.Environ == nil { - cfg.Runner.Environ = map[string]string{ - "GITHUB_API_URL": cfg.Client.Address + "/api/v1", - "GITHUB_SERVER_URL": cfg.Client.Address, - } - } - if cfg.Runner.Name == "" { - cfg.Runner.Name, _ = os.Hostname() - } - - // platform config - if cfg.Platform.OS == "" { - cfg.Platform.OS = runtime.GOOS - } - if cfg.Platform.Arch == "" { - cfg.Platform.Arch = runtime.GOARCH - } - - if file := cfg.Runner.EnvFile; file != "" { - envs, err := godotenv.Read(file) +// LoadDefault returns the default configuration. +// If file is not empty, it will be used to load the configuration. +func LoadDefault(file string) (*Config, error) { + cfg := &Config{} + if file != "" { + f, err := os.Open(file) if err != nil { - return cfg, err + return nil, err } - for k, v := range envs { - cfg.Runner.Environ[k] = v + defer f.Close() + decoder := yaml.NewDecoder(f) + if err := decoder.Decode(&cfg); err != nil { + return nil, err + } + } + compatibleWithOldEnvs(file != "", cfg) + + if cfg.Runner.EnvFile != "" { + if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() { + envs, err := godotenv.Read(cfg.Runner.EnvFile) + if err != nil { + return nil, fmt.Errorf("read env file %q: %w", cfg.Runner.EnvFile, err) + } + for k, v := range envs { + cfg.Runner.Envs[k] = v + } + } + } + + if cfg.Log.Level == "" { + cfg.Log.Level = "info" + } + if cfg.Runner.File == "" { + cfg.Runner.File = ".runner" + } + if cfg.Runner.Capacity <= 0 { + cfg.Runner.Capacity = 1 + } + if cfg.Runner.Timeout <= 0 { + cfg.Runner.Timeout = 3 * time.Hour + } + if cfg.Cache.Enabled == nil { + b := true + cfg.Cache.Enabled = &b + } + if *cfg.Cache.Enabled { + if cfg.Cache.Dir == "" { + home, _ := os.UserHomeDir() + cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache") } } diff --git a/config/deprecated.go b/config/deprecated.go new file mode 100644 index 0000000..b5051aa --- /dev/null +++ b/config/deprecated.go @@ -0,0 +1,62 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package config + +import ( + "os" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released. +// Be compatible with old envs. +func compatibleWithOldEnvs(fileUsed bool, cfg *Config) { + handleEnv := func(key string) (string, bool) { + if v, ok := os.LookupEnv(key); ok { + if fileUsed { + log.Warnf("env %s has been ignored because config file is used", key) + return "", false + } + log.Warnf("env %s will be deprecated, please use config file instead", key) + return v, true + } + return "", false + } + + if v, ok := handleEnv("GITEA_DEBUG"); ok { + if b, _ := strconv.ParseBool(v); b { + cfg.Log.Level = "debug" + } + } + if v, ok := handleEnv("GITEA_TRACE"); ok { + if b, _ := strconv.ParseBool(v); b { + cfg.Log.Level = "trace" + } + } + if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok { + if i, _ := strconv.Atoi(v); i > 0 { + cfg.Runner.Capacity = i + } + } + if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok { + cfg.Runner.File = v + } + if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok { + splits := strings.Split(v, ",") + if cfg.Runner.Envs == nil { + cfg.Runner.Envs = map[string]string{} + } + for _, split := range splits { + kv := strings.SplitN(split, ":", 2) + if len(kv) == 2 && kv[0] != "" { + cfg.Runner.Envs[kv[0]] = kv[1] + } + } + } + if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok { + cfg.Runner.EnvFile = v + } +} diff --git a/config/embed.go b/config/embed.go new file mode 100644 index 0000000..cf445cf --- /dev/null +++ b/config/embed.go @@ -0,0 +1,9 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package config + +import _ "embed" + +//go:embed config.example.yaml +var Example []byte diff --git a/config/registration.go b/config/registration.go new file mode 100644 index 0000000..be66b4f --- /dev/null +++ b/config/registration.go @@ -0,0 +1,54 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package config + +import ( + "encoding/json" + "os" +) + +const registrationWarning = "This file is automatically generated by act-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner." + +// Registration is the registration information for a runner +type Registration struct { + Warning string `json:"WARNING"` // Warning message to display, it's always the registrationWarning constant + + ID int64 `json:"id"` + UUID string `json:"uuid"` + Name string `json:"name"` + Token string `json:"token"` + Address string `json:"address"` + Labels []string `json:"labels"` +} + +func LoadRegistration(file string) (*Registration, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + + var reg Registration + if err := json.NewDecoder(f).Decode(®); err != nil { + return nil, err + } + + reg.Warning = "" + + return ®, nil +} + +func SaveRegistration(file string, reg *Registration) error { + f, err := os.Create(file) + if err != nil { + return err + } + defer f.Close() + + reg.Warning = registrationWarning + + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + return enc.Encode(reg) +} diff --git a/core/runner.go b/core/runner.go deleted file mode 100644 index 6f628eb..0000000 --- a/core/runner.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package core - -const ( - UUIDHeader = "x-runner-uuid" - TokenHeader = "x-runner-token" - VersionHeader = "x-runner-version" -) - -// Runner struct -type Runner struct { - ID int64 `json:"id"` - UUID string `json:"uuid"` - Name string `json:"name"` - Token string `json:"token"` - Address string `json:"address"` - Insecure string `json:"insecure"` - Labels []string `json:"labels"` -} diff --git a/go.mod b/go.mod index 751131a..23f10bb 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/render v1.0.2 github.com/joho/godotenv v1.5.1 - github.com/kelseyhightower/envconfig v1.4.0 github.com/mattn/go-isatty v0.0.17 github.com/nektos/act v0.0.0 github.com/sirupsen/logrus v1.9.0 @@ -19,6 +18,7 @@ require ( golang.org/x/sync v0.1.0 golang.org/x/term v0.6.0 google.golang.org/protobuf v1.28.1 + gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.14.2 xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 xorm.io/xorm v1.3.2 @@ -91,7 +91,6 @@ require ( golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.1.1 // indirect modernc.org/cc/v3 v3.35.18 // indirect modernc.org/ccgo/v3 v3.12.82 // indirect diff --git a/go.sum b/go.sum index 4302171..2205b74 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,6 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= -github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= diff --git a/poller/poller.go b/poller/poller.go index 7e39f03..228f6e2 100644 --- a/poller/poller.go +++ b/poller/poller.go @@ -10,22 +10,23 @@ import ( "time" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" - "gitea.com/gitea/act_runner/client" - "github.com/bufbuild/connect-go" log "github.com/sirupsen/logrus" + + "gitea.com/gitea/act_runner/client" + "gitea.com/gitea/act_runner/config" ) var ErrDataLock = errors.New("Data Lock Error") -func New(cli client.Client, dispatch func(context.Context, *runnerv1.Task) error, workerNum int) *Poller { +func New(cli client.Client, dispatch func(context.Context, *runnerv1.Task) error, cfg *config.Config) *Poller { return &Poller{ Client: cli, Dispatch: dispatch, routineGroup: newRoutineGroup(), metric: &metric{}, - workerNum: workerNum, ready: make(chan struct{}, 1), + cfg: cfg, } } @@ -37,13 +38,13 @@ type Poller struct { routineGroup *routineGroup metric *metric ready chan struct{} - workerNum int + cfg *config.Config } func (p *Poller) schedule() { p.Lock() defer p.Unlock() - if int(p.metric.BusyWorkers()) >= p.workerNum { + if int(p.metric.BusyWorkers()) >= p.cfg.Runner.Capacity { return } @@ -151,7 +152,7 @@ func (p *Poller) dispatchTask(ctx context.Context, task *runnerv1.Task) error { } }() - runCtx, cancel := context.WithTimeout(ctx, time.Hour) + runCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.Timeout) defer cancel() return p.Dispatch(runCtx, task) diff --git a/register/register.go b/register/register.go deleted file mode 100644 index 400d281..0000000 --- a/register/register.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package register - -import ( - "context" - "encoding/json" - "os" - "strconv" - "strings" - - runnerv1 "code.gitea.io/actions-proto-go/runner/v1" - "gitea.com/gitea/act_runner/client" - "gitea.com/gitea/act_runner/config" - "gitea.com/gitea/act_runner/core" - - "github.com/bufbuild/connect-go" - log "github.com/sirupsen/logrus" -) - -func New(cli client.Client) *Register { - return &Register{ - Client: cli, - } -} - -type Register struct { - Client client.Client -} - -func (p *Register) Register(ctx context.Context, cfg config.Runner) (*core.Runner, error) { - labels := make([]string, len(cfg.Labels)) - for i, v := range cfg.Labels { - labels[i] = strings.SplitN(v, ":", 2)[0] - } - // register new runner. - resp, err := p.Client.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{ - Name: cfg.Name, - Token: cfg.Token, - AgentLabels: labels, - })) - if err != nil { - log.WithError(err).Error("poller: cannot register new runner") - return nil, err - } - - data := &core.Runner{ - ID: resp.Msg.Runner.Id, - UUID: resp.Msg.Runner.Uuid, - Name: resp.Msg.Runner.Name, - Token: resp.Msg.Runner.Token, - Address: p.Client.Address(), - Insecure: strconv.FormatBool(p.Client.Insecure()), - Labels: cfg.Labels, - } - - file, err := json.MarshalIndent(data, "", " ") - if err != nil { - log.WithError(err).Error("poller: cannot marshal the json input") - return data, err - } - - // store runner config in .runner file - return data, os.WriteFile(cfg.File, file, 0o644) -}