rpcapi: Now wired up to the "terraform rpcapi" plumbing command

This doesn't actually do anything useful yet, but this does at least make
the RPC server accessible to external callers where they can handshake and
then get an error saying that nothing else is implemented.
pull/34738/head
Martin Atkins 3 years ago
parent 38d66ea74d
commit 3bf1a5cf53

@ -21,6 +21,7 @@ import (
"github.com/hashicorp/terraform/internal/command/webbrowser"
"github.com/hashicorp/terraform/internal/getproviders"
pluginDiscovery "github.com/hashicorp/terraform/internal/plugin/discovery"
"github.com/hashicorp/terraform/internal/rpcapi"
"github.com/hashicorp/terraform/internal/terminal"
)
@ -275,6 +276,17 @@ func initCommands(
}, nil
},
// "rpcapi" is handled a bit differently because the whole point of
// this interface is to bypass the CLI layer so wrapping automation can
// get as-direct-as-possible access to Terraform Core functionality,
// without interference from behaviors that are intended for CLI
// end-user convenience. We bypass the "command" package entirely
// for this command in particular.
"rpcapi": rpcapi.CLICommandFactory(rpcapi.CommandFactoryOpts{
ExperimentsAllowed: meta.AllowExperimentalFeatures,
ShutdownCh: meta.ShutdownCh,
}),
"show": func() (cli.Command, error) {
return &command.ShowCommand{
Meta: meta,
@ -431,9 +443,10 @@ func initCommands(
}
HiddenCommands = map[string]struct{}{
"env": {},
"internal-plugin": {},
"push": {},
"env": struct{}{},
"internal-plugin": struct{}{},
"push": struct{}{},
"rpcapi": struct{}{},
}
}

@ -0,0 +1,101 @@
package rpcapi
import (
"context"
"fmt"
"os"
"strings"
"github.com/mitchellh/cli"
)
// CLICommand is a command initialization callback for use with
// github.com/mitchellh/cli, allowing Terraform's "package main" to
// jump straight into the RPC plugin server without any interference
// from the usual Terraform CLI machinery in package "command", which
// is irrelevant here because this RPC API exists to bypass the
// Terraform CLI layer as much as possible.
func CLICommandFactory(opts CommandFactoryOpts) func() (cli.Command, error) {
return func() (cli.Command, error) {
return cliCommand{opts}, nil
}
}
type CommandFactoryOpts struct {
ExperimentsAllowed bool
ShutdownCh <-chan struct{}
}
type cliCommand struct {
opts CommandFactoryOpts
}
// Help implements cli.Command.
func (c cliCommand) Help() string {
helpText := `
Usage: terraform [global options] rpcapi
Starts a gRPC server for programmatic access to Terraform Core from
wrapping automation.
This interface is currently intended only for Terraform Cloud and is
subject to breaking changes even in patch releases. Do not use this.
`
return strings.TrimSpace(helpText)
}
// Run implements cli.Command.
func (c cliCommand) Run(args []string) int {
if len(args) != 0 {
fmt.Fprintf(os.Stderr, "This command does not accept any arguments.\n")
return 1
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
// We'll adapt the caller's "shutdown channel" into a context
// cancellation.
for {
select {
case <-c.opts.ShutdownCh:
cancel()
case <-ctx.Done():
return
}
}
}()
err := ServePlugin(ctx, ServerOpts{
ExperimentsAllowed: c.opts.ExperimentsAllowed,
})
if err != nil {
if err == ErrNotPluginClient {
fmt.Fprintf(
os.Stderr,
`
This subcommand is for use by Terraform Cloud and is not intended for direct use.
Its behavior is not subject to Terraform compatibility promises. To interact
with Terraform using the CLI workflow, refer to the main set of subcommands by
running the following command:
terraform help
`)
} else {
fmt.Fprintf(os.Stderr, "Failed to start RPC server: %s.\n", err)
}
return 1
}
// NOTE: In practice it's impossible to get here, because if ServePlugin
// doesn't error then it blocks forever and then eventually terminates
// the process itself without returning.
return 0
}
// Synopsis implements cli.Command.
func (c cliCommand) Synopsis() string {
return "An RPC server used for integration with wrapping automation"
}

@ -11,6 +11,8 @@ import (
type corePlugin struct {
plugin.Plugin
experimentsAllowed bool
}
func (p *corePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
@ -34,8 +36,8 @@ func (p *corePlugin) handshakeFunc(s *grpc.Server) func(context.Context, *terraf
// If handshaking is successful (which it currently always is, because
// we don't have any special capabilities to negotiate yet) then we
// will register all of the other services so the client can being
// doing real work. (In future the details of what we register here
// might vary based on the negotiated capabilities.)
// doing real work. In future the details of what we register here
// might vary based on the negotiated capabilities.
terraform1.RegisterDependenciesServer(s, &dependenciesServer{})
return &terraform1.ServerCapabilities{}, nil
}

@ -2,26 +2,60 @@ package rpcapi
import (
"context"
"errors"
"os"
"github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// ServePlugin attempts to complete the go-plugin protocol handshake, and then
// if successful starts the plugin server and blocks until externally
// terminated.
func ServePlugin(ctx context.Context) error {
// if successful starts the plugin server and blocks until the given context
// is cancelled.
//
// Returns [ErrNotPluginClient] if this program doesn't seem to be running as
// the child of a plugin client, which is detected based on a magic environment
// variable that the client ought to have set.
func ServePlugin(ctx context.Context, opts ServerOpts) error {
// go-plugin has its own check for the environment variable magic cookie
// but it returns a generic error message. We'll pre-check it out here
// instead so we can return a more specific error message.
if os.Getenv(handshake.MagicCookieKey) != handshake.MagicCookieValue {
return ErrNotPluginClient
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshake,
VersionedPlugins: map[int]plugin.PluginSet{
1: plugin.PluginSet{
"tfcore": &corePlugin{},
"tfcore": &corePlugin{
experimentsAllowed: opts.ExperimentsAllowed,
},
},
},
GRPCServer: plugin.DefaultGRPCServer,
GRPCServer: func(opts []grpc.ServerOption) *grpc.Server {
server := grpc.NewServer(opts...)
// We'll also monitor the given context for cancellation
// and terminate the server gracefully if we get cancelled.
go func() {
<-ctx.Done()
server.GracefulStop()
// The above will block until all of the pending RPCs have
// finished.
os.Exit(0)
}()
return server
},
})
return nil
}
var ErrNotPluginClient = errors.New("caller is not a plugin client")
type ServerOpts struct {
ExperimentsAllowed bool
}
// handshake is the HandshakeConfig used to begin negotiation between client
// and server.
var handshake = plugin.HandshakeConfig{

Loading…
Cancel
Save