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