Skip to content

Commit

Permalink
New resources: azurerm_log_analytics_cluster and `azurerm_log_analy…
Browse files Browse the repository at this point in the history
…tics_cluster_customer_managed_key` (#8946)

Co-authored-by: kt <[email protected]>
Co-authored-by: jackofallops <[email protected]>
  • Loading branch information
3 people authored Nov 17, 2020
1 parent e30fcf8 commit 4431082
Show file tree
Hide file tree
Showing 20 changed files with 1,581 additions and 7 deletions.
5 changes: 4 additions & 1 deletion .teamcity/components/settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ var serviceTestConfigurationOverrides = mapOf(
// Data Lake has a low quota
"datalake" to testConfiguration(2, defaultStartHour),

//HSM has low quota and potentially slow recycle time
// HSM has low quota and potentially slow recycle time
"hsm" to testConfiguration(1, defaultStartHour),

// Log Analytics Clusters have a max deployments of 2 - parallelism set to 1 or `importTest` fails
"loganalytics" to testConfiguration(1, defaultStartHour),

// servicebus quotas are limited and we experience failures if tests
// execute too quickly as we run out of namespaces in the sub
"servicebus" to testConfiguration(10, defaultStartHour),
Expand Down
14 changes: 14 additions & 0 deletions azurerm/helpers/azure/key_vault_child.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ type KeyVaultChildID struct {
Version string
}

func NewKeyVaultChildResourceID(keyVaultBaseUrl, childType, name, version string) (string, error) {
fmtString := "%s/%s/%s/%s"
keyVaultUrl, err := url.Parse(keyVaultBaseUrl)
if err != nil || keyVaultBaseUrl == "" {
return "", fmt.Errorf("failed to parse Key Vault Base URL %q: %+v", keyVaultBaseUrl, err)
}
// (@jackofallops) - Log Analytics service adds the port number to the API returns, so we strip it here
if hostParts := strings.Split(keyVaultUrl.Host, ":"); len(hostParts) > 1 {
keyVaultUrl.Host = hostParts[0]
}

return fmt.Sprintf(fmtString, keyVaultUrl.String(), childType, name, version), nil
}

func ParseKeyVaultChildID(id string) (*KeyVaultChildID, error) {
// example: https://tharvey-keyvault.vault.azure.net/type/bird/fdf067c93bbb4b22bff4d8b7a9a56217
idURL, err := url.ParseRequestURI(id)
Expand Down
47 changes: 46 additions & 1 deletion azurerm/helpers/azure/key_vault_child_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package azure

import "testing"
import (
"testing"
)

func TestAccAzureRMValidateKeyVaultChildID(t *testing.T) {
cases := []struct {
Expand Down Expand Up @@ -320,3 +322,46 @@ func TestAccAzureRMKeyVaultChild_validateName(t *testing.T) {
}
}
}

func TestNewKeyVaultChildResourceID(t *testing.T) {
childType := "keys"
childName := "test"
childVersion := "testVersionString"
cases := []struct {
Scenario string
keyVaultBaseUrl string
Expected string
ExpectError bool
}{
{
Scenario: "empty values",
keyVaultBaseUrl: "",
Expected: "",
ExpectError: true,
},
{
Scenario: "valid, no port",
keyVaultBaseUrl: "https://test.vault.azure.net",
Expected: "https://test.vault.azure.net/keys/test/testVersionString",
ExpectError: false,
},
{
Scenario: "valid, with port",
keyVaultBaseUrl: "https://test.vault.azure.net:443",
Expected: "https://test.vault.azure.net/keys/test/testVersionString",
ExpectError: false,
},
}
for _, tc := range cases {
id, err := NewKeyVaultChildResourceID(tc.keyVaultBaseUrl, childType, childName, childVersion)
if err != nil {
if !tc.ExpectError {
t.Fatalf("Got error for New Resource ID '%s': %+v", tc.keyVaultBaseUrl, err)
return
}
}
if id != tc.Expected {
t.Fatalf("Expected id for %q to be %q, got %q", tc.keyVaultBaseUrl, tc.Expected, id)
}
}
}
5 changes: 5 additions & 0 deletions azurerm/internal/services/loganalytics/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

type Client struct {
ClusterClient *operationalinsights.ClustersClient
DataExportClient *operationalinsights.DataExportsClient
DataSourcesClient *operationalinsights.DataSourcesClient
LinkedServicesClient *operationalinsights.LinkedServicesClient
Expand All @@ -19,6 +20,9 @@ type Client struct {
}

func NewClient(o *common.ClientOptions) *Client {
ClusterClient := operationalinsights.NewClustersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&ClusterClient.Client, o.ResourceManagerAuthorizer)

DataExportClient := operationalinsights.NewDataExportsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&DataExportClient.Client, o.ResourceManagerAuthorizer)

Expand Down Expand Up @@ -47,6 +51,7 @@ func NewClient(o *common.ClientOptions) *Client {
o.ConfigureClient(&LinkedStorageAccountClient.Client, o.ResourceManagerAuthorizer)

return &Client{
ClusterClient: &ClusterClient,
DataExportClient: &DataExportClient,
DataSourcesClient: &DataSourcesClient,
LinkedServicesClient: &LinkedServicesClient,
Expand Down
46 changes: 46 additions & 0 deletions azurerm/internal/services/loganalytics/log_analytics_cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package loganalytics

import (
"context"
"fmt"
"log"
"time"

"github.com/Azure/azure-sdk-for-go/services/preview/operationalinsights/mgmt/2020-03-01-preview/operationalinsights"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
)

func logAnalyticsClusterWaitForState(ctx context.Context, meta interface{}, timeout time.Duration, resourceGroup string, clusterName string) *resource.StateChangeConf {
return &resource.StateChangeConf{
Pending: []string{string(operationalinsights.Updating)},
Target: []string{string(operationalinsights.Succeeded)},
MinTimeout: 1 * time.Minute,
Timeout: timeout,
Refresh: logAnalyticsClusterRefresh(ctx, meta, resourceGroup, clusterName),
}
}

func logAnalyticsClusterRefresh(ctx context.Context, meta interface{}, resourceGroup string, clusterName string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
client := meta.(*clients.Client).LogAnalytics.ClusterClient

log.Printf("[INFO] checking on state of Log Analytics Cluster %q", clusterName)

resp, err := client.Get(ctx, resourceGroup, clusterName)
if err != nil {
return nil, "nil", fmt.Errorf("polling for the status of Log Analytics Cluster %q (Resource Group %q): %v", clusterName, resourceGroup, err)
}

if resp.ClusterProperties != nil {
if resp.ClusterProperties.ProvisioningState != operationalinsights.Updating && resp.ClusterProperties.ProvisioningState != operationalinsights.Succeeded {
return nil, "nil", fmt.Errorf("Log Analytics Cluster %q (Resource Group %q) unexpected Provisioning State encountered: %q", clusterName, resourceGroup, string(resp.ClusterProperties.ProvisioningState))
}

return resp, string(resp.ClusterProperties.ProvisioningState), nil
}

// I am not returning an error here as this might have just been a bad get
return resp, "nil", nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package loganalytics

import (
"fmt"
"log"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/services/preview/operationalinsights/mgmt/2020-03-01-preview/operationalinsights"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
"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/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceArmLogAnalyticsClusterCustomerManagedKey() *schema.Resource {
return &schema.Resource{
Create: resourceArmLogAnalyticsClusterCustomerManagedKeyCreate,
Read: resourceArmLogAnalyticsClusterCustomerManagedKeyRead,
Update: resourceArmLogAnalyticsClusterCustomerManagedKeyUpdate,
Delete: resourceArmLogAnalyticsClusterCustomerManagedKeyDelete,

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(6 * time.Hour),
Read: schema.DefaultTimeout(5 * time.Minute),
Update: schema.DefaultTimeout(6 * time.Hour),
Delete: schema.DefaultTimeout(30 * time.Minute),
},

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"log_analytics_cluster_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.LogAnalyticsClusterId,
},

"key_vault_key_id": {
Type: schema.TypeString,
Required: true,
ValidateFunc: azure.ValidateKeyVaultChildIdVersionOptional,
},
},
}
}

func resourceArmLogAnalyticsClusterCustomerManagedKeyCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).LogAnalytics.ClusterClient
ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

clusterIdRaw := d.Get("log_analytics_cluster_id").(string)
clusterId, err := parse.LogAnalyticsClusterID(clusterIdRaw)
if err != nil {
return err
}

resp, err := client.Get(ctx, clusterId.ResourceGroup, clusterId.Name)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
return fmt.Errorf("Log Analytics Cluster %q (resource group %q) was not found", clusterId.Name, clusterId.ResourceGroup)
}
return fmt.Errorf("failed to get details of Log Analytics Cluster %q (resource group %q): %+v", clusterId.Name, clusterId.ResourceGroup, err)
}
if resp.ClusterProperties != nil && resp.ClusterProperties.KeyVaultProperties != nil {
keyProps := *resp.ClusterProperties.KeyVaultProperties
if keyProps.KeyName != nil && *keyProps.KeyName != "" {
return tf.ImportAsExistsError("azurerm_log_analytics_cluster_customer_managed_key", fmt.Sprintf("%s/CMK", clusterIdRaw))
}
}

