diff --git a/azurerm/internal/services/loganalytics/log_analytics_linked_service.go b/azurerm/internal/services/loganalytics/log_analytics_linked_service.go new file mode 100644 index 000000000000..a7df9734e2fb --- /dev/null +++ b/azurerm/internal/services/loganalytics/log_analytics_linked_service.go @@ -0,0 +1,42 @@ +package loganalytics + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" +) + +func logAnalyticsLinkedServiceDeleteWaitForState(ctx context.Context, meta interface{}, timeout time.Duration, resourceGroup string, workspaceName string, serviceType string) *resource.StateChangeConf { + return &resource.StateChangeConf{ + Pending: []string{"Deleting"}, + Target: []string{"Deleted"}, + MinTimeout: 30 * time.Second, + Timeout: timeout, + Refresh: logAnalyticsLinkedServiceRefresh(ctx, meta, resourceGroup, workspaceName, serviceType), + } +} + +func logAnalyticsLinkedServiceRefresh(ctx context.Context, meta interface{}, resourceGroup string, workspaceName string, serviceType string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + client := meta.(*clients.Client).LogAnalytics.LinkedServicesClient + + log.Printf("[INFO] checking on state of Log Analytics Linked Service '%s/%s' (Resource Group %q)", workspaceName, serviceType, resourceGroup) + + resp, err := client.Get(ctx, resourceGroup, workspaceName, serviceType) + if err != nil { + return nil, "nil", fmt.Errorf("polling for the status of Log Analytics Linked Service '%s/%s' (Resource Group %q)", workspaceName, serviceType, resourceGroup) + } + + // (@WodansSon) - The service returns status code 200 even if the resource does not exist + // instead it returns an empty slice... + if props := resp.LinkedServiceProperties; props == nil { + return resp, "Deleted", nil + } + + return resp, "Deleting", nil + } +} diff --git a/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource.go b/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource.go index 8acd5e096bd7..618bc9140f60 100644 --- a/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource.go +++ b/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource.go @@ -3,6 +3,7 @@ package loganalytics import ( "fmt" "log" + "strings" "time" "github.com/Azure/azure-sdk-for-go/services/operationalinsights/mgmt/2020-08-01/operationalinsights" @@ -13,6 +14,7 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/loganalytics/parse" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/loganalytics/validate" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" @@ -40,29 +42,62 @@ func resourceArmLogAnalyticsLinkedService() *schema.Resource { Schema: map[string]*schema.Schema{ "resource_group_name": azure.SchemaResourceGroupNameDiffSuppress(), + // TODO: Remove in 3.0 "workspace_name": { Type: schema.TypeString, - Required: true, - ForceNew: true, + Computed: true, + Optional: true, DiffSuppressFunc: suppress.CaseDifference, ValidateFunc: validate.LogAnalyticsWorkspaceName, + ExactlyOneOf: []string{"workspace_name", "workspace_id"}, + Deprecated: "This field has been deprecated in favour of `workspace_id` and will be removed in a future version of the provider", }, + "workspace_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + DiffSuppressFunc: suppress.CaseDifference, + ValidateFunc: azure.ValidateResourceID, + ExactlyOneOf: []string{"workspace_name", "workspace_id"}, + }, + + // TODO: Remove in 3.0 "linked_service_name": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: "automation", + Type: schema.TypeString, + Computed: true, + Optional: true, + DiffSuppressFunc: suppress.CaseDifference, ValidateFunc: validation.StringInSlice([]string{ "automation", + "cluster", }, false), + Deprecated: "This field has been deprecated and will be removed in a future version of the provider", }, + // TODO: Remove in 3.0 "resource_id": { Type: schema.TypeString, - Required: true, - ForceNew: true, + Computed: true, + Optional: true, + ValidateFunc: azure.ValidateResourceID, + ExactlyOneOf: []string{"read_access_id", "write_access_id", "resource_id"}, + Deprecated: "This field has been deprecated in favour of `read_access_id` and will be removed in a future version of the provider", + }, + + "read_access_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ValidateFunc: azure.ValidateResourceID, + ExactlyOneOf: []string{"read_access_id", "write_access_id", "resource_id"}, + }, + + "write_access_id": { + Type: schema.TypeString, + Optional: true, ValidateFunc: azure.ValidateResourceID, + ExactlyOneOf: []string{"read_access_id", "write_access_id", "resource_id"}, }, // Exported properties @@ -73,25 +108,125 @@ func resourceArmLogAnalyticsLinkedService() *schema.Resource { "tags": tags.Schema(), }, + + // TODO: Remove in 3.0 + CustomizeDiff: func(d *schema.ResourceDiff, v interface{}) error { + if d.HasChange("linked_service_name") { + oldServiceName, newServiceName := d.GetChange("linked_service_name") + + // This is an unneeded field, if it is removed you can safely ignore it + // as it's value can be(and is) derived via the 'read_access_id' field. It + // is only here for backwards compatibility to avoid a breaking change + if newServiceName.(string) != "" { + // Ignore change if it's in case only + if !strings.EqualFold(oldServiceName.(string), newServiceName.(string)) { + d.ForceNew("linked_service_name") + } + } + } + + if d.HasChange("workspace_id") { + forceNew := true + _, newWorkspaceName := d.GetChange("workspace_name") + oldWorkspaceID, newWorkspaceID := d.GetChange("workspace_id") + + // If the workspcae ID has been removed, only do a force new if the new workspace name + // and the old workspace ID points to different workspaces + if oldWorkspaceID.(string) != "" && newWorkspaceName.(string) != "" && newWorkspaceID.(string) == "" { + workspace, err := parse.LogAnalyticsWorkspaceID(oldWorkspaceID.(string)) + if err == nil { + if workspace.WorkspaceName == newWorkspaceName.(string) { + forceNew = false + } + } + } + + if forceNew { + d.ForceNew("workspace_id") + } + } + + if d.HasChange("workspace_name") { + forceNew := true + oldWorkspaceName, newWorkspaceName := d.GetChange("workspace_name") + _, newWorkspaceID := d.GetChange("workspace_id") + + // If the workspcae name has been removed, only do a force new if the new workspace ID + // and the old workspace name points to different workspaces + if oldWorkspaceName.(string) != "" && newWorkspaceID.(string) != "" && newWorkspaceName.(string) == "" { + workspace, err := parse.LogAnalyticsWorkspaceID(newWorkspaceID.(string)) + if err == nil { + if workspace.WorkspaceName == oldWorkspaceName.(string) { + forceNew = false + } + } + } + + if forceNew { + d.ForceNew("workspace_name") + } + } + + return nil + }, } } func resourceArmLogAnalyticsLinkedServiceCreateUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).LogAnalytics.LinkedServicesClient + subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) defer cancel() log.Printf("[INFO] preparing arguments for AzureRM Log Analytics Linked Services creation.") - resGroup := d.Get("resource_group_name").(string) - workspaceName := d.Get("workspace_name").(string) - lsName := d.Get("linked_service_name").(string) + // TODO: Remove in 3.0 + var tmpSpace parse.LogAnalyticsWorkspaceId + var workspaceId string + + resourceGroup := d.Get("resource_group_name").(string) + readAccess := d.Get("read_access_id").(string) + writeAccess := d.Get("write_access_id").(string) + linkedServiceName := d.Get("linked_service_name").(string) + t := d.Get("tags").(map[string]interface{}) + + if resourceId := d.Get("resource_id").(string); resourceId != "" { + readAccess = resourceId + } + + if workspaceName := d.Get("workspace_name").(string); workspaceName != "" { + tmpSpace = parse.NewLogAnalyticsWorkspaceID(subscriptionId, resourceGroup, workspaceName) + workspaceId = tmpSpace.ID() + } else { + workspaceId = d.Get("workspace_id").(string) + } + + workspace, err := parse.LogAnalyticsWorkspaceID(workspaceId) + if err != nil { + return fmt.Errorf("Linked Service (Resource Group %q) unable to parse workspace id: %+v", resourceGroup, err) + } + + id := parse.NewLogAnalyticsLinkedServiceID(subscriptionId, resourceGroup, workspace.WorkspaceName, LogAnalyticsLinkedServiceType(readAccess)) + + if linkedServiceName != "" { + if !strings.EqualFold(linkedServiceName, LogAnalyticsLinkedServiceType(readAccess)) { + return fmt.Errorf("Linked Service '%s/%s' (Resource Group %q): 'linked_service_name' %q does not match expected value of %q", workspace.WorkspaceName, id.LinkedServiceName, resourceGroup, linkedServiceName, LogAnalyticsLinkedServiceType(readAccess)) + } + } + + if strings.EqualFold(id.LinkedServiceName, "Cluster") && writeAccess == "" { + return fmt.Errorf("Linked Service '%s/%s' (Resource Group %q): A linked Log Analytics Cluster requires the 'write_access_id' attribute to be set", workspace.WorkspaceName, id.LinkedServiceName, resourceGroup) + } + + if strings.EqualFold(id.LinkedServiceName, "Automation") && readAccess == "" { + return fmt.Errorf("Linked Service '%s/%s' (Resource Group %q): A linked Automation Account requires the 'read_access_id' attribute to be set", workspace.WorkspaceName, id.LinkedServiceName, resourceGroup) + } if d.IsNewResource() { - existing, err := client.Get(ctx, resGroup, workspaceName, lsName) + existing, err := client.Get(ctx, resourceGroup, workspace.WorkspaceName, id.LinkedServiceName) if err != nil { if !utils.ResponseWasNotFound(existing.Response) { - return fmt.Errorf("Error checking for presence of existing Linked Service %q (Workspace %q / Resource Group %q): %s", lsName, workspaceName, resGroup, err) + return fmt.Errorf("checking for presence of existing Linked Service '%s/%s' (Resource Group %q): %+v", workspace.WorkspaceName, id.LinkedServiceName, resourceGroup, err) } } @@ -100,35 +235,41 @@ func resourceArmLogAnalyticsLinkedServiceCreateUpdate(d *schema.ResourceData, me } } - resourceId := d.Get("resource_id").(string) - t := d.Get("tags").(map[string]interface{}) - parameters := operationalinsights.LinkedService{ - LinkedServiceProperties: &operationalinsights.LinkedServiceProperties{ - ResourceID: utils.String(resourceId), - }, - Tags: tags.Expand(t), + LinkedServiceProperties: &operationalinsights.LinkedServiceProperties{}, + Tags: tags.Expand(t), + } + + if id.LinkedServiceName == "Automation" { + parameters.LinkedServiceProperties.ResourceID = utils.String(readAccess) } - if _, err := client.CreateOrUpdate(ctx, resGroup, workspaceName, lsName, parameters); err != nil { - return fmt.Errorf("Error creating Linked Service %q (Workspace %q / Resource Group %q): %+v", lsName, workspaceName, resGroup, err) + if id.LinkedServiceName == "Cluster" { + parameters.LinkedServiceProperties.WriteAccessResourceID = utils.String(writeAccess) } - read, err := client.Get(ctx, resGroup, workspaceName, lsName) + future, err := client.CreateOrUpdate(ctx, resourceGroup, workspace.WorkspaceName, id.LinkedServiceName, parameters) if err != nil { - return fmt.Errorf("Error retrieving Linked Service %q (Worksppce %q / Resource Group %q): %+v", lsName, workspaceName, resGroup, err) + return fmt.Errorf("creating Linked Service '%s/%s' (Resource Group %q): %+v", workspace.WorkspaceName, id.LinkedServiceName, resourceGroup, err) } - if read.ID == nil { - return fmt.Errorf("Cannot read Linked Service %q (Workspace %q / Resource Group %q) ID", lsName, workspaceName, resGroup) + + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting on creating future for Linked Service '%s/%s' (Resource Group %q): %+v", workspace.WorkspaceName, id.LinkedServiceName, resourceGroup, err) + } + + _, err = client.Get(ctx, resourceGroup, workspace.WorkspaceName, id.LinkedServiceName) + if err != nil { + return fmt.Errorf("retrieving Linked Service '%s/%s' (Resource Group %q): %+v", workspace.WorkspaceName, id.LinkedServiceName, resourceGroup, err) } - d.SetId(*read.ID) + d.SetId(id.ID()) return resourceArmLogAnalyticsLinkedServiceRead(d, meta) } func resourceArmLogAnalyticsLinkedServiceRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).LogAnalytics.LinkedServicesClient + subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() @@ -137,26 +278,30 @@ func resourceArmLogAnalyticsLinkedServiceRead(d *schema.ResourceData, meta inter return err } - resGroup := id.ResourceGroup + resourceGroup := id.ResourceGroup workspaceName := id.Path["workspaces"] - lsName := id.Path["linkedservices"] + serviceType := id.Path["linkedServices"] + workspace := parse.NewLogAnalyticsWorkspaceID(subscriptionId, resourceGroup, workspaceName) - resp, err := client.Get(ctx, resGroup, workspaceName, lsName) + resp, err := client.Get(ctx, resourceGroup, workspaceName, serviceType) if err != nil { if utils.ResponseWasNotFound(resp.Response) { d.SetId("") return nil } - return fmt.Errorf("Error making Read request on AzureRM Log Analytics Linked Service '%s': %+v", lsName, err) + return fmt.Errorf("making Read request on AzureRM Log Analytics Linked Service '%s/%s' (Resource Group %q): %+v", workspace.WorkspaceName, serviceType, resourceGroup, err) } d.Set("name", resp.Name) - d.Set("resource_group_name", resGroup) + d.Set("resource_group_name", resourceGroup) + d.Set("workspace_id", workspace.ID()) d.Set("workspace_name", workspaceName) - d.Set("linked_service_name", lsName) + d.Set("linked_service_name", serviceType) if props := resp.LinkedServiceProperties; props != nil { d.Set("resource_id", props.ResourceID) + d.Set("read_access_id", props.ResourceID) + d.Set("write_access_id", props.WriteAccessResourceID) } return tags.FlattenAndSet(d, resp.Tags) @@ -172,20 +317,36 @@ func resourceArmLogAnalyticsLinkedServiceDelete(d *schema.ResourceData, meta int return err } - resGroup := id.ResourceGroup + resourceGroup := id.ResourceGroup workspaceName := id.Path["workspaces"] - lsName := id.Path["linkedservices"] + serviceType := id.Path["linkedServices"] - future, err := client.Delete(ctx, resGroup, workspaceName, lsName) + future, err := client.Delete(ctx, resourceGroup, workspaceName, serviceType) if err != nil { - return fmt.Errorf("error deleting Log Analytics Linked Service %q (Workspace %q / Resource Group %q): %+v", lsName, workspaceName, resGroup, err) + return fmt.Errorf("deleting Log Analytics Linked Service '%s/%s' (Resource Group %q): %+v", workspaceName, serviceType, resourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { if !response.WasNotFound(future.Response()) { - return fmt.Errorf("waiting for deletion of Log Analytics Linked Service %q (Workspace %q / Resource Group %q): %+v", lsName, workspaceName, resGroup, err) + return fmt.Errorf("waiting for deletion of Log Analytics Linked Service '%s/%s' (Resource Group %q): %+v", workspaceName, serviceType, resourceGroup, err) } } + // (@WodansSon) - This is a bug in the service API, it returns instantly from the delete call with a 200 + // so we must wait for the state to change before we return from the delete function + deleteWait := logAnalyticsLinkedServiceDeleteWaitForState(ctx, meta, d.Timeout(schema.TimeoutDelete), resourceGroup, workspaceName, serviceType) + + if _, err := deleteWait.WaitForState(); err != nil { + return fmt.Errorf("waiting for Log Analytics Cluster to finish deleting '%s/%s' (Resource Group %q): %+v", workspaceName, serviceType, resourceGroup, err) + } + return nil } + +func LogAnalyticsLinkedServiceType(readAccessId string) string { + if readAccessId != "" { + return "Automation" + } + + return "Cluster" +} diff --git a/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource_test.go b/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource_test.go index e2e5790724f8..6d484e717be4 100644 --- a/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource_test.go +++ b/azurerm/internal/services/loganalytics/log_analytics_linked_service_resource_test.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/terraform" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/loganalytics" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/loganalytics/parse" ) func TestAccAzureRMLogAnalyticsLinkedService_basic(t *testing.T) { @@ -23,9 +25,7 @@ func TestAccAzureRMLogAnalyticsLinkedService_basic(t *testing.T) { Config: testAccAzureRMLogAnalyticsLinkedService_basic(data), Check: resource.ComposeTestCheckFunc( testCheckAzureRMLogAnalyticsLinkedServiceExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "name", fmt.Sprintf("acctestlaw-%d/Automation", data.RandomInteger)), - resource.TestCheckResourceAttr(data.ResourceName, "workspace_name", fmt.Sprintf("acctestlaw-%d", data.RandomInteger)), - resource.TestCheckResourceAttr(data.ResourceName, "linked_service_name", "automation"), + resource.TestCheckResourceAttr(data.ResourceName, "name", fmt.Sprintf("acctestLAW-%d/Automation", data.RandomInteger)), ), }, data.ImportStep(), @@ -45,9 +45,7 @@ func TestAccAzureRMLogAnalyticsLinkedService_requiresImport(t *testing.T) { Config: testAccAzureRMLogAnalyticsLinkedService_basic(data), Check: resource.ComposeTestCheckFunc( testCheckAzureRMLogAnalyticsLinkedServiceExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "name", fmt.Sprintf("acctestlaw-%d/Automation", data.RandomInteger)), - resource.TestCheckResourceAttr(data.ResourceName, "workspace_name", fmt.Sprintf("acctestlaw-%d", data.RandomInteger)), - resource.TestCheckResourceAttr(data.ResourceName, "linked_service_name", "automation"), + resource.TestCheckResourceAttr(data.ResourceName, "name", fmt.Sprintf("acctestLAW-%d/Automation", data.RandomInteger)), ), }, { @@ -70,7 +68,45 @@ func TestAccAzureRMLogAnalyticsLinkedService_complete(t *testing.T) { Config: testAccAzureRMLogAnalyticsLinkedService_complete(data), Check: resource.ComposeTestCheckFunc( testCheckAzureRMLogAnalyticsLinkedServiceExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "linked_service_name", "automation"), + ), + }, + data.ImportStep(), + }, + }) +} + +// TODO: Remove in 3.0 +func TestAccAzureRMLogAnalyticsLinkedService_legacy(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_log_analytics_linked_service", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMLogAnalyticsLinkedServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMLogAnalyticsLinkedService_legacy(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMLogAnalyticsLinkedServiceExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMLogAnalyticsLinkedService_withWriteAccessResourceId(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_log_analytics_linked_service", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMLogAnalyticsLinkedServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMLogAnalyticsLinkedService_withWriteAccessResourceId(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMLogAnalyticsLinkedServiceExists(data.ResourceName), ), }, data.ImportStep(), @@ -88,10 +124,16 @@ func testCheckAzureRMLogAnalyticsLinkedServiceDestroy(s *terraform.State) error } resourceGroup := rs.Primary.Attributes["resource_group_name"] - workspaceName := rs.Primary.Attributes["workspace_name"] - lsName := rs.Primary.Attributes["linked_service_name"] + workspaceId := rs.Primary.Attributes["workspace_id"] + readAccess := rs.Primary.Attributes["read_access_id"] + + workspace, err := parse.LogAnalyticsWorkspaceID(workspaceId) + if err != nil { + return fmt.Errorf("Bad: Log Analytics Linked Service Destroy unable to parse workspace id: %+v", err) + } + + resp, err := conn.Get(ctx, resourceGroup, workspace.WorkspaceName, loganalytics.LogAnalyticsLinkedServiceType(readAccess)) - resp, err := conn.Get(ctx, resourceGroup, workspaceName, lsName) if err != nil { return nil } @@ -118,22 +160,28 @@ func testCheckAzureRMLogAnalyticsLinkedServiceExists(resourceName string) resour return fmt.Errorf("Not found: %s", resourceName) } + // TODO: Legacy backwards compat It May only have a workspace Name resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] - workspaceName := rs.Primary.Attributes["workspace_name"] - lsName := rs.Primary.Attributes["linked_service_name"] - name := rs.Primary.Attributes["name"] + workspaceId := rs.Primary.Attributes["workspace_id"] + readAccessId := rs.Primary.Attributes["read_access_id"] + serviceType := loganalytics.LogAnalyticsLinkedServiceType(readAccessId) + + workspace, err := parse.LogAnalyticsWorkspaceID(workspaceId) + if err != nil { + return fmt.Errorf("Bad: Log Analytics Linked Service Exists: %+v", err) + } if !hasResourceGroup { - return fmt.Errorf("Bad: no resource group found in state for Log Analytics Linked Service: '%s'", name) + return fmt.Errorf("Bad: no resource group found in state for Log Analytics Linked Service: '%q/%q'", workspace.WorkspaceName, serviceType) } - resp, err := conn.Get(ctx, resourceGroup, workspaceName, lsName) + resp, err := conn.Get(ctx, resourceGroup, workspace.WorkspaceName, serviceType) if err != nil { return fmt.Errorf("Bad: Get on Log Analytics Linked Service Client: %+v", err) } if resp.StatusCode == http.StatusNotFound { - return fmt.Errorf("Bad: Log Analytics Linked Service '%s' (resource group: '%s') does not exist", name, resourceGroup) + return fmt.Errorf("Bad: Log Analytics Linked Service '%q/%q' (resource group: '%s') does not exist", workspace.WorkspaceName, serviceType, resourceGroup) } return nil @@ -147,8 +195,8 @@ func testAccAzureRMLogAnalyticsLinkedService_basic(data acceptance.TestData) str resource "azurerm_log_analytics_linked_service" "test" { resource_group_name = azurerm_resource_group.test.name - workspace_name = azurerm_log_analytics_workspace.test.name - resource_id = azurerm_automation_account.test.id + workspace_id = azurerm_log_analytics_workspace.test.id + read_access_id = azurerm_automation_account.test.id } `, template) } @@ -160,8 +208,8 @@ func testAccAzureRMLogAnalyticsLinkedService_requiresImport(data acceptance.Test resource "azurerm_log_analytics_linked_service" "import" { resource_group_name = azurerm_log_analytics_linked_service.test.resource_group_name - workspace_name = azurerm_log_analytics_linked_service.test.workspace_name - resource_id = azurerm_log_analytics_linked_service.test.resource_id + workspace_id = azurerm_log_analytics_linked_service.test.workspace_id + read_access_id = azurerm_log_analytics_linked_service.test.read_access_id } `, template) } @@ -171,6 +219,20 @@ func testAccAzureRMLogAnalyticsLinkedService_complete(data acceptance.TestData) return fmt.Sprintf(` %s +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 +} +`, template) +} + +// TODO: Remove in 3.0 +func testAccAzureRMLogAnalyticsLinkedService_legacy(data acceptance.TestData) string { + template := testAccAzureRMLogAnalyticsLinkedService_template(data) + return fmt.Sprintf(` +%s + resource "azurerm_log_analytics_linked_service" "test" { resource_group_name = azurerm_resource_group.test.name workspace_name = azurerm_log_analytics_workspace.test.name @@ -187,7 +249,7 @@ provider "azurerm" { } resource "azurerm_resource_group" "test" { - name = "acctestRG-%d" + name = "acctestRG-la-%d" location = "%s" } @@ -212,3 +274,26 @@ resource "azurerm_log_analytics_workspace" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) } + +func testAccAzureRMLogAnalyticsLinkedService_withWriteAccessResourceId(data acceptance.TestData) string { + template := testAccAzureRMLogAnalyticsLinkedService_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_log_analytics_cluster" "test" { + name = "acctest-LA-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_log_analytics_linked_service" "test" { + resource_group_name = azurerm_resource_group.test.name + workspace_id = azurerm_log_analytics_workspace.test.id + write_access_id = azurerm_log_analytics_cluster.test.id +} +`, template, data.RandomInteger) +} diff --git a/azurerm/internal/services/loganalytics/log_analytics_workspace_resource.go b/azurerm/internal/services/loganalytics/log_analytics_workspace_resource.go index 2e77c2c2a560..e34fe0351393 100644 --- a/azurerm/internal/services/loganalytics/log_analytics_workspace_resource.go +++ b/azurerm/internal/services/loganalytics/log_analytics_workspace_resource.go @@ -83,7 +83,7 @@ func resourceArmLogAnalyticsWorkspace() *schema.Resource { string(operationalinsights.WorkspaceSkuNameEnumStandard), "Unlimited", // TODO check if this is actually no longer valid, removed in v28.0.0 of the SDK }, true), - DiffSuppressFunc: suppress.CaseDifference, + DiffSuppressFunc: logAnalyticsLinkedServiceSkuChangeCaseDifference, }, "retention_in_days": { @@ -159,6 +159,20 @@ func resourceArmLogAnalyticsWorkspaceCreateUpdate(d *schema.ResourceData, meta i Name: operationalinsights.WorkspaceSkuNameEnum(skuName), } + // (@WodansSon) - If the workspace is connected to a cluster via the linked service resource + // the workspace cannot be modified since the linked service changes the sku value within + // the workspace + if !d.IsNewResource() { + resp, err := client.Get(ctx, id.ResourceGroup, id.WorkspaceName) + if err == nil { + if azSku := resp.Sku; azSku != nil { + if strings.EqualFold(string(azSku.Name), "lacluster") { + return fmt.Errorf("Log Analytics Workspace %q (Resource Group %q): cannot be modified while it is connected to a Log Analytics cluster", name, resourceGroup) + } + } + } + } + internetIngestionEnabled := operationalinsights.Disabled if d.Get("internet_ingestion_enabled").(bool) { internetIngestionEnabled = operationalinsights.Enabled @@ -293,3 +307,14 @@ func dailyQuotaGbDiffSuppressFunc(_, _, _ string, d *schema.ResourceData) bool { return false } + +func logAnalyticsLinkedServiceSkuChangeCaseDifference(k, old, new string, d *schema.ResourceData) bool { + // (@WodansSon) - This is needed because if you connect your workspace to a log analytics linked service resource it + // will modify the value of your sku to "lacluster". We are currently in negotiations with the service team to + // see if there is another way of doing this, for now this is the workaround + if old == "lacluster" { + old = new + } + + return suppress.CaseDifference(k, old, new, d) +} diff --git a/website/docs/r/log_analytics_linked_service.html.markdown b/website/docs/r/log_analytics_linked_service.html.markdown index 46d70db20582..004c4617dcb6 100644 --- a/website/docs/r/log_analytics_linked_service.html.markdown +++ b/website/docs/r/log_analytics_linked_service.html.markdown @@ -3,12 +3,12 @@ subcategory: "Log Analytics" layout: "azurerm" page_title: "Azure Resource Manager: azurerm_log_analytics_linked_service" description: |- - Manages a Log Analytics (formally Operational Insights) Linked Service. + Manages a Log Analytics Linked Service. --- # azurerm_log_analytics_linked_service -Links a Log Analytics (formally Operational Insights) Workspace to another resource. The (currently) only linkable service is an Azure Automation Account. +Manages a Log Analytics Linked Service. ## Example Usage @@ -39,8 +39,8 @@ resource "azurerm_log_analytics_workspace" "example" { resource "azurerm_log_analytics_linked_service" "example" { resource_group_name = azurerm_resource_group.example.name - workspace_name = azurerm_log_analytics_workspace.example.name - resource_id = azurerm_automation_account.example.id + workspace_id = azurerm_log_analytics_workspace.example.id + read_access_id = azurerm_automation_account.example.id } ``` @@ -50,11 +50,19 @@ The following arguments are supported: * `resource_group_name` - (Required) The name of the resource group in which the Log Analytics Linked Service is created. Changing this forces a new resource to be created. -* `workspace_name` - (Required) Name of the Log Analytics Workspace that will contain the linkedServices resource. Changing this forces a new resource to be created. +* `workspace_name` - (Deprecated) The name of the Log Analytics Workspace that will contain the Log Analytics Linked Service resource. Changing this forces a new resource to be created. -* `linked_service_name` - (Optional) Name of the type of linkedServices resource to connect to the Log Analytics Workspace specified in `workspace_name`. Currently it defaults to and only supports `automation` as a value. Changing this forces a new resource to be created. +* `workspace_id` - (Required) The ID of the Log Analytics Workspace that will contain the Log Analytics Linked Service resource. Changing this forces a new resource to be created. -* `resource_id` - (Required) The ID of the Resource that will be linked to the workspace. Changing this forces a new resource to be created. +* `linked_service_name` - (Deprecated) Name of the type of linkedServices resource to connect to the Log Analytics Workspace specified in workspace_name. Accepted values are `automation` and `cluster`. Defaults to `automation`. Changing this forces a new resource to be created. + +* `resource_id` - (Deprecated) The ID of the Resource that will be linked to the workspace. This should be used for linking to an Automation Account resource. + +* `read_access_id` - (Optional) The ID of the readable Resource that will be linked to the workspace. This should be used for linking to an Automation Account resource. + +* `write_access_id` - (Optional) The ID of the writable Resource that will be linked to the workspace. This should be used for linking to a Log Analytics Cluster resource. + +~> **NOTE:** You must define at least one of the above access resource id attributes (e.g. `read_access_resource_id`/`resouce_id` or `write_access_resource_id`). * `tags` - (Optional) A mapping of tags to assign to the resource. @@ -64,7 +72,7 @@ The following attributes are exported: * `id` - The Log Analytics Linked Service ID. -* `name` - The automatically generated name of the Linked Service. This cannot be specified. The format is always `/` e.g. `workspace1/Automation` +* `name` - The generated name of the Linked Service. The format for this attribute is always `/`(e.g. `workspace1/Automation` or `workspace1/Cluster`) ## Timeouts @@ -80,5 +88,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/d Log Analytics Workspaces can be imported using the `resource id`, e.g. ```shell -terraform import azurerm_log_analytics_linked_service.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/linkedservices/automation +terraform import azurerm_log_analytics_linked_service.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/linkedServices/Automation ``` diff --git a/website/docs/r/log_analytics_workspace.html.markdown b/website/docs/r/log_analytics_workspace.html.markdown index dcbbcaace6d4..bb41cb16e2ad 100644 --- a/website/docs/r/log_analytics_workspace.html.markdown +++ b/website/docs/r/log_analytics_workspace.html.markdown @@ -55,6 +55,8 @@ The following arguments are supported: * `tags` - (Optional) A mapping of tags to assign to the resource. +~> **NOTE:** If a `azurerm_log_analytics_workspace` is connected to a `azurerm_log_analytics_cluster` via a `azurerm_log_analytics_linked_service` it will not be able to be modified until link between the workspace and the cluster has been broken by deleting the `azurerm_log_analytics_linked_service` resource. + ## Attributes Reference The following attributes are exported: