Files
coder/provisionersdk/serve_test.go
T
Steven Masley 60b3fd0783 chore!: send modules archive over the proto messages (#21398)
# What this does

Dynamic parameters caches the `./terraform/modules` directory for parameter usage. What this PR does is send over this archive to the provisioner when building workspaces.

This allow terraform to skip downloading modules from their registries, a step that takes seconds. 

<img width="1223" height="429" alt="Screenshot From 2025-12-29 12-57-52" src="https://github.com/user-attachments/assets/16066e0a-ac79-4296-819d-924f4b0418dc" />


# Wire protocol

The wire protocol reuses the same mechanism used to download the modules `provisoner -> coder`. It splits up large archives into multiple protobuf messages so larger archives can be sent under the message size limit.

# 🚨  Behavior Change (Breaking Change) 🚨 

**Before this PR** modules were downloaded on every workspace build. This means unpinned modules always fetched the latest version

**After this PR** modules are cached at template import time, and their versions are effectively pinned for all subsequent workspace builds.
2026-01-09 11:33:34 -06:00

171 lines
5.2 KiB
Go

package provisionersdk_test
import (
"context"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"storj.io/drpc/drpcconn"
"github.com/coder/coder/v2/codersdk/drpcsdk"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
}
func TestProvisionerSDK(t *testing.T) {
t.Parallel()
t.Run("ServeListener", func(t *testing.T) {
t.Parallel()
client, server := drpcsdk.MemTransportPipe()
defer client.Close()
defer server.Close()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
go func() {
err := provisionersdk.Serve(ctx, unimplementedServer{}, &provisionersdk.ServeOptions{
Listener: server,
WorkDirectory: t.TempDir(),
})
assert.NoError(t, err)
}()
api := proto.NewDRPCProvisionerClient(client)
s, err := api.Session(ctx)
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{}}})
require.NoError(t, err)
_, err = s.Recv()
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err)
msg, err := s.Recv()
require.NoError(t, err)
require.Equal(t, "unimplemented", msg.GetParse().GetError())
err = s.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
require.NoError(t, err)
msg, err = s.Recv()
require.NoError(t, err)
// Plan has no error so that we're allowed to run Apply
require.Equal(t, "", msg.GetPlan().GetError())
err = s.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}})
require.NoError(t, err)
msg, err = s.Recv()
require.NoError(t, err)
require.Equal(t, "unimplemented", msg.GetApply().GetError())
})
t.Run("ServeClosedPipe", func(t *testing.T) {
t.Parallel()
client, server := drpcsdk.MemTransportPipe()
_ = client.Close()
_ = server.Close()
err := provisionersdk.Serve(context.Background(), unimplementedServer{}, &provisionersdk.ServeOptions{
Listener: server,
WorkDirectory: t.TempDir(),
})
require.NoError(t, err)
})
t.Run("ServeConn", func(t *testing.T) {
t.Parallel()
client, server := net.Pipe()
defer client.Close()
defer server.Close()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
srvErr := make(chan error, 1)
go func() {
err := provisionersdk.Serve(ctx, unimplementedServer{}, &provisionersdk.ServeOptions{
Conn: server,
WorkDirectory: t.TempDir(),
})
srvErr <- err
}()
api := proto.NewDRPCProvisionerClient(drpcconn.NewWithOptions(client, drpcconn.Options{
Manager: drpcsdk.DefaultDRPCOptions(nil),
}))
s, err := api.Session(ctx)
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{}}})
require.NoError(t, err)
_, err = s.Recv()
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err)
msg, err := s.Recv()
require.NoError(t, err)
require.Equal(t, "unimplemented", msg.GetParse().GetError())
err = s.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
require.NoError(t, err)
msg, err = s.Recv()
require.NoError(t, err)
// Plan has no error so that we're allowed to run Apply
require.Equal(t, "", msg.GetPlan().GetError())
err = s.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}})
require.NoError(t, err)
msg, err = s.Recv()
require.NoError(t, err)
require.Equal(t, "unimplemented", msg.GetApply().GetError())
// Check provisioner closes when the connection does
err = s.Close()
require.NoError(t, err)
err = api.DRPCConn().Close()
require.NoError(t, err)
select {
case <-ctx.Done():
t.Fatal("timeout waiting for provisioner")
case err = <-srvErr:
require.NoError(t, err)
}
})
}
var _ provisionersdk.Server = unimplementedServer{}
type unimplementedServer struct{}
func (unimplementedServer) Init(s *provisionersdk.Session, r *provisionersdk.InitRequest, canceledOrComplete <-chan struct{}) *proto.InitComplete {
return &proto.InitComplete{}
}
func (unimplementedServer) Graph(s *provisionersdk.Session, r *proto.GraphRequest, canceledOrComplete <-chan struct{}) *proto.GraphComplete {
return &proto.GraphComplete{Error: "unimplemented"}
}
func (unimplementedServer) Parse(_ *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete {
return &proto.ParseComplete{Error: "unimplemented"}
}
func (unimplementedServer) Plan(_ *provisionersdk.Session, _ *proto.PlanRequest, _ <-chan struct{}) *proto.PlanComplete {
return &proto.PlanComplete{}
}
func (unimplementedServer) Apply(_ *provisionersdk.Session, _ *proto.ApplyRequest, _ <-chan struct{}) *proto.ApplyComplete {
return &proto.ApplyComplete{Error: "unimplemented"}
}