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  }