diff --git a/azurerm/config.go b/azurerm/config.go index d6eef74e042f..08114b0bf71c 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -125,12 +125,11 @@ type ArmClient struct { sqlServersClient sql.ServersClient appServicePlansClient web.AppServicePlansClient + appsClient web.AppsClient appInsightsClient appinsights.ComponentsClient servicePrincipalsClient graphrbac.ServicePrincipalsClient - - appsClient web.AppsClient } func withRequestLogging() autorest.SendDecorator { @@ -575,6 +574,12 @@ func (c *Config) getArmClient() (*ArmClient, error) { aspc.Sender = autorest.CreateSender(withRequestLogging()) client.appServicePlansClient = aspc + ac := web.NewAppsClientWithBaseURI(endpoint, c.SubscriptionID) + setUserAgent(&ac.Client) + ac.Authorizer = auth + ac.Sender = autorest.CreateSender(withRequestLogging()) + client.appsClient = ac + ai := appinsights.NewComponentsClientWithBaseURI(endpoint, c.SubscriptionID) setUserAgent(&ai.Client) ai.Authorizer = auth @@ -587,12 +592,6 @@ func (c *Config) getArmClient() (*ArmClient, error) { spc.Sender = autorest.CreateSender(withRequestLogging()) client.servicePrincipalsClient = spc - ac := web.NewAppsClientWithBaseURI(endpoint, c.SubscriptionID) - setUserAgent(&ac.Client) - ac.Authorizer = auth - ac.Sender = autorest.CreateSender(withRequestLogging()) - client.appsClient = ac - kvc := keyvault.NewVaultsClientWithBaseURI(endpoint, c.SubscriptionID) setUserAgent(&kvc.Client) kvc.Authorizer = auth diff --git a/azurerm/import_arm_app_service_test.go b/azurerm/import_arm_app_service_test.go new file mode 100644 index 000000000000..c37f32ec309d --- /dev/null +++ b/azurerm/import_arm_app_service_test.go @@ -0,0 +1,83 @@ +package azurerm + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAzureRMAppService_importBasic(t *testing.T) { + resourceName := "azurerm_app_service.test" + + ri := acctest.RandInt() + config := testAccAzureRMAppService_basic(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: config, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_dns_registration", "skip_custom_domain_verification", "skip_dns_registration", "delete_metrics"}, + }, + }, + }) +} + +func TestAccAzureRMAppService_importComplete(t *testing.T) { + resourceName := "azurerm_app_service.test" + + ri := acctest.RandInt() + config := testAccAzureRMAppService_complete(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: config, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_dns_registration", "skip_custom_domain_verification", "skip_dns_registration", "delete_metrics"}, + }, + }, + }) +} + +func TestAccAzureRMAppService_importCompleteAlwaysOn(t *testing.T) { + resourceName := "azurerm_app_service.test" + + ri := acctest.RandInt() + config := testAccAzureRMAppService_completeAlwaysOn(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: config, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_dns_registration", "skip_custom_domain_verification", "skip_dns_registration", "delete_metrics"}, + }, + }, + }) +} diff --git a/azurerm/provider.go b/azurerm/provider.go index 3fa362c062a0..2e85464a7691 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -69,6 +69,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "azurerm_application_insights": resourceArmApplicationInsights(), "azurerm_app_service_plan": resourceArmAppServicePlan(), + "azurerm_app_service": resourceArmAppService(), "azurerm_availability_set": resourceArmAvailabilitySet(), "azurerm_cdn_endpoint": resourceArmCdnEndpoint(), "azurerm_cdn_profile": resourceArmCdnProfile(), diff --git a/azurerm/resource_arm_app_service.go b/azurerm/resource_arm_app_service.go new file mode 100644 index 000000000000..13870ef2fcf4 --- /dev/null +++ b/azurerm/resource_arm_app_service.go @@ -0,0 +1,223 @@ +package azurerm + +import ( + "fmt" + "log" + "net/http" + "strconv" + + "github.com/Azure/azure-sdk-for-go/arm/web" + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmAppService() *schema.Resource { + return &schema.Resource{ + Create: resourceArmAppServiceCreateUpdate, + Read: resourceArmAppServiceRead, + Update: resourceArmAppServiceCreateUpdate, + Delete: resourceArmAppServiceDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "resource_group_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "skip_dns_registration": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "skip_custom_domain_verification": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "force_dns_registration": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "ttl_in_seconds": { + Type: schema.TypeInt, + Optional: true, + }, + "site_config": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "app_service_plan_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "always_on": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + }, + }, + "location": locationSchema(), + "tags": tagsSchema(), + "delete_metrics": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func resourceArmAppServiceCreateUpdate(d *schema.ResourceData, meta interface{}) error { + appClient := meta.(*ArmClient).appsClient + + log.Printf("[INFO] preparing arguments for Azure ARM App Service creation.") + + resGroup := d.Get("resource_group_name").(string) + name := d.Get("name").(string) + skipDNSRegistration := d.Get("skip_dns_registration").(bool) + skipCustomDomainVerification := d.Get("skip_custom_domain_verification").(bool) + forceDNSRegistration := d.Get("force_dns_registration").(bool) + ttlInSeconds := 0 + if v, ok := d.GetOk("ttl_in_seconds"); ok { + ttlInSeconds = v.(int) + } + location := d.Get("location").(string) + tags := d.Get("tags").(map[string]interface{}) + + siteProps := expandAzureRmAppServiceSiteProps(d) + + siteEnvelope := web.Site{ + Location: &location, + Tags: expandTags(tags), + SiteProperties: siteProps, + } + + _, error := appClient.CreateOrUpdate(resGroup, name, siteEnvelope, &skipDNSRegistration, &skipCustomDomainVerification, &forceDNSRegistration, strconv.Itoa(ttlInSeconds), make(chan struct{})) + err := <-error + if err != nil { + return err + } + + read, err := appClient.Get(resGroup, name) + if err != nil { + return err + } + if read.ID == nil { + return fmt.Errorf("Cannot read App Service %s (resource group %s) ID", name, resGroup) + } + + d.SetId(*read.ID) + + return resourceArmAppServiceRead(d, meta) +} + +func resourceArmAppServiceRead(d *schema.ResourceData, meta interface{}) error { + appClient := meta.(*ArmClient).appsClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + log.Printf("[DEBUG] Reading App Service details %s", id) + + resGroup := id.ResourceGroup + name := id.Path["sites"] + + resp, err := appClient.Get(resGroup, name) + if err != nil { + if resp.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + return fmt.Errorf("Error making Read request on AzureRM App Service %s: %+v", name, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resGroup) + d.Set("location", azureRMNormalizeLocation(*resp.Location)) + + if siteProps := resp.SiteProperties; siteProps != nil { + d.Set("site_config", flattenAzureRmAppServiceSiteProps(siteProps)) + } + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmAppServiceDelete(d *schema.ResourceData, meta interface{}) error { + appClient := meta.(*ArmClient).appsClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["sites"] + + log.Printf("[DEBUG] Deleting App Service %s: %s", resGroup, name) + + deleteMetrics := d.Get("delete_metrics").(bool) + deleteEmptyServerFarm := true + skipDNSRegistration := d.Get("skip_dns_registration").(bool) + + _, err = appClient.Delete(resGroup, name, &deleteMetrics, &deleteEmptyServerFarm, &skipDNSRegistration) + + return err +} + +func expandAzureRmAppServiceSiteProps(d *schema.ResourceData) *web.SiteProperties { + configs := d.Get("site_config").([]interface{}) + siteProps := web.SiteProperties{} + if len(configs) == 0 { + return &siteProps + } + config := configs[0].(map[string]interface{}) + + siteConfig := web.SiteConfig{} + alwaysOn := config["always_on"].(bool) + siteConfig.AlwaysOn = utils.Bool(alwaysOn) + + siteProps.SiteConfig = &siteConfig + + serverFarmID := config["app_service_plan_id"].(string) + siteProps.ServerFarmID = &serverFarmID + + return &siteProps +} + +func flattenAzureRmAppServiceSiteProps(siteProps *web.SiteProperties) []interface{} { + result := make([]interface{}, 0, 1) + site_config := make(map[string]interface{}, 0) + + if siteProps.ServerFarmID != nil { + site_config["app_service_plan_id"] = *siteProps.ServerFarmID + } + + siteConfig := siteProps.SiteConfig + log.Printf("[DEBUG] SiteConfig is %s", siteConfig) + if siteConfig != nil { + if siteConfig.AlwaysOn != nil { + site_config["always_on"] = *siteConfig.AlwaysOn + } + } + + result = append(result, site_config) + return result +} diff --git a/azurerm/resource_arm_app_service_test.go b/azurerm/resource_arm_app_service_test.go new file mode 100644 index 000000000000..360ff470f469 --- /dev/null +++ b/azurerm/resource_arm_app_service_test.go @@ -0,0 +1,204 @@ +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 TestAccAzureRMAppService_basic(t *testing.T) { + ri := acctest.RandInt() + config := testAccAzureRMAppService_basic(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceExists("azurerm_app_service.test"), + ), + }, + }, + }) +} + +func TestAccAzureRMAppService_complete(t *testing.T) { + ri := acctest.RandInt() + config := testAccAzureRMAppService_complete(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceExists("azurerm_app_service.test"), + ), + }, + }, + }) +} + +func TestAccAzureRMAppService_completeAlwaysOn(t *testing.T) { + ri := acctest.RandInt() + config := testAccAzureRMAppService_completeAlwaysOn(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceExists("azurerm_app_service.test"), + ), + }, + }, + }) +} + +func testCheckAzureRMAppServiceDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).appsClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_app_service" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(resourceGroup, name) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return err + } + + return fmt.Errorf("App Service still exists:\n%#v", resp) + } + + return nil +} + +func testCheckAzureRMAppServiceExists(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) + } + + appServiceName := 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 App Service: %s", appServiceName) + } + + conn := testAccProvider.Meta().(*ArmClient).appsClient + + resp, err := conn.Get(resourceGroup, appServiceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: App Service %q (resource group: %q) does not exist", appServiceName, resourceGroup) + } + + return fmt.Errorf("Bad: Get on appsClient: %+v", err) + } + + return nil + } +} + +func testAccAzureRMAppService_basic(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_app_service" "test" { + name = "acctestAS-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + tags { + environment = "Production" + } +} +`, rInt, location, rInt) +} + +func testAccAzureRMAppService_complete(rInt int, location string) string { + return fmt.Sprintf(` +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_app_service" "test" { + name = "acctestAS-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + site_config { + app_service_plan_id = "${azurerm_app_service_plan.test.id}" + always_on = false + } +} +`, rInt, location, rInt, rInt) +} + +func testAccAzureRMAppService_completeAlwaysOn(rInt int, location string) string { + return fmt.Sprintf(` +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_app_service" "test" { + name = "acctestAS-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + site_config { + app_service_plan_id = "${azurerm_app_service_plan.test.id}" + always_on = true + } +} +`, rInt, location, rInt, rInt) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 0c5f79ef7612..6b03ad8df7f3 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -54,6 +54,10 @@ azurerm_app_service_plan + > + azurerm_app_service + + diff --git a/website/docs/r/app_service.html.markdown b/website/docs/r/app_service.html.markdown new file mode 100644 index 000000000000..e2c53cc1dad4 --- /dev/null +++ b/website/docs/r/app_service.html.markdown @@ -0,0 +1,66 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_app_service" +sidebar_current: "docs-azurerm-resource-app-service" +description: |- + Manage an App Service (within an App Service Plan). +--- + +# azurerm\_app\_service + +Manage an App Service (within an App Service Plan). + +## Example Usage + +```hcl +resource "azurerm_resource_group" "test" { + name = "api-rg-pro" + location = "West Europe" +} + +resource "azurerm_app_service" "test" { + name = "api-appservice-pro" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the App Service Plan component. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which to create the App Service Plan component. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +`site_config` supports the following: + +* `app_service_plan_id` - (Optional) The resource ID of the app service plan. + +* `always_on` - (Optional) Alows the app to be loaded all the time. + +* `skip_dns_registration` - (Optional) If true, DNS registration is skipped. + +* `skip_custom_domain_verification` - (Optional) If true, custom (non .azurewebsites.net) domains associated with web app are not verified. + +* `force_dns_registration` - (Optional) If true, web app hostname is force registered with DNS. + +* `ttl_in_seconds` - (Optional) Time to live in seconds for app service's default domain name. + +* `tags` - (Optional) A mapping of tags to assign to the resource. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the App Service component. + +## Import + +App Service instances can be imported using the `resource id`, e.g. + +``` +terraform import azurerm_app_service.instance1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Web/sites/instance1 +```