From 889a98fd1987362ad1a43f33ed93a92dce4ed47c Mon Sep 17 00:00:00 2001 From: Joseph Denheen <39860623+DenheenJ@users.noreply.github.com> Date: Sun, 18 Aug 2019 19:49:28 +0100 Subject: [PATCH] New Resource: `azurerm_cosmosdb_sql_container` (#3871) --- azurerm/helpers/azure/cosmos.go | 22 ++ azurerm/provider.go | 1 + .../resource_arm_cosmosdb_mongo_collection.go | 11 +- .../resource_arm_cosmosdb_sql_container.go | 260 ++++++++++++++++++ ...esource_arm_cosmosdb_sql_container_test.go | 185 +++++++++++++ scripts/gofmtcheck.sh | 2 +- .../r/cosmosdb_sql_container.html.markdown | 66 +++++ 7 files changed, 541 insertions(+), 6 deletions(-) create mode 100644 azurerm/resource_arm_cosmosdb_sql_container.go create mode 100644 azurerm/resource_arm_cosmosdb_sql_container_test.go create mode 100644 website/docs/r/cosmosdb_sql_container.html.markdown diff --git a/azurerm/helpers/azure/cosmos.go b/azurerm/helpers/azure/cosmos.go index f7f21f8c743c..c3a9b09675a6 100644 --- a/azurerm/helpers/azure/cosmos.go +++ b/azurerm/helpers/azure/cosmos.go @@ -90,6 +90,28 @@ func ParseCosmosDatabaseCollectionID(id string) (*CosmosDatabaseCollectionID, er }, nil } +type CosmosDatabaseContainerID struct { + CosmosDatabaseID + Container string +} + +func ParseCosmosDatabaseContainerID(id string) (*CosmosDatabaseContainerID, error) { + subid, err := ParseCosmosDatabaseID(id) + if err != nil { + return nil, err + } + + container, ok := subid.Path["containers"] + if !ok { + return nil, fmt.Errorf("Error: Unable to parse Cosmos Database Container Resource ID: containers is missing from: %s", id) + } + + return &CosmosDatabaseContainerID{ + CosmosDatabaseID: *subid, + Container: container, + }, nil +} + type CosmosKeyspaceID struct { CosmosAccountID Keyspace string diff --git a/azurerm/provider.go b/azurerm/provider.go index b458a1b38ead..656f10c15c87 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -183,6 +183,7 @@ func Provider() terraform.ResourceProvider { "azurerm_cosmosdb_cassandra_keyspace": resourceArmCosmosDbCassandraKeyspace(), "azurerm_cosmosdb_mongo_collection": resourceArmCosmosDbMongoCollection(), "azurerm_cosmosdb_mongo_database": resourceArmCosmosDbMongoDatabase(), + "azurerm_cosmosdb_sql_container": resourceArmCosmosDbSQLContainer(), "azurerm_cosmosdb_sql_database": resourceArmCosmosDbSQLDatabase(), "azurerm_cosmosdb_table": resourceArmCosmosDbTable(), "azurerm_data_factory": resourceArmDataFactory(), diff --git a/azurerm/resource_arm_cosmosdb_mongo_collection.go b/azurerm/resource_arm_cosmosdb_mongo_collection.go index bdca49f6cc79..47c91d2ad4ed 100644 --- a/azurerm/resource_arm_cosmosdb_mongo_collection.go +++ b/azurerm/resource_arm_cosmosdb_mongo_collection.go @@ -195,12 +195,13 @@ func resourceArmCosmosDbMongoCollectionRead(d *schema.ResourceData, meta interfa d.Set("shard_key", k) } - indexes, ttl := flattenCosmosMongoCollectionIndexes(props.Indexes) - d.Set("default_ttl_seconds", ttl) - if err := d.Set("indexes", indexes); err != nil { - return fmt.Errorf("Error setting `indexes`: %+v", err) + if props.Indexes != nil { + indexes, ttl := flattenCosmosMongoCollectionIndexes(props.Indexes) + d.Set("default_ttl_seconds", ttl) + if err := d.Set("indexes", indexes); err != nil { + return fmt.Errorf("Error setting `indexes`: %+v", err) + } } - } return nil diff --git a/azurerm/resource_arm_cosmosdb_sql_container.go b/azurerm/resource_arm_cosmosdb_sql_container.go new file mode 100644 index 000000000000..60234ce017bb --- /dev/null +++ b/azurerm/resource_arm_cosmosdb_sql_container.go @@ -0,0 +1,260 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb" + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmCosmosDbSQLContainer() *schema.Resource { + return &schema.Resource{ + Create: resourceArmCosmosDbSQLContainerCreate, + Read: resourceArmCosmosDbSQLContainerRead, + Delete: resourceArmCosmosDbSQLContainerDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.CosmosEntityName, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "account_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.CosmosAccountName, + }, + + "database_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.CosmosEntityName, + }, + + "partition_key_path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "unique_key": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "paths": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validate.NoEmptyStrings, + }, + }, + }, + }, + }, + }, + } +} + +func resourceArmCosmosDbSQLContainerCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).cosmos.DatabaseClient + ctx := meta.(*ArmClient).StopContext + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + database := d.Get("database_name").(string) + account := d.Get("account_name").(string) + partitionkeypaths := d.Get("partition_key_path").(string) + + if requireResourcesToBeImported && d.IsNewResource() { + existing, err := client.GetSQLContainer(ctx, resourceGroup, account, database, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of creating Cosmos SQL Container %s (Account: %s, Database:%s): %+v", name, account, database, err) + } + } else { + id, err := azure.CosmosGetIDFromResponse(existing.Response) + if err != nil { + return fmt.Errorf("Error generating import ID for Cosmos SQL Container '%s' (Account: %s, Database:%s)", name, account, database) + } + + return tf.ImportAsExistsError("azurerm_cosmosdb_sql_container", id) + } + } + + db := documentdb.SQLContainerCreateUpdateParameters{ + SQLContainerCreateUpdateProperties: &documentdb.SQLContainerCreateUpdateProperties{ + Resource: &documentdb.SQLContainerResource{ + ID: &name, + }, + Options: map[string]*string{}, + }, + } + + if partitionkeypaths != "" { + db.SQLContainerCreateUpdateProperties.Resource.PartitionKey = &documentdb.ContainerPartitionKey{ + Paths: &[]string{partitionkeypaths}, + Kind: documentdb.PartitionKindHash, + } + } + + if keys := expandCosmosSQLContainerUniqueKeys(d.Get("unique_key").(*schema.Set)); keys != nil { + db.SQLContainerCreateUpdateProperties.Resource.UniqueKeyPolicy = &documentdb.UniqueKeyPolicy{ + UniqueKeys: keys, + } + } + + future, err := client.CreateUpdateSQLContainer(ctx, resourceGroup, account, database, name, db) + if err != nil { + return fmt.Errorf("Error issuing create/update request for Cosmos SQL Container %s (Account: %s, Database:%s): %+v", name, account, database, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting on create/update future for Cosmos SQL Container %s (Account: %s, Database:%s): %+v", name, account, database, err) + } + + resp, err := client.GetSQLContainer(ctx, resourceGroup, account, database, name) + if err != nil { + return fmt.Errorf("Error making get request for Cosmos SQL Container %s (Account: %s, Database:%s): %+v", name, account, database, err) + } + + id, err := azure.CosmosGetIDFromResponse(resp.Response) + if err != nil { + return fmt.Errorf("Error retrieving the ID for Cosmos SQL Container '%s' (Account: %s, Database:%s) ID: %v", name, account, database, err) + } + d.SetId(id) + + return resourceArmCosmosDbSQLContainerRead(d, meta) +} + +func resourceArmCosmosDbSQLContainerRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).cosmos.DatabaseClient + ctx := meta.(*ArmClient).StopContext + + id, err := azure.ParseCosmosDatabaseContainerID(d.Id()) + if err != nil { + return err + } + + resp, err := client.GetSQLContainer(ctx, id.ResourceGroup, id.Account, id.Database, id.Container) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Error reading Cosmos SQL Container %s (Account %s) - removing from state", id.Database, id.Container) + d.SetId("") + return nil + } + + return fmt.Errorf("Error reading Cosmos SQL Container %s (Account %s): %+v", id.Database, id.Container, err) + } + + d.Set("name", id.Container) + d.Set("resource_group_name", id.ResourceGroup) + d.Set("account_name", id.Account) + d.Set("database_name", id.Database) + + if props := resp.SQLContainerProperties; props != nil { + if pk := props.PartitionKey; pk != nil { + if paths := pk.Paths; paths != nil { + if len(*paths) > 1 { + return fmt.Errorf("Error reading PartitionKey Paths, more then 1 returned") + } else if len(*paths) == 1 { + d.Set("partition_key_path", (*paths)[0]) + } + } + } + + if ukp := props.UniqueKeyPolicy; ukp != nil { + if err := d.Set("unique_key", flattenCosmosSQLContainerUniqueKeys(ukp.UniqueKeys)); err != nil { + return fmt.Errorf("Error setting `unique_key`: %+v", err) + } + } + } + + return nil +} + +func resourceArmCosmosDbSQLContainerDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).cosmos.DatabaseClient + ctx := meta.(*ArmClient).StopContext + + id, err := azure.ParseCosmosDatabaseContainerID(d.Id()) + if err != nil { + return err + } + + future, err := client.DeleteSQLContainer(ctx, id.ResourceGroup, id.Account, id.Database, id.Container) + if err != nil { + if !response.WasNotFound(future.Response()) { + return fmt.Errorf("Error deleting Cosmos SQL Container %s (Account %s): %+v", id.Database, id.Container, err) + } + } + + err = future.WaitForCompletionRef(ctx, client.Client) + if err != nil { + return fmt.Errorf("Error waiting on delete future for Cosmos SQL Container %s (Account %s): %+v", id.Database, id.Account, err) + } + + return nil +} + +func expandCosmosSQLContainerUniqueKeys(s *schema.Set) *[]documentdb.UniqueKey { + i := s.List() + if len(i) <= 0 || i[0] == nil { + return nil + } + + keys := make([]documentdb.UniqueKey, 0) + for _, k := range i { + key := k.(map[string]interface{}) + + paths := key["paths"].(*schema.Set).List() + if len(paths) == 0 { + continue + } + + keys = append(keys, documentdb.UniqueKey{ + Paths: utils.ExpandStringSlice(paths), + }) + } + + return &keys +} + +func flattenCosmosSQLContainerUniqueKeys(keys *[]documentdb.UniqueKey) *[]map[string]interface{} { + if keys == nil { + return nil + } + + slice := make([]map[string]interface{}, 0) + for _, k := range *keys { + + if k.Paths == nil { + continue + } + + slice = append(slice, map[string]interface{}{ + "paths": *k.Paths, + }) + } + + return &slice +} diff --git a/azurerm/resource_arm_cosmosdb_sql_container_test.go b/azurerm/resource_arm_cosmosdb_sql_container_test.go new file mode 100644 index 000000000000..c4e9d8e6c741 --- /dev/null +++ b/azurerm/resource_arm_cosmosdb_sql_container_test.go @@ -0,0 +1,185 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMCosmosDbSqlContainer_basic(t *testing.T) { + ri := tf.AccRandTimeInt() + resourceName := "azurerm_cosmosdb_sql_container.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMCosmosDbSqlContainerDestroy, + Steps: []resource.TestStep{ + { + + Config: testAccAzureRMCosmosDbSqlContainer_basic(ri, testLocation()), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAzureRMCosmosDbSqlContainerExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMCosmosDbSqlContainer_complete(t *testing.T) { + ri := tf.AccRandTimeInt() + resourceName := "azurerm_cosmosdb_sql_container.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMCosmosDbSqlContainerDestroy, + Steps: []resource.TestStep{ + { + + Config: testAccAzureRMCosmosDbSqlContainer_complete(ri, testLocation()), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAzureRMCosmosDbSqlContainerExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMCosmosDbSqlContainer_update(t *testing.T) { + ri := tf.AccRandTimeInt() + resourceName := "azurerm_cosmosdb_sql_container.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMCosmosDbSqlContainerDestroy, + Steps: []resource.TestStep{ + { + + Config: testAccAzureRMCosmosDbSqlContainer_basic(ri, testLocation()), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAzureRMCosmosDbSqlContainerExists(resourceName), + ), + }, + { + + Config: testAccAzureRMCosmosDbSqlContainer_complete(ri, testLocation()), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAzureRMCosmosDbSqlContainerExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureRMCosmosDbSqlContainerDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).cosmos.DatabaseClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_cosmosdb_sql_container" { + continue + } + + name := rs.Primary.Attributes["name"] + account := rs.Primary.Attributes["account_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + database := rs.Primary.Attributes["database_name"] + + resp, err := client.GetSQLContainer(ctx, resourceGroup, account, database, name) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Error checking destroy for Cosmos SQL Container %s (account %s) still exists:\n%v", name, account, err) + } + } + + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Cosmos SQL Container %s (account %s) still exists:\n%#v", name, account, resp) + } + } + + return nil +} + +func testCheckAzureRMCosmosDbSqlContainerExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).cosmos.DatabaseClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + name := rs.Primary.Attributes["name"] + account := rs.Primary.Attributes["account_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + database := rs.Primary.Attributes["database_name"] + + resp, err := client.GetSQLContainer(ctx, resourceGroup, database, account, name) + if err != nil { + return fmt.Errorf("Bad: Get on cosmosAccountsClient: %+v", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Cosmos Container '%s' (account: '%s') does not exist", name, account) + } + + return nil + } +} + +func testAccAzureRMCosmosDbSqlContainer_basic(rInt int, location string) string { + return fmt.Sprintf(` +%[1]s + +resource "azurerm_cosmosdb_sql_container" "test" { + name = "acctest-CSQLC-%[2]d" + resource_group_name = "${azurerm_cosmosdb_account.test.resource_group_name}" + account_name = "${azurerm_cosmosdb_account.test.name}" + database_name = "${azurerm_cosmosdb_sql_database.test.name}" +} + + +`, testAccAzureRMCosmosDbSqlDatabase_basic(rInt, location), rInt) +} + +func testAccAzureRMCosmosDbSqlContainer_complete(rInt int, location string) string { + return fmt.Sprintf(` +%[1]s + +resource "azurerm_cosmosdb_sql_container" "test" { + name = "acctest-CSQLC-%[2]d" + resource_group_name = "${azurerm_cosmosdb_account.test.resource_group_name}" + account_name = "${azurerm_cosmosdb_account.test.name}" + database_name = "${azurerm_cosmosdb_sql_database.test.name}" + partition_key_path = "/definition/id" + unique_key { + paths = ["/definition/id1", "/definition/id2"] + } +} + +`, testAccAzureRMCosmosDbSqlDatabase_basic(rInt, location), rInt) +} diff --git a/scripts/gofmtcheck.sh b/scripts/gofmtcheck.sh index b09264530093..80bafdb3a269 100755 --- a/scripts/gofmtcheck.sh +++ b/scripts/gofmtcheck.sh @@ -12,4 +12,4 @@ if [ -n "${gofmt_files}" ]; then exit 1 fi -exit 0 +exit 0 \ No newline at end of file diff --git a/website/docs/r/cosmosdb_sql_container.html.markdown b/website/docs/r/cosmosdb_sql_container.html.markdown new file mode 100644 index 000000000000..7181256adbf1 --- /dev/null +++ b/website/docs/r/cosmosdb_sql_container.html.markdown @@ -0,0 +1,66 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_cosmosdb_sql_container" +sidebar_current: "docs-azurerm-resource-cosmosdb-sql-container" +description: |- + Manages a SQL Container within a Cosmos DB Account. +--- + +# azurerm_cosmosdb_sql_container + +Manages a SQL Container within a Cosmos DB Account. + +## Example Usage + +```hcl + +resource "azurerm_cosmosdb_sql_container" "example" { + name = "example-container" + resource_group_name = "${azurerm_cosmosdb_account.example.resource_group_name}" + account_name = "${azurerm_cosmosdb_account.example.name}" + database_name = "${azurerm_cosmosdb_sql_database.example.name}" + partition_key_path = "/definition/id" + + unique_keys { + paths = ["/definition/idlong", "/definition/idshort"] + } +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the Cosmos DB SQL Database. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which the Cosmos DB SQL Database is created. Changing this forces a new resource to be created. + +* `account_name` - (Required) The name of the Cosmos DB Account to create the container within. Changing this forces a new resource to be created. + +* `database_name` - (Required) The name of the Cosmos DB SQL Database to create the container within. Changing this forces a new resource to be created. + +* `partition_key_path` - (Optional) Define a partition key. Changing this forces a new resource to be created. + +* `unique_key` - (Optional) One or more `unique_key` blocks as defined below. Changing this forces a new resource to be created. + +--- +A `unique_key` block supports the following: + +* `paths` - (Required) A list of paths to use for this unique key. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - the Cosmos DB SQL Database ID. + +## Import + +Cosmos SQL Database can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_cosmosdb_sql_container.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1/apis/sql/databases/database1/containers/example +``` +