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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package providers
     5  
     6  import (
     7  	"errors"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/rs/zerolog/log"
    12  	"go.mondoo.com/cnquery/llx"
    13  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    14  	"go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    15  	"go.mondoo.com/cnquery/providers-sdk/v1/resources"
    16  	"go.mondoo.com/cnquery/providers-sdk/v1/upstream"
    17  	"go.mondoo.com/cnquery/types"
    18  	"go.mondoo.com/cnquery/utils/multierr"
    19  	"google.golang.org/grpc/status"
    20  )
    21  
    22  const defaultShutdownTimeout = time.Duration(time.Second * 120)
    23  
    24  // Runtimes are associated with one asset and carry all providers
    25  // and open connections for that asset.
    26  type Runtime struct {
    27  	Provider       *ConnectedProvider
    28  	UpstreamConfig *upstream.UpstreamConfig
    29  	Recording      Recording
    30  	AutoUpdate     UpdateProvidersConfig
    31  
    32  	features []byte
    33  	// coordinator is used to grab providers
    34  	coordinator *coordinator
    35  	// providers for with open connections
    36  	providers map[string]*ConnectedProvider
    37  	// schema aggregates all resources executable on this asset
    38  	schema          extensibleSchema
    39  	isClosed        bool
    40  	isEphemeral     bool
    41  	close           sync.Once
    42  	shutdownTimeout time.Duration
    43  }
    44  
    45  type ConnectedProvider struct {
    46  	Instance   *RunningProvider
    47  	Connection *plugin.ConnectRes
    48  }
    49  
    50  func (c *coordinator) RuntimeWithShutdownTimeout(timeout time.Duration) *Runtime {
    51  	runtime := c.NewRuntime()
    52  	runtime.shutdownTimeout = timeout
    53  	return runtime
    54  }
    55  
    56  type shutdownResult struct {
    57  	Response *plugin.ShutdownRes
    58  	Error    error
    59  }
    60  
    61  func (r *Runtime) tryShutdown() shutdownResult {
    62  	// Ephemeral runtimes have their primary provider be ephemeral, i.e. non-shared.
    63  	// All other providers are shared and will not be shut down from within the provider.
    64  	if r.isEphemeral {
    65  		err := r.coordinator.Stop(r.Provider.Instance, true)
    66  		return shutdownResult{Error: err}
    67  	}
    68  
    69  	return shutdownResult{}
    70  }
    71  
    72  func (r *Runtime) Close() {
    73  	r.isClosed = true
    74  	r.close.Do(func() {
    75  		if err := r.Recording.Save(); err != nil {
    76  			log.Error().Err(err).Msg("failed to save recording")
    77  		}
    78  
    79  		response := make(chan shutdownResult, 1)
    80  		go func() {
    81  			response <- r.tryShutdown()
    82  		}()
    83  		select {
    84  		case <-time.After(r.shutdownTimeout):
    85  			log.Error().Str("provider", r.Provider.Instance.Name).Msg("timed out shutting down the provider")
    86  		case result := <-response:
    87  			if result.Error != nil {
    88  				log.Error().Err(result.Error).Msg("failed to shutdown the provider")
    89  			}
    90  		}
    91  
    92  		// TODO: ideally, we try to close the provider here but only if there are no more assets that need it
    93  		// r.coordinator.Close(r.Provider.Instance)
    94  		r.schema.Close()
    95  	})
    96  }
    97  
    98  func (r *Runtime) DeactivateProviderDiscovery() {
    99  	r.schema.allLoaded = true
   100  }
   101  
   102  func (r *Runtime) AssetMRN() string {
   103  	if r.Provider != nil && r.Provider.Connection != nil && r.Provider.Connection.Asset != nil {
   104  		return r.Provider.Connection.Asset.Mrn
   105  	}
   106  	return ""
   107  }
   108  
   109  // UseProvider sets the main provider for this runtime.
   110  func (r *Runtime) UseProvider(id string) error {
   111  	// We transfer isEphemeral here because:
   112  	// 1. If the runtime is not ephemeral, it behaves as a shared provider by
   113  	// default.
   114  	// 2. If the runtime is ephemeral, we only want the main provider to be
   115  	// ephemeral. All other providers by default are shared.
   116  	// (Note: In the future we plan to have an isolated runtime mode,
   117  	// where even other providers are ephemeral, ie not shared)
   118  	res, err := r.addProvider(id, r.isEphemeral)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	r.Provider = res
   124  	return nil
   125  }
   126  
   127  func (r *Runtime) AddConnectedProvider(c *ConnectedProvider) {
   128  	r.providers[c.Instance.ID] = c
   129  	r.schema.Add(c.Instance.Name, c.Instance.Schema)
   130  }
   131  
   132  func (r *Runtime) addProvider(id string, isEphemeral bool) (*ConnectedProvider, error) {
   133  	var running *RunningProvider
   134  	var err error
   135  	if isEphemeral {
   136  		running, err = r.coordinator.Start(id, true, r.AutoUpdate)
   137  		if err != nil {
   138  			return nil, err
   139  		}
   140  
   141  	} else {
   142  		// TODO: we need to detect only the shared running providers
   143  		running = r.coordinator.RunningByID[id]
   144  		if running == nil {
   145  			var err error
   146  			running, err = r.coordinator.Start(id, false, r.AutoUpdate)
   147  			if err != nil {
   148  				return nil, err
   149  			}
   150  		}
   151  	}
   152  
   153  	res := &ConnectedProvider{Instance: running}
   154  	r.AddConnectedProvider(res)
   155  
   156  	return res, nil
   157  }
   158  
   159  // DetectProvider will try to detect and start the right provider for this
   160  // runtime. Generally recommended when you receive an asset to be scanned,
   161  // but haven't initialized any provider. It will also try to install providers
   162  // if necessary (and enabled)
   163  func (r *Runtime) DetectProvider(asset *inventory.Asset) error {
   164  	if asset == nil {
   165  		return errors.New("please provide an asset to detect the provider")
   166  	}
   167  	if len(asset.Connections) == 0 {
   168  		return errors.New("asset has no connections, can't detect provider")
   169  	}
   170  
   171  	var errs multierr.Errors
   172  	for i := range asset.Connections {
   173  		conn := asset.Connections[i]
   174  		if conn.Type == "" {
   175  			log.Warn().Msg("no connection `type` provided in inventory, falling back to deprecated `backend` field")
   176  			conn.Type = inventory.ConnBackendToType(conn.Backend)
   177  		}
   178  
   179  		provider, err := EnsureProvider("", conn.Type, true, r.coordinator.Providers)
   180  		if err != nil {
   181  			errs.Add(err)
   182  			continue
   183  		}
   184  
   185  		return r.UseProvider(provider.ID)
   186  	}
   187  
   188  	return multierr.Wrap(errs.Deduplicate(), "cannot find provider for this asset")
   189  }
   190  
   191  // Connect to an asset using the main provider
   192  func (r *Runtime) Connect(req *plugin.ConnectReq) error {
   193  	if r.Provider == nil {
   194  		return errors.New("cannot connect, please select a provider first")
   195  	}
   196  
   197  	if req.Asset == nil {
   198  		return errors.New("cannot connect, no asset info provided")
   199  	}
   200  
   201  	asset := req.Asset
   202  	if len(asset.Connections) == 0 {
   203  		return errors.New("cannot connect to asset, no connection info provided")
   204  	}
   205  
   206  	r.features = req.Features
   207  	callbacks := providerCallbacks{
   208  		runtime: r,
   209  	}
   210  
   211  	var err error
   212  	r.Provider.Connection, err = r.Provider.Instance.Plugin.Connect(req, &callbacks)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	r.Recording.EnsureAsset(r.Provider.Connection.Asset, r.Provider.Instance.ID, r.Provider.Connection.Id, asset.Connections[0])
   217  	return nil
   218  }
   219  
   220  func (r *Runtime) CreateResource(name string, args map[string]*llx.Primitive) (llx.Resource, error) {
   221  	provider, info, err := r.lookupResourceProvider(name)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  	if info == nil {
   226  		return nil, errors.New("cannot create '" + name + "', no resource info found")
   227  	}
   228  	name = info.Id
   229  
   230  	// Resources without providers are bridging resources only. They are static in nature.
   231  	if provider == nil {
   232  		return &llx.MockResource{Name: name}, nil
   233  	}
   234  
   235  	if provider.Connection == nil {
   236  		return nil, errors.New("no connection to provider")
   237  	}
   238  
   239  	res, err := provider.Instance.Plugin.GetData(&plugin.DataReq{
   240  		Connection: provider.Connection.Id,
   241  		Resource:   name,
   242  		Args:       args,
   243  	})
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	if _, ok := r.Recording.GetResource(provider.Connection.Id, name, string(res.Data.Value)); !ok {
   249  		r.Recording.AddData(provider.Connection.Id, name, string(res.Data.Value), "", nil)
   250  	}
   251  
   252  	typ := types.Type(res.Data.Type)
   253  	return &llx.MockResource{Name: typ.ResourceName(), ID: string(res.Data.Value)}, nil
   254  }
   255  
   256  func (r *Runtime) CloneResource(src llx.Resource, id string, fields []string, args map[string]*llx.Primitive) (llx.Resource, error) {
   257  	name := src.MqlName()
   258  	srcID := src.MqlID()
   259  
   260  	provider, _, err := r.lookupResourceProvider(name)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	for i := range fields {
   266  		field := fields[i]
   267  		data, err := provider.Instance.Plugin.GetData(&plugin.DataReq{
   268  			Connection: provider.Connection.Id,
   269  			Resource:   name,
   270  			ResourceId: srcID,
   271  			Field:      field,
   272  		})
   273  		if err != nil {
   274  			return nil, err
   275  		}
   276  		args[field] = data.Data
   277  	}
   278  
   279  	args["__id"] = llx.StringPrimitive(id)
   280  
   281  	_, err = provider.Instance.Plugin.StoreData(&plugin.StoreReq{
   282  		Connection: provider.Connection.Id,
   283  		Resources: []*plugin.ResourceData{{
   284  			Name:   name,
   285  			Id:     id,
   286  			Fields: PrimitiveArgsToResultArgs(args),
   287  		}},
   288  	})
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	return &llx.MockResource{Name: name, ID: id}, nil
   294  }
   295  
   296  func (r *Runtime) Unregister(watcherUID string) error {
   297  	// TODO: we don't unregister just yet...
   298  	return nil
   299  }
   300  
   301  func fieldUID(resource string, id string, field string) string {
   302  	return resource + "\x00" + id + "\x00" + field
   303  }
   304  
   305  // WatchAndUpdate a resource field and call the function if it changes with its current value
   306  func (r *Runtime) WatchAndUpdate(resource llx.Resource, field string, watcherUID string, callback func(res interface{}, err error)) error {
   307  	raw, err := r.watchAndUpdate(resource.MqlName(), resource.MqlID(), field, watcherUID)
   308  	if raw != nil {
   309  		callback(raw.Value, raw.Error)
   310  	}
   311  	return err
   312  }
   313  
   314  func (r *Runtime) watchAndUpdate(resource string, resourceID string, field string, watcherUID string) (*llx.RawData, error) {
   315  	provider, info, fieldInfo, err := r.lookupFieldProvider(resource, field)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  	if fieldInfo == nil {
   320  		return nil, errors.New("cannot get field '" + field + "' for resource '" + resource + "'")
   321  	}
   322  
   323  	if info.Provider != fieldInfo.Provider {
   324  		// technically we don't need to look up the resource provider, since
   325  		// it had to have been called beforehand to get here
   326  		_, err := provider.Instance.Plugin.StoreData(&plugin.StoreReq{
   327  			Connection: provider.Connection.Id,
   328  			Resources: []*plugin.ResourceData{{
   329  				Name: resource,
   330  				Id:   resourceID,
   331  			}},
   332  		})
   333  		if err != nil {
   334  			return nil, multierr.Wrap(err, "failed to create reference resource "+resource+" in provider "+provider.Instance.Name)
   335  		}
   336  	}
   337  
   338  	if cached, ok := r.Recording.GetData(provider.Connection.Id, resource, resourceID, field); ok {
   339  		return cached, nil
   340  	}
   341  
   342  	data, err := provider.Instance.Plugin.GetData(&plugin.DataReq{
   343  		Connection: provider.Connection.Id,
   344  		Resource:   resource,
   345  		ResourceId: resourceID,
   346  		Field:      field,
   347  	})
   348  	if err != nil {
   349  		// Recoverable errors can continue with the exeuction,
   350  		// they only store errors in the place of actual data.
   351  		// Every other error is thrown up the chain.
   352  		handled, err := r.handlePluginError(err, provider)
   353  		if !handled {
   354  			return nil, err
   355  		}
   356  		data = &plugin.DataRes{Error: err.Error()}
   357  	}
   358  
   359  	var raw *llx.RawData
   360  	if data.Error != "" {
   361  		raw = &llx.RawData{Error: errors.New(data.Error)}
   362  	} else {
   363  		raw = data.Data.RawData()
   364  	}
   365  
   366  	r.Recording.AddData(provider.Connection.Id, resource, resourceID, field, raw)
   367  	return raw, nil
   368  }
   369  
   370  func (r *Runtime) handlePluginError(err error, provider *ConnectedProvider) (bool, error) {
   371  	st, ok := status.FromError(err)
   372  	if !ok {
   373  		return false, err
   374  	}
   375  
   376  	switch st.Code() {
   377  	case 14:
   378  		// Error: Unavailable. Happens when the plugin crashes.
   379  		// TODO: try to restart the plugin and reset its connections
   380  		provider.Instance.isClosed = true
   381  		provider.Instance.err = errors.New("the '" + provider.Instance.Name + "' provider crashed")
   382  		return false, provider.Instance.err
   383  	}
   384  	return false, err
   385  }
   386  
   387  type providerCallbacks struct {
   388  	recording *assetRecording
   389  	runtime   *Runtime
   390  }
   391  
   392  func (p *providerCallbacks) GetRecording(req *plugin.DataReq) (*plugin.ResourceData, error) {
   393  	resource, ok := p.recording.resources[req.Resource+"\x00"+req.ResourceId]
   394  	if !ok {
   395  		return nil, nil
   396  	}
   397  
   398  	res := plugin.ResourceData{
   399  		Name:   req.Resource,
   400  		Id:     req.ResourceId,
   401  		Fields: make(map[string]*llx.Result, len(resource.Fields)),
   402  	}
   403  	for k, v := range resource.Fields {
   404  		res.Fields[k] = v.Result()
   405  	}
   406  
   407  	return &res, nil
   408  }
   409  
   410  func (p *providerCallbacks) GetData(req *plugin.DataReq) (*plugin.DataRes, error) {
   411  	if req.Field == "" {
   412  		res, err := p.runtime.CreateResource(req.Resource, req.Args)
   413  		if err != nil {
   414  			return nil, err
   415  		}
   416  
   417  		return &plugin.DataRes{
   418  			Data: &llx.Primitive{
   419  				Type:  string(types.Resource(res.MqlName())),
   420  				Value: []byte(res.MqlID()),
   421  			},
   422  		}, nil
   423  	}
   424  
   425  	raw, err := p.runtime.watchAndUpdate(req.Resource, req.ResourceId, req.Field, "")
   426  	if raw == nil {
   427  		return nil, err
   428  	}
   429  	res := raw.Result()
   430  	return &plugin.DataRes{
   431  		Data:  res.Data,
   432  		Error: res.Error,
   433  	}, err
   434  }
   435  
   436  func (p *providerCallbacks) Collect(req *plugin.DataRes) error {
   437  	panic("NOT YET IMPLEMENTED")
   438  	return nil
   439  }
   440  
   441  func (r *Runtime) SetRecording(recording Recording) error {
   442  	r.Recording = recording
   443  	if r.Provider == nil || r.Provider.Instance == nil {
   444  		log.Warn().Msg("set recording while no provider is set on runtime")
   445  		return nil
   446  	}
   447  	if r.Provider.Instance.ID != mockProvider.ID {
   448  		return nil
   449  	}
   450  
   451  	service := r.Provider.Instance.Plugin.(*mockProviderService)
   452  	// TODO: This is problematic, since we don't have multiple instances of
   453  	// the service!!
   454  	service.runtime = r
   455  
   456  	return nil
   457  }
   458  
   459  func baseRecording(anyRecording Recording) *recording {
   460  	var baseRecording *recording
   461  	switch x := anyRecording.(type) {
   462  	case *recording:
   463  		baseRecording = x
   464  	case *readOnlyRecording:
   465  		baseRecording = x.recording
   466  	}
   467  	return baseRecording
   468  }
   469  
   470  // SetMockRecording is only used for test utilities. Please do not use it!
   471  //
   472  // Deprecated: This function may not be necessary anymore, consider removing.
   473  func (r *Runtime) SetMockRecording(anyRecording Recording, providerID string, mockConnection bool) error {
   474  	r.Recording = anyRecording
   475  
   476  	baseRecording := baseRecording(anyRecording)
   477  	if baseRecording == nil {
   478  		return nil
   479  	}
   480  
   481  	provider, ok := r.providers[providerID]
   482  	if !ok {
   483  		return errors.New("cannot set recording, provider '" + providerID + "' not found")
   484  	}
   485  
   486  	assetRecording := &baseRecording.Assets[0]
   487  	asset := assetRecording.Asset.ToInventory()
   488  
   489  	if mockConnection {
   490  		// Dom: we may need to retain the original asset ID, not sure yet...
   491  		asset.Id = "mock-asset"
   492  		asset.Connections = []*inventory.Config{{
   493  			Type: "mock",
   494  		}}
   495  
   496  		callbacks := providerCallbacks{
   497  			recording: assetRecording,
   498  			runtime:   r,
   499  		}
   500  
   501  		res, err := provider.Instance.Plugin.Connect(&plugin.ConnectReq{
   502  			Asset:        asset,
   503  			HasRecording: true,
   504  		}, &callbacks)
   505  		if err != nil {
   506  			return multierr.Wrap(err, "failed to set mock connection for recording")
   507  		}
   508  		provider.Connection = res
   509  	}
   510  
   511  	if provider.Connection == nil {
   512  		// Dom: we may need to cancel the entire setup here, may need to be reconsidered...
   513  		log.Warn().Msg("recording cannot determine asset, no connection was set up!")
   514  	} else {
   515  		baseRecording.assets[provider.Connection.Id] = assetRecording
   516  	}
   517  
   518  	return nil
   519  }
   520  
   521  func (r *Runtime) lookupResourceProvider(resource string) (*ConnectedProvider, *resources.ResourceInfo, error) {
   522  	info := r.schema.Lookup(resource)
   523  	if info == nil {
   524  		return nil, nil, errors.New("cannot find resource '" + resource + "' in schema")
   525  	}
   526  
   527  	if info.Provider == "" {
   528  		// This case happens when the resource is only bridging a resource chain,
   529  		// i.e. it is extending in nature (which we only test for the warning).
   530  		if !info.IsExtension {
   531  			log.Warn().Msg("found a resource without a provider: '" + resource + "'")
   532  		}
   533  		return nil, info, nil
   534  	}
   535  
   536  	if provider := r.providers[info.Provider]; provider != nil {
   537  		return provider, info, nil
   538  	}
   539  
   540  	res, err := r.addProvider(info.Provider, false)
   541  	if err != nil {
   542  		return nil, nil, multierr.Wrap(err, "failed to start provider '"+info.Provider+"'")
   543  	}
   544  
   545  	conn, err := res.Instance.Plugin.Connect(&plugin.ConnectReq{
   546  		Features: r.features,
   547  		Asset:    r.Provider.Connection.Asset,
   548  	}, nil)
   549  	if err != nil {
   550  		return nil, nil, err
   551  	}
   552  
   553  	res.Connection = conn
   554  
   555  	return res, info, nil
   556  }
   557  
   558  func (r *Runtime) lookupFieldProvider(resource string, field string) (*ConnectedProvider, *resources.ResourceInfo, *resources.Field, error) {
   559  	resourceInfo, fieldInfo := r.schema.LookupField(resource, field)
   560  	if resourceInfo == nil {
   561  		return nil, nil, nil, errors.New("cannot find resource '" + resource + "' in schema")
   562  	}
   563  	if fieldInfo == nil {
   564  		return nil, nil, nil, errors.New("cannot find field '" + field + "' in resource '" + resource + "'")
   565  	}
   566  
   567  	if provider := r.providers[fieldInfo.Provider]; provider != nil {
   568  		return provider, resourceInfo, fieldInfo, nil
   569  	}
   570  
   571  	res, err := r.addProvider(fieldInfo.Provider, false)
   572  	if err != nil {
   573  		return nil, nil, nil, multierr.Wrap(err, "failed to start provider '"+fieldInfo.Provider+"'")
   574  	}
   575  
   576  	conn, err := res.Instance.Plugin.Connect(&plugin.ConnectReq{
   577  		Features: r.features,
   578  		Asset:    r.Provider.Connection.Asset,
   579  	}, nil)
   580  	if err != nil {
   581  		return nil, nil, nil, err
   582  	}
   583  
   584  	res.Connection = conn
   585  
   586  	return res, resourceInfo, fieldInfo, nil
   587  }
   588  
   589  func (r *Runtime) Schema() llx.Schema {
   590  	return &r.schema
   591  }
   592  
   593  func (r *Runtime) AddSchema(name string, schema *resources.Schema) {
   594  	r.schema.Add(name, schema)
   595  }
   596  
   597  func (r *Runtime) asset() *inventory.Asset {
   598  	if r.Provider == nil || r.Provider.Connection == nil {
   599  		return nil
   600  	}
   601  	return r.Provider.Connection.Asset
   602  }