diff --git a/internal/services/automation/automation_job_schedule_resource.go b/internal/services/automation/automation_job_schedule_resource.go index 4992c841b8d5..5ca66d60fa7c 100644 --- a/internal/services/automation/automation_job_schedule_resource.go +++ b/internal/services/automation/automation_job_schedule_resource.go @@ -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" @@ -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 }), @@ -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(), @@ -84,6 +95,11 @@ func resourceAutomationJobSchedule() *pluginsdk.Resource { Computed: true, ValidateFunc: validation.IsUUID, }, + + "resource_manager_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, }, } } @@ -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) @@ -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) - } - } - - if !response.WasNotFound(existing.HttpResponse) { - return tf.ImportAsExistsError("azurerm_automation_job_schedule", id.ID()) + return fmt.Errorf("checking for presence of existing %s: %s", id, err) } - } - - 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()) } } @@ -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) } @@ -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 err + } + + 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) } } } @@ -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 } @@ -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 + } + 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 +} diff --git a/internal/services/automation/automation_job_schedule_resource_test.go b/internal/services/automation/automation_job_schedule_resource_test.go index d98e29e654a5..f60c4e0c8252 100644 --- a/internal/services/automation/automation_job_schedule_resource_test.go +++ b/internal/services/automation/automation_job_schedule_resource_test.go @@ -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" ) @@ -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{} @@ -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" @@ -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 { @@ -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 @@ -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 @@ -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 { diff --git a/internal/services/automation/automation_runbook_resource.go b/internal/services/automation/automation_runbook_resource.go index fd0873abad0f..b0fcc7cb8ff5 100644 --- a/internal/services/automation/automation_runbook_resource.go +++ b/internal/services/automation/automation_runbook_resource.go @@ -4,6 +4,7 @@ package automation import ( + "context" "fmt" "log" "time" @@ -271,89 +272,74 @@ func resourceAutomationRunbookCreateUpdate(d *pluginsdk.ResourceData, meta inter } } - location := azure.NormalizeLocation(d.Get("location").(string)) - t := d.Get("tags").(map[string]interface{}) - - runbookType := runbook.RunbookTypeEnum(d.Get("runbook_type").(string)) - logProgress := d.Get("log_progress").(bool) - logVerbose := d.Get("log_verbose").(bool) - description := d.Get("description").(string) - - parameters := runbook.RunbookCreateOrUpdateParameters{ - Properties: runbook.RunbookCreateOrUpdateProperties{ - LogVerbose: &logVerbose, - LogProgress: &logProgress, - RunbookType: runbookType, - Description: &description, - LogActivityTrace: utils.Int64(int64(d.Get("log_activity_trace_level").(int))), - }, + // for existing runbook, if only job_schedule field updated, then skip update runbook + if d.IsNewResource() || d.HasChangeExcept("job_schedule") { - Location: &location, - } - if tagsVal := expandStringInterfaceMap(t); tagsVal != nil { - parameters.Tags = &tagsVal - } + location := azure.NormalizeLocation(d.Get("location").(string)) + t := d.Get("tags").(map[string]interface{}) - contentLink := expandContentLink(d.Get("publish_content_link").([]interface{})) - if contentLink != nil { - parameters.Properties.PublishContentLink = contentLink - } else { - parameters.Properties.Draft = &runbook.RunbookDraft{} - if draft := expandDraft(d.Get("draft").([]interface{})); draft != nil { - parameters.Properties.Draft = draft - } - } + runbookType := runbook.RunbookTypeEnum(d.Get("runbook_type").(string)) + logProgress := d.Get("log_progress").(bool) + logVerbose := d.Get("log_verbose").(bool) + description := d.Get("description").(string) - if _, err := client.CreateOrUpdate(ctx, id, parameters); err != nil { - return fmt.Errorf("creating/updating %s: %+v", id, err) - } + parameters := runbook.RunbookCreateOrUpdateParameters{ + Properties: runbook.RunbookCreateOrUpdateProperties{ + LogVerbose: &logVerbose, + LogProgress: &logProgress, + RunbookType: runbookType, + Description: &description, + LogActivityTrace: utils.Int64(int64(d.Get("log_activity_trace_level").(int))), + }, - if v, ok := d.GetOk("content"); ok { - content := v.(string) - draftRunbookID := runbookdraft.NewRunbookID(id.SubscriptionId, id.ResourceGroupName, id.AutomationAccountName, id.RunbookName) - if err := autoCli.RunbookDraft.ReplaceContentThenPoll(ctx, draftRunbookID, []byte(content)); err != nil { - return fmt.Errorf("setting the draft for %s: %+v", id, err) + Location: &location, + } + if tagsVal := expandStringInterfaceMap(t); tagsVal != nil { + parameters.Tags = &tagsVal } - if err := autoCli.Runbook.PublishThenPoll(ctx, id); err != nil { - return fmt.Errorf("publishing the updated %s: %+v", id, err) + contentLink := expandContentLink(d.Get("publish_content_link").([]interface{})) + if contentLink != nil { + parameters.Properties.PublishContentLink = contentLink + } else { + parameters.Properties.Draft = &runbook.RunbookDraft{} + if draft := expandDraft(d.Get("draft").([]interface{})); draft != nil { + parameters.Properties.Draft = draft + } } - } - d.SetId(id.ID()) + if _, err := client.CreateOrUpdate(ctx, id, parameters); err != nil { + return fmt.Errorf("creating/updating %s: %+v", id, err) + } - automationAccountId := jobschedule.NewAutomationAccountID(subscriptionID, id.ResourceGroupName, id.AutomationAccountName) - jsIterator, err := jsClient.ListByAutomationAccountComplete(ctx, automationAccountId, jobschedule.ListByAutomationAccountOperationOptions{}) - if err != nil { - return fmt.Errorf("loading Automation Account %q Job Schedule List: %+v", id.AutomationAccountName, err) - } + if v, ok := d.GetOk("content"); ok { + content := v.(string) + draftRunbookID := runbookdraft.NewRunbookID(id.SubscriptionId, id.ResourceGroupName, id.AutomationAccountName, id.RunbookName) + if err := autoCli.RunbookDraft.ReplaceContentThenPoll(ctx, draftRunbookID, []byte(content)); err != nil { + return fmt.Errorf("setting the draft for %s: %+v", id, err) + } - for _, item := range jsIterator.Items { - if itemProps := item.Properties; itemProps != nil { - if itemProps.Runbook != nil && itemProps.Runbook.Name != nil && *itemProps.Runbook.Name == id.RunbookName { - if itemProps.JobScheduleId == nil || *itemProps.JobScheduleId == "" { - return fmt.Errorf("job schedule Id is nil or empty listed by %s Job Schedule List: %+v", id, err) - } - parsedId := jobschedule.NewJobScheduleID(subscriptionID, id.ResourceGroupName, id.AutomationAccountName, *itemProps.JobScheduleId) - if resp, err := jsClient.Delete(ctx, parsedId); err != nil { - if !response.WasNotFound(resp.HttpResponse) { - return fmt.Errorf("deleting job schedule Id listed by %s Job Schedule List:%v", id, err) - } - } + if err := autoCli.Runbook.PublishThenPoll(ctx, id); err != nil { + return fmt.Errorf("publishing the updated %s: %+v", id, err) } } + + d.SetId(id.ID()) } - if v, ok := d.GetOk("job_schedule"); ok { - jsMap, err := helper.ExpandAutomationJobSchedule(v.(*pluginsdk.Set).List(), id.RunbookName) + // **don't need** to list job schedules and delete all of them. update the runbook will recreate these job schedules automatically, + // but with a different job schedule id + // crosscheck these existing jobs and jobs from tf, delete the ones not in tf, and create the ones not in api + // Fix issue: https://github.com/hashicorp/terraform-provider-azurerm/issues/8634 + jsValue, ok := d.GetOk("job_schedule") + if ok && d.HasChange("job_schedule") { + jsMap, err := helper.ExpandAutomationJobSchedule(jsValue.(*pluginsdk.Set).List(), id.RunbookName) if err != nil { return err } - for jsuuid, js := range *jsMap { - jsId := jobschedule.NewJobScheduleID(subscriptionID, id.ResourceGroupName, id.AutomationAccountName, jsuuid.String()) - if _, err := jsClient.Create(ctx, jsId, js); err != nil { - return fmt.Errorf("creating %s: %+v", id, err) - } + + if err := updatedLinkedJobSchedules(ctx, subscriptionID, jsClient, &id, *jsMap); err != nil { + return fmt.Errorf("update job schedule links: %v", err) } } @@ -416,21 +402,31 @@ func resourceAutomationRunbookRead(d *pluginsdk.ResourceData, meta interface{}) jsMap := make(map[uuid.UUID]jobschedule.JobScheduleProperties) automationAccountId := jobschedule.NewAutomationAccountID(id.SubscriptionId, id.ResourceGroupName, id.AutomationAccountName) - jsIterator, err := jsClient.ListByAutomationAccountComplete(ctx, automationAccountId, jobschedule.ListByAutomationAccountOperationOptions{}) + filter := fmt.Sprintf("properties/runbook/name eq '%s'", id.RunbookName) + jsIterator, err := jsClient.ListByAutomationAccount(ctx, automationAccountId, jobschedule.ListByAutomationAccountOperationOptions{Filter: &filter}) if err != nil { return fmt.Errorf("loading Automation Account %q Job Schedule List: %+v", id.AutomationAccountName, err) } - for _, item := range jsIterator.Items { + for _, item := range pointer.From(jsIterator.Model) { if itemProps := item.Properties; itemProps != nil { - if itemProps.Runbook != nil && itemProps.Runbook.Name != nil && *itemProps.Runbook.Name == id.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, err := uuid.FromString(*itemProps.JobScheduleId) - if err != nil { - return fmt.Errorf("parsing job schedule Id listed by Automation Account %q Job Schedule List:%v", id.AutomationAccountName, err) - } - jsMap[jsId] = *itemProps + 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, err := uuid.FromString(*itemProps.JobScheduleId) + if err != nil { + return fmt.Errorf("parsing job schedule Id listed by Automation Account %q Job Schedule List: %v", id.AutomationAccountName, err) + } + // get job schedule from GET API, `ListByAutomationAccountComplete` lost parameters + jobscheduleID, err := jobschedule.ParseJobScheduleID(pointer.From(item.Id)) + if err != nil { + return fmt.Errorf("parsing job schedule Id listed by Automation Account %q Job Schedule List: %v", id.AutomationAccountName, err) + } + jsResult, err := jsClient.Get(ctx, *jobscheduleID) + if err != nil { + return fmt.Errorf("retrieving job schedule by %s: %v", *jobscheduleID, err) + } + if jsResult.Model != nil && jsResult.Model.Properties != nil { + jsMap[jsId] = *jsResult.Model.Properties } } } @@ -535,3 +531,53 @@ func expandDraft(inputs []interface{}) *runbook.RunbookDraft { return &res } + +// if job in jsIterator but not in jsMap, then delete it +// if job in both jsIterator and jsMap, remove the entry in jsMap +// at last, create jobs still in jsMap +func updatedLinkedJobSchedules(ctx context.Context, subscriptionID string, client *jobschedule.JobScheduleClient, id *runbook.RunbookId, jsMap map[string]jobschedule.JobScheduleCreateParameters) error { + automationAccountId := jobschedule.NewAutomationAccountID(id.SubscriptionId, id.ResourceGroupName, id.AutomationAccountName) + filter := fmt.Sprintf("properties/runbook/name eq '%s'", id.RunbookName) + jsIterator, err := client.ListByAutomationAccount(ctx, automationAccountId, jobschedule.ListByAutomationAccountOperationOptions{Filter: &filter}) + if err != nil { + return fmt.Errorf("loading Automation Account %q Job Schedule List: %+v", id.AutomationAccountName, err) + } + + for _, item := range pointer.From(jsIterator.Model) { + prop := item.Properties + jobDigest := helper.ResourceAutomationJobScheduleDigest(prop) + + if _, ok := jsMap[jobDigest]; ok { + delete(jsMap, jobDigest) + } else { + if prop == nil || prop.JobScheduleId == nil || *prop.JobScheduleId == "" { + return fmt.Errorf("job schedule Id is nil or empty listed by %s Job Schedule List: %+v", id, err) + } + parsedId := jobschedule.NewJobScheduleID(id.SubscriptionId, id.ResourceGroupName, id.AutomationAccountName, pointer.From(item.Properties.JobScheduleId)) + if resp, err := client.Delete(ctx, parsedId); err != nil { + if !response.WasNotFound(resp.HttpResponse) { + return fmt.Errorf("deleting job schedule Id listed by %s Job Schedule List:%v", id, err) + } + } + } + } + + // create jobs still in jsMap + for _, js := range jsMap { + // skip if the schedule name is empty + if pointer.From(js.Properties.Schedule.Name) == "" { + continue + } + jsuuid, err := uuid.NewV4() + if err != nil { + return fmt.Errorf("creating job schedule Id(UUID) for %s: %+v", id, err) + } + + jsId := jobschedule.NewJobScheduleID(subscriptionID, id.ResourceGroupName, id.AutomationAccountName, jsuuid.String()) + if _, err := client.Create(ctx, jsId, js); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + } + + return nil +} diff --git a/internal/services/automation/automation_runbook_resource_test.go b/internal/services/automation/automation_runbook_resource_test.go index 2157a3098187..0135144a7332 100644 --- a/internal/services/automation/automation_runbook_resource_test.go +++ b/internal/services/automation/automation_runbook_resource_test.go @@ -469,7 +469,7 @@ resource "azurerm_automation_runbook" "test" { 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 with update" runbook_type = "PowerShell" content = < **NOTE** AzureRM provides this stand-alone [azurerm_automation_job_schedule](automation_job_schedule.html.markdown) and an inlined `job_schdule` property in [azurerm_runbook](automation_runbook.html.markdown) to manage the job schedules. You can only make use of one of these methods to manage a job schedule. + ## Example Usage This is an example of just the Job Schedule. A full example of the `azurerm_automation_job_schedule` resource can be found in [the `./examples/automation-account` directory within the GitHub Repository](https://github.com/hashicorp/terraform-provider-azurerm/tree/main/examples/automation-account) @@ -50,9 +52,11 @@ The following arguments are supported: In addition to the Arguments listed above - the following Attributes are exported: -* `id` - The ID of the Automation Job Schedule. +* `id` - The ID of the Automation Job Schedule. The format of the ID is `azurerm_automation_account.id|azurerm_automation_runbook.id`. There is an example in the [#Import](#import) part. + +* `job_schedule_id` - The UUID identifying the Automation Job Schedule. -* `job_schedule_id` - (Optional) The UUID identifying the Automation Job Schedule. +* `resource_manager_id` - The Resource Manager ID of the Automation Job Schedule. ## Timeouts @@ -67,5 +71,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/l Automation Job Schedules can be imported using the `resource id`, e.g. ```shell -terraform import azurerm_automation_job_schedule.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/jobSchedules/10000000-1001-1001-1001-000000000001 +terraform import azurerm_automation_job_schedule.example "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/schedules/schedule1|/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/runbooks/runbook1" ``` diff --git a/website/docs/r/automation_runbook.html.markdown b/website/docs/r/automation_runbook.html.markdown index f4f07af0ef3c..c006ddf4bcf0 100644 --- a/website/docs/r/automation_runbook.html.markdown +++ b/website/docs/r/automation_runbook.html.markdown @@ -106,7 +106,11 @@ The following arguments are supported: * `log_activity_trace_level` - (Optional) Specifies the activity-level tracing options of the runbook, available only for Graphical runbooks. Possible values are `0` for None, `9` for Basic, and `15` for Detailed. Must turn on Verbose logging in order to see the tracing. -* `draft` - (Optional) A `draft` block as defined below . +* `draft` - (Optional) A `draft` block as defined below. + +* `job_schedule` - (Optional) One or more `job_schedule` block as defined below. + +~> **NOTE** AzureRM provides a stand-alone [azurerm_automation_job_schedule](automation_job_schedule.html.markdown) and this inlined `job_schdule` property to manage the job schedules. At this time you should choose one of them to manage the job schedule resources. --- @@ -152,12 +156,32 @@ The `parameters` block supports: * `default_value` - (Optional) Specifies the default value of the parameter. +--- + +The `job_schedule` block supports: + +* `schedule_name` - (Required) The name of the Schedule. + +* `parameters` - (Optional) A map of key/value pairs corresponding to the arguments that can be passed to the Runbook. + +-> **NOTE:** The parameter keys/names must strictly be in lowercase, even if this is not the case in the runbook. This is due to a limitation in Azure Automation where the parameter names are normalized. The values specified don't have this limitation. + +* `run_on` - (Optional) Name of a Hybrid Worker Group the Runbook will be executed on. + ## Attributes Reference In addition to the Arguments listed above - the following Attributes are exported: * `id` - The Automation Runbook ID. +* `job_schedule` - One or more `job_schedule` block as defined below. + +--- + +An `job_schedule` block exports the following: + +* `job_schedule_id` - The UUID of automation runbook job schedule ID. + ## Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: