From 6b4fe80560aac90b0e8e450899d637daf3fee288 Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Fri, 10 Jan 2025 21:34:18 +0000 Subject: [PATCH] CodeRepositoryIndex cascade deletion (#12601) Co-authored-by: Nick Elliot [upstream:22008e516c62bcad85509e892707a4a01e36e6ea] Signed-off-by: Modular Magician --- .changelog/12601.txt | 3 + .../resource_gemini_code_repository_index.go | 23 +++ ...ce_gemini_code_repository_index_sweeper.go | 143 ------------------ ...ource_gemini_code_repository_index_test.go | 131 ++++++++++++++++ ...gemini_code_repository_index.html.markdown | 1 + 5 files changed, 158 insertions(+), 143 deletions(-) create mode 100644 .changelog/12601.txt delete mode 100644 google-beta/services/gemini/resource_gemini_code_repository_index_sweeper.go diff --git a/.changelog/12601.txt b/.changelog/12601.txt new file mode 100644 index 0000000000..676f16121e --- /dev/null +++ b/.changelog/12601.txt @@ -0,0 +1,3 @@ +```release-note: enhancement +gemini: added `force_destroy` field to resource `google_code_repository_index`, enabling deletion of the resource even when it has dependent RepositoryGroups +``` \ No newline at end of file diff --git a/google-beta/services/gemini/resource_gemini_code_repository_index.go b/google-beta/services/gemini/resource_gemini_code_repository_index.go index 80a6a6dd3c..5801a4d63a 100644 --- a/google-beta/services/gemini/resource_gemini_code_repository_index.go +++ b/google-beta/services/gemini/resource_gemini_code_repository_index.go @@ -122,6 +122,12 @@ SUSPENDED`, Computed: true, Description: `Output only. Update time stamp.`, }, + "force_destroy": { + Type: schema.TypeBool, + Optional: true, + Description: `If set to true, will allow deletion of the CodeRepositoryIndex even if there are existing RepositoryGroups for the resource. These RepositoryGroups will also be deleted.`, + Default: false, + }, "project": { Type: schema.TypeString, Optional: true, @@ -271,6 +277,12 @@ func resourceGeminiCodeRepositoryIndexRead(d *schema.ResourceData, meta interfac return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("GeminiCodeRepositoryIndex %q", d.Id())) } + // Explicitly set virtual fields to default values if unset + if _, ok := d.GetOkExists("force_destroy"); !ok { + if err := d.Set("force_destroy", false); err != nil { + return fmt.Errorf("Error setting force_destroy: %s", err) + } + } if err := d.Set("project", project); err != nil { return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) } @@ -424,6 +436,12 @@ func resourceGeminiCodeRepositoryIndexDelete(d *schema.ResourceData, meta interf } headers := make(http.Header) + obj = make(map[string]interface{}) + if v, ok := d.GetOk("force_destroy"); ok { + if v == true { + obj["force"] = true + } + } log.Printf("[DEBUG] Deleting CodeRepositoryIndex %q", d.Id()) res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ @@ -470,6 +488,11 @@ func resourceGeminiCodeRepositoryIndexImport(d *schema.ResourceData, meta interf } d.SetId(id) + // Explicitly set virtual fields to default values on import + if err := d.Set("force_destroy", false); err != nil { + return nil, fmt.Errorf("Error setting force_destroy: %s", err) + } + return []*schema.ResourceData{d}, nil } diff --git a/google-beta/services/gemini/resource_gemini_code_repository_index_sweeper.go b/google-beta/services/gemini/resource_gemini_code_repository_index_sweeper.go deleted file mode 100644 index 23cdc685a5..0000000000 --- a/google-beta/services/gemini/resource_gemini_code_repository_index_sweeper.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -// ---------------------------------------------------------------------------- -// -// *** AUTO GENERATED CODE *** Type: MMv1 *** -// -// ---------------------------------------------------------------------------- -// -// This file is automatically generated by Magic Modules and manual -// changes will be clobbered when the file is regenerated. -// -// Please read more about how to change this file in -// .github/CONTRIBUTING.md. -// -// ---------------------------------------------------------------------------- - -package gemini - -import ( - "context" - "log" - "strings" - "testing" - - "github.com/hashicorp/terraform-provider-google-beta/google-beta/envvar" - "github.com/hashicorp/terraform-provider-google-beta/google-beta/sweeper" - "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" - transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport" -) - -func init() { - sweeper.AddTestSweepers("GeminiCodeRepositoryIndex", testSweepGeminiCodeRepositoryIndex) -} - -// At the time of writing, the CI only passes us-central1 as the region -func testSweepGeminiCodeRepositoryIndex(region string) error { - resourceName := "GeminiCodeRepositoryIndex" - log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) - - config, err := sweeper.SharedConfigForRegion(region) - if err != nil { - log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) - return err - } - - err = config.LoadAndValidate(context.Background()) - if err != nil { - log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) - return err - } - - t := &testing.T{} - billingId := envvar.GetTestBillingAccountFromEnv(t) - - // Setup variables to replace in list template - d := &tpgresource.ResourceDataMock{ - FieldsInSchema: map[string]interface{}{ - "project": config.Project, - "region": region, - "location": region, - "zone": "-", - "billing_account": billingId, - }, - } - - listTemplate := strings.Split("https://cloudaicompanion.googleapis.com/v1/projects/{{project}}/locations/{{location}}/codeRepositoryIndexes", "?")[0] - listUrl, err := tpgresource.ReplaceVars(d, config, listTemplate) - if err != nil { - log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) - return nil - } - - res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ - Config: config, - Method: "GET", - Project: config.Project, - RawURL: listUrl, - UserAgent: config.UserAgent, - }) - if err != nil { - log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) - return nil - } - - resourceList, ok := res["codeRepositoryIndices"] - if !ok { - log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") - return nil - } - - rl := resourceList.([]interface{}) - - log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) - // Keep count of items that aren't sweepable for logging. - nonPrefixCount := 0 - for _, ri := range rl { - obj := ri.(map[string]interface{}) - var name string - // Id detected in the delete URL, attempt to use id. - if obj["id"] != nil { - name = tpgresource.GetResourceNameFromSelfLink(obj["id"].(string)) - } else if obj["name"] != nil { - name = tpgresource.GetResourceNameFromSelfLink(obj["name"].(string)) - } else { - log.Printf("[INFO][SWEEPER_LOG] %s resource name and id were nil", resourceName) - return nil - } - // Skip resources that shouldn't be sweeped - if !sweeper.IsSweepableTestResource(name) { - nonPrefixCount++ - continue - } - - deleteTemplate := "https://cloudaicompanion.googleapis.com/v1/projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}" - deleteUrl, err := tpgresource.ReplaceVars(d, config, deleteTemplate) - if err != nil { - log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) - return nil - } - deleteUrl = deleteUrl + name - - // Don't wait on operations as we may have a lot to delete - _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ - Config: config, - Method: "DELETE", - Project: config.Project, - RawURL: deleteUrl, - UserAgent: config.UserAgent, - }) - if err != nil { - log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) - } else { - log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) - } - } - - if nonPrefixCount > 0 { - log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) - } - - return nil -} diff --git a/google-beta/services/gemini/resource_gemini_code_repository_index_test.go b/google-beta/services/gemini/resource_gemini_code_repository_index_test.go index 672933f855..e58cebfbac 100644 --- a/google-beta/services/gemini/resource_gemini_code_repository_index_test.go +++ b/google-beta/services/gemini/resource_gemini_code_repository_index_test.go @@ -3,6 +3,7 @@ package gemini_test import ( + "fmt" "os" "testing" @@ -45,6 +46,136 @@ func TestAccGeminiCodeRepositoryIndex_update(t *testing.T) { }) } +// TestAccGeminiCodeRepositoryIndex_delete checks if there is no error in deleting CRI along with children resource +// note: this is an example of a bad usage, where RGs refer to the CRI using a string id, not a reference, as they +// will be force-removed upon CRI deletion, because the CRI provider uses --force option by default +// The plan after the _delete function should not be empty due to the child resource in plan +func TestAccGeminiCodeRepositoryIndex_delete(t *testing.T) { + bootstrappedKMS := acctest.BootstrapKMSKeyInLocation(t, "us-central1") + randomSuffix := acctest.RandString(t, 10) + context := map[string]interface{}{ + "random_suffix": randomSuffix, + "project_id": os.Getenv("GOOGLE_PROJECT"), + "kms_key": bootstrappedKMS.CryptoKey.Name, + "cri_id": fmt.Sprintf("tf-test-cri-index-delete-example-%s", randomSuffix), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccGeminiCodeRepositoryIndex_withChildren_basic(context), + }, + { + ResourceName: "google_gemini_code_repository_index.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"code_repository_index_id", "labels", "location", "terraform_labels", "force_destroy"}, + }, + { + Config: testAccGeminiCodeRepositoryIndex_withChildren_delete(context), + ExpectNonEmptyPlan: true, + PlanOnly: true, + }, + }, + }) +} + +func testAccGeminiCodeRepositoryIndex_withChildren_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_code_repository_index" "example" { + provider = google-beta + labels = {"ccfe_debug_note": "terraform_e2e_should_be_deleted"} + location = "us-central1" + code_repository_index_id = "%{cri_id}" + force_destroy = true +} + +resource "google_gemini_repository_group" "example" { + provider = google-beta + location = "us-central1" + code_repository_index = "%{cri_id}" + repository_group_id = "tf-test-rg-repository-group-id-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} + depends_on = [ + google_gemini_code_repository_index.example + ] +} + +resource "google_developer_connect_git_repository_link" "conn" { + provider = google-beta + git_repository_link_id = "tf-test-repository-conn-delete" + parent_connection = google_developer_connect_connection.github_conn.connection_id + clone_uri = "https://github.com/CC-R-github-robot/tf-test.git" + location = "us-central1" + annotations = {} +} + +resource "google_developer_connect_connection" "github_conn" { + provider = google-beta + location = "us-central1" + connection_id = "tf-test-cloudaicompanion-delete-%{random_suffix}" + disabled = false + + github_config { + github_app = "DEVELOPER_CONNECT" + app_installation_id = 54180648 + + authorizer_credential { + oauth_token_secret_version = "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1" + } + } +} +`, context) +} + +// Removed depends_on to not break plan test +func testAccGeminiCodeRepositoryIndex_withChildren_delete(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_repository_group" "example" { + provider = google-beta + location = "us-central1" + code_repository_index = "%{cri_id}" + repository_group_id = "tf-test-rg-repository-group-id-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_developer_connect_git_repository_link" "conn" { + provider = google-beta + git_repository_link_id = "tf-test-repository-conn-delete" + parent_connection = google_developer_connect_connection.github_conn.connection_id + clone_uri = "https://github.com/CC-R-github-robot/tf-test.git" + location = "us-central1" + annotations = {} +} + +resource "google_developer_connect_connection" "github_conn" { + provider = google-beta + location = "us-central1" + connection_id = "tf-test-cloudaicompanion-delete-%{random_suffix}" + disabled = false + + github_config { + github_app = "DEVELOPER_CONNECT" + app_installation_id = 54180648 + + authorizer_credential { + oauth_token_secret_version = "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1" + } + } +} +`, context) +} + func testAccGeminiCodeRepositoryIndex_basic(context map[string]interface{}) string { return acctest.Nprintf(` resource "google_gemini_code_repository_index" "example" { diff --git a/website/docs/r/gemini_code_repository_index.html.markdown b/website/docs/r/gemini_code_repository_index.html.markdown index ed9148e4f2..309c9cd722 100644 --- a/website/docs/r/gemini_code_repository_index.html.markdown +++ b/website/docs/r/gemini_code_repository_index.html.markdown @@ -68,6 +68,7 @@ The following arguments are supported: * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. +* `force_destroy` - (Optional) If set to true, will allow deletion of the CodeRepositoryIndex even if there are existing RepositoryGroups for the resource. These RepositoryGroups will also be deleted. ## Attributes Reference