forked from GoogleCloudPlatform/magic-modules
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce google_firestore_field resource. (GoogleCloudPlatform#7853)
* Introduce google_firestore_field resource. Introduces a single resource for managing Firestore fields, matching the REST API. Introduces 2 primary divergences from the proto/REST API: 1) Firestore fields do not support deletion (this is because all fields implicitly can exist in a schemaless system). Deletion of a terraform resource will clear any "overrides" for this field (TTL or indexing). 2) Single field indexes in the proto API were overly nested to allow reusing the same resource for composite indexes. The terraform resource unnests the 1-length field in the indexes list. fixes hashicorp/terraform-provider-google#12151 fixes hashicorp/terraform-provider-google#11419 * Address feedback in review (mostly minor cleanups). * Fix whitespace and add database to basic example.
- Loading branch information
1 parent
3c45bfe
commit 67764c9
Showing
13 changed files
with
590 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
# Copyright 2023 Google Inc. | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
--- !ruby/object:Api::Resource | ||
name: 'Field' | ||
base_url: projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields | ||
create_url: projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields/{{field}} | ||
self_link: '{{name}}' | ||
immutable: false | ||
update_verb: :PATCH | ||
update_mask: true | ||
create_verb: :PATCH | ||
description: | | ||
Represents a single field in the database. | ||
Fields are grouped by their "Collection Group", which represent all collections | ||
in the database with the same id. | ||
references: !ruby/object:Api::Resource::ReferenceLinks | ||
guides: | ||
'Official Documentation': 'https://cloud.google.com/firestore/docs/query-data/indexing' | ||
api: 'https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.collectionGroups.fields' | ||
async: !ruby/object:Api::OpAsync | ||
operation: !ruby/object:Api::OpAsync::Operation | ||
path: 'name' | ||
base_url: '{{op_id}}' | ||
wait_ms: 1000 | ||
result: !ruby/object:Api::OpAsync::Result | ||
path: 'response' | ||
resource_inside_response: true | ||
status: !ruby/object:Api::OpAsync::Status | ||
path: 'done' | ||
complete: true | ||
allowed: | ||
- true | ||
- false | ||
error: !ruby/object:Api::OpAsync::Error | ||
path: 'error' | ||
message: 'message' | ||
autogen_async: true | ||
skip_sweeper: true | ||
import_format: ["{{name}}"] | ||
docs: !ruby/object:Provider::Terraform::Docs | ||
warning: | | ||
This resource creates a Firestore Single Field override on a project that | ||
already has a Firestore database. If you haven't already created it, you may | ||
create a `google_firestore_database` resource with `location_id` set to your | ||
chosen location. | ||
examples: | ||
- !ruby/object:Provider::Terraform::Examples | ||
name: "firestore_field_basic" | ||
primary_resource_id: "basic" | ||
test_env_vars: | ||
project_id: :FIRESTORE_PROJECT_NAME | ||
- !ruby/object:Provider::Terraform::Examples | ||
name: "firestore_field_timestamp" | ||
primary_resource_id: "timestamp" | ||
test_env_vars: | ||
project_id: :FIRESTORE_PROJECT_NAME | ||
- !ruby/object:Provider::Terraform::Examples | ||
name: "firestore_field_match_override" | ||
primary_resource_id: "match_override" | ||
test_env_vars: | ||
project_id: :FIRESTORE_PROJECT_NAME | ||
custom_code: !ruby/object:Provider::Terraform::CustomCode | ||
custom_import: templates/terraform/custom_import/index_self_link_as_name_set_project.go.erb | ||
encoder: templates/terraform/encoders/firestore_field.go.erb | ||
custom_delete: templates/terraform/custom_delete/firestore_field_delete.go.erb | ||
test_check_destroy: templates/terraform/custom_check_destroy/firestore_field.go.erb | ||
properties: | ||
- !ruby/object:Api::Type::String | ||
name: 'database' | ||
default_value: '(default)' | ||
description: | | ||
The Firestore database id. Defaults to `"(default)"`. | ||
url_param_only: true | ||
- !ruby/object:Api::Type::String | ||
name: 'collection' | ||
description: | | ||
The id of the collection group to configure. | ||
required: true | ||
url_param_only: true | ||
- !ruby/object:Api::Type::String | ||
name: 'field' | ||
description: | | ||
The id of the field to configure. | ||
required: true | ||
url_param_only: true | ||
- !ruby/object:Api::Type::String | ||
name: name | ||
output: true | ||
description: | | ||
The name of this field. Format: | ||
`projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields/{{field}}` | ||
- !ruby/object:Api::Type::NestedObject | ||
name: indexConfig | ||
description: | | ||
The single field index configuration for this field. | ||
Creating an index configuration for this field will override any inherited configuration with the | ||
indexes specified. Configuring the index configuration with an empty block disables all indexes on | ||
the field. | ||
send_empty_value: true | ||
custom_expand: templates/terraform/custom_expand/firestore_field_index_config.go.erb | ||
custom_flatten: templates/terraform/custom_flatten/firestore_field_index_config.go.erb | ||
properties: | ||
- !ruby/object:Api::Type::Array | ||
name: indexes | ||
description: The indexes to configure on the field. Order or array contains must be specified. | ||
is_set: true | ||
item_type: !ruby/object:Api::Type::NestedObject | ||
properties: | ||
- !ruby/object:Api::Type::Enum | ||
name: queryScope | ||
description: | | ||
The scope at which a query is run. Collection scoped queries require you specify | ||
the collection at query time. Collection group scope allows queries across all | ||
collections with the same id. | ||
default_value: :COLLECTION | ||
values: | ||
- :COLLECTION | ||
- :COLLECTION_GROUP | ||
- !ruby/object:Api::Type::Enum | ||
name: 'order' | ||
description: | | ||
Indicates that this field supports ordering by the specified order or comparing using =, <, <=, >, >=, !=. | ||
Only one of `order` and `arrayConfig` can be specified. | ||
values: | ||
- :ASCENDING | ||
- :DESCENDING | ||
exactly_one_of: | ||
- order | ||
- arrayConfig | ||
- !ruby/object:Api::Type::Enum | ||
name: 'arrayConfig' | ||
description: | | ||
Indicates that this field supports operations on arrayValues. Only one of `order` and `arrayConfig` can | ||
be specified. | ||
values: | ||
- :CONTAINS | ||
exactly_one_of: | ||
- order | ||
- arrayConfig | ||
- !ruby/object:Api::Type::NestedObject | ||
name: ttlConfig | ||
custom_expand: templates/terraform/custom_expand/empty_object_if_set.go.erb | ||
description: | | ||
If set, this field is configured for TTL deletion. | ||
send_empty_value: true | ||
properties: | ||
- !ruby/object:Api::Type::Enum | ||
name: state | ||
description: | | ||
The state of the TTL configuration. | ||
output: true | ||
values: | ||
- :CREATING | ||
- :ACTIVE | ||
- :NEEDS_REPAIR |
29 changes: 29 additions & 0 deletions
29
mmv1/templates/terraform/custom_check_destroy/firestore_field.go.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Firestore fields are not deletable. We consider the field deleted if: | ||
// 1) the index configuration has no overrides and matches the ancestor configuration. | ||
// 2) the ttl configuration is unset. | ||
|
||
config := GoogleProviderConfig(t) | ||
|
||
url, err := replaceVarsForTest(config, rs, "{{FirestoreBasePath}}projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields/{{field}}") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
res, err := SendRequest(config, "GET", "", url, config.UserAgent, nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if v := res["indexConfig"]; v != nil { | ||
indexConfig := v.(map[string]interface{}) | ||
|
||
usesAncestorConfig, ok := indexConfig["usesAncestorConfig"].(bool) | ||
|
||
if !ok || !usesAncestorConfig { | ||
return fmt.Errorf("Index configuration is not using the ancestor config %s.", url) | ||
} | ||
} | ||
|
||
if res["ttlConfig"] != nil { | ||
return fmt.Errorf("TTL configuration was not deleted at %s.", url) | ||
} |
48 changes: 48 additions & 0 deletions
48
mmv1/templates/terraform/custom_delete/firestore_field_delete.go.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Firestore fields cannot be deleted, instead we clear the indexConfig and ttlConfig. | ||
|
||
log.Printf("[DEBUG] Deleting Field %q", d.Id()) | ||
|
||
billingProject := "" | ||
|
||
project, err := getProject(d, config) | ||
if err != nil { | ||
return fmt.Errorf("Error fetching project for App: %s", err) | ||
} | ||
billingProject = project | ||
|
||
url, err := ReplaceVars(d, config, "{{FirestoreBasePath}}{{name}}") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
updateMask := []string{"indexConfig", "ttlConfig"} | ||
|
||
url, err = AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
|
||
// err == nil indicates that the billing_project value was found | ||
if bp, err := getBillingProject(d, config); err == nil { | ||
billingProject = bp | ||
} | ||
|
||
// Clear fields by sending an empty PATCH request with appropriate update mask. | ||
req := make(map[string]interface{}) | ||
res, err := SendRequestWithTimeout(config, "PATCH", billingProject, url, userAgent, req, d.Timeout(schema.TimeoutUpdate)) | ||
|
||
if err != nil { | ||
return fmt.Errorf("Error deleting Field %q: %s", d.Id(), err) | ||
} | ||
|
||
err = FirestoreOperationWaitTime( | ||
config, res, project, "Deleting Field", userAgent, | ||
d.Timeout(schema.TimeoutDelete)) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
log.Printf("[DEBUG] Finished deleting Field %q", d.Id()) | ||
return nil |
20 changes: 20 additions & 0 deletions
20
mmv1/templates/terraform/custom_expand/empty_object_if_set.go.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/* | ||
* Expands an empty terraform config into an empty object. | ||
* | ||
* Used to differentate a user specifying an empty block versus a null/unset block. | ||
* | ||
* This is unique from send_empty_value, which will send an explicit null value | ||
* for empty configuration blocks. | ||
*/ | ||
func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { | ||
if v == nil { | ||
return nil, nil | ||
} | ||
|
||
l := v.([]interface{}) | ||
if len(l) == 0 { | ||
return nil, nil | ||
} | ||
// A set, but empty object. | ||
return struct{}{}, nil | ||
} |
52 changes: 52 additions & 0 deletions
52
mmv1/templates/terraform/custom_expand/firestore_field_index_config.go.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { | ||
// We drop all output only fields as they are unnecessary. | ||
if v == nil { | ||
return nil, nil | ||
} | ||
|
||
l := v.([]interface{}) | ||
if len(l) == 0 { | ||
return nil, nil | ||
} | ||
|
||
transformedIndexConfig := make(map[string]interface{}) | ||
|
||
// A configured, but empty, index_config block should be sent. This is how a user would remove all indexes. | ||
if l[0] == nil { | ||
return transformedIndexConfig, nil | ||
} | ||
|
||
indexConfig := l[0].(map[string]interface{}) | ||
|
||
// For Single field indexes, we put the field configuration on the index to avoid forced nesting. | ||
// Push all order/arrayConfig down into a single element fields list. | ||
l = indexConfig["indexes"].(*schema.Set).List() | ||
transformedIndexes := make([]interface{}, 0, len(l)) | ||
for _, raw := range l { | ||
if raw == nil { | ||
continue | ||
} | ||
original := raw.(map[string]interface{}) | ||
transformed := make(map[string]interface{}) | ||
transformedField := make(map[string]interface{}) | ||
|
||
if val := reflect.ValueOf(original["query_scope"]); val.IsValid() && !isEmptyValue(val) { | ||
transformed["queryScope"] = original["query_scope"] | ||
} | ||
|
||
if val := reflect.ValueOf(original["order"]); val.IsValid() && !isEmptyValue(val) { | ||
transformedField["order"] = original["order"] | ||
} | ||
|
||
if val := reflect.ValueOf(original["array_config"]); val.IsValid() && !isEmptyValue(val) { | ||
transformedField["arrayConfig"] = original["array_config"] | ||
} | ||
transformed["fields"] = [1]interface{}{ | ||
transformedField, | ||
} | ||
|
||
transformedIndexes = append(transformedIndexes, transformed) | ||
} | ||
transformedIndexConfig["indexes"] = transformedIndexes | ||
return transformedIndexConfig, nil | ||
} |
42 changes: 42 additions & 0 deletions
42
mmv1/templates/terraform/custom_flatten/firestore_field_index_config.go.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
func flatten<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d TerraformResourceData, config *transport_tpg.Config) (interface{}) { | ||
if v == nil { | ||
return v | ||
} | ||
indexConfig := v.(map[string]interface{}) | ||
|
||
usesAncestorConfig := false | ||
if indexConfig["usesAncestorConfig"] != nil { | ||
usesAncestorConfig = indexConfig["usesAncestorConfig"].(bool) | ||
} | ||
|
||
if usesAncestorConfig { | ||
// The intent when uses_ancestor_config is no config. | ||
return []interface{}{} | ||
} | ||
|
||
if indexConfig["indexes"] == nil { | ||
// No indexes, return an existing, but empty index config. | ||
return [1]interface{}{nil} | ||
} | ||
|
||
// For Single field indexes, we put the field configuration on the index to avoid forced nesting. | ||
l := indexConfig["indexes"].([]interface{}) | ||
transformed := make(map[string]interface{}) | ||
transformedIndexes := make([]interface{}, 0, len(l)) | ||
for _, raw := range l { | ||
original := raw.(map[string]interface{}) | ||
if len(original) < 1 { | ||
// Do not include empty json objects coming back from the api | ||
continue | ||
} | ||
fields := original["fields"].([]interface{}) | ||
sfi := fields[0].(map[string]interface{}) | ||
transformedIndexes = append(transformedIndexes, map[string]interface{}{ | ||
"query_scope": original["queryScope"], | ||
"order": sfi["order"], | ||
"array_config": sfi["arrayConfig"], | ||
}) | ||
} | ||
transformed["indexes"] = transformedIndexes | ||
return []interface{}{transformed} | ||
} |
29 changes: 29 additions & 0 deletions
29
mmv1/templates/terraform/custom_import/firestore_field.go.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
|
||
config := meta.(*transport_tpg.Config) | ||
|
||
// current import_formats can't import fields with forward slashes in their value | ||
if err := ParseImportId([]string{"(?P<name>.+)"}, d, config); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Re-populate split fields from the name. | ||
re := regexp.MustCompile("^projects/([^/]+)/databases/([^/]+)/collectionGroups/([^/]+)/fields/(.+)$") | ||
match := re.FindStringSubmatch(d.Get("name").(string)) | ||
if len(match) > 0 { | ||
if err := d.Set("project", match[1]); err != nil { | ||
return nil, fmt.Errorf("Error setting project: %s", err) | ||
} | ||
if err := d.Set("database", match[2]); err != nil { | ||
return nil, fmt.Errorf("Error setting database: %s", err) | ||
} | ||
if err := d.Set("collection", match[3]); err != nil { | ||
return nil, fmt.Errorf("Error setting collection: %s", err) | ||
} | ||
if err := d.Set("field", match[4]); err != nil { | ||
return nil, fmt.Errorf("Error setting field: %s", err) | ||
} | ||
} else { | ||
return nil, fmt.Errorf("import did not match the regex ^projects/([^/]+)/databases/([^/]+)/collectionGroups/([^/]+)/fields/(.+)$") | ||
} | ||
|
||
return []*schema.ResourceData{d}, nil |
Oops, something went wrong.