Skip to content

Commit cb743c8

Browse files
authored
fix(product_enablement): improve error handling for various scenarios (#651)
* fix(product_enablement): set defaults so no unexpected plan diff when block is defined with no attributes set * fix(product_enablement): improve error handling for various scenarios * fix(tests): update name computed value
1 parent 8aca814 commit cb743c8

File tree

3 files changed

+103
-31
lines changed

3 files changed

+103
-31
lines changed

fastly/block_fastly_service_product_enablement.go

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package fastly
22

33
import (
44
"context"
5+
"fmt"
56
"log"
7+
"net/http"
8+
"strings"
69

710
gofastly "github.com/fastly/go-fastly/v7/fastly"
811
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -42,6 +45,7 @@ func (h *ProductEnablementServiceAttributeHandler) GetSchema() *schema.Schema {
4245
blockAttributes["fanout"] = &schema.Schema{
4346
Type: schema.TypeBool,
4447
Optional: true,
48+
Default: false,
4549
Description: "Enable Fanout support",
4650
}
4751
}
@@ -50,21 +54,25 @@ func (h *ProductEnablementServiceAttributeHandler) GetSchema() *schema.Schema {
5054
blockAttributes["brotli_compression"] = &schema.Schema{
5155
Type: schema.TypeBool,
5256
Optional: true,
57+
Default: false,
5358
Description: "Enable Brotli Compression support",
5459
}
5560
blockAttributes["domain_inspector"] = &schema.Schema{
5661
Type: schema.TypeBool,
5762
Optional: true,
63+
Default: false,
5864
Description: "Enable Domain Inspector support",
5965
}
6066
blockAttributes["image_optimizer"] = &schema.Schema{
6167
Type: schema.TypeBool,
6268
Optional: true,
69+
Default: false,
6370
Description: "Enable Image Optimizer support (requires at least one backend with a `shield` attribute)",
6471
}
6572
blockAttributes["origin_inspector"] = &schema.Schema{
6673
Type: schema.TypeBool,
6774
Optional: true,
75+
Default: false,
6876
Description: "Enable Origin Inspector support",
6977
}
7078
}
@@ -73,6 +81,7 @@ func (h *ProductEnablementServiceAttributeHandler) GetSchema() *schema.Schema {
7381
blockAttributes["websockets"] = &schema.Schema{
7482
Type: schema.TypeBool,
7583
Optional: true,
84+
Default: false,
7685
Description: "Enable WebSockets support",
7786
}
7887

@@ -101,7 +110,7 @@ func (h *ProductEnablementServiceAttributeHandler) Create(_ context.Context, d *
101110
ServiceID: serviceID,
102111
})
103112
if err != nil {
104-
return err
113+
return fmt.Errorf("failed to enable fanout: %w", err)
105114
}
106115
}
107116
}
@@ -114,7 +123,7 @@ func (h *ProductEnablementServiceAttributeHandler) Create(_ context.Context, d *
114123
ServiceID: serviceID,
115124
})
116125
if err != nil {
117-
return err
126+
return fmt.Errorf("failed to enable brotli_compression: %w", err)
118127
}
119128
}
120129

@@ -125,7 +134,7 @@ func (h *ProductEnablementServiceAttributeHandler) Create(_ context.Context, d *
125134
ServiceID: serviceID,
126135
})
127136
if err != nil {
128-
return err
137+
return fmt.Errorf("failed to enable domain_inspector: %w", err)
129138
}
130139
}
131140

@@ -136,7 +145,7 @@ func (h *ProductEnablementServiceAttributeHandler) Create(_ context.Context, d *
136145
ServiceID: serviceID,
137146
})
138147
if err != nil {
139-
return err
148+
return fmt.Errorf("failed to enable image_optimizer: %w", err)
140149
}
141150
}
142151

@@ -147,7 +156,7 @@ func (h *ProductEnablementServiceAttributeHandler) Create(_ context.Context, d *
147156
ServiceID: serviceID,
148157
})
149158
if err != nil {
150-
return err
159+
return fmt.Errorf("failed to enable origin_inspector: %w", err)
151160
}
152161
}
153162
}
@@ -159,7 +168,7 @@ func (h *ProductEnablementServiceAttributeHandler) Create(_ context.Context, d *
159168
ServiceID: serviceID,
160169
})
161170
if err != nil {
162-
return err
171+
return fmt.Errorf("failed to enable websockets: %w", err)
163172
}
164173
}
165174

@@ -191,7 +200,7 @@ func (h *ProductEnablementServiceAttributeHandler) Read(_ context.Context, d *sc
191200
// This is done so we can benefit from the 'modified' map data passed to the
192201
// 'update' CRUD method.
193202
result := map[string]any{
194-
"name": "product_enablement",
203+
"name": "products",
195204
}
196205

197206
if h.GetServiceMetadata().serviceType == ServiceTypeCompute {
@@ -282,7 +291,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
282291
ServiceID: serviceID,
283292
})
284293
if err != nil {
285-
return err
294+
return fmt.Errorf("failed to enable fanout: %w", err)
286295
}
287296
} else {
288297
log.Println("[DEBUG] fanout not set")
@@ -291,13 +300,15 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
291300
ServiceID: serviceID,
292301
})
293302
if err != nil {
294-
return err
303+
return fmt.Errorf("failed to disable fanout: %w", err)
295304
}
296305
}
297306
}
298307
}
299308

