diff --git a/azurerm/resource_arm_storage_blob.go b/azurerm/resource_arm_storage_blob.go index ca40247cae1f..22af74b011c3 100644 --- a/azurerm/resource_arm_storage_blob.go +++ b/azurerm/resource_arm_storage_blob.go @@ -7,22 +7,28 @@ import ( "fmt" "io" "log" + "net/url" "os" "runtime" "strings" "sync" "github.com/Azure/azure-sdk-for-go/storage" + "github.com/Azure/go-autorest/autorest/azure" "github.com/hashicorp/terraform/helper/schema" ) func resourceArmStorageBlob() *schema.Resource { return &schema.Resource{ - Create: resourceArmStorageBlobCreate, - Read: resourceArmStorageBlobRead, - Update: resourceArmStorageBlobUpdate, - Exists: resourceArmStorageBlobExists, - Delete: resourceArmStorageBlobDelete, + Create: resourceArmStorageBlobCreate, + Read: resourceArmStorageBlobRead, + Update: resourceArmStorageBlobUpdate, + Delete: resourceArmStorageBlobDelete, + MigrateState: resourceStorageBlobMigrateState, + SchemaVersion: 1, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, Schema: map[string]*schema.Schema{ "name": { @@ -127,8 +133,8 @@ func validateArmStorageBlobSize(v interface{}, k string) (ws []string, errors [] func validateArmStorageBlobType(v interface{}, k string) (ws []string, errors []error) { value := strings.ToLower(v.(string)) validTypes := map[string]struct{}{ - "block": struct{}{}, - "page": struct{}{}, + "block": {}, + "page": {}, } if _, ok := validTypes[value]; !ok { @@ -140,6 +146,7 @@ func validateArmStorageBlobType(v interface{}, k string) (ws []string, errors [] func resourceArmStorageBlobCreate(d *schema.ResourceData, meta interface{}) error { armClient := meta.(*ArmClient) ctx := armClient.StopContext + env := armClient.environment resourceGroupName := d.Get("resource_group_name").(string) storageAccountName := d.Get("storage_account_name").(string) @@ -154,15 +161,16 @@ func resourceArmStorageBlobCreate(d *schema.ResourceData, meta interface{}) erro name := d.Get("name").(string) blobType := d.Get("type").(string) - cont := d.Get("storage_container_name").(string) + containerName := d.Get("storage_container_name").(string) sourceUri := d.Get("source_uri").(string) contentType := d.Get("content_type").(string) - log.Printf("[INFO] Creating blob %q in storage account %q", name, storageAccountName) + log.Printf("[INFO] Creating blob %q in container %q within storage account %q", name, containerName, storageAccountName) + container := blobClient.GetContainerReference(containerName) + blob := container.GetBlobReference(name) + if sourceUri != "" { options := &storage.CopyOptions{} - container := blobClient.GetContainerReference(cont) - blob := container.GetBlobReference(name) err := blob.Copy(sourceUri, options) if err != nil { return fmt.Errorf("Error creating storage blob on Azure: %s", err) @@ -171,8 +179,6 @@ func resourceArmStorageBlobCreate(d *schema.ResourceData, meta interface{}) erro switch strings.ToLower(blobType) { case "block": options := &storage.PutBlobOptions{} - container := blobClient.GetContainerReference(cont) - blob := container.GetBlobReference(name) err := blob.CreateBlockBlob(options) if err != nil { return fmt.Errorf("Error creating storage blob on Azure: %s", err) @@ -182,7 +188,8 @@ func resourceArmStorageBlobCreate(d *schema.ResourceData, meta interface{}) erro if source != "" { parallelism := d.Get("parallelism").(int) attempts := d.Get("attempts").(int) - if err := resourceArmStorageBlobBlockUploadFromSource(cont, name, source, contentType, blobClient, parallelism, attempts); err != nil { + + if err := resourceArmStorageBlobBlockUploadFromSource(containerName, name, source, contentType, blobClient, parallelism, attempts); err != nil { return fmt.Errorf("Error creating storage blob on Azure: %s", err) } } @@ -191,15 +198,14 @@ func resourceArmStorageBlobCreate(d *schema.ResourceData, meta interface{}) erro if source != "" { parallelism := d.Get("parallelism").(int) attempts := d.Get("attempts").(int) - if err := resourceArmStorageBlobPageUploadFromSource(cont, name, source, contentType, blobClient, parallelism, attempts); err != nil { + + if err := resourceArmStorageBlobPageUploadFromSource(containerName, name, source, contentType, blobClient, parallelism, attempts); err != nil { return fmt.Errorf("Error creating storage blob on Azure: %s", err) } } else { size := int64(d.Get("size").(int)) options := &storage.PutBlobOptions{} - container := blobClient.GetContainerReference(cont) - blob := container.GetBlobReference(name) blob.Properties.ContentLength = size blob.Properties.ContentType = contentType err := blob.PutPageBlob(options) @@ -210,7 +216,9 @@ func resourceArmStorageBlobCreate(d *schema.ResourceData, meta interface{}) erro } } - d.SetId(name) + // gives us https://example.blob.core.windows.net/container/file.vhd + id := fmt.Sprintf("https://%s.blob.%s/%s/%s", storageAccountName, env.StorageEndpointSuffix, containerName, name) + d.SetId(id) return resourceArmStorageBlobRead(d, meta) } @@ -539,22 +547,30 @@ func resourceArmStorageBlobUpdate(d *schema.ResourceData, meta interface{}) erro armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageBlobID(d.Id(), armClient.environment) + if err != nil { + return err + } - blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) + if err != nil { + return err + } + + if resourceGroup == nil { + return fmt.Errorf("Unable to determine Resource Group for Storage Account %q", id.storageAccountName) + } + + blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) if err != nil { - return fmt.Errorf("Error getting storage account %s: %+v", storageAccountName, err) + return fmt.Errorf("Error getting storage account %s: %+v", id.storageAccountName, err) } if !accountExists { - return fmt.Errorf("Storage account %s not found in resource group %s", storageAccountName, resourceGroupName) + return fmt.Errorf("Storage account %s not found in resource group %s", id.storageAccountName, *resourceGroup) } - name := d.Get("name").(string) - storageContainerName := d.Get("storage_container_name").(string) - - container := blobClient.GetContainerReference(storageContainerName) - blob := container.GetBlobReference(name) + container := blobClient.GetContainerReference(id.containerName) + blob := container.GetBlobReference(id.blobName) if d.HasChange("content_type") { blob.Properties.ContentType = d.Get("content_type").(string) @@ -563,7 +579,7 @@ func resourceArmStorageBlobUpdate(d *schema.ResourceData, meta interface{}) erro options := &storage.SetBlobPropertiesOptions{} err = blob.SetProperties(options) if err != nil { - return fmt.Errorf("Error setting properties of blob %s (container %s, storage account %s): %+v", name, storageContainerName, storageAccountName, err) + return fmt.Errorf("Error setting properties of blob %s (container %s, storage account %s): %+v", id.blobName, id.containerName, id.storageAccountName, err) } return nil @@ -573,115 +589,162 @@ func resourceArmStorageBlobRead(d *schema.ResourceData, meta interface{}) error armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageBlobID(d.Id(), armClient.environment) + if err != nil { + return err + } - blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) + if err != nil { + return err + } + + if resourceGroup == nil { + return fmt.Errorf("Unable to determine Resource Group for Storage Account %q", id.storageAccountName) + } + + blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) if err != nil { return err } if !accountExists { - log.Printf("[DEBUG] Storage account %q not found, removing blob %q from state", storageAccountName, d.Id()) + log.Printf("[DEBUG] Storage account %q not found, removing blob %q from state", id.storageAccountName, d.Id()) d.SetId("") return nil } - exists, err := resourceArmStorageBlobExists(d, meta) + log.Printf("[INFO] Checking for existence of storage blob %q in container %q.", id.blobName, id.containerName) + container := blobClient.GetContainerReference(id.containerName) + blob := container.GetBlobReference(id.blobName) + exists, err := blob.Exists() if err != nil { - return err + return fmt.Errorf("error checking for existence of storage blob %q: %s", id.blobName, err) } if !exists { - // Exists already removed this from state + log.Printf("[INFO] Storage blob %q no longer exists, removing from state...", id.blobName) + d.SetId("") return nil } - name := d.Get("name").(string) - storageContainerName := d.Get("storage_container_name").(string) - - container := blobClient.GetContainerReference(storageContainerName) - blob := container.GetBlobReference(name) - options := &storage.GetBlobPropertiesOptions{} err = blob.GetProperties(options) if err != nil { - return fmt.Errorf("Error getting properties of blob %s (container %s, storage account %s): %+v", name, storageContainerName, storageAccountName, err) + return fmt.Errorf("Error getting properties of blob %s (container %s, storage account %s): %+v", id.blobName, id.containerName, id.storageAccountName, err) } + + d.Set("name", id.blobName) + d.Set("storage_container_name", id.containerName) + d.Set("storage_account_name", id.storageAccountName) + d.Set("resource_group_name", resourceGroup) + d.Set("content_type", blob.Properties.ContentType) + d.Set("source_uri", blob.Properties.CopySource) + + blobType := strings.ToLower(strings.Replace(string(blob.Properties.BlobType), "Blob", "", 1)) + d.Set("type", blobType) + url := blob.GetURL() if url == "" { - log.Printf("[INFO] URL for %q is empty", name) + log.Printf("[INFO] URL for %q is empty", id.blobName) } d.Set("url", url) return nil } -func resourceArmStorageBlobExists(d *schema.ResourceData, meta interface{}) (bool, error) { +func resourceArmStorageBlobDelete(d *schema.ResourceData, meta interface{}) error { armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageBlobID(d.Id(), armClient.environment) + if err != nil { + return err + } - blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) if err != nil { - return false, err + return fmt.Errorf("Unable to determine Resource Group for Storage Account %q: %+v", id.storageAccountName, err) } - if !accountExists { - log.Printf("[DEBUG] Storage account %q not found, removing blob %q from state", storageAccountName, d.Id()) - d.SetId("") - return false, nil + if resourceGroup == nil { + log.Printf("[INFO] Resource Group doesn't exist so the blob won't exist") + return nil } - name := d.Get("name").(string) - storageContainerName := d.Get("storage_container_name").(string) - - log.Printf("[INFO] Checking for existence of storage blob %q.", name) - container := blobClient.GetContainerReference(storageContainerName) - blob := container.GetBlobReference(name) - exists, err := blob.Exists() + blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) if err != nil { - return false, fmt.Errorf("error testing existence of storage blob %q: %s", name, err) + return err + } + if !accountExists { + log.Printf("[INFO] Storage Account %q doesn't exist so the blob won't exist", id.storageAccountName) + return nil } - if !exists { - log.Printf("[INFO] Storage blob %q no longer exists, removing from state...", name) - d.SetId("") + log.Printf("[INFO] Deleting storage blob %q", id.blobName) + options := &storage.DeleteBlobOptions{} + container := blobClient.GetContainerReference(id.containerName) + blob := container.GetBlobReference(id.blobName) + _, err = blob.DeleteIfExists(options) + if err != nil { + return fmt.Errorf("Error deleting storage blob %q: %s", id.blobName, err) } - return exists, nil + return nil } -func resourceArmStorageBlobDelete(d *schema.ResourceData, meta interface{}) error { - armClient := meta.(*ArmClient) - ctx := armClient.StopContext - - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) +type storageBlobId struct { + storageAccountName string + containerName string + blobName string +} - blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, resourceGroupName, storageAccountName) +func parseStorageBlobID(input string, environment azure.Environment) (*storageBlobId, error) { + uri, err := url.Parse(input) if err != nil { - return err + return nil, fmt.Errorf("Error parsing %q as URI: %+v", input, err) } - if !accountExists { - log.Printf("[INFO]Storage Account %q doesn't exist so the blob won't exist", storageAccountName) - return nil + + // trim the leading `/` + segments := strings.Split(strings.TrimPrefix(uri.Path, "/"), "/") + if len(segments) < 2 { + return nil, fmt.Errorf("Expected number of segments in the path to be < 2 but got %d", len(segments)) } - name := d.Get("name").(string) - storageContainerName := d.Get("storage_container_name").(string) + storageAccountName := strings.Replace(uri.Host, fmt.Sprintf(".blob.%s", environment.StorageEndpointSuffix), "", 1) + containerName := segments[0] + blobName := strings.TrimPrefix(uri.Path, fmt.Sprintf("/%s/", containerName)) - log.Printf("[INFO] Deleting storage blob %q", name) - options := &storage.DeleteBlobOptions{} - container := blobClient.GetContainerReference(storageContainerName) - blob := container.GetBlobReference(name) - _, err = blob.DeleteIfExists(options) + id := storageBlobId{ + storageAccountName: storageAccountName, + containerName: containerName, + blobName: blobName, + } + return &id, nil +} + +func determineResourceGroupForStorageAccount(accountName string, client *ArmClient) (*string, error) { + storageClient := client.storageServiceClient + ctx := client.StopContext + + // first locate which resource group the storage account is in + groupsResp, err := storageClient.List(ctx) if err != nil { - return fmt.Errorf("Error deleting storage blob %q: %s", name, err) + return nil, fmt.Errorf("Error loading the Resource Groups for Storage Account %q: %+v", accountName, err) } - d.SetId("") - return nil + if groups := groupsResp.Value; groups != nil { + for _, group := range *groups { + if group.Name != nil && *group.Name == accountName { + groupId, err := parseAzureResourceID(*group.ID) + if err != nil { + return nil, err + } + + return &groupId.ResourceGroup, nil + } + } + } + + return nil, nil } diff --git a/azurerm/resource_arm_storage_blob_migration.go b/azurerm/resource_arm_storage_blob_migration.go new file mode 100644 index 000000000000..f0e643a58ad8 --- /dev/null +++ b/azurerm/resource_arm_storage_blob_migration.go @@ -0,0 +1,40 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/terraform" +) + +func resourceStorageBlobMigrateState(v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AzureRM Storage Blob State v0; migrating to v1") + return migrateStorageBlobStateV0toV1(is, meta) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +func migrateStorageBlobStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + log.Printf("[DEBUG] ARM Storage Blob Attributes before Migration: %#v", is.Attributes) + + environment := meta.(*ArmClient).environment + + blobName := is.Attributes["name"] + containerName := is.Attributes["storage_container_name"] + storageAccountName := is.Attributes["storage_account_name"] + newID := fmt.Sprintf("https://%s.blob.%s/%s/%s", storageAccountName, environment.StorageEndpointSuffix, containerName, blobName) + is.Attributes["id"] = newID + is.ID = newID + + log.Printf("[DEBUG] ARM Storage Blob Attributes after State Migration: %#v", is.Attributes) + + return is, nil +} diff --git a/azurerm/resource_arm_storage_blob_migration_test.go b/azurerm/resource_arm_storage_blob_migration_test.go new file mode 100644 index 000000000000..bcb5b4f4c829 --- /dev/null +++ b/azurerm/resource_arm_storage_blob_migration_test.go @@ -0,0 +1,67 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +// NOTE: this is intentionally an acceptance test (and we're not explicitly setting the env) +// as we want to run this depending on the cloud we're in. +func TestAccAzureRMStorageBlobMigrateState(t *testing.T) { + config := testGetAzureConfig(t) + if config == nil { + t.SkipNow() + return + } + + client, err := getArmClient(config) + if err != nil { + t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) + return + } + + client.StopContext = testAccProvider.StopContext() + + suffix := client.environment.StorageEndpointSuffix + + cases := map[string]struct { + StateVersion int + ID string + InputAttributes map[string]string + ExpectedAttributes map[string]string + }{ + "v0_1_without_value": { + StateVersion: 0, + ID: "some_id", + InputAttributes: map[string]string{ + "name": "blob.vhd", + "storage_container_name": "container", + "storage_account_name": "example", + }, + ExpectedAttributes: map[string]string{ + "id": fmt.Sprintf("https://example.blob.%s/container/blob.vhd", suffix), + }, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: tc.ID, + Attributes: tc.InputAttributes, + } + is, err := resourceStorageBlobMigrateState(tc.StateVersion, is, client) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.ExpectedAttributes { + actual := is.Attributes[k] + if actual != v { + t.Fatalf("Bad Storage Blob Migrate for %q: %q\n\n expected: %q", k, actual, v) + } + } + } +} diff --git a/azurerm/resource_arm_storage_blob_test.go b/azurerm/resource_arm_storage_blob_test.go index 116a70fa7cec..324f8179f55d 100644 --- a/azurerm/resource_arm_storage_blob_test.go +++ b/azurerm/resource_arm_storage_blob_test.go @@ -144,6 +144,7 @@ func TestResourceAzureRMStorageBlobAttempts_validation(t *testing.T) { } func TestAccAzureRMStorageBlob_basic(t *testing.T) { + resourceName := "azurerm_storage_blob.test" ri := acctest.RandInt() rs := strings.ToLower(acctest.RandString(11)) config := testAccAzureRMStorageBlob_basic(ri, rs, testLocation()) @@ -156,14 +157,21 @@ func TestAccAzureRMStorageBlob_basic(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageBlobExists("azurerm_storage_blob.test"), + testCheckAzureRMStorageBlobExists(resourceName), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"attempts", "parallelism", "size", "type"}, + }, }, }) } func TestAccAzureRMStorageBlob_disappears(t *testing.T) { + resourceName := "azurerm_storage_blob.test" ri := acctest.RandInt() rs := strings.ToLower(acctest.RandString(11)) config := testAccAzureRMStorageBlob_basic(ri, rs, testLocation()) @@ -176,8 +184,8 @@ func TestAccAzureRMStorageBlob_disappears(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageBlobExists("azurerm_storage_blob.test"), - testCheckAzureRMStorageBlobDisappears("azurerm_storage_blob.test"), + testCheckAzureRMStorageBlobExists(resourceName), + testCheckAzureRMStorageBlobDisappears(resourceName), ), ExpectNonEmptyPlan: true, }, @@ -221,6 +229,7 @@ func TestAccAzureRMStorageBlobBlock_source(t *testing.T) { } func TestAccAzureRMStorageBlobPage_source(t *testing.T) { + resourceName := "azurerm_storage_blob.source" ri := acctest.RandInt() rs := strings.ToLower(acctest.RandString(11)) sourceBlob, err := ioutil.TempFile("", "") @@ -272,7 +281,7 @@ func TestAccAzureRMStorageBlobPage_source(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageBlobMatchesFile("azurerm_storage_blob.source", storage.BlobTypePage, sourceBlob.Name()), + testCheckAzureRMStorageBlobMatchesFile(resourceName, storage.BlobTypePage, sourceBlob.Name()), ), }, }, @@ -280,6 +289,7 @@ func TestAccAzureRMStorageBlobPage_source(t *testing.T) { } func TestAccAzureRMStorageBlob_source_uri(t *testing.T) { + resourceName := "azurerm_storage_blob.destination" ri := acctest.RandInt() rs := strings.ToLower(acctest.RandString(11)) sourceBlob, err := ioutil.TempFile("", "") @@ -307,9 +317,15 @@ func TestAccAzureRMStorageBlob_source_uri(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageBlobMatchesFile("azurerm_storage_blob.destination", storage.BlobTypeBlock, sourceBlob.Name()), + testCheckAzureRMStorageBlobMatchesFile(resourceName, storage.BlobTypeBlock, sourceBlob.Name()), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"attempts", "parallelism", "size", "type"}, + }, }, }) } @@ -683,31 +699,32 @@ resource "azurerm_storage_account" "source" { } resource "azurerm_storage_container" "source" { - name = "source" - resource_group_name = "${azurerm_resource_group.test.name}" - storage_account_name = "${azurerm_storage_account.source.name}" - container_access_type = "blob" + name = "source" + resource_group_name = "${azurerm_resource_group.test.name}" + storage_account_name = "${azurerm_storage_account.source.name}" + container_access_type = "blob" } resource "azurerm_storage_blob" "source" { - name = "source.vhd" + name = "source.vhd" - resource_group_name = "${azurerm_resource_group.test.name}" - storage_account_name = "${azurerm_storage_account.source.name}" - storage_container_name = "${azurerm_storage_container.source.name}" + resource_group_name = "${azurerm_resource_group.test.name}" + storage_account_name = "${azurerm_storage_account.source.name}" + storage_container_name = "${azurerm_storage_container.source.name}" - type = "block" - source = "%s" - parallelism = 4 - attempts = 2 + type = "block" + source = "%s" + parallelism = 4 + attempts = 2 } resource "azurerm_storage_blob" "destination" { - name = "destination.vhd" - resource_group_name = "${azurerm_resource_group.test.name}" - storage_account_name = "${azurerm_storage_account.source.name}" - storage_container_name = "${azurerm_storage_container.source.name}" - source_uri = "${azurerm_storage_blob.source.url}" + name = "destination.vhd" + resource_group_name = "${azurerm_resource_group.test.name}" + storage_account_name = "${azurerm_storage_account.source.name}" + storage_container_name = "${azurerm_storage_container.source.name}" + source_uri = "${azurerm_storage_blob.source.url}" + type = "block" } `, rInt, location, rString, sourceBlobName) } diff --git a/azurerm/resource_arm_storage_container.go b/azurerm/resource_arm_storage_container.go index c028f69dcac1..2fcd0b394e3b 100644 --- a/azurerm/resource_arm_storage_container.go +++ b/azurerm/resource_arm_storage_container.go @@ -3,22 +3,27 @@ package azurerm import ( "fmt" "log" + "net/url" + "regexp" "strings" "time" - "regexp" - "github.com/Azure/azure-sdk-for-go/storage" + "github.com/Azure/go-autorest/autorest/azure" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) func resourceArmStorageContainer() *schema.Resource { return &schema.Resource{ - Create: resourceArmStorageContainerCreate, - Read: resourceArmStorageContainerRead, - Exists: resourceArmStorageContainerExists, - Delete: resourceArmStorageContainerDelete, + Create: resourceArmStorageContainerCreate, + Read: resourceArmStorageContainerRead, + Delete: resourceArmStorageContainerDelete, + MigrateState: resourceStorageContainerMigrateState, + SchemaVersion: 1, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, Schema: map[string]*schema.Schema{ "name": { @@ -40,6 +45,7 @@ func resourceArmStorageContainer() *schema.Resource { Default: "private", ValidateFunc: validateArmStorageContainerAccessType, }, + "properties": { Type: schema.TypeMap, Computed: true, @@ -85,6 +91,7 @@ func resourceArmStorageContainerCreate(d *schema.ResourceData, meta interface{}) armClient := meta.(*ArmClient) ctx := armClient.StopContext + name := d.Get("name").(string) resourceGroupName := d.Get("resource_group_name").(string) storageAccountName := d.Get("storage_account_name").(string) @@ -96,8 +103,6 @@ func resourceArmStorageContainerCreate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Storage Account %q Not Found", storageAccountName) } - name := d.Get("name").(string) - var accessType storage.ContainerAccessType if d.Get("container_access_type").(string) == "private" { accessType = storage.ContainerAccessType("") @@ -122,105 +127,87 @@ func resourceArmStorageContainerCreate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Error setting permissions for container %s in storage account %s: %+v", name, storageAccountName, err) } - d.SetId(name) + id := fmt.Sprintf("https://%s.blob.%s/%s", storageAccountName, armClient.environment.StorageEndpointSuffix, name) + d.SetId(id) return resourceArmStorageContainerRead(d, meta) } -func checkContainerIsCreated(reference *storage.Container) func() *resource.RetryError { - return func() *resource.RetryError { - createOptions := &storage.CreateContainerOptions{} - _, err := reference.CreateIfNotExists(createOptions) - if err != nil { - return resource.RetryableError(err) - } - - return nil - } -} - // resourceAzureStorageContainerRead does all the necessary API calls to // read the status of the storage container off Azure. func resourceArmStorageContainerRead(d *schema.ResourceData, meta interface{}) error { armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageContainerID(d.Id(), armClient.environment) + if err != nil { + return err + } - blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) + if err != nil { + return err + } + if resourceGroup == nil { + log.Printf("Cannot locate Resource Group for Storage Account %q (presuming it's gone) - removing from state", id.storageAccountName) + d.SetId("") + return nil + } + + blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) if err != nil { return err } if !accountExists { - log.Printf("[DEBUG] Storage account %q not found, removing container %q from state", storageAccountName, d.Id()) + log.Printf("[DEBUG] Storage account %q not found, removing container %q from state", id.storageAccountName, d.Id()) d.SetId("") return nil } - name := d.Get("name").(string) containers, err := blobClient.ListContainers(storage.ListContainersParameters{ - Prefix: name, + Prefix: id.containerName, Timeout: 90, }) if err != nil { - return fmt.Errorf("Failed to retrieve storage containers in account %q: %s", name, err) + return fmt.Errorf("Failed to retrieve storage containers in account %q: %s", id.containerName, err) } - var found bool + var container *storage.Container for _, cont := range containers.Containers { - if cont.Name == name { - found = true - - props := make(map[string]interface{}) - props["last_modified"] = cont.Properties.LastModified - props["lease_status"] = cont.Properties.LeaseStatus - props["lease_state"] = cont.Properties.LeaseState - props["lease_duration"] = cont.Properties.LeaseDuration - - d.Set("properties", props) + if cont.Name == id.containerName { + container = &cont + break } } - if !found { - log.Printf("[INFO] Storage container %q does not exist in account %q, removing from state...", name, storageAccountName) + if container == nil { + log.Printf("[INFO] Storage container %q does not exist in account %q, removing from state...", id.containerName, id.storageAccountName) d.SetId("") + return nil } - return nil -} - -func resourceArmStorageContainerExists(d *schema.ResourceData, meta interface{}) (bool, error) { - armClient := meta.(*ArmClient) - ctx := armClient.StopContext - - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + d.Set("name", id.containerName) + d.Set("storage_account_name", id.storageAccountName) + d.Set("resource_group_name", resourceGroup) - blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, resourceGroupName, storageAccountName) - if err != nil { - return false, err - } - if !accountExists { - log.Printf("[DEBUG] Storage account %q not found, removing container %q from state", storageAccountName, d.Id()) - d.SetId("") - return false, nil + // for historical reasons, "private" above is an empty string in the API + if container.Properties.PublicAccess == storage.ContainerAccessTypePrivate { + d.Set("container_access_type", "private") + } else { + d.Set("container_access_type", string(container.Properties.PublicAccess)) } - name := d.Get("name").(string) + output := make(map[string]interface{}) - log.Printf("[INFO] Checking existence of storage container %q in storage account %q", name, storageAccountName) - reference := blobClient.GetContainerReference(name) - exists, err := reference.Exists() - if err != nil { - return false, fmt.Errorf("Error querying existence of storage container %q in storage account %q: %s", name, storageAccountName, err) - } + output["last_modified"] = container.Properties.LastModified + output["lease_status"] = container.Properties.LeaseStatus + output["lease_state"] = container.Properties.LeaseState + output["lease_duration"] = container.Properties.LeaseDuration - if !exists { - log.Printf("[INFO] Storage container %q does not exist in account %q, removing from state...", name, storageAccountName) - d.SetId("") + if err := d.Set("properties", output); err != nil { + return fmt.Errorf("Error flattening `properties`: %+v", err) } - return exists, nil + return nil } // resourceAzureStorageContainerDelete does all the necessary API calls to @@ -229,27 +216,74 @@ func resourceArmStorageContainerDelete(d *schema.ResourceData, meta interface{}) armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageContainerID(d.Id(), armClient.environment) + if err != nil { + return err + } - blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) if err != nil { return err } - if !accountExists { - log.Printf("[INFO]Storage Account %q doesn't exist so the container won't exist", storageAccountName) + if resourceGroup == nil { + log.Printf("Cannot locate Resource Group for Storage Account %q (presuming it's gone) - removing from state", id.storageAccountName) return nil } - name := d.Get("name").(string) + blobClient, accountExists, err := armClient.getBlobStorageClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) + if err != nil { + return err + } + if !accountExists { + log.Printf("[INFO] Storage Account %q doesn't exist so the container won't exist", id.storageAccountName) + return nil + } - log.Printf("[INFO] Deleting storage container %q in account %q", name, storageAccountName) - reference := blobClient.GetContainerReference(name) + log.Printf("[INFO] Deleting storage container %q in account %q", id.containerName, id.storageAccountName) + reference := blobClient.GetContainerReference(id.containerName) deleteOptions := &storage.DeleteContainerOptions{} if _, err := reference.DeleteIfExists(deleteOptions); err != nil { - return fmt.Errorf("Error deleting storage container %q from storage account %q: %s", name, storageAccountName, err) + return fmt.Errorf("Error deleting storage container %q from storage account %q: %s", id.containerName, id.storageAccountName, err) } - d.SetId("") return nil } + +func checkContainerIsCreated(reference *storage.Container) func() *resource.RetryError { + return func() *resource.RetryError { + createOptions := &storage.CreateContainerOptions{} + _, err := reference.CreateIfNotExists(createOptions) + if err != nil { + return resource.RetryableError(err) + } + + return nil + } +} + +type storageContainerId struct { + storageAccountName string + containerName string +} + +func parseStorageContainerID(input string, environment azure.Environment) (*storageContainerId, error) { + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("Error parsing %q as URI: %+v", input, err) + } + + // remove the leading `/` + segments := strings.Split(strings.TrimPrefix(uri.Path, "/"), "/") + if len(segments) < 1 { + return nil, fmt.Errorf("Expected number of segments in the path to be < 1 but got %d", len(segments)) + } + + storageAccountName := strings.Replace(uri.Host, fmt.Sprintf(".blob.%s", environment.StorageEndpointSuffix), "", 1) + containerName := segments[0] + + id := storageContainerId{ + storageAccountName: storageAccountName, + containerName: containerName, + } + return &id, nil +} diff --git a/azurerm/resource_arm_storage_container_migration.go b/azurerm/resource_arm_storage_container_migration.go new file mode 100644 index 000000000000..c8ed4d5a7cd9 --- /dev/null +++ b/azurerm/resource_arm_storage_container_migration.go @@ -0,0 +1,39 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/terraform" +) + +func resourceStorageContainerMigrateState(v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AzureRM Storage Container State v0; migrating to v1") + return migrateStorageContainerStateV0toV1(is, meta) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +func migrateStorageContainerStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + log.Printf("[DEBUG] ARM Storage Container Attributes before Migration: %#v", is.Attributes) + + environment := meta.(*ArmClient).environment + + containerName := is.Attributes["name"] + storageAccountName := is.Attributes["storage_account_name"] + newID := fmt.Sprintf("https://%s.blob.%s/%s", storageAccountName, environment.StorageEndpointSuffix, containerName) + is.Attributes["id"] = newID + is.ID = newID + + log.Printf("[DEBUG] ARM Storage Container Attributes after State Migration: %#v", is.Attributes) + + return is, nil +} diff --git a/azurerm/resource_arm_storage_container_migration_test.go b/azurerm/resource_arm_storage_container_migration_test.go new file mode 100644 index 000000000000..ce1eb4658b23 --- /dev/null +++ b/azurerm/resource_arm_storage_container_migration_test.go @@ -0,0 +1,66 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +// NOTE: this is intentionally an acceptance test (and we're not explicitly setting the env) +// as we want to run this depending on the cloud we're in. +func TestAccAzureRMStorageContainerMigrateState(t *testing.T) { + config := testGetAzureConfig(t) + if config == nil { + t.SkipNow() + return + } + + client, err := getArmClient(config) + if err != nil { + t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) + return + } + + client.StopContext = testAccProvider.StopContext() + + suffix := client.environment.StorageEndpointSuffix + + cases := map[string]struct { + StateVersion int + ID string + InputAttributes map[string]string + ExpectedAttributes map[string]string + }{ + "v0_1_without_value": { + StateVersion: 0, + ID: "some_id", + InputAttributes: map[string]string{ + "name": "container", + "storage_account_name": "example", + }, + ExpectedAttributes: map[string]string{ + "id": fmt.Sprintf("https://example.blob.%s/container", suffix), + }, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: tc.ID, + Attributes: tc.InputAttributes, + } + is, err := resourceStorageContainerMigrateState(tc.StateVersion, is, client) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.ExpectedAttributes { + actual := is.Attributes[k] + if actual != v { + t.Fatalf("Bad Storage Container Migrate for %q: %q\n\n expected: %q", k, actual, v) + } + } + } +} diff --git a/azurerm/resource_arm_storage_container_test.go b/azurerm/resource_arm_storage_container_test.go index b844000ada76..8b841cd14a64 100644 --- a/azurerm/resource_arm_storage_container_test.go +++ b/azurerm/resource_arm_storage_container_test.go @@ -13,6 +13,7 @@ import ( ) func TestAccAzureRMStorageContainer_basic(t *testing.T) { + resourceName := "azurerm_storage_container.test" var c storage.Container ri := acctest.RandInt() @@ -27,9 +28,14 @@ func TestAccAzureRMStorageContainer_basic(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageContainerExists("azurerm_storage_container.test", &c), + testCheckAzureRMStorageContainerExists(resourceName, &c), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -59,6 +65,7 @@ func TestAccAzureRMStorageContainer_disappears(t *testing.T) { } func TestAccAzureRMStorageContainer_root(t *testing.T) { + resourceName := "azurerm_storage_container.test" var c storage.Container ri := acctest.RandInt() @@ -73,10 +80,15 @@ func TestAccAzureRMStorageContainer_root(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageContainerExists("azurerm_storage_container.test", &c), - resource.TestCheckResourceAttr("azurerm_storage_container.test", "name", "$root"), + testCheckAzureRMStorageContainerExists(resourceName, &c), + resource.TestCheckResourceAttr(resourceName, "name", "$root"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/azurerm/resource_arm_storage_queue.go b/azurerm/resource_arm_storage_queue.go index d6c58ae9a207..8f3bcf9cf3fd 100644 --- a/azurerm/resource_arm_storage_queue.go +++ b/azurerm/resource_arm_storage_queue.go @@ -3,7 +3,9 @@ package azurerm import ( "fmt" "log" + "net/url" "regexp" + "strings" "github.com/Azure/azure-sdk-for-go/storage" "github.com/hashicorp/terraform/helper/schema" @@ -13,8 +15,12 @@ func resourceArmStorageQueue() *schema.Resource { return &schema.Resource{ Create: resourceArmStorageQueueCreate, Read: resourceArmStorageQueueRead, - Exists: resourceArmStorageQueueExists, Delete: resourceArmStorageQueueDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + SchemaVersion: 1, + MigrateState: resourceStorageQueueMigrateState, Schema: map[string]*schema.Schema{ "name": { @@ -65,7 +71,9 @@ func validateArmStorageQueueName(v interface{}, k string) (ws []string, errors [ func resourceArmStorageQueueCreate(d *schema.ResourceData, meta interface{}) error { armClient := meta.(*ArmClient) ctx := armClient.StopContext + environment := armClient.environment + name := d.Get("name").(string) resourceGroupName := d.Get("resource_group_name").(string) storageAccountName := d.Get("storage_account_name").(string) @@ -77,8 +85,6 @@ func resourceArmStorageQueueCreate(d *schema.ResourceData, meta interface{}) err return fmt.Errorf("Storage Account %q Not Found", storageAccountName) } - name := d.Get("name").(string) - log.Printf("[INFO] Creating queue %q in storage account %q", name, storageAccountName) queueReference := queueClient.GetQueueReference(name) options := &storage.QueueServiceOptions{} @@ -87,84 +93,122 @@ func resourceArmStorageQueueCreate(d *schema.ResourceData, meta interface{}) err return fmt.Errorf("Error creating storage queue on Azure: %s", err) } - d.SetId(name) + id := fmt.Sprintf("https://%s.queue.%s/%s", storageAccountName, environment.StorageEndpointSuffix, name) + d.SetId(id) return resourceArmStorageQueueRead(d, meta) } func resourceArmStorageQueueRead(d *schema.ResourceData, meta interface{}) error { + armClient := meta.(*ArmClient) + ctx := armClient.StopContext - exists, err := resourceArmStorageQueueExists(d, meta) + id, err := parseStorageQueueID(d.Id()) if err != nil { return err } - if !exists { - // Exists already removed this from state - return nil + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) + if err != nil { + return err } - return nil -} - -func resourceArmStorageQueueExists(d *schema.ResourceData, meta interface{}) (bool, error) { - armClient := meta.(*ArmClient) - ctx := armClient.StopContext - - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + if resourceGroup == nil { + log.Printf("[WARN] Unable to determine Resource Group for Storage Account %q (assuming removed) - removing from state", id.storageAccountName) + d.SetId("") + return nil + } - queueClient, accountExists, err := armClient.getQueueServiceClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + queueClient, accountExists, err := armClient.getQueueServiceClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) if err != nil { - return false, err + return err } if !accountExists { - log.Printf("[DEBUG] Storage account %q not found, removing queue %q from state", storageAccountName, d.Id()) + log.Printf("[DEBUG] Storage account %q not found, removing queue %q from state", id.storageAccountName, id.queueName) d.SetId("") - return false, nil + return nil } - name := d.Get("name").(string) - - log.Printf("[INFO] Checking for existence of storage queue %q.", name) - queueReference := queueClient.GetQueueReference(name) + log.Printf("[INFO] Checking for existence of storage queue %q.", id.queueName) + queueReference := queueClient.GetQueueReference(id.queueName) exists, err := queueReference.Exists() if err != nil { - return false, fmt.Errorf("error testing existence of storage queue %q: %s", name, err) + return fmt.Errorf("error checking if storage queue %q exists: %s", id.queueName, err) } if !exists { - log.Printf("[INFO] Storage queue %q no longer exists, removing from state...", name) + log.Printf("[INFO] Storage queue %q no longer exists, removing from state...", id.queueName) d.SetId("") + return nil } - return exists, nil + d.Set("name", id.queueName) + d.Set("storage_account_name", id.storageAccountName) + d.Set("resource_group_name", *resourceGroup) + + return nil } func resourceArmStorageQueueDelete(d *schema.ResourceData, meta interface{}) error { armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageQueueID(d.Id()) + if err != nil { + return err + } - queueClient, accountExists, err := armClient.getQueueServiceClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) if err != nil { return err } - if !accountExists { - log.Printf("[INFO]Storage Account %q doesn't exist so the blob won't exist", storageAccountName) + + if resourceGroup == nil { + log.Printf("[WARN] Unable to determine Resource Group for Storage Account %q (assuming removed) - removing from state", id.storageAccountName) return nil } - name := d.Get("name").(string) + queueClient, accountExists, err := armClient.getQueueServiceClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) + if err != nil { + return err + } + if !accountExists { + log.Printf("[INFO]Storage Account %q doesn't exist so the blob won't exist", id.storageAccountName) + return nil + } - log.Printf("[INFO] Deleting storage queue %q", name) - queueReference := queueClient.GetQueueReference(name) + log.Printf("[INFO] Deleting storage queue %q", id.queueName) + queueReference := queueClient.GetQueueReference(id.queueName) options := &storage.QueueServiceOptions{} if err = queueReference.Delete(options); err != nil { - return fmt.Errorf("Error deleting storage queue %q: %s", name, err) + return fmt.Errorf("Error deleting storage queue %q: %s", id.queueName, err) } - d.SetId("") return nil } + +type storageQueueId struct { + storageAccountName string + queueName string +} + +func parseStorageQueueID(input string) (*storageQueueId, error) { + // https://myaccount.queue.core.windows.net/myqueue + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("Error parsing %q as a URI: %+v", input, err) + } + + segments := strings.Split(uri.Host, ".") + if len(segments) > 0 { + storageAccountName := segments[0] + // remove the leading `/` + queue := strings.TrimPrefix(uri.Path, "/") + id := storageQueueId{ + storageAccountName: storageAccountName, + queueName: queue, + } + return &id, nil + } + + return nil, nil +} diff --git a/azurerm/resource_arm_storage_queue_migration.go b/azurerm/resource_arm_storage_queue_migration.go new file mode 100644 index 000000000000..db83cb1b5f7d --- /dev/null +++ b/azurerm/resource_arm_storage_queue_migration.go @@ -0,0 +1,40 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/terraform" +) + +func resourceStorageQueueMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AzureRM Storage Queue State v0; migrating to v1") + return migrateStorageQueueStateV0toV1(is, meta) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +func migrateStorageQueueStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + log.Printf("[DEBUG] ARM Storage Queue Attributes before Migration: %#v", is.Attributes) + + environment := meta.(*ArmClient).environment + + queueName := is.Attributes["name"] + storageAccountName := is.Attributes["storage_account_name"] + newID := fmt.Sprintf("https://%s.queue.%s/%s", storageAccountName, environment.StorageEndpointSuffix, queueName) + is.Attributes["id"] = newID + is.ID = newID + + log.Printf("[DEBUG] ARM Storage Queue Attributes after State Migration: %#v", is.Attributes) + + return is, nil +} diff --git a/azurerm/resource_arm_storage_queue_migration_test.go b/azurerm/resource_arm_storage_queue_migration_test.go new file mode 100644 index 000000000000..5399acef9d5f --- /dev/null +++ b/azurerm/resource_arm_storage_queue_migration_test.go @@ -0,0 +1,66 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +// NOTE: this is intentionally an acceptance test (and we're not explicitly setting the env) +// as we want to run this depending on the cloud we're in. +func TestAccAzureRMStorageQueueMigrateState(t *testing.T) { + config := testGetAzureConfig(t) + if config == nil { + t.SkipNow() + return + } + + client, err := getArmClient(config) + if err != nil { + t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) + return + } + + client.StopContext = testAccProvider.StopContext() + + suffix := client.environment.StorageEndpointSuffix + + cases := map[string]struct { + StateVersion int + ID string + InputAttributes map[string]string + ExpectedAttributes map[string]string + }{ + "v0_1_without_value": { + StateVersion: 0, + ID: "some_id", + InputAttributes: map[string]string{ + "name": "queue", + "storage_account_name": "example", + }, + ExpectedAttributes: map[string]string{ + "id": fmt.Sprintf("https://example.queue.%s/queue", suffix), + }, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: tc.ID, + Attributes: tc.InputAttributes, + } + is, err := resourceStorageQueueMigrateState(tc.StateVersion, is, client) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.ExpectedAttributes { + actual := is.Attributes[k] + if actual != v { + t.Fatalf("Bad Storage Queue Migrate for %q: %q\n\n expected: %q", k, actual, v) + } + } + } +} diff --git a/azurerm/resource_arm_storage_queue_test.go b/azurerm/resource_arm_storage_queue_test.go index 223835c15814..caec84139db0 100644 --- a/azurerm/resource_arm_storage_queue_test.go +++ b/azurerm/resource_arm_storage_queue_test.go @@ -51,6 +51,7 @@ func TestResourceAzureRMStorageQueueName_Validation(t *testing.T) { } func TestAccAzureRMStorageQueue_basic(t *testing.T) { + resourceName := "azurerm_storage_queue.test" ri := acctest.RandInt() rs := strings.ToLower(acctest.RandString(11)) config := testAccAzureRMStorageQueue_basic(ri, rs, testLocation()) @@ -63,9 +64,14 @@ func TestAccAzureRMStorageQueue_basic(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageQueueExists("azurerm_storage_queue.test"), + testCheckAzureRMStorageQueueExists(resourceName), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/azurerm/resource_arm_storage_table.go b/azurerm/resource_arm_storage_table.go index e5cc2e14e10a..63ab4fdc7d04 100644 --- a/azurerm/resource_arm_storage_table.go +++ b/azurerm/resource_arm_storage_table.go @@ -3,7 +3,9 @@ package azurerm import ( "fmt" "log" + "net/url" "regexp" + "strings" "github.com/Azure/azure-sdk-for-go/storage" "github.com/hashicorp/terraform/helper/schema" @@ -14,6 +16,11 @@ func resourceArmStorageTable() *schema.Resource { Create: resourceArmStorageTableCreate, Read: resourceArmStorageTableRead, Delete: resourceArmStorageTableDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + SchemaVersion: 1, + MigrateState: resourceStorageTableMigrateState, Schema: map[string]*schema.Schema{ "name": { @@ -51,7 +58,9 @@ func validateArmStorageTableName(v interface{}, k string) (ws []string, errors [ func resourceArmStorageTableCreate(d *schema.ResourceData, meta interface{}) error { armClient := meta.(*ArmClient) ctx := armClient.StopContext + environment := armClient.environment + name := d.Get("name").(string) resourceGroupName := d.Get("resource_group_name").(string) storageAccountName := d.Get("storage_account_name").(string) @@ -63,7 +72,6 @@ func resourceArmStorageTableCreate(d *schema.ResourceData, meta interface{}) err return fmt.Errorf("Storage Account %q Not Found", storageAccountName) } - name := d.Get("name").(string) table := tableClient.GetTableReference(name) log.Printf("[INFO] Creating table %q in storage account %q.", name, storageAccountName) @@ -75,8 +83,8 @@ func resourceArmStorageTableCreate(d *schema.ResourceData, meta interface{}) err return fmt.Errorf("Error creating table %q in storage account %q: %s", name, storageAccountName, err) } - d.SetId(name) - + id := fmt.Sprintf("https://%s.table.%s/%s", storageAccountName, environment.StorageEndpointSuffix, name) + d.SetId(id) return resourceArmStorageTableRead(d, meta) } @@ -84,41 +92,58 @@ func resourceArmStorageTableRead(d *schema.ResourceData, meta interface{}) error armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageTableID(d.Id()) + if err != nil { + return err + } - tableClient, accountExists, err := armClient.getTableServiceClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) + if err != nil { + return err + } + + if resourceGroup == nil { + log.Printf("Unable to determine Resource Group for Storage Account %q (assuming removed)", id.storageAccountName) + d.SetId("") + return nil + } + + tableClient, accountExists, err := armClient.getTableServiceClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) if err != nil { return err } + if !accountExists { - log.Printf("[DEBUG] Storage account %q not found, removing table %q from state", storageAccountName, d.Id()) + log.Printf("[DEBUG] Storage account %q not found, removing table %q from state", id.storageAccountName, id.tableName) d.SetId("") return nil } - name := d.Get("name").(string) metaDataLevel := storage.MinimalMetadata options := &storage.QueryTablesOptions{} tables, err := tableClient.QueryTables(metaDataLevel, options) if err != nil { - return fmt.Errorf("Failed to retrieve storage tables in account %q: %s", name, err) + return fmt.Errorf("Failed to retrieve Tables in Storage Account %q: %s", id.tableName, err) } - var found bool + var storageTable *storage.Table for _, table := range tables.Tables { - tableName := string(table.Name) - if tableName == name { - found = true - d.Set("name", tableName) + if string(table.Name) == id.tableName { + storageTable = &table + break } } - if !found { - log.Printf("[INFO] Storage table %q does not exist in account %q, removing from state...", name, storageAccountName) + if storageTable == nil { + log.Printf("[INFO] Table %q does not exist in Storage Account %q, removing from state...", id.tableName, id.storageAccountName) d.SetId("") + return nil } + d.Set("name", id.tableName) + d.Set("storage_account_name", id.storageAccountName) + d.Set("resource_group_name", resourceGroup) + return nil } @@ -126,28 +151,64 @@ func resourceArmStorageTableDelete(d *schema.ResourceData, meta interface{}) err armClient := meta.(*ArmClient) ctx := armClient.StopContext - resourceGroupName := d.Get("resource_group_name").(string) - storageAccountName := d.Get("storage_account_name").(string) + id, err := parseStorageTableID(d.Id()) + if err != nil { + return err + } - tableClient, accountExists, err := armClient.getTableServiceClientForStorageAccount(ctx, resourceGroupName, storageAccountName) + resourceGroup, err := determineResourceGroupForStorageAccount(id.storageAccountName, armClient) + if err != nil { + return err + } + + if resourceGroup == nil { + log.Printf("Unable to determine Resource Group for Storage Account %q (assuming removed)", id.storageAccountName) + return nil + } + + tableClient, accountExists, err := armClient.getTableServiceClientForStorageAccount(ctx, *resourceGroup, id.storageAccountName) if err != nil { return err } if !accountExists { - log.Printf("[INFO] Storage Account %q doesn't exist so the table won't exist", storageAccountName) + log.Printf("[INFO] Storage Account %q doesn't exist so the table won't exist", id.storageAccountName) return nil } - name := d.Get("name").(string) - table := tableClient.GetTableReference(name) + table := tableClient.GetTableReference(id.tableName) timeout := uint(60) options := &storage.TableOptions{} - log.Printf("[INFO] Deleting storage table %q in account %q", name, storageAccountName) + log.Printf("[INFO] Deleting Table %q in Storage Account %q", id.tableName, id.storageAccountName) if err := table.Delete(timeout, options); err != nil { - return fmt.Errorf("Error deleting storage table %q from storage account %q: %s", name, storageAccountName, err) + return fmt.Errorf("Error deleting table %q from Storage Account %q: %s", id.tableName, id.storageAccountName, err) } - d.SetId("") return nil } + +type storageTableId struct { + storageAccountName string + tableName string +} + +func parseStorageTableID(input string) (*storageTableId, error) { + // https://myaccount.table.core.windows.net/table1 + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("Error parsing %q as a URI: %+v", input, err) + } + + segments := strings.Split(uri.Host, ".") + if len(segments) > 0 { + storageAccountName := segments[0] + table := strings.Replace(uri.Path, "/", "", 1) + id := storageTableId{ + storageAccountName: storageAccountName, + tableName: table, + } + return &id, nil + } + + return nil, nil +} diff --git a/azurerm/resource_arm_storage_table_migration.go b/azurerm/resource_arm_storage_table_migration.go new file mode 100644 index 000000000000..e2da0116cca7 --- /dev/null +++ b/azurerm/resource_arm_storage_table_migration.go @@ -0,0 +1,40 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/terraform" +) + +func resourceStorageTableMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AzureRM Storage Table State v0; migrating to v1") + return migrateStorageTableStateV0toV1(is, meta) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +func migrateStorageTableStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + log.Printf("[DEBUG] ARM Storage Table Attributes before Migration: %#v", is.Attributes) + + environment := meta.(*ArmClient).environment + + tableName := is.Attributes["name"] + storageAccountName := is.Attributes["storage_account_name"] + newID := fmt.Sprintf("https://%s.table.%s/%s", storageAccountName, environment.StorageEndpointSuffix, tableName) + is.Attributes["id"] = newID + is.ID = newID + + log.Printf("[DEBUG] ARM Storage Table Attributes after State Migration: %#v", is.Attributes) + + return is, nil +} diff --git a/azurerm/resource_arm_storage_table_migration_test.go b/azurerm/resource_arm_storage_table_migration_test.go new file mode 100644 index 000000000000..198468243c39 --- /dev/null +++ b/azurerm/resource_arm_storage_table_migration_test.go @@ -0,0 +1,66 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +// NOTE: this is intentionally an acceptance test (and we're not explicitly setting the env) +// as we want to run this depending on the cloud we're in. +func TestAccAzureRMStorageTableMigrateState(t *testing.T) { + config := testGetAzureConfig(t) + if config == nil { + t.SkipNow() + return + } + + client, err := getArmClient(config) + if err != nil { + t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) + return + } + + client.StopContext = testAccProvider.StopContext() + + suffix := client.environment.StorageEndpointSuffix + + cases := map[string]struct { + StateVersion int + ID string + InputAttributes map[string]string + ExpectedAttributes map[string]string + }{ + "v0_1_without_value": { + StateVersion: 0, + ID: "some_id", + InputAttributes: map[string]string{ + "name": "table1", + "storage_account_name": "example", + }, + ExpectedAttributes: map[string]string{ + "id": fmt.Sprintf("https://example.table.%s/table1", suffix), + }, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: tc.ID, + Attributes: tc.InputAttributes, + } + is, err := resourceStorageTableMigrateState(tc.StateVersion, is, client) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.ExpectedAttributes { + actual := is.Attributes[k] + if actual != v { + t.Fatalf("Bad Storage Table Migrate for %q: %q\n\n expected: %q", k, actual, v) + } + } + } +} diff --git a/azurerm/resource_arm_storage_table_test.go b/azurerm/resource_arm_storage_table_test.go index 983c3ff5edc8..4014787ef35f 100644 --- a/azurerm/resource_arm_storage_table_test.go +++ b/azurerm/resource_arm_storage_table_test.go @@ -13,6 +13,7 @@ import ( ) func TestAccAzureRMStorageTable_basic(t *testing.T) { + resourceName := "azurerm_storage_table.test" var table storage.Table ri := acctest.RandInt() @@ -27,9 +28,14 @@ func TestAccAzureRMStorageTable_basic(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - testCheckAzureRMStorageTableExists("azurerm_storage_table.test", &table), + testCheckAzureRMStorageTableExists(resourceName, &table), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/website/docs/r/storage_blob.html.markdown b/website/docs/r/storage_blob.html.markdown index 12c3d369ff09..e0782c81c9de 100644 --- a/website/docs/r/storage_blob.html.markdown +++ b/website/docs/r/storage_blob.html.markdown @@ -79,5 +79,13 @@ The following arguments are supported: The following attributes are exported in addition to the arguments listed above: -* `id` - The storage blob Resource ID. +* `id` - The ID of the Storage Blob. * `url` - The URL of the blob + +## Import + +Storage Blob's can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_storage_blob.blob1 https://example.blob.core.windows.net/container/blob.vhd +``` diff --git a/website/docs/r/storage_container.html.markdown b/website/docs/r/storage_container.html.markdown index 75646f7ca57b..2ad592eb1899 100644 --- a/website/docs/r/storage_container.html.markdown +++ b/website/docs/r/storage_container.html.markdown @@ -56,5 +56,13 @@ The following arguments are supported: The following attributes are exported in addition to the arguments listed above: -* `id` - The storage container Resource ID. +* `id` - The ID of the Storage Container. * `properties` - Key-value definition of additional properties associated to the storage container + +## Import + +Storage Containers can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_storage_container.container1 https://example.blob.core.windows.net/container +``` diff --git a/website/docs/r/storage_queue.html.markdown b/website/docs/r/storage_queue.html.markdown index a5619267a897..dacadf36bdb9 100644 --- a/website/docs/r/storage_queue.html.markdown +++ b/website/docs/r/storage_queue.html.markdown @@ -49,4 +49,12 @@ The following arguments are supported: The following attributes are exported in addition to the arguments listed above: -* `id` - The storage queue Resource ID. +* `id` - The ID of the Storage Queue. + +## Import + +Storage Queue's can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_storage_queue.queue1 https://example.queue.core.windows.net/queue1 +``` diff --git a/website/docs/r/storage_table.html.markdown b/website/docs/r/storage_table.html.markdown index 4bc10fb20d35..e383c0445feb 100644 --- a/website/docs/r/storage_table.html.markdown +++ b/website/docs/r/storage_table.html.markdown @@ -49,4 +49,12 @@ The following arguments are supported: The following attributes are exported in addition to the arguments listed above: -* `id` - The storage table Resource ID. +* `id` - The ID of the Storage Table. + +## Import + +Storage Table's can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_storage_table.table1 https://example.table.core.windows.net/table1 +```