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 }