300309
if h.GetServiceMetadata().serviceType == ServiceTypeVCL {
310+
// FIXME: Looks like `modified` contains products that haven't been updated.
311+
// The only practical issue here is that an unnecessary API request is made.
301312
if v, ok := modified["brotli_compression"]; ok {
302313
if v.(bool) {
303314
log.Println("[DEBUG] brotli_compression set")
@@ -306,7 +317,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
306317
ServiceID: serviceID,
307318
})
308319
if err != nil {
309-
return err
320+
return fmt.Errorf("failed to enable brotli_compression: %w", err)
310321
}
311322
} else {
312323
log.Println("[DEBUG] brotli_compression not set")
@@ -315,7 +326,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
315326
ServiceID: serviceID,
316327
})
317328
if err != nil {
318-
return err
329+
return fmt.Errorf("failed to disable brotli_compression: %w", err)
319330
}
320331
}
321332
}
@@ -328,7 +339,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
328339
ServiceID: serviceID,
329340
})
330341
if err != nil {
331-
return err
342+
return fmt.Errorf("failed to enable domain_inspector: %w", err)
332343
}
333344
} else {
334345
log.Println("[DEBUG] domain_inspector not set")
@@ -337,7 +348,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
337348
ServiceID: serviceID,
338349
})
339350
if err != nil {
340-
return err
351+
return fmt.Errorf("failed to disable domain_inspector: %w", err)
341352
}
342353
}
343354
}
@@ -350,7 +361,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
350361
ServiceID: serviceID,
351362
})
352363
if err != nil {
353-
return err
364+
return fmt.Errorf("failed to enable image_optimizer: %w", err)
354365
}
355366
} else {
356367
log.Println("[DEBUG] image_optimizer not set")
@@ -359,7 +370,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
359370
ServiceID: serviceID,
360371
})
361372
if err != nil {
362-
return err
373+
return fmt.Errorf("failed to disable image_optimizer: %w", err)
363374
}
364375
}
365376
}
@@ -372,7 +383,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
372383
ServiceID: serviceID,
373384
})
374385
if err != nil {
375-
return err
386+
return fmt.Errorf("failed to enable origin_inspector: %w", err)
376387
}
377388
} else {
378389
log.Println("[DEBUG] origin_inspector not set")
@@ -381,7 +392,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
381392
ServiceID: serviceID,
382393
})
383394
if err != nil {
384-
return err
395+
return fmt.Errorf("failed to disable origin_inspector: %w", err)
385396
}
386397
}
387398
}
@@ -395,7 +406,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
395406
ServiceID: serviceID,
396407
})
397408
if err != nil {
398-
return err
409+
return fmt.Errorf("failed to enable websockets: %w", err)
399410
}
400411
} else {
401412
log.Println("[DEBUG] websockets not set")
@@ -404,7 +415,7 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
404415
ServiceID: serviceID,
405416
})
406417
if err != nil {
407-
return err
418+
return fmt.Errorf("failed to disable websockets: %w", err)
408419
}
409420
}
410421
}
@@ -414,60 +425,121 @@ func (h *ProductEnablementServiceAttributeHandler) Update(_ context.Context, d *
414425

415426
// Delete deletes the resource.
416427
//
417-
// FIXME: This implementation causes unnecessary API calls.
418-
// We should check if the specific 'products' have been disabled.
428+
// IMPORTANT: We must allow a user to clean-up their state.
429+
// If a user doesn't have self-enablement for a particular product and they add
430+
// the product_enablement block to their config, then they'll have errors trying
431+
// to enable that product. The problem now is if they remove the block from
432+
// their config completely, they still won't be able to successfully apply the
433+
// deletion because the API will error telling them they're not entitled to
434+
// disable a product. So if that's the error we're getting back from the API,
435+
// then we'll skip the error as we want the `terraform apply` to be successful
436+
// and for the user to end up with a clean state.
437+
//
438+
// NOTE: We don't return the nil error, ensuring all products are processed.
439+
// We don't want to return the nil error (e.g. when a user is trying to clean-up
440+
// their state as they're not entitled to enable/disable the product) because
441+
// returning nil will short-circuit the `Delete` method and we'll not process
442+
// the disabling of other products they might be entitled to disable!
443+
//
444+
// FIXME: Looks like the use of a TypeSet means unnecessary API calls.
445+
// In a scenario where a new product is set to `true` (e.g. to be enabled) the
446+
// set hash changes and so the set 'as a whole' is deleted (causing all the
447+
// products to be disabled) and then all the APIs are called again to re-enable
448+
// the products (even though they might not have actually been set to `false` in
449+
// the first place). The solution would be to swap TypeSet for TypeList but then
450+
// that means we'd lose the 'modified' data diff abstraction that was built into
451+
// the Fastly provider. Look at the `package` block as an example of a TypeList,
452+
// and you'll see it doesn't implement standard CRUD methods but has a single
453+
// `Process` method that handles both CREATE and UPDATE stages and doesn't get
454+
// passed a data structure that indicates what has changed like we do with the
455+
// TypeSet data type. So it'll be a trade-off.
419456
func (h *ProductEnablementServiceAttributeHandler) Delete(_ context.Context, d *schema.ResourceData, resource map[string]any, serviceVersion int, conn *gofastly.Client) error {
420457
if h.GetServiceMetadata().serviceType == ServiceTypeCompute {
458+
log.Println("[DEBUG] disable fanout")
421459
err := conn.DisableProduct(&gofastly.ProductEnablementInput{
422460
ProductID: gofastly.ProductFanout,
423461
ServiceID: d.Id(),
424462
})
425463
if err != nil {
426-
return err
464+
if e := h.checkAPIError(err); e != nil {
465+
return e
466+
}
427467
}
428468
}
429469

430470
if h.GetServiceMetadata().serviceType == ServiceTypeVCL {
471+
log.Println("[DEBUG] disable brotli_compression")
431472
err := conn.DisableProduct(&gofastly.ProductEnablementInput{
432473
ProductID: gofastly.ProductBrotliCompression,
433474
ServiceID: d.Id(),
434475
})
435476
if err != nil {
436-
return err
477+
if e := h.checkAPIError(err); e != nil {
478+
return e
479+
}
437480
}
438481

482+
log.Println("[DEBUG] disable domain_inspector")
439483
err = conn.DisableProduct(&gofastly.ProductEnablementInput{
440484
ProductID: gofastly.ProductDomainInspector,
441485
ServiceID: d.Id(),
442486
})
443487
if err != nil {
444-
return err
488+
if e := h.checkAPIError(err); e != nil {
489+
return e
490+
}
445491
}
446492

493+
log.Println("[DEBUG] disable image_optimizer")
447494
err = conn.DisableProduct(&gofastly.ProductEnablementInput{
448495
ProductID: gofastly.ProductImageOptimizer,
449496
ServiceID: d.Id(),
450497
})
451498
if err != nil {
452-
return err
499+
if e := h.checkAPIError(err); e != nil {
500+
return e
501+
}
453502
}
454503

504+
log.Println("[DEBUG] disable origin_inspector")
455505
err = conn.DisableProduct(&gofastly.ProductEnablementInput{
456506
ProductID: gofastly.ProductOriginInspector,
457507
ServiceID: d.Id(),
458508
})
459509
if err != nil {
460-
return err
510+
if e := h.checkAPIError(err); e != nil {
511+
return e
512+
}
461513
}
462514
}
463515

516+
log.Println("[DEBUG] disable websockets")
464517
err := conn.DisableProduct(&gofastly.ProductEnablementInput{
465518
ProductID: gofastly.ProductWebSockets,
466519
ServiceID: d.Id(),
467520
})
468521
if err != nil {
469-
return err
522+
if e := h.checkAPIError(err); e != nil {
523+
return e
524+
}
470525
}
471526

472527
return nil
473528
}
529+
530+
// checkAPIError inspects the error type for a title that has a message
531+
// indicating the user cannot call the API because they are not entitled. For
532+
// these users we want to skip the error so that we can allow them to clean up
533+
// their Terraform state.
534+
func (h *ProductEnablementServiceAttributeHandler) checkAPIError(err error) error {
535+
if he, ok := err.(*gofastly.HTTPError); ok {
536+
if he.StatusCode == http.StatusBadRequest {
537+
for _, e := range he.Errors {
538+
if strings.Contains(e.Title, "is not entitled to disable product") {
539+
return nil
540+
}
541+
}
542+
}
543+
}
544+
return err
545+
}

fastly/resource_fastly_service_compute_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ func TestAccFastlyServiceCompute_basic(t *testing.T) {
6060
if s[0].Attributes["product_enablement.0.websockets"] != "false" {
6161
return fmt.Errorf("expected websockets to be false")
6262
}
63-
if s[0].Attributes["product_enablement.0.name"] != "product_enablement" {
64-
return fmt.Errorf("expected the generated 'name' key to be 'product_enablement'")
63+
if s[0].Attributes["product_enablement.0.name"] != "products" {
64+
return fmt.Errorf("expected the generated 'name' key to be 'products'")
6565
}
6666
return nil
6767
},

fastly/resource_fastly_service_vcl_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,8 @@ func TestAccFastlyServiceVCL_basic(t *testing.T) {
377377
if s[0].Attributes["product_enablement.0.websockets"] != "false" {
378378
return fmt.Errorf("expected websockets to be false")
379379
}
380-
if s[0].Attributes["product_enablement.0.name"] != "product_enablement" {
381-
return fmt.Errorf("expected the generated 'name' key to be 'product_enablement'")
380+
if s[0].Attributes["product_enablement.0.name"] != "products" {
381+
return fmt.Errorf("expected the generated 'name' key to be 'products'")
382382
}
383383
return nil
384384
},

0 commit comments

Comments
 (0)