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 <xiaolunwen@gmail.com>
This commit is contained in:
Jason Song 2023-04-02 22:41:48 +08:00 committed by Earl Warren
parent 028451bf22
commit c817236aa4
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
18 changed files with 376 additions and 274 deletions

1
.gitignore vendored
View file

@ -4,6 +4,7 @@ forgejo-runner
.runner .runner
coverage.txt coverage.txt
/gitea-vet /gitea-vet
/config.yaml
# MS VSCode # MS VSCode
.vscode .vscode

View file

@ -42,14 +42,15 @@ type Handler struct {
outboundIP string outboundIP string
} }
func NewHandler() (*Handler, error) { func NewHandler(dir, outboundIP string, port uint16) (*Handler, error) {
h := &Handler{} h := &Handler{}
dir := "" // TODO: make the dir configurable if necessary if dir == "" {
if home, err := os.UserHomeDir(); err != nil { if home, err := os.UserHomeDir(); err != nil {
return nil, err return nil, err
} else { } else {
dir = filepath.Join(home, ".cache/actcache") dir = filepath.Join(home, ".cache", "actcache")
}
} }
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err return nil, err
@ -70,7 +71,9 @@ func NewHandler() (*Handler, error) {
} }
h.storage = storage h.storage = storage
if ip, err := getOutboundIP(); err != nil { if outboundIP != "" {
h.outboundIP = outboundIP
} else if ip, err := getOutboundIP(); err != nil {
return nil, err return nil, err
} else { } else {
h.outboundIP = ip.String() h.outboundIP = ip.String()
@ -102,8 +105,7 @@ func NewHandler() (*Handler, error) {
h.gcCache() h.gcCache()
// TODO: make the port configurable if necessary listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
listener, err := net.Listen("tcp", ":0") // random available port
if err != nil { if err != nil {
return nil, err return nil, err
} }

10
client/header.go Normal file
View file

@ -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"
)

View file

@ -8,7 +8,6 @@ import (
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect" "code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
"codeberg.org/forgejo/runner/core"
"github.com/bufbuild/connect-go" "github.com/bufbuild/connect-go"
) )
@ -32,13 +31,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 { opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
if uuid != "" { if uuid != "" {
req.Header().Set(core.UUIDHeader, uuid) req.Header().Set(UUIDHeader, uuid)
} }
if token != "" { if token != "" {
req.Header().Set(core.TokenHeader, token) req.Header().Set(TokenHeader, token)
} }
if runnerVersion != "" { if runnerVersion != "" {
req.Header().Set(core.VersionHeader, runnerVersion) req.Header().Set(VersionHeader, runnerVersion)
} }
return next(ctx, req) return next(ctx, req)
} }

View file

