@@ -2,7 +2,10 @@ package fastly
22
33import (
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.
419456func (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+ }
0 commit comments