Repository: getmoto/moto

Issue:
Error: Cant create DynamoDB table
Version: I used the actual master.

The terraform code:

```
resource "aws_dynamodb_table" "grades" {
  name         = "${var.service_name}.grades"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"
  range_key    = "reference"

  attribute {
    name = "id"
    type = "S"
  }

  attribute {
    name = "reference"
    type = "S"
  }

  global_secondary_index {
    name            = "byReference"
    hash_key        = "reference"
    projection_type = "ALL"
  }

  lifecycle {
    prevent_destroy = true
  }

  tags = local.tags
}
```

The debug output of terraform:

```
2020-06-12T14:48:02.175+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 2020/06/12 14:48:02 [DEBUG] [aws-sdk-go] {"Table": {"AttributeDefinitions": [{"AttributeName": "reference", "AttributeType": "S"}, {"AttributeName": "id", "AttributeType": "S"}], "ProvisionedThroughput": {"WriteCapacityUnits": 10, "ReadCapacityUnits": 10, "NumberOfDecreasesToday": 0}, "TableSizeBytes": 0, "TableName": "componenttest-widget-service.grades", "TableStatus": "ACTIVE", "TableArn": "arn:aws:dynamodb:us-east-1:123456789011:table/componenttest-widget-service.grades", "KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}, {"AttributeName": "reference", "KeyType": "RANGE"}], "ItemCount": 0, "CreationDateTime": 1591966082.035368, "GlobalSecondaryIndexes": [{"IndexName": "byReference", "KeySchema": [{"AttributeName": "reference", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}, "IndexStatus": "ACTIVE"}], "LocalSecondaryIndexes": []}}
```

```
 runtime error: invalid memory address or nil pointer dereference
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: [signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x4ea19ab]
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4:
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: goroutine 134 [running]:
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: github.com/terraform-providers/terraform-provider-aws/aws.flattenAwsDynamoDbTableResource(0xc001406b60, 0xc0012eec00, 0xc000652270, 0x0)
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/aws/structure.go:4149 +0x105b
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: github.com/terraform-providers/terraform-provider-aws/aws.resourceAwsDynamoDbTableRead(0xc001406b60, 0x5b37620, 0xc000301900, 0x64b2400, 0xc000e7fd20)
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/aws/resource_aws_dynamodb_table.go:665 +0x330
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: github.com/terraform-providers/terraform-provider-aws/aws.resourceAwsDynamoDbTableCreate(0xc001406b60, 0x5b37620, 0xc000301900, 0x2, 0xa406520)
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/aws/resource_aws_dynamodb_table.go:439 +0x1548
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: github.com/hashicorp/terraform-plugin-sdk/helper/schema.(*Resource).Apply(0xc000566000, 0xc000a346e0, 0xc0010007e0, 0x5b37620, 0xc000301900, 0xc0008c0701, 0xc000d52990, 0xc0008c0790)
2020-06-12T15:26:43.227+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/github.com/hashicorp/terraform-plugin-sdk/helper/schema/resource.go:310 +0x365
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: github.com/hashicorp/terraform-plugin-sdk/helper/schema.(*Provider).Apply(0xc000152180, 0xc000df59c8, 0xc000a346e0, 0xc0010007e0, 0xc000881828, 0xc0011aa2e0, 0x5946b60)
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/github.com/hashicorp/terraform-plugin-sdk/helper/schema/provider.go:294 +0x99
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: github.com/hashicorp/terraform-plugin-sdk/internal/helper/plugin.(*GRPCProviderServer).ApplyResourceChange(0xc000595138, 0x702f440, 0xc000b0df80, 0xc001406540, 0xc000595138, 0xc000b0df80, 0xc000e17b30)
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/github.com/hashicorp/terraform-plugin-sdk/internal/helper/plugin/grpc_provider.go:885 +0x8b4
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: github.com/hashicorp/terraform-plugin-sdk/internal/tfplugin5._Provider_ApplyResourceChange_Handler(0x64da560, 0xc000595138, 0x702f440, 0xc000b0df80, 0xc001178ba0, 0x0, 0x702f440, 0xc000b0df80, 0xc001366b00, 0x562)
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/github.com/hashicorp/terraform-plugin-sdk/internal/tfplugin5/tfplugin5.pb.go:3305 +0x217
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: google.golang.org/grpc.(*Server).processUnaryRPC(0xc0008a9680, 0x7051c00, 0xc000e0e480, 0xc0012ee100, 0xc000958cc0, 0xa3d9780, 0x0, 0x0, 0x0)
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/google.golang.org/grpc/server.go:1024 +0x4f4
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: google.golang.org/grpc.(*Server).handleStream(0xc0008a9680, 0x7051c00, 0xc000e0e480, 0xc0012ee100, 0x0)
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/google.golang.org/grpc/server.go:1313 +0xd97
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: google.golang.org/grpc.(*Server).serveStreams.func1.1(0xc00013a0e0, 0xc0008a9680, 0x7051c00, 0xc000e0e480, 0xc0012ee100)
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/google.golang.org/grpc/server.go:722 +0xbb
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: created by google.golang.org/grpc.(*Server).serveStreams.func1
2020-06-12T15:26:43.228+0200 [DEBUG] plugin.terraform-provider-aws_v2.66.0_x4: 	/opt/teamcity-agent/work/5d79fe75d4460a2f/src/github.com/terraform-providers/terraform-provider-aws/vendor/google.golang.org/grpc/server.go:720 +0xa1
```

