diff --git a/azurerm/helpers/azure/app_service_schedule_backup.go b/azurerm/helpers/azure/app_service_schedule_backup.go new file mode 100644 index 000000000000..c18549894cd1 --- /dev/null +++ b/azurerm/helpers/azure/app_service_schedule_backup.go @@ -0,0 +1,115 @@ +package azure + +import ( + "fmt" + "time" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" + + "github.com/Azure/go-autorest/autorest/date" + + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2018-02-01/web" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" +) + +func SchemaAppServiceScheduleBackup() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "frequency_interval": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateFrequencyInterval, + }, + + "frequency_unit": { + Type: schema.TypeString, + Optional: true, + Default: "Day", + ValidateFunc: validation.StringInSlice([]string{ + "Day", + "Hour", + }, false), + }, + + "keep_at_least_one_backup": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "retention_period_in_days": { + Type: schema.TypeInt, + Optional: true, + Default: 30, + ValidateFunc: validateRetentionPeriod, + }, + + "start_time": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppress.RFC3339Time, + ValidateFunc: validate.RFC3339Time, + }, + }, + }, + } +} + +func validateFrequencyInterval(val interface{}, key string) (warns []string, errs []error) { + v := val.(int) + + if v < 0 || v > 1000 { + errs = append(errs, fmt.Errorf("%q must be between 0 and 1000 inclusive, got: %d", key, v)) + } + return +} + +func validateRetentionPeriod(val interface{}, key string) (warns []string, errs []error) { + v := val.(int) + + if v < 0 || v > 9999999 { + errs = append(errs, fmt.Errorf("%q must be between 0 and 9999999 inclusive, got: %d", key, v)) + } + return +} + +func ExpandAppServiceScheduleBackup(input interface{}) web.BackupSchedule { + configs := input.([]interface{}) + backupSchedule := web.BackupSchedule{} + + if len(configs) == 0 { + return backupSchedule + } + + config := configs[0].(map[string]interface{}) + + if v, ok := config["frequency_interval"].(int); ok { + backupSchedule.FrequencyInterval = utils.Int32(int32(v)) + } + + if v, ok := config["frequency_unit"]; ok { + backupSchedule.FrequencyUnit = web.FrequencyUnit(v.(string)) + } + + if v, ok := config["keep_at_least_one_backup"]; ok { + backupSchedule.KeepAtLeastOneBackup = utils.Bool(v.(bool)) + } + + if v, ok := config["retention_period_in_days"].(int); ok { + backupSchedule.RetentionPeriodInDays = utils.Int32(int32(v)) + } + + if v, ok := config["start_time"].(string); ok { + dateTimeToStart, _ := time.Parse(time.RFC3339, v) //validated by schema + backupSchedule.StartTime = &date.Time{Time: (dateTimeToStart)} + } + + return backupSchedule +} diff --git a/azurerm/resource_arm_app_service.go b/azurerm/resource_arm_app_service.go index 7f9839ce9746..515fefe6870b 100644 --- a/azurerm/resource_arm_app_service.go +++ b/azurerm/resource_arm_app_service.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform/helper/validation" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" "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/utils" ) @@ -69,6 +70,21 @@ func resourceArmAppService() *schema.Resource { "site_config": azure.SchemaAppServiceSiteConfig(), + "backup_schedule": azure.SchemaAppServiceScheduleBackup(), + + "storage_account_url": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validate.URLIsHTTPS, + }, + + "backup_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.NoEmptyStrings, + }, + "client_affinity_enabled": { Type: schema.TypeBool, Optional: true, @@ -294,6 +310,9 @@ func resourceArmAppServiceUpdate(d *schema.ResourceData, meta interface{}) error resGroup := id.ResourceGroup name := id.Path["sites"] + storageAccountURL := d.Get("storage_account_url").(string) + backupName := d.Get("backup_name").(string) + location := azureRMNormalizeLocation(d.Get("location").(string)) appServicePlanId := d.Get("app_service_plan_id").(string) enabled := d.Get("enabled").(bool) @@ -338,6 +357,29 @@ func resourceArmAppServiceUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("backup_schedule") { + backupSchedule := azure.ExpandAppServiceScheduleBackup(d.Get("backup_schedule")) + if storageAccountURL != "" { + request := web.BackupRequest{ + BackupRequestProperties: &web.BackupRequestProperties{ + BackupName: utils.String(backupName), + StorageAccountURL: utils.String(storageAccountURL), + Enabled: utils.Bool(true), + BackupSchedule: &backupSchedule, + }, + } + _, err = client.UpdateBackupConfiguration(ctx, resGroup, name, request) + if err != nil { + return err + } + } else { + err = resourceArmDeleteScheduleBackup(d, meta) + if err != nil { + return err + } + } + } + if d.HasChange("client_affinity_enabled") { affinity := d.Get("client_affinity_enabled").(bool) @@ -674,3 +716,23 @@ func flattenAppServiceSiteCredential(input *web.UserProperties) []interface{} { return append(results, result) } + +func resourceArmDeleteScheduleBackup(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).appServicesClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["sites"] + + _, err = client.DeleteBackupConfiguration(ctx, resGroup, name) + if err != nil { + return err + } + + return nil +} diff --git a/examples/app-service/scheduler-backup/README.md b/examples/app-service/scheduler-backup/README.md new file mode 100644 index 000000000000..dba01c6f608e --- /dev/null +++ b/examples/app-service/scheduler-backup/README.md @@ -0,0 +1,3 @@ +# Example: a Basic schedule backup App Service + +This example provisions a schedule backup App Service. diff --git a/examples/app-service/scheduler-backup/main.tf b/examples/app-service/scheduler-backup/main.tf new file mode 100644 index 000000000000..68db6601e276 --- /dev/null +++ b/examples/app-service/scheduler-backup/main.tf @@ -0,0 +1,88 @@ +resource "azurerm_resource_group" "testrg" { + name = "${var.prefix}-resources" + location = "${var.location}" +} + +resource "azurerm_storage_account" "testsa" { + name = "${var.prefix}sa" + resource_group_name = "${azurerm_resource_group.testrg.name}" + location = "${azurerm_resource_group.testrg.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "${var.prefix}-sc" + resource_group_name = "${azurerm_resource_group.testrg.name}" + storage_account_name = "${azurerm_storage_account.testsa.name}" + container_access_type = "private" +} + +resource "azurerm_app_service_plan" "test" { + name = "${var.prefix}-splan" + location = "${azurerm_resource_group.testrg.location}" + resource_group_name = "${azurerm_resource_group.testrg.name}" + + sku { + tier = "Standard" + size = "S1" + } +} + +data "azurerm_storage_account_sas" "test" { + connection_string = "${azurerm_storage_account.testsa.primary_connection_string}" + https_only = true + + resource_types { + service = false + container = false + object = true + } + + services { + blob = true + queue = false + table = false + file = false + } + + start = "2019-03-21" + expiry = "2020-03-21" + + permissions { + read = false + write = true + delete = false + list = false + add = false + create = false + update = false + process = false + } +} + +resource "azurerm_app_service" "test" { + name = "${var.prefix}-appservice" + location = "${azurerm_resource_group.testrg.location}" + resource_group_name = "${azurerm_resource_group.testrg.name}" + app_service_plan_id = "${azurerm_app_service_plan.test.id}" + storage_account_url = "https://${azurerm_storage_account.testsa.name}.blob.core.windows.net/${azurerm_storage_container.test.name}${data.azurerm_storage_account_sas.test.sas}&sr=b" + + backup_schedule { + frequency_interval = "30" +# frequency_unit = "Day" +# keep_at_least_one_backup = false +# retention_period_in_days = "9" +# start_time = "2019-04-29T09:40:00+02:00" + } + + site_config { + dotnet_framework_version = "v4.0" + scm_type = "LocalGit" + } + + app_settings = { + "SOME_KEY" = "some-value" + } + +} diff --git a/examples/app-service/scheduler-backup/output.tf b/examples/app-service/scheduler-backup/output.tf new file mode 100644 index 000000000000..c69d97b414c5 --- /dev/null +++ b/examples/app-service/scheduler-backup/output.tf @@ -0,0 +1,7 @@ +output "app_service_name" { + value = "${azurerm_app_service.test.name}" +} + +output "app_service_default_hostname" { + value = "https://${azurerm_app_service.test.default_site_hostname}" +} \ No newline at end of file diff --git a/examples/app-service/scheduler-backup/variables.tf b/examples/app-service/scheduler-backup/variables.tf new file mode 100644 index 000000000000..9efea09ff138 --- /dev/null +++ b/examples/app-service/scheduler-backup/variables.tf @@ -0,0 +1,7 @@ +variable "prefix" { + description = "The prefix used for all resources in this example" +} + +variable "location" { + description = "The Azure location where all resources in this example should be created" +} \ No newline at end of file diff --git a/website/docs/r/app_service.html.markdown b/website/docs/r/app_service.html.markdown index 57d0e0584e61..aa91ed125984 100644 --- a/website/docs/r/app_service.html.markdown +++ b/website/docs/r/app_service.html.markdown @@ -87,6 +87,10 @@ The following arguments are supported: * `identity` - (Optional) A Managed Service Identity block as defined below. +* `storage_account_url` (Optional) Sets SAS URL to the container where save the backup. + +* `backup_name` (Optional) Sets name of the backup. + --- A `connection_string` block supports the following: @@ -177,6 +181,20 @@ Elements of `ip_restriction` support: * `subnet_mask` - (Optional) The Subnet mask used for this IP Restriction. Defaults to `255.255.255.255`. +--- + +A `backup_schedule` block supports the following: + +* `frequency_interval` - (Required) Sets how often the backup should be executed. + +* `frequency_unit` - (Optional) Sets the unit of time for how often the backup should be executed, possible values: `Day` or `Hour`. Defaults to `Day`. + +* `keep_at_least_one_backup` - (Optional) Sets `true` if the retention policy should always keep at least one backup in the storage account, regardless how old it is; `false` otherwise. Defaults to `true`. + +* `retention_period_in_days` - (Optional) Sets after how many days backups should be deleted. Defaults to `30`. + +* `start_time` - (Optional) Sets when the schedule should start working. + ## Attributes Reference The following attributes are exported: