diff --git a/azurerm/import_arm_function_app_test.go b/azurerm/import_arm_function_app_test.go new file mode 100644 index 000000000000..ce743022a33f --- /dev/null +++ b/azurerm/import_arm_function_app_test.go @@ -0,0 +1,80 @@ +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, + }, + }, + }) +} + +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/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..d16cc294a9f0 --- /dev/null +++ b/azurerm/resource_arm_function_app.go @@ -0,0 +1,273 @@ +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, + + // TODO: (tombuildsstuff) support Update once the API is fixed: + // https://github.com/Azure/azure-rest-api-specs/issues/1697 + 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, + Sensitive: true, + }, + + "app_settings": { + Type: schema.TypeMap, + Optional: true, + }, + + // TODO: (tombuildsstuff) support Update once the API is fixed: + // https://github.com/Azure/azure-rest-api-specs/issues/1697 + "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) + tags := d.Get("tags").(map[string]interface{}) + basicAppSettings := getBasicFunctionAppAppSettings(d) + + 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: &basicAppSettings, + }, + }, + } + + 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") || d.HasChange("version") { + appSettings := expandFunctionAppAppSettings(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) + + 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 +} + +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 +} + +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 +} diff --git a/azurerm/resource_arm_function_app_test.go b/azurerm/resource_arm_function_app_test.go new file mode 100644 index 000000000000..751da2c73c3e --- /dev/null +++ b/azurerm/resource_arm_function_app_test.go @@ -0,0 +1,325 @@ +package azurerm + +import ( + "fmt" + "strings" + "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() + rs := strings.ToLower(acctest.RandString(11)) + config := testAccAzureRMFunctionApp_basic(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, "version", "~1"), + ), + }, + }, + }) +} + +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() + 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"), + ), + }, + }, + }) +} + +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, 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}" +}`, 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" { + 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) +} + +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) +} diff --git a/azurerm/resource_arm_storage_account.go b/azurerm/resource_arm_storage_account.go index d23ca9ed60e2..78656d0484b6 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, @@ -466,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 { @@ -526,6 +537,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=%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=%s", *resp.Name, *accessKeys[1].Value, endpointSuffix) + 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) 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 +