The problem seems to be the global secondary index.


Discussion:

Resolving patch:
diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py
--- a/moto/dynamodb2/models/__init__.py
+++ b/moto/dynamodb2/models/__init__.py
@@ -272,6 +272,66 @@ def get(self, start, quantity):
         return [i.to_json() for i in self.items[start:end]]
 
 
+class LocalSecondaryIndex(BaseModel):
+    def __init__(self, index_name, schema, projection):
+        self.name = index_name
+        self.schema = schema
+        self.projection = projection
+
+    def describe(self):
+        return {
+            "IndexName": self.name,
+            "KeySchema": self.schema,
+            "Projection": self.projection,
+        }
+
+    @staticmethod
+    def create(dct):
+        return LocalSecondaryIndex(
+            index_name=dct["IndexName"],
+            schema=dct["KeySchema"],
+            projection=dct["Projection"],
+        )
+
+
+class GlobalSecondaryIndex(BaseModel):
+    def __init__(
+        self, index_name, schema, projection, status="ACTIVE", throughput=None
+    ):
+        self.name = index_name
+        self.schema = schema
+        self.projection = projection
+        self.status = status
+        self.throughput = throughput or {
+            "ReadCapacityUnits": 0,
+            "WriteCapacityUnits": 0,
+        }
+
+    def describe(self):
+        return {
+            "IndexName": self.name,
+            "KeySchema": self.schema,
+            "Projection": self.projection,
+            "IndexStatus": self.status,
+            "ProvisionedThroughput": self.throughput,
+        }
+
+    @staticmethod
+    def create(dct):
+        return GlobalSecondaryIndex(
+            index_name=dct["IndexName"],
+            schema=dct["KeySchema"],
+            projection=dct["Projection"],
+            throughput=dct.get("ProvisionedThroughput", None),
+        )
+
+    def update(self, u):
+        self.name = u.get("IndexName", self.name)
+        self.schema = u.get("KeySchema", self.schema)
+        self.projection = u.get("Projection", self.projection)
+        self.throughput = u.get("ProvisionedThroughput", self.throughput)
+
+
 class Table(BaseModel):
     def __init__(
         self,
@@ -302,12 +362,13 @@ def __init__(
         else:
             self.throughput = throughput
         self.throughput["NumberOfDecreasesToday"] = 0
-        self.indexes = indexes
-        self.global_indexes = global_indexes if global_indexes else []
-        for index in self.global_indexes:
-            index[
-                "IndexStatus"
-            ] = "ACTIVE"  # One of 'CREATING'|'UPDATING'|'DELETING'|'ACTIVE'
+        self.indexes = [
+            LocalSecondaryIndex.create(i) for i in (indexes if indexes else [])
+        ]
+        self.global_indexes = [
+            GlobalSecondaryIndex.create(i)
+            for i in (global_indexes if global_indexes else [])
+        ]
         self.created_at = datetime.datetime.utcnow()
         self.items = defaultdict(dict)
         self.table_arn = self._generate_arn(table_name)
@@ -374,8 +435,10 @@ def describe(self, base_key="TableDescription"):
                 "KeySchema": self.schema,
                 "ItemCount": len(self),
                 "CreationDateTime": unix_time(self.created_at),
-                "GlobalSecondaryIndexes": [index for index in self.global_indexes],
-                "LocalSecondaryIndexes": [index for index in self.indexes],
+                "GlobalSecondaryIndexes": [
+                    index.describe() for index in self.global_indexes
+                ],
+                "LocalSecondaryIndexes": [index.describe() for index in self.indexes],
             }
         }
         if self.stream_specification and self.stream_specification["StreamEnabled"]:
