| // 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) { | |
| // 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 | |
| } |