github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/choria/framework.go (about) 1 // Copyright (c) 2017-2023, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package choria 6 7 import ( 8 "context" 9 "crypto/md5" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "net" 15 "net/http" 16 "net/url" 17 "os" 18 "path/filepath" 19 "strings" 20 "sync" 21 "time" 22 23 "github.com/choria-io/go-choria/inter" 24 "github.com/choria-io/go-choria/providers/ddlresolver" 25 election "github.com/choria-io/go-choria/providers/election/streams" 26 governor "github.com/choria-io/go-choria/providers/governor/streams" 27 "github.com/choria-io/go-choria/providers/kv" 28 "github.com/choria-io/go-choria/providers/provtarget" 29 "github.com/choria-io/go-choria/providers/security/choria" 30 "github.com/choria-io/go-choria/providers/signers" 31 "github.com/choria-io/tokens" 32 "github.com/fatih/color" 33 "github.com/nats-io/nats.go" 34 "github.com/segmentio/ksuid" 35 "golang.org/x/term" 36 37 "github.com/choria-io/go-choria/internal/util" 38 "github.com/choria-io/go-choria/protocol" 39 certmanagersec "github.com/choria-io/go-choria/providers/security/certmanager" 40 41 "github.com/choria-io/go-choria/build" 42 "github.com/choria-io/go-choria/config" 43 44 "github.com/choria-io/go-choria/providers/security/filesec" 45 "github.com/choria-io/go-choria/providers/security/puppetsec" 46 "github.com/choria-io/go-choria/puppet" 47 "github.com/choria-io/go-choria/srvcache" 48 log "github.com/sirupsen/logrus" 49 ) 50 51 // Framework is a utility encompassing choria config and various utilities 52 type Framework struct { 53 Config *config.Config 54 55 inProcessConnection nats.InProcessConnProvider 56 customRequestSigner inter.RequestSigner 57 security inter.SecurityProvider 58 log *log.Logger 59 60 bi *build.Info 61 srvcache *srvcache.Cache 62 puppet *puppet.Wrapper 63 mu *sync.Mutex 64 } 65 66 type Option func(fw *Framework) error 67 68 // WithCustomRequestSigner sets a custom request signer, generally only used in tests 69 func WithCustomRequestSigner(s inter.RequestSigner) Option { 70 return func(fw *Framework) error { 71 fw.customRequestSigner = s 72 return nil 73 } 74 } 75 76 // New sets up a Choria with all its config loaded and so forth 77 func New(path string, opts ...Option) (*Framework, error) { 78 conf, err := config.NewConfig(path) 79 if err != nil { 80 return nil, err 81 } 82 83 conf.ApplyBuildSettings(BuildInfo()) 84 85 return NewWithConfig(conf, opts...) 86 } 87 88 // NewWithConfig creates a new instance of the framework with the supplied config instance 89 func NewWithConfig(cfg *config.Config, opts ...Option) (*Framework, error) { 90 c := Framework{ 91 Config: cfg, 92 mu: &sync.Mutex{}, 93 bi: BuildInfo(), 94 } 95 96 for _, opt := range opts { 97 err := opt(&c) 98 if err != nil { 99 return nil, err 100 } 101 } 102 103 err := c.SetupLogging(false) 104 if err != nil { 105 return &c, fmt.Errorf("could not set up logging: %s", err) 106 } 107 108 config.Mutate(cfg, c.Logger("config")) 109 110 c.srvcache = srvcache.New(cfg.Identity, 5*time.Second, net.LookupSRV, c.Logger("srvcache")) 111 c.puppet = puppet.New() 112 113 err = c.setupSecurity() 114 if err != nil { 115 return &c, fmt.Errorf("could not set up security framework: %s", err) 116 } 117 118 return &c, nil 119 } 120 121 // SetInProcessConnProvider sets a nats.InProcessConnProvider to use, connector will make connections using that if set 122 func (fw *Framework) SetInProcessConnProvider(p nats.InProcessConnProvider) { 123 fw.mu.Lock() 124 fw.inProcessConnection = p 125 fw.mu.Unlock() 126 } 127 128 // InProcessConnProvider provides an in-process connection for nats if configured using SetInProcessConnProvider(), nil when not set 129 func (fw *Framework) InProcessConnProvider() nats.InProcessConnProvider { 130 fw.mu.Lock() 131 defer fw.mu.Unlock() 132 133 return fw.inProcessConnection 134 } 135 136 // BuildInfo retrieves build information 137 func (fw *Framework) BuildInfo() *build.Info { 138 return BuildInfo() 139 } 140 141 func (fw *Framework) setupSecurity() error { 142 var ( 143 err error 144 signer inter.RequestSigner 145 ) 146 147 switch { 148 case fw.customRequestSigner != nil: 149 signer = fw.customRequestSigner 150 case fw.Config.Choria.RemoteSignerService: 151 signer = signers.NewAAAServiceRPCSigner(fw) 152 case fw.Config.Choria.RemoteSignerURL != "": 153 signer = signers.NewAAAServiceHTTPSigner() 154 } 155 156 switch fw.Config.Choria.SecurityProvider { 157 case "puppet": 158 fw.security, err = puppetsec.New( 159 puppetsec.WithResolver(fw), 160 puppetsec.WithChoriaConfig(fw.BuildInfo(), fw.Config), 161 puppetsec.WithLog(fw.Logger("security")), 162 puppetsec.WithSigner(signer)) 163 164 case "file": 165 fw.security, err = filesec.New( 166 filesec.WithChoriaConfig(fw.BuildInfo(), fw.Config), 167 filesec.WithLog(fw.Logger("security")), 168 filesec.WithSigner(signer)) 169 170 case "pkcs11": 171 err = fw.setupPKCS11(signer) 172 173 case "certmanager": 174 fw.security, err = certmanagersec.New( 175 certmanagersec.WithChoriaConfig(fw.Config), 176 certmanagersec.WithLog(fw.Logger("security")), 177 certmanagersec.WithContext(context.Background())) 178 179 case "choria": 180 fw.security, err = choria.New( 181 choria.WithChoriaConfig(fw.Config), 182 choria.WithLog(fw.Logger("security")), 183 choria.WithSigner(signer)) 184 185 default: 186 err = fmt.Errorf("unknown security provider %s", fw.Config.Choria.SecurityProvider) 187 } 188 189 if err != nil { 190 return err 191 } 192 193 if !(fw.Config.DisableSecurityProviderVerify || fw.Config.DisableTLS) && protocol.IsSecure() { 194 errors, ok := fw.security.Validate() 195 if !ok { 196 return fmt.Errorf("security setup is not valid, %d errors encountered: %s", len(errors), strings.Join(errors, ", ")) 197 } 198 } 199 200 return nil 201 } 202 203 // ProvisionMode determines if this instance is in provisioning mode 204 // if the setting `plugin.choria.server.provision` is set at all then 205 // the value of that is returned, else it the build time property 206 // ProvisionDefault is consulted 207 func (fw *Framework) ProvisionMode() bool { 208 if !fw.Config.InitiatedByServer || (fw.bi.ProvisionBrokerURLs() == "" && fw.bi.ProvisionJWTFile() == "" && fw.bi.ProvisionToken() == "") { 209 return false 210 } 211 212 if fw.Config.HasOption("plugin.choria.server.provision") { 213 return fw.Config.Choria.Provision 214 } 215 216 // some build environments might go through provisioning as a test phase 217 // and if those base images do not remove their config they might start up 218 // with still-valid tokens from building phase instead of re-provisioning 219 // as intended, typically in those cases the hostnames will have changed between 220 // base image and eventual running OS. So we can detect that by comparing identity 221 // of the provisioned token and the running identity. 222 // 223 // Regardless how it happened this is bad because the node will subscribe to its 224 // node subject with its identity set but the token will have another identity in 225 // it causing the broker to not set appropriate allow rules for that connection 226 caller, _, _, _, _ := fw.UniqueIDFromUnverifiedToken() 227 if caller != "" { 228 if fw.Config.Identity != caller { 229 fw.log.Warnf("Caller %q from server token does not match identity %q, enabling provisioning", caller, fw.Config.Identity) 230 return true 231 } 232 } 233 234 return fw.bi.ProvisionDefault() 235 } 236 237 // PrometheusTextFileDir is the configured directory where to write prometheus text file stats 238 func (fw *Framework) PrometheusTextFileDir() string { 239 return fw.Config.Choria.PrometheusTextFileDir 240 } 241 242 // SupportsProvisioning determines if a node can auto provision 243 func (fw *Framework) SupportsProvisioning() bool { 244 if fw.ProvisionMode() { 245 return true 246 } 247 248 return fw.bi.SupportsProvisioning() 249 } 250 251 // ConfigureProvisioning adjusts the active configuration to match the provisioning profile 252 func (fw *Framework) ConfigureProvisioning(ctx context.Context) { 253 provtarget.Configure(ctx, fw.Config, fw.Logger("provtarget")) 254 255 if !fw.ProvisionMode() { 256 return 257 } 258 259 fw.Config.RPCAuthorization = false 260 fw.Config.Choria.FederationCollectives = []string{} 261 fw.Config.Collectives = []string{"provisioning"} 262 fw.Config.MainCollective = "provisioning" 263 fw.Config.Registration = []string{} 264 fw.Config.FactSourceFile = fw.bi.ProvisionFacts() 265 fw.Config.Choria.NatsUser = fw.bi.ProvisioningBrokerUsername() 266 fw.Config.Choria.NatsPass = fw.bi.ProvisioningBrokerPassword() 267 fw.Config.Choria.ProvisionAllowUpdate = fw.bi.ProvisionAllowServerUpdate() 268 fw.Config.Choria.SSLDir = filepath.Join(filepath.Dir(fw.Config.ConfigFile), "ssl") 269 fw.Config.Choria.SecurityProvider = "file" 270 271 if fw.bi.ProvisionStatusFile() != "" { 272 fw.Config.Choria.StatusFilePath = fw.bi.ProvisionStatusFile() 273 fw.Config.Choria.StatusUpdateSeconds = 10 274 } 275 276 if fw.bi.ProvisionRegistrationData() != "" { 277 fw.Config.RegistrationCollective = "provisioning" 278 fw.Config.Registration = []string{"file_content"} 279 fw.Config.RegisterInterval = 120 280 fw.Config.RegistrationSplay = false 281 fw.Config.Choria.FileContentRegistrationTarget = "provisioning.registration.data" 282 fw.Config.Choria.FileContentRegistrationData = fw.bi.ProvisionRegistrationData() 283 } 284 285 if !fw.bi.ProvisionSecurity() { 286 protocol.Secure = "false" 287 } 288 289 if fw.bi.ProvisionBrokerSRVDomain() != "" { 290 fw.Config.Choria.UseSRVRecords = true 291 fw.Config.Choria.SRVDomain = fw.bi.ProvisionBrokerSRVDomain() 292 } 293 294 if fw.bi.ProvisionJWTFile() != "" && fw.bi.ProvisionUsingVersion2() { 295 fw.Config.Choria.SecurityProvider = "choria" 296 fw.Config.Choria.ChoriaSecurityTokenFile = fw.bi.ProvisionJWTFile() 297 fw.Config.Choria.ChoriaSecuritySignReplies = false 298 protocol.Secure = "false" 299 300 err := fw.setupSecurity() 301 if err != nil { 302 fw.log.Errorf("Could not setup security to enable protocol v2: %v", err) 303 } 304 } 305 } 306 307 // IsFederated determines if the configuration is setting up any Federation collectives 308 func (fw *Framework) IsFederated() (result bool) { 309 return len(fw.FederationCollectives()) != 0 310 } 311 312 // Logger creates a new logrus entry 313 func (fw *Framework) Logger(component string) *log.Entry { 314 return fw.log.WithFields(log.Fields{"component": component}) 315 } 316 317 // FederationCollectives determines the known Federation Member 318 // Collectives based on the CHORIA_FED_COLLECTIVE environment 319 // variable or the choria.federation.collectives config item 320 func (fw *Framework) FederationCollectives() (collectives []string) { 321 var found []string 322 323 env := os.Getenv("CHORIA_FED_COLLECTIVE") 324 325 if env != "" { 326 found = strings.Split(env, ",") 327 } 328 329 if len(found) == 0 { 330 found = fw.Config.Choria.FederationCollectives 331 } 332 333 for _, collective := range found { 334 collectives = append(collectives, strings.TrimSpace(collective)) 335 } 336 337 return 338 } 339 340 // FederationMiddlewareServers determines the correct Federation Middleware Servers 341 // 342 // It does this by: 343 // 344 // - looking for choria.federation_middleware_hosts configuration 345 // - Doing SRV lookups of _mcollective-federation_server._tcp and _x-puppet-mcollective_federation._tcp 346 func (fw *Framework) FederationMiddlewareServers() (servers srvcache.Servers, err error) { 347 configured := fw.Config.Choria.FederationMiddlewareHosts 348 servers = srvcache.NewServers() 349 350 if len(configured) > 0 { 351 servers, err = srvcache.StringHostsToServers(configured, "nats") 352 if err != nil { 353 return servers, fmt.Errorf("could not parse configured Federation Middleware: %s", err) 354 } 355 } 356 357 if servers.Count() == 0 { 358 servers, err = fw.QuerySrvRecords([]string{"_mcollective-federation_server._tcp", "_x-puppet-mcollective_federation._tcp"}) 359 if err != nil { 360 return servers, fmt.Errorf("could not resolve Federation Middleware Server SRV records: %s", err) 361 } 362 } 363 364 servers.Each(func(s srvcache.Server) { 365 if s.Scheme() == "" { 366 s.SetScheme("nats") 367 } 368 }) 369 370 return servers, err 371 } 372 373 // PuppetDBServers resolves the PuppetDB server based on configuration of _x-puppet-db._tcp 374 func (fw *Framework) PuppetDBServers() (servers srvcache.Servers, err error) { 375 if fw.Config.Choria.PuppetDBHost != "" { 376 configured := fmt.Sprintf("%s:%d", fw.Config.Choria.PuppetDBHost, fw.Config.Choria.PuppetDBPort) 377 378 servers, err = srvcache.StringHostsToServers([]string{configured}, "https") 379 if err != nil { 380 return servers, fmt.Errorf("could not parse configured PuppetDB host: %s", err) 381 } 382 383 return servers, nil 384 } 385 386 if fw.Config.Choria.UseSRVRecords { 387 servers, err = fw.QuerySrvRecords([]string{"_x-puppet-db._tcp"}) 388 if err != nil { 389 return servers, fmt.Errorf("could not resolve PuppetDB Server SRV records: %s", err) 390 } 391 392 if servers.Count() == 0 { 393 servers, err = fw.QuerySrvRecords([]string{"_x-puppet._tcp"}) 394 if err != nil { 395 return servers, fmt.Errorf("could not resolve Puppet Server SRV records: %s", err) 396 } 397 398 // In the case where we take _x-puppet._tcp SRV records we unfortunately have 399 // to force the port else it uses the one from Puppet which will 404 400 servers.Each(func(s srvcache.Server) { 401 s.SetPort(fw.Config.Choria.PuppetDBPort) 402 }) 403 } 404 405 servers.Each(func(s srvcache.Server) { 406 if s.Scheme() == "" { 407 s.SetScheme("https") 408 } 409 }) 410 } 411 412 if servers == nil || servers.Count() == 0 { 413 configured := fmt.Sprintf("%s:%d", "puppet", fw.Config.Choria.PuppetDBPort) 414 415 servers, err = srvcache.StringHostsToServers([]string{configured}, "https") 416 if err != nil { 417 return servers, fmt.Errorf("could not parse configured PuppetDB host: %s", err) 418 } 419 } 420 421 return servers, nil 422 } 423 424 // ProvisioningServers determines the build time provisioning servers 425 // when it's unset or results in an empty server list this will return 426 // an error 427 func (fw *Framework) ProvisioningServers(ctx context.Context) (srvcache.Servers, error) { 428 return provtarget.Targets(ctx, fw.Logger("provtarget")) 429 } 430 431 // MiddlewareServers determines the correct Middleware Servers 432 // 433 // It does this by: 434 // 435 // - if ngs is configured and credentials are set and middleware_hosts are empty, use ngs 436 // - looking for choria.federation_middleware_hosts configuration 437 // - Doing SRV lookups of _mcollective-server._tcp and __x-puppet-mcollective._tcp 438 // - Defaulting to puppet:4222 439 func (fw *Framework) MiddlewareServers() (servers srvcache.Servers, err error) { 440 configured := fw.Config.Choria.MiddlewareHosts 441 442 if fw.IsFederated() { 443 return fw.FederationMiddlewareServers() 444 } 445 446 servers = srvcache.NewServers() 447 448 if len(configured) > 0 { 449 servers, err = srvcache.StringHostsToServers(configured, "nats") 450 if err != nil { 451 return servers, fmt.Errorf("could not parse configured Middleware: %s", err) 452 } 453 } 454 455 if servers.Count() == 0 && fw.Config.Choria.UseSRVRecords { 456 servers, err = fw.QuerySrvRecords([]string{"_mcollective-server._tcp", "_x-puppet-mcollective._tcp"}) 457 if err != nil { 458 log.Warnf("Could not resolve Middleware Server SRV records: %s", err) 459 } 460 } 461 462 if servers.Count() == 0 { 463 servers = srvcache.NewServers(srvcache.NewServer("puppet", 4222, "nats")) 464 } 465 466 servers.Each(func(s srvcache.Server) { 467 if s.Scheme() == "" { 468 s.SetScheme("nats") 469 } 470 }) 471 472 return servers, nil 473 } 474 475 func (fw *Framework) SetLogWriter(out io.Writer) { 476 if fw.log != nil { 477 fw.log.SetOutput(out) 478 } 479 } 480 481 func (fw *Framework) commonLogOpener() error { 482 switch { 483 case strings.ToLower(fw.Config.LogFile) == "discard": 484 fw.log.SetOutput(io.Discard) 485 486 case strings.ToLower(fw.Config.LogFile) == "stdout": 487 fw.log.SetOutput(os.Stdout) 488 489 case strings.ToLower(fw.Config.LogFile) == "stderr": 490 fw.log.SetOutput(os.Stderr) 491 492 case fw.Config.LogFile != "": 493 fw.log.Formatter = &log.JSONFormatter{} 494 495 file, err := os.OpenFile(fw.Config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) 496 if err != nil { 497 return fmt.Errorf("could not set up logging: %s", err) 498 } 499 500 fw.log.SetOutput(file) 501 } 502 503 return nil 504 } 505 506 // SetLogger sets the logger to use 507 func (fw *Framework) SetLogger(logger *log.Logger) { 508 fw.log = logger 509 } 510 511 // SetupLogging configures logging based on choria config directives 512 // currently only file and console behaviors are supported 513 func (fw *Framework) SetupLogging(debug bool) (err error) { 514 if fw.Config.CustomLogger != nil { 515 fw.log = fw.Config.CustomLogger 516 return 517 } 518 519 fw.log = log.New() 520 fw.log.SetOutput(os.Stdout) 521 522 err = fw.openLogfile() 523 if err != nil { 524 return err 525 } 526 527 switch fw.Config.LogLevel { 528 case "debug": 529 fw.log.SetLevel(log.DebugLevel) 530 case "info": 531 fw.log.SetLevel(log.InfoLevel) 532 case "warn": 533 fw.log.SetLevel(log.WarnLevel) 534 case "error": 535 fw.log.SetLevel(log.ErrorLevel) 536 case "fatal": 537 fw.log.SetLevel(log.FatalLevel) 538 default: 539 fw.log.SetLevel(log.WarnLevel) 540 } 541 542 if debug { 543 fw.log.SetLevel(log.DebugLevel) 544 } 545 546 log.SetFormatter(fw.log.Formatter) 547 log.SetLevel(fw.log.Level) 548 log.SetOutput(fw.log.Out) 549 550 return 551 } 552 553 // TrySrvLookup will attempt to look up a series of names returning the first found 554 // if SRV lookups are disabled or nothing is found the default will be returned 555 func (fw *Framework) TrySrvLookup(names []string, defaultSrv srvcache.Server) (srvcache.Server, error) { 556 if !fw.Config.Choria.UseSRVRecords { 557 return defaultSrv, nil 558 } 559 560 for _, q := range names { 561 a, err := fw.QuerySrvRecords([]string{q}) 562 if err == nil && a.Count() > 0 { 563 found := a.Servers()[0] 564 log.Infof("Found %s:%d from %s SRV lookups", found.Host(), found.Port(), strings.Join(names, ", ")) 565 566 return found, nil 567 } 568 } 569 570 log.Debugf("Could not find SRV records for %s, returning defaults %s:%d", strings.Join(names, ", "), defaultSrv.Host(), defaultSrv.Port()) 571 572 return defaultSrv, nil 573 } 574 575 var errSRVDisabled = errors.New("SRV lookups are disabled in the configuration file") 576 577 // QuerySrvRecords looks for SRV records within the right domain either 578 // thanks to facter domain or the configured domain. 579 // 580 // If the config disables SRV then a error is returned. 581 func (fw *Framework) QuerySrvRecords(records []string) (srvcache.Servers, error) { 582 servers := srvcache.NewServers() 583 584 if !fw.Config.Choria.UseSRVRecords { 585 return servers, errSRVDisabled 586 } 587 588 domain := fw.Config.Choria.SRVDomain 589 var err error 590 591 if fw.Config.Choria.SRVDomain == "" { 592 domain, err = fw.FacterDomain() 593 if err != nil { 594 return servers, err 595 } 596 597 // cache the result to speed things up 598 fw.Config.Choria.SRVDomain = domain 599 } 600 601 for _, q := range records { 602 record := q + "." + domain 603 log.Debugf("Attempting SRV lookup for %s", record) 604 605 servers, err = fw.srvcache.LookupSrvServers("", "", record, "") 606 if err != nil { 607 log.Debugf("Failed to resolve %s: %s", record, err) 608 continue 609 } 610 611 log.Debugf("Found %d SRV records for %s", servers.Count(), record) 612 break 613 } 614 615 return servers, nil 616 } 617 618 // NetworkBrokerPeers are peers in the broker cluster resolved from 619 // _mcollective-broker._tcp or from the plugin config 620 func (fw *Framework) NetworkBrokerPeers() (servers srvcache.Servers, err error) { 621 servers, err = fw.QuerySrvRecords([]string{"_mcollective-broker._tcp"}) 622 if err != nil { 623 if !errors.Is(err, errSRVDisabled) { 624 fw.log.Errorf("SRV lookup for _mcollective-broker._tcp failed: %s", err) 625 } 626 627 err = nil 628 } 629 630 if servers.Count() == 0 { 631 servers, err = srvcache.StringHostsToServers(fw.Config.Choria.NetworkPeers, "nats") 632 if err != nil { 633 return servers, fmt.Errorf("could not parse network peers: %s", err) 634 } 635 } 636 637 servers.Each(func(f srvcache.Server) { 638 if f.Scheme() == "" { 639 f.SetScheme("nats") 640 } 641 }) 642 643 return servers, nil 644 } 645 646 // Getuid returns the numeric user id of the caller 647 func (fw *Framework) Getuid() int { 648 return os.Getuid() 649 } 650 651 // PuppetSetting retrieves a config setting by shelling out to puppet apply --configprint 652 func (fw *Framework) PuppetSetting(setting string) (string, error) { 653 return fw.puppet.Setting(setting) 654 } 655 656 // FacterStringFact looks up a facter fact, returns "" when unknown 657 func (fw *Framework) FacterStringFact(fact string) (string, error) { 658 return fw.puppet.FacterStringFact(fact) 659 } 660 661 // FacterFQDN determines the machines fqdn by querying facter. Returns "" when unknown 662 func (fw *Framework) FacterFQDN() (string, error) { 663 return fw.puppet.FacterStringFact("networking.fqdn") 664 } 665 666 // FacterDomain determines the machines domain by querying facter. Returns "" when unknown 667 func (fw *Framework) FacterDomain() (string, error) { 668 return fw.puppet.FacterStringFact("networking.domain") 669 } 670 671 // FacterCmd finds the path to facter using first AIO path then a `which` like command 672 func (fw *Framework) FacterCmd() string { 673 return fw.puppet.AIOCmd("facter", "") 674 } 675 676 // PuppetAIOCmd looks up a command in the AIO paths, if it's not there 677 // it will try PATH and finally return a default if not in PATH 678 func (fw *Framework) PuppetAIOCmd(command string, def string) string { 679 return fw.puppet.AIOCmd(command, def) 680 } 681 682 // NewRequestID Creates a new RequestID 683 func (fw *Framework) NewRequestID() (string, error) { 684 if fw.RequestProtocol() == protocol.RequestV2 { 685 kid, err := ksuid.NewRandom() 686 if err != nil { 687 return "", err 688 } 689 return kid.String(), nil 690 } 691 692 return strings.Replace(util.UniqueID(), "-", "", -1), nil 693 } 694 695 // UniqueID creates a new unique ID, usually a v4 uuid, if that fails a random string based ID is made 696 func (fw *Framework) UniqueID() string { 697 return util.UniqueID() 698 } 699 700 // CallerID determines the cert based callerid 701 func (fw *Framework) CallerID() string { 702 caller, _, _, _, err := fw.UniqueIDFromUnverifiedToken() 703 if err == nil { 704 return caller 705 } 706 707 return fmt.Sprintf("choria=%s", fw.Certname()) 708 } 709 710 // HasCollective determines if a collective is known in the configuration 711 func (fw *Framework) HasCollective(collective string) bool { 712 for _, c := range fw.Config.Collectives { 713 if c == collective { 714 return true 715 } 716 } 717 718 return false 719 } 720 721 // OverrideCertname indicates if the user wish to force a specific certname, empty when not 722 func (fw *Framework) OverrideCertname() string { 723 return fw.Config.OverrideCertname 724 } 725 726 // DisableTLSVerify indicates if the user whish to disable TLS verification 727 func (fw *Framework) DisableTLSVerify() bool { 728 return fw.Config.DisableTLSVerify 729 } 730 731 // Configuration returns the active configuration 732 func (fw *Framework) Configuration() *config.Config { 733 return fw.Config 734 } 735 736 // UniqueIDFromUnverifiedToken extracts the caller id or identity from a token, the token is not verified as we do not have the certificate 737 func (fw *Framework) UniqueIDFromUnverifiedToken() (id string, uid string, exp time.Time, token string, err error) { 738 ts, exp, err := fw.SignerToken() 739 if err != nil { 740 return "", "", exp, "", err 741 } 742 743 if fw.Config.InitiatedByServer { 744 t, id, err := tokens.UnverifiedIdentityFromServerToken(ts) 745 if err != nil { 746 return "", "", exp, "", err 747 } 748 749 return id, fmt.Sprintf("%x", md5.Sum([]byte(id))), exp, t.Raw, nil 750 } else { 751 t, caller, err := tokens.UnverifiedCallerFromClientIDToken(ts) 752 if err != nil { 753 return "", "", exp, "", err 754 } 755 756 return caller, fmt.Sprintf("%x", md5.Sum([]byte(caller))), exp, t.Raw, nil 757 } 758 } 759 760 // SignerSeedFile is the path to the seed file for JWT auth 761 // TODO: we need to revisit the many ways to set a seed file here and try to come up with fewer options (1740) 762 func (fw *Framework) SignerSeedFile() (f string, err error) { 763 switch { 764 case fw.Config.Choria.ChoriaSecuritySeedFile != "": 765 return fw.Config.Choria.ChoriaSecuritySeedFile, nil 766 case fw.Config.Choria.ServerAnonTLS: 767 if fw.Config.Choria.ServerTokenSeedFile != "" { 768 return fw.Config.Choria.ServerTokenSeedFile, nil 769 } 770 case fw.Config.Choria.RemoteSignerTokenSeedFile != "": 771 return fw.Config.Choria.RemoteSignerTokenSeedFile, nil 772 } 773 774 t, err := fw.SignerTokenFile() 775 if err != nil { 776 return "", err 777 } 778 779 return fmt.Sprintf("%s.key", strings.TrimSuffix(t, filepath.Ext(t))), nil 780 } 781 782 // SignerTokenFile is the path to the token file, supports clients and servers 783 // TODO: we need to revisit the many ways to set a token file here and try to come up with fewer options (1740) 784 func (fw *Framework) SignerTokenFile() (f string, err error) { 785 tf := "" 786 787 switch { 788 case fw.Config.Choria.ChoriaSecurityTokenFile != "": 789 tf = fw.Config.Choria.ChoriaSecurityTokenFile 790 case fw.Config.Choria.RemoteSignerTokenFile != "": 791 tf = fw.Config.Choria.RemoteSignerTokenFile 792 case fw.Config.Choria.ServerAnonTLS: 793 tf = fw.Config.Choria.ServerTokenFile 794 } 795 796 if tf == "" { 797 return "", fmt.Errorf("no token file defined") 798 } 799 800 return tf, nil 801 } 802 803 // SignerToken retrieves the token used for signing requests or connecting to the broker 804 func (fw *Framework) SignerToken() (token string, expiry time.Time, err error) { 805 var exp time.Time 806 807 tf, err := fw.SignerTokenFile() 808 if err != nil { 809 return "", exp, err 810 } 811 812 tb, err := os.ReadFile(tf) 813 if err != nil { 814 return "", exp, fmt.Errorf("could not read token file: %v", err) 815 } 816 817 purpose := tokens.TokenPurpose(string(tb)) 818 switch purpose { 819 case tokens.ClientIDPurpose: 820 claims, err := tokens.ParseClientIDTokenUnverified(string(tb)) 821 if err != nil { 822 return "", exp, err 823 } 824 err = claims.Valid() 825 if err != nil { 826 fw.log.Warnf("Authentication token %s is not valid: %v", tf, err) 827 return "", exp, err 828 } 829 exp = claims.ExpireTime() 830 831 case tokens.ServerPurpose: 832 claims, err := tokens.ParseServerTokenUnverified(string(tb)) 833 if err != nil { 834 return "", exp, err 835 } 836 err = claims.Valid() 837 if err != nil { 838 fw.log.Warnf("Authentication token %s is not valid: %v", tf, err) 839 return "", exp, err 840 } 841 exp = claims.ExpireTime() 842 843 case tokens.ProvisioningPurpose: 844 // nothing to verify here 845 846 default: 847 return "", exp, fmt.Errorf("cannot use token %s with purpose %q as signer token", tf, purpose) 848 } 849 850 return strings.TrimSpace(string(tb)), exp, err 851 } 852 853 // HTTPClient creates a *http.Client prepared by the security provider with certificates and more set 854 func (fw *Framework) HTTPClient(secure bool) (*http.Client, error) { 855 return fw.security.HTTPClient(secure) 856 } 857 858 func (fw *Framework) PQLQuery(query string) ([]byte, error) { 859 q := url.Values{} 860 q.Set("query", query) 861 path := fmt.Sprintf("/pdb/query/v4?%s", q.Encode()) 862 863 pdb, err := fw.PuppetDBServers() 864 if err != nil { 865 return nil, err 866 } 867 pdbhost := pdb.Strings()[0] 868 869 fw.log.Debugf("Performing PQL query against %s: %s", pdbhost, query) 870 871 client, err := fw.HTTPClient(true) 872 if err != nil { 873 return nil, err 874 } 875 request, err := http.NewRequest("GET", fmt.Sprintf("%s%s", pdbhost, path), nil) 876 if err != nil { 877 return nil, err 878 } 879 880 resp, err := client.Do(request) 881 if err != nil { 882 return nil, err 883 } 884 defer resp.Body.Close() 885 886 if resp.StatusCode != 200 { 887 return nil, fmt.Errorf("invalid PuppetDB response: %s", resp.Status) 888 } 889 890 body, err := io.ReadAll(resp.Body) 891 if err != nil { 892 return nil, err 893 } 894 895 return body, nil 896 } 897 898 func (fw *Framework) PQLQueryCertNames(query string) ([]string, error) { 899 body, err := fw.PQLQuery(query) 900 if err != nil { 901 return nil, err 902 } 903 904 var res []struct { 905 Certname string `json:"certname"` 906 Deactivated bool `json:"deactivated"` 907 } 908 909 err = json.Unmarshal(body, &res) 910 if err != nil { 911 return nil, err 912 } 913 914 var nodes []string 915 for _, r := range res { 916 if !r.Deactivated { 917 nodes = append(nodes, r.Certname) 918 } 919 } 920 921 return nodes, nil 922 } 923 924 // Colorize returns a string of either 'red', 'green' or 'yellow'. If the 'color' configuration 925 // is set to false then the string will have no color hints 926 func (fw *Framework) Colorize(c string, format string, a ...any) string { 927 if !fw.Config.Color { 928 return fmt.Sprintf(format, a...) 929 } 930 931 switch c { 932 case "red": 933 return color.RedString(fmt.Sprintf(format, a...)) 934 case "green": 935 return color.GreenString(fmt.Sprintf(format, a...)) 936 case "yellow": 937 return color.YellowString(fmt.Sprintf(format, a...)) 938 default: 939 return fmt.Sprintf(format, a...) 940 } 941 } 942 943 // ProgressWidth determines the width of the progress bar, when -1 there is not enough space for a progress bar 944 func (fw *Framework) ProgressWidth() int { 945 width, _, err := term.GetSize(0) 946 if err != nil { 947 width = 80 948 } 949 950 if width < 35 { 951 return -1 952 } 953 954 width -= 30 955 if width > 80 { 956 width = 80 957 } 958 959 return width 960 } 961 962 // GovernorSubject the subject to use for choria managed Governors 963 func (fw *Framework) GovernorSubject(name string) string { 964 return util.GovernorSubject(name, fw.Config.MainCollective) 965 } 966 967 // NewGovernor creates a new governor client with its own connection when none is given 968 func (fw *Framework) NewGovernor(ctx context.Context, name string, conn inter.Connector, opts ...governor.Option) (governor.Governor, inter.Connector, error) { 969 var err error 970 if conn == nil { 971 conn, err = fw.NewConnector(ctx, fw.MiddlewareServers, fmt.Sprintf("governor client: %s", name), fw.Logger("governor")) 972 if err != nil { 973 return nil, nil, err 974 } 975 } 976 977 return governor.New(name, conn.Nats(), opts...), conn, nil 978 } 979 980 // NewGovernorManager creates a new governor manager with its own connection when none is given 981 func (fw *Framework) NewGovernorManager(ctx context.Context, name string, limit uint64, maxAge time.Duration, replicas uint, update bool, conn inter.Connector, opts ...governor.Option) (governor.Manager, inter.Connector, error) { 982 var err error 983 984 if conn == nil { 985 conn, err = fw.NewConnector(ctx, fw.MiddlewareServers, fmt.Sprintf("governor manager: %s", name), fw.Logger("governor")) 986 if err != nil { 987 return nil, nil, err 988 } 989 } 990 991 gov, err := governor.NewManager(name, limit, maxAge, replicas, conn.Nats(), update, opts...) 992 if err != nil { 993 return nil, nil, err 994 } 995 996 return gov, conn, nil 997 } 998 999 // NewElection establishes a new, named, leader election requiring a Choria Streams bucket called CHORIA_LEADER_ELECTION. 1000 // This will create a new network connection per election, see NewElectionWithConn() to re-use an existing connection 1001 func (fw *Framework) NewElection(ctx context.Context, conn inter.Connector, name string, imported bool, opts ...election.Option) (inter.Election, error) { 1002 e, _, err := fw.NewElectionWithConn(ctx, conn, name, imported, opts...) 1003 1004 return e, err 1005 } 1006 1007 // NewElectionWithConn establish a new, named, leader election requiring a Choria Streams bucket called CHORIA_LEADER_ELECTION. 1008 func (fw *Framework) NewElectionWithConn(ctx context.Context, conn inter.Connector, name string, imported bool, opts ...election.Option) (inter.Election, inter.Connector, error) { 1009 var err error 1010 1011 if conn == nil { 1012 conn, err = fw.NewConnector(ctx, fw.MiddlewareServers, fmt.Sprintf("election %s %s", name, fw.Config.Identity), fw.Logger("election")) 1013 if err != nil { 1014 return nil, nil, err 1015 } 1016 } 1017 1018 var jsopt []nats.JSOpt 1019 if imported { 1020 jsopt = append(jsopt, nats.APIPrefix("choria.streams")) 1021 } 1022 1023 js, err := conn.Nats().JetStream(jsopt...) 1024 if err != nil { 1025 return nil, nil, fmt.Errorf("cannot connect to Choria Streams: %s", err) 1026 } 1027 1028 kv, err := js.KeyValue("CHORIA_LEADER_ELECTION") 1029 if err != nil { 1030 return nil, nil, fmt.Errorf("cannot access KV Bucket CHORIA_LEADER_ELECTION") 1031 } 1032 1033 e, err := election.NewElection(fw.Config.Identity, name, kv, opts...) 1034 if err != nil { 1035 return nil, nil, err 1036 } 1037 1038 return e, conn, nil 1039 } 1040 1041 // KV creates a connection to a key-value store and gives access to the connector 1042 func (fw *Framework) KV(ctx context.Context, conn inter.Connector, bucket string, create bool, opts ...kv.Option) (nats.KeyValue, error) { 1043 kv, _, err := fw.KVWithConn(ctx, conn, bucket, create, opts...) 1044 return kv, err 1045 } 1046 1047 // KVWithConn creates a connection to a key-value store and gives access to the connector 1048 func (fw *Framework) KVWithConn(ctx context.Context, conn inter.Connector, bucket string, create bool, opts ...kv.Option) (nats.KeyValue, inter.Connector, error) { 1049 logger := fw.Logger("kv") 1050 1051 var err error 1052 1053 if conn == nil { 1054 conn, err = fw.NewConnector(ctx, fw.MiddlewareServers, fmt.Sprintf("kv %s", fw.CallerID()), logger) 1055 if err != nil { 1056 return nil, nil, err 1057 } 1058 } 1059 1060 b, err := kv.NewKV(conn.Nats(), bucket, create, opts...) 1061 if err != nil { 1062 return nil, nil, err 1063 } 1064 1065 return b, conn, err 1066 } 1067 1068 func (fw *Framework) DDLResolvers() ([]inter.DDLResolver, error) { 1069 resolvers := []inter.DDLResolver{ 1070 &ddlresolver.InternalCachedDDLResolver{}, 1071 &ddlresolver.FileSystemDDLResolver{}, 1072 } 1073 1074 if fw.Config.Choria.RegistryClientCache != "" { 1075 resolvers = append(resolvers, &ddlresolver.RegistryDDLResolver{}) 1076 } 1077 1078 return resolvers, nil 1079 }