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

azurerm_automation_job_schedule: update jobSchedule resource id format #22164

Merged
merged 11 commits into from
Jun 14, 2024
154 changes: 98 additions & 56 deletions internal/services/automation/automation_job_schedule_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@
package automation

import (
"context"
"fmt"
"log"
"strings"
"time"

"github.com/gofrs/uuid"
"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema"
"github.com/hashicorp/go-azure-sdk/resource-manager/automation/2023-11-01/jobschedule"
"github.com/hashicorp/go-azure-sdk/resource-manager/automation/2023-11-01/runbook"
"github.com/hashicorp/go-azure-sdk/resource-manager/automation/2023-11-01/schedule"
"github.com/hashicorp/terraform-provider-azurerm/helpers/tf"
"github.com/hashicorp/terraform-provider-azurerm/internal/clients"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/automation/migration"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/automation/validate"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation"
Expand All @@ -28,7 +34,7 @@ func resourceAutomationJobSchedule() *pluginsdk.Resource {
Delete: resourceAutomationJobScheduleDelete,

Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error {
_, err := jobschedule.ParseJobScheduleID(id)
_, err := commonids.ParseCompositeResourceID(id, &schedule.ScheduleId{}, &runbook.RunbookId{})
return err
}),

Expand All @@ -38,6 +44,11 @@ func resourceAutomationJobSchedule() *pluginsdk.Resource {
Delete: pluginsdk.DefaultTimeout(30 * time.Minute),
},

SchemaVersion: 1,
StateUpgraders: pluginsdk.StateUpgrades(map[int]pluginsdk.StateUpgrade{
0: migration.AutomationJobScheduleV0ToV1{},
}),

Schema: map[string]*pluginsdk.Schema{
"resource_group_name": commonschema.ResourceGroupName(),

Expand Down Expand Up @@ -84,6 +95,11 @@ func resourceAutomationJobSchedule() *pluginsdk.Resource {
Computed: true,
ValidateFunc: validation.IsUUID,
},

"resource_manager_id": {
Type: pluginsdk.TypeString,
Computed: true,
},
},
}
}
Expand All @@ -96,6 +112,8 @@ func resourceAutomationJobScheduleCreate(d *pluginsdk.ResourceData, meta interfa

log.Printf("[INFO] preparing arguments for AzureRM Automation Job Schedule creation.")

resourceGroup := d.Get("resource_group_name").(string)
accountName := d.Get("automation_account_name").(string)
runbookName := d.Get("runbook_name").(string)
scheduleName := d.Get("schedule_name").(string)

Expand All @@ -107,43 +125,23 @@ func resourceAutomationJobScheduleCreate(d *pluginsdk.ResourceData, meta interfa
jobScheduleUUID = uuid.FromStringOrNil(jobScheduleID.(string))
}

id := jobschedule.NewJobScheduleID(subscriptionId, d.Get("resource_group_name").(string), d.Get("automation_account_name").(string), jobScheduleUUID.String())
scheduleID := schedule.NewScheduleID(subscriptionId, resourceGroup, accountName, scheduleName)
runbookID := runbook.NewRunbookID(subscriptionId, resourceGroup, accountName, runbookName)
id := jobschedule.NewJobScheduleID(subscriptionId, resourceGroup, accountName, jobScheduleUUID.String())

tfID := &commonids.CompositeResourceID[*schedule.ScheduleId, *runbook.RunbookId]{
First: &scheduleID,
Second: &runbookID,
}

if d.IsNewResource() {
existing, err := client.Get(ctx, id)
existing, err := GetJobScheduleFromTFID(ctx, client, tfID)
if err != nil {
if !response.WasNotFound(existing.HttpResponse) {
return fmt.Errorf("checking for presence of existing %s: %s", id, err)
}
return fmt.Errorf("checking for presence of existing %s: %s", id, err)
}

if !response.WasNotFound(existing.HttpResponse) {
return tf.ImportAsExistsError("azurerm_automation_job_schedule", id.ID())
}
}

automationAccountId := jobschedule.NewAutomationAccountID(subscriptionId, id.ResourceGroupName, id.AutomationAccountName)

// fix issue: https://github.com/hashicorp/terraform-provider-azurerm/issues/7130
// When the runbook has some updates, it'll update all related job schedule id, so the elder job schedule will not exist
// We need to delete the job schedule id if exists to recreate the job schedule
jsIterator, err := client.ListByAutomationAccountComplete(ctx, automationAccountId, jobschedule.ListByAutomationAccountOperationOptions{})
if err != nil {
return fmt.Errorf("loading Automation Account %q Job Schedule List: %+v", id.AutomationAccountName, err)
}

for _, item := range jsIterator.Items {
if itemProps := item.Properties; itemProps != nil {
if itemProps.Schedule != nil && itemProps.Schedule.Name != nil && *itemProps.Schedule.Name == scheduleName && itemProps.Runbook != nil && itemProps.Runbook.Name != nil && *itemProps.Runbook.Name == runbookName {
if itemProps.JobScheduleId == nil || *itemProps.JobScheduleId == "" {
return fmt.Errorf("job schedule Id is nil or empty listed by Automation Account %q Job Schedule List: %+v", id.AutomationAccountName, err)
}

jsId := jobschedule.NewJobScheduleID(id.SubscriptionId, id.ResourceGroupName, id.AutomationAccountName, *itemProps.JobScheduleId)
if _, err := client.Delete(ctx, jsId); err != nil {
return fmt.Errorf("deleting job schedule Id listed by Automation Account %q Job Schedule List:%v", id.AutomationAccountName, err)
}
}
if existing != nil {
return tf.ImportAsExistsError("azurerm_automation_job_schedule", tfID.ID())
}
}

Expand Down Expand Up @@ -177,7 +175,8 @@ func resourceAutomationJobScheduleCreate(d *pluginsdk.ResourceData, meta interfa
return err
}

d.SetId(id.ID())
d.SetId(tfID.ID())
d.Set("resource_manager_id", id.ID())

return resourceAutomationJobScheduleRead(d, meta)
}
Expand All @@ -187,41 +186,53 @@ func resourceAutomationJobScheduleRead(d *pluginsdk.ResourceData, meta interface
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := jobschedule.ParseJobScheduleID(d.Id())
// the jobSchedule ID may be updated by Runbook, so need to get the real id by list API
tfID, err := commonids.ParseCompositeResourceID(d.Id(), &schedule.ScheduleId{}, &runbook.RunbookId{})
if err != nil {
return err
}

resp, err := client.Get(ctx, *id)
js, err := GetJobScheduleFromTFID(ctx, client, tfID)
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
d.SetId("")
return nil
}
return fmt.Errorf("making Read request on %s: %+v", *id, err)
return err
}
if js == nil {
d.SetId("")
return nil
}

