commands/config.go (237 lines of code) (raw):
package commands
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/helpers"
"gitlab.com/gitlab-org/gitlab-runner/network"
)
func GetDefaultConfigFile() string {
return filepath.Join(getDefaultConfigDirectory(), "config.toml")
}
func getDefaultCertificateDirectory() string {
return filepath.Join(getDefaultConfigDirectory(), "certs")
}
var (
_ prometheus.Collector = &configAccessCollector{}
)
type configAccessCollector struct {
loadingError prometheus.Counter
loaded prometheus.Counter
savingError prometheus.Counter
saved prometheus.Counter
}
func newConfigAccessCollector() *configAccessCollector {
return &configAccessCollector{
loadingError: prometheus.NewCounter(prometheus.CounterOpts{
Name: "gitlab_runner_configuration_loading_error_total",
Help: "Total number of times the configuration file was not loaded by Runner process due to errors",
}),
loaded: prometheus.NewCounter(prometheus.CounterOpts{
Name: "gitlab_runner_configuration_loaded_total",
Help: "Total number of times the configuration file was loaded by Runner process",
}),
savingError: prometheus.NewCounter(prometheus.CounterOpts{
Name: "gitlab_runner_configuration_saving_error_total",
Help: "Total number of times the configuration file was not saved by Runner process due to errors",
}),
saved: prometheus.NewCounter(prometheus.CounterOpts{
Name: "gitlab_runner_configuration_saved_total",
Help: "Total number of times the configuration file was saved by Runner process",
}),
}
}
func (c *configAccessCollector) Describe(descs chan<- *prometheus.Desc) {
c.loadingError.Describe(descs)
c.loaded.Describe(descs)
c.savingError.Describe(descs)
c.saved.Describe(descs)
}
func (c *configAccessCollector) Collect(metrics chan<- prometheus.Metric) {
c.loadingError.Collect(metrics)
c.loaded.Collect(metrics)
c.savingError.Collect(metrics)
c.saved.Collect(metrics)
}
type configOptions struct {
configMutex sync.Mutex
config *common.Config
loadedSystemIDState *common.SystemIDState
configAccessCollector *configAccessCollector
ConfigFile string `short:"c" long:"config" env:"CONFIG_FILE" description:"Config file"`
}
// getConfig returns a copy of the config as it was during the function call.
// This makes sure the properties of the config won't change after the mutex
// in this function has been unlocked. We don't change the inner objects,
// which makes a shallow copy OK in this case, if that ever changes we should
// look into deep copying or other alternatives.
// All writes of the config property should be protected by a mutex while
// all reads should use this function.
func (c *configOptions) getConfig() *common.Config {
c.configMutex.Lock()
defer c.configMutex.Unlock()
if c.config == nil {
return nil
}
config := *c.config
return &config
}
func (c *configOptions) saveConfig() error {
err := c.config.SaveConfig(c.ConfigFile)
if err != nil {
c.onConfigurationAccessCollector(func(m *configAccessCollector) {
m.savingError.Inc()
})
return err
}
c.onConfigurationAccessCollector(func(m *configAccessCollector) {
m.saved.Inc()
})
return nil
}
func (c *configOptions) onConfigurationAccessCollector(callback func(*configAccessCollector)) {
if c.configAccessCollector == nil {
return
}
callback(c.configAccessCollector)
}
func (c *configOptions) loadConfig() error {
c.configMutex.Lock()
defer c.configMutex.Unlock()
config := common.NewConfig()
err := config.LoadConfig(c.ConfigFile)
if err != nil {
c.onConfigurationAccessCollector(func(m *configAccessCollector) {
m.loadingError.Inc()
})
return err
}
// Config validation is best-effort
if err := common.Validate(config); err != nil {
logrus.Infof(
"There might be a problem with your config based on "+
"jsonschema annotations in common/config.go "+
"(experimental feature):\n%v\n",
err,
)
}
c.onConfigurationAccessCollector(func(m *configAccessCollector) {
m.loaded.Inc()
})
systemIDState, err := c.loadSystemID(filepath.Join(filepath.Dir(c.ConfigFile), ".runner_system_id"))
if err != nil {
return fmt.Errorf("loading system ID file: %w", err)
}
c.config = config
for _, runnerCfg := range c.config.Runners {
runnerCfg.SystemIDState = systemIDState
runnerCfg.ConfigLoadedAt = time.Now()
runnerCfg.ConfigDir = filepath.Dir(c.ConfigFile)
}
c.loadedSystemIDState = systemIDState
return nil
}
func (c *configOptions) loadSystemID(filePath string) (*common.SystemIDState, error) {
systemIDState := common.NewSystemIDState()
err := systemIDState.LoadFromFile(filePath)
if err != nil {
return nil, err
}
// ensure we have a system ID
if systemIDState.GetSystemID() == "" {
err = systemIDState.EnsureSystemID()
if err != nil {
return nil, err
}
err = systemIDState.SaveConfig(filePath)
if err != nil {
logrus.
WithFields(logrus.Fields{
"state_file": filePath,
"system_id": systemIDState.GetSystemID(),
}).
Warningf("Couldn't save new system ID on state file. "+
"In order to reliably identify this runner in jobs with a known identifier,\n"+
"please ensure there is a text file at the location specified in `state_file` "+
"with the contents of `system_id`. Example: echo %q > %q\n", systemIDState.GetSystemID(), filePath)
}
}
return systemIDState, nil
}
func (c *configOptions) RunnerByName(name string) (*common.RunnerConfig, error) {
config := c.getConfig()
if config == nil {
return nil, fmt.Errorf("config has not been loaded")
}
for _, runner := range config.Runners {
if runner.Name == name {
return runner, nil
}
}
return nil, fmt.Errorf("could not find a runner with the name '%s'", name)
}
func (c *configOptions) RunnerByToken(token string) (*common.RunnerConfig, error) {
config := c.getConfig()
if config == nil {
return nil, fmt.Errorf("config has not been loaded")
}
for _, runner := range config.Runners {
if runner.Token == token {
return runner, nil
}
}
return nil, fmt.Errorf("could not find a runner with the token '%s'", helpers.ShortenToken(token))
}
func (c *configOptions) RunnerByURLAndID(url string, id int64) (*common.RunnerConfig, error) {
config := c.getConfig()
if config == nil {
return nil, fmt.Errorf("config has not been loaded")
}
for _, runner := range config.Runners {
if runner.URL == url && runner.ID == id {
return runner, nil
}
}
return nil, fmt.Errorf("could not find a runner with the URL %q and ID %d", url, id)
}
func (c *configOptions) RunnerByNameAndToken(name string, token string) (*common.RunnerConfig, error) {
config := c.getConfig()
if config == nil {
return nil, fmt.Errorf("config has not been loaded")
}
for _, runner := range config.Runners {
if runner.Name == name && runner.Token == token {
return runner, nil
}
}
return nil, fmt.Errorf("could not find a runner with the Name '%s' and Token '%s'", name, token)
}
type configOptionsWithListenAddress struct {
configOptions
ListenAddress string `long:"listen-address" env:"LISTEN_ADDRESS" description:"Metrics / pprof server listening address"`
}
func (c *configOptionsWithListenAddress) listenAddress() (string, error) {
address := c.getConfig().ListenAddress
if c.ListenAddress != "" {
address = c.ListenAddress
}
if address == "" {
return "", nil
}
_, port, err := net.SplitHostPort(address)
if err != nil && !strings.Contains(err.Error(), "missing port in address") {
return "", err
}
if port == "" {
return fmt.Sprintf("%s:%d", address, common.DefaultMetricsServerPort), nil
}
return address, nil
}
func init() {
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
err := os.Setenv("CONFIG_FILE", GetDefaultConfigFile())
if err != nil {
logrus.WithError(err).Fatal("Couldn't set CONFIG_FILE environment variable")
}
}
network.CertificateDirectory = getDefaultCertificateDirectory()
}