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 }