Skip to content

Commit

Permalink
feat(stacks): add CLI config in RPC API handshake (#35146)
Browse files Browse the repository at this point in the history
Adds support for including fields typically found in the Terraform CLI configuration in the RPC API handshake. This allows us to include global configuration arguments that impact the RPC API session without requiring instrumentation for each RPC service. The new Config struct currently supports only service discovery credentials, but it can be expanded in the future.
  • Loading branch information
mjyocca committed May 17, 2024
1 parent 7012371 commit ee5cda7
Show file tree
Hide file tree
Showing 7 changed files with 2,398 additions and 2,175 deletions.
40 changes: 40 additions & 0 deletions internal/rpcapi/credentials_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package rpcapi

import (
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform-svchost/auth"
"github.com/zclconf/go-cty/cty"
)

var _ auth.CredentialsSource = &credentialsSource{}

type credentialsSource struct {
configured map[svchost.Hostname]cty.Value
}

func newCredentialsSource() *credentialsSource {
return &credentialsSource{
configured: map[svchost.Hostname]cty.Value{},
}
}

func (c *credentialsSource) ForHost(host svchost.Hostname) (auth.HostCredentials, error) {
v, ok := c.configured[host]
if ok {
return auth.HostCredentialsFromObject(v), nil
}
return nil, nil
}

func (c *credentialsSource) StoreForHost(host svchost.Hostname, credentials auth.HostCredentialsWritable) error {
c.configured[host] = credentials.ToStore()
return nil
}

func (c *credentialsSource) ForgetForHost(host svchost.Hostname) error {
delete(c.configured, host)
return nil
}
5 changes: 5 additions & 0 deletions internal/rpcapi/internal_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ func (c *Client) Dependencies() terraform1.DependenciesClient {
return terraform1.NewDependenciesClient(c.conn)
}

// Packages returns a client for the Packages service of the RPC API.
func (c *Client) Packages() terraform1.PackagesClient {
return terraform1.NewPackagesClient(c.conn)
}

// Stacks returns a client for the Stacks service of the RPC API.
func (c *Client) Stacks() terraform1.StacksClient {
return terraform1.NewStacksClient(c.conn)
Expand Down
27 changes: 24 additions & 3 deletions internal/rpcapi/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"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"

Expand Down Expand Up @@ -43,15 +45,15 @@ func registerGRPCServices(s *grpc.Server, opts *serviceOpts) {
terraform1.RegisterSetupServer(s, setup)
}

func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error) {
func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *terraform1.Handshake_Request) (*terraform1.ServerCapabilities, error) {
dependencies := dynrpcserver.NewDependenciesStub()
terraform1.RegisterDependenciesServer(s, dependencies)
stacks := dynrpcserver.NewStacksStub()
terraform1.RegisterStacksServer(s, stacks)
packages := dynrpcserver.NewPackagesStub()
terraform1.RegisterPackagesServer(s, packages)

return func(ctx context.Context, clientCaps *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error) {
return func(ctx context.Context, request *terraform1.Handshake_Request) (*terraform1.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()
Expand All @@ -66,7 +68,10 @@ func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *t
// 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 := disco.New()
services, err := newServiceDisco(request.GetConfig())
if err != nil {
return &terraform1.ServerCapabilities{}, err
}

// If handshaking is successful (which it currently always is, because
// we don't have any special capabilities to negotiate yet) then we
Expand All @@ -91,3 +96,19 @@ func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *t
type serviceOpts struct {
experimentsAllowed bool
}

func newServiceDisco(config *terraform1.Config) (*disco.Disco, error) {
services := disco.New()
credSrc := newCredentialsSource()

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
}
6 changes: 3 additions & 3 deletions internal/rpcapi/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ type setupServer struct {
// initOthers is the callback used to perform the capability negotiation
// step and initialize all of the other API services based on what was
// negotiated.
initOthers func(context.Context, *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error)
initOthers func(context.Context, *terraform1.Handshake_Request) (*terraform1.ServerCapabilities, error)
mu sync.Mutex
}

func newSetupServer(initOthers func(context.Context, *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error)) terraform1.SetupServer {
func newSetupServer(initOthers func(context.Context, *terraform1.Handshake_Request) (*terraform1.ServerCapabilities, error)) terraform1.SetupServer {
return &setupServer{
initOthers: initOthers,
}
Expand All @@ -47,7 +47,7 @@ func (s *setupServer) Handshake(ctx context.Context, req *terraform1.Handshake_R
var err error
{
ctx, span := tracer.Start(ctx, "initialize RPC services")
serverCaps, err = s.initOthers(ctx, req.Capabilities)
serverCaps, err = s.initOthers(ctx, req)
span.End()
}
s.initOthers = nil // cannot handshake again
Expand Down
2 changes: 1 addition & 1 deletion internal/rpcapi/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func TestTelemetryInTestsGRPC(t *testing.T) {

client, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) {
setup := &setupServer{
initOthers: func(ctx context.Context, cc *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error) {
initOthers: func(ctx context.Context, cc *terraform1.Handshake_Request) (*terraform1.ServerCapabilities, error) {
return &terraform1.ServerCapabilities{}, nil
},
}
Expand Down

0 comments on commit ee5cda7

Please sign in to comment.