go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/coordinator.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package providers
     5  
     6  import (
     7  	"os"
     8  	"os/exec"
     9  	"strconv"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/cockroachdb/errors"
    14  	"github.com/hashicorp/go-plugin"
    15  	"github.com/muesli/termenv"
    16  	"github.com/rs/zerolog/log"
    17  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    18  	pp "go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    19  	"go.mondoo.com/cnquery/providers-sdk/v1/resources"
    20  	coreconf "go.mondoo.com/cnquery/providers/core/config"
    21  	"go.mondoo.com/cnquery/providers/core/resources/versions/semver"
    22  )
    23  
    24  var BuiltinCoreID = coreconf.Config.ID
    25  
    26  var Coordinator = coordinator{
    27  	RunningByID:      map[string]*RunningProvider{},
    28  	RunningEphemeral: map[*RunningProvider]struct{}{},
    29  	runtimes:         map[string]*Runtime{},
    30  }
    31  
    32  type coordinator struct {
    33  	Providers        Providers
    34  	RunningByID      map[string]*RunningProvider
    35  	RunningEphemeral map[*RunningProvider]struct{}
    36  
    37  	unprocessedRuntimes []*Runtime
    38  	runtimes            map[string]*Runtime
    39  	runtimeCnt          int
    40  	mutex               sync.Mutex
    41  }
    42  
    43  type builtinProvider struct {
    44  	Runtime *RunningProvider
    45  	Config  *pp.Provider
    46  }
    47  
    48  type RunningProvider struct {
    49  	Name   string
    50  	ID     string
    51  	Plugin pp.ProviderPlugin
    52  	Client *plugin.Client
    53  	Schema *resources.Schema
    54  
    55  	// isClosed is true for any provider that is not running anymore,
    56  	// either via shutdown or via crash
    57  	isClosed bool
    58  	// isShutdown is only used once during provider shutdown
    59  	isShutdown bool
    60  	// provider errors which are evaluated and printed during shutdown of the provider
    61  	err  error
    62  	lock sync.Mutex
    63  }
    64  
    65  func (p *RunningProvider) Shutdown() error {
    66  	p.lock.Lock()
    67  	defer p.lock.Unlock()
    68  
    69  	if p.isShutdown {
    70  		return nil
    71  	}
    72  
    73  	// This is an error that happened earlier, so we print it directly.
    74  	// The error this function returns is about failing to shutdown.
    75  	if p.err != nil {
    76  		log.Error().Msg(p.err.Error())
    77  	}
    78  
    79  	var err error
    80  	if !p.isClosed {
    81  		_, err = p.Plugin.Shutdown(&pp.ShutdownReq{})
    82  		if err != nil {
    83  			log.Debug().Err(err).Str("plugin", p.Name).Msg("error in plugin shutdown")
    84  		}
    85  
    86  		// If the plugin was not in active use, we may not have a client at this
    87  		// point. Since all of this is run within a sync-lock, we can check the
    88  		// client and if it exists use it to send the kill signal.
    89  		if p.Client != nil {
    90  			p.Client.Kill()
    91  		}
    92  		p.isClosed = true
    93  	}
    94  
    95  	p.isShutdown = true
    96  	return err
    97  }
    98  
    99  type UpdateProvidersConfig struct {
   100  	// if true, will try to update providers when new versions are available
   101  	Enabled bool
   102  	// seconds until we try to refresh the providers version again
   103  	RefreshInterval int
   104  }
   105  
   106  func (c *coordinator) Start(id string, isEphemeral bool, update UpdateProvidersConfig) (*RunningProvider, error) {
   107  	if x, ok := builtinProviders[id]; ok {
   108  		// We don't warn for core providers, which are the only providers
   109  		// built into the binary (for now).
   110  		if id != BuiltinCoreID && id != mockProvider.ID {
   111  			log.Warn().Msg("using builtin provider for " + x.Config.Name)
   112  		}
   113  		if id == mockProvider.ID {
   114  			mp := x.Runtime.Plugin.(*mockProviderService)
   115  			mp.Init(x.Runtime)
   116  		}
   117  		return x.Runtime, nil
   118  	}
   119  
   120  	if c.Providers == nil {
   121  		var err error
   122  		c.Providers, err = ListActive()
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  	}
   127  
   128  	provider, ok := c.Providers[id]
   129  	if !ok {
   130  		return nil, errors.New("cannot find provider " + id)
   131  	}
   132  
   133  	if update.Enabled {
   134  		// We do not stop on failed updates. Up until some other errors happens,
   135  		// things are still functional. We want to consider failure, possibly
   136  		// with a config entry in the future.
   137  		updated, err := c.tryProviderUpdate(provider, update)
   138  		if err != nil {
   139  			log.Error().
   140  				Err(err).
   141  				Str("provider", provider.Name).
   142  				Msg("failed to update provider")
   143  		} else {
   144  			provider = updated
   145  		}
   146  	}
   147  
   148  	if provider.Schema == nil {
   149  		if err := provider.LoadResources(); err != nil {
   150  			return nil, errors.Wrap(err, "failed to load provider "+id+" resources info")
   151  		}
   152  	}
   153  
   154  	pluginCmd := exec.Command(provider.binPath(), "run_as_plugin")
   155  	log.Debug().Str("path", pluginCmd.Path).Msg("running provider plugin")
   156  
   157  	addColorConfig(pluginCmd)
   158  
   159  	client := plugin.NewClient(&plugin.ClientConfig{
   160  		HandshakeConfig: pp.Handshake,
   161  		Plugins:         pp.PluginMap,
   162  		Cmd:             pluginCmd,
   163  		AllowedProtocols: []plugin.Protocol{
   164  			plugin.ProtocolNetRPC, plugin.ProtocolGRPC,
   165  		},
   166  		Logger: &hclogger{},
   167  		Stderr: os.Stderr,
   168  	})
   169  
   170  	// Connect via RPC
   171  	rpcClient, err := client.Client()
   172  	if err != nil {
   173  		client.Kill()
   174  		return nil, errors.Wrap(err, "failed to initialize plugin client")
   175  	}
   176  
   177  	// Request the plugin
   178  	pluginName := "provider"
   179  	raw, err := rpcClient.Dispense(pluginName)
   180  	if err != nil {
   181  		client.Kill()
   182  		return nil, errors.Wrap(err, "failed to call "+pluginName+" plugin")
   183  	}
   184  
   185  	res := &RunningProvider{
   186  		Name:   provider.Name,
   187  		ID:     provider.ID,
   188  		Plugin: raw.(pp.ProviderPlugin),
   189  		Client: client,
   190  		Schema: provider.Schema,
   191  	}
   192  
   193  	c.mutex.Lock()
   194  	if isEphemeral {
   195  		c.RunningEphemeral[res] = struct{}{}
   196  	} else {
   197  		c.RunningByID[res.ID] = res
   198  	}
   199  	c.mutex.Unlock()
   200  
   201  	return res, nil
   202  }
   203  
   204  type ProviderVersions struct {
   205  	Providers []ProviderVersion `json:"providers"`
   206  }
   207  
   208  type ProviderVersion struct {
   209  	Name    string `json:"name"`
   210  	Version string `json:"version"`
   211  }
   212  
   213  func (c *coordinator) tryProviderUpdate(provider *Provider, update UpdateProvidersConfig) (*Provider, error) {
   214  	if provider.Path == "" {
   215  		return nil, errors.New("cannot determine installation path for provider")
   216  	}
   217  
   218  	binPath := provider.binPath()
   219  	stat, err := os.Stat(binPath)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	if update.RefreshInterval > 0 {
   225  		mtime := stat.ModTime()
   226  		secs := time.Since(mtime).Seconds()
   227  		if secs < float64(update.RefreshInterval) {
   228  			lastRefresh := time.Since(mtime).String()
   229  			log.Debug().
   230  				Str("last-refresh", lastRefresh).
   231  				Str("provider", provider.Name).
   232  				Msg("no need to update provider")
   233  			return provider, nil
   234  		}
   235  	}
   236  
   237  	latest, err := LatestVersion(provider.Name)
   238  	if err != nil {
   239  		log.Warn().Msg(err.Error())
   240  		// we can just continue with the existing provider, no need to error up,
   241  		// the warning is enough since we are still functional
   242  		return provider, nil
   243  	}
   244  
   245  	semver := semver.Parser{}
   246  	diff, err := semver.Compare(provider.Version, latest)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	if diff >= 0 {
   251  		return provider, nil
   252  	}
   253  
   254  	log.Info().
   255  		Str("installed", provider.Version).
   256  		Str("latest", latest).
   257  		Msg("found a new version for '" + provider.Name + "' provider")
   258  	provider, err = installVersion(provider.Name, latest)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	PrintInstallResults([]*Provider{provider})
   263  	now := time.Now()
   264  	if err := os.Chtimes(binPath, now, now); err != nil {
   265  		log.Warn().
   266  			Str("provider", provider.Name).
   267  			Msg("failed to update refresh time on provider")
   268  	}
   269  
   270  	return provider, nil
   271  }
   272  
   273  func (c *coordinator) NewRuntime() *Runtime {
   274  	return c.newRuntime(false)
   275  }
   276  
   277  func (c *coordinator) newRuntime(isEphemeral bool) *Runtime {
   278  	res := &Runtime{
   279  		coordinator: c,
   280  		providers:   map[string]*ConnectedProvider{},
   281  		schema: extensibleSchema{
   282  			loaded: map[string]struct{}{},
   283  			Schema: resources.Schema{
   284  				Resources: map[string]*resources.ResourceInfo{},
   285  			},
   286  		},
   287  		Recording:       NullRecording{},
   288  		shutdownTimeout: defaultShutdownTimeout,
   289  		isEphemeral:     isEphemeral,
   290  	}
   291  	res.schema.runtime = res
   292  
   293  	// TODO: do this dynamically in the future
   294  	res.schema.loadAllSchemas()
   295  
   296  	if !isEphemeral {
   297  		c.mutex.Lock()
   298  		c.unprocessedRuntimes = append(c.unprocessedRuntimes, res)
   299  		c.runtimeCnt++
   300  		cnt := c.runtimeCnt
   301  		c.mutex.Unlock()
   302  		log.Debug().Msg("Started a new runtime (" + strconv.Itoa(cnt) + " total)")
   303  	}
   304  
   305  	return res
   306  }
   307  
   308  func (c *coordinator) NewRuntimeFrom(parent *Runtime) *Runtime {
   309  	res := c.NewRuntime()
   310  	res.Recording = parent.Recording
   311  	for k, v := range parent.providers {
   312  		res.providers[k] = v
   313  	}
   314  	return res
   315  }
   316  
   317  // RuntimFor an asset will return a new or existing runtime for a given asset.
   318  // If a runtime for this asset already exists, it will re-use it. If the runtime
   319  // is new, it will create it and detect the provider.
   320  // The asset and parent must be defined.
   321  func (c *coordinator) RuntimeFor(asset *inventory.Asset, parent *Runtime) (*Runtime, error) {
   322  	c.mutex.Lock()
   323  	c.unsafeRefreshRuntimes()
   324  	res := c.unsafeGetAssetRuntime(asset)
   325  	c.mutex.Unlock()
   326  
   327  	if res != nil {
   328  		return res, nil
   329  	}
   330  
   331  	res = c.NewRuntimeFrom(parent)
   332  	return res, res.DetectProvider(asset)
   333  }
   334  
   335  // EphemeralRuntimeFor an asset, creates a new ephemeral runtime and connectors.
   336  // These are designed to be thrown away at the end of their use.
   337  // Note: at the time of writing they may still share auxiliary providers with
   338  // other runtimes, e.g. if provider X spawns another provider Y, the latter
   339  // may be a shared provider. The majority of memory load should be on the
   340  // primary provider (eg X in this case) so that it can effectively clear
   341  // its memory at the end of its use.
   342  func (c *coordinator) EphemeralRuntimeFor(asset *inventory.Asset) (*Runtime, error) {
   343  	res := c.newRuntime(true)
   344  	return res, res.DetectProvider(asset)
   345  }
   346  
   347  // Only call this with a mutex lock around it!
   348  func (c *coordinator) unsafeRefreshRuntimes() {
   349  	var remaining []*Runtime
   350  	for i := range c.unprocessedRuntimes {
   351  		rt := c.unprocessedRuntimes[i]
   352  		if asset := rt.asset(); asset == nil || !c.unsafeSetAssetRuntime(asset, rt) {
   353  			remaining = append(remaining, rt)
   354  		}
   355  	}
   356  	c.unprocessedRuntimes = remaining
   357  }
   358  
   359  func (c *coordinator) unsafeGetAssetRuntime(asset *inventory.Asset) *Runtime {
   360  	if asset.Mrn != "" {
   361  		if rt := c.runtimes[asset.Mrn]; rt != nil {
   362  			return rt
   363  		}
   364  	}
   365  	for _, id := range asset.PlatformIds {
   366  		if rt := c.runtimes[id]; rt != nil {
   367  			return rt
   368  		}
   369  	}
   370  	return nil
   371  }
   372  
   373  // Returns true if we were able to set the runtime index for this asset,
   374  // i.e. if either the MRN and/or its platform IDs were identified.
   375  func (c *coordinator) unsafeSetAssetRuntime(asset *inventory.Asset, runtime *Runtime) bool {
   376  	found := false
   377  	if asset.Mrn != "" {
   378  		c.runtimes[asset.Mrn] = runtime
   379  		found = true
   380  	}
   381  	for _, id := range asset.PlatformIds {
   382  		c.runtimes[id] = runtime
   383  		found = true
   384  	}
   385  	return found
   386  }
   387  
   388  func (c *coordinator) Stop(provider *RunningProvider, isEphemeral bool) error {
   389  	if provider == nil {
   390  		return nil
   391  	}
   392  	c.mutex.Lock()
   393  	defer c.mutex.Unlock()
   394  
   395  	if isEphemeral {
   396  		delete(c.RunningEphemeral, provider)
   397  	} else {
   398  		found := c.RunningByID[provider.ID]
   399  		if found != nil {
   400  			delete(c.RunningByID, provider.ID)
   401  		}
   402  	}
   403  
   404  	return provider.Shutdown()
   405  }
   406  
   407  func (c *coordinator) Shutdown() {
   408  	c.mutex.Lock()
   409  
   410  	for cur := range c.RunningEphemeral {
   411  		if err := cur.Shutdown(); err != nil {
   412  			log.Warn().Err(err).Str("provider", cur.Name).Msg("failed to shut down provider")
   413  		}
   414  		cur.isClosed = true
   415  		cur.Client.Kill()
   416  	}
   417  	c.RunningEphemeral = map[*RunningProvider]struct{}{}
   418  
   419  	for _, runtime := range c.RunningByID {
   420  		if err := runtime.Shutdown(); err != nil {
   421  			log.Warn().Err(err).Str("provider", runtime.Name).Msg("failed to shut down provider")
   422  		}
   423  		runtime.isClosed = true
   424  		runtime.Client.Kill()
   425  	}
   426  	c.RunningByID = map[string]*RunningProvider{}
   427  
   428  	c.mutex.Unlock()
   429  }
   430  
   431  func (c *coordinator) LoadSchema(name string) (*resources.Schema, error) {
   432  	if x, ok := builtinProviders[name]; ok {
   433  		return x.Runtime.Schema, nil
   434  	}
   435  
   436  	provider, ok := c.Providers[name]
   437  	if !ok {
   438  		return nil, errors.New("cannot find provider '" + name + "'")
   439  	}
   440  
   441  	if provider.Schema == nil {
   442  		if err := provider.LoadResources(); err != nil {
   443  			return nil, errors.Wrap(err, "failed to load provider '"+name+"' resources info")
   444  		}
   445  	}
   446  
   447  	return provider.Schema, nil
   448  }
   449  
   450  func addColorConfig(cmd *exec.Cmd) {
   451  	switch termenv.EnvColorProfile() {
   452  	case termenv.ANSI256, termenv.ANSI, termenv.TrueColor:
   453  		cmd.Env = append(cmd.Env, "CLICOLOR_FORCE=1")
   454  	default:
   455  		// it will default to no-color, since it's run as a plugin
   456  		// so there is nothing to do here
   457  	}
   458  }