@ -1,24 +1,20 @@
// SPDX-License-Identifier: MIT
package cmd package cmd
import ( import (
"context" "context"
"fmt"
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"codeberg.org/forgejo/runner/config"
) )
// the version of act_runner // the version of act_runner
var version = "develop" var version = "develop"
type globalArgs struct {
EnvFile string
}
func Execute(ctx context.Context) { func Execute(ctx context.Context) {
// task := runtime.NewTask("gitea", 0, nil, nil)
var gArgs globalArgs
// ./act_runner // ./act_runner
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"", Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
@ -26,9 +22,9 @@ func Execute(ctx context.Context) {
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
Version: version, Version: version,
SilenceUsage: true, SilenceUsage: true,
RunE: runDaemon(ctx, gArgs.EnvFile),
} }
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 // ./act_runner register
var regArgs registerArgs var regArgs registerArgs
@ -36,11 +32,10 @@ func Execute(ctx context.Context) {
Use: "register", Use: "register",
Short: "Register a runner to the server", Short: "Register a runner to the server",
Args: cobra.MaximumNArgs(0), Args: cobra.MaximumNArgs(0),
RunE: runRegister(ctx, &regArgs, gArgs.EnvFile), // must use a pointer to regArgs RunE: runRegister(ctx, &regArgs, &configFile), // must use a pointer to regArgs
} }
registerCmd.Flags().BoolVar(&regArgs.NoInteractive, "no-interactive", false, "Disable interactive mode") registerCmd.Flags().BoolVar(&regArgs.NoInteractive, "no-interactive", false, "Disable interactive mode")
registerCmd.Flags().StringVar(&regArgs.InstanceAddr, "instance", "", "Forgejo instance address") registerCmd.Flags().StringVar(&regArgs.InstanceAddr, "instance", "", "Forgejo instance address")
registerCmd.Flags().BoolVar(&regArgs.Insecure, "insecure", false, "If check server's certificate if it's https protocol")
registerCmd.Flags().StringVar(&regArgs.Token, "token", "", "Runner token") registerCmd.Flags().StringVar(&regArgs.Token, "token", "", "Runner token")
registerCmd.Flags().StringVar(&regArgs.RunnerName, "name", "", "Runner name") registerCmd.Flags().StringVar(&regArgs.RunnerName, "name", "", "Runner name")
registerCmd.Flags().StringVar(&regArgs.Labels, "labels", "", "Runner tags, comma separated") registerCmd.Flags().StringVar(&regArgs.Labels, "labels", "", "Runner tags, comma separated")
@ -51,13 +46,23 @@ func Execute(ctx context.Context) {
Use: "daemon", Use: "daemon",
Short: "Run as a runner daemon", Short: "Run as a runner daemon",
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: runDaemon(ctx, gArgs.EnvFile), RunE: runDaemon(ctx, &configFile),
} }
rootCmd.AddCommand(daemonCmd) rootCmd.AddCommand(daemonCmd)
// ./act_runner exec // ./act_runner exec
rootCmd.AddCommand(loadExecCmd(ctx)) 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 // hide completion command
rootCmd.CompletionOptions.HiddenDefaultCmd = true rootCmd.CompletionOptions.HiddenDefaultCmd = true

View file

