// Copyright (c) HashiCorp, Inc. // 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 }