From d68cf303c1a55e938c7619d77f032d1271319f0c Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Wed, 20 Dec 2017 17:55:13 -0800 Subject: [PATCH 01/11] Introduce Azure Functions resource azurerm_function_app. --- azurerm/provider.go | 1 + azurerm/resource_arm_function_app.go | 243 ++++++++++++++++++++++++ azurerm/resource_arm_storage_account.go | 20 ++ 3 files changed, 264 insertions(+) create mode 100644 azurerm/resource_arm_function_app.go diff --git a/azurerm/provider.go b/azurerm/provider.go index eea804a46705..7a35ab7d3d55 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -112,6 +112,7 @@ func Provider() terraform.ResourceProvider { "azurerm_eventhub_consumer_group": resourceArmEventHubConsumerGroup(), "azurerm_eventhub_namespace": resourceArmEventHubNamespace(), "azurerm_express_route_circuit": resourceArmExpressRouteCircuit(), + "azurerm_function_app": resourceArmFunctionApp(), "azurerm_image": resourceArmImage(), "azurerm_key_vault": resourceArmKeyVault(), "azurerm_key_vault_certificate": resourceArmKeyVaultCertificate(), diff --git a/azurerm/resource_arm_function_app.go b/azurerm/resource_arm_function_app.go new file mode 100644 index 000000000000..b986f08166d3 --- /dev/null +++ b/azurerm/resource_arm_function_app.go @@ -0,0 +1,243 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/arm/web" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +// Azure Function App shares the same infrastructure with Azure App Service. +// So this resource will reuse most of the App Service code, but remove the configurations which are not applicable for Function App. +func resourceArmFunctionApp() *schema.Resource { + return &schema.Resource{ + Create: resourceArmFunctionAppCreate, + Read: resourceArmFunctionAppRead, + Update: resourceArmFunctionAppUpdate, + Delete: resourceArmFunctionAppDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAppServiceName, + }, + + "resource_group_name": resourceGroupNameSchema(), + + "location": locationSchema(), + + "app_service_plan_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "version": { + Type: schema.TypeString, + Optional: true, + Default: "~1", + ValidateFunc: validation.StringInSlice([]string{ + "~1", + "beta", + }, false), + }, + + "storage_connection_string": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "app_settings": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + }, + + "tags": tagsForceNewSchema(), + + "default_hostname": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceArmFunctionAppCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).appServicesClient + + log.Printf("[INFO] preparing arguments for AzureRM Function App creation.") + + name := d.Get("name").(string) + resGroup := d.Get("resource_group_name").(string) + location := d.Get("location").(string) + kind := "functionapp" + appServicePlanID := d.Get("app_service_plan_id").(string) + enabled := d.Get("enabled").(bool) + storageConnection := d.Get("storage_connection_string").(string) + functionVersion := d.Get("version").(string) + contentShare := name + "-content" + tags := d.Get("tags").(map[string]interface{}) + + dashboardPropName := "AzureWebJobsDashboard" + storagePropName := "AzureWebJobsStorage" + functionVersionPropName := "FUNCTIONS_EXTENSION_VERSION" + contentSharePropName := "WEBSITE_CONTENTSHARE" + contentFileConnStringPropName := "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" + siteEnvelope := web.Site{ + Kind: &kind, + Location: &location, + Tags: expandTags(tags), + SiteProperties: &web.SiteProperties{ + ServerFarmID: utils.String(appServicePlanID), + Enabled: utils.Bool(enabled), + SiteConfig: &web.SiteConfig{ + AppSettings: &[]web.NameValuePair{ + {Name: &dashboardPropName, Value: &storageConnection}, + {Name: &storagePropName, Value: &storageConnection}, + {Name: &functionVersionPropName, Value: &functionVersion}, + {Name: &contentSharePropName, Value: &contentShare}, + {Name: &contentFileConnStringPropName, Value: &storageConnection}, + }, + }, + }, + } + + skipDNSRegistration := false + forceDNSRegistration := false + skipCustomDomainVerification := true + ttlInSeconds := "60" + _, createErr := client.CreateOrUpdate(resGroup, name, siteEnvelope, &skipDNSRegistration, &skipCustomDomainVerification, &forceDNSRegistration, ttlInSeconds, make(chan struct{})) + err := <-createErr + if err != nil { + return err + } + + read, err := client.Get(resGroup, name) + if err != nil { + return err + } + if read.ID == nil { + return fmt.Errorf("Cannot read Function App %s (resource group %s) ID", name, resGroup) + } + + d.SetId(*read.ID) + + return resourceArmFunctionAppUpdate(d, meta) +} + +func resourceArmFunctionAppUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).appServicesClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["sites"] + + if d.HasChange("app_settings") { + appSettings := expandAppServiceAppSettings(d) + settings := web.StringDictionary{ + Properties: appSettings, + } + + _, err := client.UpdateApplicationSettings(resGroup, name, settings) + if err != nil { + return fmt.Errorf("Error updating Application Settings for Function App %q: %+v", name, err) + } + } + + return resourceArmFunctionAppRead(d, meta) +} + +func resourceArmFunctionAppRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).appServicesClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["sites"] + + resp, err := client.Get(resGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Function App %q (resource group %q) was not found - removing from state", name, resGroup) + d.SetId("") + return nil + } + return fmt.Errorf("Error making Read request on AzureRM Function App %q: %+v", name, err) + } + + appSettingsResp, err := client.ListApplicationSettings(resGroup, name) + if err != nil { + return fmt.Errorf("Error making Read request on AzureRM Function App AppSettings %q: %+v", name, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resGroup) + d.Set("location", azureRMNormalizeLocation(*resp.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) + } + + appSettings := flattenAppServiceAppSettings(appSettingsResp.Properties) + if err := d.Set("app_settings", appSettings); err != nil { + return err + } + + d.Set("storage_connection_string", appSettings["AzureWebJobsStorage"]) + d.Set("version", appSettings["FUNCTIONS_EXTENSION_VERSION"]) + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmFunctionAppDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).appServicesClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["sites"] + + log.Printf("[DEBUG] Deleting Function App %q (resource group %q)", name, resGroup) + + deleteMetrics := true + deleteEmptyServerFarm := false + skipDNSRegistration := true + resp, err := client.Delete(resGroup, name, &deleteMetrics, &deleteEmptyServerFarm, &skipDNSRegistration) + if err != nil { + if !utils.ResponseWasNotFound(resp) { + return err + } + } + + return nil +} diff --git a/azurerm/resource_arm_storage_account.go b/azurerm/resource_arm_storage_account.go index d23ca9ed60e2..e0ed5aa534cb 100644 --- a/azurerm/resource_arm_storage_account.go +++ b/azurerm/resource_arm_storage_account.go @@ -198,6 +198,16 @@ func resourceArmStorageAccount() *schema.Resource { Computed: true, }, + "primary_connection_string": { + Type: schema.TypeString, + Computed: true, + }, + + "secondary_connection_string": { + Type: schema.TypeString, + Computed: true, + }, + "primary_blob_connection_string": { Type: schema.TypeString, Computed: true, @@ -526,6 +536,16 @@ func resourceArmStorageAccountRead(d *schema.ResourceData, meta interface{}) err d.Set("primary_location", props.PrimaryLocation) d.Set("secondary_location", props.SecondaryLocation) + if len(accessKeys) > 0 { + pcs := fmt.Sprintf("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net", *resp.Name, *accessKeys[0].Value) + d.Set("primary_connection_string", pcs) + } + + if len(accessKeys) > 1 { + scs := fmt.Sprintf("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net", *resp.Name, *accessKeys[1].Value) + d.Set("secondary_connection_string", scs) + } + if endpoints := props.PrimaryEndpoints; endpoints != nil { d.Set("primary_blob_endpoint", endpoints.Blob) d.Set("primary_queue_endpoint", endpoints.Queue) From 1f25bbe46243dbb29938f91a044b7fd4a5c9345d Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Thu, 21 Dec 2017 16:23:19 -0800 Subject: [PATCH 02/11] Update the documentation to reflect the new changes of function_app and storage_account. --- website/docs/r/function_app.html.markdown | 79 ++++++++++++++++++++ website/docs/r/storage_account.html.markdown | 2 + 2 files changed, 81 insertions(+) create mode 100644 website/docs/r/function_app.html.markdown diff --git a/website/docs/r/function_app.html.markdown b/website/docs/r/function_app.html.markdown new file mode 100644 index 000000000000..d634eeca95c2 --- /dev/null +++ b/website/docs/r/function_app.html.markdown @@ -0,0 +1,79 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_function_app" +sidebar_current: "docs-azurerm-resource-function-app-x" +description: |- + Manages an Azure Functions service. + +--- + +# azurerm_function_app + +Manages an Azure Functions service. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "test" { + name = "azure-functions-test-rg" + location = "westus2" +} + +resource "azurerm_storage_account" "test" { + name = "azure-functions-test-sa" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_app_service_plan" "test" { + name = "azure-functions-test-service-plan" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "test" { + name = "test-azure-functions" + 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}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the Azure Functions service. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which to create the Azure Functions service. + +* `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 Azure Functions service. Changing this forces a new resource to be created. + +* `storage_connection_string` - (Required) The connection string of the backend storage account which will be used by this Azure Functions service (such as the dashboard, logs). + +* `app_settings` - (Optional) A key-value pair of App Settings. + +* `enabled` - (Optional) Is the Azure Function service enabled? Changing this forces a new resource to be created. + +* `version` - (Optional) The runtime version of this Azure Function service. Possible values are `~1` (this is the default value) and `beta`. + +* `tags` - (Optional) A mapping of tags to assign to the resource. Changing this forces a new resource to be created. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Azure Functions service + +* `default_hostname` - The default hostname associated with the Azure Functions service - such as `mysite.azurewebsites.net` diff --git a/website/docs/r/storage_account.html.markdown b/website/docs/r/storage_account.html.markdown index 9098d3510ba5..5cae1a39585a 100644 --- a/website/docs/r/storage_account.html.markdown +++ b/website/docs/r/storage_account.html.markdown @@ -99,6 +99,8 @@ The following attributes are exported in addition to the arguments listed above: * `primary_file_endpoint` - The endpoint URL for file storage in the primary location. * `primary_access_key` - The primary access key for the storage account * `secondary_access_key` - The secondary access key for the storage account +* `primary_connection_string` - The connection string associated with the primary location +* `secondary_connection_string` - The connection string associated with the secondary location * `primary_blob_connection_string` - The connection string associated with the primary blob location * `secondary_blob_connection_string` - The connection string associated with the secondary blob location From f3509b8814644a5b27d2bd40e9c0ed6cfadc5514 Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Thu, 21 Dec 2017 16:58:39 -0800 Subject: [PATCH 03/11] Add test for the new function_app resource. --- azurerm/resource_arm_function_app_test.go | 122 ++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 azurerm/resource_arm_function_app_test.go diff --git a/azurerm/resource_arm_function_app_test.go b/azurerm/resource_arm_function_app_test.go new file mode 100644 index 000000000000..8fdde263ca26 --- /dev/null +++ b/azurerm/resource_arm_function_app_test.go @@ -0,0 +1,122 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMFunctionApp_basic(t *testing.T) { + resourceName := "azurerm_function_app.test" + ri := acctest.RandInt() + config := testAccAzureRMFunctionApp_basic(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(resourceName), + ), + }, + }, + }) +} + +func testCheckAzureRMFunctionAppDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).appServicesClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_function_app" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := client.Get(resourceGroup, name) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return err + } + + return fmt.Errorf("Function App still exists:\n%#v", resp) + } + + return nil +} + +func testCheckAzureRMFunctionAppExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + functionAppName := rs.Primary.Attributes["name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for Function App: %s", functionAppName) + } + + client := testAccProvider.Meta().(*ArmClient).appServicesClient + + resp, err := client.Get(resourceGroup, functionAppName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Function App %q (resource group: %q) does not exist", functionAppName, resourceGroup) + } + + return fmt.Errorf("Bad: Get on appServicesClient: %+v", err) + } + + return nil + } +} + +func testAccAzureRMFunctionApp_basic(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +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_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}" +} +`, rInt, location, rInt%1000000000000, rInt, rInt) +} From d33cff0c4ddd2a4f06ac49ae5dca4f71422d329e Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Tue, 9 Jan 2018 16:13:34 -0800 Subject: [PATCH 04/11] Mitigate the default and user-defined AppSettings update issue. --- azurerm/resource_arm_function_app.go | 70 ++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/azurerm/resource_arm_function_app.go b/azurerm/resource_arm_function_app.go index b986f08166d3..df3e081de0e8 100644 --- a/azurerm/resource_arm_function_app.go +++ b/azurerm/resource_arm_function_app.go @@ -44,6 +44,9 @@ func resourceArmFunctionApp() *schema.Resource { Type: schema.TypeBool, Optional: true, Default: true, + + // TODO: (tombuildsstuff) support Update once the API is fixed: + // https://github.com/Azure/azure-rest-api-specs/issues/1697 ForceNew: true, }, @@ -69,6 +72,8 @@ func resourceArmFunctionApp() *schema.Resource { Computed: true, }, + // TODO: (tombuildsstuff) support Update once the API is fixed: + // https://github.com/Azure/azure-rest-api-specs/issues/1697 "tags": tagsForceNewSchema(), "default_hostname": { @@ -90,16 +95,9 @@ func resourceArmFunctionAppCreate(d *schema.ResourceData, meta interface{}) erro kind := "functionapp" appServicePlanID := d.Get("app_service_plan_id").(string) enabled := d.Get("enabled").(bool) - storageConnection := d.Get("storage_connection_string").(string) - functionVersion := d.Get("version").(string) - contentShare := name + "-content" tags := d.Get("tags").(map[string]interface{}) + basicAppSettings := getBasicFunctionAppAppSettings(d) - dashboardPropName := "AzureWebJobsDashboard" - storagePropName := "AzureWebJobsStorage" - functionVersionPropName := "FUNCTIONS_EXTENSION_VERSION" - contentSharePropName := "WEBSITE_CONTENTSHARE" - contentFileConnStringPropName := "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" siteEnvelope := web.Site{ Kind: &kind, Location: &location, @@ -108,13 +106,7 @@ func resourceArmFunctionAppCreate(d *schema.ResourceData, meta interface{}) erro ServerFarmID: utils.String(appServicePlanID), Enabled: utils.Bool(enabled), SiteConfig: &web.SiteConfig{ - AppSettings: &[]web.NameValuePair{ - {Name: &dashboardPropName, Value: &storageConnection}, - {Name: &storagePropName, Value: &storageConnection}, - {Name: &functionVersionPropName, Value: &functionVersion}, - {Name: &contentSharePropName, Value: &contentShare}, - {Name: &contentFileConnStringPropName, Value: &storageConnection}, - }, + AppSettings: &basicAppSettings, }, }, } @@ -153,8 +145,8 @@ func resourceArmFunctionAppUpdate(d *schema.ResourceData, meta interface{}) erro resGroup := id.ResourceGroup name := id.Path["sites"] - if d.HasChange("app_settings") { - appSettings := expandAppServiceAppSettings(d) + if d.HasChange("app_settings") || d.HasChange("version") { + appSettings := expandFunctionAppAppSettings(d) settings := web.StringDictionary{ Properties: appSettings, } @@ -205,13 +197,20 @@ func resourceArmFunctionAppRead(d *schema.ResourceData, meta interface{}) error } appSettings := flattenAppServiceAppSettings(appSettingsResp.Properties) - if err := d.Set("app_settings", appSettings); err != nil { - return err - } d.Set("storage_connection_string", appSettings["AzureWebJobsStorage"]) d.Set("version", appSettings["FUNCTIONS_EXTENSION_VERSION"]) + 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 + } + flattenAndSetTags(d, resp.Tags) return nil @@ -241,3 +240,34 @@ func resourceArmFunctionAppDelete(d *schema.ResourceData, meta interface{}) erro return nil } + +func getBasicFunctionAppAppSettings(d *schema.ResourceData) []web.NameValuePair { + dashboardPropName := "AzureWebJobsDashboard" + storagePropName := "AzureWebJobsStorage" + functionVersionPropName := "FUNCTIONS_EXTENSION_VERSION" + contentSharePropName := "WEBSITE_CONTENTSHARE" + contentFileConnStringPropName := "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" + + storageConnection := d.Get("storage_connection_string").(string) + functionVersion := d.Get("version").(string) + contentShare := d.Get("name").(string) + "-content" + + return []web.NameValuePair{ + {Name: &dashboardPropName, Value: &storageConnection}, + {Name: &storagePropName, Value: &storageConnection}, + {Name: &functionVersionPropName, Value: &functionVersion}, + {Name: &contentSharePropName, Value: &contentShare}, + {Name: &contentFileConnStringPropName, Value: &storageConnection}, + } +} + +func expandFunctionAppAppSettings(d *schema.ResourceData) *map[string]*string { + output := expandAppServiceAppSettings(d) + + basicAppSettings := getBasicFunctionAppAppSettings(d) + for _, p := range basicAppSettings { + (*output)[*p.Name] = p.Value + } + + return output +} From 8e92273190cda6cdca4a4a947180a2ea8f62f7d4 Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Tue, 9 Jan 2018 16:21:45 -0800 Subject: [PATCH 05/11] Update EndpointSuffix part of the storage primary and secondary connection string. --- azurerm/resource_arm_storage_account.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/azurerm/resource_arm_storage_account.go b/azurerm/resource_arm_storage_account.go index e0ed5aa534cb..78656d0484b6 100644 --- a/azurerm/resource_arm_storage_account.go +++ b/azurerm/resource_arm_storage_account.go @@ -476,6 +476,7 @@ func resourceArmStorageAccountUpdate(d *schema.ResourceData, meta interface{}) e func resourceArmStorageAccountRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient).storageServiceClient + endpointSuffix := meta.(*ArmClient).environment.StorageEndpointSuffix id, err := parseAzureResourceID(d.Id()) if err != nil { @@ -537,12 +538,12 @@ func resourceArmStorageAccountRead(d *schema.ResourceData, meta interface{}) err d.Set("secondary_location", props.SecondaryLocation) if len(accessKeys) > 0 { - pcs := fmt.Sprintf("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net", *resp.Name, *accessKeys[0].Value) + pcs := fmt.Sprintf("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=%s", *resp.Name, *accessKeys[0].Value, endpointSuffix) d.Set("primary_connection_string", pcs) } if len(accessKeys) > 1 { - scs := fmt.Sprintf("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net", *resp.Name, *accessKeys[1].Value) + scs := fmt.Sprintf("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=%s", *resp.Name, *accessKeys[1].Value, endpointSuffix) d.Set("secondary_connection_string", scs) } From 5c38bae1366ef70b45d16f100f3db76e8e5aa96c Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Tue, 9 Jan 2018 17:50:16 -0800 Subject: [PATCH 06/11] Add test of updating Function App version. --- azurerm/resource_arm_function_app_test.go | 84 ++++++++++++++++++++--- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/azurerm/resource_arm_function_app_test.go b/azurerm/resource_arm_function_app_test.go index 8fdde263ca26..b118afde7e97 100644 --- a/azurerm/resource_arm_function_app_test.go +++ b/azurerm/resource_arm_function_app_test.go @@ -2,6 +2,7 @@ package azurerm import ( "fmt" + "strings" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -13,7 +14,8 @@ import ( func TestAccAzureRMFunctionApp_basic(t *testing.T) { resourceName := "azurerm_function_app.test" ri := acctest.RandInt() - config := testAccAzureRMFunctionApp_basic(ri, testLocation()) + rs := strings.ToLower(acctest.RandString(11)) + config := testAccAzureRMFunctionApp_basic(ri, rs, testLocation()) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -24,6 +26,37 @@ func TestAccAzureRMFunctionApp_basic(t *testing.T) { Config: config, Check: resource.ComposeTestCheckFunc( testCheckAzureRMFunctionAppExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "version", "~1"), + ), + }, + }, + }) +} + +func TestAccAzureRMFunctionApp_updateVersion(t *testing.T) { + resourceName := "azurerm_function_app.test" + ri := acctest.RandInt() + rs := strings.ToLower(acctest.RandString(11)) + preConfig := testAccAzureRMFunctionApp_version(ri, rs, testLocation(), "beta") + postConfig := testAccAzureRMFunctionApp_version(ri, rs, testLocation(), "~1") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "version", "beta"), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "version", "~1"), ), }, }, @@ -85,15 +118,15 @@ func testCheckAzureRMFunctionAppExists(name string) resource.TestCheckFunc { } } -func testAccAzureRMFunctionApp_basic(rInt int, location string) string { +func testAccAzureRMFunctionApp_basic(rInt int, storage string, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { - name = "acctestRG-%d" - location = "%s" + name = "acctestRG-%[1]d" + location = "%[2]s" } resource "azurerm_storage_account" "test" { - name = "acctestsa%d" + name = "acctestsa%[3]s" resource_group_name = "${azurerm_resource_group.test.name}" location = "${azurerm_resource_group.test.location}" account_tier = "Standard" @@ -101,10 +134,9 @@ resource "azurerm_storage_account" "test" { } resource "azurerm_app_service_plan" "test" { - name = "acctestASP-%d" + name = "acctestASP-%[1]d" location = "${azurerm_resource_group.test.location}" resource_group_name = "${azurerm_resource_group.test.name}" - sku { tier = "Standard" size = "S1" @@ -112,11 +144,45 @@ resource "azurerm_app_service_plan" "test" { } resource "azurerm_function_app" "test" { - name = "acctestFA-%d" + name = "acctest-%[1]d-func" 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}" +}`, rInt, location, storage) } -`, rInt, location, rInt%1000000000000, rInt, rInt) + +func testAccAzureRMFunctionApp_version(rInt int, storage string, location string, version string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[3]s" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%[1]d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "test" { + name = "acctest-%[1]d-func" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + app_service_plan_id = "${azurerm_app_service_plan.test.id}" + version = "%[4]s" + storage_connection_string = "${azurerm_storage_account.test.primary_connection_string}" +}`, rInt, location, storage, version) } From eb8a625e21e2534021efe6680496719466284c5e Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Tue, 9 Jan 2018 18:04:00 -0800 Subject: [PATCH 07/11] Update Function App documentation according to pull request. --- website/azurerm.erb | 4 ++++ website/docs/r/function_app.html.markdown | 26 ++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/website/azurerm.erb b/website/azurerm.erb index e03f1261ab57..3cdc7f11135c 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -91,6 +91,10 @@ azurerm_app_service_plan + > + azurerm_function_app + + diff --git a/website/docs/r/function_app.html.markdown b/website/docs/r/function_app.html.markdown index d634eeca95c2..de3a1ae332f5 100644 --- a/website/docs/r/function_app.html.markdown +++ b/website/docs/r/function_app.html.markdown @@ -1,17 +1,19 @@ --- layout: "azurerm" page_title: "Azure Resource Manager: azurerm_function_app" -sidebar_current: "docs-azurerm-resource-function-app-x" +sidebar_current: "docs-azurerm-resource-function-app" description: |- - Manages an Azure Functions service. + Manages a Function App. --- # azurerm_function_app -Manages an Azure Functions service. +Manages a Function App. -## Example Usage +> **Note:** Function Apps can be deployed to either an App Service Plan or to a Consumption Plan. At this time it's possible to deploy a Function App into an existing Consumption Plan or a new/existing App Service Plan - however it's not currently possible to create a new Consumption Plan. Support for this will be added in the future, and in the interim can be achieved by using [the `azurerm_template_deployment` resource](template_deployment.html). + +## Example Usage (with App Service Plan) ```hcl resource "azurerm_resource_group" "test" { @@ -20,7 +22,7 @@ resource "azurerm_resource_group" "test" { } resource "azurerm_storage_account" "test" { - name = "azure-functions-test-sa" + name = "functionsapptestsa" resource_group_name = "${azurerm_resource_group.test.name}" location = "${azurerm_resource_group.test.location}" account_tier = "Standard" @@ -51,21 +53,21 @@ resource "azurerm_function_app" "test" { The following arguments are supported: -* `name` - (Required) Specifies the name of the Azure Functions service. Changing this forces a new resource to be created. +* `name` - (Required) Specifies the name of the Function App. Changing this forces a new resource to be created. -* `resource_group_name` - (Required) The name of the resource group in which to create the Azure Functions service. +* `resource_group_name` - (Required) The name of the resource group in which to create the Function App. * `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 Azure Functions service. 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. Changing this forces a new resource to be created. -* `storage_connection_string` - (Required) The connection string of the backend storage account which will be used by this Azure Functions service (such as the dashboard, logs). +* `storage_connection_string` - (Required) The connection string of the backend storage account which will be used by this Function App (such as the dashboard, logs). * `app_settings` - (Optional) A key-value pair of App Settings. * `enabled` - (Optional) Is the Azure Function service enabled? Changing this forces a new resource to be created. -* `version` - (Optional) The runtime version of this Azure Function service. Possible values are `~1` (this is the default value) and `beta`. +* `version` - (Optional) The runtime version associated with the Function App. Possible values are `~1` and `beta`. Defaults to `~1`. * `tags` - (Optional) A mapping of tags to assign to the resource. Changing this forces a new resource to be created. @@ -74,6 +76,6 @@ The following arguments are supported: The following attributes are exported: -* `id` - The ID of the Azure Functions service +* `id` - The ID of the Function App -* `default_hostname` - The default hostname associated with the Azure Functions service - such as `mysite.azurewebsites.net` +* `default_hostname` - The default hostname associated with the Function App - such as `mysite.azurewebsites.net` From ce731b3f6b1cbc961aa3c6bdd01ac4fbbfe861ea Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 11 Jan 2018 14:09:00 +0000 Subject: [PATCH 08/11] Documenting Import support for Function Apps --- website/docs/r/function_app.html.markdown | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/website/docs/r/function_app.html.markdown b/website/docs/r/function_app.html.markdown index de3a1ae332f5..42ce035020cd 100644 --- a/website/docs/r/function_app.html.markdown +++ b/website/docs/r/function_app.html.markdown @@ -11,7 +11,7 @@ description: |- Manages a Function App. -> **Note:** Function Apps can be deployed to either an App Service Plan or to a Consumption Plan. At this time it's possible to deploy a Function App into an existing Consumption Plan or a new/existing App Service Plan - however it's not currently possible to create a new Consumption Plan. Support for this will be added in the future, and in the interim can be achieved by using [the `azurerm_template_deployment` resource](template_deployment.html). +-> **Note:** Function Apps can be deployed to either an App Service Plan or to a Consumption Plan. At this time it's possible to deploy a Function App into an existing Consumption Plan or a new/existing App Service Plan - however it's not currently possible to create a new Consumption Plan. Support for this will be added in the future, and in the interim can be achieved by using [the `azurerm_template_deployment` resource](template_deployment.html). ## Example Usage (with App Service Plan) @@ -65,7 +65,7 @@ The following arguments are supported: * `app_settings` - (Optional) A key-value pair of App Settings. -* `enabled` - (Optional) Is the Azure Function service enabled? Changing this forces a new resource to be created. +* `enabled` - (Optional) Is the Function App enabled? Changing this forces a new resource to be created. * `version` - (Optional) The runtime version associated with the Function App. Possible values are `~1` and `beta`. Defaults to `~1`. @@ -79,3 +79,12 @@ The following attributes are exported: * `id` - The ID of the Function App * `default_hostname` - The default hostname associated with the Function App - such as `mysite.azurewebsites.net` + + +## Import + +Function Apps 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 +``` From a7ccef659a5208f410f621f5e9c353fbf9128922 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 11 Jan 2018 14:09:13 +0000 Subject: [PATCH 09/11] Adding an Import test --- azurerm/import_arm_function_app_test.go | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 azurerm/import_arm_function_app_test.go diff --git a/azurerm/import_arm_function_app_test.go b/azurerm/import_arm_function_app_test.go new file mode 100644 index 000000000000..f2bc39cd91d7 --- /dev/null +++ b/azurerm/import_arm_function_app_test.go @@ -0,0 +1,32 @@ +package azurerm + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAzureRMFunctionApp_importBasic(t *testing.T) { + resourceName := "azurerm_function_app.test" + + ri := acctest.RandInt() + rs := acctest.RandString(5) + config := testAccAzureRMFunctionApp_basic(ri, rs, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} From 8185aa6241bf1eea8a519a0f6fd58412121c296c Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 11 Jan 2018 14:59:30 +0000 Subject: [PATCH 10/11] Making storage connection strings sensitive --- azurerm/resource_arm_function_app.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/azurerm/resource_arm_function_app.go b/azurerm/resource_arm_function_app.go index df3e081de0e8..54505e6dbf2f 100644 --- a/azurerm/resource_arm_function_app.go +++ b/azurerm/resource_arm_function_app.go @@ -61,9 +61,10 @@ func resourceArmFunctionApp() *schema.Resource { }, "storage_connection_string": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Sensitive: true, }, "app_settings": { From afcdaac0d110f3ec7cab0d3ddad6fdc9711bd365 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Thu, 11 Jan 2018 15:31:56 +0000 Subject: [PATCH 11/11] Adding acceptance tests covering app settings + tags --- azurerm/import_arm_function_app_test.go | 48 ++++++++ azurerm/resource_arm_function_app.go | 1 - azurerm/resource_arm_function_app_test.go | 137 ++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/azurerm/import_arm_function_app_test.go b/azurerm/import_arm_function_app_test.go index f2bc39cd91d7..ce743022a33f 100644 --- a/azurerm/import_arm_function_app_test.go +++ b/azurerm/import_arm_function_app_test.go @@ -30,3 +30,51 @@ func TestAccAzureRMFunctionApp_importBasic(t *testing.T) { }, }) } + +func TestAccAzureRMFunctionApp_importTags(t *testing.T) { + resourceName := "azurerm_function_app.test" + + ri := acctest.RandInt() + rs := acctest.RandString(5) + config := testAccAzureRMFunctionApp_tags(ri, rs, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMFunctionApp_importAppSettings(t *testing.T) { + resourceName := "azurerm_function_app.test" + + ri := acctest.RandInt() + rs := acctest.RandString(5) + config := testAccAzureRMFunctionApp_appSettings(ri, rs, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/azurerm/resource_arm_function_app.go b/azurerm/resource_arm_function_app.go index 54505e6dbf2f..d16cc294a9f0 100644 --- a/azurerm/resource_arm_function_app.go +++ b/azurerm/resource_arm_function_app.go @@ -70,7 +70,6 @@ func resourceArmFunctionApp() *schema.Resource { "app_settings": { Type: schema.TypeMap, Optional: true, - Computed: true, }, // TODO: (tombuildsstuff) support Update once the API is fixed: diff --git a/azurerm/resource_arm_function_app_test.go b/azurerm/resource_arm_function_app_test.go index b118afde7e97..751da2c73c3e 100644 --- a/azurerm/resource_arm_function_app_test.go +++ b/azurerm/resource_arm_function_app_test.go @@ -33,6 +33,67 @@ func TestAccAzureRMFunctionApp_basic(t *testing.T) { }) } +func TestAccAzureRMFunctionApp_tags(t *testing.T) { + resourceName := "azurerm_function_app.test" + ri := acctest.RandInt() + rs := strings.ToLower(acctest.RandString(11)) + config := testAccAzureRMFunctionApp_tags(ri, rs, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.environment", "production"), + ), + }, + }, + }) +} + +func TestAccAzureRMFunctionApp_appSettings(t *testing.T) { + resourceName := "azurerm_function_app.test" + ri := acctest.RandInt() + rs := strings.ToLower(acctest.RandString(11)) + config := testAccAzureRMFunctionApp_basic(ri, rs, testLocation()) + updatedConfig := testAccAzureRMFunctionApp_appSettings(ri, rs, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_settings.%", "0"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_settings.%", "1"), + resource.TestCheckResourceAttr(resourceName, "app_settings.hello", "world"), + ), + }, + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_settings.%", "0"), + ), + }, + }, + }) +} + func TestAccAzureRMFunctionApp_updateVersion(t *testing.T) { resourceName := "azurerm_function_app.test" ri := acctest.RandInt() @@ -152,6 +213,44 @@ resource "azurerm_function_app" "test" { }`, rInt, location, storage) } +func testAccAzureRMFunctionApp_tags(rInt int, storage string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[3]s" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%[1]d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "test" { + name = "acctest-%[1]d-func" + 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}" + tags { + environment = "production" + } +} +`, rInt, location, storage) +} + func testAccAzureRMFunctionApp_version(rInt int, storage string, location string, version string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { @@ -186,3 +285,41 @@ resource "azurerm_function_app" "test" { storage_connection_string = "${azurerm_storage_account.test.primary_connection_string}" }`, rInt, location, storage, version) } + +func testAccAzureRMFunctionApp_appSettings(rInt int, rString, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[3]s" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%[1]d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "test" { + name = "acctest-%[1]d-func" + 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}" + app_settings { + "hello" = "world" + } +} +`, rInt, location, rString) +}