d.SetId(fmt.Sprintf("%s/CMK", clusterIdRaw))
return resourceArmLogAnalyticsClusterCustomerManagedKeyUpdate(d, meta)
}

func resourceArmLogAnalyticsClusterCustomerManagedKeyUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).LogAnalytics.ClusterClient
ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

keyId, err := azure.ParseKeyVaultChildIDVersionOptional(d.Get("key_vault_key_id").(string))
if err != nil {
return fmt.Errorf("could not parse Key Vault Key ID: %+v", err)
}

clusterId, err := parse.LogAnalyticsClusterID(d.Get("log_analytics_cluster_id").(string))
if err != nil {
return err
}

clusterPatch := operationalinsights.ClusterPatch{
ClusterPatchProperties: &operationalinsights.ClusterPatchProperties{
KeyVaultProperties: &operationalinsights.KeyVaultProperties{
KeyVaultURI: utils.String(keyId.KeyVaultBaseUrl),
KeyName: utils.String(keyId.Name),
KeyVersion: utils.String(keyId.Version),
},
},
}

if _, err := client.Update(ctx, clusterId.ResourceGroup, clusterId.Name, clusterPatch); err != nil {
return fmt.Errorf("updating Log Analytics Cluster %q (Resource Group %q): %+v", clusterId.Name, clusterId.ResourceGroup, err)
}

