Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_log_analytics_linked_service - Add new fields #9410

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b52abf9
first commit and wait clusters ready
dw511214992 Oct 27, 2020
a132f01
Merge branch 'master' into add-fields-to-loganalytics-linked-service
dw511214992 Nov 16, 2020
b5c8205
merge master
dw511214992 Nov 16, 2020
31b0bad
merge master
dw511214992 Nov 17, 2020
ebd7bf9
revert client.go
dw511214992 Nov 17, 2020
474d991
Normalize workspace checks and schema updates
WodansSon Nov 18, 2020
82ea076
Update test with new schema
WodansSon Nov 18, 2020
837628c
Update documentation to match schema update
WodansSon Nov 18, 2020
3395dac
Added LF for code readability
WodansSon Nov 18, 2020
4f3b781
Merge pull request #1 from terraform-providers/master
dw511214992 Nov 19, 2020
f822cd7
Normalize ID casing
WodansSon Nov 20, 2020
86329d1
Test cases for LogAnalyticsLinkedServiceId
WodansSon Nov 20, 2020
7d01f03
Removed unneeded attribute linked_service_type
WodansSon Nov 20, 2020
b18d79a
Workaround for link service changing SKU
WodansSon Nov 20, 2020
581ce18
Delete wait state and block workspace modification
WodansSon Nov 21, 2020
2e26393
Update docs
WodansSon Nov 21, 2020
b3804e6
Split out by type
WodansSon Dec 3, 2020
961ff2b
Remove redundant code and update workspace name
WodansSon Dec 3, 2020
88b768c
Adding removed fields back into schema
WodansSon Dec 16, 2020
b046c9e
Resolve Conflicts
WodansSon Dec 16, 2020
19a975e
Merging Changes between master and branch
WodansSon Dec 16, 2020
e64ea3a
Merge branch 'master' into add-fields-to-loganalytics-linked-service
WodansSon Dec 16, 2020
d814881
Mostly working again... few tweaks needed...
WodansSon Dec 16, 2020
b51fa89
Add legacy attribute todo's
WodansSon Dec 16, 2020
3100fe7
Fix lint errors
WodansSon Dec 16, 2020
0608ed8
model legacy behavior add legacy field test case
WodansSon Dec 17, 2020
d759859
Fix lint error
WodansSon Dec 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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
}
}
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}

Expand All @@ -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()

Expand All @@ -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)
Expand All @@ -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"
}