Skip to content

Commit

Permalink
Merge #9410
Browse files Browse the repository at this point in the history
* first commit and wait clusters ready

* merge master

* merge master

* revert client.go

* Normalize workspace checks and schema updates

* Update test with new schema

* Update documentation to match schema update

* Added LF for code readability

* Normalize ID casing

* Test cases for LogAnalyticsLinkedServiceId

* Removed unneeded attribute linked_service_type

* Workaround for link service changing SKU

* Delete wait state and block workspace modification

* Update docs

* Split out by type

* Remove redundant code and update workspace name

* Adding removed fields back into schema

* Resolve Conflicts

* Merging Changes between master and branch

* Mostly working again... few tweaks needed...

* Add legacy attribute todo's

* Fix lint errors

* model legacy behavior add legacy field test case

* Fix lint error

Co-authored-by: Jeffrey Cline <[email protected]>
  • Loading branch information
dw511214992 and WodansSon authored Dec 17, 2020
1 parent f8a77e6 commit 6be9876
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
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"
}
Loading

0 comments on commit 6be9876

Please sign in to comment.