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:
parent
028451bf22
commit
c817236aa4
18 changed files with 376 additions and 274 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@ forgejo-runner
|
|||
.runner
|
||||
coverage.txt
|
||||
/gitea-vet
|
||||
/config.yaml
|
||||
|
||||
# MS VSCode
|
||||
.vscode
|
||||
|
|
|
@ -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 dir == "" {
|
||||
if home, err := os.UserHomeDir(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
dir = filepath.Join(home, ".cache/actcache")
|
||||
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
|
||||
}
|
||||
|
|
10
client/header.go
Normal file
10
client/header.go
Normal 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"
|
||||
)
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
"codeberg.org/forgejo/runner/core"
|
||||
"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 {
|
||||
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)
|
||||
}
|
||||
|
|
31
cmd/cmd.go
31
cmd/cmd.go
|
@ -1,24 +1,20 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"codeberg.org/forgejo/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\"",
|
||||
|
@ -26,9 +22,9 @@ func Execute(ctx context.Context) {
|
|||
Args: cobra.MaximumNArgs(1),
|
||||
Version: version,
|
||||
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
|
||||
var regArgs registerArgs
|
||||
|
@ -36,11 +32,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", "", "Forgejo 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")
|
||||
|
@ -51,13 +46,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
|
||||
|
||||
|
|
|
@ -2,9 +2,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"
|
||||
|
@ -18,22 +18,28 @@ import (
|
|||
"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 {
|
||||
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
|
||||
|
@ -52,40 +58,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 {
|
||||
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 {
|
||||
|
@ -105,17 +111,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 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)
|
||||
}
|
||||
if cfg.Trace {
|
||||
log.SetLevel(log.TraceLevel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -14,21 +14,20 @@ import (
|
|||
"time"
|
||||
|
||||
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/config"
|
||||
"codeberg.org/forgejo/runner/register"
|
||||
"codeberg.org/forgejo/runner/runtime"
|
||||
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// 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())
|
||||
|
@ -48,14 +47,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)
|
||||
|
@ -74,7 +72,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
|
||||
|
@ -102,7 +99,6 @@ var defaultLabels = []string{
|
|||
|
||||
type registerInputs struct {
|
||||
InstanceAddr string
|
||||
Insecure bool
|
||||
Token string
|
||||
RunnerName string
|
||||
CustomLabels []string
|
||||
|
@ -174,16 +170,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
|
||||
}
|
||||
|
@ -199,7 +196,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.")
|
||||
|
@ -236,12 +233,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,
|
||||
|
@ -258,7 +256,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
|
||||
}
|
||||
|
@ -272,7 +270,7 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
|||
// initial http client
|
||||
cli := client.New(
|
||||
inputs.InstanceAddr,
|
||||
inputs.Insecure,
|
||||
cfg.Runner.Insecure,
|
||||
"",
|
||||
"",
|
||||
version,
|
||||
|
@ -301,9 +299,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)
|
||||
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
|
||||
}
|
||||
|
|
38
config/config.example.yaml
Normal file
38
config/config.example.yaml
Normal 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
|
167
config/config.go
167
config/config.go
|
@ -1,115 +1,84 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/forgejo/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:"FORGEJO_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
|
||||
}
|
||||
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.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
62
config/deprecated.go
Normal 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
9
config/embed.go
Normal 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
54
config/registration.go
Normal 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(®); 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)
|
||||
}
|
|
@ -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
3
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -7,22 +7,23 @@ import (
|
|||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"codeberg.org/forgejo/runner/client"
|
||||
|
||||
"github.com/bufbuild/connect-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"codeberg.org/forgejo/runner/client"
|
||||
"codeberg.org/forgejo/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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,13 +35,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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
return p.Dispatch(runCtx, task)
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Reference in a new issue