id, err := jobschedule.ParseJobScheduleID(pointer.From(js.Id))
if err != nil {
return fmt.Errorf("parse jobSchedule id %s: %v", pointer.From(js.Id), err)
catriona-m marked this conversation as resolved.
Show resolved Hide resolved
}

d.Set("resource_manager_id", id.ID())
d.Set("job_schedule_id", id.JobScheduleId)
d.Set("resource_group_name", id.ResourceGroupName)
d.Set("automation_account_name", id.AutomationAccountName)

if model := resp.Model; model != nil {
if props := model.Properties; props != nil {
d.Set("runbook_name", props.Runbook.Name)
d.Set("schedule_name", props.Schedule.Name)
// The response from the list API has no parameter field, so use Get API to get the JobSchedule
resp, err := client.Get(ctx, *id)
if err != nil {
return err
}

if v := props.RunOn; v != nil {
d.Set("run_on", v)
}
if resp.Model != nil && resp.Model.Properties != nil {
props := resp.Model.Properties
d.Set("runbook_name", props.Runbook.Name)
d.Set("schedule_name", props.Schedule.Name)

if props.Parameters != nil {
if v := *props.Parameters; v != nil {
jsParameters := make(map[string]interface{})
for key, value := range v {
jsParameters[strings.ToLower(key)] = value
}
d.Set("parameters", jsParameters)
if v := props.RunOn; v != nil {
d.Set("run_on", v)
}

if props.Parameters != nil {
if v := *props.Parameters; v != nil {
jsParameters := make(map[string]interface{})
for key, value := range v {
jsParameters[strings.ToLower(key)] = value
}
d.Set("parameters", jsParameters)
}
}
}
Expand All @@ -234,7 +245,20 @@ func resourceAutomationJobScheduleDelete(d *pluginsdk.ResourceData, meta interfa
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := jobschedule.ParseJobScheduleID(d.Id())
tfID, err := commonids.ParseCompositeResourceID(d.Id(), &schedule.ScheduleId{}, &runbook.RunbookId{})
if err != nil {
return err
}
js, err := GetJobScheduleFromTFID(ctx, client, tfID)
if err != nil {
return err
}

if js == nil {
return nil
}

id, err := jobschedule.ParseJobScheduleID(pointer.From(js.Id))
if err != nil {
return err
}
Expand All @@ -248,3 +272,21 @@ func resourceAutomationJobScheduleDelete(d *pluginsdk.ResourceData, meta interfa

return nil
}

func GetJobScheduleFromTFID(ctx context.Context, client *jobschedule.JobScheduleClient, id *commonids.CompositeResourceID[*schedule.ScheduleId, *runbook.RunbookId]) (js *jobschedule.JobSchedule, err error) {
accountID := jobschedule.NewAutomationAccountID(id.First.SubscriptionId, id.First.ResourceGroupName, id.First.AutomationAccountName)
filter := fmt.Sprintf("properties/schedule/name eq '%s' and properties/runbook/name eq '%s'", id.First.ScheduleName, id.Second.RunbookName)
jsList, err := client.ListByAutomationAccountComplete(ctx, accountID, jobschedule.ListByAutomationAccountOperationOptions{Filter: &filter})
if err != nil {
if response.WasNotFound(jsList.LatestHttpResponse) {
return nil, nil
}
catriona-m marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("loading Automation Account %q Job Schedule List: %+v", accountID.AutomationAccountName, err)
}

if len(jsList.Items) > 0 {
js = &jsList.Items[0]
}

return js, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import (
"testing"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-sdk/resource-manager/automation/2023-11-01/jobschedule"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-sdk/resource-manager/automation/2023-11-01/runbook"
"github.com/hashicorp/go-azure-sdk/resource-manager/automation/2023-11-01/schedule"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check"
"github.com/hashicorp/terraform-provider-azurerm/internal/clients"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/automation"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
)

Expand Down Expand Up @@ -75,6 +78,28 @@ func TestAccAutomationJobSchedule_update(t *testing.T) {
})
}

func TestAccAutomationJobSchedule_updateRunbook(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_automation_job_schedule", "test")
r := AutomationJobScheduleResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
{
Config: r.basic(data, "Update Runbook auto update"),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func TestAccAutomationJobSchedule_requiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_automation_job_schedule", "test")
r := AutomationJobScheduleResource{}
Expand All @@ -94,32 +119,37 @@ func TestAccAutomationJobSchedule_requiresImport(t *testing.T) {
}

func (t AutomationJobScheduleResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
id, err := jobschedule.ParseJobScheduleID(state.ID)
id, err := commonids.ParseCompositeResourceID(state.ID, &schedule.ScheduleId{}, &runbook.RunbookId{})
if err != nil {
return nil, err
}

resp, err := clients.Automation.JobSchedule.Get(ctx, *id)
resp, err := automation.GetJobScheduleFromTFID(ctx, clients.Automation.JobSchedule, id)
if err != nil {
return nil, fmt.Errorf("retrieving %s: %v", *id, err)
}

return pointer.To(resp.Model != nil), nil
return pointer.To(resp != nil), nil
}

func (AutomationJobScheduleResource) template(data acceptance.TestData) string {
func (AutomationJobScheduleResource) template(data acceptance.TestData, runbookDesc ...string) string {
var description string
if len(runbookDesc) > 0 {
description = runbookDesc[0]
}

return fmt.Sprintf(`
provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "test" {
name = "acctestRG-auto-%d"
location = "%s"
name = "acctestRG-auto-%[1]d"
location = "%[2]s"
}

resource "azurerm_automation_account" "test" {
name = "acctestAA-%d"
name = "acctestAA-%[1]d"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
sku_name = "Basic"
Expand All @@ -132,7 +162,7 @@ resource "azurerm_automation_runbook" "test" {
automation_account_name = azurerm_automation_account.test.name
log_verbose = "true"
log_progress = "true"
description = "This is a test runbook for terraform acceptance test"
description = "This is a test runbook for terraform acceptance test.%[3]s"
runbook_type = "PowerShell"

publish_content_link {
Expand All @@ -157,15 +187,15 @@ EOF
}

resource "azurerm_automation_schedule" "test" {
name = "acctestAS-%d"
name = "acctestAS-%[1]d"
resource_group_name = azurerm_resource_group.test.name
automation_account_name = azurerm_automation_account.test.name
frequency = "OneTime"
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger)
`, data.RandomInteger, data.Locations.Primary, description)
}

func (AutomationJobScheduleResource) basic(data acceptance.TestData) string {
func (AutomationJobScheduleResource) basic(data acceptance.TestData, runbookDesc ...string) string {
return fmt.Sprintf(`
%s

Expand All @@ -175,10 +205,10 @@ resource "azurerm_automation_job_schedule" "test" {
schedule_name = azurerm_automation_schedule.test.name
runbook_name = azurerm_automation_runbook.test.name
}
`, AutomationJobScheduleResource{}.template(data))
`, AutomationJobScheduleResource{}.template(data, runbookDesc...))
}

func (AutomationJobScheduleResource) complete(data acceptance.TestData) string {
func (AutomationJobScheduleResource) complete(data acceptance.TestData, runbookDesc ...string) string {
return fmt.Sprintf(`
%s

Expand All @@ -196,7 +226,7 @@ resource "azurerm_automation_job_schedule" "test" {
url = "https://www.Example.com"
}
}
`, AutomationJobScheduleResource{}.template(data))
`, AutomationJobScheduleResource{}.template(data, runbookDesc...))
}

func (AutomationJobScheduleResource) requiresImport(data acceptance.TestData) string {
Expand Down
Loading
Loading