Skip to content

Commit 57e041c

Browse files
mjeffryesjustinvp
andauthored
Report partial errors on create and update (#428)
* Report partial errors * test partial errors * Fix apply error Use a different name than `err` when calling `applyModuleOperation `, otherwise `err` will be overwritten by the call to `PublishViewSteps`. * More logging * Detect more error cases for views This definitely could be refactored/improved (it's not really the right place to be eliding the error), but it does correctly handle the `ReplaceDestroyBeforeCreate` case when delete succeeds but create fails (we assume). * clean up viewStepStatusCheck --------- Co-authored-by: Justin Van Patten <[email protected]>
1 parent 374049e commit 57e041c

File tree

6 files changed

+104
-49
lines changed

6 files changed

+104
-49
lines changed

pkg/modprovider/module.go

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -325,15 +325,8 @@ func (h *moduleHandler) applyModuleOperation(
325325
}
326326

327327
if applyErr != nil {
328-
// TODO[pulumi/pulumi-terraform-module#342] Possibly wrap partial errors in initializationError. This
329-
// does not quite work as expected yet as views get recorded into state as pending_operations. They
330-
// need to be recorded as finalized operations because they did complete.
331-
if 1+2 == 4 {
332-
applyErr = h.initializationError(moduleOutputs, applyErr.Error())
333-
}
334-
335-
// Instead, log and propagate the error for now. This will forget partial TF state but fail Pulumi.
336-
logger.Log(ctx, tfsandbox.Error, fmt.Sprintf("partial failure in apply: %v", applyErr))
328+
// we have a partial error, wrap it with ErrorResourceInitFailed
329+
applyErr = h.initializationError(moduleOutputs, applyErr.Error())
337330
}
338331

339332
hasOutputFieldMappings := inferredModule != nil &&
@@ -486,7 +479,7 @@ func (h *moduleHandler) Update(
486479

487480
//q.Q("Update", req.GetPreview())
488481

489-
moduleOutputs, views, err := h.applyModuleOperation(
482+
moduleOutputs, views, applyErr := h.applyModuleOperation(
490483
ctx,
491484
urn,
492485
moduleInputs,
@@ -499,18 +492,21 @@ func (h *moduleHandler) Update(
499492
req.GetPreview(),
500493
executor,
501494
)
502-
// TODO[pulumi/pulumi-terraform-module#342] partial error handling needs to modify this.
503-
if err != nil {
504-
return nil, err
495+
496+
// Publish views even if applyErr != nil as is the case of partial failures.
497+
if views != nil {
498+
_, err = statusClient.PublishViewSteps(ctx, &pulumirpc.PublishViewStepsRequest{
499+
Token: req.ResourceStatusToken,
500+
Steps: views,
501+
})
502+
if err != nil {
503+
logger.Log(ctx, tfsandbox.Debug, fmt.Sprintf("error publishing view steps after Update: %v", err))
504+
return nil, err
505+
}
505506
}
506507

507-
_, err = statusClient.PublishViewSteps(ctx, &pulumirpc.PublishViewStepsRequest{
508-
Token: req.ResourceStatusToken,
509-
Steps: views,
510-
})
511-
if err != nil {
512-
logger.Log(ctx, tfsandbox.Debug, fmt.Sprintf("error publishing view steps after Update: %v", err))
513-
return nil, err
508+
if applyErr != nil {
509+
return nil, applyErr
514510
}
515511

516512
props, err := plugin.MarshalProperties(moduleOutputs, h.marshalOpts())

pkg/modprovider/views.go

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -154,38 +154,46 @@ func viewStepOp(changeKind tfsandbox.ChangeKind, _ bool /*drift*/) []pulumirpc.V
154154

155155
// Starting with very basic error checks for starters. It should be possible to extract more information from TF.
156156
func viewStepStatusCheck(
157+
op pulumirpc.ViewStep_Op,
157158
changeKind tfsandbox.ChangeKind,
158159
finalState *tfsandbox.ResourceState, // may be nil when planning or failed to create
159160
) error {
160161

161-
switch {
162+
switch changeKind {
162163

163164
// Planned a create but there is no final state. Resource creation must have failed. Neither TF state nor TF
164165
// plan contains the correct error message, so using a generic message for now before TF errors can be properly
165166
// correlated to a resource by address.
166-
case changeKind == tfsandbox.Create && finalState == nil:
167-
return fmt.Errorf("failed to create")
167+
case tfsandbox.Create:
168+
if finalState == nil {
169+
return fmt.Errorf("resource operation failed: %s missing final state", op.String())
170+
}
171+
172+
// All these operations when successful imply the resource must exist in the final state.
173+
case tfsandbox.NoOp, tfsandbox.Update:
174+
if finalState == nil {
175+
return fmt.Errorf("resource operation failed: %s missing final state", op.String())
176+
}
168177

169-
default:
170-
return nil
178+
// Replace expects a final state to be present, so a missing final state implies the create step failed.
179+
// A replace has three view steps, but we only want to return an error on the create step
180+
// when the final state is missing. (That must mean the delete succeeded.)
181+
// It is possible that both the create and delete steps could have failed,
182+
// but we can't easily tell that here. It should be ok to tell the engine that these view steps succeeded,
183+
// and count on a subsequent refresh to correct state.
184+
case tfsandbox.Replace, tfsandbox.ReplaceDestroyBeforeCreate:
185+
if op == pulumirpc.ViewStep_CREATE_REPLACEMENT && finalState == nil {
186+
return fmt.Errorf("resource operation failed: %s missing final state", op.String())
187+
}
171188

189+
// These operations if successful imply the resource must not exist in the final state.
190+
case tfsandbox.Delete, tfsandbox.Forget:
191+
if finalState != nil {
192+
return fmt.Errorf("resource operation failed: %s left resource in state", op.String())
193+
}
172194
}
173195

174-
// All these operations when successful imply the resource must exist in the final state.
175-
// case tfsandbox.NoOp, tfsandbox.Update, tfsandbox.Create,
176-
// tfsandbox.Replace, tfsandbox.ReplaceDestroyBeforeCreate:
177-
// if finalState == nil {
178-
// return errors.New("resource operation failed")
179-
// }
180-
181-
// // These operations if successful imply the resource must not exist in the final state.
182-
// case tfsandbox.Delete, tfsandbox.Forget:
183-
// if finalState != nil {
184-
// return errors.New("resource operation failed")
185-
// }
186-
// }
187-
188-
// return nil
196+
return nil
189197
}
190198

191199
// A resource that has not changed and therefore has no Plan entry in TF needs a ViewStep.
@@ -262,7 +270,7 @@ func viewStepsForResource(
262270
}
263271

264272
if !preview {
265-
if err := viewStepStatusCheck(rplan.ChangeKind(), finalState); err != nil {
273+
if err := viewStepStatusCheck(op, rplan.ChangeKind(), finalState); err != nil {
266274
step.Error = err.Error()
267275
}
268276
}

tests/acc_test.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,6 @@ func TestLambdaMemorySizeDiff(t *testing.T) {
245245
func TestPartialApply(t *testing.T) {
246246
t.Parallel()
247247

248-
t.Skip("TODO[pulumi/pulumi#19635]")
249-
250248
var debugOpts debug.LoggingOptions
251249

252250
// To enable debug logging in this test, un-comment:
@@ -279,6 +277,10 @@ func TestPartialApply(t *testing.T) {
279277
// Generate package
280278
pulumiPackageAdd(t, integrationTest, localProviderBinPath, localMod, "localmod")
281279

280+
t.Logf("################################################################################")
281+
t.Logf("step 1 - partial failure in create")
282+
t.Logf("################################################################################")
283+
282284
_, err = integrationTest.CurrentStack().Up(
283285
integrationTest.Context(),
284286
optup.Diff(),
@@ -298,7 +300,7 @@ func TestPartialApply(t *testing.T) {
298300
mustFindDeploymentResourceByType(t, integrationTest, "localmod:tf:aws_iam_role")
299301

300302
t.Logf("################################################################################")
301-
t.Logf("step 2")
303+
t.Logf("step 2 - complete create")
302304
t.Logf("################################################################################")
303305

304306
integrationTest.SetConfig(t, "step", "2")
@@ -311,11 +313,53 @@ func TestPartialApply(t *testing.T) {
311313
)
312314
changes2 := *upRes2.Summary.ResourceChanges
313315
assert.Equal(t, map[string]int{
314-
"update": 1,
316+
"update": 2,
315317
"create": 1,
316-
"same": 2,
318+
"same": 1,
317319
}, changes2)
318320
assert.Contains(t, upRes2.Outputs, "roleArn")
321+
322+
t.Logf("State: %s", string(integrationTest.ExportStack(t).Deployment))
323+
324+
t.Logf("################################################################################")
325+
t.Logf("step 3 - partial failure in update")
326+
t.Logf("################################################################################")
327+
328+
integrationTest.SetConfig(t, "step", "3")
329+
330+
_, err = integrationTest.CurrentStack().Up(
331+
integrationTest.Context(),
332+
optup.Diff(),
333+
optup.ErrorProgressStreams(testWriter),
334+
optup.ProgressStreams(testWriter),
335+
optup.DebugLogging(debugOpts),
336+
)
337+
338+
assert.Errorf(t, err, "expected error on up")
339+
340+
t.Logf("State: %s", string(integrationTest.ExportStack(t).Deployment))
341+
342+
t.Logf("################################################################################")
343+
t.Logf("step 4 - complete update")
344+
t.Logf("################################################################################")
345+
346+
integrationTest.SetConfig(t, "step", "4")
347+
348+
upRes4 := integrationTest.Up(t,
349+
optup.Diff(),
350+
optup.ErrorProgressStreams(testWriter),
351+
optup.ProgressStreams(testWriter),
352+
optup.DebugLogging(debugOpts),
353+
)
354+
changes4 := *upRes4.Summary.ResourceChanges
355+
assert.Equal(t, map[string]int{
356+
"update": 2,
357+
"create": 1,
358+
"same": 1,
359+
}, changes4)
360+
assert.Contains(t, upRes4.Outputs, "roleArn")
361+
362+
t.Logf("State: %s", string(integrationTest.ExportStack(t).Deployment))
319363
}
320364

321365
// Sanity check that we can provision two instances of the same module side-by-side, in particular

tests/testdata/programs/ts/partial-apply/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ const prefix = config.get('prefix') ?? pulumi.getStack();
66
const step = config.getNumber('step') ?? 1;
77

88
const mod = new localmod.Module('test-localmod', {
9-
name_prefix: prefix,
10-
should_fail: step === 1 ? true : false,
9+
name_prefix: prefix,
10+
description: `Step ${step}`,
11+
should_fail: step % 2 === 1 ? true : false,
1112
});
1213

13-
export const roleArn = mod.role_arn;
14+
export const roleArn = mod.role_arn;
1415

tests/testdata/programs/ts/partial-apply/local_module/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
resource "aws_iam_role" "this" {
22
name_prefix = var.name_prefix
3+
description = var.description
34
assume_role_policy = jsonencode({
45
Version = "2012-10-17"
56
Statement = [

tests/testdata/programs/ts/partial-apply/local_module/variables.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ variable "name_prefix" {
33
description = "Prefix to use for the name of the IAM role"
44
}
55

6+
variable "description" {
7+
type = string
8+
description = "description of the IAM role"
9+
}
10+
611
variable "should_fail" {
712
type = bool
813
description = "Whether the module should fail"

0 commit comments

Comments
 (0)