@ -2,9 +2,9 @@ package cmd
import ( import (
"context" "context"
"fmt"
"os" "os"
"github.com/joho/godotenv"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -18,22 +18,28 @@ import (
"codeberg.org/forgejo/runner/runtime" "codeberg.org/forgejo/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 { return func(cmd *cobra.Command, args []string) error {
log.Infoln("Starting runner daemon") log.Infoln("Starting runner daemon")
_ = godotenv.Load(envFile) cfg, err := config.LoadDefault(*configFile)
cfg, err := config.FromEnviron()
if err != nil { if err != nil {
log.WithError(err). return fmt.Errorf("invalid configuration: %w", err)
Fatalln("invalid configuration")
} }
initLogging(cfg) 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 // require docker if a runner label uses a docker backend
needsDocker := false needsDocker := false
for _, l := range cfg.Runner.Labels { for _, l := range reg.Labels {
_, schema, _, _ := runtime.ParseLabel(l) _, schema, _, _ := runtime.ParseLabel(l)
if schema == "docker" { if schema == "docker" {
needsDocker = true needsDocker = true
@ -52,40 +58,40 @@ func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, arg
var g errgroup.Group var g errgroup.Group
cli := client.New( cli := client.New(
cfg.Client.Address, reg.Address,
cfg.Client.Insecure, cfg.Runner.Insecure,
cfg.Runner.UUID, reg.UUID,
cfg.Runner.Token, reg.Token,
version, version,
) )
runner := &runtime.Runner{ runner := &runtime.Runner{
Client: cli, Client: cli,
Machine: cfg.Runner.Name, Machine: reg.Name,
ForgeInstance: cfg.Client.Address, ForgeInstance: reg.Address,
Environ: cfg.Runner.Environ, Environ: cfg.Runner.Envs,
Labels: cfg.Runner.Labels, Labels: reg.Labels,
Version: version, Version: version,
} }
if handler, err := artifactcache.NewHandler(); err != nil { 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) log.Errorf("cannot init cache server, it will be disabled: %v", err)
} else { } else {
log.Infof("cache handler listens on: %v", handler.ExternalURL()) log.Infof("cache handler listens on: %v", handler.ExternalURL())
runner.CacheHandler = handler runner.CacheHandler = handler
} }
}
poller := poller.New( poller := poller.New(
cli, cli,
runner.Run, runner.Run,
cfg.Runner.Capacity, cfg,
) )
g.Go(func() error { g.Go(func() error {
l := log.WithField("capacity", cfg.Runner.Capacity). l := log.WithField("capacity", cfg.Runner.Capacity).
WithField("endpoint", cfg.Client.Address). WithField("endpoint", reg.Address)
WithField("os", cfg.Platform.OS).
WithField("arch", cfg.Platform.Arch)
l.Infoln("polling the remote server") l.Infoln("polling the remote server")
if err := poller.Poll(ctx); err != nil { if err := poller.Poll(ctx); err != nil {
@ -105,17 +111,22 @@ func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, arg
} }
// initLogging setup the global logrus logger. // initLogging setup the global logrus logger.
func initLogging(cfg config.Config) { func initLogging(cfg *config.Config) {
isTerm := isatty.IsTerminal(os.Stdout.Fd()) isTerm := isatty.IsTerminal(os.Stdout.Fd())
log.SetFormatter(&log.TextFormatter{ log.SetFormatter(&log.TextFormatter{
DisableColors: !isTerm, DisableColors: !isTerm,
FullTimestamp: true, FullTimestamp: true,
}) })
if cfg.Debug { if l := cfg.Log.Level; l != "" {
log.SetLevel(log.DebugLevel) 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)
} }
if cfg.Trace {
log.SetLevel(log.TraceLevel)
} }
} }

View file

@ -348,7 +348,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
} }
// init a cache server // init a cache server
handler, err := artifactcache.NewHandler() handler, err := artifactcache.NewHandler("", "", 0)
if err != nil { if err != nil {
return err return err
} }

View file

@ -14,21 +14,20 @@ import (
"time" "time"
pingv1 "code.gitea.io/actions-proto-go/ping/v1" pingv1 "code.gitea.io/actions-proto-go/ping/v1"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"codeberg.org/forgejo/runner/client" "codeberg.org/forgejo/runner/client"
"codeberg.org/forgejo/runner/config" "codeberg.org/forgejo/runner/config"
"codeberg.org/forgejo/runner/register"
"codeberg.org/forgejo/runner/runtime" "codeberg.org/forgejo/runner/runtime"
"github.com/bufbuild/connect-go" "github.com/bufbuild/connect-go"
"github.com/joho/godotenv"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// runRegister registers a runner to the server // 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 { return func(cmd *cobra.Command, args []string) error {
log.SetReportCaller(false) log.SetReportCaller(false)
isTerm := isatty.IsTerminal(os.Stdout.Fd()) isTerm := isatty.IsTerminal(os.Stdout.Fd())
@ -48,14 +47,13 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun
} }
if regArgs.NoInteractive { if regArgs.NoInteractive {
if err := registerNoInteractive(envFile, regArgs); err != nil { if err := registerNoInteractive(*configFile, regArgs); err != nil {
return err return err
} }
} else { } else {
go func() { go func() {
if err := registerInteractive(envFile); err != nil { if err := registerInteractive(*configFile); err != nil {
// log.Errorln(err) log.Fatal(err)
os.Exit(2)
return return
} }
os.Exit(0) os.Exit(0)
@ -74,7 +72,6 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun
type registerArgs struct { type registerArgs struct {
NoInteractive bool NoInteractive bool
InstanceAddr string InstanceAddr string
Insecure bool
Token string Token string
RunnerName string RunnerName string
Labels string Labels string
@ -102,7 +99,6 @@ var defaultLabels = []string{
type registerInputs struct { type registerInputs struct {
InstanceAddr string InstanceAddr string
Insecure bool
Token string Token string
RunnerName string RunnerName string
CustomLabels []string CustomLabels []string
@ -174,16 +170,17 @@ func (r *registerInputs) assignToNext(stage registerStage, value string) registe
return StageUnknown return StageUnknown
} }
func registerInteractive(envFile string) error { func registerInteractive(configFile string) error {
var ( var (
reader = bufio.NewReader(os.Stdin) reader = bufio.NewReader(os.Stdin)
stage = StageInputInstance stage = StageInputInstance
inputs = new(registerInputs) inputs = new(registerInputs)
) )
// check if overwrite local config cfg, err := config.LoadDefault(configFile)
_ = godotenv.Load(envFile) if err != nil {
cfg, _ := config.FromEnviron() return fmt.Errorf("failed to load config: %v", err)
}
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() { if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
stage = StageOverwriteLocalConfig stage = StageOverwriteLocalConfig
} }
@ -199,7 +196,7 @@ func registerInteractive(envFile string) error {
if stage == StageWaitingForRegistration { if stage == StageWaitingForRegistration {
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels) 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) log.Errorf("Failed to register runner: %v", err)
} else { } else {
log.Infof("Runner registered successfully.") log.Infof("Runner registered successfully.")
@ -236,12 +233,13 @@ func printStageHelp(stage registerStage) {
} }
} }
func registerNoInteractive(envFile string, regArgs *registerArgs) error { func registerNoInteractive(configFile string, regArgs *registerArgs) error {
_ = godotenv.Load(envFile) cfg, err := config.LoadDefault(configFile)
cfg, _ := config.FromEnviron() if err != nil {
return err
}
inputs := &registerInputs{ inputs := &registerInputs{
InstanceAddr: regArgs.InstanceAddr, InstanceAddr: regArgs.InstanceAddr,
Insecure: regArgs.Insecure,
Token: regArgs.Token, Token: regArgs.Token,
RunnerName: regArgs.RunnerName, RunnerName: regArgs.RunnerName,
CustomLabels: defaultLabels, CustomLabels: defaultLabels,
@ -258,7 +256,7 @@ func registerNoInteractive(envFile string, regArgs *registerArgs) error {
log.WithError(err).Errorf("Invalid input, please re-run act command.") log.WithError(err).Errorf("Invalid input, please re-run act command.")
return nil return nil
} }
if err := doRegister(&cfg, inputs); err != nil { if err := doRegister(cfg, inputs); err != nil {
log.Errorf("Failed to register runner: %v", err) log.Errorf("Failed to register runner: %v", err)
return nil return nil
} }
@ -272,7 +270,7 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
// initial http client // initial http client
cli := client.New( cli := client.New(
inputs.InstanceAddr, inputs.InstanceAddr,
inputs.Insecure, cfg.Runner.Insecure,
"", "",
"", "",
version, version,
@ -301,9 +299,36 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
} }
} }
cfg.Runner.Name = inputs.RunnerName reg := &config.Registration{
cfg.Runner.Token = inputs.Token Name: inputs.RunnerName,
cfg.Runner.Labels = inputs.CustomLabels Token: inputs.Token,
_, err := register.New(cli).Register(ctx, cfg.Runner) 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 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
}

View file

@ -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

View file

@ -1,115 +1,84 @@
package config package config
import ( import (
"encoding/json" "fmt"
"io"
"os" "os"
"runtime" "path/filepath"
"strconv" "time"
"codeberg.org/forgejo/runner/core"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig" "gopkg.in/yaml.v3"
) )
type ( type Config struct {
// Config provides the system configuration. Log struct {
Config struct { Level string `yaml:"level"`
Debug bool `envconfig:"GITEA_DEBUG"` } `yaml:"log"`
Trace bool `envconfig:"GITEA_TRACE"`
Client Client
Runner Runner
Platform Platform
}
Client struct {
Address string `ignored:"true"`
Insecure bool
}
Runner struct { Runner struct {
UUID string `ignored:"true"` File string `yaml:"file"`
Name string `envconfig:"GITEA_RUNNER_NAME"` Capacity int `yaml:"capacity"`
Token string `ignored:"true"` Envs map[string]string `yaml:"envs"`
Capacity int `envconfig:"GITEA_RUNNER_CAPACITY" default:"1"` EnvFile string `yaml:"env_file"`
File string `envconfig:"FORGEJO_RUNNER_FILE" default:".runner"` Timeout time.Duration `yaml:"timeout"`
Environ map[string]string `envconfig:"GITEA_RUNNER_ENVIRON"` Insecure bool `yaml:"insecure"`
EnvFile string `envconfig:"GITEA_RUNNER_ENV_FILE"` } `yaml:"runner"`
Labels []string `envconfig:"GITEA_RUNNER_LABELS"` 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 { // LoadDefault returns the default configuration.
OS string `envconfig:"GITEA_PLATFORM_OS"` // If file is not empty, it will be used to load the configuration.
Arch string `envconfig:"GITEA_PLATFORM_ARCH"` func LoadDefault(file string) (*Config, error) {
} cfg := &Config{}
) if file != "" {
f, err := os.Open(file)
// 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)
if err != nil { if err != nil {
return cfg, err return nil, err
}
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 { for k, v := range envs {
cfg.Runner.Environ[k] = v 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")
} }
} }

62
config/deprecated.go Normal file
View file

@ -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
}
}

9
config/embed.go Normal file
View file

@ -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

54
config/registration.go Normal file
View file

@ -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(&reg); err != nil {
return nil, err
}
reg.Warning = ""
return &reg, 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)
}

View file

@ -1,18 +0,0 @@
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"`
}

3
go.mod
View file

@ -11,7 +11,6 @@ require (
github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/render v1.0.2 github.com/go-chi/render v1.0.2
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-isatty v0.0.17
github.com/nektos/act v0.0.0 github.com/nektos/act v0.0.0
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
@ -19,6 +18,7 @@ require (
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0
golang.org/x/term v0.6.0 golang.org/x/term v0.6.0
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.14.2 modernc.org/sqlite v1.14.2
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978
xorm.io/xorm v1.3.2 xorm.io/xorm v1.3.2
@ -91,7 +91,6 @@ require (
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.1.1 // indirect lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.35.18 // indirect modernc.org/cc/v3 v3.35.18 // indirect
modernc.org/ccgo/v3 v3.12.82 // indirect modernc.org/ccgo/v3 v3.12.82 // indirect

2
go.sum
View file

@ -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/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 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 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 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 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=

View file

@ -7,22 +7,23 @@ import (
"time" "time"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1" runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"codeberg.org/forgejo/runner/client"
"github.com/bufbuild/connect-go" "github.com/bufbuild/connect-go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"codeberg.org/forgejo/runner/client"
"codeberg.org/forgejo/runner/config"
) )
var ErrDataLock = errors.New("Data Lock Error") 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{ return &Poller{
Client: cli, Client: cli,
Dispatch: dispatch, Dispatch: dispatch,
routineGroup: newRoutineGroup(), routineGroup: newRoutineGroup(),
metric: &metric{}, metric: &metric{},
workerNum: workerNum,
ready: make(chan struct{}, 1), ready: make(chan struct{}, 1),
cfg: cfg,
} }
} }
@ -34,13 +35,13 @@ type Poller struct {
routineGroup *routineGroup routineGroup *routineGroup
metric *metric metric *metric
ready chan struct{} ready chan struct{}
workerNum int cfg *config.Config
} }
func (p *Poller) schedule() { func (p *Poller) schedule() {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
if int(p.metric.BusyWorkers()) >= p.workerNum { if int(p.metric.BusyWorkers()) >= p.cfg.Runner.Capacity {
return return
} }
@ -148,7 +149,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() defer cancel()
return p.Dispatch(runCtx, task) return p.Dispatch(runCtx, task)

View file

@ -1,63 +0,0 @@
package register
import (
"context"
"encoding/json"
"os"
"strconv"
"strings"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"codeberg.org/forgejo/runner/client"
"codeberg.org/forgejo/runner/config"
"codeberg.org/forgejo/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)
}