diff --git a/azurerm/internal/common/client_options.go b/azurerm/internal/common/client_options.go index abe91cd852fd..8bd3552adaac 100644 --- a/azurerm/internal/common/client_options.go +++ b/azurerm/internal/common/client_options.go @@ -11,6 +11,7 @@ import ( "github.com/Azure/go-autorest/autorest/azure" "github.com/hashicorp/go-azure-helpers/sender" "github.com/hashicorp/terraform/httpclient" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" "github.com/terraform-providers/terraform-provider-azurerm/version" ) @@ -27,10 +28,12 @@ type ClientOptions struct { ResourceManagerEndpoint string StorageAuthorizer autorest.Authorizer - PollingDuration time.Duration SkipProviderReg bool DisableCorrelationRequestID bool Environment azure.Environment + + // TODO: remove me in 2.0 + PollingDuration time.Duration } func (o ClientOptions) ConfigureClient(c *autorest.Client, authorizer autorest.Authorizer) { @@ -38,11 +41,15 @@ func (o ClientOptions) ConfigureClient(c *autorest.Client, authorizer autorest.A c.Authorizer = authorizer c.Sender = sender.BuildSender("AzureRM") - c.PollingDuration = o.PollingDuration c.SkipResourceProviderRegistration = o.SkipProviderReg if !o.DisableCorrelationRequestID { c.RequestInspector = WithCorrelationRequestID(CorrelationRequestID()) } + + // TODO: remove in 2.0 + if !features.SupportsCustomTimeouts() { + c.PollingDuration = o.PollingDuration + } } func setUserAgent(client *autorest.Client, tfVersion, partnerID string) { diff --git a/azurerm/internal/features/custom_timeouts.go b/azurerm/internal/features/custom_timeouts.go new file mode 100644 index 000000000000..a7743fc3ef51 --- /dev/null +++ b/azurerm/internal/features/custom_timeouts.go @@ -0,0 +1,22 @@ +package features + +import ( + "os" + "strings" +) + +// SupportsCustomTimeouts returns whether Custom Timeouts are supported +// +// This feature allows Resources to define Custom Timeouts for Creation, Updating and Deletion +// which helps work with Azure resources that take longer to provision/delete. +// When this feature is disabled, all resources have a hard-coded timeout of 3 hours. +// +// This feature-toggle defaults to off in 1.x versions of the Azure Provider, however this will +// become the default behaviour in version 2.0 of the Azure Provider. As outlined in the announcement +// for v2.0 of the Azure Provider: https://github.com/terraform-providers/terraform-provider-azurerm/issues/2807 +// +// Operators wishing to adopt this behaviour can opt-into this behaviour in 1.x versions of the +// Azure Provider by setting the Environment Variable 'ARM_PROVIDER_CUSTOM_TIMEOUTS' to 'true' +func SupportsCustomTimeouts() bool { + return strings.EqualFold(os.Getenv("ARM_PROVIDER_CUSTOM_TIMEOUTS"), "true") +} diff --git a/azurerm/internal/features/custom_timeouts_test.go b/azurerm/internal/features/custom_timeouts_test.go new file mode 100644 index 000000000000..cd260cf573b4 --- /dev/null +++ b/azurerm/internal/features/custom_timeouts_test.go @@ -0,0 +1,55 @@ +package features + +import ( + "os" + "testing" +) + +func TestCustomTimeouts(t *testing.T) { + testData := []struct { + name string + value string + expected bool + }{ + { + name: "unset", + value: "", + expected: false, + }, + { + name: "disabled lower-case", + value: "false", + expected: false, + }, + { + name: "disabled upper-case", + value: "FALSE", + expected: false, + }, + { + name: "enabled lower-case", + value: "true", + expected: true, + }, + { + name: "enabled upper-case", + value: "TRUE", + expected: true, + }, + { + name: "invalid", + value: "pandas", + expected: false, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Test %q..", v.name) + + os.Setenv("ARM_PROVIDER_CUSTOM_TIMEOUTS", v.value) + actual := SupportsCustomTimeouts() + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} diff --git a/azurerm/internal/timeouts/determine.go b/azurerm/internal/timeouts/determine.go new file mode 100644 index 000000000000..7928459c7701 --- /dev/null +++ b/azurerm/internal/timeouts/determine.go @@ -0,0 +1,66 @@ +package timeouts + +import ( + "context" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" +) + +// TODO: tests for this + +// ForCreate returns the context wrapped with the timeout for an Create operation +// +// If the 'SupportsCustomTimeouts' feature toggle is enabled - this is wrapped with a context +// Otherwise this returns the default context +func ForCreate(ctx context.Context, d *schema.ResourceData) (context.Context, context.CancelFunc) { + return buildWithTimeout(ctx, d.Timeout(schema.TimeoutCreate)) +} + +// ForCreateUpdate returns the context wrapped with the timeout for an combined Create/Update operation +// +// If the 'SupportsCustomTimeouts' feature toggle is enabled - this is wrapped with a context +// Otherwise this returns the default context +func ForCreateUpdate(ctx context.Context, d *schema.ResourceData) (context.Context, context.CancelFunc) { + if d.IsNewResource() { + return ForCreate(ctx, d) + } + + return ForUpdate(ctx, d) +} + +// ForDelete returns the context wrapped with the timeout for an Delete operation +// +// If the 'SupportsCustomTimeouts' feature toggle is enabled - this is wrapped with a context +// Otherwise this returns the default context +func ForDelete(ctx context.Context, d *schema.ResourceData) (context.Context, context.CancelFunc) { + return buildWithTimeout(ctx, d.Timeout(schema.TimeoutDelete)) +} + +// ForRead returns the context wrapped with the timeout for an Read operation +// +// If the 'SupportsCustomTimeouts' feature toggle is enabled - this is wrapped with a context +// Otherwise this returns the default context +func ForRead(ctx context.Context, d *schema.ResourceData) (context.Context, context.CancelFunc) { + return buildWithTimeout(ctx, d.Timeout(schema.TimeoutRead)) +} + +// ForUpdate returns the context wrapped with the timeout for an Update operation +// +// If the 'SupportsCustomTimeouts' feature toggle is enabled - this is wrapped with a context +// Otherwise this returns the default context +func ForUpdate(ctx context.Context, d *schema.ResourceData) (context.Context, context.CancelFunc) { + return buildWithTimeout(ctx, d.Timeout(schema.TimeoutUpdate)) +} + +func buildWithTimeout(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { + if features.SupportsCustomTimeouts() { + return context.WithTimeout(ctx, timeout) + } + + nullFunc := func() { + // do nothing on cancel since timeouts aren't enabled + } + return ctx, nullFunc +} diff --git a/azurerm/provider.go b/azurerm/provider.go index 0138b9fbc463..6023a7b26e8d 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -5,11 +5,13 @@ import ( "log" "os" "strings" + "time" "github.com/hashicorp/go-azure-helpers/authentication" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/common" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" @@ -476,6 +478,29 @@ func Provider() terraform.ResourceProvider { } } + // TODO: remove all of this in 2.0 once Custom Timeouts are supported + if features.SupportsCustomTimeouts() { + // default everything to 3 hours for now + for _, v := range resources { + if v.Timeouts == nil { + v.Timeouts = &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(3 * time.Hour), + Update: schema.DefaultTimeout(3 * time.Hour), + Delete: schema.DefaultTimeout(3 * time.Hour), + Default: schema.DefaultTimeout(3 * time.Hour), + + // Read is the only exception, since if it's taken more than 5 minutes something's seriously wrong + Read: schema.DefaultTimeout(5 * time.Minute), + } + } + } + } else { + // ensure any timeouts configured on the resources are removed until 2.0 + for _, v := range resources { + v.Timeouts = nil + } + } + p := &schema.Provider{ Schema: map[string]*schema.Schema{ "subscription_id": { diff --git a/azurerm/resource_arm_resource_group.go b/azurerm/resource_arm_resource_group.go index b72ed31b2aa8..837fcc147bd2 100644 --- a/azurerm/resource_arm_resource_group.go +++ b/azurerm/resource_arm_resource_group.go @@ -8,6 +8,7 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources" "github.com/hashicorp/terraform/helper/schema" @@ -37,7 +38,8 @@ func resourceArmResourceGroup() *schema.Resource { func resourceArmResourceGroupCreateUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient).resource.GroupsClient - ctx := meta.(*ArmClient).StopContext + ctx, cancel := timeouts.ForCreateUpdate(meta.(*ArmClient).StopContext, d) + defer cancel() name := d.Get("name").(string) location := azure.NormalizeLocation(d.Get("location").(string)) @@ -77,7 +79,8 @@ func resourceArmResourceGroupCreateUpdate(d *schema.ResourceData, meta interface func resourceArmResourceGroupRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient).resource.GroupsClient - ctx := meta.(*ArmClient).StopContext + ctx, cancel := timeouts.ForRead(meta.(*ArmClient).StopContext, d) + defer cancel() id, err := azure.ParseAzureResourceID(d.Id()) if err != nil { @@ -106,7 +109,8 @@ func resourceArmResourceGroupRead(d *schema.ResourceData, meta interface{}) erro func resourceArmResourceGroupDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient).resource.GroupsClient - ctx := meta.(*ArmClient).StopContext + ctx, cancel := timeouts.ForDelete(meta.(*ArmClient).StopContext, d) + defer cancel() id, err := azure.ParseAzureResourceID(d.Id()) if err != nil {