Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App Service: Add schedule backup functionality #3330

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions azurerm/helpers/azure/app_service_schedule_backup.go
Original file line number Diff line number Diff line change
@@ -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",
tombuildsstuff marked this conversation as resolved.
Show resolved Hide resolved
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
}
62 changes: 62 additions & 0 deletions azurerm/resource_arm_app_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we move all of these into a backup block?


"client_affinity_enabled": {
Type: schema.TypeBool,
Optional: true,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -338,6 +357,29 @@ func resourceArmAppServiceUpdate(d *schema.ResourceData, meta interface{}) error
}
}

if d.HasChange("backup_schedule") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to run into a perpetual Terraform state difference here if a user changes backup_name or storage_account_url without changing the backup_schedule since those wouldn't get updated unless backup_schedule has been changed. My above comment on wrapping everything into a backup TypeList should fix this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this - could we make this change?

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
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we also read these values back into the State in the Read function (the Read function contains some examples of this)? this allows diff's to be detected

if d.HasChange("client_affinity_enabled") {

affinity := d.Get("client_affinity_enabled").(bool)
Expand Down Expand Up @@ -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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than calling this method - could we switch to calling these 4 lined directly above?


return nil
}
3 changes: 3 additions & 0 deletions examples/app-service/scheduler-backup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Example: a Basic schedule backup App Service
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we make this:

Suggested change
# Example: a Basic schedule backup App Service
# Example: an App Service with a Scheduled Backup


This example provisions a schedule backup App Service.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we make this:

Suggested change
This example provisions a schedule backup App Service.
This example provisions an App Service with a Backup configured.

88 changes: 88 additions & 0 deletions examples/app-service/scheduler-backup/main.tf
Original file line number Diff line number Diff line change
@@ -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"
}

}
7 changes: 7 additions & 0 deletions examples/app-service/scheduler-backup/output.tf
Original file line number Diff line number Diff line change
@@ -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}"
}
7 changes: 7 additions & 0 deletions examples/app-service/scheduler-backup/variables.tf
Original file line number Diff line number Diff line change
@@ -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"
}
18 changes: 18 additions & 0 deletions website/docs/r/app_service.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down