diff --git a/products/firestore/api.yaml b/products/firestore/api.yaml index 9a6afefdeb2b..1f0e4c21fd8c 100644 --- a/products/firestore/api.yaml +++ b/products/firestore/api.yaml @@ -19,23 +19,6 @@ versions: base_url: https://firestore.googleapis.com/v1/ scopes: - https://www.googleapis.com/auth/cloud-platform -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' objects: - !ruby/object:Api::Resource name: 'Index' @@ -48,6 +31,23 @@ objects: 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.indexes' + 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' properties: - !ruby/object:Api::Type::String name: name @@ -108,3 +108,61 @@ objects: be specified. values: - :CONTAINS + - !ruby/object:Api::Resource + name: 'Document' + base_url: projects/{{project}}/databases/{{database}}/documents/{{collection}} + create_url: projects/{{project}}/databases/{{database}}/documents/{{collection}}?documentId={{document_id}} + update_verb: :PATCH + self_link: '{{name}}' + description: | + In Cloud Firestore, the unit of storage is the document. A document is a lightweight record + that contains fields, which map to values. Each document is identified by a name. + references: !ruby/object:Api::Resource::ReferenceLinks + guides: + 'Official Documentation': 'https://cloud.google.com/firestore/docs/manage-data/add-data' + api: 'https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.documents' + parameters: + - !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 collection ID, relative to database. For example: chatrooms or chatrooms/my-document/private-messages. + required: true + url_param_only: true + - !ruby/object:Api::Type::String + name: 'documentId' + description: | + The client-assigned document ID to use for this document during creation. + required: true + url_param_only: true + properties: + - !ruby/object:Api::Type::String + name: name + output: true + description: | + A server defined name for this index. Format: + `projects/{{project_id}}/databases/{{database_id}}/documents/{{path}}/{{document_id}}` + - !ruby/object:Api::Type::String + name: path + output: true + description: | + A relative path to the collection this document exists within + - !ruby/object:Api::Type::String + # This is a string instead of a NestedObject because fields can be deeply nested + name: fields + required: true + description: | + The document's [fields](https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.documents) formated as a json string. + - !ruby/object:Api::Type::Time + name: 'createTime' + description: 'Creation timestamp in RFC3339 format.' + output: true + - !ruby/object:Api::Type::Time + name: 'updateTime' + description: 'Last update timestamp in RFC3339 format.' + output: true diff --git a/products/firestore/terraform.yaml b/products/firestore/terraform.yaml index 832ebbc655b3..171fa8df618c 100644 --- a/products/firestore/terraform.yaml +++ b/products/firestore/terraform.yaml @@ -50,6 +50,36 @@ overrides: !ruby/object:Overrides::ResourceOverrides ignore_read: true collection: !ruby/object:Overrides::Terraform::PropertyOverride ignore_read: true + Document: !ruby/object:Overrides::Terraform::ResourceOverride + import_format: ["{{name}}"] + docs: !ruby/object:Provider::Terraform::Docs + warning: | + This resource creates a Firestore Document on a project that already has + Firestore enabled. If you haven't already enabled it, you can create a + `google_app_engine_application` resource with `database_type` set to + `"CLOUD_FIRESTORE"` to do so. Your Firestore location will be the same as + the App Engine location specified. + examples: + - !ruby/object:Provider::Terraform::Examples + name: "firestore_document_basic" + primary_resource_id: "mydoc" + test_env_vars: + project_id: :FIRESTORE_PROJECT_NAME + - !ruby/object:Provider::Terraform::Examples + name: "firestore_document_nested_document" + primary_resource_id: "mydoc" + test_env_vars: + project_id: :FIRESTORE_PROJECT_NAME + custom_code: !ruby/object:Provider::Terraform::CustomCode + custom_import: templates/terraform/custom_import/firestore_document.go.erb + decoder: templates/terraform/decoders/firestore_document.go.erb + properties: + fields: !ruby/object:Overrides::Terraform::PropertyOverride + custom_expand: 'templates/terraform/custom_expand/json_schema.erb' + custom_flatten: 'templates/terraform/custom_flatten/json_schema.erb' + state_func: 'func(v interface{}) string { s, _ := structure.NormalizeJsonString(v); return s }' + validation: !ruby/object:Provider::Terraform::Validation + function: 'validation.StringIsJSON' # This is for copying files over files: !ruby/object:Provider::Config::Files diff --git a/templates/terraform/custom_import/firestore_document.go.erb b/templates/terraform/custom_import/firestore_document.go.erb new file mode 100644 index 000000000000..4a091ad89ff3 --- /dev/null +++ b/templates/terraform/custom_import/firestore_document.go.erb @@ -0,0 +1,28 @@ + + config := meta.(*Config) + + // current import_formats can't import fields with forward slashes in their value + if err := parseImportId([]string{"(?P.+)"}, d, config); err != nil { + return nil, err + } + + re := regexp.MustCompile("^projects/([^/]+)/databases/([^/]+)/documents/(.+)/([^/]+)$") + 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 project: %s", err) + } + if err := d.Set("collection", match[3]); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("document_id", match[4]); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + } else { + return nil, fmt.Errorf("import did not match the regex ^projects/([^/]+)/databases/([^/]+)/documents/(.+)/([^/]+)$") + } + + return []*schema.ResourceData{d}, nil diff --git a/templates/terraform/decoders/firestore_document.go.erb b/templates/terraform/decoders/firestore_document.go.erb new file mode 100644 index 000000000000..1ad7637b672c --- /dev/null +++ b/templates/terraform/decoders/firestore_document.go.erb @@ -0,0 +1,23 @@ +<%# The license inside this block applies to this file. + # Copyright 2020 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. +-%> +// We use this decoder to add the path field +if name, ok := res["name"]; ok { + re := regexp.MustCompile("^projects/[^/]+/databases/[^/]+/documents/(.+)$") + match := re.FindStringSubmatch(name.(string)) + if len(match) > 0{ + res["path"] = match[1] + } +} +return res, nil diff --git a/templates/terraform/examples/firestore_document_basic.tf.erb b/templates/terraform/examples/firestore_document_basic.tf.erb new file mode 100644 index 000000000000..842e935b931d --- /dev/null +++ b/templates/terraform/examples/firestore_document_basic.tf.erb @@ -0,0 +1,6 @@ +resource "google_firestore_document" "<%= ctx[:primary_resource_id] %>" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + collection = "somenewcollection" + document_id = "my-doc-%{random_suffix}" + fields = "{\"something\":{\"mapValue\":{\"fields\":{\"akey\":{\"stringValue\":\"avalue\"}}}}}" +} diff --git a/templates/terraform/examples/firestore_document_nested_document.tf.erb b/templates/terraform/examples/firestore_document_nested_document.tf.erb new file mode 100644 index 000000000000..a2ada694b629 --- /dev/null +++ b/templates/terraform/examples/firestore_document_nested_document.tf.erb @@ -0,0 +1,20 @@ +resource "google_firestore_document" "<%= ctx[:primary_resource_id] %>" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + collection = "somenewcollection" + document_id = "my-doc-%{random_suffix}" + fields = "{\"something\":{\"mapValue\":{\"fields\":{\"akey\":{\"stringValue\":\"avalue\"}}}}}" +} + +resource "google_firestore_document" "sub_document" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + collection = "${google_firestore_document.<%= ctx[:primary_resource_id] %>.path}/subdocs" + document_id = "bitcoinkey" + fields = "{\"something\":{\"mapValue\":{\"fields\":{\"ayo\":{\"stringValue\":\"val2\"}}}}}" +} + +resource "google_firestore_document" "sub_sub_document" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + collection = "${google_firestore_document.sub_document.path}/subsubdocs" + document_id = "asecret" + fields = "{\"something\":{\"mapValue\":{\"fields\":{\"secret\":{\"stringValue\":\"hithere\"}}}}}" +} diff --git a/third_party/terraform/tests/resource_firestore_document_test.go b/third_party/terraform/tests/resource_firestore_document_test.go new file mode 100644 index 000000000000..98483cad2a9e --- /dev/null +++ b/third_party/terraform/tests/resource_firestore_document_test.go @@ -0,0 +1,62 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccFirestoreDocument_update(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("tf-test-%d", randInt(t)) + project := getTestFirestoreProjectFromEnv(t) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccFirestoreDocument_update(project, name), + }, + { + ResourceName: "google_firestore_document.instance", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccFirestoreDocument_update2(project, name), + }, + { + ResourceName: "google_firestore_document.instance", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccFirestoreDocument_update(project, name string) string { + return fmt.Sprintf(` +resource "google_firestore_document" "instance" { + project = "%s" + database = "(default)" + collection = "somenewcollection" + document_id = "%s" + fields = "{\"something\":{\"mapValue\":{\"fields\":{\"yo\":{\"stringValue\":\"val1\"}}}}}" +} +`, project, name) +} + +func testAccFirestoreDocument_update2(project, name string) string { + return fmt.Sprintf(` +resource "google_firestore_document" "instance" { + project = "%s" + database = "(default)" + collection = "somenewcollection" + document_id = "%s" + fields = "{\"something\":{\"mapValue\":{\"fields\":{\"yo\":{\"stringValue\":\"val2\"}}}}}" +} +`, project, name) +}