github.com/opentofu/opentofu@v1.7.1/internal/plugin/grpc_provisioner.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package plugin 7 8 import ( 9 "context" 10 "errors" 11 "io" 12 "sync" 13 14 plugin "github.com/hashicorp/go-plugin" 15 "github.com/opentofu/opentofu/internal/configs/configschema" 16 "github.com/opentofu/opentofu/internal/plugin/convert" 17 "github.com/opentofu/opentofu/internal/provisioners" 18 proto "github.com/opentofu/opentofu/internal/tfplugin5" 19 "github.com/zclconf/go-cty/cty" 20 "github.com/zclconf/go-cty/cty/msgpack" 21 "google.golang.org/grpc" 22 ) 23 24 // GRPCProvisionerPlugin is the plugin.GRPCPlugin implementation. 25 type GRPCProvisionerPlugin struct { 26 plugin.Plugin 27 GRPCProvisioner func() proto.ProvisionerServer 28 } 29 30 func (p *GRPCProvisionerPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 31 return &GRPCProvisioner{ 32 client: proto.NewProvisionerClient(c), 33 ctx: ctx, 34 }, nil 35 } 36 37 func (p *GRPCProvisionerPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 38 proto.RegisterProvisionerServer(s, p.GRPCProvisioner()) 39 return nil 40 } 41 42 // provisioners.Interface grpc implementation 43 type GRPCProvisioner struct { 44 // PluginClient provides a reference to the plugin.Client which controls the plugin process. 45 // This allows the GRPCProvider a way to shutdown the plugin process. 46 PluginClient *plugin.Client 47 48 client proto.ProvisionerClient 49 ctx context.Context 50 51 // Cache the schema since we need it for serialization in each method call. 52 mu sync.Mutex 53 schema *configschema.Block 54 } 55 56 func (p *GRPCProvisioner) GetSchema() (resp provisioners.GetSchemaResponse) { 57 p.mu.Lock() 58 defer p.mu.Unlock() 59 60 if p.schema != nil { 61 return provisioners.GetSchemaResponse{ 62 Provisioner: p.schema, 63 } 64 } 65 66 protoResp, err := p.client.GetSchema(p.ctx, new(proto.GetProvisionerSchema_Request)) 67 if err != nil { 68 resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) 69 return resp 70 } 71 resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) 72 73 if protoResp.Provisioner == nil { 74 resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing provisioner schema")) 75 return resp 76 } 77 78 resp.Provisioner = convert.ProtoToConfigSchema(protoResp.Provisioner.Block) 79 80 p.schema = resp.Provisioner 81 82 return resp 83 } 84 85 func (p *GRPCProvisioner) ValidateProvisionerConfig(r provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) { 86 schema := p.GetSchema() 87 if schema.Diagnostics.HasErrors() { 88 resp.Diagnostics = resp.Diagnostics.Append(schema.Diagnostics) 89 return resp 90 } 91 92 mp, err := msgpack.Marshal(r.Config, schema.Provisioner.ImpliedType()) 93 if err != nil { 94 resp.Diagnostics = resp.Diagnostics.Append(err) 95 return resp 96 } 97 98 protoReq := &proto.ValidateProvisionerConfig_Request{ 99 Config: &proto.DynamicValue{Msgpack: mp}, 100 } 101 protoResp, err := p.client.ValidateProvisionerConfig(p.ctx, protoReq) 102 if err != nil { 103 resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) 104 return resp 105 } 106 resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) 107 return resp 108 } 109 110 func (p *GRPCProvisioner) ProvisionResource(r provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { 111 schema := p.GetSchema() 112 if schema.Diagnostics.HasErrors() { 113 resp.Diagnostics = resp.Diagnostics.Append(schema.Diagnostics) 114 return resp 115 } 116 117 mp, err := msgpack.Marshal(r.Config, schema.Provisioner.ImpliedType()) 118 if err != nil { 119 resp.Diagnostics = resp.Diagnostics.Append(err) 120 return resp 121 } 122 123 // connection is always assumed to be a simple string map 124 connMP, err := msgpack.Marshal(r.Connection, cty.Map(cty.String)) 125 if err != nil { 126 resp.Diagnostics = resp.Diagnostics.Append(err) 127 return resp 128 } 129 130 protoReq := &proto.ProvisionResource_Request{ 131 Config: &proto.DynamicValue{Msgpack: mp}, 132 Connection: &proto.DynamicValue{Msgpack: connMP}, 133 } 134 135 outputClient, err := p.client.ProvisionResource(p.ctx, protoReq) 136 if err != nil { 137 resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) 138 return resp 139 } 140 141 for { 142 rcv, err := outputClient.Recv() 143 if rcv != nil { 144 r.UIOutput.Output(rcv.Output) 145 } 146 if err != nil { 147 if err != io.EOF { 148 resp.Diagnostics = resp.Diagnostics.Append(err) 149 } 150 break 151 } 152 153 if len(rcv.Diagnostics) > 0 { 154 resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(rcv.Diagnostics)) 155 break 156 } 157 } 158 159 return resp 160 } 161 162 func (p *GRPCProvisioner) Stop() error { 163 protoResp, err := p.client.Stop(p.ctx, &proto.Stop_Request{}) 164 if err != nil { 165 return err 166 } 167 if protoResp.Error != "" { 168 return errors.New(protoResp.Error) 169 } 170 return nil 171 } 172 173 func (p *GRPCProvisioner) Close() error { 174 // check this since it's not automatically inserted during plugin creation 175 if p.PluginClient == nil { 176 logger.Debug("provisioner has no plugin.Client") 177 return nil 178 } 179 180 p.PluginClient.Kill() 181 return nil 182 }