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 }