Skip to content

Commit a6794d7

Browse files
committed
prototype: migrate command for terraform stacks
1 parent 3a3bea8 commit a6794d7

File tree

20 files changed

+4489
-2438
lines changed

20 files changed

+4489
-2438
lines changed

internal/collections/set.go

+9
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ func (s Set[T]) Add(vs ...T) {
7676
}
7777
}
7878

79+
// AddAll inserts all the members of vs into the set.
80+
//
81+
// The behavior is the same as calling Add for each member of vs.
82+
func (s Set[T]) AddAll(vs Set[T]) {
83+
for v := range vs.All() {
84+
s.Add(v)
85+
}
86+
}
87+
7988
// Remove removes the given member from the set, or does nothing if no
8089
// equivalent value was present.
8190
func (s Set[T]) Remove(v T) {

internal/rpcapi/dynrpcserver/stacks.go

+24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/rpcapi/handles.go

+15
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import (
88
"sync"
99

1010
"github.com/hashicorp/go-slug/sourcebundle"
11+
1112
"github.com/hashicorp/terraform/internal/depsfile"
1213
"github.com/hashicorp/terraform/internal/providercache"
1314
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
1415
"github.com/hashicorp/terraform/internal/stacks/stackplan"
1516
"github.com/hashicorp/terraform/internal/stacks/stackstate"
17+
"github.com/hashicorp/terraform/internal/states"
1618
)
1719

1820
// handle represents an identifier shared between client and server to identify
@@ -138,6 +140,19 @@ func (t *handleTable) CloseStackPlan(hnd handle[*stackplan.Plan]) error {
138140
return closeHandle(t, hnd)
139141
}
140142

143+
func (t *handleTable) NewTerraformState(state *states.State) handle[*states.State] {
144+
return newHandle(t, state)
145+
}
146+
147+
func (t *handleTable) TerraformState(hnd handle[*states.State]) *states.State {
148+
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
149+
return ret
150+
}
151+
152+
func (t *handleTable) CloseTerraformState(hnd handle[*states.State]) error {
153+
return closeHandle(t, hnd)
154+
}
155+
141156
func (t *handleTable) NewDependencyLocks(locks *depsfile.Locks) handle[*depsfile.Locks] {
142157
// NOTE: We intentionally don't track a dependency on a source bundle
143158
// here for two reasons:

internal/rpcapi/plugin.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *s
8484
// doing real work. In future the details of what we register here
8585
// might vary based on the negotiated capabilities.
8686
dependenciesStub.ActivateRPCServer(newDependenciesServer(handles, services))
87-
stacksStub.ActivateRPCServer(newStacksServer(stopper, handles, opts))
87+
stacksStub.ActivateRPCServer(newStacksServer(stopper, handles, services, opts))
8888
packagesStub.ActivateRPCServer(newPackagesServer(services))
8989

9090
// If the client requested any extra capabililties that we're going

internal/rpcapi/stacks.go

+151-2
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,50 @@
44
package rpcapi
55

66
import (
7+
"bytes"
78
"context"
89
"fmt"
910
"io"
11+
"os"
12+
"path/filepath"
1013
"time"
1114

1215
"github.com/hashicorp/go-slug/sourceaddrs"
1316
"github.com/hashicorp/go-slug/sourcebundle"
17+
"github.com/hashicorp/terraform-svchost/disco"
1418
"go.opentelemetry.io/otel/attribute"
1519
otelCodes "go.opentelemetry.io/otel/codes"
1620
"go.opentelemetry.io/otel/trace"
1721
"google.golang.org/grpc/codes"
1822
"google.golang.org/grpc/status"
1923

2024
"github.com/hashicorp/terraform/internal/addrs"
25+
"github.com/hashicorp/terraform/internal/backend"
26+
"github.com/hashicorp/terraform/internal/backend/local"
27+
"github.com/hashicorp/terraform/internal/command/workdir"
2128
"github.com/hashicorp/terraform/internal/depsfile"
2229
"github.com/hashicorp/terraform/internal/plans"
2330
"github.com/hashicorp/terraform/internal/providercache"
2431
"github.com/hashicorp/terraform/internal/providers"
32+
"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
2533
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
2634
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
2735
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
36+
"github.com/hashicorp/terraform/internal/stacks/stackmigrate"
2837
"github.com/hashicorp/terraform/internal/stacks/stackplan"
2938
"github.com/hashicorp/terraform/internal/stacks/stackruntime"
3039
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
3140
"github.com/hashicorp/terraform/internal/stacks/stackstate"
41+
"github.com/hashicorp/terraform/internal/states"
42+
"github.com/hashicorp/terraform/internal/states/statefile"
3243
"github.com/hashicorp/terraform/internal/tfdiags"
3344
)
3445

3546
type stacksServer struct {
3647
stacks.UnimplementedStacksServer
3748

3849
stopper *stopper
50+
services *disco.Disco
3951
handles *handleTable
4052
experimentsAllowed bool
4153

@@ -55,9 +67,10 @@ type stacksServer struct {
5567

5668
var _ stacks.StacksServer = (*stacksServer)(nil)
5769

58-
func newStacksServer(stopper *stopper, handles *handleTable, opts *serviceOpts) *stacksServer {
70+
func newStacksServer(stopper *stopper, handles *handleTable, services *disco.Disco, opts *serviceOpts) *stacksServer {
5971
return &stacksServer{
6072
stopper: stopper,
73+
services: services,
6174
handles: handles,
6275
experimentsAllowed: opts.experimentsAllowed,
6376
}
@@ -79,7 +92,7 @@ func (s *stacksServer) OpenStackConfiguration(ctx context.Context, req *stacks.O
7992
if diags.HasErrors() {
8093
// For errors in the configuration itself we treat that as a successful
8194
// result from OpenStackConfiguration but with diagnostics in the
82-
// response and no source handle.
95+
// response and no source handle. f
8396
return &stacks.OpenStackConfiguration_Response{
8497
Diagnostics: diagnosticsToProto(diags),
8598
}, nil
@@ -776,6 +789,142 @@ func (s *stacksServer) InspectExpressionResult(ctx context.Context, req *stacks.
776789
return insp.InspectExpressionResult(ctx, req)
777790
}
778791

792+
func (s *stacksServer) OpenTerraformState(ctx context.Context, request *stacks.OpenTerraformState_Request) (*stacks.OpenTerraformState_Response, error) {
793+
switch data := request.State.(type) {
794+
case *stacks.OpenTerraformState_Request_ConfigPath:
795+
// load the state specified by this configuration
796+
797+
workingDirectory := workdir.NewDir(data.ConfigPath)
798+
if data := os.Getenv("TF_DATA_DIR"); len(data) > 0 {
799+
workingDirectory.OverrideDataDir(data)
800+
}
801+
802+
// Load the currently active workspace from the environment, defaulting
803+
// to the default workspace if not set.
804+
805+
workspace := backend.DefaultStateName
806+
if ws := os.Getenv("TF_WORKSPACE"); len(ws) > 0 {
807+
workspace = ws
808+
}
809+
810+
workspaceData, err := os.ReadFile(filepath.Join(workingDirectory.DataDir(), local.DefaultWorkspaceFile))
811+
if err != nil && !os.IsNotExist(err) {
812+
return nil, status.Errorf(codes.InvalidArgument, "failed to read workspace file: %s", err)
813+
}
814+
if len(workspaceData) > 0 {
815+
workspace = string(workspaceData)
816+
}
817+
818+
// Load the state from the backend specified by the .terraform.tfstate
819+
// file. This function should return an empty state even if the diags
820+
// has errors. This makes it easier for the caller, as they should
821+
// close the state handle regardless of the diags.
822+
state, diags := stackmigrate.Load(workingDirectory.RootModuleDir(), filepath.Join(workingDirectory.DataDir(), ".terraform.tfstate"), workspace)
823+
824+
hnd := s.handles.NewTerraformState(state)
825+
return &stacks.OpenTerraformState_Response{
826+
StateHandle: hnd.ForProtobuf(),
827+
Diagnostics: diagnosticsToProto(diags),
828+
}, nil
829+
830+
case *stacks.OpenTerraformState_Request_Raw:
831+
// load the state from the raw data
832+
833+
file, err := statefile.Read(bytes.NewReader(data.Raw))
834+
if err != nil {
835+
return nil, status.Errorf(codes.InvalidArgument, "invalid raw state data: %s", err)
836+
}
837+
838+
hnd := s.handles.NewTerraformState(file.State)
839+
return &stacks.OpenTerraformState_Response{
840+
StateHandle: hnd.ForProtobuf(),
841+
}, nil
842+
843+
default:
844+
return nil, status.Error(codes.InvalidArgument, "invalid state source")
845+
}
846+
}
847+
848+
func (s *stacksServer) CloseTerraformState(ctx context.Context, request *stacks.CloseTerraformState_Request) (*stacks.CloseTerraformState_Response, error) {
849+
hnd := handle[*states.State](request.StateHandle)
850+
err := s.handles.CloseTerraformState(hnd)
851+
if err != nil {
852+
return nil, status.Error(codes.InvalidArgument, err.Error())
853+
}
854+
return new(stacks.CloseTerraformState_Response), nil
855+
}
856+
857+
func (s *stacksServer) MigrateTerraformState(request *stacks.MigrateTerraformState_Request, server stacks.Stacks_MigrateTerraformStateServer) error {
858+
859+
previousStateHandle := handle[*states.State](request.StateHandle)
860+
previousState := s.handles.TerraformState(previousStateHandle)
861+
if previousState == nil {
862+
return status.Error(codes.InvalidArgument, "the given state handle is invalid")
863+
}
864+
865+
configHandle := handle[*stackconfig.Config](request.ConfigHandle)
866+
config := s.handles.StackConfig(configHandle)
867+
if config == nil {
868+
return status.Error(codes.InvalidArgument, "the given config handle is invalid")
869+
}
870+
871+
dependencyLocksHandle := handle[*depsfile.Locks](request.DependencyLocksHandle)
872+
dependencyLocks := s.handles.DependencyLocks(dependencyLocksHandle)
873+
if dependencyLocks == nil {
874+
return status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid")
875+
}
876+
877+
providerCacheHandle := handle[*providercache.Dir](request.ProviderCacheHandle)
878+
providerCache := s.handles.ProviderPluginCache(providerCacheHandle)
879+
if providerCache == nil {
880+
return status.Error(codes.InvalidArgument, "the given provider cache handle is invalid")
881+
}
882+
883+
providerFactories, err := providerFactoriesForLocks(dependencyLocks, providerCache)
884+
if err != nil {
885+
return status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err)
886+
}
887+
888+
migrate := &stackmigrate.Migration{
889+
Providers: providerFactories,
890+
PreviousState: previousState,
891+
Config: config,
892+
}
893+
894+
emit := func(change stackstate.AppliedChange) {
895+
proto, err := change.AppliedChangeProto()
896+
if err != nil {
897+
server.Send(&stacks.MigrateTerraformState_Event{
898+
Result: &stacks.MigrateTerraformState_Event_Diagnostic{
899+
Diagnostic: &terraform1.Diagnostic{
900+
Severity: terraform1.Diagnostic_ERROR,
901+
Summary: "Failed to serialize change",
902+
Detail: fmt.Sprintf("Failed to serialize state change for recording in the migration plan: %s", err),
903+
},
904+
},
905+
})
906+
return
907+
}
908+
909+
server.Send(&stacks.MigrateTerraformState_Event{
910+
Result: &stacks.MigrateTerraformState_Event_AppliedChange{
911+
AppliedChange: proto,
912+
},
913+
})
914+
}
915+
916+
emitDiag := func(diagnostic tfdiags.Diagnostic) {
917+
server.Send(&stacks.MigrateTerraformState_Event{
918+
Result: &stacks.MigrateTerraformState_Event_Diagnostic{
919+
Diagnostic: diagnosticToProto(diagnostic),
920+
},
921+
})
922+
}
923+
924+
migrate.Migrate(request.ResourceAddressMap, request.ModuleAddressMap, emit, emitDiag)
925+
return nil
926+
}
927+
779928
func stackPlanHooks(evts *syncPlanStackChangesServer, mainStackSource sourceaddrs.FinalSource) *stackruntime.Hooks {
780929
return stackChangeHooks(
781930
func(scp *stacks.StackChangeProgress) error {

internal/rpcapi/stacks_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestStacksOpenCloseStackConfiguration(t *testing.T) {
4343
ctx := context.Background()
4444

4545
handles := newHandleTable()
46-
stacksServer := newStacksServer(newStopper(), handles, &serviceOpts{})
46+
stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{})
4747

4848
// In normal use a client would have previously opened a source bundle
4949
// using Dependencies.OpenSourceBundle, so we'll simulate the effect
@@ -125,7 +125,7 @@ func TestStacksFindStackConfigurationComponents(t *testing.T) {
125125
ctx := context.Background()
126126

127127
handles := newHandleTable()
128-
stacksServer := newStacksServer(newStopper(), handles, &serviceOpts{})
128+
stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{})
129129

130130
// In normal use a client would have previously opened a source bundle
131131
// using Dependencies.OpenSourceBundle, so we'll simulate the effect
@@ -256,7 +256,7 @@ func TestStacksOpenState(t *testing.T) {
256256
ctx := context.Background()
257257

258258
handles := newHandleTable()
259-
stacksServer := newStacksServer(newStopper(), handles, &serviceOpts{})
259+
stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{})
260260

261261
grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) {
262262
stacks.RegisterStacksServer(srv, stacksServer)
@@ -321,7 +321,7 @@ func TestStacksOpenPlan(t *testing.T) {
321321
ctx := context.Background()
322322

323323
handles := newHandleTable()
324-
stacksServer := newStacksServer(newStopper(), handles, &serviceOpts{})
324+
stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{})
325325

326326
grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) {
327327
stacks.RegisterStacksServer(srv, stacksServer)
@@ -392,7 +392,7 @@ func TestStacksPlanStackChanges(t *testing.T) {
392392
}
393393

394394
handles := newHandleTable()
395-
stacksServer := newStacksServer(newStopper(), handles, &serviceOpts{})
395+
stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{})
396396
stacksServer.planTimestampOverride = &fakePlanTimestamp
397397

398398
fakeSourceBundle := &sourcebundle.Bundle{}
@@ -812,7 +812,7 @@ func TestStackChangeProgress(t *testing.T) {
812812
ctx := context.Background()
813813

814814
handles := newHandleTable()
815-
stacksServer := newStacksServer(newStopper(), handles, &serviceOpts{})
815+
stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{})
816816

817817
// For this test, we do actually want to use a "real" provider. We'll
818818
// use the providerCacheOverride to side-load the testing provider.

0 commit comments

Comments
 (0)