diff --git a/internal/services/automation/automation_software_update_configuration.go b/internal/services/automation/automation_software_update_configuration.go new file mode 100644 index 000000000000..5ed768b4047d --- /dev/null +++ b/internal/services/automation/automation_software_update_configuration.go @@ -0,0 +1,959 @@ +package automation + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/automation/mgmt/2020-01-13-preview/automation" + "github.com/Azure/go-autorest/autorest/date" + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + validate4 "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + validate3 "github.com/hashicorp/terraform-provider-azurerm/internal/services/apimanagement/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/automation/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/automation/validate" + computeValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/compute/validate" + validate2 "github.com/hashicorp/terraform-provider-azurerm/internal/services/resource/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/suppress" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type Tag struct { + Tag string `tfschema:"tag"` + Values []string `tfschema:"values"` +} + +type AzureQuery struct { + Scope []string `tfschema:"scope"` + Locations []string `tfschema:"locations"` + Tags []Tag `tfschema:"tags"` + TagFilter string `tfschema:"tag_filter"` +} + +func (a *AzureQuery) LoadSDKTags(tags map[string][]string) { + if tags == nil { + return + } + for k, vs := range tags { + t := Tag{} + t.Tag = k + for _, v := range vs { + t.Values = append(t.Values, v) + } + a.Tags = append(a.Tags, t) + } +} +func (a *AzureQuery) ToSDKTags() map[string][]string { + if len(a.Tags) == 0 { + return nil + } + m := map[string][]string{} + for _, tag := range a.Tags { + m[tag.Tag] = tag.Values + } + return m +} + +type Linux struct { + Reboot string `tfschema:"reboot"` + Classification string `tfschema:"classification_included"` + ExcludedPackages []string `tfschema:"excluded_packages"` + IncludedPackages []string `tfschema:"included_packages"` +} + +type MonthlyOccurrence struct { + Occurrence int32 `tfschema:"occurrence"` + Day string `tfschema:"day"` +} + +type NonAzureQuery struct { + FunctionAlias string `tfschema:"function_alias"` + WorkspaceId string `tfschema:"workspace_id"` +} + +type UpdateTask struct { + Source string `tfschema:"source"` + Parameters map[string]string `tfschema:"parameters"` +} + +func updateTaskFromSDK(prop *automation.TaskProperties) (res []UpdateTask) { + if prop == nil { + return + } + res = append(res, UpdateTask{ + Source: utils.NormalizeNilableString(prop.Source), + }) + for k, v := range prop.Parameters { + res[0].Parameters[k] = *v + } + return +} + +func (u *UpdateTask) ToSDKModel() *automation.TaskProperties { + if u == nil { + return nil + } + res := &automation.TaskProperties{ + Parameters: map[string]*string{}, + Source: utils.String(u.Source), + } + for k, v := range u.Parameters { + vCopy := v + res.Parameters[k] = &vCopy + } + return res +} + +type Schedule struct { + Description string `tfschema:"description"` + StartTime string `tfschema:"start_time"` + StartTimeOffsetMinutes float64 `tfschema:"start_time_offset_minutes"` + ExpiryTime string `tfschema:"expiry_time"` + ExpiryTimeOffsetMinutes float64 `tfschema:"expiry_time_offset_minutes"` + IsEnabled bool `tfschema:"is_enabled"` + NextRun string `tfschema:"next_run"` + NextRunOffsetMinutes float64 `tfschema:"next_run_offset_minutes"` + Interval int `tfschema:"interval"` + Frequency string `tfschema:"frequency"` + CreationTime string `tfschema:"creation_time"` + LastModifiedTime string `tfschema:"last_modified_time"` + TimeZone string `tfschema:"time_zone"` + AdvancedWeekDays []string `tfschema:"advanced_week_days"` + AdvancedMonthDays []int `tfschema:"advanced_month_days"` + MonthlyOccurrence []MonthlyOccurrence `tfschema:"monthly_occurrence"` +} + +func (s *Schedule) LoadSDKModel(info *automation.SUCScheduleProperties) { + timeString := func(t *date.Time) string { + if t == nil { + return "" + } + return t.Time.Format(time.RFC3339) + } + + s.StartTime = timeString(info.StartTime) + s.StartTimeOffsetMinutes = pointer.ToFloat64(info.StartTimeOffsetMinutes) + s.ExpiryTime = timeString(info.ExpiryTime) + s.ExpiryTimeOffsetMinutes = pointer.ToFloat64(info.ExpiryTimeOffsetMinutes) + s.IsEnabled = utils.NormaliseNilableBool(info.IsEnabled) + s.NextRun = timeString(info.NextRun) + s.NextRunOffsetMinutes = pointer.ToFloat64(info.NextRunOffsetMinutes) + if info.Interval != nil { + s.Interval = int(*info.Interval) + } + s.Frequency = string(info.Frequency) + s.TimeZone = utils.NormalizeNilableString(info.TimeZone) + s.CreationTime = timeString(info.CreationTime) + s.LastModifiedTime = timeString(info.LastModifiedTime) + s.Description = utils.NormalizeNilableString(info.Description) + + if setting := info.AdvancedSchedule; setting != nil { + s.AdvancedWeekDays = pointer.ToSliceOfStrings(setting.WeekDays) + if setting.MonthDays != nil { + for _, v := range *(setting.MonthDays) { + s.AdvancedMonthDays = append(s.AdvancedMonthDays, int(v)) + } + } + + if setting.MonthlyOccurrences != nil { + for _, occ := range *setting.MonthlyOccurrences { + s.MonthlyOccurrence = append(s.MonthlyOccurrence, MonthlyOccurrence{ + Occurrence: utils.NormaliseNilableInt32(occ.Occurrence), + Day: string(occ.Day), + }) + } + } + } +} + +// will keep old values encode from config +func scheduleFromSDK(info *automation.SUCScheduleProperties, old []Schedule) []Schedule { + if info == nil { + return old + } + if len(old) == 0 { + old = append(old, Schedule{}) + } + old[0].LoadSDKModel(info) + + return old +} + +func (s *Schedule) ToSDKModel() *automation.SUCScheduleProperties { + if s == nil { + return nil + } + + parseTime := func(s string) *date.Time { + t, _ := time.Parse(time.RFC3339, s) + return &date.Time{Time: t} + } + + res := automation.SUCScheduleProperties{ + StartTime: parseTime(s.StartTime), + StartTimeOffsetMinutes: utils.Float(s.StartTimeOffsetMinutes), + ExpiryTime: parseTime(s.ExpiryTime), + ExpiryTimeOffsetMinutes: utils.Float(s.ExpiryTimeOffsetMinutes), + IsEnabled: utils.Bool(s.IsEnabled), + NextRun: parseTime(s.NextRun), + NextRunOffsetMinutes: utils.Float(s.NextRunOffsetMinutes), + Interval: utils.Int64(int64(s.Interval)), + TimeZone: utils.String(s.TimeZone), + AdvancedSchedule: &automation.AdvancedSchedule{}, + Description: utils.String(s.Description), + Frequency: automation.ScheduleFrequency(s.Frequency), + } + + if len(s.AdvancedWeekDays) > 0 { + res.AdvancedSchedule.WeekDays = &s.AdvancedWeekDays + } + + if len(s.AdvancedMonthDays) > 0 { + var is []int32 + for _, v := range s.AdvancedMonthDays { + is = append(is, int32(v)) + } + res.AdvancedSchedule.MonthDays = &is + } + + var occ []automation.AdvancedScheduleMonthlyOccurrence + for _, m := range s.MonthlyOccurrence { + occ = append(occ, automation.AdvancedScheduleMonthlyOccurrence{ + Occurrence: utils.Int32(m.Occurrence), + Day: automation.ScheduleDay(m.Day), + }) + } + + if len(occ) > 0 { + res.AdvancedSchedule.MonthlyOccurrences = &occ + } + return &res +} + +type Target struct { + AzureQueries []AzureQuery `tfschema:"azure_query"` + NonAzureQueries []NonAzureQuery `tfschema:"non_azure_query"` +} + +func targetsFromSDK(prop *automation.TargetProperties) []Target { + if prop == nil { + return nil + } + + var t Target + if prop.AzureQueries != nil { + for _, az := range *prop.AzureQueries { + q := AzureQuery{ + Scope: pointer.ToSliceOfStrings(az.Scope), + Locations: pointer.ToSliceOfStrings(az.Locations), + } + if setting := az.TagSettings; setting != nil { + q.LoadSDKTags(setting.Tags) + q.TagFilter = string(setting.FilterOperator) + } + t.AzureQueries = append(t.AzureQueries, q) + } + } + + if prop.NonAzureQueries != nil { + for _, az := range *prop.NonAzureQueries { + q := NonAzureQuery{ + FunctionAlias: utils.NormalizeNilableString(az.FunctionAlias), + WorkspaceId: utils.NormalizeNilableString(az.WorkspaceID), + } + t.NonAzureQueries = append(t.NonAzureQueries, q) + } + } + + return []Target{t} +} + +type Windows struct { + Classification string `tfschema:"classification_included"` + ExcludedKbs []string `tfschema:"excluded_knowledge_base_numbers"` + IncludedKbs []string `tfschema:"included_knowledge_base_numbers"` + RebootSetting string `tfschema:"reboot"` +} + +type SoftwareUpdateConfigurationModel struct { + AutomationAccountID string `tfschema:"automation_account_id"` + Name string `tfschema:"name"` + ErrorCode string `tfschema:"error_code"` + ErrorMeesage string `tfschema:"error_meesage"` + OperatingSystem string `tfschema:"operating_system"` + Linux []Linux `tfschema:"linux"` + Windows []Windows `tfschema:"windows"` + Duration string `tfschema:"duration"` + VirtualMachines []string `tfschema:"virtual_machine_ids"` + NonAzureComputerNames []string `tfschema:"non_azure_computer_names"` + Targets []Target `tfschema:"target"` + Schedule []Schedule `tfschema:"schedule"` + PreTask []UpdateTask `tfschema:"pre_task"` + PostTask []UpdateTask `tfschema:"post_task"` +} + +func (s *SoftwareUpdateConfigurationModel) ToSDKModel() automation.SoftwareUpdateConfiguration { + var param automation.SoftwareUpdateConfiguration + param.Name = utils.String(s.Name) + param.SoftwareUpdateConfigurationProperties = &automation.SoftwareUpdateConfigurationProperties{} + prop := param.SoftwareUpdateConfigurationProperties + prop.UpdateConfiguration = &automation.UpdateConfiguration{} + upd := prop.UpdateConfiguration + upd.OperatingSystem = automation.OperatingSystemType(s.OperatingSystem) + + if len(s.Linux) > 0 { + l := s.Linux[0] + upd.Linux = &automation.LinuxProperties{ + IncludedPackageClassifications: automation.LinuxUpdateClasses(l.Classification), + } + if l.Reboot != "" { + upd.Linux.RebootSetting = utils.String(l.Reboot) + } + + upd.Linux.IncludedPackageNameMasks = utils.StringSlice(l.IncludedPackages) + upd.Linux.ExcludedPackageNameMasks = utils.StringSlice(l.ExcludedPackages) + } + + if len(s.Windows) > 0 { + w := s.Windows[0] + upd.Windows = &automation.WindowsProperties{ + IncludedUpdateClassifications: automation.WindowsUpdateClasses(w.Classification), + } + + if w.RebootSetting != "" { + upd.Windows.RebootSetting = utils.String(w.RebootSetting) + } + upd.Windows.IncludedKbNumbers = utils.StringSlice(w.IncludedKbs) + upd.Windows.ExcludedKbNumbers = utils.StringSlice(w.ExcludedKbs) + } + + upd.Duration = utils.String(s.Duration) + upd.AzureVirtualMachines = utils.StringSlice(s.VirtualMachines) + upd.NonAzureComputerNames = utils.StringSlice(s.NonAzureComputerNames) + + if len(s.Targets) > 0 { + upd.Targets = &automation.TargetProperties{} + var azureQueries []automation.AzureQueryProperties + t := s.Targets[0] + for _, az := range t.AzureQueries { + q := automation.AzureQueryProperties{ + Scope: utils.StringSlice(az.Scope), + Locations: utils.StringSlice(az.Locations), + TagSettings: nil, + } + tag := automation.TagSettingsProperties{} + tag.Tags = az.ToSDKTags() + tag.FilterOperator = automation.TagOperators(az.TagFilter) + q.TagSettings = &tag + azureQueries = append(azureQueries, q) + } + + if azureQueries != nil { + upd.Targets.AzureQueries = &azureQueries + } + + var nonAzureQueries []automation.NonAzureQueryProperties + for _, az := range t.NonAzureQueries { + q := automation.NonAzureQueryProperties{ + FunctionAlias: utils.String(az.FunctionAlias), + WorkspaceID: utils.String(az.WorkspaceId), + } + nonAzureQueries = append(nonAzureQueries, q) + } + + if nonAzureQueries != nil { + upd.Targets.NonAzureQueries = &nonAzureQueries + } + } + + if len(s.Schedule) > 0 { + prop.ScheduleInfo = s.Schedule[0].ToSDKModel() + } + + prop.Tasks = &automation.SoftwareUpdateConfigurationTasks{} + if len(s.PreTask) > 0 { + prop.Tasks.PreTask = s.PreTask[0].ToSDKModel() + } + if len(s.PostTask) > 0 { + prop.Tasks.PostTask = s.PostTask[0].ToSDKModel() + } + return param +} + +func (s *SoftwareUpdateConfigurationModel) LoadSDKModel(prop *automation.SoftwareUpdateConfigurationProperties) { + if prop == nil { + return + } + + if prop.Error != nil { + s.ErrorCode = utils.NormalizeNilableString(prop.Error.Code) + s.ErrorMeesage = utils.NormalizeNilableString(prop.Error.Message) + } + + if conf := prop.UpdateConfiguration; conf != nil { + s.OperatingSystem = string(conf.OperatingSystem) + + if l := conf.Linux; l != nil { + s.Linux = []Linux{{ + Reboot: utils.NormalizeNilableString(l.RebootSetting), + Classification: string(l.IncludedPackageClassifications), + ExcludedPackages: pointer.ToSliceOfStrings(l.ExcludedPackageNameMasks), + IncludedPackages: pointer.ToSliceOfStrings(l.IncludedPackageNameMasks), + }} + } + + if w := conf.Windows; w != nil { + s.Windows = []Windows{ + { + Classification: string(w.IncludedUpdateClassifications), + ExcludedKbs: pointer.ToSliceOfStrings(w.ExcludedKbNumbers), + IncludedKbs: pointer.ToSliceOfStrings(w.IncludedKbNumbers), + RebootSetting: utils.NormalizeNilableString(w.RebootSetting), + }} + } + + s.Duration = utils.NormalizeNilableString(conf.Duration) + s.VirtualMachines = pointer.ToSliceOfStrings(conf.AzureVirtualMachines) + s.NonAzureComputerNames = pointer.ToSliceOfStrings(conf.NonAzureComputerNames) + s.Targets = targetsFromSDK(conf.Targets) + } + + // service api response scheduleInfo.advancedSchedule as null, which cause import lost it + s.Schedule = scheduleFromSDK(prop.ScheduleInfo, s.Schedule) + if tasks := prop.Tasks; tasks != nil { + s.PreTask = updateTaskFromSDK(tasks.PreTask) + s.PostTask = updateTaskFromSDK(tasks.PostTask) + } +} + +type SoftwareUpdateConfigurationResource struct{} + +var _ sdk.ResourceWithUpdate = (*SoftwareUpdateConfigurationResource)(nil) + +func (m SoftwareUpdateConfigurationResource) 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, + }, + + "operating_system": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(automation.OperatingSystemTypeLinux), + string(automation.OperatingSystemTypeWindows), + }, false), + }, + + "linux": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + + "reboot": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "classification_included": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(func() (vs []string) { + for _, v := range automation.PossibleLinuxUpdateClassesValues() { + vs = append(vs, string(v)) + } + return + }(), false), + }, + + "excluded_packages": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "included_packages": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + + "windows": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + + "classification_included": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(func() (vs []string) { + for _, v := range automation.PossibleWindowsUpdateClassesValues() { + vs = append(vs, string(v)) + } + return + }(), false), + }, + + "excluded_knowledge_base_numbers": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "included_knowledge_base_numbers": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "reboot": { + Type: pluginsdk.TypeString, + Optional: true, + }, + }, + }, + }, + + "duration": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validate4.ISO8601Duration, + }, + + "virtual_machine_ids": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: computeValidate.VirtualMachineID, + }, + }, + + "non_azure_computer_names": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "target": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + + "azure_query": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "scope": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + // Subscription or Resource Group ARM Id + ValidateFunc: func(i interface{}, s string) ([]string, []error) { + w, e := validate2.ResourceGroupID(i, s) + if len(e) == 0 { + return w, e + } + w, e = validate3.SubscriptionID(i, s) + return w, e + }, + }, + }, + + "locations": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "tags": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "tag": { + Type: pluginsdk.TypeString, + Required: true, + }, + "values": { + Type: pluginsdk.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + + "tag_filter": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(automation.TagOperatorsAny), + string(automation.TagOperatorsAll), + }, false), + }, + }, + }, + }, + + "non_azure_query": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "function_alias": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "workspace_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + }, + }, + + "schedule": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "description": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "start_time": { + Type: pluginsdk.TypeString, + Optional: true, + DiffSuppressFunc: suppress.RFC3339MinuteTime, + ValidateFunc: validation.IsRFC3339Time, + }, + + "start_time_offset_minutes": { + Type: pluginsdk.TypeFloat, + Optional: true, + Computed: true, + }, + + "expiry_time": { + Type: pluginsdk.TypeString, + Optional: true, + DiffSuppressFunc: suppress.RFC3339MinuteTime, + ValidateFunc: validation.IsRFC3339Time, + }, + + "expiry_time_offset_minutes": { + Type: pluginsdk.TypeFloat, + Optional: true, + Computed: true, + }, + + "is_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + + "next_run": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + DiffSuppressFunc: suppress.RFC3339MinuteTime, + ValidateFunc: validation.IsRFC3339Time, + }, + + "next_run_offset_minutes": { + Type: pluginsdk.TypeFloat, + Optional: true, + Computed: true, + }, + + "interval": { + Type: pluginsdk.TypeInt, + Optional: true, + }, + + "frequency": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "creation_time": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "last_modified_time": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "time_zone": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "advanced_week_days": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: nil, + }, + }, + + "advanced_month_days": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeInt, + ValidateFunc: validation.IntBetween(1, 31), + }, + }, + + "monthly_occurrence": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "occurrence": { + Type: pluginsdk.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 5), + }, + + "day": { + Type: pluginsdk.TypeString, + Required: true, + // not hardcode Enum values + ValidateFunc: func(i interface{}, s string) ([]string, []error) { + var vs []string + for _, v := range automation.PossibleScheduleDayValues() { + vs = append(vs, string(v)) + } + vf := validation.StringInSlice(vs, false) + return vf(i, s) + }, + }, + }, + }, + }, + }, + }, + }, + + "pre_task": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "source": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "parameters": { + Type: pluginsdk.TypeMap, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: nil, + }, + }, + }, + }, + }, + + "post_task": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + + "source": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "parameters": { + Type: pluginsdk.TypeMap, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: nil, + }, + }, + }, + }, + }, + } +} + +func (m SoftwareUpdateConfigurationResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "error_code": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "error_meesage": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (m SoftwareUpdateConfigurationResource) ModelObject() interface{} { + return &SoftwareUpdateConfigurationModel{} +} + +func (m SoftwareUpdateConfigurationResource) ResourceType() string { + return "azurerm_automation_software_update_configuration" +} + +func (m SoftwareUpdateConfigurationResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + client := meta.Client.Automation.SoftwareUpdateConfigClient + + var model SoftwareUpdateConfigurationModel + if err := meta.Decode(&model); err != nil { + return err + } + automationID, _ := parse.AutomationAccountID(model.AutomationAccountID) + + subscriptionID := meta.Client.Account.SubscriptionId + id := parse.NewSoftwareUpdateConfigurationID(subscriptionID, automationID.ResourceGroup, automationID.Name, model.Name) + existing, err := client.GetByName(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name, "") + if !utils.ResponseWasNotFound(existing.Response) { + if err != nil { + return fmt.Errorf("retreiving %s: %v", id, err) + } + if meta.ResourceData.IsNewResource() { + return meta.ResourceRequiresImport(m.ResourceType(), id) + } + } + + param := model.ToSDKModel() + future, err := client.Create(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name, param, "") + _ = future + + if err != nil { + return fmt.Errorf("creating %s: %v", id, err) + } + + meta.SetID(id) + return nil + }, + } +} + +func (m SoftwareUpdateConfigurationResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + id, err := parse.SoftwareUpdateConfigurationID(meta.ResourceData.Id()) + if err != nil { + return err + } + client := meta.Client.Automation.SoftwareUpdateConfigClient + result, err := client.GetByName(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name, "") + if err != nil { + return err + } + var output SoftwareUpdateConfigurationModel + if err := meta.Decode(&output); err != nil { + return err + } + + output.Name = id.Name + output.AutomationAccountID = parse.NewAutomationAccountID(id.SubscriptionId, id.ResourceGroup, id.AutomationAccountName).ID() + output.LoadSDKModel(result.SoftwareUpdateConfigurationProperties) + + return meta.Encode(&output) + }, + } +} + +func (m SoftwareUpdateConfigurationResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: m.Create().Func, + Timeout: 10 * time.Minute, + } +} + +func (m SoftwareUpdateConfigurationResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + id, err := parse.SoftwareUpdateConfigurationID(meta.ResourceData.Id()) + if err != nil { + return err + } + meta.Logger.Infof("deleting %s", id) + client := meta.Client.Automation.SoftwareUpdateConfigClient + 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 SoftwareUpdateConfigurationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.SoftwareUpdateConfigurationID +} diff --git a/internal/services/automation/automation_software_update_configuration_test.go b/internal/services/automation/automation_software_update_configuration_test.go new file mode 100644 index 000000000000..1be49ec1fa3b --- /dev/null +++ b/internal/services/automation/automation_software_update_configuration_test.go @@ -0,0 +1,233 @@ +package automation_test + +import ( + "context" + "fmt" + "testing" + "time" + + "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 SoftwareUpdateConfigurationResource struct { + startTime string + expireTime string +} + +func newSoftwareUpdateConfigurationResource() SoftwareUpdateConfigurationResource { + // The start time of the schedule must be at least 5 minutes after the time you create the schedule, + // so we cannot hardcode the time string. + // we use timezone as UTC so the time string should be in UTC format + ins := SoftwareUpdateConfigurationResource{ + startTime: time.Now().Add(time.Hour * 10).In(time.UTC).Format(time.RFC3339), + expireTime: time.Now().Add(time.Hour * 50).In(time.UTC).Format(time.RFC3339), + } + return ins +} + +func (a SoftwareUpdateConfigurationResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.SoftwareUpdateConfigurationID(state.ID) + if err != nil { + return nil, err + } + resp, err := client.Automation.SoftwareUpdateConfigClient.GetByName(ctx, id.ResourceGroup, id.AutomationAccountName, id.Name, "") + if err != nil { + return nil, fmt.Errorf("retrieving Type %s: %+v", id, err) + } + return utils.Bool(resp.SoftwareUpdateConfigurationProperties != nil), nil +} + +func TestAccSoftwareUpdateConfiguration_basic(t *testing.T) { + data := acceptance.BuildTestData(t, automation.SoftwareUpdateConfigurationResource{}.ResourceType(), "test") + r := newSoftwareUpdateConfigurationResource() + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + // scheduleInfo.advancedSchedule always return null + data.ImportStep("schedule.0.advanced", "schedule.0.monthly_occurrence"), + }) +} + +func TestAccSoftwareUpdateConfiguration_update(t *testing.T) { + data := acceptance.BuildTestData(t, automation.SoftwareUpdateConfigurationResource{}.ResourceType(), "test") + r := newSoftwareUpdateConfigurationResource() + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + // scheduleInfo.advancedSchedule always return null + data.ImportStep("schedule.0.advanced", "schedule.0.monthly_occurrence"), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + // scheduleInfo.advancedSchedule always return null + data.ImportStep("schedule.0.advanced", "schedule.0.monthly_occurrence"), + }) +} + +func (a SoftwareUpdateConfigurationResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` + + +%s + +resource "azurerm_automation_software_update_configuration" "test" { + automation_account_id = azurerm_automation_account.test.id + name = "acctest-suc-%[2]d" + operating_system = "Linux" + + linux { + classification_included = "Security" + excluded_packages = ["apt"] + included_packages = ["vim"] + reboot = "IfRequired" + } + + duration = "PT1H1M1S" + virtual_machine_ids = [] + + target { + azure_query { + scope = [azurerm_resource_group.test.id] + locations = [azurerm_resource_group.test.location] + tags { + tag = "foo" + values = ["barbar2"] + } + tag_filter = "Any" + } + + non_azure_query { + function_alias = "savedSearch1" + workspace_id = azurerm_log_analytics_workspace.test.id + } + } + + schedule { + description = "foo-schedule" + start_time = "%[3]s" + expiry_time = "%[4]s" + is_enabled = true + interval = 1 + frequency = "Hour" + time_zone = "Etc/UTC" + advanced_week_days = ["Monday", "Tuesday"] + advanced_month_days = [1, 10, 15] + monthly_occurrence { + occurrence = 1 + day = "Tuesday" + } + } + + depends_on = [azurerm_log_analytics_linked_service.test] +} +`, a.template(data), data.RandomInteger, a.startTime, a.expireTime) +} + +func (a SoftwareUpdateConfigurationResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` + + +%s + +resource "azurerm_automation_software_update_configuration" "test" { + automation_account_id = azurerm_automation_account.test.id + name = "acctest-suc-%[2]d" + operating_system = "Linux" + + linux { + classification_included = "Security" + excluded_packages = ["apt"] + included_packages = ["vim"] + reboot = "IfRequired" + } + + duration = "PT2H2M2S" + virtual_machine_ids = [] + + target { + azure_query { + scope = [azurerm_resource_group.test.id] + locations = [azurerm_resource_group.test.location] + tags { + tag = "foo" + values = ["barbar2"] + } + tag_filter = "Any" + } + + non_azure_query { + function_alias = "savedSearch1" + workspace_id = azurerm_log_analytics_workspace.test.id + } + } + + schedule { + description = "foobar-schedule" + start_time = "%[3]s" + expiry_time = "%[4]s" + is_enabled = true + interval = 1 + frequency = "Hour" + time_zone = "Etc/UTC" + advanced_week_days = ["Monday", "Tuesday"] + } + + depends_on = [azurerm_log_analytics_linked_service.test] +} +`, a.template(data), data.RandomInteger, a.startTime, a.expireTime) +} + +// software update need log analytic location map correct, if use a random location like `East US` will cause +// error like `chosen Azure Automation does not have a Log Analytics workspace linked for operation to succeed`. +// so location hardcode as `West US` +// see more https://learn.microsoft.com/en-us/azure/automation/how-to/region-mappings +func (a SoftwareUpdateConfigurationResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-auto-%[1]d" + location = "West US" +} + +resource "azurerm_automation_account" "test" { + name = "acctest-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "Basic" +} + +resource "azurerm_log_analytics_workspace" "test" { + name = "acctestLAW-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku = "PerGB2018" + retention_in_days = 30 +} + +resource "azurerm_log_analytics_linked_service" "test" { + resource_group_name = azurerm_resource_group.test.name + workspace_id = azurerm_log_analytics_workspace.test.id + read_access_id = azurerm_automation_account.test.id +} +`, data.RandomInteger) +} diff --git a/internal/services/automation/client/client.go b/internal/services/automation/client/client.go index c3bdefc8b2f1..3d5a5b897a3e 100644 --- a/internal/services/automation/client/client.go +++ b/internal/services/automation/client/client.go @@ -24,6 +24,7 @@ type Client struct { RunBookWgClient *hybridrunbookworkergroup.HybridRunbookWorkerGroupClient RunbookWorkerClient *hybridrunbookworker.HybridRunbookWorkerClient ScheduleClient *automation.ScheduleClient + SoftwareUpdateConfigClient *automation.SoftwareUpdateConfigurationsClient SourceControlClient *automation.SourceControlClient VariableClient *automation.VariableClient WatcherClient *automation.WatcherClient @@ -79,6 +80,9 @@ func NewClient(o *common.ClientOptions) *Client { scheduleClient := automation.NewScheduleClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&scheduleClient.Client, o.ResourceManagerAuthorizer) + softUpClient := automation.NewSoftwareUpdateConfigurationsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&softUpClient.Client, o.ResourceManagerAuthorizer) + variableClient := automation.NewVariableClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&variableClient.Client, o.ResourceManagerAuthorizer) @@ -104,6 +108,7 @@ func NewClient(o *common.ClientOptions) *Client { RunBookWgClient: &runbookWgClient, RunbookWorkerClient: &runbookWorkerClient, ScheduleClient: &scheduleClient, + SoftwareUpdateConfigClient: &softUpClient, SourceControlClient: &sourceCtlClient, VariableClient: &variableClient, WatcherClient: &watcherClient, diff --git a/internal/services/automation/parse/software_update_configuration.go b/internal/services/automation/parse/software_update_configuration.go new file mode 100644 index 000000000000..e203f0ded313 --- /dev/null +++ b/internal/services/automation/parse/software_update_configuration.go @@ -0,0 +1,75 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type SoftwareUpdateConfigurationId struct { + SubscriptionId string + ResourceGroup string + AutomationAccountName string + Name string +} + +func NewSoftwareUpdateConfigurationID(subscriptionId, resourceGroup, automationAccountName, name string) SoftwareUpdateConfigurationId { + return SoftwareUpdateConfigurationId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + AutomationAccountName: automationAccountName, + Name: name, + } +} + +func (id SoftwareUpdateConfigurationId) String() string { + segments := []string{ + fmt.Sprintf("Name %q", id.Name), + fmt.Sprintf("Automation Account Name %q", id.AutomationAccountName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Software Update Configuration", segmentsStr) +} + +func (id SoftwareUpdateConfigurationId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Automation/automationAccounts/%s/softwareUpdateConfigurations/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.AutomationAccountName, id.Name) +} + +// SoftwareUpdateConfigurationID parses a SoftwareUpdateConfiguration ID into an SoftwareUpdateConfigurationId struct +func SoftwareUpdateConfigurationID(input string) (*SoftwareUpdateConfigurationId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := SoftwareUpdateConfigurationId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.AutomationAccountName, err = id.PopSegment("automationAccounts"); err != nil { + return nil, err + } + if resourceId.Name, err = id.PopSegment("softwareUpdateConfigurations"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/internal/services/automation/parse/software_update_configuration_test.go b/internal/services/automation/parse/software_update_configuration_test.go new file mode 100644 index 000000000000..1076d6936963 --- /dev/null +++ b/internal/services/automation/parse/software_update_configuration_test.go @@ -0,0 +1,128 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = SoftwareUpdateConfigurationId{} + +func TestSoftwareUpdateConfigurationIDFormatter(t *testing.T) { + actual := NewSoftwareUpdateConfigurationID("12345678-1234-9876-4563-123456789012", "group1", "account1", "up1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/softwareUpdateConfigurations/up1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestSoftwareUpdateConfigurationID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *SoftwareUpdateConfigurationId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing AutomationAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/", + Error: true, + }, + + { + // missing value for AutomationAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/gro", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/softwareUpdateConfigurations/up1", + Expected: &SoftwareUpdateConfigurationId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "group1", + AutomationAccountName: "account1", + Name: "up1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/GROUP1/PROVIDERS/MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/ACCOUNT1/SOFTWAREUPDATECONFIGURATIONS/UP1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := SoftwareUpdateConfigurationID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.AutomationAccountName != v.Expected.AutomationAccountName { + t.Fatalf("Expected %q but got %q for AutomationAccountName", v.Expected.AutomationAccountName, actual.AutomationAccountName) + } + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/internal/services/automation/registration.go b/internal/services/automation/registration.go index 2df2fc1ecd6a..b91fbfdb27ef 100644 --- a/internal/services/automation/registration.go +++ b/internal/services/automation/registration.go @@ -19,8 +19,9 @@ func (r Registration) Resources() []sdk.Resource { AutomationConnectionTypeResource{}, HybridRunbookWorkerGroupResource{}, HybridRunbookWorkerResource{}, - WatcherResource{}, + SoftwareUpdateConfigurationResource{}, SourceControlResource{}, + WatcherResource{}, } } diff --git a/internal/services/automation/resourceids.go b/internal/services/automation/resourceids.go index 591489b9e533..489bc0e1c03b 100644 --- a/internal/services/automation/resourceids.go +++ b/internal/services/automation/resourceids.go @@ -10,6 +10,7 @@ package automation //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Module -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/modules/module1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=NodeConfiguration -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/nodeConfigurations/nodeconfig1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Runbook -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/runbooks/runbook1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=SoftwareUpdateConfiguration -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/softwareUpdateConfigurations/up1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Configuration -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/configurations/config1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=JobSchedule -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/jobSchedules/schedule1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Variable -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/variables/variable1 diff --git a/internal/services/automation/validate/software_update_configuration_id.go b/internal/services/automation/validate/software_update_configuration_id.go new file mode 100644 index 000000000000..e53320ff1e21 --- /dev/null +++ b/internal/services/automation/validate/software_update_configuration_id.go @@ -0,0 +1,23 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/automation/parse" +) + +func SoftwareUpdateConfigurationID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.SoftwareUpdateConfigurationID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/automation/validate/software_update_configuration_id_test.go b/internal/services/automation/validate/software_update_configuration_id_test.go new file mode 100644 index 000000000000..0370c441abd8 --- /dev/null +++ b/internal/services/automation/validate/software_update_configuration_id_test.go @@ -0,0 +1,88 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestSoftwareUpdateConfigurationID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing AutomationAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/", + Valid: false, + }, + + { + // missing value for AutomationAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/", + Valid: false, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/", + Valid: false, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/gro", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/softwareUpdateConfigurations/up1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/GROUP1/PROVIDERS/MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/ACCOUNT1/SOFTWAREUPDATECONFIGURATIONS/UP1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := SoftwareUpdateConfigurationID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/internal/tf/suppress/time.go b/internal/tf/suppress/time.go index bdeae5196c10..dd591fc758ae 100644 --- a/internal/tf/suppress/time.go +++ b/internal/tf/suppress/time.go @@ -16,3 +16,14 @@ func RFC3339Time(_, old, new string, _ *schema.ResourceData) bool { return nt.Equal(ot) } + +func RFC3339MinuteTime(_, old, new string, _ *schema.ResourceData) bool { + ot, oerr := time.Parse(time.RFC3339, old) + nt, nerr := time.Parse(time.RFC3339, new) + + if oerr != nil || nerr != nil { + return false + } + + return nt.Unix()-int64(nt.Second()) == ot.Unix()-int64(ot.Second()) +} diff --git a/utils/pointer.go b/utils/pointer.go index bba617138c05..953e9832f2cd 100644 --- a/utils/pointer.go +++ b/utils/pointer.go @@ -23,3 +23,10 @@ func Float(input float64) *float64 { func String(input string) *string { return &input } + +func StringSlice(input []string) *[]string { + if input == nil { + return nil + } + return &input +} diff --git a/website/docs/r/automation_software_update_configuration.html.markdown b/website/docs/r/automation_software_update_configuration.html.markdown new file mode 100644 index 000000000000..60b8382926dd --- /dev/null +++ b/website/docs/r/automation_software_update_configuration.html.markdown @@ -0,0 +1,211 @@ +--- +subcategory: "Automation" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_automation_software_update_configuration" +description: |- + Manages an Automation Software Update Configuration. +--- + +# azurerm_automation_software_update_configuration + +Manages an Automation Software Update Configuraion. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "example-rg" + location = "East US" +} + +resource "azurerm_automation_account" "test" { + name = "example" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "Basic" +} + +resource "azurerm_automation_software_update_configuration" "example" { + name = "example" + automation_account_id = azurerm_automation_account.test.id + operating_system = "Linux" + + linux { + classification = "Security" + excluded_packages = ["apt"] + included_packages = ["vim"] + reboot = "IfRequired" + } + + duration = "PT2H2M2S" +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name which should be used for this Automation. Changing this forces a new Automation to be created. + +* `automation_account_id` (Required) The ID of Automation Account to manage this Source Control. Changing this forces a new Automation Source Control to be created. + +* `operating_system` - (Required) The Operating system of target machines. Possible values are `Windows` and `Linux`. + +--- + +* `duration` - (Optional) Maximum time allowed for the software update configuration run. using format `PT[n]H[n]M[n]S` as per ISO8601. + +* `linux` - (Optional) One or more `linux` blocks as defined below. + +* `windows` - (Optional) One or more `windows` blocks as defined below. + +* `virtual_machine_ids` - (Optional) Specifies a list of azure resource Ids of azure virtual machines. + +* `non_azure_computer_names` - (Optional) Specifies a list of names of non-azure machines for the software update configuration. + +* `target` - (Optional) One or more `target` blocks as defined below. + +* `post_task` - (Optional) One or more `post_task` blocks as defined below. + +* `pre_task` - (Optional) One or more `pre_task` blocks as defined below. + +* `schedule` - (Optional) One or more `schedule` blocks as defined below. + +--- + +A `linux` block supports the following: + +* `classification_included` - (Optional) Specifies the update classifications included in the Software Update Configuration. Possible values are `Unclassified`, `Critical`, `Security` and `Other`. + +* `excluded_packages` - (Optional) Specifies a list of packages to excluded from the Software Update Configuration. + +* `included_packages` - (Optional) Specifies a list of packages to included from the Software Update Configuration. + +* `reboot` - (Optional) Specifies the reboot settings after software update, possible values are `IfRequired`, `Never` and `Always` + +--- + +A `windows` block supports the following: + +* `classification_included` - (Optional) Specifies the update classification. Possible values are `Unclassified`, `Critical`, `Security`, `UpdateRollup`, `FeaturePack`, `ServicePack`, `Definition`, `Tools` and `Updates`. + +* `excluded_knowledge_base_numbers` - (Optional) Specifies a list of knowledge base numbers excluded. + +* `included_knowledge_base_numbers` - (Optional) Specifies a list of knowledge base numbers included. + +* `reboot` - (Optional) Specifies the reboot settings after software update, possible values are `IfRequired`, `Never` and `Always` + +--- + +A `target` block supports the following: + +* `azure_query` - (Optional) One or more `azure_query` blocks as defined above. + +* `non_azure_query` - (Optional) One or more `non_azure_query` blocks as defined above. + +--- + +A `azure_query` block supports the following: + +* `locations` - (Optional) Specifies a list of locations to scope the query to. + +* `scope` - (Optional) Specifies a list of Subscription or Resource Group ARM Ids to query. + +* `tag_filter` - (Optional) Specifies how the specified tags to filter VMs. Possible values are `Any` and `All`. + +* `tags` - (Optional) A mapping of tags used for query filter. + +--- + +A `tags` block supports the following: + +* `tag` - (Required) Specifies the name of the tag to filter. + +* `values` - (Required) Specifies a list of values for this tag key. + +--- + +A `non_azure_query` block supports the following: + +* `function_alias` - (Optional) Specifies the Log Analytics save search name. + +* `workspace_id` - (Optional) The workspace id for Log Analytics in which the saved search in. + +--- + +A `pre_task` block supports the following: + +* `parameters` - (Optional) Specifies a map of parameters for the task. + +* `source` - (Optional) The name of the runbook for the pre task. + +--- + +A `post_task` block supports the following: + +* `parameters` - (Optional) Specifies a map of parameters for the task. + +* `source` - (Optional) The name of the runbook for the post task. + +--- + +A `schedule` block supports the following: + +* `is_enabled` - (Optional) Whether the schedule is enabled. + +* `frequency` - (Required) The frequency of the schedule. - can be either `OneTime`, `Day`, `Hour`, `Week`, or `Month`. + +* `description` - (Optional) A description for this Schedule. + +* `interval` - (Optional) The number of `frequency`s between runs. Only valid when frequency is `Day`, `Hour`, `Week`, or `Month` and defaults to `1`. + +* `start_time` - (Optional) Start time of the schedule. Must be at least five minutes in the future. Defaults to seven minutes in the future from the time the resource is created. + +* `expiry_time` - (Optional) The end time of the schedule. + +* `timezone` - (Optional) The timezone of the start time. Defaults to `UTC`. For possible values see: + +* `advanced_week_days` - (Optional) List of days of the week that the job should execute on. Only valid when frequency is `Week`. + +* `advanced_month_days` - (Optional) List of days of the month that the job should execute on. Must be between `1` and `31`. `-1` for last day of the month. Only valid when frequency is `Month`. + +* `monthly_occurrence` - (Optional) List of occurrences of days within a month. Only valid when frequency is `Month`. The `monthly_occurrence` block supports fields documented below. + +--- + +The `monthly_occurrence` block supports: + +* `day` - (Required) Day of the occurrence. Must be one of `Monday`, `Tuesday`, `Wednesday`, `Thursday`, `Friday`, `Saturday`, `Sunday`. + +* `occurrence` - (Required) Occurrence of the week within the month. Must be between `1` and `5`. `-1` for last week within the month. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Automation Software Update Configuration. + +* `error_code` - The Error code when failed. + +* `error_meesage` - The Error message indicating why the operation failed. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Automation. +* `read` - (Defaults to 5 minutes) Used when retrieving the Automation. +* `update` - (Defaults to 10 minutes) Used when updating the Automation. +* `delete` - (Defaults to 10 minutes) Used when deleting the Automation. + +## Import + +Automations Software Update Configuration can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_automation_software_update_configuration.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Automation/automationAccounts/account1/softwareUpdateConfigurations/suc1 +```