github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/config/config.go (about)

     1  // Copyright (c) 2018-2023, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package config
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  
    15  	"github.com/choria-io/go-choria/build"
    16  	iu "github.com/choria-io/go-choria/internal/util"
    17  	"github.com/fatih/color"
    18  	log "github.com/sirupsen/logrus"
    19  
    20  	"github.com/choria-io/go-choria/confkey"
    21  	"github.com/choria-io/go-choria/puppet"
    22  )
    23  
    24  var forceDotParse bool
    25  
    26  // Config represents Choria cofnfiguration
    27  //
    28  // NOTE: When adding or updating doc strings please run `go generate` in the root of the repository
    29  type Config struct {
    30  	// The plugins used when publishing Registration data, when this is unset or empty sending registration data is disabled
    31  	Registration []string `confkey:"registration" type:"comma_split"`
    32  
    33  	// The Sub Collective to publish registration data to
    34  	RegistrationCollective string `confkey:"registration_collective"`
    35  
    36  	// How often to publish registration data
    37  	RegisterInterval int `confkey:"registerinterval" default:"300"`
    38  
    39  	// When true delays initial registration publish by a random period up to registerinterval following registration publishes will be at registerinterval without further splay
    40  	RegistrationSplay bool `confkey:"registration_splay" default:"true"`
    41  
    42  	// The list of known Sub Collectives this node will join or communicate with, Servers will subscribe the node and each agent to each sub collective and Clients will publish to a chosen sub collective. Defaults to the build settin build.DefaultCollectives
    43  	Collectives []string `confkey:"collectives" type:"comma_split"`
    44  
    45  	// The Sub Collective where a Client will publish to when no specific Sub Collective is configured
    46  	MainCollective string `confkey:"main_collective"`
    47  
    48  	// The file to write logs to, when set to 'discard' logging will be disabled. Also supports 'stdout' and 'stderr' as special log destinations.
    49  	LogFile string `confkey:"logfile" type:"path_string" default:"stdout"`
    50  
    51  	// The lowest level log to add to the logfile
    52  	LogLevel string `confkey:"loglevel" default:"info" validate:"enum=debug,info,warn,error,fatal"`
    53  
    54  	// The directory where Agents, DDLs and other plugins are found
    55  	LibDir []string `confkey:"libdir" type:"path_split"`
    56  
    57  	// The identity this machine is known as, when empty it's derived based on the operating system hostname or by calling facter fqdn
    58  	Identity string `confkey:"identity"`
    59  
    60  	// Disables or enable CLI color
    61  	Color bool `confkey:"color" default:"true"`
    62  
    63  	// Path to a file listing configuration classes applied to a node, used in matches using Class filters
    64  	ClassesFile string `confkey:"classesfile" default:"/opt/puppetlabs/puppet/cache/state/classes.txt" type:"path_string"`
    65  
    66  	// How long to wait for responses while doing broadcast discovery
    67  	DiscoveryTimeout int `confkey:"discovery_timeout" default:"2"`
    68  
    69  	// When enabled uses rpcauditprovider to audit RPC requests processed by the server
    70  	RPCAudit bool `confkey:"rpcaudit" default:"false" url:"https://choria.io/docs/configuration/aaa/"`
    71  
    72  	// When enables authorization is performed on every RPC request based on rpcauthprovider
    73  	RPCAuthorization bool `confkey:"rpcauthorization" default:"true" url:"https://choria.io/docs/configuration/aaa/"`
    74  
    75  	// The Authorization system to use
    76  	RPCAuthorizationProvider string `confkey:"rpcauthprovider" type:"title_string" default:"action_policy" url:"https://choria.io/docs/configuration/aaa/"`
    77  
    78  	// When limiting nodes to a subset of discovered nodes this is the method to use, random is influenced by
    79  	RPCLimitMethod string `confkey:"rpclimitmethod" default:"first" validate:"enum=first,random"`
    80  
    81  	// How long published messages are allowed to linger on the network, lower numbers have a higher reliance on clocks being in sync
    82  	TTL int `confkey:"ttl" default:"60"`
    83  
    84  	// The default discovery plugin to use. The default "mc" uses a network broadcast, "choria" uses PuppetDB, external calls external commands
    85  	DefaultDiscoveryMethod string `confkey:"default_discovery_method" default:"mc" validate:"enum=mc,broadcast,puppetdb,choria,external,inventory"`
    86  
    87  	// Where to look for YAML or JSON based facts
    88  	FactSourceFile string `confkey:"plugin.yaml" type:"path_string"`
    89  
    90  	// Default options to pass to the discovery plugin
    91  	DefaultDiscoveryOptions []string `confkey:"default_discovery_options"`
    92  
    93  	// The amount of time to allow the server to exit, after this memory and thread dumps will be performed and a force exit will be done
    94  	SoftShutdownTimeout int `confkey:"soft_shutdown_timeout" default:"2"`
    95  
    96  	// ConfigFile is the main configuration that got parsed
    97  	ConfigFile string
    98  
    99  	// ParsedFiles is a list of all files parsed to create the current config
   100  	ParsedFiles []string
   101  
   102  	// the options exactly as they were found in the config files
   103  	rawOpts map[string]string
   104  
   105  	Choria *ChoriaPluginConfig
   106  
   107  	// options that are not user configurable via config files but can be
   108  	// used by things like the emulator to set up a TLS free setup
   109  
   110  	// DisableSecurityProviderVerify skips calling security provider Validate()
   111  	DisableSecurityProviderVerify bool
   112  
   113  	// DisableTLS turns off TLS and skips calling security provider Validate()
   114  	DisableTLS bool
   115  
   116  	// DisableTLSVerify turns off CA validation etc in TLS connections
   117  	DisableTLSVerify bool
   118  
   119  	// OverrideCertname sets a arbitrary certname and short circuits calling Puppet etc
   120  	// this is mainly used by tests to adjust the certname on the fly
   121  	OverrideCertname string
   122  
   123  	// InitiatedByServer indicates to the framework that certain server specific
   124  	// initialization steps - like Provisioning mode - should be performed.
   125  	InitiatedByServer bool
   126  
   127  	// Puppet provides access to puppet config data, settings and facts
   128  	Puppet *puppet.Wrapper
   129  
   130  	// CacheBatchedTransports should be true when a agent provider does batched
   131  	// requests where effectively the same request can span many publishes often
   132  	// long apart. The problem is that in these cases the security framework might
   133  	// require frequent 2FA and users might be prompted for 2FA mid-batch.  This
   134  	// setting will hint to choria.Message to return the same transport message
   135  	// repeatedly
   136  	CacheBatchedTransports bool
   137  
   138  	// Allow things like completion to put the DDL Registry in cache-only mode
   139  	RegistryCacheOnly bool
   140  
   141  	// CustomLogger sets a logger instance that Choria framework will use and
   142  	// not change any configuration, if you do this you should take care of
   143  	// configuring the Logrus standard logger as some places Choria will log
   144  	// via that
   145  	CustomLogger *log.Logger
   146  }
   147  
   148  // NewDefaultSystemConfig creates a new configuration for system services
   149  func NewDefaultSystemConfig(server bool) (*Config, error) {
   150  	c := newConfig()
   151  	c.InitiatedByServer = server
   152  
   153  	err := c.normalize()
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	return c, nil
   159  }
   160  
   161  // NewDefaultConfig creates a empty configuration
   162  func NewDefaultConfig() (*Config, error) {
   163  	c := newConfig()
   164  
   165  	err := c.normalize()
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	return c, nil
   171  }
   172  
   173  func NewSystemConfig(path string, server bool) (*Config, error) {
   174  	c := newConfig()
   175  	c.InitiatedByServer = server
   176  
   177  	err := loadConfigFiles(path, false, c)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	return c, nil
   183  }
   184  
   185  func loadConfigFiles(path string, projects bool, c *Config) error {
   186  	if !filepath.IsAbs(path) {
   187  		path, _ = filepath.Abs(path)
   188  	}
   189  
   190  	c.ConfigFile = path
   191  
   192  	err := parseConfig(path, c, "", c.rawOpts)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	err = parseConfig(path, c.Choria, "", c.rawOpts)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	err = c.parseAllDotCfg()
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	if projects {
   208  		pwd, err := os.Getwd()
   209  		if err != nil {
   210  			return err
   211  		}
   212  		pfiles, err := ProjectConfigurationFiles(pwd)
   213  		if err != nil {
   214  			return err
   215  		}
   216  
   217  		for _, pp := range pfiles {
   218  			err = parseConfig(pp, c, "", c.rawOpts)
   219  			if err != nil {
   220  				return err
   221  			}
   222  
   223  			err = parseConfig(pp, c.Choria, "", c.rawOpts)
   224  			if err != nil {
   225  				return err
   226  			}
   227  		}
   228  	}
   229  
   230  	return c.normalize()
   231  }
   232  
   233  // NewConfig parses a config file and return the config
   234  func NewConfig(path string) (*Config, error) {
   235  	c := newConfig()
   236  
   237  	err := loadConfigFiles(path, true, c)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	return c, nil
   243  }
   244  
   245  // NewConfigForTests creates a configuration for use in testing tools
   246  func NewConfigForTests() *Config {
   247  	c := newConfig()
   248  	c.MainCollective = "ginkgo"
   249  	c.Collectives = []string{"ginkgo", "mcollective"}
   250  	c.RegistrationCollective = "ginkgo"
   251  	c.Identity = "ginkgo.example.net"
   252  	c.OverrideCertname = "rip.mcollective"
   253  	c.LogLevel = "fatal"
   254  	c.Choria.SSLDir = "/nonexisting"
   255  	c.DisableSecurityProviderVerify = true
   256  	c.LogFile = "discard"
   257  	c.RPCAuthorization = false
   258  
   259  	return c
   260  }
   261  
   262  func (c *Config) normalize() error {
   263  	if len(c.Collectives) == 0 {
   264  		c.Collectives = strings.Split(build.DefaultCollectives, ",")
   265  		if len(c.Collectives) == 0 {
   266  			c.Collectives = []string{"mcollective"}
   267  		}
   268  
   269  		for i, collective := range c.Collectives {
   270  			c.Collectives[i] = strings.TrimSpace(collective)
   271  		}
   272  
   273  		// when using the choria security provider we switch default collectives
   274  		if c.Choria.SecurityProvider == "choria" && len(c.Collectives) == 1 && c.Collectives[0] == "mcollective" {
   275  			c.Collectives = []string{"choria"}
   276  		}
   277  	}
   278  
   279  	if c.MainCollective == "" {
   280  		c.MainCollective = c.Collectives[0]
   281  	}
   282  
   283  	if c.RegistrationCollective == "" {
   284  		c.RegistrationCollective = c.MainCollective
   285  	}
   286  
   287  	if c.Identity == "" {
   288  		hn, err := os.Hostname()
   289  		if err != nil {
   290  			return fmt.Errorf("could not determine hostname: %s", err)
   291  		}
   292  
   293  		// if os.Hostname gets a full hostname use that as it's quicker, then try facter if
   294  		// that's not available then use whatever os.Hostname gave even if its a short name
   295  		//
   296  		// kubernetes does not have domain names in the pod hosts so we just take whats there
   297  		// when running in a pod
   298  		if strings.Count(hn, ".") > 1 {
   299  			c.Identity = hn
   300  		} else if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
   301  			c.Identity = hn
   302  			fqdn, err := DNSFQDN()
   303  			if err == nil {
   304  				c.Identity = fqdn
   305  			}
   306  		} else if fqdn, _ := DNSFQDN(); fqdn != "" {
   307  			c.Identity = fqdn
   308  		} else if fqdn, _ := c.Puppet.FacterFQDN(); fqdn != "" {
   309  			c.Identity = fqdn
   310  		} else {
   311  			c.Identity = hn
   312  		}
   313  
   314  		if c.Identity == "" {
   315  			return errors.New("could not determine identity from os.Hostname or facter, please set identity in the configuration")
   316  		}
   317  	}
   318  
   319  	if c.LogLevel == "" {
   320  		c.LogLevel = "debug"
   321  	}
   322  
   323  	if c.LogLevel == "debug" {
   324  		log.SetLevel(log.DebugLevel)
   325  	}
   326  
   327  	if c.Choria.ClientAnonTLS {
   328  		if c.Choria.RemoteSignerURL == "" && !c.Choria.RemoteSignerService {
   329  			return fmt.Errorf("anonymous TLS can only be enabled when a remote signer is configured")
   330  		}
   331  
   332  		c.DisableTLSVerify = true
   333  		c.DisableSecurityProviderVerify = true
   334  	}
   335  
   336  	if c.Choria.ServerAnonTLS {
   337  		c.DisableTLSVerify = true
   338  		c.DisableSecurityProviderVerify = true
   339  
   340  		if c.Choria.ServerTokenFile == "" {
   341  			if c.ConfigFile == "" {
   342  				return fmt.Errorf("cannot determine path to server token file")
   343  			}
   344  			c.Choria.ServerTokenFile = filepath.Join(filepath.Dir(c.ConfigFile), "server.jwt")
   345  		}
   346  
   347  		if c.Choria.ServerTokenSeedFile == "" {
   348  			if c.ConfigFile == "" {
   349  				return fmt.Errorf("cannot determine path to server token file")
   350  			}
   351  			c.Choria.ServerTokenSeedFile = filepath.Join(filepath.Dir(c.ConfigFile), "server.seed")
   352  		}
   353  	}
   354  
   355  	if runtime.GOOS == "windows" {
   356  		c.Color = false
   357  	}
   358  
   359  	if !c.Color {
   360  		color.NoColor = true
   361  	}
   362  
   363  	return nil
   364  }
   365  
   366  // BuildInfoProvider provides build time information
   367  type BuildInfoProvider interface {
   368  	HasTLS() bool
   369  }
   370  
   371  // ApplyBuildSettings applies build time overrides to the configuration
   372  func (c *Config) ApplyBuildSettings(b BuildInfoProvider) {
   373  	c.DisableTLS = !b.HasTLS()
   374  }
   375  
   376  // HasOption determines if a specific option was set from a config key.
   377  // The option given would be something like `plugin.choria.use_srv`
   378  // and true would indicate that it was set by config vs using defaults
   379  func (c *Config) HasOption(option string) bool {
   380  	_, ok := c.rawOpts[option]
   381  
   382  	return ok
   383  }
   384  
   385  // Option retrieves the raw string representation of a given option
   386  // from that was loaded from the configuration
   387  func (c *Config) Option(option string, deflt string) string {
   388  	v, ok := c.rawOpts[option]
   389  
   390  	if !ok {
   391  		return deflt
   392  	}
   393  
   394  	return v
   395  }
   396  
   397  // SetOption sets a raw string option, can be used to programmatically
   398  // set plugin options etc, setting a main config item value here does
   399  // not update the values in the strings, so this is only really useful
   400  // for setting plugin options
   401  func (c *Config) SetOption(option string, value string) {
   402  	c.rawOpts[option] = value
   403  }
   404  
   405  // UnParsedOptions are the options loaded
   406  func (c *Config) UnParsedOptions() map[string]string {
   407  	return c.rawOpts
   408  }
   409  
   410  func (c *Config) dotdDir() string {
   411  	if !forceDotParse {
   412  		home, err := iu.HomeDir()
   413  		if err == nil {
   414  			if strings.HasPrefix(c.ConfigFile, home) {
   415  				return ""
   416  			}
   417  		}
   418  	}
   419  
   420  	return filepath.Join(filepath.Dir(c.ConfigFile), "plugin.d")
   421  }
   422  
   423  func newConfig() *Config {
   424  	m := &Config{
   425  		Choria:  newChoria(),
   426  		rawOpts: make(map[string]string),
   427  		Puppet:  puppet.New(),
   428  	}
   429  
   430  	err := confkey.SetStructDefaults(m)
   431  	if err != nil {
   432  		log.Errorf("Config creation failed: %s", err)
   433  	}
   434  
   435  	return m
   436  }