@@ -401,7 +464,7 @@ def hash_key_names(self):
         keys = [self.hash_key_attr]
         for index in self.global_indexes:
             hash_key = None
-            for key in index["KeySchema"]:
+            for key in index.schema:
                 if key["KeyType"] == "HASH":
                     hash_key = key["AttributeName"]
             keys.append(hash_key)
@@ -412,7 +475,7 @@ def range_key_names(self):
         keys = [self.range_key_attr]
         for index in self.global_indexes:
             range_key = None
-            for key in index["KeySchema"]:
+            for key in index.schema:
                 if key["KeyType"] == "RANGE":
                     range_key = keys.append(key["AttributeName"])
             keys.append(range_key)
@@ -545,7 +608,7 @@ def query(
 
         if index_name:
             all_indexes = self.all_indexes()
-            indexes_by_name = dict((i["IndexName"], i) for i in all_indexes)
+            indexes_by_name = dict((i.name, i) for i in all_indexes)
             if index_name not in indexes_by_name:
                 raise ValueError(
                     "Invalid index: %s for table: %s. Available indexes are: %s"
@@ -555,14 +618,14 @@ def query(
             index = indexes_by_name[index_name]
             try:
                 index_hash_key = [
-                    key for key in index["KeySchema"] if key["KeyType"] == "HASH"
+                    key for key in index.schema if key["KeyType"] == "HASH"
                 ][0]
             except IndexError:
-                raise ValueError("Missing Hash Key. KeySchema: %s" % index["KeySchema"])
+                raise ValueError("Missing Hash Key. KeySchema: %s" % index.name)
 
             try:
                 index_range_key = [
-                    key for key in index["KeySchema"] if key["KeyType"] == "RANGE"
+                    key for key in index.schema if key["KeyType"] == "RANGE"
                 ][0]
             except IndexError:
                 index_range_key = None
@@ -667,9 +730,9 @@ def all_indexes(self):
     def has_idx_items(self, index_name):
 
         all_indexes = self.all_indexes()
-        indexes_by_name = dict((i["IndexName"], i) for i in all_indexes)
+        indexes_by_name = dict((i.name, i) for i in all_indexes)
         idx = indexes_by_name[index_name]
-        idx_col_set = set([i["AttributeName"] for i in idx["KeySchema"]])
+        idx_col_set = set([i["AttributeName"] for i in idx.schema])
 
         for hash_set in self.items.values():
             if self.range_key_attr:
@@ -692,7 +755,7 @@ def scan(
         results = []
         scanned_count = 0
         all_indexes = self.all_indexes()
-        indexes_by_name = dict((i["IndexName"], i) for i in all_indexes)
+        indexes_by_name = dict((i.name, i) for i in all_indexes)
 
         if index_name:
             if index_name not in indexes_by_name:
@@ -773,9 +836,9 @@ def _trim_results(self, results, limit, exclusive_start_key, scanned_index=None)
 
             if scanned_index:
                 all_indexes = self.all_indexes()
-                indexes_by_name = dict((i["IndexName"], i) for i in all_indexes)
+                indexes_by_name = dict((i.name, i) for i in all_indexes)
                 idx = indexes_by_name[scanned_index]
-                idx_col_list = [i["AttributeName"] for i in idx["KeySchema"]]
+                idx_col_list = [i["AttributeName"] for i in idx.schema]
                 for col in idx_col_list:
                     last_evaluated_key[col] = results[-1].attrs[col]
 
@@ -885,7 +948,7 @@ def update_table_streams(self, name, stream_specification):
 
     def update_table_global_indexes(self, name, global_index_updates):
         table = self.tables[name]
-        gsis_by_name = dict((i["IndexName"], i) for i in table.global_indexes)
+        gsis_by_name = dict((i.name, i) for i in table.global_indexes)
         for gsi_update in global_index_updates:
             gsi_to_create = gsi_update.get("Create")
             gsi_to_update = gsi_update.get("Update")
@@ -906,7 +969,7 @@ def update_table_global_indexes(self, name, global_index_updates):
                 if index_name not in gsis_by_name:
                     raise ValueError(
                         "Global Secondary Index does not exist, but tried to update: %s"
-                        % gsi_to_update["IndexName"]
+                        % index_name
                     )
                 gsis_by_name[index_name].update(gsi_to_update)
 
@@ -917,7 +980,9 @@ def update_table_global_indexes(self, name, global_index_updates):
                         % gsi_to_create["IndexName"]
                     )
 
-                gsis_by_name[gsi_to_create["IndexName"]] = gsi_to_create
+                gsis_by_name[gsi_to_create["IndexName"]] = GlobalSecondaryIndex.create(
+                    gsi_to_create
+                )
 
         # in python 3.6, dict.values() returns a dict_values object, but we expect it to be a list in other
         # parts of the codebase
diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py
--- a/moto/dynamodb2/responses.py
+++ b/moto/dynamodb2/responses.py
@@ -411,7 +411,6 @@ def _contains_duplicates(self, keys):
 
     def query(self):
         name = self.body["TableName"]
-        # {u'KeyConditionExpression': u'#n0 = :v0', u'ExpressionAttributeValues': {u':v0': {u'S': u'johndoe'}}, u'ExpressionAttributeNames': {u'#n0': u'username'}}
         key_condition_expression = self.body.get("KeyConditionExpression")
         projection_expression = self.body.get("ProjectionExpression")
         expression_attribute_names = self.body.get("ExpressionAttributeNames", {})
@@ -439,7 +438,7 @@ def query(self):
             index_name = self.body.get("IndexName")
             if index_name:
                 all_indexes = (table.global_indexes or []) + (table.indexes or [])
-                indexes_by_name = dict((i["IndexName"], i) for i in all_indexes)
+                indexes_by_name = dict((i.name, i) for i in all_indexes)
                 if index_name not in indexes_by_name:
                     er = "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
                     return self.error(
@@ -449,7 +448,7 @@ def query(self):
                         ),
                     )
 
-                index = indexes_by_name[index_name]["KeySchema"]
+                index = indexes_by_name[index_name].schema
             else:
                 index = table.schema
 


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py
--- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py
+++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py
@@ -931,6 +931,83 @@ def test_conflicting_writes():
 """
 
 
+@mock_dynamodb2
+def test_boto3_create_table_with_gsi():
+    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
+
+    table = dynamodb.create_table(
+        TableName="users",
+        KeySchema=[
+            {"AttributeName": "forum_name", "KeyType": "HASH"},
+            {"AttributeName": "subject", "KeyType": "RANGE"},
+        ],
+        AttributeDefinitions=[
+            {"AttributeName": "forum_name", "AttributeType": "S"},
+            {"AttributeName": "subject", "AttributeType": "S"},
+        ],
+        BillingMode="PAY_PER_REQUEST",
+        GlobalSecondaryIndexes=[
+            {
+                "IndexName": "test_gsi",
+                "KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}],
+                "Projection": {"ProjectionType": "ALL"},
+            }
+        ],
+    )
+    table["TableDescription"]["GlobalSecondaryIndexes"].should.equal(
+        [
+            {
+                "KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}],
+                "IndexName": "test_gsi",
+                "Projection": {"ProjectionType": "ALL"},
+                "IndexStatus": "ACTIVE",
+                "ProvisionedThroughput": {
+                    "ReadCapacityUnits": 0,
+                    "WriteCapacityUnits": 0,
+                },
+            }
+        ]
+    )
+
+    table = dynamodb.create_table(
+        TableName="users2",
+        KeySchema=[
+            {"AttributeName": "forum_name", "KeyType": "HASH"},
+            {"AttributeName": "subject", "KeyType": "RANGE"},
+        ],
+        AttributeDefinitions=[
+            {"AttributeName": "forum_name", "AttributeType": "S"},
+            {"AttributeName": "subject", "AttributeType": "S"},
+        ],
+        BillingMode="PAY_PER_REQUEST",
+        GlobalSecondaryIndexes=[
+            {
+                "IndexName": "test_gsi",
+                "KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}],
+                "Projection": {"ProjectionType": "ALL"},
+                "ProvisionedThroughput": {
+                    "ReadCapacityUnits": 3,
+                    "WriteCapacityUnits": 5,
+                },
+            }
+        ],
+    )
+    table["TableDescription"]["GlobalSecondaryIndexes"].should.equal(
+        [
+            {
+                "KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}],
+                "IndexName": "test_gsi",
+                "Projection": {"ProjectionType": "ALL"},
+                "IndexStatus": "ACTIVE",
+                "ProvisionedThroughput": {
+                    "ReadCapacityUnits": 3,
+                    "WriteCapacityUnits": 5,
+                },
+            }
+        ]
+    )
+
+
 @mock_dynamodb2
 def test_boto3_conditions():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")