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

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

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