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