Skip to content

Commit 71dbc7d

Browse files
authored
migrate command for terraform stacks (#36482)
1 parent 8b7e7ad commit 71dbc7d

File tree

37 files changed

+8711
-3527
lines changed

37 files changed

+8711
-3527
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/configs/source_bundle_parser.go

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ func (p *SourceBundleParser) IsConfigDir(source sourceaddrs.FinalSource) bool {
8080
return (len(primaryPaths) + len(overridePaths)) > 0
8181
}
8282

83+
// Bundle returns the source bundle that this parser is reading from.
84+
func (p *SourceBundleParser) Bundle() *sourcebundle.Bundle {
85+
return p.sources
86+
}
87+
8388
func (p *SourceBundleParser) dirSources(source sourceaddrs.FinalSource) (primary, override []sourceaddrs.FinalSource, diags hcl.Diagnostics) {
8489
localDir, err := p.sources.LocalPathForSource(source)
8590
if err != nil {

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

+148-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
package rpcapi
55

66
import (
7+
"bytes"
78
"context"
89
"fmt"
910
"io"
1011
"time"
1112

1213
"github.com/hashicorp/go-slug/sourceaddrs"
1314
"github.com/hashicorp/go-slug/sourcebundle"
15+
"github.com/hashicorp/terraform-svchost/disco"
1416
"go.opentelemetry.io/otel/attribute"
1517
otelCodes "go.opentelemetry.io/otel/codes"
1618
"go.opentelemetry.io/otel/trace"
@@ -22,20 +24,25 @@ import (
2224
"github.com/hashicorp/terraform/internal/plans"
2325
"github.com/hashicorp/terraform/internal/providercache"
2426
"github.com/hashicorp/terraform/internal/providers"
27+
"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
2528
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
2629
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
2730
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
31+
"github.com/hashicorp/terraform/internal/stacks/stackmigrate"
2832
"github.com/hashicorp/terraform/internal/stacks/stackplan"
2933
"github.com/hashicorp/terraform/internal/stacks/stackruntime"
3034
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
3135
"github.com/hashicorp/terraform/internal/stacks/stackstate"
36+
"github.com/hashicorp/terraform/internal/states"
37+
"github.com/hashicorp/terraform/internal/states/statefile"
3238
"github.com/hashicorp/terraform/internal/tfdiags"
3339
)
3440

3541
type stacksServer struct {
3642
stacks.UnimplementedStacksServer
3743

3844
stopper *stopper
45+
services *disco.Disco
3946
handles *handleTable
4047
experimentsAllowed bool
4148

@@ -53,11 +60,16 @@ type stacksServer struct {
5360
planTimestampOverride *time.Time
5461
}
5562

56-
var _ stacks.StacksServer = (*stacksServer)(nil)
63+
var (
64+
_ stacks.StacksServer = (*stacksServer)(nil)
5765

58-
func newStacksServer(stopper *stopper, handles *handleTable, opts *serviceOpts) *stacksServer {
66+
WorkspaceNameEnvVar = "TF_WORKSPACE"
67+
)
68+
69+
func newStacksServer(stopper *stopper, handles *handleTable, services *disco.Disco, opts *serviceOpts) *stacksServer {
5970
return &stacksServer{
6071
stopper: stopper,
72+
services: services,
6173
handles: handles,
6274
experimentsAllowed: opts.experimentsAllowed,
6375
}
@@ -860,6 +872,140 @@ func (s *stacksServer) InspectExpressionResult(ctx context.Context, req *stacks.
860872
return insp.InspectExpressionResult(ctx, req)
861873
}
862874

875+
func (s *stacksServer) OpenTerraformState(ctx context.Context, request *stacks.OpenTerraformState_Request) (*stacks.OpenTerraformState_Response, error) {
876+
switch data := request.State.(type) {
877+
case *stacks.OpenTerraformState_Request_ConfigPath:
878+
// Load the state from the backend.
879+
// This function should return an empty state even if the diags
880+
// has errors. This makes it easier for the caller, as they should
881+
// close the state handle regardless of the diags.
882+
loader := stackmigrate.Loader{Discovery: s.services}
883+
state, diags := loader.LoadState(data.ConfigPath)
884+
885+
hnd := s.handles.NewTerraformState(state)
886+
return &stacks.OpenTerraformState_Response{
887+
StateHandle: hnd.ForProtobuf(),
888+
Diagnostics: diagnosticsToProto(diags),
889+
}, nil
890+
891+
case *stacks.OpenTerraformState_Request_Raw:
892+
// load the state from the raw data
893+
file, err := statefile.Read(bytes.NewReader(data.Raw))
894+
if err != nil {
895+
return nil, status.Errorf(codes.InvalidArgument, "invalid raw state data: %s", err)
896+
}
897+
898+
hnd := s.handles.NewTerraformState(file.State)
899+
return &stacks.OpenTerraformState_Response{
900+
StateHandle: hnd.ForProtobuf(),
901+
}, nil
902+
903+
default:
904+
return nil, status.Error(codes.InvalidArgument, "invalid state source")
905+
}
906+
}
907+
908+
func (s *stacksServer) CloseTerraformState(ctx context.Context, request *stacks.CloseTerraformState_Request) (*stacks.CloseTerraformState_Response, error) {
909+
hnd := handle[*states.State](request.StateHandle)
910+
err := s.handles.CloseTerraformState(hnd)
911+
if err != nil {
912+
return nil, status.Error(codes.InvalidArgument, err.Error())
913+
}
914+
return new(stacks.CloseTerraformState_Response), nil
915+
}
916+
917+
func (s *stacksServer) MigrateTerraformState(request *stacks.MigrateTerraformState_Request, server stacks.Stacks_MigrateTerraformStateServer) error {
918+
919+
previousStateHandle := handle[*states.State](request.StateHandle)
920+
previousState := s.handles.TerraformState(previousStateHandle)
921+
if previousState == nil {
922+
return status.Error(codes.InvalidArgument, "the given state handle is invalid")
923+
}
924+
925+
configHandle := handle[*stackconfig.Config](request.ConfigHandle)
926+
config := s.handles.StackConfig(configHandle)
927+
if config == nil {
928+
return status.Error(codes.InvalidArgument, "the given config handle is invalid")
929+
}
930+
931+
dependencyLocksHandle := handle[*depsfile.Locks](request.DependencyLocksHandle)
932+
dependencyLocks := s.handles.DependencyLocks(dependencyLocksHandle)
933+
if dependencyLocks == nil {
934+
return status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid")
935+
}
936+
937+
var providerFactories map[addrs.Provider]providers.Factory
938+
if s.providerCacheOverride != nil {
939+
// This is only used in tests to side load providers without needing a
940+
// real provider cache.
941+
providerFactories = s.providerCacheOverride
942+
} else {
943+
providerCacheHandle := handle[*providercache.Dir](request.ProviderCacheHandle)
944+
providerCache := s.handles.ProviderPluginCache(providerCacheHandle)
945+
if providerCache == nil {
946+
return status.Error(codes.InvalidArgument, "the given provider cache handle is invalid")
947+
}
948+
949+
var err error
950+
providerFactories, err = providerFactoriesForLocks(dependencyLocks, providerCache)
951+
if err != nil {
952+
return status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err)
953+
}
954+
}
955+
956+
migrate := &stackmigrate.Migration{
957+
Providers: providerFactories,
958+
PreviousState: previousState,
959+
Config: config,
960+
}
961+
962+
emit := func(change stackstate.AppliedChange) {
963+
proto, err := change.AppliedChangeProto()
964+
if err != nil {
965+
server.Send(&stacks.MigrateTerraformState_Event{
966+
Result: &stacks.MigrateTerraformState_Event_Diagnostic{
967+
Diagnostic: &terraform1.Diagnostic{
968+
Severity: terraform1.Diagnostic_ERROR,
969+
Summary: "Failed to serialize change",
970+
Detail: fmt.Sprintf("Failed to serialize state change for recording in the migration plan: %s", err),
971+
},
972+
},
973+
})
974+
return
975+
}
976+
977+
server.Send(&stacks.MigrateTerraformState_Event{
978+
Result: &stacks.MigrateTerraformState_Event_AppliedChange{
979+
AppliedChange: proto,
980+
},
981+
})
982+
}
983+
984+
emitDiag := func(diagnostic tfdiags.Diagnostic) {
985+
server.Send(&stacks.MigrateTerraformState_Event{
986+
Result: &stacks.MigrateTerraformState_Event_Diagnostic{
987+
Diagnostic: diagnosticToProto(diagnostic),
988+
},
989+
})
990+
}
991+
992+
mapping := request.GetMapping()
993+
if mapping == nil {
994+
return status.Error(codes.InvalidArgument, "missing migration mapping")
995+
}
996+
switch mapping := mapping.(type) {
997+
case *stacks.MigrateTerraformState_Request_Simple:
998+
migrate.Migrate(
999+
mapping.Simple.ResourceAddressMap,
1000+
mapping.Simple.ModuleAddressMap,
1001+
emit, emitDiag)
1002+
default:
1003+
return status.Error(codes.InvalidArgument, "unsupported migration mapping")
1004+
}
1005+
1006+
return nil
1007+
}
1008+
8631009
func stackPlanHooks(evts *syncPlanStackChangesServer, mainStackSource sourceaddrs.FinalSource) *stackruntime.Hooks {
8641010
return stackChangeHooks(
8651011
func(scp *stacks.StackChangeProgress) error {

0 commit comments

Comments
 (0)