github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/resource/deploy/deploytest/pluginhost.go (about) 1 // Copyright 2016-2018, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package deploytest 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "io" 22 "path/filepath" 23 "sync" 24 25 "github.com/blang/semver" 26 pbempty "github.com/golang/protobuf/ptypes/empty" 27 28 "google.golang.org/grpc" 29 30 "github.com/pulumi/pulumi/sdk/v3/go/common/diag" 31 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 32 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" 33 "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" 34 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 35 "github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil" 36 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 37 pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" 38 ) 39 40 var UseGrpcPluginsByDefault = false 41 42 type LoadPluginFunc func(opts interface{}) (interface{}, error) 43 type LoadPluginWithHostFunc func(opts interface{}, host plugin.Host) (interface{}, error) 44 45 type LoadProviderFunc func() (plugin.Provider, error) 46 type LoadProviderWithHostFunc func(host plugin.Host) (plugin.Provider, error) 47 48 type LoadAnalyzerFunc func(opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error) 49 type LoadAnalyzerWithHostFunc func(opts *plugin.PolicyAnalyzerOptions, host plugin.Host) (plugin.Analyzer, error) 50 51 type PluginOption func(p *PluginLoader) 52 53 func WithoutGrpc(p *PluginLoader) { 54 p.useGRPC = false 55 } 56 57 func WithGrpc(p *PluginLoader) { 58 p.useGRPC = true 59 } 60 61 func WithPath(path string) func(p *PluginLoader) { 62 return func(p *PluginLoader) { 63 p.path = path 64 } 65 } 66 67 type PluginLoader struct { 68 kind workspace.PluginKind 69 name string 70 version semver.Version 71 load LoadPluginFunc 72 loadWithHost LoadPluginWithHostFunc 73 path string 74 useGRPC bool 75 } 76 77 type ProviderOption = PluginOption 78 type ProviderLoader = PluginLoader 79 80 func NewProviderLoader(pkg tokens.Package, version semver.Version, load LoadProviderFunc, 81 opts ...ProviderOption) *ProviderLoader { 82 83 p := &ProviderLoader{ 84 kind: workspace.ResourcePlugin, 85 name: string(pkg), 86 version: version, 87 load: func(_ interface{}) (interface{}, error) { return load() }, 88 useGRPC: UseGrpcPluginsByDefault, 89 } 90 for _, o := range opts { 91 o(p) 92 } 93 return p 94 } 95 96 func NewProviderLoaderWithHost(pkg tokens.Package, version semver.Version, 97 load LoadProviderWithHostFunc, opts ...ProviderOption) *ProviderLoader { 98 99 p := &ProviderLoader{ 100 kind: workspace.ResourcePlugin, 101 name: string(pkg), 102 version: version, 103 loadWithHost: func(_ interface{}, host plugin.Host) (interface{}, error) { return load(host) }, 104 useGRPC: UseGrpcPluginsByDefault, 105 } 106 for _, o := range opts { 107 o(p) 108 } 109 return p 110 } 111 112 func NewAnalyzerLoader(name string, load LoadAnalyzerFunc, opts ...PluginOption) *PluginLoader { 113 p := &PluginLoader{ 114 kind: workspace.AnalyzerPlugin, 115 name: name, 116 load: func(optsI interface{}) (interface{}, error) { 117 opts, _ := optsI.(*plugin.PolicyAnalyzerOptions) 118 return load(opts) 119 }, 120 } 121 for _, o := range opts { 122 o(p) 123 } 124 return p 125 } 126 127 func NewAnalyzerLoaderWithHost(name string, load LoadAnalyzerWithHostFunc, opts ...PluginOption) *PluginLoader { 128 p := &PluginLoader{ 129 kind: workspace.AnalyzerPlugin, 130 name: name, 131 loadWithHost: func(optsI interface{}, host plugin.Host) (interface{}, error) { 132 opts, _ := optsI.(*plugin.PolicyAnalyzerOptions) 133 return load(opts, host) 134 }, 135 } 136 for _, o := range opts { 137 o(p) 138 } 139 return p 140 } 141 142 type nopCloserT int 143 144 func (nopCloserT) Close() error { return nil } 145 146 var nopCloser io.Closer = nopCloserT(0) 147 148 type grpcWrapper struct { 149 stop chan bool 150 } 151 152 func (w *grpcWrapper) Close() error { 153 go func() { w.stop <- true }() 154 return nil 155 } 156 157 func wrapProviderWithGrpc(provider plugin.Provider) (plugin.Provider, io.Closer, error) { 158 wrapper := &grpcWrapper{stop: make(chan bool)} 159 handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{ 160 Cancel: wrapper.stop, 161 Init: func(srv *grpc.Server) error { 162 pulumirpc.RegisterResourceProviderServer(srv, plugin.NewProviderServer(provider)) 163 return nil 164 }, 165 Options: rpcutil.OpenTracingServerInterceptorOptions(nil), 166 }) 167 if err != nil { 168 return nil, nil, fmt.Errorf("could not start resource provider service: %w", err) 169 } 170 conn, err := grpc.Dial( 171 fmt.Sprintf("127.0.0.1:%v", handle.Port), 172 grpc.WithInsecure(), 173 grpc.WithUnaryInterceptor(rpcutil.OpenTracingClientInterceptor()), 174 grpc.WithStreamInterceptor(rpcutil.OpenTracingStreamClientInterceptor()), 175 rpcutil.GrpcChannelOptions(), 176 ) 177 if err != nil { 178 contract.IgnoreClose(wrapper) 179 return nil, nil, fmt.Errorf("could not connect to resource provider service: %v", err) 180 } 181 wrapped := plugin.NewProviderWithClient(nil, provider.Pkg(), pulumirpc.NewResourceProviderClient(conn), false) 182 return wrapped, wrapper, nil 183 } 184 185 type hostEngine struct { 186 sink diag.Sink 187 statusSink diag.Sink 188 189 address string 190 stop chan bool 191 } 192 193 func (e *hostEngine) Log(_ context.Context, req *pulumirpc.LogRequest) (*pbempty.Empty, error) { 194 var sev diag.Severity 195 switch req.Severity { 196 case pulumirpc.LogSeverity_DEBUG: 197 sev = diag.Debug 198 case pulumirpc.LogSeverity_INFO: 199 sev = diag.Info 200 case pulumirpc.LogSeverity_WARNING: 201 sev = diag.Warning 202 case pulumirpc.LogSeverity_ERROR: 203 sev = diag.Error 204 default: 205 return nil, fmt.Errorf("Unrecognized logging severity: %v", req.Severity) 206 } 207 208 if req.Ephemeral { 209 e.statusSink.Logf(sev, diag.StreamMessage(resource.URN(req.Urn), req.Message, req.StreamId)) 210 } else { 211 e.sink.Logf(sev, diag.StreamMessage(resource.URN(req.Urn), req.Message, req.StreamId)) 212 } 213 return &pbempty.Empty{}, nil 214 } 215 func (e *hostEngine) GetRootResource(_ context.Context, 216 req *pulumirpc.GetRootResourceRequest) (*pulumirpc.GetRootResourceResponse, error) { 217 return nil, errors.New("unsupported") 218 } 219 func (e *hostEngine) SetRootResource(_ context.Context, 220 req *pulumirpc.SetRootResourceRequest) (*pulumirpc.SetRootResourceResponse, error) { 221 return nil, errors.New("unsupported") 222 } 223 224 type pluginHost struct { 225 pluginLoaders []*ProviderLoader 226 languageRuntime plugin.LanguageRuntime 227 sink diag.Sink 228 statusSink diag.Sink 229 230 engine *hostEngine 231 232 providers []plugin.Provider 233 analyzers []plugin.Analyzer 234 plugins map[interface{}]io.Closer 235 closed bool 236 m sync.Mutex 237 } 238 239 func NewPluginHost(sink, statusSink diag.Sink, languageRuntime plugin.LanguageRuntime, 240 pluginLoaders ...*ProviderLoader) plugin.Host { 241 242 engine := &hostEngine{ 243 sink: sink, 244 statusSink: statusSink, 245 stop: make(chan bool), 246 } 247 handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{ 248 Cancel: engine.stop, 249 Init: func(srv *grpc.Server) error { 250 pulumirpc.RegisterEngineServer(srv, engine) 251 return nil 252 }, 253 Options: rpcutil.OpenTracingServerInterceptorOptions(nil), 254 }) 255 if err != nil { 256 panic(fmt.Errorf("could not start engine service: %v", err)) 257 } 258 engine.address = fmt.Sprintf("127.0.0.1:%v", handle.Port) 259 260 return &pluginHost{ 261 pluginLoaders: pluginLoaders, 262 languageRuntime: languageRuntime, 263 sink: sink, 264 statusSink: statusSink, 265 engine: engine, 266 plugins: map[interface{}]io.Closer{}, 267 } 268 } 269 270 func (host *pluginHost) isClosed() bool { 271 host.m.Lock() 272 defer host.m.Unlock() 273 return host.closed 274 } 275 276 func (host *pluginHost) plugin(kind workspace.PluginKind, name string, version *semver.Version, 277 opts interface{}) (interface{}, error) { 278 279 var best *PluginLoader 280 for _, l := range host.pluginLoaders { 281 if l.kind != kind || l.name != name { 282 continue 283 } 284 285 if version != nil { 286 if l.version.EQ(*version) { 287 best = l 288 break 289 } 290 } else if best == nil || l.version.GT(best.version) { 291 best = l 292 } 293 } 294 if best == nil { 295 return nil, nil 296 } 297 298 load := best.load 299 if load == nil { 300 load = func(opts interface{}) (interface{}, error) { 301 return best.loadWithHost(opts, host) 302 } 303 } 304 305 plug, err := load(opts) 306 if err != nil { 307 return nil, err 308 } 309 310 closer := nopCloser 311 if best.useGRPC { 312 plug, closer, err = wrapProviderWithGrpc(plug.(plugin.Provider)) 313 if err != nil { 314 return nil, err 315 } 316 } 317 318 host.m.Lock() 319 defer host.m.Unlock() 320 321 switch kind { 322 case workspace.AnalyzerPlugin: 323 host.analyzers = append(host.analyzers, plug.(plugin.Analyzer)) 324 case workspace.ResourcePlugin: 325 host.providers = append(host.providers, plug.(plugin.Provider)) 326 } 327 328 host.plugins[plug] = closer 329 return plug, nil 330 } 331 332 func (host *pluginHost) Provider(pkg tokens.Package, version *semver.Version) (plugin.Provider, error) { 333 plug, err := host.plugin(workspace.ResourcePlugin, string(pkg), version, nil) 334 if err != nil { 335 return nil, err 336 } 337 if plug == nil { 338 v := "nil" 339 if version != nil { 340 v = version.String() 341 } 342 return nil, fmt.Errorf("Could not find plugin for (%s, %s)", pkg.String(), v) 343 } 344 return plug.(plugin.Provider), nil 345 } 346 347 func (host *pluginHost) LanguageRuntime(runtime string) (plugin.LanguageRuntime, error) { 348 return host.languageRuntime, nil 349 } 350 351 func (host *pluginHost) SignalCancellation() error { 352 host.m.Lock() 353 defer host.m.Unlock() 354 355 var err error 356 for _, prov := range host.providers { 357 if pErr := prov.SignalCancellation(); pErr != nil { 358 err = pErr 359 } 360 } 361 return err 362 } 363 func (host *pluginHost) Close() error { 364 host.m.Lock() 365 defer host.m.Unlock() 366 367 var err error 368 for _, closer := range host.plugins { 369 if pErr := closer.Close(); pErr != nil { 370 err = pErr 371 } 372 } 373 374 go func() { host.engine.stop <- true }() 375 host.closed = true 376 return err 377 } 378 func (host *pluginHost) ServerAddr() string { 379 return host.engine.address 380 } 381 func (host *pluginHost) Log(sev diag.Severity, urn resource.URN, msg string, streamID int32) { 382 if !host.isClosed() { 383 host.sink.Logf(sev, diag.StreamMessage(urn, msg, streamID)) 384 } 385 } 386 func (host *pluginHost) LogStatus(sev diag.Severity, urn resource.URN, msg string, streamID int32) { 387 if !host.isClosed() { 388 host.statusSink.Logf(sev, diag.StreamMessage(urn, msg, streamID)) 389 } 390 } 391 func (host *pluginHost) Analyzer(nm tokens.QName) (plugin.Analyzer, error) { 392 return host.PolicyAnalyzer(nm, "", nil) 393 } 394 func (host *pluginHost) CloseProvider(provider plugin.Provider) error { 395 host.m.Lock() 396 defer host.m.Unlock() 397 398 delete(host.plugins, provider) 399 return nil 400 } 401 func (host *pluginHost) EnsurePlugins(plugins []workspace.PluginSpec, kinds plugin.Flags) error { 402 return nil 403 } 404 func (host *pluginHost) InstallPlugin(plugin workspace.PluginSpec) error { 405 return nil 406 } 407 func (host *pluginHost) ResolvePlugin( 408 kind workspace.PluginKind, name string, version *semver.Version) (*workspace.PluginInfo, error) { 409 plugins := make([]workspace.PluginInfo, 0, len(host.pluginLoaders)) 410 411 for _, v := range host.pluginLoaders { 412 p := workspace.PluginInfo{ 413 Kind: v.kind, 414 Name: v.name, 415 Path: v.path, 416 Version: &v.version, 417 SchemaPath: filepath.Join(v.path, name+"-"+v.version.String()+".json"), 418 // SchemaTime not set as caching is indefinite. 419 } 420 plugins = append(plugins, p) 421 } 422 423 var semverRange semver.Range 424 if version == nil { 425 semverRange = func(v semver.Version) bool { 426 return true 427 } 428 } else { 429 // Require an exact match: 430 semverRange = version.EQ 431 } 432 433 match, err := workspace.SelectCompatiblePlugin(plugins, kind, name, semverRange) 434 if err == nil { 435 return &match, nil 436 } 437 return nil, fmt.Errorf("could not locate a compatible plugin in deploytest, the makefile and "+ 438 "& constructor of the plugin host must define the location of the schema: %w", err) 439 } 440 441 func (host *pluginHost) GetRequiredPlugins(info plugin.ProgInfo, 442 kinds plugin.Flags) ([]workspace.PluginSpec, error) { 443 return host.languageRuntime.GetRequiredPlugins(info) 444 } 445 446 func (host *pluginHost) GetProjectPlugins() []workspace.ProjectPlugin { 447 return nil 448 } 449 450 func (host *pluginHost) PolicyAnalyzer(name tokens.QName, path string, 451 opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error) { 452 453 plug, err := host.plugin(workspace.AnalyzerPlugin, string(name), nil, opts) 454 if err != nil || plug == nil { 455 return nil, err 456 } 457 return plug.(plugin.Analyzer), nil 458 } 459 460 func (host *pluginHost) ListAnalyzers() []plugin.Analyzer { 461 host.m.Lock() 462 defer host.m.Unlock() 463 464 return host.analyzers 465 }