From d87a9f17aa160e540f700df61e1b09f323bdcaa0 Mon Sep 17 00:00:00 2001 From: Harshita <[harshiita@google.com](mailto:harshiita@google.com)> Date: Wed, 4 Mar 2026 11:47:58 +0000 Subject: [PATCH] Add DataTable Resource --- mmv1/products/chronicle/DataTable.yaml | 221 ++++++++++++++++++ mmv1/products/chronicle/product.yaml | 2 +- .../chronicle_data_table_basic.tf.tmpl | 47 ++++ ...le_data_table_with_optional_fields.tf.tmpl | 60 +++++ .../pre_delete/chronicle_data_table.go.tmpl | 4 + .../resource_chronicle_data_table_test.go | 136 +++++++++++ 6 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 mmv1/products/chronicle/DataTable.yaml create mode 100644 mmv1/templates/terraform/examples/chronicle_data_table_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/examples/chronicle_data_table_with_optional_fields.tf.tmpl create mode 100644 mmv1/templates/terraform/pre_delete/chronicle_data_table.go.tmpl create mode 100644 mmv1/third_party/terraform/services/chronicle/resource_chronicle_data_table_test.go diff --git a/mmv1/products/chronicle/DataTable.yaml b/mmv1/products/chronicle/DataTable.yaml new file mode 100644 index 000000000000..e622058d9b97 --- /dev/null +++ b/mmv1/products/chronicle/DataTable.yaml @@ -0,0 +1,221 @@ +# Copyright 2026 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. + +--- +name: DataTable +description: Represents a Chronicle Data Table, a multicolumn structure used to ingest your own data into Google SecOps. + +base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataTables +create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataTables?dataTableId={{data_table_id}} +self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataTables/{{data_table_id}} +id_format: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataTables/{{data_table_id}} +import_format: + - projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataTables/{{data_table_id}} + +update_verb: PATCH +update_mask: true +min_version: 'beta' +references: + guides: + 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' + api: 'https://cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.dataTables' +examples: + - name: 'chronicle_data_table_basic' + config_path: 'templates/terraform/examples/chronicle_data_table_basic.tf.tmpl' + primary_resource_id: 'example' + min_version: 'beta' + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + vars: + data_table_id: 'terraform_test' + - name: 'chronicle_data_table_with_optional_fields' + config_path: 'templates/terraform/examples/chronicle_data_table_with_optional_fields.tf.tmpl' + primary_resource_id: 'example_dt' + min_version: 'beta' + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + vars: + data_table_id: 'tf_test_full' + data_access_scope_id: 'tf-scope-opt' +autogen_status: RGF0YVRhYmxl + +custom_code: + pre_delete: 'templates/terraform/pre_delete/chronicle_data_table.go.tmpl' + +virtual_fields: + - name: 'deletion_policy' + description: | + The policy governing the deletion of the data table. + If set to `FORCE`, allows the deletion of the data table even if it contains rows. + If set to `DEFAULT`,or if the field is omitted, the data table must be empty before it can be deleted. + Possible values: DEFAULT, FORCE + type: String + default_value: "DEFAULT" + +parameters: + - name: location + type: String + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + required: true + - name: instance + type: String + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + required: true + - name: dataTableId + type: String + description: |- + The ID to use for the data table. This is also the display name for + the data table. It must satisfy the following requirements: + - Starts with letter. + - Contains only letters, numbers and underscore. + - Must be unique and has length < 256. + immutable: true + url_param_only: true + required: true +properties: + - name: approximateRowCount + type: Integer + description: The count of rows in the data table. + output: true + - name: columnInfo + type: Array + description: Details of all the columns in the table + immutable: true + item_type: + type: NestedObject + properties: + - name: columnIndex + type: Integer + description: Column Index. 0,1,2... + required: true + immutable: true + - name: columnType + type: Enum + description: |- + Column type can be STRING, CIDR (Ex- 10.1.1.0/24), REGEX + Possible values: + STRING + REGEX + CIDR + NUMBER + enum_values: + - STRING + - REGEX + - CIDR + - NUMBER + immutable: true + - name: keyColumn + type: Boolean + description: |- + Whether to include this column in the calculation of the row ID. + If no columns have key_column = true, all columns will be included in the + calculation of the row ID. + immutable: true + - name: mappedColumnPath + type: String + description: Entity proto field path that the column is mapped to + immutable: true + - name: originalColumn + type: String + description: |- + Original column name of the Data Table (present in the CSV header in case + of creation of data tables using file uploads). It must satisfy the + following requirements: + - Starts with letter. + - Contains only letters, numbers and underscore. + - Must be unique and has length < 256 + required: true + immutable: true + - name: repeatedValues + type: Boolean + description: Whether the column is a repeated values column. + immutable: true + - name: createTime + type: String + description: Table create time + output: true + - name: dataTableUuid + type: String + description: Data table unique id + output: true + - name: description + type: String + description: A user-provided description of the data table. + required: true + - name: displayName + type: String + description: The unique display name of the data table. + output: true + - name: name + type: String + description: |- + Identifier. The resource name of the data table + Format: + "{project}/locations/{region}/instances/{instance}/dataTables/{data_table}" + output: true + - name: rowTimeToLive + type: String + description: User-provided TTL of the data table. + - name: rowTimeToLiveUpdateTime + type: String + description: Last update time of the TTL of the data table. + output: true + - name: ruleAssociationsCount + type: Integer + description: The count of rules using the data table. + output: true + - name: rules + type: Array + description: |- + The resource names for the associated Rules that use this + data table. Format: + projects/{project}/locations/{location}/instances/{instance}/rules/{rule}. + {rule} here refers to the rule id. + output: true + item_type: + type: String + - name: scopeInfo + type: NestedObject + diff_suppress_func: 'tpgresource.ProjectNumberDiffSuppress' + description: DataTableScopeInfo specifies the scope info of the data table. + properties: + - name: dataAccessScopes + type: Array + description: |- + Contains the list of scope names of the data table. If the list is empty, + the data table is treated as unscoped. The scope names should be + full resource names and should be of the format: + "projects/{project}/locations/{location}/instances/{instance}/dataAccessScopes/{scope_name}" + required: true + item_type: + type: String + - name: updateSource + type: Enum + description: |2- + Possible values: + USER + RULE + SEARCH + enum_values: + - USER + - RULE + - SEARCH + output: true + - name: updateTime + type: String + description: Table update time + output: true diff --git a/mmv1/products/chronicle/product.yaml b/mmv1/products/chronicle/product.yaml index 8b9e0adb109f..8fa225b0c5f5 100644 --- a/mmv1/products/chronicle/product.yaml +++ b/mmv1/products/chronicle/product.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 Google Inc. +# Copyright 2026 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 diff --git a/mmv1/templates/terraform/examples/chronicle_data_table_basic.tf.tmpl b/mmv1/templates/terraform/examples/chronicle_data_table_basic.tf.tmpl new file mode 100644 index 000000000000..49c5e5623ca6 --- /dev/null +++ b/mmv1/templates/terraform/examples/chronicle_data_table_basic.tf.tmpl @@ -0,0 +1,47 @@ +resource "google_chronicle_data_table" "example" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + data_table_id = "{{index $.Vars "data_table_id"}}" + description = "sample desc" + column_info { + column_index = 0 + original_column = "username" + column_type = "STRING" + } + column_info { + column_index = 1 + original_column = "ip_address" + column_type = "CIDR" + } +} + +output "data_table_name" { + description = "The resource name of the data table." + value = google_chronicle_data_table.example.name +} + +output "data_table_id" { + description = "The user-provided ID of the data table." + value = google_chronicle_data_table.example.data_table_id +} + +output "data_table_uuid" { + description = "The system-generated UUID of the data table." + value = google_chronicle_data_table.example.data_table_uuid +} + +output "data_table_description" { + description = "The description of the data table." + value = google_chronicle_data_table.example.description +} + +output "data_table_create_time" { + description = "The creation time of the data table." + value = google_chronicle_data_table.example.create_time +} + +output "data_table_ttl" { + description = "The row time to live for the data table." + value = google_chronicle_data_table.example.row_time_to_live +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/chronicle_data_table_with_optional_fields.tf.tmpl b/mmv1/templates/terraform/examples/chronicle_data_table_with_optional_fields.tf.tmpl new file mode 100644 index 000000000000..fd37b2ac476f --- /dev/null +++ b/mmv1/templates/terraform/examples/chronicle_data_table_with_optional_fields.tf.tmpl @@ -0,0 +1,60 @@ +resource "google_chronicle_data_access_scope" "test_scope_allow_everyone" { +provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + data_access_scope_id = "{{index $.Vars "data_access_scope_id"}}" + description = "scope-description" + allowed_data_access_labels { + log_type = "GCP_CLOUDAUDIT" + } +} + +resource "google_chronicle_data_table" "example_dt" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + data_table_id = "{{index $.Vars "data_table_id"}}" + description = "Comprehensive test table with all teh fields" + row_time_to_live = "48h" + + column_info { + column_index = 0 + original_column = "username" + key_column = true + mapped_column_path = "entity.user.userid" + repeated_values = false + } + column_info { + column_index = 1 + original_column = "ip_address" + column_type = "CIDR" + key_column = false + repeated_values = false + } + + scope_info { + data_access_scopes = [google_chronicle_data_access_scope.test_scope_allow_everyone.name] + } + depends_on = [google_chronicle_data_access_scope.test_scope_allow_everyone] +} + +output "data_table_name" { + description = "The resource name of the created data table." + value = google_chronicle_data_table.example_dt.name +} + +output "data_table_id" { + description = "The ID of the created data table." + value = google_chronicle_data_table.example_dt.id +} + +output "data_table_create_time" { + description = "The creation time of the data table." + value = google_chronicle_data_table.example_dt.create_time +} + +output "data_table_column_info" { + description = "The column info of the data table." + value = google_chronicle_data_table.example_dt.column_info +} + diff --git a/mmv1/templates/terraform/pre_delete/chronicle_data_table.go.tmpl b/mmv1/templates/terraform/pre_delete/chronicle_data_table.go.tmpl new file mode 100644 index 000000000000..77dd6e425b08 --- /dev/null +++ b/mmv1/templates/terraform/pre_delete/chronicle_data_table.go.tmpl @@ -0,0 +1,4 @@ +// Forcefully delete data table even if there are any rows associated. +if deletionPolicy := d.Get("deletion_policy"); deletionPolicy == "FORCE" { + url = url + "?force=true" +} \ No newline at end of file diff --git a/mmv1/third_party/terraform/services/chronicle/resource_chronicle_data_table_test.go b/mmv1/third_party/terraform/services/chronicle/resource_chronicle_data_table_test.go new file mode 100644 index 000000000000..b9aea05bb912 --- /dev/null +++ b/mmv1/third_party/terraform/services/chronicle/resource_chronicle_data_table_test.go @@ -0,0 +1,136 @@ +package chronicle_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccChronicleDataTable_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "chronicle_id": envvar.GetTestChronicleInstanceIdFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + "description_initial": "Initial description", + "ttl_initial": "24h", + "username_initial": "username", // Initial column name + "description_updated": "Updated description", + "ttl_updated": "48h", + "username_updated": "updated_username", // Updated column name for immutability test + } + resourceName := "google_chronicle_data_table.example" + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Basic creation + { + Config: testAccChronicleDataTable_Config(context, context["description_initial"].(string), context["ttl_initial"].(string), context["username_initial"].(string)), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", context["description_initial"].(string)), + resource.TestCheckResourceAttr(resourceName, "row_time_to_live", context["ttl_initial"].(string)), + resource.TestCheckResourceAttr(resourceName, "column_info.0.original_column", context["username_initial"].(string)), + ), + }, + // Step 2: Import check + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"data_table_id", "instance", "location"}, + }, + // Step 3: Update mutable fields (description, row_time_to_live) + { + Config: testAccChronicleDataTable_Config(context, context["description_updated"].(string), context["ttl_updated"].(string), context["username_initial"].(string)), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", context["description_updated"].(string)), + resource.TestCheckResourceAttr(resourceName, "row_time_to_live", context["ttl_updated"].(string)), + resource.TestCheckResourceAttr(resourceName, "column_info.0.original_column", context["username_initial"].(string)), // Should not have changed + ), + }, + // Step 4: Import check after mutable update + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"data_table_id", "instance", "location"}, + }, + // Step 5: Attempt to update an immutable field (column_info.0.original_column) + { + Config: testAccChronicleDataTable_Config(context, context["description_updated"].(string), context["ttl_updated"].(string), context["username_updated"].(string)), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "column_info.0.original_column", context["username_updated"].(string)), + resource.TestCheckResourceAttr(resourceName, "description", context["description_updated"].(string)), + resource.TestCheckResourceAttr(resourceName, "row_time_to_live", context["ttl_updated"].(string)), + ), + }, + // Step 6: Final import check after immutable change (Force New) + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"data_table_id", "instance", "location"}, + }, + }, + }) +} + +func testAccChronicleDataTable_Config(context map[string]interface{}, description, ttl, username string) string { + return acctest.Nprintf(fmt.Sprintf(` +resource "google_chronicle_data_access_scope" "test_scope_update" { + provider = google-beta + location = "us" + instance = "%%{chronicle_id}" + data_access_scope_id = "tf-scope-update-%%{random_suffix}" + description = "Scope for update test" + allowed_data_access_labels { + log_type = "GCP_CLOUDAUDIT" + } +} + +resource "google_chronicle_data_table" "example" { + provider = google-beta + location = "us" + instance = "%%{chronicle_id}" + data_table_id = "tf_test_update_%%{random_suffix}" + description = "%s" + row_time_to_live = "%s" + + column_info { + column_index = 0 + original_column = "%s" + column_type = "STRING" + } + column_info { + column_index = 1 + original_column = "ip_address" + column_type = "CIDR" + } + scope_info { + data_access_scopes = [google_chronicle_data_access_scope.test_scope_update.name] + } + depends_on = [google_chronicle_data_access_scope.test_scope_update] +} + +output "data_table_name" { + value = google_chronicle_data_table.example.name +} +`, description, ttl, username), context) +}