terraform/internal/rpcapi/plugin.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

136 lines
5.3 KiB
Go
Raw Permalink Normal View History

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package rpcapi
import (
"context"
"fmt"
"github.com/hashicorp/go-plugin"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform-svchost/auth"
"github.com/hashicorp/terraform-svchost/disco"
"google.golang.org/grpc"
"github.com/hashicorp/terraform/internal/command/cliconfig"
pluginDiscovery "github.com/hashicorp/terraform/internal/plugin/discovery"
"github.com/hashicorp/terraform/internal/rpcapi/dynrpcserver"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
)
type corePlugin struct {
plugin.Plugin
experimentsAllowed bool
}
func (p *corePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
// This codebase only provides a server implementation of this plugin.
// Clients must live elsewhere.
return nil, fmt.Errorf("there is no client implementation in this codebase")
}
func (p *corePlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
generalOpts := &serviceOpts{
experimentsAllowed: p.experimentsAllowed,
}
registerGRPCServices(s, generalOpts)
return nil
}
func registerGRPCServices(s *grpc.Server, opts *serviceOpts) {
// We initially only register the setup server, because the registration
// of other services can vary depending on the capabilities negotiated
// during handshake.
server := newSetupServer(serverHandshake(s, opts))
setup.RegisterSetupServer(s, server)
}
func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *setup.Handshake_Request, *stopper) (*setup.ServerCapabilities, error) {
dependenciesStub := dynrpcserver.NewDependenciesStub()
dependencies.RegisterDependenciesServer(s, dependenciesStub)
stacksStub := dynrpcserver.NewStacksStub()
stacks.RegisterStacksServer(s, stacksStub)
packagesStub := dynrpcserver.NewPackagesStub()
packages.RegisterPackagesServer(s, packagesStub)
return func(ctx context.Context, request *setup.Handshake_Request, stopper *stopper) (*setup.ServerCapabilities, error) {
rpcapi: Management of "handles" As with many other systems that involve a client indirectly manipulating objects on the other side of a trust boundary, we're going to use opaque "handles" to represent objects that remain active on the server side between different RPC API calls. A handle is just a 64-bit integer corresponding to an entry in a lookup table maintained by the server, in the "handleTable" type. In the public API we don't make any promises about uniqueness of handles between different types of objects, but to minimize the impact of client bugs we'll internally ensure that we never issue the same handle number for two objects of different types. This means that a client getting their handles mixed up will typically cause an explicit error rather than just doing something weird with an unrelated object that happened to have a matching handle. Our internal API for this will use explicit separate methods for each type of handle and use type parameters so that the Go compiler can call us out if we're inconsistent in how we're using the handles. From a client perspective though these all just layer to protobuf int64; clients might choose to add their own similar abstraction to track handle types, but that's their own business since we can't represent such things efficiently within the protocol buffers schema model. This commit includes initial implementations of Dependencies.OpenSourceBundle and Dependencies.CloseSourceBundle just as some initial evidence that this design works. We'll continue implementing the other real service functions in later commits once the basic infrastructure (like this) is in place.
2023-06-07 14:15:07 -04:00
// All of our servers will share a common handles table so that objects
// can be passed from one service to another.
handles := newHandleTable()
// NOTE: This is intentionally not the same disco that "package main"
// instantiates for Terraform CLI, because the RPC API is
// architecturally independent from CLI despite being launched through
// it, and so it is not subject to any ambient CLI configuration files
// that might be in scope. If we later discover requirements for
// callers to customize the service discovery settings, consider
// adding new fields to terraform1.ClientCapabilities (even though
// this isn't strictly a "capability") so that the RPC caller has
// full control without needing to also tinker with the current user's
// CLI configuration.
services, err := newServiceDisco(request.GetConfig())
if err != nil {
return &setup.ServerCapabilities{}, err
}
// If handshaking is successful (which it currently always is, because
// we don't have any special capabilities to negotiate yet) then we
// will initialize all of the other services so the client can begin
// doing real work. In future the details of what we register here
// might vary based on the negotiated capabilities.
dependenciesStub.ActivateRPCServer(newDependenciesServer(handles, services))
stacksStub.ActivateRPCServer(newStacksServer(stopper, handles, services, opts))
packagesStub.ActivateRPCServer(newPackagesServer(services))
// If the client requested any extra capabililties that we're going
// to honor then we should announce them in this result.
return &setup.ServerCapabilities{}, nil
}
}
// serviceOpts are options that could potentially apply to all of our
// individual RPC services.
//
// This could potentially be embedded inside a service-specific options
// structure, if needed.
type serviceOpts struct {
experimentsAllowed bool
}
func newServiceDisco(config *setup.Config) (*disco.Disco, error) {
// First, we'll try and load any credentials that might have been available
// to the UI. It's perfectly fine if there are none so any errors we find
// are from malformed credentials rather than missing ones.
file, diags := cliconfig.LoadConfig()
if diags.HasErrors() {
return nil, fmt.Errorf("problem loading CLI configuration: %w", diags.ErrWithWarnings())
}
helperPlugins := pluginDiscovery.FindPlugins("credentials", cliconfig.GlobalPluginDirs())
src, err := file.CredentialsSource(helperPlugins)
if err != nil {
return nil, fmt.Errorf("problem creating credentials source: %w", err)
}
services := disco.NewWithCredentialsSource(src)
// Second, we'll side-load any credentials that might have been passed in.
credSrc := services.CredentialsSource()
if config != nil {
for host, cred := range config.GetCredentials() {
if err := credSrc.StoreForHost(svchost.Hostname(host), auth.HostCredentialsToken(cred.Token)); err != nil {
return nil, fmt.Errorf("problem storing credential for host %s with: %w", host, err)
}
}
services.SetCredentialsSource(credSrc)
}
return services, nil
}