diff --git a/internal/services/automation/automation_watcher.go b/internal/services/automation/automation_watcher.go new file mode 100644 index 000000000000..ecddfa9c44cf --- /dev/null +++ b/internal/services/automation/automation_watcher.go @@ -0,0 +1,250 @@ +package automation + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-provider-azurerm/internal/tags" + + "github.com/Azure/azure-sdk-for-go/services/preview/automation/mgmt/2020-01-13-preview/automation" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/automation/parse" + "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/utils" +) + +type WatcherModel struct { + AutomationAccountID string `tfschema:"automation_account_id"` + Name string `tfschema:"name"` + Location string `tfschema:"location"` + Tags map[string]interface{} `tfschema:"tags"` + Etag string `tfschema:"etag"` + ExecutionFrequencyInSeconds int64 `tfschema:"execution_frequency_in_seconds"` + ScriptName string `tfschema:"script_name"` + ScriptParameters map[string]interface{} `tfschema:"script_parameters"` + ScriptRunOn string `tfschema:"script_run_on"` + Description string `tfschema:"description"` + Status string `tfschema:"status"` +} + +type WatcherResource struct{} + +var _ sdk.Resource = (*WatcherResource)(nil) + +func (m WatcherResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "automation_account_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.AutomationAccountID, + }, + + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "location": commonschema.Location(), + + "tags": commonschema.Tags(), + + "etag": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "execution_frequency_in_seconds": { + Type: pluginsdk.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(0), + }, + + "script_name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "script_parameters": { + Type: pluginsdk.TypeMap, + Optional: true, + ForceNew: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "script_run_on": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "description": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + } +} + +func (m WatcherResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "status": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (m WatcherResource) ModelObject() interface{} { + return &WatcherModel{} +} + +func (m WatcherResource) ResourceType() string { + return "azurerm_automation_watcher" +} + +func (m WatcherResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + client := meta.Client.Automation.WatcherClient + + var model WatcherModel + if err := meta.Decode(&model); err != nil { + return err + } + + subscriptionID := meta.Client.Account.SubscriptionId + accountID, _ := parse.AutomationAccountID(model.AutomationAccountID) + id := parse.NewWatcherID(subscriptionID, accountID.ResourceGroup, accountID.Name, model.Name) + + existing, err := client.Get(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name) + if !utils.ResponseWasNotFound(existing.Response) { + if err != nil { + return fmt.Errorf("retreiving %s: %v", id, err) + } + return meta.ResourceRequiresImport(m.ResourceType(), id) + } + + var param automation.Watcher + param.Tags = tags.Expand(model.Tags) + param.Etag = utils.String(model.Etag) + param.Location = utils.String(model.Location) + param.WatcherProperties = &automation.WatcherProperties{} + prop := param.WatcherProperties + prop.ExecutionFrequencyInSeconds = utils.Int64(model.ExecutionFrequencyInSeconds) + prop.ScriptName = utils.String(model.ScriptName) + prop.ScriptParameters = tags.Expand(model.ScriptParameters) + prop.ScriptRunOn = utils.String(model.ScriptRunOn) + prop.Description = utils.String(model.Description) + + _, err = client.CreateOrUpdate(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name, param) + if err != nil { + return fmt.Errorf("creating %s: %v", id, err) + } + + meta.SetID(id) + return nil + }, + } +} + +func (m WatcherResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + id, err := parse.WatcherID(meta.ResourceData.Id()) + if err != nil { + return err + } + client := meta.Client.Automation.WatcherClient + result, err := client.Get(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name) + if err != nil { + return err + } + + var output WatcherModel + if err := meta.Decode(&output); err != nil { + return err + } + + prop := result.WatcherProperties + output.AutomationAccountID = parse.NewAutomationAccountID(id.SubscriptionId, id.ResourceGroup, id.AutomationAccountName).ID() + output.Name = id.Name + output.ExecutionFrequencyInSeconds = utils.NormaliseNilableInt64(prop.ExecutionFrequencyInSeconds) + output.ScriptName = utils.NormalizeNilableString(prop.ScriptName) + output.ScriptParameters = tags.Flatten(prop.ScriptParameters) + output.ScriptRunOn = utils.NormalizeNilableString(prop.ScriptRunOn) + output.Description = utils.NormalizeNilableString(prop.Description) + output.Status = utils.NormalizeNilableString(prop.Status) + + // tags, etag and location do not returned by response, so do NOT encode them + + return meta.Encode(&output) + }, + } +} + +func (m WatcherResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) (err error) { + client := meta.Client.Automation.WatcherClient + + id, err := parse.WatcherID(meta.ResourceData.Id()) + if err != nil { + return err + } + + var model WatcherModel + if err = meta.Decode(&model); err != nil { + return fmt.Errorf("decoding err: %+v", err) + } + + var upd automation.WatcherUpdateParameters + upd.WatcherUpdateProperties = &automation.WatcherUpdateProperties{} + if meta.ResourceData.HasChange("execution_frequency_in_seconds") { + upd.WatcherUpdateProperties.ExecutionFrequencyInSeconds = utils.Int64(model.ExecutionFrequencyInSeconds) + } + if _, err = client.Update(ctx, id.ResourceGroup, id.Name, id.AutomationAccountName, upd); err != nil { + return fmt.Errorf("updating %s: %v", id, err) + } + + return nil + }, + } +} + +func (m WatcherResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + id, err := parse.WatcherID(meta.ResourceData.Id()) + if err != nil { + return err + } + meta.Logger.Infof("deleting %s", id) + client := meta.Client.Automation.WatcherClient + if _, err = client.Delete(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name); err != nil { + return fmt.Errorf("deleting %s: %v", id, err) + } + return nil + }, + } +} + +func (m WatcherResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.WatcherID +} diff --git a/internal/services/automation/automation_watcher_test.go b/internal/services/automation/automation_watcher_test.go new file mode 100644 index 000000000000..a531cb33797a --- /dev/null +++ b/internal/services/automation/automation_watcher_test.go @@ -0,0 +1,237 @@ +package automation_test + +import ( + "context" + "fmt" + "testing" + + "github.com/google/uuid" + "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/services/automation/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type WatcherResource struct{} + +func TestAccWatcher_basic(t *testing.T) { + data := acceptance.BuildTestData(t, automation.WatcherResource{}.ResourceType(), "test") + r := WatcherResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("execution_frequency_in_seconds").HasValue("2"), + ), + }, + data.ImportStep("tags", "etag", "location"), + }) +} + +func TestAccWatcher_update(t *testing.T) { + data := acceptance.BuildTestData(t, automation.WatcherResource{}.ResourceType(), "test") + r := WatcherResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("execution_frequency_in_seconds").HasValue("2"), + ), + }, + data.ImportStep("tags", "etag", "location"), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("execution_frequency_in_seconds").HasValue("20"), + ), + }, + data.ImportStep("tags", "etag", "location"), + }) +} + +func (a WatcherResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.WatcherID(state.ID) + if err != nil { + return nil, err + } + resp, err := client.Automation.WatcherClient.Get(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name) + if err != nil { + return nil, fmt.Errorf("retrieving Type %s: %+v", id, err) + } + return utils.Bool(resp.WatcherProperties != nil), nil +} + +func (a WatcherResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` + + +%s + +resource "azurerm_automation_watcher" "test" { + automation_account_id = azurerm_automation_account.test.id + name = "acctest-watcher-%[2]d" + location = "%[3]s" + + tags = { + foo = "bar" + } + + script_parameters = { + param_foo = "arg_bar" + } + + script_run_on = azurerm_automation_hybrid_runbook_worker_group.test.name + description = "example-watcher desc" + etag = "etag example" + script_name = azurerm_automation_runbook.test.name + execution_frequency_in_seconds = 2 +} +`, a.template(data), data.RandomInteger, data.Locations.Primary) +} + +func (a WatcherResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` + + +%s + +resource "azurerm_automation_watcher" "test" { + automation_account_id = azurerm_automation_account.test.id + name = "acctest-watcher-%[2]d" + location = "%[3]s" + + tags = { + "foo" = "bar" + } + + script_parameters = { + foo = "bar" + } + + etag = "etag example" + execution_frequency_in_seconds = 20 + script_name = azurerm_automation_runbook.test.name + script_run_on = azurerm_automation_hybrid_runbook_worker_group.test.name + description = "example-watcher desc" +} +`, a.template(data), data.RandomInteger, data.Locations.Primary) +} + +func (a WatcherResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-auto-%[1]d" + location = "%[2]s" +} + +resource "azurerm_automation_account" "test" { + name = "acctestAA-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "Basic" +} + +resource "azurerm_automation_credential" "test" { + name = "acctest-%[1]d" + resource_group_name = azurerm_resource_group.test.name + automation_account_name = azurerm_automation_account.test.name + username = "test_user" + password = "test_pwd" +} + +resource "azurerm_automation_hybrid_runbook_worker_group" "test" { + resource_group_name = azurerm_resource_group.test.name + automation_account_name = azurerm_automation_account.test.name + name = "acctest-%[1]d" + credential_name = azurerm_automation_credential.test.name +} + +resource "azurerm_virtual_network" "test" { + name = "acctestnw-%[1]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "internal" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + ip_configuration { + name = "testconfiguration1" + subnet_id = azurerm_subnet.test.id + private_ip_address_allocation = "Dynamic" + } +} + +resource "azurerm_linux_virtual_machine" "test" { + name = "acctestVM-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_F2" + admin_username = "adminuser" + admin_password = "P@$$w0rd1234!" + + disable_password_authentication = false + + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-LTS" + version = "latest" + } + + tags = { + azsecpack = "nonprod" + "platformsettings.host_environment.service.platform_optedin_for_rootcerts" = "true" + } +} + +resource "azurerm_automation_runbook" "test" { + name = "acc-runbook-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + automation_account_name = azurerm_automation_account.test.name + + log_verbose = "true" + log_progress = "true" + description = "This is a test runbook for terraform acceptance test" + runbook_type = "PowerShell" + + content = <