Skip to content

Commit 9103ea1

Browse files
authored
Fix dependency handling (#179)
This change fixes dependency handling. The fix is coarse-grained but important for correctly ordering deletes. The implementation is currently lacking capabilities to track dependencies precisely through the TF black box. What this does: - if dependencies are passed via dependsOn ResourceOption, Module ComponentResource now correctly depends on these - if dependencies are passed implicitly with inputs, ModuleState Custom Resource now depends on all these dependencies. This means, in particular, that `tofu destroy` will now have to complete before the dependencies start getting destroyed themselves. This fixes issues such as #234 - if Pulumi resources depend on any of the module's outputs, they now also will depend on ModuleState custom resource directly, and transitively on all the dependencies of the module's inputs. They should order their destroy operations accordingly. They also depend on the Component resource itself, respecting dependsOn option on that. Fixes #151 Fixes #234
1 parent ab31764 commit 9103ea1

File tree

15 files changed

+441
-89
lines changed

15 files changed

+441
-89
lines changed

pkg/modprovider/child.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/internals"
3131
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
3232

33-
"github.com/pulumi/pulumi-terraform-module/pkg/property"
33+
"github.com/pulumi/pulumi-terraform-module/pkg/pulumix"
3434
"github.com/pulumi/pulumi-terraform-module/pkg/tfsandbox"
3535
)
3636

@@ -69,7 +69,7 @@ func newChildResource(
6969
inputs := childResourceInputs(modUrn, sop.Address(), sop.Values())
7070
t := childResourceTypeToken(pkgName, sop.Type())
7171
name := childResourceName(sop)
72-
inputsMap := property.MustUnmarshalPropertyMap(ctx, inputs)
72+
inputsMap := pulumix.MustUnmarshalPropertyMap(ctx, inputs)
7373
err := ctx.RegisterPackageResource(string(t), name, inputsMap, &resource, packageRef, opts...)
7474
if err != nil {
7575
return nil, fmt.Errorf("RegisterResource failed for a child resource: %w", err)

pkg/modprovider/module_component.go

+33-35
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
2727
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/internals"
2828

29-
"github.com/pulumi/pulumi-terraform-module/pkg/property"
29+
"github.com/pulumi/pulumi-terraform-module/pkg/pulumix"
3030
"github.com/pulumi/pulumi-terraform-module/pkg/tfsandbox"
3131
)
3232

@@ -52,7 +52,7 @@ func componentTypeToken(packageName packageName, compTypeName componentTypeName)
5252
return tokens.Type(fmt.Sprintf("%s:index:%s", packageName, compTypeName))
5353
}
5454

55-
func NewModuleComponentResource(
55+
func newModuleComponentResource(
5656
ctx *pulumi.Context,
5757
stateStore moduleStateStore,
5858
planStore *planStore,
@@ -67,12 +67,12 @@ func NewModuleComponentResource(
6767
providerSelfURN pulumi.URN,
6868
providersConfig map[string]resource.PropertyMap,
6969
opts ...pulumi.ResourceOption,
70-
) (componentUrn *urn.URN, outputs pulumi.Input, finalError error) {
70+
) (componentUrn *urn.URN, moduleStateResource *moduleStateResource, outputs pulumi.Map, finalError error) {
7171
component := ModuleComponentResource{}
7272
tok := componentTypeToken(pkgName, compTypeName)
7373
err := ctx.RegisterComponentResource(string(tok), name, &component, opts...)
7474
if err != nil {
75-
return nil, nil, fmt.Errorf("RegisterComponentResource failed: %w", err)
75+
return nil, nil, nil, fmt.Errorf("RegisterComponentResource failed: %w", err)
7676
}
7777

7878
urn := component.MustURN(ctx.Context())
@@ -91,27 +91,25 @@ func NewModuleComponentResource(
9191
providerSelfRef = newProviderSelfReference(ctx, providerSelfURN)
9292
}
9393

94-
go func() {
95-
resourceOptions := []pulumi.ResourceOption{
96-
pulumi.Parent(&component),
97-
}
94+
resourceOptions := []pulumi.ResourceOption{
95+
pulumi.Parent(&component),
96+
}
9897

99-
if providerSelfRef != nil {
100-
resourceOptions = append(resourceOptions, pulumi.Provider(providerSelfRef))
101-
}
98+
if providerSelfRef != nil {
99+
resourceOptions = append(resourceOptions, pulumi.Provider(providerSelfRef))
100+
}
102101

103-
_, err := newModuleStateResource(ctx,
104-
// Needs to be prefixed by parent to avoid "duplicate URN".
105-
fmt.Sprintf("%s-state", name),
106-
pkgName,
107-
urn,
108-
packageRef,
109-
moduleInputs,
110-
resourceOptions...,
111-
)
112-
113-
contract.AssertNoErrorf(err, "newModuleStateResource failed")
114-
}()
102+
modStateResource, err := newModuleStateResource(ctx,
103+
// Needs to be prefixed by parent to avoid "duplicate URN".
104+
fmt.Sprintf("%s-state", name),
105+
pkgName,
106+
urn,
107+
packageRef,
108+
moduleInputs,
109+
resourceOptions...,
110+
)
111+
112+
contract.AssertNoErrorf(err, "newModuleStateResource failed")
115113

116114
state := stateStore.AwaitOldState(urn)
117115
defer func() {
@@ -126,7 +124,7 @@ func NewModuleComponentResource(
126124
wd := tfsandbox.ModuleInstanceWorkdir(urn)
127125
tf, err := tfsandbox.NewTofu(ctx.Context(), wd)
128126
if err != nil {
129-
return nil, nil, fmt.Errorf("Sandbox construction failed: %w", err)
127+
return nil, nil, nil, fmt.Errorf("Sandbox construction failed: %w", err)
130128
}
131129

132130
// Important: the name of the module instance in TF must be at least unique enough to
@@ -147,20 +145,20 @@ func NewModuleComponentResource(
147145
moduleInputs, outputSpecs, providersConfig)
148146

149147
if err != nil {
150-
return nil, nil, fmt.Errorf("Seed file generation failed: %w", err)
148+
return nil, nil, nil, fmt.Errorf("Seed file generation failed: %w", err)
151149
}
152150

153151
var moduleOutputs resource.PropertyMap
154152
err = tf.PushStateAndLockFile(ctx.Context(), state.rawState, state.rawLockFile)
155153
if err != nil {
156-
return nil, nil, fmt.Errorf("PushStateAndLockFile failed: %w", err)
154+
return nil, nil, nil, fmt.Errorf("PushStateAndLockFile failed: %w", err)
157155
}
158156

159157
logger := newComponentLogger(ctx.Log, &component)
160158

161159
err = tf.Init(ctx.Context(), logger)
162160
if err != nil {
163-
return nil, nil, fmt.Errorf("Init failed: %w", err)
161+
return nil, nil, nil, fmt.Errorf("Init failed: %w", err)
164162
}
165163

166164
var childResources []*childResource
@@ -169,7 +167,7 @@ func NewModuleComponentResource(
169167
// may be able to reuse the plan from DryRun for the subsequent application.
170168
plan, err := tf.Plan(ctx.Context(), logger)
171169
if err != nil {
172-
return nil, nil, fmt.Errorf("Plan failed: %w", err)
170+
return nil, nil, nil, fmt.Errorf("Plan failed: %w", err)
173171
}
174172

175173
planStore.SetPlan(urn, plan)
@@ -203,21 +201,21 @@ func NewModuleComponentResource(
203201
}
204202
})
205203
if err := errors.Join(errs...); err != nil {
206-
return nil, nil, fmt.Errorf("Child resource init failed: %w", err)
204+
return nil, nil, nil, fmt.Errorf("Child resource init failed: %w", err)
207205
}
208206
moduleOutputs = plan.Outputs()
209207
} else {
210208
// DryRun() = false corresponds to running pulumi up
211209
tfState, err := tf.Apply(ctx.Context(), logger)
212210
if err != nil {
213-
return nil, nil, fmt.Errorf("Apply failed: %w", err)
211+
return nil, nil, nil, fmt.Errorf("Apply failed: %w", err)
214212
}
215213

216214
planStore.SetState(urn, tfState)
217215

218216
rawState, rawLockFile, err := tf.PullStateAndLockFile(ctx.Context())
219217
if err != nil {
220-
return nil, nil, fmt.Errorf("PullStateAndLockFile failed: %w", err)
218+
return nil, nil, nil, fmt.Errorf("PullStateAndLockFile failed: %w", err)
221219
}
222220
state.rawState = rawState
223221
state.rawLockFile = rawLockFile
@@ -246,7 +244,7 @@ func NewModuleComponentResource(
246244
}
247245
})
248246
if err := errors.Join(errs...); err != nil {
249-
return nil, nil, fmt.Errorf("Child resource init failed: %w", err)
247+
return nil, nil, nil, fmt.Errorf("Child resource init failed: %w", err)
250248
}
251249

252250
moduleOutputs = tfState.Outputs()
@@ -259,12 +257,12 @@ func NewModuleComponentResource(
259257
cr.Await(ctx.Context())
260258
}
261259

262-
marshalledOutputs := property.MustUnmarshalPropertyMap(ctx, moduleOutputs)
260+
marshalledOutputs := pulumix.MustUnmarshalPropertyMap(ctx, moduleOutputs)
263261
if err := ctx.RegisterResourceOutputs(&component, marshalledOutputs); err != nil {
264-
return nil, nil, fmt.Errorf("RegisterResourceOutputs failed: %w", err)
262+
return nil, nil, nil, fmt.Errorf("RegisterResourceOutputs failed: %w", err)
265263
}
266264

267-
return &urn, marshalledOutputs, nil
265+
return &urn, modStateResource, marshalledOutputs, nil
268266
}
269267

270268
func newProviderSelfReference(ctx *pulumi.Context, urn1 pulumi.URN) pulumi.ProviderResource {

pkg/modprovider/server.go

+34-17
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import (
3535
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
3636
pulumiprovider "github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider"
3737
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
38+
39+
"github.com/pulumi/pulumi-terraform-module/pkg/pulumix"
3840
)
3941

4042
func StartServer(hostClient *provider.HostClient) (pulumirpc.ResourceProviderServer, error) {
@@ -62,8 +64,12 @@ type server struct {
6264
packageVersion packageVersion
6365
componentTypeName componentTypeName
6466
inferredModuleSchema *InferredModuleSchema
65-
providerConfig resource.PropertyMap
6667
providerSelfURN pulumi.URN
68+
69+
// Note that providerConfig does not include any first-class dependencies passed as Output values. In fact
70+
// there are no Output values inside this map. In the current implementation this is OK as the data is only
71+
// used to produce Terraform files to feed to opentofu and lacks the capability to track these dependencies.
72+
providerConfig resource.PropertyMap
6773
}
6874

6975
func (s *server) Parameterize(
@@ -212,9 +218,12 @@ func (s *server) Configure(
212218
req *pulumirpc.ConfigureRequest,
213219
) (*pulumirpc.ConfigureResponse, error) {
214220
config, err := plugin.UnmarshalProperties(req.Args, plugin.MarshalOptions{
215-
KeepUnknowns: true,
216-
RejectAssets: true,
217-
KeepSecrets: true,
221+
KeepUnknowns: true,
222+
RejectAssets: true,
223+
KeepSecrets: true,
224+
225+
// This is only used to store s.providerConfig so it is OK to ignore dependencies in any Output values
226+
// present in the request.
218227
KeepOutputValues: false,
219228
})
220229

@@ -341,11 +350,10 @@ func (s *server) Construct(
341350
req *pulumirpc.ConstructRequest,
342351
) (*pulumirpc.ConstructResponse, error) {
343352
inputProps, err := plugin.UnmarshalProperties(req.GetInputs(), plugin.MarshalOptions{
344-
KeepUnknowns: true,
345-
KeepSecrets: true,
346-
KeepResources: true,
347-
// TODO[https://github.com/pulumi/pulumi-terraform-module/issues/151] support Outputs in Unmarshal
348-
KeepOutputValues: false,
353+
KeepUnknowns: true,
354+
KeepSecrets: true,
355+
KeepResources: true,
356+
KeepOutputValues: true,
349357
})
350358

351359
providersConfig := cleanProvidersConfig(s.providerConfig)
@@ -361,12 +369,12 @@ func (s *server) Construct(
361369
return pulumiprovider.Construct(ctx, req, s.hostClient.EngineConn(), func(
362370
ctx *pulumi.Context, typ, name string,
363371
_ pulumiprovider.ConstructInputs,
364-
_ pulumi.ResourceOption,
372+
resourceOptions pulumi.ResourceOption,
365373
) (*pulumiprovider.ConstructResult, error) {
366374
ctok := componentTypeToken(s.packageName, s.componentTypeName)
367375
switch typ {
368376
case string(ctok):
369-
componentUrn, outputs, err := NewModuleComponentResource(ctx,
377+
componentUrn, modStateResource, outputs, err := newModuleComponentResource(ctx,
370378
s.stateStore,
371379
s.planStore,
372380
s.packageName,
@@ -378,14 +386,20 @@ func (s *server) Construct(
378386
s.inferredModuleSchema,
379387
packageRef,
380388
s.providerSelfURN,
381-
providersConfig)
389+
providersConfig,
390+
resourceOptions,
391+
)
382392

383393
if err != nil {
384394
return nil, fmt.Errorf("NewModuleComponentResource failed: %w", err)
385395
}
396+
386397
constructResult := &pulumiprovider.ConstructResult{
387-
URN: pulumi.URN(string(*componentUrn)),
388-
State: outputs,
398+
URN: pulumi.URN(string(*componentUrn)),
399+
// Every Output needs to depend on the modStateResource.
400+
State: pulumix.MapWithBroadcastDependencies(ctx.Context(), []pulumi.Resource{
401+
modStateResource,
402+
}, outputs),
389403
}
390404
return constructResult, nil
391405
default:
@@ -415,9 +429,12 @@ func (s *server) CheckConfig(
415429
s.providerSelfURN = pulumi.URN(req.Urn)
416430

417431
config, err := plugin.UnmarshalProperties(req.GetNews(), plugin.MarshalOptions{
418-
KeepUnknowns: true,
419-
RejectAssets: true,
420-
KeepSecrets: true,
432+
KeepUnknowns: true,
433+
RejectAssets: true,
434+
KeepSecrets: true,
435+
436+
// This is only used to store s.providerConfig so it is OK to ignore dependencies in any Output values
437+
// present in the request.
421438
KeepOutputValues: false,
422439
})
423440

pkg/modprovider/state.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import (
3131
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
3232
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
3333

34-
"github.com/pulumi/pulumi-terraform-module/pkg/property"
34+
"github.com/pulumi/pulumi-terraform-module/pkg/pulumix"
3535
"github.com/pulumi/pulumi-terraform-module/pkg/tfsandbox"
3636
)
3737

@@ -119,7 +119,7 @@ func newModuleStateResource(
119119
var res moduleStateResource
120120
tok := moduleStateTypeToken(pkgName)
121121

122-
inputsMap := property.MustUnmarshalPropertyMap(ctx, resource.PropertyMap{
122+
inputsMap := pulumix.MustUnmarshalPropertyMap(ctx, resource.PropertyMap{
123123
moduleURNPropName: resource.NewStringProperty(string(modUrn)),
124124
"moduleInputs": resource.NewObjectProperty(moduleInputs),
125125
})
@@ -268,7 +268,11 @@ func (h *moduleStateHandler) Delete(
268268
KeepUnknowns: true,
269269
KeepSecrets: true,
270270
KeepResources: true,
271-
// TODO[pulumi/pulumi-terraform-module#151] support Outputs in Unmarshal
271+
272+
// If there are any resource.NewOutputProperty values in old inputs with dependencies, this setting
273+
// will ignore the dependencies and remove these values in favor of simpler Computed or Secret values.
274+
// This is OK for the purposes of Delete running tofu destroy because the code cannot take advantage of
275+
// these precisely tracked dependencies here anyway. So it is OK to ignore them.
272276
KeepOutputValues: false,
273277
})
274278
if err != nil {

pkg/pulumix/map.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2016-2025, Pulumi Corporation.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package pulumix
16+
17+
import (
18+
"context"
19+
20+
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
21+
)
22+
23+
// Constructs an updated Map where every Input is augmented with the same set of additional dependencies.
24+
func MapWithBroadcastDependencies(ctx context.Context, dependencies []pulumi.Resource, out pulumi.Map) pulumi.Map {
25+
if len(dependencies) == 0 {
26+
return out
27+
}
28+
result := pulumi.Map{}
29+
for k, input := range out {
30+
output := pulumi.ToOutputWithContext(ctx, input)
31+
result[k] = pulumi.OutputWithDependencies(ctx, output, dependencies...)
32+
}
33+
return result
34+
}

pkg/property/package.go pkg/pulumix/package.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// Package property exposes PropertyMap/PropertyValue helpers that did not make it into the official Pulumi Go SDK yet.
15+
// Package pulumix exposes helpers that did not make it into the official Pulumi Go SDK yet.
1616
//
1717
// See also: https://github.com/pulumi/pulumi/issues/18447
18-
package property
18+
package pulumix

0 commit comments

Comments
 (0)