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 10 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
89 changes: 89 additions & 0 deletions azurerm/helpers/azure/app_service_schedule_backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package azure

import (
"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/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,
Copy link
Member

Choose a reason for hiding this comment

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

Is there any validation we can add here? Like greater than 0 and Less than some number?

},

"frequency_unit": {
Type: schema.TypeString,
Optional: true,
Default: "Day",
tombuildsstuff marked this conversation as resolved.
Show resolved Hide resolved
},

"keep_at_least_one_backup": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},

"retention_period_in_days": {
Type: schema.TypeInt,
Optional: true,
Default: 30,
Copy link
Member

Choose a reason for hiding this comment

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

Thoughts around validation here too?

},

"start_time": {
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: suppress.RFC3339Time,
ValidateFunc: validate.RFC3339Time,
},
},
},
}
}

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
}
78 changes: 78 additions & 0 deletions azurerm/resource_arm_app_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ func resourceArmAppService() *schema.Resource {

"site_config": azure.SchemaAppServiceSiteConfig(),

"backup_schedule": azure.SchemaAppServiceScheduleBackup(),

"storage_account_url": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
namku marked this conversation as resolved.
Show resolved Hide resolved
},

"backup_name": {
Type: schema.TypeString,
Optional: true,
namku marked this conversation as resolved.
Show resolved Hide resolved
},
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 @@ -199,6 +212,9 @@ func resourceArmAppServiceCreate(d *schema.ResourceData, meta interface{}) error
name := d.Get("name").(string)
resGroup := d.Get("resource_group_name").(string)

storageAccountURL := d.Get("storage_account_url").(string)
backupName := d.Get("backup_name").(string)

if requireResourcesToBeImported && d.IsNewResource() {
existing, err := client.Get(ctx, resGroup, name)
if err != nil {
Expand Down Expand Up @@ -269,6 +285,22 @@ func resourceArmAppServiceCreate(d *schema.ResourceData, meta interface{}) error
return err
}

backupSchedule := azure.ExpandAppServiceScheduleBackup(d.Get("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.

I don't believe we need this in the create method because this will be covered by the update that we go into immediately after creating.

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

read, err := client.Get(ctx, resGroup, name)
if err != nil {
return err
Expand All @@ -294,6 +326,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 +373,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 +732,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