diff --git a/azurerm/internal/services/web/function_app_slot.go b/azurerm/internal/services/web/function_app_slot.go new file mode 100644 index 0000000000000..c045bba18b1e0 --- /dev/null +++ b/azurerm/internal/services/web/function_app_slot.go @@ -0,0 +1,40 @@ +package web + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type FunctionAppSlotResourceID struct { + ResourceGroup string + FunctionAppName string + Name string +} + +func ParseFunctionAppSlotID(input string) (*FunctionAppSlotResourceID, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse App Service Slot ID %q: %+v", input, err) + } + + slot := FunctionAppSlotResourceID{ + ResourceGroup: id.ResourceGroup, + FunctionAppName: id.Path["sites"], + Name: id.Path["slots"], + } + + if slot.FunctionAppName, err = id.PopSegment("sites"); err != nil { + return nil, err + } + + if slot.Name, err = id.PopSegment("slots"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &slot, nil +} diff --git a/azurerm/internal/services/web/registration.go b/azurerm/internal/services/web/registration.go index 282159c5e36e0..60ae99de46521 100644 --- a/azurerm/internal/services/web/registration.go +++ b/azurerm/internal/services/web/registration.go @@ -44,5 +44,6 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_app_service_virtual_network_swift_connection": resourceArmAppServiceVirtualNetworkSwiftConnection(), "azurerm_app_service": resourceArmAppService(), "azurerm_function_app": resourceArmFunctionApp(), + "azurerm_function_app_slot": resourceArmFunctionAppSlot(), } } diff --git a/azurerm/internal/services/web/resource_arm_function_slot.go b/azurerm/internal/services/web/resource_arm_function_slot.go new file mode 100644 index 0000000000000..3385c2787db03 --- /dev/null +++ b/azurerm/internal/services/web/resource_arm_function_slot.go @@ -0,0 +1,637 @@ +package web + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmFunctionAppSlot() *schema.Resource { + return &schema.Resource{ + Create: resourceArmFunctionAppSlotCreateUpdate, + Read: resourceArmFunctionAppSlotRead, + Update: resourceArmFunctionAppSlotCreateUpdate, + Delete: resourceArmFunctionAppSlotDelete, + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := ParseFunctionAppSlotID(id) + return err + }), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "location": azure.SchemaLocation(), + + "identity": azure.SchemaAppServiceIdentity(), + + "function_app_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "kind": { + Type: schema.TypeString, + Computed: true, + }, + + "app_service_plan_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "version": { + Type: schema.TypeString, + Optional: true, + Default: "~1", + }, + + "storage_connection_string": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Sensitive: true, + }, + + "app_settings": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "enable_builtin_logging": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "client_affinity_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "os_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "linux", + }, false), + }, + + "https_only": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "daily_memory_time_quota": { + Type: schema.TypeInt, + Optional: true, + }, + + "connection_string": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(web.APIHub), + string(web.Custom), + string(web.DocDb), + string(web.EventHub), + string(web.MySQL), + string(web.NotificationHub), + string(web.PostgreSQL), + string(web.RedisCache), + string(web.ServiceBus), + string(web.SQLAzure), + string(web.SQLServer), + }, true), + DiffSuppressFunc: suppress.CaseDifference, + }, + }, + }, + }, + + "default_hostname": { + Type: schema.TypeString, + Computed: true, + }, + + "outbound_ip_addresses": { + Type: schema.TypeString, + Computed: true, + }, + + "possible_outbound_ip_addresses": { + Type: schema.TypeString, + Computed: true, + }, + + "site_config": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "always_on": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "use_32_bit_worker_process": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "websockets_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "linux_fx_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "http2_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "ip_restriction": { + Type: schema.TypeList, + Optional: true, + Computed: true, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip_address": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.CIDR, + }, + "subnet_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + "min_tls_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + string(web.OneFullStopZero), + string(web.OneFullStopOne), + string(web.OneFullStopTwo), + }, false), + }, + "ftps_state": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + string(web.AllAllowed), + string(web.Disabled), + string(web.FtpsOnly), + }, false), + }, + "cors": azure.SchemaWebCorsSettings(), + }, + }, + }, + + "auth_settings": azure.SchemaAppServiceAuthSettings(), + + "site_credential": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Computed: true, + }, + "password": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + }, + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmFunctionAppSlotCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServicesClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + slot := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + functionAppName := d.Get("function_app_name").(string) + + if features.ShouldResourcesBeImported() && d.IsNewResource() { + existing, err := client.GetSlot(ctx, resourceGroup, functionAppName, slot) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing Slot %q (Function App %q / Resource Group %q): %s", slot, functionAppName, resourceGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_function_app_slot", *existing.ID) + } + } + + location := azure.NormalizeLocation(d.Get("location").(string)) + appServicePlanId := d.Get("app_service_plan_id").(string) + enabled := d.Get("enabled").(bool) + clientAffinityEnabled := d.Get("client_affinity_enabled").(bool) + httpsOnly := d.Get("https_only").(bool) + t := d.Get("tags").(map[string]interface{}) + appServiceTier, err := getFunctionAppServiceTier(ctx, appServicePlanId, meta) + if err != nil { + return err + } + + basicAppSettings := getBasicFunctionAppAppSettings(d, appServiceTier) + siteConfig, err := expandFunctionAppSiteConfig(d) + if err != nil { + return fmt.Errorf("Error expanding `site_config` for Function App Slot %q (Resource Group %q): %s", slot, resourceGroup, err) + } + + siteConfig.AppSettings = &basicAppSettings + + siteEnvelope := web.Site{ + Location: &location, + Tags: tags.Expand(t), + SiteProperties: &web.SiteProperties{ + ServerFarmID: utils.String(appServicePlanId), + Enabled: utils.Bool(enabled), + ClientAffinityEnabled: utils.Bool(clientAffinityEnabled), + HTTPSOnly: utils.Bool(httpsOnly), + SiteConfig: &siteConfig, + }, + } + + if _, ok := d.GetOk("identity"); ok { + appServiceIdentityRaw := d.Get("identity").([]interface{}) + appServiceIdentity := azure.ExpandAppServiceIdentity(appServiceIdentityRaw) + siteEnvelope.Identity = appServiceIdentity + } + + createFuture, err := client.CreateOrUpdateSlot(ctx, resourceGroup, functionAppName, siteEnvelope, slot) + if err != nil { + return fmt.Errorf("Error creating Slot %q (Function App %q / Resource Group %q): %s", slot, functionAppName, resourceGroup, err) + } + + err = createFuture.WaitForCompletionRef(ctx, client.Client) + if err != nil { + return fmt.Errorf("Error waiting for creation of Slot %q (Function App %q / Resource Group %q): %s", slot, functionAppName, resourceGroup, err) + } + + read, err := client.GetSlot(ctx, resourceGroup, functionAppName, slot) + if err != nil { + return fmt.Errorf("Error retrieving Slot %q (Function App Slot %q / Resource Group %q): %s", slot, functionAppName, resourceGroup, err) + } + + if read.ID == nil { + return fmt.Errorf("Cannot read ID for Slot %q (Function App Slot %q / Resource Group %q) ID", slot, functionAppName, resourceGroup) + } + + d.SetId(*read.ID) + + authSettingsRaw := d.Get("auth_settings").([]interface{}) + authSettings := azure.ExpandAppServiceAuthSettings(authSettingsRaw) + + auth := web.SiteAuthSettings{ + ID: read.ID, + SiteAuthSettingsProperties: &authSettings} + + if _, err := client.UpdateAuthSettingsSlot(ctx, resourceGroup, functionAppName, auth, slot); err != nil { + return fmt.Errorf("Error updating auth settings for Function App Slot %q (resource group %q): %+s", slot, resourceGroup, err) + } + + return resourceArmFunctionAppSlotUpdate(d, meta) +} + +func resourceArmFunctionAppSlotUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServicesClient + ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := ParseFunctionAppSlotID(d.Id()) + if err != nil { + return err + } + + location := azure.NormalizeLocation(d.Get("location").(string)) + kind := "functionapp" + if osTypeRaw, ok := d.GetOk("os_type"); ok { + osType := osTypeRaw.(string) + if osType == "Linux" { + kind = "functionapp,linux" + } + } + appServicePlanId := d.Get("app_service_plan_id").(string) + enabled := d.Get("enabled").(bool) + clientAffinityEnabled := d.Get("client_affinity_enabled").(bool) + httpsOnly := d.Get("https_only").(bool) + dailyMemoryTimeQuota := d.Get("daily_memory_time_quota").(int) + t := d.Get("tags").(map[string]interface{}) + + appServiceTier, err := getFunctionAppServiceTier(ctx, appServicePlanId, meta) + + if err != nil { + return err + } + basicAppSettings := getBasicFunctionAppAppSettings(d, appServiceTier) + siteConfig, err := expandFunctionAppSiteConfig(d) + if err != nil { + return fmt.Errorf("Error expanding `site_config` for Function App %q (Resource Group %q): %s", id.Name, id.ResourceGroup, err) + } + + siteConfig.AppSettings = &basicAppSettings + + siteEnvelope := web.Site{ + Kind: &kind, + Location: &location, + Tags: tags.Expand(t), + SiteProperties: &web.SiteProperties{ + ServerFarmID: utils.String(appServicePlanId), + Enabled: utils.Bool(enabled), + ClientAffinityEnabled: utils.Bool(clientAffinityEnabled), + HTTPSOnly: utils.Bool(httpsOnly), + DailyMemoryTimeQuota: utils.Int32(int32(dailyMemoryTimeQuota)), + SiteConfig: &siteConfig, + }, + } + + if _, ok := d.GetOk("identity"); ok { + appServiceIdentityRaw := d.Get("identity").([]interface{}) + appServiceIdentity := azure.ExpandAppServiceIdentity(appServiceIdentityRaw) + siteEnvelope.Identity = appServiceIdentity + } + + future, err := client.CreateOrUpdateSlot(ctx, id.ResourceGroup, id.FunctionAppName, siteEnvelope, id.Name) + if err != nil { + return fmt.Errorf("Error updating Slot %q (Function App %q / Resource Group %q): %s", id.Name, id.FunctionAppName, id.ResourceGroup, err) + } + + err = future.WaitForCompletionRef(ctx, client.Client) + if err != nil { + return fmt.Errorf("Error waiting for update of Slot %q (Function App %q / Resource Group %q): %s", id.Name, id.FunctionAppName, id.ResourceGroup, err) + } + + if d.HasChange("site_config") { + siteConfig, err := expandFunctionAppSiteConfig(d) + if err != nil { + return fmt.Errorf("Error expanding `site_config` for Function App %q (Resource Group %q): %s", id.Name, id.ResourceGroup, err) + } + siteConfigResource := web.SiteConfigResource{ + SiteConfig: &siteConfig, + } + if _, err := client.CreateOrUpdateConfiguration(ctx, id.ResourceGroup, id.Name, siteConfigResource); err != nil { + return fmt.Errorf("Error updating Configuration for Function App %q: %+v", id.Name, err) + } + } + + if d.HasChange("auth_settings") { + authSettingsRaw := d.Get("auth_settings").([]interface{}) + authSettingsProperties := azure.ExpandAppServiceAuthSettings(authSettingsRaw) + authSettings := web.SiteAuthSettings{ + ID: utils.String(d.Id()), + SiteAuthSettingsProperties: &authSettingsProperties, + } + + if _, err := client.UpdateAuthSettings(ctx, id.ResourceGroup, id.Name, authSettings); err != nil { + return fmt.Errorf("Error updating Authentication Settings for Function App Slot %q: %+v", id.Name, err) + } + } + + if d.HasChange("connection_string") { + // update the ConnectionStrings + connectionStrings := expandFunctionAppConnectionStrings(d) + properties := web.ConnectionStringDictionary{ + Properties: connectionStrings, + } + + if _, err := client.UpdateConnectionStrings(ctx, id.ResourceGroup, id.Name, properties); err != nil { + return fmt.Errorf("Error updating Connection Strings for App Service %q: %+v", id.Name, err) + } + } + + return resourceArmFunctionAppSlotRead(d, meta) +} + +func resourceArmFunctionAppSlotRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServicesClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := ParseFunctionAppSlotID(d.Id()) + if err != nil { + return err + } + + resp, err := client.GetSlot(ctx, id.ResourceGroup, id.FunctionAppName, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Slot %q (Function App %q / Resource Group %q) were not found - removing from state!", id.Name, id.FunctionAppName, id.ResourceGroup) + d.SetId("") + return nil + } + + return fmt.Errorf("Error reading Slot %q (Function App %q / Resource Group %q): %s", id.Name, id.FunctionAppName, id.ResourceGroup, err) + } + + appSettingsResp, err := client.ListApplicationSettingsSlot(ctx, id.ResourceGroup, id.FunctionAppName, id.Name) + if err != nil { + if utils.ResponseWasNotFound(appSettingsResp.Response) { + log.Printf("[DEBUG] Application Settings of Function App Slot %q (resource group %q) were not found", id.Name, id.ResourceGroup) + d.SetId("") + return nil + } + return fmt.Errorf("Error making Read request on AzureRM Function App Slot AppSettings %q: %+v", id.Name, err) + } + + connectionStringsResp, err := client.ListConnectionStringsSlot(ctx, id.ResourceGroup, id.FunctionAppName, id.Name) + if err != nil { + return fmt.Errorf("Error making Read request on AzureRM Function App Slot ConnectionStrings %q: %+v", id.Name, err) + } + + siteCredFuture, err := client.ListPublishingCredentialsSlot(ctx, id.ResourceGroup, id.FunctionAppName, id.Name) + if err != nil { + return err + } + err = siteCredFuture.WaitForCompletionRef(ctx, client.Client) + if err != nil { + return err + } + siteCredResp, err := siteCredFuture.Result(*client) + if err != nil { + return fmt.Errorf("Error making Read request on AzureRM App Service Site Credential %q: %+v", id.Name, err) + } + authResp, err := client.GetAuthSettingsSlot(ctx, id.ResourceGroup, id.FunctionAppName, id.Name) + if err != nil { + return fmt.Errorf("Error retrieving the AuthSettings for Function App Slot %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + + d.Set("name", id.Name) + d.Set("resource_group_name", id.ResourceGroup) + d.Set("function_app_name", id.FunctionAppName) + d.Set("kind", resp.Kind) + osType := "" + if v := resp.Kind; v != nil && strings.Contains(*v, "linux") { + osType = "linux" + } + d.Set("os_type", osType) + + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if props := resp.SiteProperties; props != nil { + d.Set("app_service_plan_id", props.ServerFarmID) + d.Set("enabled", props.Enabled) + d.Set("default_hostname", props.DefaultHostName) + d.Set("https_only", props.HTTPSOnly) + d.Set("daily_memory_time_quota", props.DailyMemoryTimeQuota) + d.Set("outbound_ip_addresses", props.OutboundIPAddresses) + d.Set("possible_outbound_ip_addresses", props.PossibleOutboundIPAddresses) + d.Set("client_affinity_enabled", props.ClientAffinityEnabled) + } + + appSettings := flattenAppServiceAppSettings(appSettingsResp.Properties) + + d.Set("storage_connection_string", appSettings["AzureWebJobsStorage"]) + d.Set("version", appSettings["FUNCTIONS_EXTENSION_VERSION"]) + + dashboard, ok := appSettings["AzureWebJobsDashboard"] + d.Set("enable_builtin_logging", ok && dashboard != "") + + delete(appSettings, "AzureWebJobsDashboard") + delete(appSettings, "AzureWebJobsStorage") + delete(appSettings, "FUNCTIONS_EXTENSION_VERSION") + delete(appSettings, "WEBSITE_CONTENTSHARE") + delete(appSettings, "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING") + + if err = d.Set("app_settings", appSettings); err != nil { + return err + } + if err = d.Set("connection_string", flattenFunctionAppConnectionStrings(connectionStringsResp.Properties)); err != nil { + return err + } + + identity := azure.FlattenAppServiceIdentity(resp.Identity) + if err := d.Set("identity", identity); err != nil { + return fmt.Errorf("Error setting `identity`: %s", err) + } + + configResp, err := client.GetConfigurationSlot(ctx, id.ResourceGroup, id.FunctionAppName, id.Name) + if err != nil { + return fmt.Errorf("Error making Read request on AzureRM Function App Configuration %q: %+v", id.Name, err) + } + + siteConfig := flattenFunctionAppSiteConfig(configResp.SiteConfig) + if err = d.Set("site_config", siteConfig); err != nil { + return err + } + + authSettings := azure.FlattenAppServiceAuthSettings(authResp.SiteAuthSettingsProperties) + if err := d.Set("auth_settings", authSettings); err != nil { + return fmt.Errorf("Error setting `auth_settings`: %s", err) + } + + siteCred := flattenFunctionAppSiteCredential(siteCredResp.UserProperties) + if err = d.Set("site_credential", siteCred); err != nil { + return err + } + + return tags.FlattenAndSet(d, resp.Tags) +} + +func resourceArmFunctionAppSlotDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServicesClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := ParseFunctionAppSlotID(d.Id()) + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting Slot %q (Function App %q / Resource Group %q)", id.Name, id.FunctionAppName, id.ResourceGroup) + + deleteMetrics := true + deleteEmptyServerFarm := false + resp, err := client.DeleteSlot(ctx, id.ResourceGroup, id.FunctionAppName, id.Name, &deleteMetrics, &deleteEmptyServerFarm) + if err != nil { + if !utils.ResponseWasNotFound(resp) { + return fmt.Errorf("Error deleting Slot %q (Function App %q / Resource Group %q): %s", id.Name, id.FunctionAppName, id.ResourceGroup, err) + } + } + + return nil +} diff --git a/azurerm/internal/services/web/tests/resource_arm_function_app_slot_test.go b/azurerm/internal/services/web/tests/resource_arm_function_app_slot_test.go new file mode 100644 index 0000000000000..d31fc98b379f7 --- /dev/null +++ b/azurerm/internal/services/web/tests/resource_arm_function_app_slot_test.go @@ -0,0 +1,314 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMFunctionAppSlot_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_slot", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMFunctionAppSlotDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMFunctionAppSlot_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppSlotExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMFunctionAppSlot_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + data := acceptance.BuildTestData(t, "azurerm_function_app_slot", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMFunctionAppSlotDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMFunctionAppSlot_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppSlotExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMFunctionAppSlot_requiresImport), + }, + }) +} + +func TestAccAzureRMFunctionAppSlot_tagsUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_slot", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMFunctionAppSlotDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMFunctionAppSlot_tags(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppSlotExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.Hello", "World"), + ), + }, + { + Config: testAccAzureRMFunctionAppSlot_tagsUpdated(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppSlotExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.Hello", "World"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.Terraform", "AcceptanceTests"), + ), + }, + }, + }) +} + +func testCheckAzureRMFunctionAppSlotDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Web.AppServicesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_function_app_slot" { + continue + } + + slot := rs.Primary.Attributes["name"] + FunctionAppName := rs.Primary.Attributes["function_app_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := client.GetSlot(ctx, resourceGroup, FunctionAppName, slot) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return err + } + + return nil + } + + return nil +} + +func testCheckAzureRMFunctionAppSlotExists(slot string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Web.AppServicesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[slot] + if !ok { + return fmt.Errorf("Slot Not found: %q", slot) + } + + FunctionAppName := rs.Primary.Attributes["function_app_name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for Function App Slot: %q/%q", FunctionAppName, slot) + } + + resp, err := client.GetSlot(ctx, resourceGroup, FunctionAppName, slot) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Function App slot %q/%q (resource group: %q) does not exist", FunctionAppName, slot, resourceGroup) + } + + return fmt.Errorf("Bad: Get on AppServicesClient: %+v", err) + } + + return nil + } +} + +func testAccAzureRMFunctionAppSlot_basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_function_app" "test" { + name = "acctestFA-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + storage_connection_string = azurerm_storage_account.test.primary_connection_string +} + +resource "azurerm_function_app_slot" "test" { + name = "acctestFASlot-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + function_app_name = azurerm_function_app.test.name + storage_connection_string = azurerm_storage_account.test.primary_connection_string +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomString, data.RandomInteger, data.RandomInteger) +} + +func testAccAzureRMFunctionAppSlot_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMFunctionAppSlot_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_function_app_slot" "import" { + name = azurerm_function_app_slot.test.name + location = azurerm_function_app_slot.test.location + resource_group_name = azurerm_function_app_slot.test.resource_group_name + app_service_plan_id = azurerm_function_app_slot.test.app_service_plan_id + function_app_name = azurerm_function_app_slot.test.function_app_name + storage_connection_string = azurerm_storage_account.test.primary_connection_string +} +`, template) +} + +func testAccAzureRMFunctionAppSlot_tags(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_function_app" "test" { + name = "acctestFA-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + storage_connection_string = azurerm_storage_account.test.primary_connection_string +} + +resource "azurerm_function_app_slot" "test" { + name = "acctestFASlot-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + function_app_name = azurerm_function_app.test.name + storage_connection_string = azurerm_storage_account.test.primary_connection_string + + tags = { + Hello = "World" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomString, data.RandomInteger, data.RandomInteger) +} + +func testAccAzureRMFunctionAppSlot_tagsUpdated(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_function_app" "test" { + name = "acctestFA-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + storage_connection_string = azurerm_storage_account.test.primary_connection_string +} + +resource "azurerm_function_app_slot" "test" { + name = "acctestFASlot-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + function_app_name = azurerm_function_app.test.name + storage_connection_string = azurerm_storage_account.test.primary_connection_string + + tags = { + "Hello" = "World" + "Terraform" = "AcceptanceTests" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomString, data.RandomInteger, data.RandomInteger) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 0145ba90d2d50..fcc3038e9d21a 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -762,6 +762,10 @@
  • azurerm_function_app
  • + +
  • + azurerm_function_app_slot +
  • diff --git a/website/docs/r/function_app_slot.html.markdown b/website/docs/r/function_app_slot.html.markdown new file mode 100644 index 0000000000000..1a6cb6d80de1a --- /dev/null +++ b/website/docs/r/function_app_slot.html.markdown @@ -0,0 +1,97 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_function_app_slot" +description: |- + Manages a Function App Deployment Slot. + +--- + +# azurerm_function_app_slot + +Manages a Function App deployment Slot. + +## Example Usage (with App Service Plan) + +```hcl +resource "azurerm_resource_group" "example" { + name = "azure-functions-test-rg" + location = "westus2" +} + +resource "azurerm_storage_account" "example" { + name = "functionsapptestsa" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_app_service_plan" "example" { + name = "azure-functions-test-service-plan" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "example" { + name = "test-azure-functions" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + app_service_plan_id = azurerm_app_service_plan.example.id + storage_connection_string = azurerm_storage_account.example.primary_connection_string +} + +resource "azurerm_function_app_slot" "example" { + name = "test-azure-functions_slot" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + app_service_plan_id = azurerm_app_service_plan.example.id + function_app_name = azurerm_function_app.example.name +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the Function App deployment Slot. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which to create the Function App Deployment Slot. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +* `app_service_plan_id` - (Required) The ID of the App Service Plan within which to create this Function App Deployment Slot. + +* `function_app_name` - (Required) The name of the Function App to create a Deployment Slot for. + +* `tags` - (Optional) A mapping of tags to assign to the resource. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Function App + + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Function App Deployment Slot. +* `update` - (Defaults to 30 minutes) Used when updating the Function App Deployment Slot. +* `read` - (Defaults to 5 minutes) Used when retrieving the Function App Deployment Slot. +* `delete` - (Defaults to 30 minutes) Used when deleting the Function App Deployment Slot. + +## Import + +Function Apps Deployment Slots can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_function_app.functionapp1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Web/sites/functionapp1/slots/staging +```