updateWait := logAnalyticsClusterWaitForState(ctx, meta, d.Timeout(schema.TimeoutUpdate), clusterId.ResourceGroup, clusterId.Name)

if _, err := updateWait.WaitForState(); err != nil {
return fmt.Errorf("waiting for Log Analytics Cluster to finish updating %q (Resource Group %q): %v", clusterId.Name, clusterId.ResourceGroup, err)
}

return resourceArmLogAnalyticsClusterCustomerManagedKeyRead(d, meta)
}

func resourceArmLogAnalyticsClusterCustomerManagedKeyRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).LogAnalytics.ClusterClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

idRaw := strings.TrimRight(d.Id(), "/CMK")

id, err := parse.LogAnalyticsClusterID(idRaw)
if err != nil {
return err
}

d.Set("log_analytics_cluster_id", idRaw)

resp, err := client.Get(ctx, id.ResourceGroup, id.Name)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("[INFO] Log Analytics %q does not exist - removing from state", d.Id())
d.SetId("")
return nil
}
return fmt.Errorf("retrieving Log Analytics Cluster %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
}

if props := resp.ClusterProperties; props != nil {
if kvProps := props.KeyVaultProperties; kvProps != nil {
var keyVaultUri, keyName, keyVersion string
if kvProps.KeyVaultURI != nil && *kvProps.KeyVaultURI != "" {
keyVaultUri = *kvProps.KeyVaultURI
} else {
return fmt.Errorf("empty value returned for Key Vault URI")
}
if kvProps.KeyName != nil && *kvProps.KeyName != "" {
keyName = *kvProps.KeyName
} else {
return fmt.Errorf("empty value returned for Key Vault Key Name")
}
if kvProps.KeyVersion != nil {
keyVersion = *kvProps.KeyVersion
}
keyVaultKeyId, err := azure.NewKeyVaultChildResourceID(keyVaultUri, "keys", keyName, keyVersion)
if err != nil {
return err
}
d.Set("key_vault_key_id", keyVaultKeyId)
}
}

return nil
}

func resourceArmLogAnalyticsClusterCustomerManagedKeyDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).LogAnalytics.ClusterClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

clusterId, err := parse.LogAnalyticsClusterID(d.Get("log_analytics_cluster_id").(string))
if err != nil {
return err
}

clusterPatch := operationalinsights.ClusterPatch{
ClusterPatchProperties: &operationalinsights.ClusterPatchProperties{
KeyVaultProperties: &operationalinsights.KeyVaultProperties{
KeyVaultURI: nil,
KeyName: nil,
KeyVersion: nil,
},
},
}

if _, err = client.Update(ctx, clusterId.ResourceGroup, clusterId.Name, clusterPatch); err != nil {
return fmt.Errorf("removing Log Analytics Cluster Customer Managed Key from cluster %q (resource group %q)", clusterId.Name, clusterId.ResourceGroup)
}

deleteWait := logAnalyticsClusterWaitForState(ctx, meta, d.Timeout(schema.TimeoutDelete), clusterId.ResourceGroup, clusterId.Name)

if _, err := deleteWait.WaitForState(); err != nil {
return fmt.Errorf("waiting for Log Analytics Cluster to finish updating %q (Resource Group %q): %v", clusterId.Name, clusterId.ResourceGroup, err)
}

return nil
}
Loading

0 comments on commit 4431082

Please sign in to comment.