diff --git a/internal/services/loganalytics/log_analytics_solution_resource.go b/internal/services/loganalytics/log_analytics_solution_resource.go index 5ae195a14a80..cd395f520ecc 100644 --- a/internal/services/loganalytics/log_analytics_solution_resource.go +++ b/internal/services/loganalytics/log_analytics_solution_resource.go @@ -4,255 +4,324 @@ package loganalytics import ( + "context" "fmt" - "log" "strings" "time" + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" "github.com/hashicorp/go-azure-sdk/resource-manager/operationalinsights/2020-08-01/workspaces" "github.com/hashicorp/go-azure-sdk/resource-manager/operationsmanagement/2015-11-01-preview/solution" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" - "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/loganalytics/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/loganalytics/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tags" "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/internal/timeouts" "github.com/hashicorp/terraform-provider-azurerm/utils" ) -func resourceLogAnalyticsSolution() *pluginsdk.Resource { - return &pluginsdk.Resource{ - Create: resourceLogAnalyticsSolutionCreateUpdate, - Read: resourceLogAnalyticsSolutionRead, - Update: resourceLogAnalyticsSolutionCreateUpdate, - Delete: resourceLogAnalyticsSolutionDelete, - Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { - _, err := solution.ParseSolutionID(id) - return err - }), - - Timeouts: &pluginsdk.ResourceTimeout{ - Create: pluginsdk.DefaultTimeout(30 * time.Minute), - Read: pluginsdk.DefaultTimeout(5 * time.Minute), - Update: pluginsdk.DefaultTimeout(30 * time.Minute), - Delete: pluginsdk.DefaultTimeout(30 * time.Minute), - }, +type LogAnalyticsSolutionResource struct{} +func (s LogAnalyticsSolutionResource) StateUpgraders() sdk.StateUpgradeData { + return sdk.StateUpgradeData{ SchemaVersion: 1, - StateUpgraders: pluginsdk.StateUpgrades(map[int]pluginsdk.StateUpgrade{ + Upgraders: map[int]pluginsdk.StateUpgrade{ 0: migration.SolutionV0ToV1{}, - }), - - Schema: map[string]*pluginsdk.Schema{ - "solution_name": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringIsNotEmpty, - }, + }, + } +} - "workspace_name": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validate.LogAnalyticsWorkspaceName, - }, +type SolutionResourceModel struct { + SolutionName string `tfschema:"solution_name"` + WorkspaceName string `tfschema:"workspace_name"` + WorkspaceResourceId string `tfschema:"workspace_resource_id"` + Location string `tfschema:"location"` + ResourceGroupName string `tfschema:"resource_group_name"` + SolutionPlan []SolutionPlanModel `tfschema:"plan"` + Tags map[string]string `tfschema:"tags"` +} - "workspace_resource_id": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - DiffSuppressFunc: suppress.CaseDifference, - }, +type SolutionPlanModel struct { + Name string `tfschema:"name"` + Publisher string `tfschema:"publisher"` + PromotionCode string `tfschema:"promotion_code"` + Product string `tfschema:"product"` +} + +var _ sdk.ResourceWithUpdate = LogAnalyticsSolutionResource{} +var _ sdk.ResourceWithStateMigration = LogAnalyticsSolutionResource{} + +func (s LogAnalyticsSolutionResource) ModelObject() interface{} { + return &SolutionResourceModel{} +} + +func (s LogAnalyticsSolutionResource) ResourceType() string { + return "azurerm_log_analytics_solution" +} + +func (s LogAnalyticsSolutionResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return solution.ValidateSolutionID +} + +func (s LogAnalyticsSolutionResource) Arguments() map[string]*schema.Schema { + return map[string]*pluginsdk.Schema{ + "solution_name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "workspace_name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.LogAnalyticsWorkspaceName, + }, + + "workspace_resource_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: suppress.CaseDifference, + }, + + "location": commonschema.Location(), - "location": commonschema.Location(), - - "resource_group_name": azure.SchemaResourceGroupNameDiffSuppress(), - - "plan": { - Type: pluginsdk.TypeList, - Required: true, - MaxItems: 1, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "name": { - Type: pluginsdk.TypeString, - Computed: true, - }, - "publisher": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - }, - "promotion_code": { - Type: pluginsdk.TypeString, - Optional: true, - ForceNew: true, - }, - "product": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - }, + "resource_group_name": azure.SchemaResourceGroupNameDiffSuppress(), + + "plan": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "publisher": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + "promotion_code": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + }, + "product": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, }, }, }, - - "tags": tags.Schema(), }, + + "tags": tags.Schema(), } } -func resourceLogAnalyticsSolutionCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { - client := meta.(*clients.Client).LogAnalytics.SolutionsClient - subscriptionId := meta.(*clients.Client).Account.SubscriptionId - ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) - defer cancel() - log.Printf("[INFO] preparing arguments for Log Analytics Solution creation.") +func (s LogAnalyticsSolutionResource) Attributes() map[string]*schema.Schema { + return map[string]*pluginsdk.Schema{} +} +func (s LogAnalyticsSolutionResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.LogAnalytics.SolutionsClient + subscriptionId := metadata.Client.Account.SubscriptionId + + var config SolutionResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding %v", err) + } + + // The resource requires both .name and .plan.name are set in the format + // "SolutionName(WorkspaceName)". Feedback will be submitted to the OMS team as IMO this isn't ideal. + id := solution.NewSolutionID(subscriptionId, config.ResourceGroupName, fmt.Sprintf("%s(%s)", config.SolutionName, config.WorkspaceName)) - // The resource requires both .name and .plan.name are set in the format - // "SolutionName(WorkspaceName)". Feedback will be submitted to the OMS team as IMO this isn't ideal. - id := solution.NewSolutionID(subscriptionId, d.Get("resource_group_name").(string), fmt.Sprintf("%s(%s)", d.Get("solution_name").(string), d.Get("workspace_name").(string))) + existing, err := client.Get(ctx, id) + if err != nil { + if !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %s", id, err) + } + } - if d.IsNewResource() { - existing, err := client.Get(ctx, id) - if err != nil { if !response.WasNotFound(existing.HttpResponse) { - return fmt.Errorf("checking for presence of existing %s: %s", id, err) + return tf.ImportAsExistsError("azurerm_log_analytics_solution", id.ID()) } - } - if !response.WasNotFound(existing.HttpResponse) { - return tf.ImportAsExistsError("azurerm_log_analytics_solution", id.ID()) - } - } + workspaceID, err := workspaces.ParseWorkspaceID(config.WorkspaceResourceId) + if err != nil { + return err + } - solutionPlan := expandAzureRmLogAnalyticsSolutionPlan(d) - solutionPlan.Name = &id.SolutionName + parameters := solution.Solution{ + Name: pointer.To(id.SolutionName), + Location: pointer.To(azure.NormalizeLocation(config.Location)), + Properties: &solution.SolutionProperties{ + WorkspaceResourceId: workspaceID.ID(), + }, + Tags: pointer.To(config.Tags), + } - location := azure.NormalizeLocation(d.Get("location").(string)) - workspaceID, err := workspaces.ParseWorkspaceID(d.Get("workspace_resource_id").(string)) - if err != nil { - return err - } + if len(config.SolutionPlan) > 0 { + solutionPlan := expandAzureRmLogAnalyticsSolutionPlan(config.SolutionPlan) + solutionPlan.Name = &id.SolutionName + parameters.Plan = &solutionPlan + } - parameters := solution.Solution{ - Name: utils.String(id.SolutionName), - Location: utils.String(location), - Plan: &solutionPlan, - Properties: &solution.SolutionProperties{ - WorkspaceResourceId: workspaceID.ID(), + err = client.CreateOrUpdateThenPoll(ctx, id, parameters) + if err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + + return nil }, - Tags: expandTags(d.Get("tags").(map[string]interface{})), } +} - err = client.CreateOrUpdateThenPoll(ctx, id, parameters) - if err != nil { - return fmt.Errorf("creating/updating %s: %+v", id, err) - } +func (s LogAnalyticsSolutionResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.LogAnalytics.SolutionsClient - d.SetId(id.ID()) + id, err := solution.ParseSolutionID(metadata.ResourceData.Id()) + if err != nil { + return err + } - return resourceLogAnalyticsSolutionRead(d, meta) -} + resp, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", metadata.ResourceData.Id(), err) + } + + state := SolutionResourceModel{ + ResourceGroupName: id.ResourceGroupName, + } + + if model := resp.Model; model != nil { + if location := model.Location; location != nil { + state.Location = azure.NormalizeLocation(*location) + } -func resourceLogAnalyticsSolutionRead(d *pluginsdk.ResourceData, meta interface{}) error { - client := meta.(*clients.Client).LogAnalytics.SolutionsClient - ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) - defer cancel() - id, err := solution.ParseSolutionID(d.Id()) - if err != nil { - return err + // Reversing the mapping used to get .solution_name + // expecting resp.Name to be in format "SolutionName(WorkspaceName)". + if v := model.Name; v != nil { + val := pointer.From(v) + segments := strings.Split(val, "(") + if len(segments) != 2 { + return fmt.Errorf("expected %q to match 'Solution(WorkspaceName)'", val) + } + + solutionName := segments[0] + workspaceName := strings.TrimSuffix(segments[1], ")") + state.SolutionName = solutionName + state.WorkspaceName = workspaceName + } + + if props := model.Properties; props != nil { + var workspaceId string + if props.WorkspaceResourceId != "" { + id, err := workspaces.ParseWorkspaceIDInsensitively(props.WorkspaceResourceId) + if err != nil { + return err + } + workspaceId = id.ID() + } + state.WorkspaceResourceId = workspaceId + } + + if plan := model.Plan; plan != nil { + state.SolutionPlan = flattenAzureRmLogAnalyticsSolutionPlan(plan) + } + + state.Tags = pointer.From(model.Tags) + + } + + return metadata.Encode(&state) + }, } +} + +func (s LogAnalyticsSolutionResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.LogAnalytics.SolutionsClient + + id, err := solution.ParseSolutionID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + err = client.DeleteThenPoll(ctx, *id) + if err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } - resp, err := client.Get(ctx, *id) - if err != nil { - if response.WasNotFound(resp.HttpResponse) { - d.SetId("") return nil - } - return fmt.Errorf("making Read request on %s: %+v", *id, err) + }, } +} + +func (s LogAnalyticsSolutionResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.LogAnalytics.SolutionsClient - if model := resp.Model; model != nil { - if model.Plan == nil { - return fmt.Errorf("making Read request on %s: Plan was nil", *id) - } - - d.Set("resource_group_name", id.ResourceGroupName) - if location := model.Location; location != nil { - d.Set("location", azure.NormalizeLocation(*location)) - } - - // Reversing the mapping used to get .solution_name - // expecting resp.Name to be in format "SolutionName(WorkspaceName)". - if v := model.Name; v != nil { - val := *v - segments := strings.Split(*v, "(") - if len(segments) != 2 { - return fmt.Errorf("expected %q to match 'Solution(WorkspaceName)'", val) + id, err := solution.ParseSolutionID(metadata.ResourceData.Id()) + if err != nil { + return err } - solutionName := segments[0] - workspaceName := strings.TrimSuffix(segments[1], ")") - d.Set("solution_name", solutionName) - d.Set("workspace_name", workspaceName) - } - - if props := model.Properties; props != nil { - var workspaceId string - if props.WorkspaceResourceId != "" { - id, err := workspaces.ParseWorkspaceIDInsensitively(props.WorkspaceResourceId) - if err != nil { - return err - } - workspaceId = id.ID() + var config SolutionResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding %v", err) } - d.Set("workspace_resource_id", workspaceId) - } - if err := d.Set("plan", flattenAzureRmLogAnalyticsSolutionPlan(model.Plan)); err != nil { - return fmt.Errorf("setting `plan`: %+v", err) - } + payload := solution.SolutionPatch{} - if err = tags.FlattenAndSet(d, flattenTags(model.Tags)); err != nil { - return err - } - } - return nil -} + if metadata.ResourceData.HasChange("tags") { + payload.Tags = pointer.To(config.Tags) + } -func resourceLogAnalyticsSolutionDelete(d *pluginsdk.ResourceData, meta interface{}) error { - client := meta.(*clients.Client).LogAnalytics.SolutionsClient - ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) - defer cancel() - id, err := solution.ParseSolutionID(d.Id()) - if err != nil { - return err - } + if err := client.UpdateThenPoll(ctx, *id, payload); err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } - err = client.DeleteThenPoll(ctx, *id) - if err != nil { - return fmt.Errorf("deleting %s: %+v", *id, err) + return nil + }, } - - return nil } -func expandAzureRmLogAnalyticsSolutionPlan(d *pluginsdk.ResourceData) solution.SolutionPlan { - plans := d.Get("plan").([]interface{}) - plan := plans[0].(map[string]interface{}) +func expandAzureRmLogAnalyticsSolutionPlan(plans []SolutionPlanModel) solution.SolutionPlan { + if len(plans) == 0 { + return solution.SolutionPlan{} + } - name := plan["name"].(string) - publisher := plan["publisher"].(string) - promotionCode := plan["promotion_code"].(string) - product := plan["product"].(string) + plan := plans[0] + name := plan.Name + publisher := plan.Publisher + promotionCode := plan.PromotionCode + product := plan.Product expandedPlan := solution.SolutionPlan{ Name: utils.String(name), @@ -264,29 +333,18 @@ func expandAzureRmLogAnalyticsSolutionPlan(d *pluginsdk.ResourceData) solution.S return expandedPlan } -func flattenAzureRmLogAnalyticsSolutionPlan(input *solution.SolutionPlan) []interface{} { - output := make([]interface{}, 0) +func flattenAzureRmLogAnalyticsSolutionPlan(input *solution.SolutionPlan) []SolutionPlanModel { + output := make([]SolutionPlanModel, 0) if input == nil { return output } - values := make(map[string]interface{}) - - if input.Name != nil { - values["name"] = *input.Name - } + plan := SolutionPlanModel{} - if input.Product != nil { - values["product"] = *input.Product - } - - if input.PromotionCode != nil { - values["promotion_code"] = *input.PromotionCode - } - - if input.Publisher != nil { - values["publisher"] = *input.Publisher - } + plan.Name = pointer.From(input.Name) + plan.Product = pointer.From(input.Product) + plan.PromotionCode = pointer.From(input.PromotionCode) + plan.Publisher = pointer.From(input.Publisher) - return append(output, values) + return append(output, plan) } diff --git a/internal/services/loganalytics/log_analytics_solution_resource_test.go b/internal/services/loganalytics/log_analytics_solution_resource_test.go index a9fc439229a2..d8b1cd4e16d1 100644 --- a/internal/services/loganalytics/log_analytics_solution_resource_test.go +++ b/internal/services/loganalytics/log_analytics_solution_resource_test.go @@ -33,6 +33,28 @@ func TestAccLogAnalyticsSolution_basicContainerMonitoring(t *testing.T) { }) } +func TestAccLogAnalyticsSolution_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_log_analytics_solution", "test") + r := LogAnalyticsSolutionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.containerMonitoring(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccLogAnalyticsSolution_requiresImport(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_log_analytics_solution", "test") r := LogAnalyticsSolutionResource{} @@ -117,6 +139,43 @@ resource "azurerm_log_analytics_solution" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } +func (LogAnalyticsSolutionResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_log_analytics_workspace" "test" { + name = "acctestLAW-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku = "PerGB2018" +} + +resource "azurerm_log_analytics_solution" "test" { + solution_name = "ContainerInsights" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + workspace_resource_id = azurerm_log_analytics_workspace.test.id + workspace_name = azurerm_log_analytics_workspace.test.name + + plan { + publisher = "Microsoft" + product = "OMSGallery/ContainerInsights" + } + + tags = { + Environment = "Test2" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + func (r LogAnalyticsSolutionResource) requiresImport(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/internal/services/loganalytics/registration.go b/internal/services/loganalytics/registration.go index 7e9f2c54840c..dcbd895f49bb 100644 --- a/internal/services/loganalytics/registration.go +++ b/internal/services/loganalytics/registration.go @@ -27,6 +27,7 @@ func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ LogAnalyticsQueryPackResource{}, LogAnalyticsQueryPackQueryResource{}, + LogAnalyticsSolutionResource{}, } } @@ -60,7 +61,6 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { "azurerm_log_analytics_linked_service": resourceLogAnalyticsLinkedService(), "azurerm_log_analytics_linked_storage_account": resourceLogAnalyticsLinkedStorageAccount(), "azurerm_log_analytics_saved_search": resourceLogAnalyticsSavedSearch(), - "azurerm_log_analytics_solution": resourceLogAnalyticsSolution(), "azurerm_log_analytics_storage_insights": resourceLogAnalyticsStorageInsights(), "azurerm_log_analytics_workspace": resourceLogAnalyticsWorkspace(), }